<?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>ubuntu Archives - LucD notes</title>
	<atom:link href="https://www.lucd.info/tag/ubuntu/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.lucd.info/tag/ubuntu/</link>
	<description>My PowerShell ramblings</description>
	<lastBuildDate>Wed, 11 Dec 2019 17:26:50 +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>ubuntu Archives - LucD notes</title>
	<link>https://www.lucd.info/tag/ubuntu/</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 5 &#8211; Running Containers</title>
		<link>https://www.lucd.info/2019/12/11/cloud-init-part-5-running-containers/</link>
					<comments>https://www.lucd.info/2019/12/11/cloud-init-part-5-running-containers/#comments</comments>
		
		<dc:creator><![CDATA[LucD]]></dc:creator>
		<pubDate>Wed, 11 Dec 2019 14:30:53 +0000</pubDate>
				<category><![CDATA[Cloud-init]]></category>
		<category><![CDATA[Containers]]></category>
		<category><![CDATA[Photon]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[ubuntu]]></category>
		<guid isPermaLink="false">http://www.lucd.info/?p=6917</guid>

					<description><![CDATA[In this last part of this series (for now) we will show how [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>In this last part of this series (for now) we will show how to use <strong>containers</strong> to run your PowerShell/PowerCLI scripts on the deployed instances. And although technically not a ‘<em>real</em>‘&nbsp;<a href="https://cloudinit.readthedocs.io/en/latest/" target="_blank" rel="noopener noreferrer">cloud-init</a>&nbsp;post, I consider it related to&nbsp;<a href="https://www.lucd.info/2019/12/06/cloud-init-part-1-the-basics/" target="_blank" rel="noopener noreferrer">Part 1</a>,&nbsp;<a href="https://www.lucd.info/2019/12/07/cloud-init-part-2-advanced-ubuntu/" target="_blank" rel="noopener noreferrer">Part 2</a>&nbsp;and&nbsp;<a href="https://www.lucd.info/2019/12/08/cloud-init-part-3-photon-os/" target="_blank" rel="noopener noreferrer">Part3</a> in this series.&nbsp;</p>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="730" height="349" src="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-5-2.png" alt="" class="wp-image-6923" srcset="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-5-2.png 730w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-5-2-300x143.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-5-2-720x344.png 720w" sizes="(max-width: 730px) 100vw, 730px" /></figure>



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



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



<p>In <a rel="noreferrer noopener" aria-label="Post 4 (opens in a new tab)" href="https://www.lucd.info/2019/12/09/cloud-init-part-4-running-scripts/" target="_blank">Post 4</a> of the <a href="https://www.lucd.info/?s=cloud-init" target="_blank" rel="noreferrer noopener" aria-label="cloud-init series (opens in a new tab)">cloud-init series</a>, we ran our PowerShell/PowerCLI scripts natively in the Guest OS of the instance.</p>



<p>That might have an impact in some situations. When the first script installs for example the VMware PowerCLI modules, these modules will stay installed in the Guest OS. Consider this as a kind of <strong>PowerShell tattooing</strong>.</p>



<p>Another task that might be somewhat of a challenge is testing your PowerShell scripts against multiple environments, different module versions, different OS versions&#8230; Again, when we make changes to the Guest OS, these changes might be <strong>hard to remove</strong>.</p>



<p>Besides deploying a new &#8216;cattle&#8217; station for each possible combination, we can also run our scripts in <a rel="noreferrer noopener" aria-label="Docker containers (opens in a new tab)" href="https://www.docker.com/" target="_blank">Docker containers</a>. Once the script is done, we throw away the container instance. And we are back to a pristine OS.</p>



<p>And as it turns out, containers also offer some new features that are harder, or even impossible, to implement when running locally in the Guest OS.</p>



<p>Since we will be using several docker commands in the following sections, it will be useful to have the <a href="https://docs.docker.com/reference/" target="_blank" rel="noreferrer noopener" aria-label="Docker Reference (opens in a new tab)">Docker Reference</a> documentation nearby. </p>



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



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



<p>The Ubuntu OVAs we have been installing in the previous posts in this series, do not contain the docker package. We can easily include that in our YAML file under the <strong>runcmd</strong> section.</p>



<pre class="lang:yaml decode:true ">#cloud-config
hostname: ubuntubioniccontainer
fqdn: ubuntubioniccontainer.local.lab
write_files:
- path: /etc/netplan/50-cloud-init.yaml
  content: |
    network:
     version: 2
     ethernets:
      ens192:
       addresses: [192.168.10.84/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 -y autoremove
- apt-get update
- apt-get remove docker docker-engine docker.io
- apt install docker.io -y
- systemctl enable docker
- systemctl start docker
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$2csGx3AAX0RPRMPB$85sEEZ76z0V93M9pvXGtTFrwlMOaV2VjRHB6IKPRrwc.4qEqcs2EnDzn8oUlcVVb9Biq.Pg7qQchLgNLuetkT0
    luc:$6$.4Kyvju2v5iR$bvZHOntkH9EqgGdMc7fKZKbB132LYHVIFgxN7Ho.mlMGloUayOaKijR6MvLp/ncf4SynTigjaOKGk11EF9gru/
    root:$6$m1TXY60tUAViCYb1$/w.CqV50.M0Nlt6mx2726.bxWZ7/TCqWjOOw4S3h1H8AN.SPWQnKoASXth2E3euBXwebU4Q7dpoGU1SiQontT1
  expire: false
package_upgrade: true
package_reboot_if_required: true</pre>



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



<p><strong>Line 33-36</strong>: Install the docker package, start it and make sure it runs after a reboot.</p>



<p><strong>Line 33</strong>: If there should already be a docker package installed, this line removes that package. If no docker package is installed, the line does nothing.</p>



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



<p>The Photon OVA we use comes with docker installed. No need for any additional commands in the YAML file.</p>



<h2 class="wp-block-heading">Image microsoft/powershell</h2>



<p>In the following sections we will run a number of scripts in the microsoft/powershell container on Ubuntu and Photon. Only when there is a difference between both platforms, will the text mention the platforms.</p>



<h3 class="wp-block-heading">A Simple Run</h3>



<p>Let&#8217;s try running a simple PowerShell script in a container on the station.</p>



<pre class="lang:ps decode:true  ">$vmName = 'ubuntubioniccontainer'
$code = @'
sudo docker run --rm microsoft/powershell:latest pwsh -C 'whoami;Get-Process;pwd'
'@

$credItem = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credItem.User, (ConvertTo-SecureString $credItem.Password -AsPlainText -Force))

$sInvoke = @{
    VM = $vmName
    GuestCredential = $cred
    ScriptType = 'bash'
    ScriptText = $code
    NoIPinCert = $true
    Verbose = $true
}
Invoke-VMScriptPlus @sInvoke
</pre>



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



<p><strong>Line 3</strong>: The docker command we sent to the station. The details of the different parts in this line are explained in the following table.</p>




<table id="tablepress-2" class="tablepress tablepress-id-2">
<tbody>
<tr class="row-1">
	<td class="column-1">run --rm</td><td class="column-2">Run a command in a container and clean up when the container exits</td>
</tr>
<tr class="row-2">
	<td class="column-1">microsoft/powershell:latest</td><td class="column-2">The name of the Image. In this case, the latest Microsoft provided stable PowerShell image</td>
</tr>
<tr class="row-3">
	<td class="column-1">pwsh -C</td><td class="column-2">The command and argument we send to the container. In this case we start PowerShell and use the Command parameter</td>
</tr>
<tr class="row-4">
	<td class="column-1">whoami;Get-Process;pwd</td><td class="column-2">A series of commands, separated by semi-columns, that will be executed by ‘pwsh’</td>
</tr>
</tbody>
</table>
<!-- #tablepress-2 from cache -->



<p>The output will be something like this.</p>



<figure class="wp-block-image size-large"><img decoding="async" width="855" height="1024" src="https://www.lucd.info/wp-content/uploads/2019/12/crun1-855x1024.png" alt="" class="wp-image-6984" srcset="https://www.lucd.info/wp-content/uploads/2019/12/crun1-855x1024.png 855w, https://www.lucd.info/wp-content/uploads/2019/12/crun1-251x300.png 251w, https://www.lucd.info/wp-content/uploads/2019/12/crun1-768x919.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/crun1-720x862.png 720w, https://www.lucd.info/wp-content/uploads/2019/12/crun1.png 944w" sizes="(max-width: 855px) 100vw, 855px" /></figure>



<p>Notice how docker will search for and install the image (since it isn&#8217;t present). That is the output in the <mark>yellow</mark> box. The actual output of the commands we sent to <em>pwsh</em>, are in the <span style="background-color:#00d084" class="tadv-background-color">green</span> box.</p>



<p>The second time we run the same script, we will only see the actual output of the commands. And that&#8217;s because the image is already present.</p>



<figure class="wp-block-image size-large"><img decoding="async" width="910" height="524" src="https://www.lucd.info/wp-content/uploads/2019/12/crun2.png" alt="" class="wp-image-6985" srcset="https://www.lucd.info/wp-content/uploads/2019/12/crun2.png 910w, https://www.lucd.info/wp-content/uploads/2019/12/crun2-300x173.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/crun2-768x442.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/crun2-720x415.png 720w" sizes="(max-width: 910px) 100vw, 910px" /></figure>



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



<p>Time to start diving into PowerCLI. For that we need to install the VMware PowerCLI modules of course.</p>



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

$script = @'
$ProgressPreference = "SilentlyContinue"
Install-Module -Name VMware.PowerCLI -AllowClobber -Force | Out-Null
Get-Module -Name VMware* -ListAvailable
'@

$docker = @'
sudo docker run --rm microsoft/powershell:latest pwsh -C '$($script.Replace("<code>r",'').Split("</code>n") -join ';')'
'@

$credItem = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credItem.User, (ConvertTo-SecureString $credItem.Password -AsPlainText -Force))

$sInvoke = @{
    VM = $vmName
    GuestCredential = $cred
    ScriptType = 'bash'
    ScriptText = $ExecutionContext.InvokeCommand.ExpandString($docker)
    NoIPinCert = $true
    Verbose = $true
}
Invoke-VMScriptPlus @sInvoke
</pre>



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



<p><strong>Line 3-7</strong>: We use a here-string for the script we want to execute under <em>pwsh</em> in the container.</p>



<p><strong>Line 9-11</strong>: And another here-string for the <em>docker</em> command. </p>



<p><strong>Line 10</strong>: The script is running on a Windows box, hence the &lt;CR&gt;&lt;LF&gt; to &lt;LF&gt;. The second manipulation is to convert the array of lines to a single line, where the lines are separated with a semi-column. Note that the actual conversion does not happen here.</p>



<p><strong>Line 20</strong>: The script uses the ExpandString method to perform the text manipulations we described in the comments on <strong>Line 10</strong>. Also note that we don&#8217;t have to escape the dollar-signs in the $script part, since the variable substitution will not go that deep.</p>



<p>The result (don&#8217;t mind the funny characters for now).</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="937" height="794" src="https://www.lucd.info/wp-content/uploads/2019/12/crun3.png" alt="" class="wp-image-6992" srcset="https://www.lucd.info/wp-content/uploads/2019/12/crun3.png 937w, https://www.lucd.info/wp-content/uploads/2019/12/crun3-300x254.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/crun3-768x651.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/crun3-720x610.png 720w" sizes="auto, (max-width: 937px) 100vw, 937px" /></figure>



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



<p>When we use all the defaults in the container, we seem to end up with a simple screen that has the default 80 x 25 dimensions. For better results, we can introduce bigger screens inside the container.</p>


<pre class="urvanov-syntax-highlighter-plain-tag">$vmName = 'ubuntubioniccontainer'

$script = @'
reset -w
$ProgressPreference = "SilentlyContinue"
Install-Module -Name VMware.PowerCLI -AllowClobber -Force | Out-Null
Get-Module -Name VMware* -ListAvailable
'@
$docker = @'
sudo docker run --rm --tty \
    -e TERM=xterm -e LINES=25 -e COLUMNS=135 \
    microsoft/powershell:latest \
    pwsh -C '$($script.Replace("`r",'').Split("`n") -join ';')'
'@

$credVM = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credVM.User, (ConvertTo-SecureString $credVM.Password -AsPlainText -Force))

$sInvoke = @{
    VM = $vmName
    GuestCredential = $cred
    ScriptType = 'bash'
    ScriptText = $ExecutionContext.InvokeCommand.ExpandString($docker)
    NoIPinCert = $true
    Verbose = $true
}
Invoke-VMScriptPlus @sInvoke</pre>


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



<p><strong>Line 4</strong>: The <em>reset -w</em> command is a fix for an <a rel="noreferrer noopener" aria-label="issue (opens in a new tab)" href="https://github.com/moby/moby/issues/33794" target="_blank">issue</a> that Docker seems to have when a <strong>pseudo-terminal</strong> (the <strong>&#8211;tty</strong> parameter) is used. With the <em>reset -w</em> command, the terminal will reread the content of <strong>setupterm</strong> and adjust the lines and columns accordingly.</p>



<p><strong>Line 10</strong>: The <strong>&#8211;tty</strong> parameter allocates a pseudo-terminal to the container.</p>



<p><strong>Line 11</strong>: The -e parameter is used to set a number of environment variables for the container. In this case, specify that the container shall use an <strong>xterm</strong> with <strong>dimensions 25 x 135</strong>.</p>



<p>The changed screen is visible in the output (compare it with the width of the previous output).</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="521" src="https://www.lucd.info/wp-content/uploads/2019/12/crun4-1024x521.png" alt="" class="wp-image-7006" srcset="https://www.lucd.info/wp-content/uploads/2019/12/crun4-1024x521.png 1024w, https://www.lucd.info/wp-content/uploads/2019/12/crun4-300x153.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/crun4-768x391.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/crun4-1536x782.png 1536w, https://www.lucd.info/wp-content/uploads/2019/12/crun4-720x367.png 720w, https://www.lucd.info/wp-content/uploads/2019/12/crun4.png 1756w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



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



<p>With the method we just showed, we will have to incur the overhead of installing the VMware PowerCLI modules on each run, because we throw away the container after we used it.</p>



<p>But there is a solution for this, by using <a href="https://docs.docker.com/engine/reference/commandline/volume_create/" target="_blank" rel="noreferrer noopener" aria-label="docker volumes (opens in a new tab)">docker volumes</a>. They are kept inside the guest OS of the instance and can be attached to each container we run.</p>





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

$script = @'
reset -w
\`$ProgressPreference = "SilentlyContinue"
if(-not (Get-Module -Name VMware.PowerCLI -ListAvailable)){
    Install-Module -Name VMware.PowerCLI -AllowClobber -Force | Out-Null
}
\`$WarningPreference = 'SilentlyContinue'
\`$sConf = @{
    InvalidCertificateAction = 'Ignore'
    ParticipateInCeip = \`$false
    DisplayDeprecationWarnings = \`$false
    Scope = 'User'
    Confirm = \`$false
}
Set-PowerCLIConfiguration @sConf | Out-Null
Connect-VIServer -Server $vcsaName -User $vcsaUser -Password $vcsaPswd | Out-Null
Get-VMHost
Disconnect-VIServer -Server $vcsaName -Confirm:\`$false
'@

$docker = @'
sudo docker volume create PCLIModules &gt; /dev/null
cat &gt; ~/script.ps1 &lt;&lt; EOF
$($script.Split("<code>r"))
EOF
sudo docker run --rm \
    --tty \
    -v /PCLIModules:/root/.local/share/powershell/Modules \
    -v ~:/Scripts \
    -e TERM=xterm -e LINES=25 -e COLUMNS=135 \
    microsoft/powershell:latest \
    pwsh -F /Scripts/script.ps1
rm ~/script.ps1
'@

$vcsaName = 'vcsa.local.lab'
$credVCSA = Get-VICredentialStoreItem -Host $vcsaName
$vcsaUser = $credVCSA.User
$vcsaPswd = $credVCSA.Password

$credVM = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credVM.User, (ConvertTo-SecureString $credVM.Password -AsPlainText -Force))

#$credItem = Get-VICredentialStoreItem -Host $vmName
#$cred = New-Object System.Management.Automation.PSCredential ($credItem.User, (ConvertTo-SecureString $credItem.Password -AsPlainText -Force))
$cred = New-Object System.Management.Automation.PSCredential ($vmUser, (ConvertTo-SecureString $vmPswd -AsPlainText -Force))

$dockerExpanded = $ExecutionContext.InvokeCommand.ExpandString($docker)
$sInvoke = @{
    VM = $vmName
    GuestCredential = $cred
    ScriptType = 'bash'
    ScriptText = $ExecutionContext.InvokeCommand.ExpandString($dockerExpanded)
    NoIPinCert = $true
}
Invoke-VMScriptPlus @sInvoke</pre>



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



<p><strong>Line 5</strong>: We have some 'Inception' going on here, hence the '</code>' sequence before the dollar sign. In <strong>line 50</strong> we substitute the variables in the bash script itself. That way we can provide the script as an inline file in the bash script. But since bash also interprets that back-tick, we have to escape it. The back-tick itself is to avoid variable substitution that is done in line 55.</p>



<p><strong>Line 24</strong>: We create a docker volume named <strong>PCLIModules</strong>. If that volume already exists, the bash script will just continue.</p>



<p><strong>Line 25-27</strong>: Instead of having the PowerShell/PowerCLI script in the <strong>Command</strong> parameter of the <em>pwsh</em> command, we use an inline file. This effectively creates a .ps1 file in the home directory of the user in the guest OS.</p>



<p><strong>Line 30</strong>: The Volume parameter on the docker run command maps the docker volume <strong>PCLIModules</strong> to the folder  <strong>/root/.local/share/powershell/Modules</strong> in the container. With an Install-Module, the modules will be installed in this  <strong>/root/.local/share/powershell/Modules</strong> , and thus be stored on the docker volume <strong>PCLIModules</strong>. This volume can be reused on subsequent runs of the container, eliminating the need to install the VMware PowerCLI modules on each run.</p>



<p><strong>Line 31</strong>: On the docker run command we map the <strong>user's home directory</strong> (~) in the guest OS to a folder, named <strong>Scripts</strong> in the container. </p>



<p><strong>Line 34</strong>: The <em>pwsh</em> command that runs in the container uses the <strong>File</strong> parameter to run the .ps1 file created in lines 25-27.</p>



<p><strong>Line 35</strong>: The .ps1 file that was created before calling the <em>docker run</em> command, is now <strong>removed</strong>.</p>



<p><strong>Line 55</strong>: The second substitution round. In this case we substitute primarily all non-escaped (back-tick) dollar sign in the PowerShell script (at this point in the script present as an inline file in the bash script).</p>



<p>The result when we run the script.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="790" src="https://www.lucd.info/wp-content/uploads/2019/12/crun5-1024x790.png" alt="" class="wp-image-7028" srcset="https://www.lucd.info/wp-content/uploads/2019/12/crun5-1024x790.png 1024w, https://www.lucd.info/wp-content/uploads/2019/12/crun5-300x231.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/crun5-768x593.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/crun5-720x556.png 720w, https://www.lucd.info/wp-content/uploads/2019/12/crun5.png 1274w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>Notice how the <strong>ScriptText</strong> property contains the bash script as it is presented to the Guest OS of the instance. In there the dollar signs are escaped (with a back-slash) to <strong>avoid substitution</strong> by bash.</p>



<h3 class="wp-block-heading">ANSI codes and Existing Scripts</h3>



<p>In this section, we will tackle two subjects.</p>



<ul class="wp-block-list"><li>Eliminate those 'funny' characters in the output</li><li>How to run my existing script, similar to what we did in <a rel="noreferrer noopener" aria-label="Part 4 - Running Script (opens in a new tab)" href="https://www.lucd.info/2019/12/09/cloud-init-part-4-running-scripts/" target="_blank">Part 4 - Running Script</a>s.</li></ul>



<p>As it turns out, these 'funny characters' are <a rel="noreferrer noopener" aria-label="ANSI escape codes (opens in a new tab)" href="https://en.wikipedia.org/wiki/ANSI_escape_code" target="_blank">ANSI escape codes</a>. In the container, we are using a pseudo-terminal, and as a result, the OS in the container sends those ANSI escape codes to position the cursor, change the text colour, but also those '...' (three dots) when PowerShell has to cut short the value of a property.</p>



<p>For now, the only way if found to eliminate those ANSI escape codes, is to find and replace them.</p>



<p>For using your existing scripts in this setup, we adopt the same strategy as we did in <a href="https://www.lucd.info/2019/12/09/cloud-init-part-4-running-scripts/" target="_blank" rel="noreferrer noopener" aria-label="Part 4 - Running Scripts (opens in a new tab)">Part 4 - Running Scripts</a>.</p>



<p>Our existing script, in <strong>Sample-PCLI-Script3.ps1</strong>, looks like this.</p>



<pre class="lang:ps decode:true  ">Get-VMHost |
    Select Name, NumCpu, Version |
    ConvertTo-Html
</pre>



<p>And the script to run all this looks like this</p>



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

$prefix = @'
reset -w
<code>$ProgressPreference = "SilentlyContinue"
if(-not (Get-Module -Name VMware.PowerCLI -ListAvailable)){
    Install-Module -Name VMware.PowerCLI -AllowClobber -Force | Out-Null
}
</code>$WarningPreference = 'SilentlyContinue'
<code>$sConf = @{
    InvalidCertificateAction = 'Ignore'
    ParticipateInCeip = </code>$false
    DisplayDeprecationWarnings = <code>$false
    Scope = 'User'
    Confirm = </code>$false
}
Set-PowerCLIConfiguration @sConf | Out-Null
Connect-VIServer -Server $vcsaName -User $vcsaUser -Password $vcsaPswd | Out-Null
'@
$script = Get-Content -Path .\Sample-PCLI-Script3.ps1
$suffix = @'
Disconnect-VIServer -Server $vcsaName -Confirm:<code>$false
'@

$docker = @'
sudo docker volume create PCLIModules &gt; /dev/null
cat &gt; ~/script.ps1 &lt;&lt; 'EOF'
$($prefix.Split("</code>r"))
$($script -join "<code>n")
$($suffix.Split("</code>r"))
EOF
sudo docker run --rm \
    --tty \
    -v /PCLIModules:/root/.local/share/powershell/Modules \
    -v ~:/Scripts \
    -e TERM=xterm -e LINES=25 -e COLUMNS=135 \
    microsoft/powershell:latest \
    pwsh -F /Scripts/script.ps1 &gt; ~/result.txt
cat ~/result.txt
rm ~/result.txt
rm ~/script.ps1
'@

$vcsaName = 'vcsa.local.lab'
$credVCSA = Get-VICredentialStoreItem -Host $vcsaName
$vcsaUser = $credVCSA.User
$vcsaPswd = $credVCSA.Password

$credVM = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credVM.User, (ConvertTo-SecureString $credVM.Password -AsPlainText -Force))

$dockerExpanded = $ExecutionContext.InvokeCommand.ExpandString($docker)
$sInvoke = @{
    VM = $vmName
    GuestCredential = $cred
    ScriptType = 'bash'
    ScriptText = $ExecutionContext.InvokeCommand.ExpandString($dockerExpanded)
    NoIPinCert = $true
    Verbose = $true
}
$result = Invoke-VMScriptPlus @sInvoke

# Remove ANSI Escape Sequences
($result.ScriptOutput.Split("<code>n") |
    Foreach-Object -Process {
        ($_ -replace "\xe2\x80\xa6","…") -replace "\x1b\[\?[1-9]h|\x1b\[[1-9]n|\x1b=|\x1b\]0;|\x07|\x1b\]0;", ''
    }) -join "</code>n"
</pre>



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



<p><strong>Line 3, 20, 21</strong>: We split the code into three parts. A fixed prefix, the code of our existing script and a suffix.</p>



<p><strong>Line 27-31</strong>: We use the same concept of an inline file as we did in the previous section. The only difference is that we now have three parts instead of one.</p>



<p><strong>Line 64-67</strong>: The ANSI escape sequences are <strong>removed</strong> from the output. The <strong>RegEx expression</strong> does not cover all possible ANSI escape sequences, but it did the job for all the ones I encountered. On the other hand, the RegEx expression can easily be expanded.</p>



<p>The result, which is an HTML file, looks like this.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="357" height="209" src="https://www.lucd.info/wp-content/uploads/2019/12/crun6.png" alt="" class="wp-image-7043" srcset="https://www.lucd.info/wp-content/uploads/2019/12/crun6.png 357w, https://www.lucd.info/wp-content/uploads/2019/12/crun6-300x176.png 300w" sizes="auto, (max-width: 357px) 100vw, 357px" /></figure>



<p>The part where we filter away the ANSI code sequence is also used to translate some well-known characters. Remember the funny characters in the module list?</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="785" height="544" src="https://www.lucd.info/wp-content/uploads/2019/12/mod1.png" alt="" class="wp-image-7050" srcset="https://www.lucd.info/wp-content/uploads/2019/12/mod1.png 785w, https://www.lucd.info/wp-content/uploads/2019/12/mod1-300x208.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/mod1-768x532.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/mod1-720x499.png 720w" sizes="auto, (max-width: 785px) 100vw, 785px" /></figure>



<p>With the first -replace operator in the previous code, we can change this back to the character we are used to. The hex sequence 0xE2, 0x80, 0xA6 that get back is, in fact, the code for the horizontal ellipsis, the 3 horizontal dots PowerShell uses to indicate a cut off value.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="763" height="540" src="https://www.lucd.info/wp-content/uploads/2019/12/mod2.png" alt="" class="wp-image-7051" srcset="https://www.lucd.info/wp-content/uploads/2019/12/mod2.png 763w, https://www.lucd.info/wp-content/uploads/2019/12/mod2-300x212.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/mod2-720x510.png 720w" sizes="auto, (max-width: 763px) 100vw, 763px" /></figure>



<p>There are most probably many other characters or character combinations that do not translate well, but at least we have a method to correct this.</p>



<h3 class="wp-block-heading">InFile &amp; OutFile</h3>



<p>Using inline files and the stdout stream for input and results works in most cases, but it might make things needlessly complex in others. To avoid that, we can make use of the <strong>InFile</strong> and <strong>OutFile</strong> parameters on the <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 parameters allows us to pass one or more files along with the script, and retrieve one or more files back from the environment where the script runs.</p>



<p>If we use <strong>InFile</strong> and <strong>OutFile</strong> together with containers, there is some <a rel="noreferrer noopener" aria-label="Inception (opens in a new tab)" href="https://en.wikipedia.org/wiki/Inception" target="_blank">Inception</a> going on. The following schematic might make this a bit more visible.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="995" height="455" src="https://www.lucd.info/wp-content/uploads/2019/12/inception.png" alt="" class="wp-image-7055" srcset="https://www.lucd.info/wp-content/uploads/2019/12/inception.png 995w, https://www.lucd.info/wp-content/uploads/2019/12/inception-300x137.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/inception-768x351.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/inception-720x329.png 720w" sizes="auto, (max-width: 995px) 100vw, 995px" /></figure>



<p>As you can see, the <strong>InFile</strong> files are stored in the guest OS of the instance. And then through the Volume parameter on the docker command, passed along into the container.</p>



<p>The same goes for the <strong>OutFile</strong> files but in the reverse sense.</p>



<p>We are going to execute an existing script. Note how to make a <strong>small change</strong> in the path of the resulting file. We have to use the path to the <strong>Scripts</strong> folder, which will be mapped through the <strong>Volume</strong> parameter.</p>



<pre class="lang:ps decode:true ">Get-VMHost |
Select Name, NumCpu, Version |
ConvertTo-Html | Out-File -FilePath \Scripts\report.html

Write-Host "Script completed"
</pre>



<p>And this is the script that will run that script in a container. </p>



<pre class="lang:ps decode:true ">$vmName = 'ubuntubioniccontainer'
$oldScript = '.\Sample-PCLI-Script.ps1'
$tempScript = 'script.ps1'

$script = @'
reset -w
<code>$ProgressPreference = "SilentlyContinue"
if(-not (Get-Module -Name VMware.PowerCLI -ListAvailable)){
    Install-Module -Name VMware.PowerCLI -AllowClobber -Force | Out-Null
}
</code>$WarningPreference = 'SilentlyContinue'
<code>$sConf = @{
    InvalidCertificateAction = 'Ignore'
    ParticipateInCeip = </code>$false
    DisplayDeprecationWarnings = <code>$false
    Scope = 'User'
    Confirm = </code>$false
}
Set-PowerCLIConfiguration @sConf | Out-Null
Connect-VIServer -Server $vcsaName -User $vcsaUser -Password $vcsaPswd | Out-Null

####### My Script ######
$(Get-Content -Path $oldScript -Raw)
########################

Disconnect-VIServer -Server $vcsaName -Confirm:<code>$false
'@

$docker = @'
sudo docker volume create PCLIModules &gt; /dev/null
sudo docker run --rm \
    --tty \
    -v /PCLIModules:/root/.local/share/powershell/Modules \
    -v </code>$(pwd):/Scripts \
    -e TERM=xterm -e LINES=25 -e COLUMNS=135 \
    microsoft/powershell:latest \
    pwsh -F /Scripts/$($tempScript)
'@

$vcsaName = 'vcsa.local.lab'
$credVCSA = Get-VICredentialStoreItem -Host $vcsaName
$vcsaUser = $credVCSA.User
$vcsaPswd = $credVCSA.Password

$credVM = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credVM.User, (ConvertTo-SecureString $credVM.Password -AsPlainText -Force))

$tempScript = 'script.ps1'
$ExecutionContext.InvokeCommand.ExpandString($script) | Out-File -LiteralPath $tempScript
$sInvoke = @{
    VM = $vmName
    GuestCredential = $cred
    ScriptType = 'bash'
    ScriptText = $ExecutionContext.InvokeCommand.ExpandString($docker)
    NoIPinCert = $true
    InFile = $tempScript
    OutFile = 'report.html'
}
Invoke-VMScriptPlus @sInvoke

Remove-Item -LiteralPath $tempScript -Confirm:$false

Invoke-Item -Path .\report.html
</pre>



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



<p><strong>Line 2</strong>: The name of our script we want to execute. Note that we had to change the <strong>Path</strong> of the resulting file. This to allow the mapping through the <strong>volume</strong> parameter on the <em>docker run</em> command.</p>



<p><strong>Line 3</strong>: A filename for the script we are going to run in the container. Ultimately this could be replaced by a randomly generated filename.</p>



<p><strong>Line 5-27</strong>: The full script we are sending to the container. It is comprised of the Prefix, our original script and the Suffix. The Prefix and Suffix are the same as we have shown in previous sections. The only difference is that all three parts now come together in one here-string.</p>



<p><strong>Line 23</strong>: We substitute this line with our original script later on with the call to <strong>ExpandString</strong>.</p>



<p><strong>Line 29-38</strong>: The actual bash script, with the docker commands, that we are running inside the Guest OS of the instance.</p>



<p><strong>Line 49</strong>: We expand the $script variable and save the resulting text to a file.</p>



<p><strong>Line 54</strong>: Another call to <strong>ExpandString</strong>. This time to substitute the variables in the <strong>$docker</strong> variable. </p>



<p><strong>Line 56</strong>: The script file we send along to the Guest OS.</p>



<p><strong>Line 57</strong>: The output file we retrieve from the Guest OS.</p>



<p>The script's output looks like this. Notice that it only contains the result from the Write-Host line in our original script.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="719" height="468" src="https://www.lucd.info/wp-content/uploads/2019/12/out5.png" alt="" class="wp-image-7057" srcset="https://www.lucd.info/wp-content/uploads/2019/12/out5.png 719w, https://www.lucd.info/wp-content/uploads/2019/12/out5-300x195.png 300w" sizes="auto, (max-width: 719px) 100vw, 719px" /></figure>



<p>And the result, report.html, is displayed in a web browser through the Invoke-Item cmdlet.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="338" height="203" src="https://www.lucd.info/wp-content/uploads/2019/12/out6.png" alt="" class="wp-image-7058" srcset="https://www.lucd.info/wp-content/uploads/2019/12/out6.png 338w, https://www.lucd.info/wp-content/uploads/2019/12/out6-300x180.png 300w" sizes="auto, (max-width: 338px) 100vw, 338px" /></figure>



<h2 class="wp-block-heading">Image vmware/powerclicore </h2>



<p>Shortly after the new <strong>tdnf</strong> package for <strong>PowerShell</strong> was made available, a new version of the <a href="https://hub.docker.com/r/vmware/powerclicore/" target="_blank" rel="noreferrer noopener" aria-label="vmware/powerclicore (opens in a new tab)">vmware/powerclicore</a> docker image was created.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="595" height="274" src="https://www.lucd.info/wp-content/uploads/2019/12/image.png" alt="" class="wp-image-7076" srcset="https://www.lucd.info/wp-content/uploads/2019/12/image.png 595w, https://www.lucd.info/wp-content/uploads/2019/12/image-300x138.png 300w" sizes="auto, (max-width: 595px) 100vw, 595px" /></figure>



<p>The difference with the <a rel="noreferrer noopener" aria-label="microsoft/powershell (opens in a new tab)" href="https://hub.docker.com/_/microsoft-powershell" target="_blank">microsoft/powershell</a> image is the underlying OS, Photon v3R2 instead of Ubuntu 18.04.</p>



<p>Let's start with a simple script, similar to the one we used earlier.</p>



<pre class="lang:ps decode:true  ">$code = @'
docker run --rm microsoft/powershell:latest pwsh -C 'whoami;Get-Process;pwd'
'@

$credItem = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credItem.User, (ConvertTo-SecureString $credItem.Password -AsPlainText -Force))

$sInvoke = @{
    VM = $vmName
    GuestCredential = $cred
    ScriptType = 'bash'
    ScriptText = $code
    NoIPinCert = $true
    GuestOSType = 'linux'
}
Invoke-VMScriptPlus @sInvoke | select -ExpandProperty ScriptOutput
</pre>



<p>With the <strong>microsoft/powershell image</strong> this produces the following. We see the result of all three commands.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="662" height="254" src="https://www.lucd.info/wp-content/uploads/2019/12/cmp1.png" alt="" class="wp-image-7080" srcset="https://www.lucd.info/wp-content/uploads/2019/12/cmp1.png 662w, https://www.lucd.info/wp-content/uploads/2019/12/cmp1-300x115.png 300w" sizes="auto, (max-width: 662px) 100vw, 662px" /></figure>



<p>On the <strong>vmware/powerclicore</strong> image, the result is a bit different. The <em>whoami</em> output is there, but the output from the other two commands is missing. </p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="530" height="122" src="https://www.lucd.info/wp-content/uploads/2019/12/cmp2-2.png" alt="" class="wp-image-7087" srcset="https://www.lucd.info/wp-content/uploads/2019/12/cmp2-2.png 530w, https://www.lucd.info/wp-content/uploads/2019/12/cmp2-2-300x69.png 300w" sizes="auto, (max-width: 530px) 100vw, 530px" /></figure>



<p>As it turns out, there is an issue when passing commands to pwsh via the Command parameter. When using the File parameter everything seems to work well.</p>



<p>One difference to note, compared with the micorosft/powershell image, the vmware/powerclicore:latest image, and the Photon OS in that image, have minimal terminal types present. As a result, you will need to set the TERM environment variable to <strong>linux</strong> instead of <strong>xterm</strong>.</p>



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



<pre class="lang:sh decode:true  ">docker volume create PCLIModules &gt; /dev/null
docker run --rm \
    --tty \
    -v /PCLIModules:/root/.local/share/powershell/Modules \
    -v `$(pwd):/Scripts \
    -e TERM=linux -e LINES=25 -e COLUMNS=135 \
    vmware/powerclicore:latest \
    pwsh -F /Scripts/script.ps1</pre>



<h2 class="wp-block-heading">Image  mcr.microsoft.com/powershell:preview </h2>



<p>Note that there is also an image for the latest PS v7 Preview available. If you want to test/develop your scripts in this PS v7 Preview, this is a simple and easy method to do it.</p>



<p>All previous methods discussed in this post work against this Preview.</p>



<p>Enjoy!</p>



<p>p</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.lucd.info/2019/12/11/cloud-init-part-5-running-containers/feed/</wfw:commentRss>
			<slash:comments>1</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>
