<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>OVA Archives - LucD notes</title>
	<atom:link href="https://www.lucd.info/category/cloud-init/ova/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.lucd.info/category/cloud-init/ova/</link>
	<description>My PowerShell ramblings</description>
	<lastBuildDate>Mon, 09 Dec 2019 09:10:19 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9</generator>

<image>
	<url>https://www.lucd.info/wp-content/uploads/2018/12/cropped-120px-Tibetan_Dharmacakra-32x32.png</url>
	<title>OVA Archives - LucD notes</title>
	<link>https://www.lucd.info/category/cloud-init/ova/</link>
	<width>32</width>
	<height>32</height>
</image> 
<atom:link rel="hub" href="https://pubsubhubbub.appspot.com"/><atom:link rel="hub" href="https://pubsubhubbub.superfeedr.com"/><atom:link rel="hub" href="https://websubhub.com/hub"/>	<item>
		<title>Cloud-init &#8211; Part 3 &#8211; Photon OS</title>
		<link>https://www.lucd.info/2019/12/08/cloud-init-part-3-photon-os/</link>
					<comments>https://www.lucd.info/2019/12/08/cloud-init-part-3-photon-os/#comments</comments>
		
		<dc:creator><![CDATA[LucD]]></dc:creator>
		<pubDate>Sun, 08 Dec 2019 21:52:03 +0000</pubDate>
				<category><![CDATA[Cloud-init]]></category>
		<category><![CDATA[Deploy]]></category>
		<category><![CDATA[OVA]]></category>
		<category><![CDATA[Photon]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[powershellv6]]></category>
		<category><![CDATA[YAML]]></category>
		<category><![CDATA[cloud-init]]></category>
		<category><![CDATA[seedISO]]></category>
		<guid isPermaLink="false">http://www.lucd.info/?p=6695</guid>

					<description><![CDATA[In Part 1 and Part 2 of this series we used Ubuntu as [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>In <a rel="noreferrer noopener" aria-label="Part 1 (opens in a new tab)" href="https://www.lucd.info/2019/12/06/cloud-init-part-1-the-basics/" target="_blank">Part 1</a> and <a href="https://www.lucd.info/2019/12/07/cloud-init-part-2-advanced-ubuntu/">Part 2</a> of this series we used Ubuntu as the guest OS of our target instances. In Part 3 we will show how to use VMware&#8217;s <a rel="noreferrer noopener" aria-label="Photon OS (opens in a new tab)" href="https://vmware.github.io/photon/" target="_blank">Photon OS</a> as our guest OS.</p>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="693" height="295" src="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-3.png" alt="" class="wp-image-6784" srcset="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-3.png 693w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-3-300x128.png 300w" sizes="(max-width: 693px) 100vw, 693px" /></figure>



<p>The main reason to use Photon OS is that it is <strong>open-sourced</strong>, it has a <strong>small footprint</strong> and it is <strong>optimised for VMware vSphere</strong>.</p>



<span id="more-6695"></span>



<h2 class="wp-block-heading">Some Background</h2>



<p>Does this mean that <strong>Photon OS</strong> is the ideal guest OS for our &#8216;<em>cattle</em>&#8216; stations where we want to run our PowerShell scripts?</p>



<p>Not really, the major flaw with Photon OS, imho, is that the available packages are <strong>limited</strong> and rather <strong>infrequently maintained</strong>. </p>



<p>In fact, until recently the available <strong>PowerShell package</strong> was an outdated, <strong>not supported </strong>anymore version. But the worst part of the deal, it didn&#8217;t even allow to perform an <a rel="noreferrer noopener" aria-label="Install-Module (opens in a new tab)" href="https://docs.microsoft.com/en-us/powershell/module/powershellget/install-module?view=powershell-6" target="_blank">Install-Module</a>. Which, again imho, is a basic concept in PowerShell. And which didn&#8217;t allow me to install, for example, <a rel="noreferrer noopener" aria-label="VMware PowerCLI (opens in a new tab)" href="https://www.vmware.com/support/developer/PowerCLI/" target="_blank">VMware PowerCLI</a> on a Photon station.</p>



<p>But then recently this happened!</p>



<figure class="wp-block-image size-large"><img decoding="async" width="521" height="363" src="https://www.lucd.info/wp-content/uploads/2019/12/ps6-photon.png" alt="" class="wp-image-6788" srcset="https://www.lucd.info/wp-content/uploads/2019/12/ps6-photon.png 521w, https://www.lucd.info/wp-content/uploads/2019/12/ps6-photon-300x209.png 300w" sizes="(max-width: 521px) 100vw, 521px" /></figure>



<p>The available PowerShell v6 <a href="https://vmware.github.io/photon/assets/files/html/3.0/photon_admin/tdnf.html" target="_blank" rel="noopener noreferrer">TDNF</a> package was upgraded to a currently supported version, and more importantly, the <strong>Import-Module</strong> cmdlet&nbsp; worked!&nbsp;</p>
<p>Allowing us to install VMware PowerCLI on a Photon instance.</p>



<p>Is this the end of all the PowerShell woes on Photon OS?</p>



<p>Not really! The PowerShell version in this TDNF package will also run out of support in a not so far future and a PowerShell TDNF package upgrade is (still) not automated and somewhat of a black art. </p>



<p>But for now, let&#8217;s be happy with what we have and &#8216;cloud-init&#8217; our Photon &#8216;cattle&#8217; station!</p>



<h2 class="wp-block-heading">Issues and some Solutions</h2>



<h3 class="wp-block-heading">No UserData Property</h3>



<p>While the Ubuntu cloud image, as we saw in <a rel="noreferrer noopener" aria-label="Part 1 (opens in a new tab)" href="https://www.lucd.info/2019/12/06/cloud-init-part-1-the-basics/" target="_blank">Part 1</a> of the series, has the option to pass the <strong>user-data</strong> to cloud-init via an <strong>OVF property</strong>, this feature is unfortunately not available in the Photon OVA.</p>



<p>There are some solutions available.</p>



<ul class="wp-block-list"><li>Roll your own OVA image</li><li>Use the <a rel="noreferrer noopener" aria-label="NoCloud (opens in a new tab)" href="https://cloudinit.readthedocs.io/en/latest/topics/datasources/nocloud.html" target="_blank">NoCloud</a> option, which means creating an ISO containing the user-data and attaching this ISO to the instance before cloud-init runs.</li></ul>



<p>Since I prefer to use the &#8216;standard&#8217; OVA, I opt for creating such a seed ISO.</p>



<h3 class="wp-block-heading">The Root Password</h3>



<p>This is somewhat of a <a rel="noreferrer noopener" aria-label="chicken-and-the-egg (opens in a new tab)" href="https://en.wikipedia.org/wiki/Chicken_or_the_egg" target="_blank">chicken-and-the-egg</a> dilemma.</p>



<p>In the Photon OVA image, the root account is set to force a password change on first use. But unfortunately, this also disallows setting the root password via the user-data. See <a href="https://github.com/vmware/photon/issues/931">Issue#931</a> for more details.</p>



<p>Again, there are two options available.</p>



<ul class="wp-block-list"><li>Roll your own OVA image</li><li>Use a two-stage process, wherein the first stage we use <a rel="noreferrer noopener" aria-label="William Lam (opens in a new tab)" href="https://twitter.com/lamw" target="_blank">William Lam</a>&#8216;s  <a rel="noreferrer noopener" aria-label="Set-VMKeystrokes (opens in a new tab)" href="https://www.virtuallyghetto.com/2017/09/automating-vm-keystrokes-using-the-vsphere-api-powercli.html" target="_blank">Set-VMKeystrokes</a> function. Then in the second stage, we attach the seed ISO and start the instance. And thus kicking off the cloud-init process.</li></ul>



<p>And again, since I prefer to use the &#8216;standard&#8217; OVA, I opt for the Set-VMKeystrokes solution.</p>



<h2 class="wp-block-heading">Install-CloudInitVM Revisited</h2>



<pre class="lang:ps decode:true  ">function Install-CloudInitVM
{
  &lt;#
.SYNOPSIS
  Deploy a VM from an OVA file and use cloud-init for the configuration
  .DESCRIPTION
  This function will deploy an OVA file.
  The function transfer the user-data to the cloud-init process on the VM with
  one of the OVF properties.
.NOTES
  Author:  Luc Dekens
  Version:
  1.0 05/12/19  Initial release
  1.1 07/12/19  Added Photon deployment
                Added seed ISO option
.PARAMETER OvaFile
  Specifies the path to the OVA file
.PARAMETER VmName
  The displayname of the VM
.PARAMETER ClusterName
  The cluster onto which the VM shall be deployed
.PARAMETER DsName
  The datastore on which the VM shall be deployed
.PARAMETER PgName
  The portgroupname to which the VM shall be connected
.PARAMETER CloudConfig
  The path to the YAML file containing the user-data
.PARAMETER CloudConfigISO
  The ISO seed file, containing the meta-data and user-data
.PARAMETER Credential
  The credentials for a user in the VM's guest OS
.PARAMETER Photon
  Switch to indicate a Photon is deployed. This will add an
  additional step to reset the root password before starting
  the actual cloud-init based deployment
.EXAMPLE
  $sCloudInitVM = @{
    OvaFile = '.\bionic-server-cloudimg-amd64.ova'
    VmName = $vmName
    ClusterName = $clusterName
    DsName = $dsName
    PgName = $pgName
    CloudConfig = '.\user-data.yaml'
    Credential = $cred
  }
  Install-CloudInitVM @sCloudInitVM
.EXAMPLE
  $sCloudInitVM = @{
    OvaFile = '.\photon-hw13_uefi-3.0-9355405.ova'
    VmName = $vmName
    ClusterName = $clusterName
    DsName = $dsName
    PgName = $pgName
    CloudConfigISO = '.\seed.iso'
    Credential = $cred
    Photon = $true
  }
  Install-CloudInitVM @sCloudInitVM
#&gt;

  [cmdletbinding()]
  param(
    [string]$OvaFile,
    [string]$VmName,
    [string]$ClusterName,
    [string]$DsName,
    [string]$PgName,
    [Parameter(ParameterSetName = 'YAML')]
    [string]$CloudConfig,
    [Parameter(ParameterSetName = 'SeedISO')]
    [string]$CloudConfigISO,
    [PSCredential[]]$Credential,
    [Switch]$Photon
  )

  $waitJob = (Get-Command -Name .\Wait-Job.ps1 ).ScriptBlock
  if ($PSCmdlet.ParameterSetName -eq 'YAML')
  {
    $userData = Get-Content -Path $CloudConfig -Raw
  }

  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Starting deployment of $vmName"
  if ($Photon)
  {
    Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - This is a Photon deployment"
  }

  $start = Get-Date

  $vm = Get-VM -Name $vmName -ErrorAction SilentlyContinue -Verbose:$false
  if ($vm)
  {
    Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Cleaning up"
    if ($vm.PowerState -eq 'PoweredOn')
    {
      Stop-VM -VM $vm -Confirm:$false -Verbose:$false | Out-Null
    }
    Remove-VM -VM $vm -DeletePermanently -Confirm:$false -Verbose:$false
  }

  $ovfProp = Get-OvfConfiguration -Ovf $ovaFile -Verbose:$false
  if (-not $Photon)
  {
    $ovfProp.Common.user_data.Value = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($userData))
    $ovfProp.NetworkMapping.VM_Network.Value = $pgName
  }
  else
  {
    $ovfProp.NetworkMapping.None.Value = $pgName
  }
  $sApp = @{
    Source = $ovaFile
    Name = $vmName
    Datastore = Get-Datastore -Name $dsName -Verbose:$false
    DiskStorageFormat = 'Thin'
    VMHost = Get-Cluster -Name $clusterName -Verbose:$false | Get-VMHost  -Verbose:$false | Get-Random
    OvfConfiguration = $ovfProp
    Verbose = $false
  }
  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Importing OVA"
  $vm = Import-VApp @sApp

  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Starting the VM"
  Start-VM -VM $vm -Confirm:$false -RunAsync -Verbose:$false | Out-Null

  if ($Photon)
  {
    while (-not $vm.ExtensionData.Guest.GuestOperationsReady)
    {
      Start-Sleep 2
      $vm.ExtensionData.UpdateViewData('Guest')
    }

    Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Resetting password"

    $PswdIsReset = $false
    while (-not $PswdIsReset)
    {
      Set-VMKeystrokes -VMName $VM.Name -StringInput 'root' -ReturnCarriage $true | Out-Null
      Start-Sleep 1
      Set-VMKeystrokes -VMName $VM.Name -StringInput 'changeme' -ReturnCarriage $true | Out-Null
      Start-Sleep 1
      Set-VMKeystrokes -VMName $VM.Name -StringInput 'changeme' -ReturnCarriage $true | Out-Null
      Start-Sleep 1
      Set-VMKeystrokes -VMName $VM.Name -StringInput 'Welcome2019!' -ReturnCarriage $true | Out-Null
      Start-Sleep 1
      Set-VMKeystrokes -VMName $VM.Name -StringInput 'Welcome2019!' -ReturnCarriage $true | Out-Null
      Start-Sleep 1
      Set-VMKeystrokes -VMName $VM.Name -StringInput "exit" -ReturnCarriage $true | Out-Null

      Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Testing new password"

      $sInvoke = @{
        VM = $vm
        ScriptType = 'bash'
        ScriptText = 'whoami'
        GuestUser = $Credential.GetNetworkCredential().UserName
        GuestPassword = $Credential.GetNetworkCredential().Password
        Verbose = $false
      }
      try
      {
        Invoke-VMScript @sInvoke -ErrorAction Stop | Out-Null
        $PswdIsReset = $true
      }
      catch [VMware.VimAutomation.ViCore.Types.V1.ErrorHandling.InvalidGuestLogin]
      {
        Start-Sleep -Seconds 1
      }
      catch
      {
        Write-Error "Exception $($error[0].Exception.GetType().FullName)"
        return
      }
    }

    Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Shutdown VM"

    Shutdown-VMGuest -VM $vm -Confirm:$false -Verbose:$false | Out-Null
    while ($vm.ExtensionData.Runtime.PowerState -ne 'poweredOff')
    {
      Start-Sleep 2
      $vm.ExtensionData.UpdateViewData('Runtime.PowerState')
    }

    Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Attaching seed ISO"

    $isoName = Split-Path -Path $CloudConfigISO -Leaf
    $isoFile = "DS:\$($vm.ExtensionData.Summary.Config.VmPathName.Split(' ')[1].Split('/')[0])\$($isoName)"
    Get-PSDrive -Name DS -ErrorAction SilentlyContinue -Verbose:$false | Remove-PSDrive -Confirm:$false -Verbose:$false
    New-PSDrive -Location (Get-Datastore -VM $vm) -Name DS -PSProvider VimDatastore -Root '\' -Verbose:$false | Out-Null
    Get-Item -Path $CloudConfigISO |
    Copy-DatastoreItem -Destination $isoFile -Force -Confirm:$false -Verbose:$false
    $isoDSPath = $vm.ExtensionData.Summary.Config.VmPathName.Replace("$vmName.vmx", $isoName)
    Get-CDDrive -VM $vm -Verbose:$false | Set-CDDrive -IsoPath $isoDSPath -StartConnected:$true -Confirm:$false -Verbose:$false | Out-Null

    Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Starting the VM"
    Start-VM -VM $vm -Confirm:$false -Verbose:$false | Out-Null
  }

  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Waiting for cloud-init to finish"

  $User = $Credential.GetNetworkCredential().UserName
  $Password = $Credential.GetNetworkCredential().Password

  $sJob = @{
    Name = 'WaitForCloudInit'
    ScriptBlock = $waitJob
    ArgumentList = $vm.Name, $User, $Password, $global:DefaultVIServer.Name, $global:DefaultVIServer.SessionId
  }
  Start-Job @sJob | Receive-Job -Wait

  if ($Photon)
  {
    Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Restart OS"
    $vmGuest = Stop-VMGuest -VM $vm -Confirm:$false -Verbose:$false
    while ($vmGuest.VM.ExtensionData.Runtime.PowerState -ne 'poweredOff')
    {
      Start-Sleep 2
      $vmGuest.VM.ExtensionData.UpdateViewData('Runtime.PowerState')
    }
    $vm = Start-VM -VM $vmName -Confirm:$false -Verbose:$false
    while ($vm.PowerState -ne 'PoweredOn')
    {
      Start-Sleep 2
      $vm = Get-VM -Name $VmName -Verbose:$false
    }
    while (-not $vm.ExtensionData.Guest.GuestOperationsReady)
    {
      Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Waiting for GuestOperations ready"
      Start-Sleep 2
      $vm.ExtensionData.UpdateViewData('Guest')
    }
    Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Eject CDROM"
    $sInvoke = @{
      VM = $vm
      ScriptType = 'bash'
      ScriptText = 'eject cdrom'
      GuestUser = $Credential.GetNetworkCredential().UserName
      GuestPassword = $Credential.GetNetworkCredential().Password
      Verbose = $false
    }
    Invoke-VMScript @sInvoke | Out-Null
    Get-CDDrive -VM $vm -Verbose:$false | Set-CDDrive -NoMedia -Confirm:$false -Verbose:$false | Out-Null
  }

  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Deployment complete"
  Write-Verbose "`nDeployment took $([math]::Round((New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds,0)) seconds"
}</pre>
<p>&nbsp;</p>



<h3 class="wp-block-heading">Annotations</h3>



<p>The v1.0 version of this function has already been annotated in <a href="https://www.lucd.info/2019/12/06/cloud-init-part-1-the-basics/" target="_blank" rel="noreferrer noopener" aria-label="Part 1 (opens in a new tab)">Part 1</a> of this series. Those annotations will not be repeated here.</p>



<p><strong>Line 68-71</strong>: To distinguish between the way the user-data is passed, the function now has two mutually exclusive parameters, CloudConfig and CloudConfigIso.</p>



<p><strong>Line 73</strong>: Since Photon requires some special actions, the function now has a switch, obviously named Photon, that specifies if the cloud-init deployment is intended for a Photon instance or not.</p>



<p><strong>Line 77-80</strong>: When the user-data is provided as a YAML file, the function reads the file.</p>



<p><strong>Line 83-86</strong>: A small reminder in the verbose output that the function will handle this as a Photon deployment.</p>



<p><strong>Line 102-110</strong>: The Ubuntu and the Photon OVA files have different OVF properties. This If-Then-Else handles the differences.</p>



<p><strong>Line 126-199</strong>: This part of the function handles most of the peculiarities of a Photon deployment.</p>



<p><strong>Line 136-175</strong>: This part handles the reset of the root password. As mentioned earlier, it uses William&#8217;s <a rel="noreferrer noopener" aria-label="Set-VMKeystrokes (opens in a new tab)" href="https://www.virtuallyghetto.com/2017/09/automating-vm-keystrokes-using-the-vsphere-api-powercli.html" target="_blank">Set-VMKeystrokes</a> function. I experienced intermittent race conditions in sending the keystrokes. Hence the loop with the retries. As a test, to check if the root password was reset, the command &#8216;<em>whoami</em>&#8216; is executed trough <a rel="noreferrer noopener" aria-label="Invoke-VMScript (opens in a new tab)" href="https://vdc-repo.vmware.com/vmwb-repository/dcr-public/6fb85470-f6ca-4341-858d-12ffd94d975e/4bee17f3-579b-474e-b51c-898e38cc0abb/doc/Invoke-VMScript.html" target="_blank">Invoke-VMScript</a> on the instance.</p>



<p><strong>Line 179-184</strong>: Before attaching the seed ISO, the Photon OS is shutdown gracefully. In a While-loop the script test until the instance is actually powered off.</p>



<p><strong>Line 188-195</strong>: The seed ISO is copied to the VM&#8217;s folder, and then attached.</p>



<p><strong>Line 198</strong>: The VM is started with the seed ISO attached. As a result, cloud-init will now use the YAML files it finds on the CD drive as datasource.</p>



<p><strong>Line 216-221</strong>: Since the cloud-init implementation on Photon is <a rel="noreferrer noopener" aria-label="not configured (opens in a new tab)" href="https://github.com/vmware/photon/issues/950" target="_blank">not configured</a> to use the power-state module, the test we used on the Ubuntu instance, can not be used for Photon. As a bypass, the function stops/starts the guest OS on the instance.</p>



<p><strong>Line 235-244</strong>: The function uses the stop/start sequence to disconnect the seed ISO. To avoid having a question on the VM, the function first runs the &#8216;<em>eject cdrom</em>&#8216; command inside the instance.</p>



<h2 class="wp-block-heading">The Seed ISO</h2>



<p>As mentioned earlier, I prefer to use a seed ISO file to solve the issue of not having an OVF property for the user-data.</p>



<p>To create this seed ISO file, we need a Linux station that has the &#8216;<a rel="noreferrer noopener" aria-label="genisoimage (opens in a new tab)" href="https://linux.die.net/man/1/genisoimage" target="_blank">genisoimage</a>&#8216; command available. In the following sample script, I use an  Ubuntu station just for that purpose.</p>



<pre class="lang:ps decode:true  ">$workVM = 'UbuntuPS'
$isoName = 'genisoimage-ps.iso'
$isoVolId = 'cidata'
$isoFiles = @{
    'meta-data' = 'meta-data.yaml'
    'user-data' = 'user-data.yaml'
}

$code1 = @'
sudo apt-get install genisoimage -y &gt; null
genisoimage -output $isoName -volid $isoVolId -joliet -rock $($isoFiles.Keys -join ' ')
'@

$workCred = Get-VICredentialStoreItem -Host $workVM

$isoFiles.GetEnumerator() | ForEach-Object -Process {
    Copy-Item -Path $_.Value -Destination $_.Name
}

$sInvoke = @{
    VM = $workVM
    ScriptType = 'Bash'
    ScriptText = $ExecutionContext.InvokeCommand.ExpandString($code1)
    GuestUser = $workCred.User
    GuestPassword = ConvertTo-SecureString -String $workCred.Password -AsPlainText -Force
    GuestOSType = 'Linux'
    Verbose = $true
    NoIPinCert = $true
    InFile = $isoFiles.Keys
    OutFile = $isoName
}
Invoke-VMScriptPlus @sInvoke

Remove-Item -Path @($isoFiles.Keys) -Confirm:$false
Move-Item -Path $isoName -Destination "..\$isoName" -Force -Confirm:$false</pre>



<h3 class="wp-block-heading">Annotations</h3>



<p><strong>Line 4-7, 16-18</strong>: The script converts (copies) the provided YAML files to standard named YAML files. This allows the script to be used with different versions of the data and only needs a change in the <strong>$isoFiles</strong> hash table.</p>



<p><strong>Line 32</strong>: This script uses the latest version of my <a rel="noreferrer noopener" aria-label="Invoke-VMScriptPlus (opens in a new tab)" href="https://www.lucd.info/2019/11/17/invoke-vmscriptplus-v3/" target="_blank">Invoke-VMScriptPlus</a> function. The reason is twofold: the bash script is multi-line and the script used the InFile/OutFile parameters to copy the YAML files to the Linux box and the ISO file back to the calling station.</p>



<h2 class="wp-block-heading">User-Data</h2>



<p>The user-data that is used for a Photon based instance is, for now, rather simple and straightforward. </p>



<p>Note the installation of the <strong>tdnf</strong> <strong>PowerShell</strong> package (as mentioned earlier in this post) in the <strong>runcmd</strong> section.</p>



<pre class="lang:yaml decode:true ">#cloud-config
fqdn: PhotonPS.local.lab
timezone: Europe/Brussels
hostname: PhotonPS
write_files:
    - path: /etc/systemd/network/10-static.network
      permissions: 0644
      content: |
        [Match]
        Name=eth0

        [Network]
        Address=192.168.10.81/24
        Gateway=192.168.10.1
        DNS=192.168.10.2 192.168.10.3
        DHCP=no
        Domains=local.lab
        NTP=192.168.10.2 192.168.10.3
        LinkLocalAddressing=no
        LLDP=true
    - path: /etc/systemd/network/99-dhcp-en.network
      permissions: 0644
      content: |
        [Match]
        Name=e*

        [Network]
        DHCP=no
    - path: /etc/gnutls/default-priorities
      content: |
        SYSTEM=NONE:!VERS-SSL3.0:!VERS-TLS1.0:+VERS-TLS1.1:+VERS-TLS1.2:+AES-128-CBC:+RSA:+SHA1:+COMP-NULL
runcmd:
- tdnf distro-sync
- tdnf update -y
- tdnf install bindutils -y
- tdnf install powershell -y
- tdnf install gnuTLS -y
- touch /etc/gnutls/default-priorities
package_upgrade: true
package_reboot_if_required: true
</pre>



<h2 class="wp-block-heading">Meta-data</h2>



<p>If you plan on deploying multiple instance, just make sure that the <strong>instance-id</strong> has a unique value.</p>



<pre class="lang:yaml decode:true ">instance-id: iid-local01
local-hostname: photonps
</pre>



<h2 class="wp-block-heading">Sample Run</h2>



<p>For a Photon instance the function is called like this.</p>



<pre class="lang:ps decode:true  ">$vmName = 'PhotonPS'

# Get credential for logging on to the guest OS
$viCred = Get-VICredentialStoreItem -Host $vmName
$secPassword = ConvertTo-SecureString -String $viCred.Password -AsPlainText -Force
$cred = [Management.Automation.PSCredential]::new($viCred.User, $secPassword)

$sCloudInitVM = @{
  OvaFile = 'D:\Repository\VMware\Photon\photon-hw13_uefi-3.0-9355405.ova'
  VmName = $vmName
  ClusterName = 'cluster'
  DsName = 'vsanDatastore'
  PgName = 'vdPg1'
  CloudConfigISO = '..\genisoimage-ps.iso'
  Photon = $true
  Credential = $cred
  Verbose = $true
}
Install-CloudInitVM @sCloudInitVM</pre>



<p>The result of this, since we used the Verbose switch, looks like this.</p>



<figure class="wp-block-image size-large"><img decoding="async" width="687" height="533" src="https://www.lucd.info/wp-content/uploads/2019/12/photon-deploy-1.png" alt="" class="wp-image-6832" srcset="https://www.lucd.info/wp-content/uploads/2019/12/photon-deploy-1.png 687w, https://www.lucd.info/wp-content/uploads/2019/12/photon-deploy-1-300x233.png 300w" sizes="(max-width: 687px) 100vw, 687px" /></figure>



<p>Again, don&#8217;t give too much importance to the total execution time. This was a run in my lab environment, which has limited resources.</p>



<p>On a side-note, notice how the PowerCLI cmdlets used in the function all show this, imho, annoying &#8216;<em><strong>Finished execution</strong></em>&#8216; message.<br>I would love to be able to suppress these verbose messages from the PowerCLI cmdlets. That is why I launched <a rel="noreferrer noopener" aria-label="PowerCLI Idea #225 (opens in a new tab)" href="https://powercli.ideas.aha.io/ideas/PCLI-I-225?utm_source=idea_mailer&amp;utm_medium=email&amp;utm_campaign=submitted_idea" target="_blank">PowerCLI Idea #225</a> &#8220;<em>Get rid of the over-eager verbosity on PowerCLI cmdlets</em>&#8220;.</p>



<p>Enjoy!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.lucd.info/2019/12/08/cloud-init-part-3-photon-os/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>Cloud-init &#8211; Part 1 &#8211; The Basics</title>
		<link>https://www.lucd.info/2019/12/06/cloud-init-part-1-the-basics/</link>
					<comments>https://www.lucd.info/2019/12/06/cloud-init-part-1-the-basics/#comments</comments>
		
		<dc:creator><![CDATA[LucD]]></dc:creator>
		<pubDate>Fri, 06 Dec 2019 12:16:08 +0000</pubDate>
				<category><![CDATA[Cloud-init]]></category>
		<category><![CDATA[Deploy]]></category>
		<category><![CDATA[OVA]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[YAML]]></category>
		<category><![CDATA[cloud-init]]></category>
		<category><![CDATA[PowerCLI]]></category>
		<category><![CDATA[ubuntu]]></category>
		<category><![CDATA[VM]]></category>
		<guid isPermaLink="false">http://www.lucd.info/?p=6586</guid>

					<description><![CDATA[In this first part, we will introduce cloud-init and how you can use it from your PowerShell/PowerCLI scripts. Since the Ubuntu distribution is very popular...]]></description>
										<content:encoded><![CDATA[
<p>One of the important <a href="https://en.wikipedia.org/wiki/DevOps">DevOps</a> adagios in my book is “<em>Treat your servers as cattle, not as pets</em>”. Meaning that you roll out your stations when you need them, use them and throw them away after you used them. This series of posts will document one such way of deploying such &#8216;cattle&#8217; stations. The method is named <a href="https://cloud-init.io/" target="_blank" rel="noreferrer noopener" aria-label="cloud-init (opens in a new tab)">cloud-init</a>.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="678" height="295" src="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-1.png" alt="Post logo" class="wp-image-6589" srcset="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-1.png 678w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-1-300x131.png 300w" sizes="auto, (max-width: 678px) 100vw, 678px" /></figure>



<p>In this first part, we will introduce <strong>cloud-init</strong> and how you can use it from your <strong>PowerShell/PowerCLI</strong> scripts. Since the <a rel="noreferrer noopener" aria-label="Ubuntu (opens in a new tab)" href="https://en.wikipedia.org/wiki/Ubuntu" target="_blank">Ubuntu</a> distribution is very popular, on-premises and in the cloud, this introduction will focus on that distro to demonstrate the concept. In the following parts, we will tackle <a rel="noreferrer noopener" aria-label="Photon (opens in a new tab)" href="https://vmware.github.io/photon/" target="_blank">Photon</a>, containers and how to run your scripts on these stations.</p>



<span id="more-6586"></span>



<p>Before diving into the technical stuff, first a reminder why we should treat our <strong>servers as cattle</strong>.</p>



<p>A list with some of the major arguments (at least for me).</p>



<ul class="wp-block-list"><li>No maintenance required when using the latest distributions. Meaning no long-running updates and security patches before usage.</li><li>No “<em>less than 5% used</em>” machines anymore in your environment.</li><li>No risk of system tattooing from any previous usage.</li><li>An easy way to test new versions of your scripts, the OS, PowerShell and PowerCLI.</li></ul>



<h2 class="wp-block-heading">Ingredients</h2>



<p>To use cloud-init, the requirements, in the cases we will handle here, are minimal.</p>



<ul class="wp-block-list"><li>An <strong>OVA image</strong> that is configured to use cloud-init<ul><li>Several Linux distributions nowadays have such a &#8216;cloud&#8217; image: <a rel="noreferrer noopener" aria-label="Ubuntu (opens in a new tab)" href="https://cloud-images.ubuntu.com/" target="_blank">Ubuntu</a>, <a rel="noreferrer noopener" aria-label="Photon (opens in a new tab)" href="https://github.com/vmware/photon/wiki/Downloading-Photon-OS" target="_blank">Photon</a>&#8230;</li></ul></li><li>A <a rel="noreferrer noopener" aria-label="datasource (opens in a new tab)" href="https://cloudinit.readthedocs.io/en/latest/topics/datasources.html" target="_blank">datasource</a>, which is the configuration data provided by the user to the cloud-init process, and which defines how the resulting station will be configured.</li><li>A <strong>cloud</strong>, in it&#8217;s broadest sense, to run the VM. From the cloud-init documentation &#8220;&#8230; <em>all major public cloud providers, provisioning systems for private cloud infrastructure, and bare-metal installations</em>.&#8221;</li><li>A <strong>script</strong> to trigger and control it all. </li></ul>



<p><a rel="noreferrer noopener" aria-label="Canonical (opens in a new tab)" href="https://twitter.com/canonical" target="_blank">Canonical</a>, the company behind Ubuntu, maintains a document, named <a href="https://pages.ubuntu.com/rs/066-EOV-335/images/CloudInit_Whitepaper.pdf?utm_source=marketo&amp;utm_medium=landingpage&amp;utm_campaign=CY19_DC_Server_Whitepaper_CloudInit" target="_blank" rel="noreferrer noopener" aria-label="Cloud Instance Initialisation with cloud-init (opens in a new tab)">Cloud Instance Initialisation with cloud-init</a>, that has a schematic that captures the cloud-init process perfectly.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="923" height="487" src="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-schema.png" alt="Cloud-init schematic" class="wp-image-6600" srcset="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-schema.png 923w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-schema-300x158.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-schema-768x405.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-schema-720x380.png 720w" sizes="auto, (max-width: 923px) 100vw, 923px" /></figure>



<p>If we annotate and update this schema for the usage we have in mind, it becomes like this.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="860" height="487" src="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-schema-vsphere.png" alt="Cloud-init for a vSphere environment schematic" class="wp-image-6601" srcset="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-schema-vsphere.png 860w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-schema-vsphere-300x170.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-schema-vsphere-768x435.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-schema-vsphere-720x408.png 720w" sizes="auto, (max-width: 860px) 100vw, 860px" /></figure>



<h2 class="wp-block-heading">The OVA File</h2>



<p>Since our target cloud platform in this series is a vSphere environment, we will use OVA files as the source for the VM(s) we are going to deploy.</p>



<p>The <a rel="noreferrer noopener" aria-label="Ubuntu OVA image (opens in a new tab)" href="https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.ova" target="_blank">Ubuntu OVA image</a> that we will be using in this post is for <strong>Ubuntu Server 18.04 LTS</strong>, aka <strong>Bionic Beaver</strong>.</p>



<p>This OVA image allows using the <a rel="noreferrer noopener" aria-label="OVFProperties (opens in a new tab)" href="https://blogs.vmware.com/PowerCLI/2014/09/powercli-5-8-new-feature-get-ovfconfiguration-part-1-2.html" target="_blank">OVFProperties</a> as the datasource. Note that the user-data we pass in this way needs to be <a rel="noreferrer noopener" aria-label="Base64 (opens in a new tab)" href="https://en.wikipedia.org/wiki/Base64" target="_blank">Base64</a> encoded.</p>



<h2 class="wp-block-heading">The User-data</h2>



<p>As mentioned in the previous section, in this Ubuntu image we will use the <strong>OVFProperties</strong> as the datasource for our user-data.</p>



<p>The content of the <strong>user-data</strong> is provided to the script as a <a rel="noreferrer noopener" aria-label="YAML (opens in a new tab)" href="https://en.wikipedia.org/wiki/YAML" target="_blank">YAML</a> file.  The syntax for such a file and the available is defined in the <a href="https://readthedocs.org/projects/cloudinit/downloads/pdf/latest/" target="_blank" rel="noreferrer noopener" aria-label="cloud-init Documentation (opens in a new tab)">cloud-init Documentation</a>.</p>



<p>The following is the sample YAML file we will use in the rest of this post.</p>



<pre class="theme:vs2012-black lang:yaml decode:true ">#cloud-config
hostname: ubuntubionic
fqdn: ubuntubionic.local.lab
write_files:
- path: /etc/netplan/50-cloud-init.yaml
  content: |
    network:
     version: 2
     ethernets:
      ens192:
       addresses: [192.168.10.79/24]
       gateway4: 192.168.10.1
       dhcp6: false
       nameservers:
         addresses:
           - 192.168.10.2
           - 192.168.10.3
         search:
           - local.lab
       dhcp4: false
       optional: true
- path: /etc/sysctl.d/60-disable-ipv6.conf
  owner: root
  content: |
    net.ipv6.conf.all.disable_ipv6=1
    net.ipv6.conf.default.disable_ipv6=1
runcmd:
- netplan --debug apply
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- sysctl -w net.ipv6.conf.default.disable_ipv6=1
- apt-get -y update
- add-apt-repository universe
- apt-get -y clean
- apt-get -y autoremove --purge
timezone: Europe/Brussels
system_info:
  default_user:
    name: default-user
    lock_passwd: false
    sudo: ["ALL=(ALL) NOPASSWD:ALL"]
disable_root: false
ssh_pwauth: yes
users:
  - default
  - name: luc
    gecos: LucD
    lock_passwd: false
    groups: sudo, users, admin
    shell: /bin/bash
    sudo: ['ALL=(ALL) NOPASSWD:ALL']
chpasswd:
  list: |
    default-user:$6$aPp//e2ueP$ETEXcAhAyQuJ4qNCbqxmmSYGZbg2wFwpP/YITvoXdgxwZBnf32drePKi2OIn5fLqtH5pHO03yRdPXK3ToLG6b0
    luc:$6$kW1WwJ2K$M6415du1BZd.qt92SvR6X.RuyDhEZmgR4hz4NcKH9XHn2850Vc6zHpubXM6uUeqMUaJQ740ogROB74gfBEhn9.
    root:$6$Js9CVr06$br9qf0VxuBsdY7Vtg/0pk9jLlycYBDLVsvbKwLDleCK7dSDheOxWaFOWdjkiqSPRrWG./N8V5RgCVwugZGnTc1
  expire: false
package_upgrade: true
package_reboot_if_required: true
power_state:
  delay: now
  mode: reboot
  message: Rebooting the OS
  condition: if [ -e /var/run/reboot-required ]; then exit 0; else exit 1; fi
</pre>



<h3 class="wp-block-heading">Annotations</h3>



<p><strong>Line 1</strong>: the user-data file always has to start with the line &#8216;# cloud-config&#8217;</p>



<p><strong>Line 2</strong>-3: These lines follow the regular <a rel="noreferrer noopener" aria-label="YAML key-value syntax (opens in a new tab)" href="https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html" target="_blank">YAML syntax</a> of key-value entries.  The lines defines the <strong>hostname</strong> and the <strong>FQDN</strong> that the instance will get.</p>



<p><strong>Line 4-26</strong>: The <strong>write-files</strong> section instructs cloud-init to <strong>create files</strong>, with the specified properties and content on the instance. In this example file the files are used to define a static IPv4 address and to disable IPv6. Which files need to be used is dependent on the OS that runs in the VM. Note that this example will use <a href="https://netplan.io/" target="_blank" rel="noreferrer noopener" aria-label="netplan (opens in a new tab)">netplan</a> for the network configuration.</p>



<p><strong>Line 27-34</strong>: The <strong>runcmd</strong> section defines commands that will run on the instance during the <strong>first boot</strong> of the guest OS. In this example the commands are used to activate the network (it uses netplan), set some variables and set and update the application repositories.</p>



<p><strong>Line 35</strong>: This line defines which timezone needs to be configured.</p>



<p><strong>Line 36-40</strong>: An Ubuntu image comes configured with a <strong>default user. In the system_info section,</strong> the settings for this default user are specified.</p>



<p><strong>Line 43-50</strong>: The <strong>users</strong> section allows to define additional users on the instance. Note that the <strong>default</strong> user needs to come first in this list.</p>



<p><strong>Line 51-56</strong>: The <strong>chpasswd</strong> allows you to change and/or set the passwords for the users. The required format and how these entries are created will be discussed in one of the following sections.</p>



<p><strong>Line 57-58</strong>: These lines request an upgrade of the <strong>packages</strong> present on the instance. And perform a reboot when one of the package upgrades would require that.</p>



<p><strong>Line 59-63</strong>: The <strong>power-state</strong> section instructs cloud-init in which state the instance shall be left after cloud-init completes. In this example, the system is rebooted on condition that a specific file exists. This file is created by cloud-init when it has decided that a reboot of the instance is required.</p>



<h3 class="wp-block-heading">Password encoding</h3>



<p>In <strong>user-data,</strong> there are several places where a password can be provided. The value needs to be the hash of the password, not the password itself.  With the help of an existing Linux box (<strong>UbuntuWork</strong> in this case), I use the following script to create such SHA-512 encoded password hashes.  The script uses the <a rel="noreferrer noopener" aria-label="mkpasswd (opens in a new tab)" href="https://manpages.ubuntu.com/manpages/trusty/man1/mkpasswd.1.html" target="_blank">mkpasswd</a> command to generate the hash. </p>



<p>Automation rules!</p>



<pre class="lang:ps decode:true  ">function Get-PasswordHash
{
    param(
        [String]$VMName,
        [String]$GuestUser,
        [String]$GuestPassword,
        [String]$Password
    )

    $sInvoke = @{
        VM = $VMName
        ScriptType = 'bash'
        ScriptText = "mkpasswd -m SHA-512 $Password"
        GuestUser = $GuestUser
        GuestPassword = $GuestPassword
    }
    (Invoke-VMScript @sInvoke).ScriptOutput.Trim("<code>n")
}

$sHash = @{
    VM = 'UbuntuWork'
    GuestUser = 'root'
    GuestPassword = 'VMware1!'
    Password = 'VMware1!'
}
Get-PasswordHash @sHash
</pre>



<p>This returns something like this.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="944" height="81" src="https://www.lucd.info/wp-content/uploads/2019/12/hash.png" alt="Password hash" class="wp-image-6613" srcset="https://www.lucd.info/wp-content/uploads/2019/12/hash.png 944w, https://www.lucd.info/wp-content/uploads/2019/12/hash-300x26.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/hash-768x66.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/hash-720x62.png 720w" sizes="auto, (max-width: 944px) 100vw, 944px" /></figure>



<p>A word of warning, such hashed password are not super-safe. A decent password cracker with sufficient compute power and a good wordlist can most probably crack this in less than one minute.</p>



<h2 class="wp-block-heading">The Script</h2>



<pre class="lang:ps decode:true   ">function Install-CloudInitVM
{
  &lt;#
.SYNOPSIS
  Deploy a VM from an OVA file and use cloud-init for the configuration
  .DESCRIPTION
  This function will deploy an OVA file.
  The function transfer the user-data to the cloud-init process on the VM with
  one of the OVF properties.
.NOTES
  Author:  Luc Dekens
  Version:
  1.0 05/12/19  Initial release
.PARAMETER OvaFile
  Specifies the path to the OVA file
.PARAMETER VmName
  The displayname of the VM
.PARAMETER ClusterName
  The cluster onto which the VM shall be deployed
.PARAMETER DsName
  The datastore on which the VM shall be deployed
.PARAMETER PgName
  The portgroupname to which the VM shall be connected
.PARAMETER CloudConfig
  The path to the YAML file containing the user-data
.PARAMETER Credential
  The credentials for a user in the VM's guest OS
.EXAMPLE
  $sCloudInitVM = @{
    OvaFile = '.\bionic-server-cloudimg-amd64.ova'
    VmName = $vmName
    ClusterName = $clusterName
    DsName = $dsName
    PgName = $pgName
    CloudConfig = '.\user-data.yaml'
    Credential = $cred
  }
  Install-CloudInitVM @sCloudInitVM
  #&gt;

  [cmdletbinding()]
  param(
    [string]$OvaFile,
    [string]$VmName,
    [string]$ClusterName,
    [string]$DsName,
    [string]$PgName,
    [string]$CloudConfig,
    [PSCredential[]]$Credential
  )

  $waitJob = (Get-Command -Name .\Wait-Job.ps1).ScriptBlock
  $userData = Get-Content -Path $CloudConfig -Raw

  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Starting deployment of $vmName"

  $start = Get-Date

  $vm = Get-VM -Name $vmName -ErrorAction SilentlyContinue
  if ($vm)
  {
    Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Cleaning up"
    if ($vm.PowerState -eq 'PoweredOn')
    {
      Stop-VM -VM $vm -Confirm:$false | Out-Null
    }
    Remove-VM -VM $vm -DeletePermanently -Confirm:$false
  }

  $ovfProp = Get-OvfConfiguration -Ovf $ovaFile
  $ovfProp.Common.user_data.Value = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($userData))
  $ovfProp.NetworkMapping.VM_Network.Value = $pgName

  $sApp = @{
    Source = $ovaFile
    Name = $vmName
    Datastore = Get-Datastore -Name $dsName
    DiskStorageFormat = 'Thin'
    VMHost = Get-Cluster -Name $clusterName | Get-VMHost | Get-Random
    OvfConfiguration = $ovfProp
  }
  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Importing OVA"
  $vm = Import-VApp @sApp

  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Starting the VM"
  Start-VM -VM $vm -Confirm:$false -RunAsync | Out-Null

  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Waiting for cloud-init to finish"

  $User = $Credential.GetNetworkCredential().UserName
  $Password = $Credential.GetNetworkCredential().Password

  $sJob = @{
    Name = 'WaitForCloudInit'
    ScriptBlock = $waitJob
    ArgumentList = $vm.Name, $User, $Password, $global:DefaultVIServer.Name, $global:DefaultVIServer.SessionId
  }
  Start-Job @sJob | Receive-Job -Wait

  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Deployment complete"

  Write-Verbose "</code>nDeployment took $([math]::Round((New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds,0)) seconds"
}
</pre>



<h3 class="wp-block-heading">Annotations</h3>



<p><strong>Line 52</strong>: To find out when cloud-init has finished it's run, the function uses a background job. This background job is stored in a separate .ps1 file. More on the content and purpose of that 'wait' script later.</p>



<p><strong>Line 53</strong>: The user-data is read from the YAML file. It is important to use the <strong>Raw</strong> switch, otherwise, we would lose the line separators.</p>



<p><strong>Line 60-68</strong>: If a VM with the same Displayname is already present, it will be stopped (when powered on) and removed.</p>



<p><strong>Line 70-71</strong>: The user-data is assigned to the <strong>Common.user_data</strong> property of the OVF properties. The user-data needs to be converted to <strong>base64</strong>.</p>



<p><strong>Line 72</strong>: The Ubuntu OVA allows to pass the Portgroup to which the VM shall be connected.</p>



<p><strong>Line 74-83</strong>: The VM is installed from the OVA with the <a rel="noreferrer noopener" aria-label="Import-VApp (opens in a new tab)" href="https://vdc-repo.vmware.com/vmwb-repository/dcr-public/6fb85470-f6ca-4341-858d-12ffd94d975e/4bee17f3-579b-474e-b51c-898e38cc0abb/doc/Import-VApp.html" target="_blank">Import-VApp</a> cmdlet.</p>



<p><strong>Line 86</strong>: The VM is powered on. During the boot process, the first phase of cloud-init will run.</p>



<p><strong>Line 90-98</strong>: The function uses a background job to check if the cloud-init process has completed. More on that later.</p>



<h3 class="wp-block-heading">The Wait job</h3>



<p>I decided to store the WaitJob script in a separate .ps1 file. Primarily because I can easily reuse it this way and it makes the code of scripts that use shorter.</p>



<p>The Wait Job is started as a background job. This allows the calling code to simply use the <a href="https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/wait-job?view=powershell-6" target="_blank" rel="noreferrer noopener" aria-label="Wait-Job (opens in a new tab)">Wait-Job</a> cmdlet.</p>



<pre class="lang:ps decode:true   ">[CmdletBinding()]
param(
    [string]$vmName,
    [string]$user,
    [string]$pswd,
    [string]$vcsaName,
    [string]$SessionId
)

$sleepTime = 5
Connect-VIServer -Server $vcsaName -Session $SessionId | Out-Null
$notFinished = $true
while ($notFinished)
{
    Try
    {
        $vm = Get-VM -Name $vmName -ErrorAction Stop
        while ($vm.PowerState -ne 'PoweredOn' -and -not $vm.ExtensionData.Guest.GuestOperationsReady)
        {
            Start-Sleep -Seconds $sleepTime
        }
        $fileExist = $false
        while (-not $fileExist)
        {
            $sInvoke = @{
                VM = $vm
                ScriptType = 'bash'
                ScriptText = '[ -f /var/lib/cloud/instance/boot-finished ] &amp;&amp; echo "File exist"'
                GuestUser = $user
                GuestPassword = $pswd
                ErrorAction = 'Stop'
            }
            try
            {
                $result = Invoke-VMScript @sInvoke
                $fileExist = [Boolean]$result.ScriptOutput
            }
            catch
            {
                Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') Exception:<code>tId: $($error[0].Exception.ErrorId)  Category: $($error[0].Exception.ErrorCategory)"
                Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff')</code>t<code>tLine: $($error[0].InvocationInfo.Line)"
            }
        }
        $sInvoke = @{
            VM = $vm
            ScriptType = 'bash'
            ScriptText = 'cat /var/lib/cloud/instance/boot-finished'
            GuestUser = $user
            GuestPassword = $pswd
        }
        $result = Invoke-VMScript @sInvoke
        $result.ScriptOutput
        $notFinished = $false
    }
    catch
    {
        Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') Exception:</code>tId: $($error[0].Exception.ErrorId)  Category: $($error[0].Exception.ErrorCategory)"
        Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff')<code>t</code>tLine: $($error[0].InvocationInfo.Line)"
    }
    Start-Sleep -Seconds $sleepTime
}</pre>
<p>&nbsp;</p>



<h4 class="wp-block-heading">Annotations</h4>



<p><strong>Line 7 + 11</strong>: In a background job we do not inherit any open connections to a vSphere Server. A script can use the SessionId of an open connection in the calling script, to open a connection, without having to provide credentials.</p>



<p><strong>Line 18</strong>: Before using the call to <a rel="noreferrer noopener" aria-label="Invoke-VMScript (opens in a new tab)" href="https://vdc-repo.vmware.com/vmwb-repository/dcr-public/6fb85470-f6ca-4341-858d-12ffd94d975e/4bee17f3-579b-474e-b51c-898e38cc0abb/doc/Invoke-VMScript.html" target="_blank">Invoke-VMScript</a>, the Wait Job makes sure the VM and the VMware Tools in there, are ready to receive such a call.</p>



<p><strong>Line 28</strong>: Inside the guest OS, the function uses the simple bash expression to test for the presence of a file. The file <strong>/var/lib/cloud/instance/boot-finished</strong> is created by cloud-init when the process completes. </p>



<p><strong>Line 44-52</strong>: The Wait Job function returns the content of the file to the caller.</p>



<h2 class="wp-block-heading">Sample Run</h2>



<p>With the above code, we have everything in place to deploy a <strong>VM</strong> from an <strong>Ubuntu OVA</strong>, with the guest OS configuration is done through cloud-init, based on a <strong>YAML</strong> file.</p>



<p>A typical call could look something like this.</p>



<pre class="lang:ps decode:true  ">$vmName = 'UbuntuBionic'

# Get credential for logging on to the guest OS

$viCred = Get-VICredentialStoreItem -Host $vmName
$secPassword = ConvertTo-SecureString -String $viCred.Password -AsPlainText -Force
$cred = [Management.Automation.PSCredential]::new($viCred.User, $secPassword)

$sCloudInitVM = @{
  OvaFile = '.\bionic-server-cloudimg-amd64.ova'
  VmName = $vmName
  ClusterName = 'cluster'
  DsName = 'vsanDatastore'
  PgName = 'vdPg1'
  CloudConfig = '.\user-data-bionic.yaml'
  Credential = $cred
  Verbose = $true
}
Install-CloudInitVM @sCloudInitVM</pre>


<p>With the <strong>Verbose</strong> switch set to $true, the output of this call would look like this.</p>


<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="763" height="390" src="https://www.lucd.info/wp-content/uploads/2019/12/bionicVM.png" alt="Deployment - Verbose output" class="wp-image-6662" srcset="https://www.lucd.info/wp-content/uploads/2019/12/bionicVM.png 763w, https://www.lucd.info/wp-content/uploads/2019/12/bionicVM-300x153.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/bionicVM-720x368.png 720w" sizes="auto, (max-width: 763px) 100vw, 763px" /></figure>



<p>I highlighted with <mark><span style="background-color:#f78da7" class="tadv-background-color">red</span></mark> boxes the verbose messages from the function. The text in the <span style="background-color:#7bdcb5" class="tadv-background-color">green</span> box is the content of the  <strong>/var/lib/cloud/instance/boot-finished file</strong>.</p>



<p>Do not look too much at the timings. This run happened in my lab environment, which has limited resources. The main reason for showing this is to demonstrate how easy such a cloud-init based deployment can be incorporated in your pipelines.</p>



<p>This concludes Part 1 in the cloud-init series. It provides some functions and scripts to deploy a VM, starting from an OVA file, with the configuration of the guest OS done through cloud-init.</p>



<p>In the upcoming parts in this series, I will show some more advanced Ubuntu deployments, show how cloud-init can be used with Photon and show how you can use these deployed VMs as your '<em>cattle</em>'.</p>



<p>Enjoy!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.lucd.info/2019/12/06/cloud-init-part-1-the-basics/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
	</channel>
</rss>
