Jekyll2023-04-18T18:50:44+00:00https://techdrabble.com/feed.xmlTechdrabbleMobility & AutomationRyan ButlerInvoking vSAN Re-layout with PowerCLI2022-11-07T00:00:00+00:002022-11-07T00:00:00+00:00https://techdrabble.com/vmware/RelayoutPowerCLI<p>Skyline Health was reporting “vSAN object format health” alerts after upgrading a bunch of clusters to 7.x. Each cluster requires a “re-layout” to clear the alert. Kicking off the process is fairly straight forward just requires a lot of clicks.</p>
<p><img src="/assets/images/content/relayout/objecthealth.png" alt="main" class="align-center" /></p>
<p>I figured I could do this with PowerCLI but struggled with finding an example anywhere. After a good amount of time of trial and error I have the below working code to perform the process.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#connect to vcenter</span><span class="w">
</span><span class="n">connect-viserver</span><span class="w"> </span><span class="nt">-server</span><span class="w"> </span><span class="s2">"myvsphere.domain.com"</span><span class="w">
</span><span class="c">#Get view #https://developer.vmware.com/docs/powercli/latest/vmware.vimautomation.storage/commands/get-vsanview/#Default</span><span class="w">
</span><span class="c">#Run Get-VSANView to get full list</span><span class="w">
</span><span class="nv">$vsanobj</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-VSANView</span><span class="w"> </span><span class="nt">-Id</span><span class="w"> </span><span class="s2">"VsanObjectSystem-vsan-cluster-object-system"</span><span class="w">
</span><span class="c">#get clusters to perform layout</span><span class="w">
</span><span class="nv">$clusters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">get-cluster</span><span class="w">
</span><span class="nx">foreach</span><span class="p">(</span><span class="nv">$cluster</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$clusters</span><span class="p">)</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="n">write-host</span><span class="w"> </span><span class="nv">$cluster</span><span class="o">.</span><span class="nf">name</span><span class="w">
</span><span class="c">#invoke re-layout (async)</span><span class="w">
</span><span class="nv">$vsanobj</span><span class="o">.</span><span class="nf">RelayoutObjects</span><span class="p">(</span><span class="nv">$cluster</span><span class="o">.</span><span class="nf">id</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>Ryan ButlerSkyline Health was reporting “vSAN object format health” alerts after upgrading a bunch of clusters to 7.x. Each cluster requires a “re-layout” to clear the alert. Kicking off the process is fairly straight forward just requires a lot of clicks.Using vSphere Guest Customization Attributes with Windows and Terraform2021-12-07T00:00:00+00:002021-12-07T00:00:00+00:00https://techdrabble.com/hashicorp/Terraformvspherewindows<p>VMware has capability to leverage <strong>GuestInfo</strong> attributes which allow passing of <strong>Metadata</strong> and <strong>Userdata</strong> blobs that can be used for <a href="https://github.com/canonical/cloud-init/blob/main/doc/rtd/topics/datasources/vmware.rst">Cloud-init</a>. In very simple terms, once the machine boots it looks up these these values via VM tools and executes the declared YAML file using Cloud-Init. This post covers leveraging these attributes for Windows to bootstrap the operating system with a custom PowerShell script.</p>
<p><img src="/assets/images/content/terraform-win/main.png" alt="main" class="align-center" /></p>
<p>If you haven’t started using Cloud-Init on your Linux templates with vSphere now is the time. Unfortunately, this leaves Windows out in the cold since Cloud-Init is Linux only. While investigating a solution with Windows I did find <a href="https://cloudbase-init.readthedocs.io/en/latest/">Cloudbase-init</a> which looked promising at first, but soon found it didn’t have any native capability of joining a Windows domain and had a lot of strange timing issues. I decided to dump Cloudbase-init and figure something else out.</p>
<h2 id="current-process">Current Process</h2>
<p>If you ever tried to use the <strong>run-once</strong> portion of a Windows customization you know it’s extremely limited in what you can run. Most of the time you can run a few basic commands or maybe a PowerShell script that is already included within the image but doesn’t offer a lot. Since these commands are run from the Sysprep process the commands become stored in the <code class="language-plaintext highlighter-rouge">unattend.xml</code> file stored in <code class="language-plaintext highlighter-rouge">C:\Windows\Panther</code> which could contain secrets being passed into any external command (e.g. domain join credentials).</p>
<h2 id="solution">Solution</h2>
<p>To prevent the need for an external script or handling of the unattend.xml file I decided to leverage the metadata attribute to store my PowerShell script. The script gets rendered via Terraform to allow variable substitution then converted to base64 and finally being set in the attribute. The main components of the process:</p>
<p class="notice--success">Code is available here: https://github.com/ryancbutler/terraform-vsphere-windows-userdata-vm</p>
<h3 id="powershell-template">PowerShell Template</h3>
<p>The <code class="language-plaintext highlighter-rouge">bootstrap.ps1</code> script uses variable substitution to render the script with any needed data for the VM such as passwords and domain info. Anything with <code class="language-plaintext highlighter-rouge">{var}</code> gets replaced via Terraform.</p>
<p>For example, the <code class="language-plaintext highlighter-rouge">bootstrap.ps1</code> template contains the following code:</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#Add to domain</span><span class="w">
</span><span class="n">write-host</span><span class="w"> </span><span class="s2">"Add to domain"</span><span class="w">
</span><span class="nv">$domain</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"</span><span class="nv">${addomain}</span><span class="s2">"</span><span class="w">
</span><span class="nv">$password</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"</span><span class="nv">${adpass}</span><span class="s2">"</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-SecureString</span><span class="w"> </span><span class="nt">-asPlainText</span><span class="w"> </span><span class="nt">-Force</span><span class="w">
</span><span class="nv">$username</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"</span><span class="nv">$domain</span><span class="s2">\</span><span class="nv">${aduser}</span><span class="s2">"</span><span class="w">
</span><span class="nv">$credential</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-Object</span><span class="w"> </span><span class="nx">System.Management.Automation.PSCredential</span><span class="p">(</span><span class="nv">$username</span><span class="p">,</span><span class="w"> </span><span class="nv">$password</span><span class="p">)</span><span class="w">
</span><span class="n">Add-Computer</span><span class="w"> </span><span class="nt">-DomainName</span><span class="w"> </span><span class="nv">$domain</span><span class="w"> </span><span class="nt">-OUPath</span><span class="w"> </span><span class="s2">"</span><span class="nv">${adou}</span><span class="s2">"</span><span class="w"> </span><span class="nt">-Credential</span><span class="w"> </span><span class="nv">$credential</span><span class="w">
</span></code></pre></div></div>
<p>Then Terraform renders the file with the <strong>data</strong> block within <code class="language-plaintext highlighter-rouge">main.tf</code>:</p>
<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">data</span> <span class="s2">"template_file"</span> <span class="s2">"init"</span> <span class="p">{</span>
<span class="nx">template</span> <span class="p">=</span> <span class="nx">file</span><span class="p">(</span><span class="s2">"</span><span class="k">${</span><span class="nx">path</span><span class="p">.</span><span class="k">module}</span><span class="s2">/templates/bootstrap.ps1"</span><span class="p">)</span>
<span class="nx">vars</span> <span class="p">=</span> <span class="p">{</span>
<span class="nx">adpass</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">ADPass</span>
<span class="nx">adou</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">ADOU</span>
<span class="nx">aduser</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">ADUser</span>
<span class="nx">addomain</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">ADDomain</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="setting-attribute">Setting Attribute</h3>
<p>Now that the script is rendered the VM attribute needs to be set.</p>
<p>In the Terraform code it’s done with the <code class="language-plaintext highlighter-rouge">extra_config</code> block in the <code class="language-plaintext highlighter-rouge">vsphere_virtual_machine</code> resource within <code class="language-plaintext highlighter-rouge">main.tf</code>:</p>
<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">extra_config</span> <span class="err">=</span> <span class="p">{</span>
<span class="s2">"guestinfo.userdata"</span> <span class="p">=</span> <span class="nx">base64encode</span><span class="p">(</span><span class="k">data</span><span class="p">.</span><span class="nx">template_file</span><span class="p">.</span><span class="nx">init</span><span class="p">.</span><span class="nx">rendered</span><span class="p">)</span>
<span class="s2">"guestinfo.userdata.encoding"</span> <span class="p">=</span> <span class="s2">"base64"</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="running-the-code">Running the code</h3>
<p>With the attribute set, we need to execute it on boot. To do this I’m using the run-once command from the Windows customization spec to pull down the attribute blob using the <strong>rpctool.exe</strong> that comes with VMware tools (no extra install needed), convert the base64 to PowerShell then execute the script which is being set in the <code class="language-plaintext highlighter-rouge">vsphere_virtual_machine</code> resource as you can see here in the <code class="language-plaintext highlighter-rouge">run_once_command_list</code> within <code class="language-plaintext highlighter-rouge">main.tf</code>:</p>
<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">clone</span> <span class="p">{</span>
<span class="nx">template_uuid</span> <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">vsphere_virtual_machine</span><span class="p">.</span><span class="nx">template</span><span class="p">.</span><span class="nx">id</span>
<span class="nx">linked_clone</span> <span class="p">=</span> <span class="kc">false</span>
<span class="nx">customize</span> <span class="p">{</span>
<span class="nx">windows_options</span> <span class="p">{</span>
<span class="nx">computer_name</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">vm_name</span>
<span class="nx">admin_password</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">vm_password</span> <span class="p">==</span> <span class="s2">""</span> <span class="err">?</span> <span class="nx">random_password</span><span class="p">.</span><span class="nx">password</span><span class="p">.</span><span class="nx">result</span> <span class="err">:</span> <span class="kd">var</span><span class="p">.</span><span class="nx">vm_password</span>
<span class="nx">workgroup</span> <span class="p">=</span> <span class="s2">"WORKGROUP"</span>
<span class="nx">auto_logon</span> <span class="p">=</span> <span class="kc">true</span>
<span class="nx">auto_logon_count</span> <span class="p">=</span> <span class="mi">1</span>
<span class="nx">time_zone</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">vm_timezone</span>
<span class="nx">run_once_command_list</span> <span class="p">=</span> <span class="p">[</span>
<span class="s2">"powershell </span><span class="se">\"</span><span class="s2">cd </span><span class="se">\"</span><span class="err">$</span><span class="s2">env:ProgramFiles</span><span class="se">\\</span><span class="s2">VMware</span><span class="se">\\</span><span class="s2">VMware~1</span><span class="se">\"</span><span class="s2">;[System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String(</span><span class="err">$</span><span class="s2">(.</span><span class="se">\\</span><span class="s2">rpctool.exe </span><span class="se">\\\"</span><span class="s2">info-get guestinfo.userdata</span><span class="se">\\\"</span><span class="s2">)))|out-file C:</span><span class="se">\\</span><span class="s2">bootstrap.ps1</span><span class="se">\"</span><span class="s2">"</span><span class="p">,</span>
<span class="s2">"cmd.exe /C Powershell.exe -ExecutionPolicy Bypass -File C:</span><span class="se">\\</span><span class="s2">bootstrap.ps1"</span>
<span class="p">]</span>
<span class="p">}</span>
<span class="nx">network_interface</span> <span class="p">{</span>
<span class="nx">ipv4_address</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">vm_ip</span>
<span class="nx">ipv4_netmask</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">vm_netmask</span>
<span class="nx">dns_server_list</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">vm_dns_servers</span>
<span class="nx">dns_domain</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">vm_dns_domain</span>
<span class="p">}</span>
<span class="nx">ipv4_gateway</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">vm_gateway</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p class="notice--info">It’s important to have <code class="language-plaintext highlighter-rouge">auto_logon_count</code> set to <code class="language-plaintext highlighter-rouge">1</code> so the VM logs in after the Sysprep process to kickoff the command.</p>
<h3 id="cleaning-up">Cleaning up</h3>
<p>In order not to leak secrets in either the attribute or script I do the following as part of my bootstrap process in the PowerShell script.</p>
<ul>
<li>Clear out the metadata attribute with rpctool so the base64 blob is deleted (notice two spaces at end)</li>
<li>Delete the created PS1 file</li>
</ul>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#Clear userdata</span><span class="w">
</span><span class="n">write-host</span><span class="w"> </span><span class="s2">"Clear userdata"</span><span class="w">
</span><span class="n">set-location</span><span class="w"> </span><span class="s2">"</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">ProgramFiles</span><span class="s2">\VMware\VMware~1\"</span><span class="w">
</span><span class="o">.</span><span class="n">\rpctool.exe</span><span class="w"> </span><span class="s2">"info-set guestinfo.userdata "</span><span class="w">
</span><span class="c">#Remove Script</span><span class="w">
</span><span class="n">write-host</span><span class="w"> </span><span class="s2">"Remove Script"</span><span class="w">
</span><span class="n">Remove-Item</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s2">"C:\bootstrap.ps1"</span><span class="w"> </span><span class="nt">-Force</span><span class="w">
</span></code></pre></div></div>Ryan ButlerVMware has capability to leverage GuestInfo attributes which allow passing of Metadata and Userdata blobs that can be used for Cloud-init. In very simple terms, once the machine boots it looks up these these values via VM tools and executes the declared YAML file using Cloud-Init. This post covers leveraging these attributes for Windows to bootstrap the operating system with a custom PowerShell script.Citrix Converge 20202020-11-08T00:00:00+00:002020-11-08T00:00:00+00:00https://techdrabble.com/citrix/Converge-2020<p>Use an Azure DevOps pipeline to deploy a Windows VDA using HashiCorp Packer. This produces a gold image ready for Citrix Virtual Apps and Desktops.</p>
<p>This repo is part of my <strong><a href="https://www.citrix.com/blogs/2020/09/02/announcing-citrix-converge-2020-developing-the-future-of-work-together/">Citrix Converge 2020</a></strong> presentation</p>
<p><a href="https://www.youtube.com/watch?v=sKlx9HqI5Rc">Full Video Presentation</a></p>
<!-- Courtesy of embedresponsively.com //-->
<div class="responsive-video-container">
<iframe src="https://www.youtube-nocookie.com/embed/sKlx9HqI5Rc" frameborder="0" webkitallowfullscreen="" mozallowfullscreen="" allowfullscreen=""></iframe>
</div>
<h2 id="prerequisite">Prerequisite</h2>
<p>You’ll need the following pre-reqs before getting started.</p>
<h3 id="azure-devops">Azure DevOps</h3>
<ul>
<li>Account (Can be free version!)</li>
<li>Access to an existing or new Azure Devops project</li>
</ul>
<h3 id="azure">Azure</h3>
<ul>
<li>Service Principal information</li>
</ul>
<p><code class="language-plaintext highlighter-rouge">az ad sp create-for-rbac --name Converge2020</code></p>
<ul>
<li>Storage Account
<ul>
<li>Document Storage account access key</li>
</ul>
</li>
<li>Container within Storage Account for media installation files
<ul>
<li>Download and upload <a href="https://www.citrix.com/downloads/citrix-cloud/product-software/xenapp-and-xendesktop-service.html">Citrix VDA</a> <em>VDAServerSetup_2006.exe</em></li>
<li>Download and upload <a href="https://support.citrix.com/article/CTX224676">Citrix Optimizer</a> (<em>CitrixOptimizer.zip</em>)</li>
</ul>
</li>
<li>Container within Storage Account for built gold images to reside</li>
</ul>
<h2 id="steps">Steps</h2>
<p>Follow the below process to build a Windows 2019 VDA</p>
<ol>
<li>From Azure DevOps. Select Repos</li>
<li>Import a Git repository
<ul>
<li>https://github.com/ryancbutler/converge-2020</li>
</ul>
</li>
<li>From Pipelines create new
<ul>
<li>Select “Azure Repos Git”</li>
<li>Select imported repo (eg Converge2020)</li>
<li>Existing Azure Pipelines YAML file</li>
<li>Select <code class="language-plaintext highlighter-rouge">/pipeline.yml</code></li>
<li>Select Variables and add the below table</li>
</ul>
</li>
<li>Run pipeline</li>
</ol>
<h3 id="pipeline-variables">Pipeline Variables</h3>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>client_id</td>
<td>SPN Client ID (appid)</td>
<td>Secret</td>
</tr>
<tr>
<td>client_secret</td>
<td>SPN Client Secret (password)</td>
<td>Secret</td>
</tr>
<tr>
<td>goldimage_container</td>
<td>Storage Account Container that will contain gold image</td>
<td>goldimages</td>
</tr>
<tr>
<td>location</td>
<td>Location to deploy temp VM</td>
<td>centralus</td>
</tr>
<tr>
<td>media_container</td>
<td>Storage account container that contatins install media</td>
<td>media</td>
</tr>
<tr>
<td>rg_name</td>
<td>Resource group where storage account is located</td>
<td>rg-converge-central</td>
</tr>
<tr>
<td>sa_key</td>
<td>Storage Account Key</td>
<td>Secret</td>
</tr>
<tr>
<td>storage_account</td>
<td>Storage Account Name</td>
<td>mysupercoolpackerstorage</td>
</tr>
<tr>
<td>subid</td>
<td>Azure Subscription ID</td>
<td>Secret</td>
</tr>
<tr>
<td>tenantid</td>
<td>SPN Tenant ID (tenant)</td>
<td>Secret</td>
</tr>
<tr>
<td>vda</td>
<td>VDA File Name</td>
<td>VDAServerSetup_2006.exe</td>
</tr>
<tr>
<td>vdacontrollers</td>
<td>Comma seperated list of cloud connectors</td>
<td>CTX-CC01.LAB.LOCAL</td>
</tr>
</tbody>
</table>Ryan ButlerUse an Azure DevOps pipeline to deploy a Windows VDA using HashiCorp Packer. This produces a gold image ready for Citrix Virtual Apps and Desktops.Use Packer and Vagrant to Create a Local MDT Lab2020-09-17T00:00:00+00:002020-09-17T00:00:00+00:00https://techdrabble.com/hashicorp/mdt-lab-vagrant<p>Uses Packer and Vagrant to create a local MDT lab leveraging <a href="https://github.com/haavarstein/Automation-Framework-Community-Edition">Automation Framework Community Edition</a> running on Virtual Box.</p>
<p class="notice--info">Located here: <a href="https://github.com/ryancbutler/mdt-lab-vagrant">https://github.com/ryancbutler/mdt-lab-vagrant</a></p>
<p><img src="/assets/images/content/mdt-lab-vagrant/design.png" alt="design" /></p>
<h2 id="creates-the-following">Creates the following</h2>
<ul>
<li>2019 Datacenter image created via Packer</li>
<li>2019 Domain controller via Vagrant</li>
<li>2019 MDT server via Vagrant
<ul>
<li>DHCP server and scope</li>
<li>AFCE installed</li>
<li>WDS (Windows Deployment Services) installed, patched and configured</li>
</ul>
</li>
</ul>
<h2 id="big-thanks">Big thanks</h2>
<ul>
<li><a href="https://github.com/haavarstein">Trond Eirik Haavarstein</a> for dedicating a massive amount of time for making MDT super easy to get started with. Check out the paid version of <a href="https://xenapptraining.com">Automation Framework</a> for even more automation goodness.
<ul>
<li>https://github.com/haavarstein/Automation-Framework-Community-Edition/</li>
</ul>
</li>
<li><a href="https://github.com/rgl/">Rui Lopes</a> for so many awesome Packer and Vagrant scripts used for this.
<ul>
<li>https://github.com/rgl/windows-vagrant</li>
<li>https://github.com/rgl/windows-domain-controller-vagrant</li>
</ul>
</li>
</ul>
<h1 id="pre-reqs">Pre-Reqs</h1>
<p>The following needs to be done on the local PC:</p>
<ul>
<li>Clone <code class="language-plaintext highlighter-rouge">git clone https://github.com/ryancbutler/mdt-lab-vagrant.git</code> or download <code class="language-plaintext highlighter-rouge">https://github.com/ryancbutler/mdt-lab-vagrant/archive/master.zip</code> this repo</li>
<li>Download and copy a <a href="https://www.microsoft.com/en-us/cloud-platform/windows-server-trial">Windows 2019 Eval ISO</a> to repo dir (eg “C:\mdt-lab-vagrant")</li>
<li><a href="https://www.virtualbox.org/wiki/Downloads">Virtualbox</a> installed</li>
<li><a href="https://www.vagrantup.com/downloads">Vagrant</a> installed</li>
<li><a href="https://www.packer.io/downloads">Packer</a> installed</li>
</ul>
<p><strong>Note:</strong> All these can be installed with <em><a href="https://chocolatey.org/">choco</a></em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#Install choco if needed
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
choco install -y virtualbox packer vagrant
</code></pre></div></div>
<h2 id="vagrant-plugins">Vagrant Plugins</h2>
<ul>
<li><a href="https://github.com/rgl/packer-provisioner-windows-update">vagrant-windows-sysprep</a></li>
</ul>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vagrant plugin install vagrant-windows-sysprep
</code></pre></div></div>
<h2 id="packer-plugins">Packer Plugins</h2>
<ul>
<li><a href="https://github.com/rgl/packer-provisioner-windows-update">packer-provisioner-windows-update</a></li>
</ul>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>choco install packer-provisioner-windows-update
</code></pre></div></div>
<h1 id="build-windows-2019-image">Build Windows 2019 Image</h1>
<p>First we need to build the 2019 Datacenter image using Packer. This can take awhile.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cd packer
packer build -only=windows-2019-amd64-virtualbox windows-2019.json
#When completed
vagrant box add --name "windows-2019-amd64" -f windows-2019-amd64-virtualbox.box
</code></pre></div></div>
<h1 id="virtual-box-setup">Virtual Box Setup</h1>
<p>While the image builds we can adjust our virtual box network and create our temp machine</p>
<h2 id="network">Network</h2>
<p>DHCP must be <strong>disabled</strong> on the <em>192.168.56.0/24</em> Virtual Box host-only network</p>
<p><img src="/assets/images/content/mdt-lab-vagrant/network1.png" alt="network1" /></p>
<p><img src="/assets/images/content/mdt-lab-vagrant/network2.png" alt="network2" /></p>
<h2 id="test-vm">Test VM</h2>
<p>This VM will be used to actually test MDT with</p>
<ul>
<li>Create a new VM</li>
</ul>
<p><img src="/assets/images/content/mdt-lab-vagrant/vm1.png" alt="vm1" /></p>
<ul>
<li>Adjust memory anything you can spare. 2GB should work.</li>
<li>Create a new virtual disk with VDI (format doesn’t really matter)
<ul>
<li>Dynamically allocated</li>
<li>Suggest 40gb plus for size</li>
</ul>
</li>
<li>After machine is created select <em>Settings</em> and <em>Network</em> on the left
<ul>
<li>Set adapter 1 to host-only</li>
</ul>
<p><img src="/assets/images/content/mdt-lab-vagrant/vm2.png" alt="vm2" /></p>
<ul>
<li>Set adapter 2 to NAT (this will allow internet access)</li>
</ul>
<p><img src="/assets/images/content/mdt-lab-vagrant/vm3.png" alt="vm3" /></p>
</li>
</ul>
<h1 id="run-vagrant">Run Vagrant</h1>
<p>This will deploy the environment using the Packer image created earlier</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#If still in Packer dir
cd ..
#From repo root
#may need to run as admin
vagrant up
</code></pre></div></div>
<h1 id="connecting">Connecting</h1>
<ul>
<li>Once the Vagrant deployment is complete you can access the MDT server with <code class="language-plaintext highlighter-rouge">vagrant rdp mdt</code> or <code class="language-plaintext highlighter-rouge">vagrant powershell mdt</code></li>
<li><strong>mylab\vagrant</strong> can be used for authentication
<ul>
<li>Configured as Domain and Enterprise admin</li>
</ul>
</li>
<li>Local <strong>administrator</strong> password is set to <strong>P@ssw0rd</strong></li>
</ul>
<h1 id="test-mdt">Test MDT</h1>
<ul>
<li>Boot your test VM and</li>
<li>Cancel the intial boot disk screen (only shows once)</li>
<li>Press <strong>F12</strong> at the bios screen</li>
<li>Select <em>l</em> to boot from lan</li>
<li>PXE process should pickup from here</li>
<li>Happy labbing</li>
</ul>
<h1 id="power-environment-down">Power environment down</h1>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vagrant halt
</code></pre></div></div>
<h1 id="destroy-environment">Destroy Environment</h1>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vagrant destroy -f
</code></pre></div></div>Ryan ButlerUses Packer and Vagrant to create a local MDT lab leveraging Automation Framework Community Edition running on Virtual Box.Build a Vagrant TFE Instance2020-07-18T00:00:00+00:002020-07-18T00:00:00+00:00https://techdrabble.com/hashicorp/tfe-vagrant<p>Builds a Vagrant TFE instance for development or testing using Virtualbox and VMware Workstation\Fusion.</p>
<p class="notice--info">Located here: <a href="https://github.com/ryancbutler/TFE-Vagrant">https://github.com/ryancbutler/TFE-Vagrant</a></p>
<p>A license will be required.</p>
<ul>
<li>Request a trial https://www.hashicorp.com/go/terraform-enterprise-trial/</li>
</ul>
<h1 id="ports">Ports</h1>
<p>Uses the following local ports:</p>
<ul>
<li>80</li>
<li>443</li>
<li>8800</li>
</ul>
<h1 id="to-run-install-manually">To Run Install Manually</h1>
<ul>
<li>Go to directory and launch <code class="language-plaintext highlighter-rouge">vagrant up</code></li>
<li>Instance will upgrade and install TFE</li>
<li>Once completed check <code class="language-plaintext highlighter-rouge">http://<this_server_address>:8800</code> to continue</li>
</ul>
<h1 id="to-run-install-automatically">To Run Install Automatically</h1>
<p>Installs TFE with a mounted disk configuration fully automated</p>
<ul>
<li>Copy TFE license to <strong>config</strong> directory</li>
<li>Rename license to <strong>license.rli</strong></li>
<li>Go to directory and launch <code class="language-plaintext highlighter-rouge">vagrant up</code></li>
<li>Instance will upgrade and install TFE</li>
<li>Once completed check <code class="language-plaintext highlighter-rouge">http://<this_server_address>:8800</code> to continue</li>
<li>Will also output initial user creation URL</li>
</ul>
<p>Note: Console password is located in <strong>config\replicated.conf</strong></p>
<h1 id="https-tunnel-with-ngrok">HTTPS Tunnel with NGROK</h1>
<p>Allows external internet access to a local instance for version control system (VCS) integration. Requires a token from https://dashboard.ngrok.com/ (FREE ACCOUNT!!)</p>
<h2 id="steps">Steps</h2>
<p>Access vagrant box (<code class="language-plaintext highlighter-rouge">vagrant ssh</code>)</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo apt install unzip -y
#https://ngrok.com/download
wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip
unzip ngrok-stable-linux-amd64.zip
./ngrok authtoken MYTOKEN
./ngrok http 443
</code></pre></div></div>Ryan ButlerBuilds a Vagrant TFE instance for development or testing using Virtualbox and VMware Workstation\Fusion.Getting Started with the Citrix App Layering Cloud API Service2019-10-29T00:00:00+00:002019-10-29T00:00:00+00:00https://techdrabble.com/citrix/app%20layering/getting-started-with-the-citrix-app-layering-cloud-api-service<p>Last week Citrix <a href="https://www.citrix.com/blogs/2019/10/22/improve-job-throughput-with-citrix-app-layering-automation-enhancements/">released</a> a “Tech Preview” of the App Layering API. The API as a starting point has some basic functionality that customers have been wanting for a long time such as querying layers and resources. The Citrix documentation provides a JSON spec but I also imported into <a href="https://app.swaggerhub.com/apis-docs/ryan_c_butler/app-layering_layering_services_api">Swagger</a> to quickly see what it offers. The API is very different than other Citrix SDKs\APIS in that it requires an on-premises appliance that communicates to the ELM appliance while the API calls are made over the internet. This post will go over the basics of getting started and provide a script that provides some of the functionality.</p>
<p><img src="/assets/images/content/layeringapi/main.png" alt="main" /></p>
<h2 id="agent-appliance-configuration">Agent Appliance Configuration</h2>
<p>In order to gain access to the appliance and API, <a href="https://developer.cloud.com/applayering/overview">you will need</a> both a Citrix Cloud account ID and a submitted <a href="https://podio.com/webforms/23191783/1654530">form</a> requesting access. Once the request is approved you will receive instructions on how to install the appliance and provide the needed Cloud account information. Once the appliance is registered with Citrix cloud make note of the registration ID to be used later.</p>
<p><img src="/assets//images/content/layeringapi/agentid.png" alt="agentid" /></p>
<h2 id="registering-elm-appliance-with-agent">Registering ELM Appliance with Agent</h2>
<p>Once the appliance is registered with Citrix Cloud the appliance then needs to register the on-prem ELM appliance. In order to register the appliance you will need to identify your endpoint URL. There are currently three.</p>
<ul>
<li><a href="https://us.workstreams.cloud.com/services/Citrix.AppLayering/master/LayeringServices">https://us.workstreams.cloud.com/services/Citrix.AppLayering/master/LayeringServices</a></li>
<li><a href="https://eu.workstreams.cloud.com/services/Citrix.AppLayering/master/LayeringServices">https://eu.workstreams.cloud.com/services/Citrix.AppLayering/master/LayeringServices</a></li>
<li><a href="https://ap-s.workstreams.cloud.com/services/Citrix.AppLayering/master/LayeringServices">https://ap-s.workstreams.cloud.com/services/Citrix.AppLayering/master/LayeringServices</a></li>
</ul>
<p>Like all the Citrix Cloud APIs a token will need to be requested to do any further commands. In order to request a token your cloud id and secret will be used. Again, the below code will be provided in a single script at the end of this post.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Get-BearerToken</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="w"> </span><span class="nv">$customerId</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="w"> </span><span class="nv">$clientId</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="w"> </span><span class="nv">$secret</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="nv">$requestUri</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"https://trust.citrixworkspacesapi.net/</span><span class="nv">$customerId</span><span class="s2">/tokens/clients"</span><span class="w">
</span><span class="nv">$headers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="s2">"Content-Type"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"application/json"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="nv">$auth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="s2">"ClientId"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$clientId</span><span class="w">
</span><span class="s2">"ClientSecret"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$secret</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="nv">$response</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-RestMethod</span><span class="w"> </span><span class="nt">-Uri</span><span class="w"> </span><span class="nv">$requestUri</span><span class="w"> </span><span class="nt">-Method</span><span class="w"> </span><span class="nx">POST</span><span class="w"> </span><span class="nt">-Headers</span><span class="w"> </span><span class="nv">$headers</span><span class="w"> </span><span class="nt">-Body</span><span class="w"> </span><span class="p">(</span><span class="n">ConvertTo-Json</span><span class="w"> </span><span class="nv">$auth</span><span class="p">)</span><span class="w">
</span><span class="kr">return</span><span class="w"> </span><span class="nv">$response</span><span class="o">.</span><span class="nf">token</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p><em>This function will return a token to be used for all further commands.</em></p>
<p>Now that we have our token we can register our ELM appliance with the API agent. In addition to the token you will also need the agent ID that we documented earlier, the IP or hostname of the ELM appliance and admin credentials.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Invoke-ApplianceReg</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="w"> </span><span class="nv">$customerId</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="w"> </span><span class="nv">$token</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="w"> </span><span class="nv">$url</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="nv">$requestUri</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$url</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"/resources/"</span><span class="w">
</span><span class="nv">$headers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="s2">"Content-Type"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"application/json; charset=utf-8"</span><span class="w">
</span><span class="s2">"Authorization"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"CWSAuth bearer=</span><span class="nv">$token</span><span class="s2">"</span><span class="w">
</span><span class="s2">"Accept"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"application/json;version=preview"</span><span class="w">
</span><span class="s2">"Citrix-CustomerId"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$customerId</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="nv">$body</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="s2">"name"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Butler's Appliance"</span><span class="w">
</span><span class="s2">"description"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"A description of my App Layering appliance"</span><span class="w">
</span><span class="s2">"type"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Appliance"</span><span class="w">
</span><span class="s2">"resourceLocationId"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"1333b292...."</span><span class="w">
</span><span class="s2">"applianceConnection"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="s2">"address"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"192.168.2.50"</span><span class="w">
</span><span class="s2">"auth"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="s2">"credentials"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="s2">"username"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"administrator"</span><span class="w">
</span><span class="s2">"password"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"mypassword"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="nv">$response</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-RestMethod</span><span class="w"> </span><span class="nt">-Uri</span><span class="w"> </span><span class="nv">$requestUri</span><span class="w"> </span><span class="nt">-Method</span><span class="w"> </span><span class="nx">POST</span><span class="w"> </span><span class="nt">-Headers</span><span class="w"> </span><span class="nv">$headers</span><span class="w"> </span><span class="nt">-Body</span><span class="w"> </span><span class="p">(</span><span class="nv">$body</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Json</span><span class="w"> </span><span class="nt">-Depth</span><span class="w"> </span><span class="nx">5</span><span class="p">)</span><span class="w">
</span><span class="kr">return</span><span class="w"> </span><span class="nv">$response</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Make sure to adjust the script to reflect your information. Once the script is run you should receive information about your registration.</p>
<p><img src="/assets/images/content/layeringapi/appliance.png" alt="appliance" /></p>
<h2 id="sync-appliance">Sync Appliance</h2>
<p>Once the ELM is registered it must be synced with the agent in order to receive the needed metadata such as layers and appliance info.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Invoke-ApplianceSync</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="w"> </span><span class="nv">$customerId</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="w"> </span><span class="nv">$token</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="w"> </span><span class="nv">$url</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="w"> </span><span class="nv">$ApplianceID</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="nv">$headers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="s2">"Content-Type"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"application/json; charset=utf-8"</span><span class="w">
</span><span class="s2">"Authorization"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"CWSAuth bearer=</span><span class="nv">$token</span><span class="s2">"</span><span class="w">
</span><span class="s2">"Accept"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"application/json;version=preview"</span><span class="w">
</span><span class="s2">"Citrix-CustomerId"</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$customerId</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="nv">$requestUri</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$url</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"/resources/"</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nv">$ApplianceID</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"/</span><span class="se">`$</span><span class="s2">sync?async=true"</span><span class="w">
</span><span class="nv">$response</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-WebRequest</span><span class="w"> </span><span class="nt">-Uri</span><span class="w"> </span><span class="nv">$requestUri</span><span class="w"> </span><span class="nt">-Method</span><span class="w"> </span><span class="nx">POST</span><span class="w"> </span><span class="nt">-Headers</span><span class="w"> </span><span class="nv">$headers</span><span class="w">
</span><span class="kr">return</span><span class="w"> </span><span class="nv">$response</span><span class="o">.</span><span class="nf">headers</span><span class="o">.</span><span class="nf">Location</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>This will return a Job URL that can be queried to see the progress. After the job is completed you will notice further info available and you are ready to start automating!</p>
<p class="notice--warning"><strong>Note: There is no automatic sync.</strong> If layers are updated on the ELM a sync should be run to update the API agent</p>
<p><img src="/assets/images/content/layeringapi/appliancemore.png" alt="appliancemore" /></p>
<h2 id="the-script">The Script</h2>
<p>The script is available <a href="https://github.com/ryancbutler/Citrix/tree/master/Cloud%20API%20Service">here</a> that includes the above-mentioned functions and a few others to get you started.</p>Ryan ButlerLast week Citrix released a “Tech Preview” of the App Layering API. The API as a starting point has some basic functionality that customers have been wanting for a long time such as querying layers and resources. The Citrix documentation provides a JSON spec but I also imported into Swagger to quickly see what it offers. The API is very different than other Citrix SDKs\APIS in that it requires an on-premises appliance that communicates to the ELM appliance while the API calls are made over the internet. This post will go over the basics of getting started and provide a script that provides some of the functionality.Citrix Virtual Apps and Desktop vCenter Lab Deploy2019-09-18T00:00:00+00:002019-09-18T00:00:00+00:00https://techdrabble.com/citrix/Citrix-CVAD-LAB<p>Uses Terraform and Ansible to deploy a fully functional CVAD environment. Many of the scripts used are thanks to <a href="https://dennisspan.com">Dennis Span</a> and his fantastic blog.</p>
<p class="notice--info">Located here: <a href="https://github.com/ryancbutler/Citrix-VAD-LAB/">https://github.com/ryancbutler/Citrix-VAD-LAB/</a></p>
<h2 id="what-it-does">What it does</h2>
<p>Deploys the following:</p>
<ul>
<li>2 DDC Controllers with Director</li>
<li>2 Storefront Servers (Cluster)</li>
<li>1 SQL and License Server</li>
<li>1 Stand alone VDA</li>
</ul>
<h3 id="ddc">DDC</h3>
<ul>
<li>Installs components including director</li>
<li>Creates Citrix site</li>
<li>Creates 1 Machine Catalog</li>
<li>Creates 1 Delivery Group</li>
<li>Creates 1 Published Desktop</li>
<li>Creates 3 Applications
<ul>
<li>Notepad</li>
<li>Calculator</li>
<li>Paint</li>
</ul>
</li>
<li>Configures director
<ul>
<li>Adds logon domain</li>
<li>Sets default page</li>
<li>Removes SSL Warning</li>
</ul>
</li>
</ul>
<h3 id="storefront">Storefront</h3>
<ul>
<li>Installs Storefront components</li>
<li>Creates Storefront cluster</li>
<li>Configures Storefromt
<ul>
<li>Adds Citrix Gateway</li>
<li>Sets default page</li>
<li>Enables HTTP loopback for SSL offload</li>
<li>Adjusts logoff behavior</li>
</ul>
</li>
</ul>
<h3 id="sql-and-citrix-license">SQL and Citrix License</h3>
<ul>
<li>Installs SQL and license server</li>
<li>Installs SQL management tools</li>
<li>Configures SQL for admins and service account</li>
<li>Copies Citrix license files</li>
</ul>
<h3 id="vda">VDA</h3>
<ul>
<li>Installs VDA components</li>
<li>Configures for DDCs</li>
</ul>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>Need CVAD ISO contents copied to accessible share via Ansible account (eg \\mynas\isos\Citrix\Citrix_Virtual_Apps_and_Desktops_7_1906_2)
<ul>
<li>I used CVAD 1906 2 ISO</li>
</ul>
</li>
<li>Need SQL ISO contents copied to accessible share via Ansible account (eg \\mynas\isos\Microsoft\SQL\en_sql_server_2017_standard_x64_dvd_11294407)
<ul>
<li>I used SQL 2017 but other versions should work</li>
</ul>
</li>
<li>DHCP enabled network</li>
<li>vCenter access and rights capable of deploying machines</li>
<li>(optional for remote state) <a href="https://app.terraform.io/signup/account">Terraform Cloud</a> account created and API key for remote state.</li>
</ul>
<h3 id="deploy-machine">Deploy machine</h3>
<p>I used <a href="https://docs.microsoft.com/en-us/windows/wsl/install-win10">Ubuntu WSL</a> to deploy from</p>
<ol>
<li><a href="https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-ansible-on-ubuntu-18-04">Ansible installed</a>
<ul>
<li>Install <strong>pywinrm</strong> <code class="language-plaintext highlighter-rouge">pip install pywinrm</code> and <code class="language-plaintext highlighter-rouge">pip install pywinrm[credssp]</code></li>
</ul>
</li>
<li><a href="https://askubuntu.com/questions/983351/how-to-install-terraform-in-ubuntu">Terraform installed</a></li>
<li><a href="https://github.com/adammck/terraform-inventory/releases">Terraform-Inventory</a> installed in path. This is used for the Ansible inventory
<ul>
<li>I copied to <em>/usr/bin/</em></li>
</ul>
</li>
<li>(If using remote state)<a href="https://www.terraform.io/docs/cloud/free/index.html#configure-access-for-the-terraform-cli">Configure Access for the Terraform CLI</a></li>
<li>This REPO cloned down</li>
</ol>
<h3 id="vcenter-windows-server-template">vCenter Windows Server Template</h3>
<ol>
<li>I used Windows Server 2019 but I assume 2016 should also work.</li>
<li>WinRM needs to be configured and <strong>CredSSP</strong> enabled
<ul>
<li>Ansible provides a great script to enable quickly https://github.com/ansible/ansible/blob/devel/examples/scripts/ConfigureRemotingForAnsible.ps1</li>
<li>Run manually <code class="language-plaintext highlighter-rouge">Enable-WSManCredSSP -Role Server -Force</code></li>
</ul>
</li>
<li>I use linked clones to quickly deploy. In order for this to work the template needs to be converted to a VM with a <strong>single snapshot</strong> created.</li>
</ol>
<h2 id="getting-started">Getting Started</h2>
<h3 id="terraform">Terraform</h3>
<ol>
<li>From the <em>terraform</em> directory copy <strong>lab.tfvars.sample</strong> to <strong>lab.tfvars</strong></li>
<li>Adjust variables to reflect vCenter environment</li>
<li>Review <strong>main.tf</strong> and adjust any VM resources if needed</li>
<li>(If using remote cloud state) At the bottom of <strong>main.tf</strong> uncomment the <em>terraform</em> section and edit the <em>organization</em> and <em>workspaces</em> fields
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>terraform {
backend "remote" {
organization = "TechDrabble"
workspaces {
name = "cvad-lab"
}
}
}
</code></pre></div> </div>
</li>
<li>run <code class="language-plaintext highlighter-rouge">terraform init</code> to install needed provider</li>
</ol>
<h3 id="ansible">Ansible</h3>
<ol>
<li>From the <em>ansible</em> directory copy <strong>vars.yml.sample</strong> to <strong>vars.yml</strong></li>
<li>Adjust variables to reflect environment</li>
<li>If you want to license CVAD environment place generated license file in <strong>ansible\roles\license\files</strong></li>
</ol>
<h2 id="deploy">Deploy</h2>
<p>If you are comfortable with below process <code class="language-plaintext highlighter-rouge">build.sh</code> handles the below steps.</p>
<p><strong>Note:</strong> If you prefer to run many of the tasks asynchronously switch the <code class="language-plaintext highlighter-rouge">ansible-playbook</code> lines within <code class="language-plaintext highlighter-rouge">build.sh</code> which will call a seperate playbook. This is faster but can consume more resources and less informative output.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#Sync
#ansible-playbook --inventory-file=/usr/bin/terraform-inventory ./ansible/playbook.yml -e @./ansible/vars.yml
#If you prefer to run most of the tasks async (can increase resources)
ansible-playbook --inventory-file=/usr/bin/terraform-inventory ./ansible/playbook-async.yml -e @./ansible/vars.yml
</code></pre></div></div>
<h2 id="terraform-1">Terraform</h2>
<ol>
<li>From the <em>terraform</em> directory run <code class="language-plaintext highlighter-rouge">terraform apply --var-file="lab.tfvars"</code></li>
<li>Verify the results and type <code class="language-plaintext highlighter-rouge">yes</code> to start the build</li>
</ol>
<h2 id="ansible-1">Ansible</h2>
<ol>
<li>From the <em>root</em> directory and the terraform deployment is completed run the following
<ul>
<li><code class="language-plaintext highlighter-rouge">export TF_STATE=./terraform</code> used for the inventory script</li>
<li>Synchronous run (Serial tasks)
<ul>
<li><code class="language-plaintext highlighter-rouge">ansible-playbook --inventory-file=/usr/bin/terraform-inventory ./ansible/playbook.yml -e @./ansible/vars.yml</code> to start the playbook</li>
</ul>
</li>
<li>Asynchronous run (Parallel tasks)
<ul>
<li><code class="language-plaintext highlighter-rouge">ansible-playbook --inventory-file=/usr/bin/terraform-inventory ./ansible/playbook-async.yml -e @./ansible/vars.yml</code> to start the playbook</li>
</ul>
</li>
<li>Grab coffee</li>
</ul>
</li>
</ol>
<h2 id="destroy">Destroy</h2>
<p>If you are comfortable with below process <code class="language-plaintext highlighter-rouge">destroy.sh</code> handles the below steps. <strong>Please note this does not clean up the computer accounts</strong></p>
<h2 id="terraform-2">Terraform</h2>
<ol>
<li>From the <em>terraform</em> directory run <code class="language-plaintext highlighter-rouge">terraform destroy --var-file="lab.tfvars"</code></li>
<li>Verify the results and type <code class="language-plaintext highlighter-rouge">yes</code> to destroy</li>
</ol>Ryan ButlerUses Terraform and Ansible to deploy a fully functional CVAD environment. Many of the scripts used are thanks to Dennis Span and his fantastic blog.Use a Consul Watch to Monitor Vault Seal Status2019-02-14T00:00:00+00:002019-02-14T00:00:00+00:00https://techdrabble.com/hashicorp/use-a-consul-watch-to-monitor-vault-seal-status<p>In order for a Vault node to be functional, it needs to be in an unsealed state which decrypts the encryption key used for decryption and encryption of secret data. If a Vault node is sealed no secret data can be retrieved until it’s unsealed. A node can become sealed for a variety of reasons such as if a node reboots after an OS update or the vault service restarts. In this post, I’ll go over how a “Consul Watch” can be used to monitor the Vault service (or any other service) and send a slack alert if found to be critical (sealed).</p>
<p><img src="/assets/images/content/consul-watcher/consul_watcher-main.png" alt="consul watcher main" /></p>
<hr />
<h2 id="getting-started">Getting Started</h2>
<p>You’ll need the following before starting.</p>
<ol>
<li>Vault installed and configured (using v1.0.3)</li>
<li>Consul configured for Vault backend (using v1.4.2)</li>
<li><a href="https://api.slack.com/incoming-webhooks">Slack WebHook URL</a></li>
<li>Requires “Requests” Python module (pip install requests)</li>
</ol>
<h2 id="consul-watch-overview">Consul Watch Overview</h2>
<p>A <a href="https://www.consul.io/docs/agent/watches.html">Consul Watch</a> can be configured to fire either a script file, HTTP request or STDOUT on the ‘type’ of watch configured. To create a simple onetime watch the following can be run from the command line on one of the Consul nodes.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>consul watch -type checks -service vault
</code></pre></div></div>
<p>You’ll see that the command outputs the health of the service located on every Vault node in JSON format to STDOUT. To take this a step further if you run..</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>consul watch -type checks -state critical
</code></pre></div></div>
<p>This will only output data if a service is found in a critical state which will be the basis of our Vault watch. We only want to alert if the service is found to be in a critical state.</p>
<h2 id="script">Script</h2>
<p>In order for the watch to actually do something if an event is encountered, I have created a script that will send updated service information to Slack. This script needs to be modified with your slack URL and copied to the Consul server that will be running the watch. Remember Requests is required for the script!</p>
<script src="https://gist.github.com/ryancbutler/8bd6ecbf12cee6e0398534fb197e1b6f.js" type="text/javascript"></script>
<p>For this example, I have copied the script to /usr/bin. Make sure that script can be executed (chmod +x checkservice.py). The output to Slack using the WebHook URL will be something similar to…</p>
<p><img src="/assets/images/content/consul-watcher/consul_watcher.png" alt="consul watcher" /></p>
<h2 id="watch">Watch</h2>
<p>The simplest way to launch a watch is with the command line. Now that the script is in place we can configure our watch to look for critical services, then fire the script if found.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>consul watch -type checks -state critical /usr/bin/checkservice.py
</code></pre></div></div>
<p>You can now test the watch by restarting the Vault service from another terminal window. Once the Vault service restarts it will come up as sealed which in turn reports to Consul as critical and the script fires.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>service vault restart
</code></pre></div></div>
<p>You’ll notice after launching the watch from the command line the output remains in the terminal which will stop the watch from functioning as soon as you log out. In order for the watch to run while logged out, Consul can run watches with config files that the Consul service will use in the background.</p>
<p>Before continuing let’s make sure the watch is stopped (ctrl-c) and Vault is unsealed.</p>
<p>Within the Consul config directory create a JSON file (eg /etc/consul/consul.d/watch-critical.json). If more watches are needed in the future you can simply add to this JSON or create a brand new file in the same format since the entire directory is parsed.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
"watches": [
{
"type": "checks",
"state": "critical",
"handler_type": "script",
"args": ["/usr/bin/checkservice.py"]
}
]
}
</code></pre></div></div>
<p>To enable the watch the Consul service needs to be restart</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>service consul restart
</code></pre></div></div>
<p>To test the watch, restart the Vault service from one of the nodes.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>service vault restart
</code></pre></div></div>
<h2 id="closing">Closing</h2>
<p>A Consul watch can be a quick way to monitor Consul services and a lot more with very little effort. I do wish there was more capability of filtering when watches fire and can get noisy with a lot of watches. I feel like watches can do a good job supplementing very specific Consul or Vault monitoring but doesn’t come close to replace normal system monitoring.</p>Ryan ButlerIn order for a Vault node to be functional, it needs to be in an unsealed state which decrypts the encryption key used for decryption and encryption of secret data. If a Vault node is sealed no secret data can be retrieved until it’s unsealed. A node can become sealed for a variety of reasons such as if a node reboots after an OS update or the vault service restarts. In this post, I’ll go over how a “Consul Watch” can be used to monitor the Vault service (or any other service) and send a slack alert if found to be critical (sealed).Install Ansible, Molecule, Vagrant on Windows WSL2019-02-07T00:00:00+00:002019-02-07T00:00:00+00:00https://techdrabble.com/ansible/install-ansible-molecule-vagrant-on-windows-wsl<p>Ansible is a really cool and very popular config management (and a lot more!) tool but sadly the control plane only runs on Linux based systems. As primarily a Windows user I wanted to see if it would function in the “newish” WSL environment and after a lot of trial and error found that it works great! In this quick post I go over how to install Ansible for config management, Molecule to test roles, Vagrant to run the tests all while running on Windows WSL.</p>
<p><img src="/assets/images/content/molecule/main.png" alt="main" height="75%" width="75%" /></p>
<hr />
<h2 id="windows-setup">Windows Setup</h2>
<ol>
<li><a href="https://www.virtualbox.org/wiki/Downloads">Install Virtual Box</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/wsl/install-win10">Install Ubuntu WSL</a></li>
<li>Reboot computer</li>
</ol>
<h2 id="wsl-setup">WSL Setup</h2>
<ol>
<li>
<p>Run Ubuntu from the start menu and upgrade all packages</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo apt update && apt upgrade -y
</code></pre></div> </div>
</li>
<li>
<p>Install Ansible</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo apt-add-repository ppa:ansible/ansible
sudo apt install ansible
</code></pre></div> </div>
</li>
<li>
<p>Install Python pip</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo apt-get install -y python-pip libssl-dev
</code></pre></div> </div>
</li>
<li>
<p>Install Pip packages</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo pip install molecule
sudo pip install python-vagrant
</code></pre></div> </div>
</li>
<li>
<p>Set ENV variable so vagrant knows its running in WSL</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>export VAGRANT_WSL_ENABLE_WINDOWS_ACCESS="1"
</code></pre></div> </div>
</li>
<li>
<p>Install Vagrant and plugins</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo apt install vagrant
sudo vagrant plugin install vagrant-libvirt
</code></pre></div> </div>
</li>
<li>
<p>In order for WSL to know where Virtual box is installed some additional paths need to be added</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>export PATH=$PATH:/mnt/c/Windows/System32
export PATH="$PATH:/mnt/c/Program Files/Oracle/VirtualBox"
</code></pre></div> </div>
</li>
<li>
<p>If you want to prevent the need to run the above <strong>EXPORT</strong> commands on every login add the following to the bottom of ~/.bashrc</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>export VAGRANT_WSL_ENABLE_WINDOWS_ACCESS="1"
export PATH=$PATH:/mnt/c/Windows/System32
export PATH="$PATH:/mnt/c/Program Files/Oracle/VirtualBox"
</code></pre></div> </div>
</li>
<li>
<p>You are now ready to run Ansible and Molecule with WSL!</p>
</li>
</ol>
<h2 id="molecule-gotchas">Molecule Gotchas</h2>
<p>By default, Molecule uses the ubuntu/xenial64 box for test runs. During my initial testing I kept encountering an error like the one below. </p>
<p><img src="/assets/images/content/molecule/error.png" alt="error" /></p>
<p>After inspecting the <strong>/tmp/molecule/base-test/default/vagrant-instance.err</strong> log I found this.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Stderr: VBoxManage.exe: error: RawFile#0 failed to create the raw output file /usr/local/lib/python2.7/dist-packages/molecule/provisioner/ansible/playbooks/vagrant/ubuntu-xenial-16.04-cloudimg-console.log (VERR_PATH_NOT_FOUND)
VBoxManage.exe: error: Details: code E_FAIL (0x80004005), component ConsoleWrap, interface IConsole
</code></pre></div></div>
<p>After some searching, I found the following workaround for <strong>molecule.yml</strong> and everything started to run correctly.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"customize [ 'modifyvm', :id, '--uartmode1', 'disconnected' ]"
</code></pre></div></div>
<p><img src="/assets/images/content/molecule/workaround.png" alt="workaround" /></p>Ryan ButlerAnsible is a really cool and very popular config management (and a lot more!) tool but sadly the control plane only runs on Linux based systems. As primarily a Windows user I wanted to see if it would function in the “newish” WSL environment and after a lot of trial and error found that it works great! In this quick post I go over how to install Ansible for config management, Molecule to test roles, Vagrant to run the tests all while running on Windows WSL.Citrix Optimizer Community Template Marketplace2018-12-21T22:37:02+00:002018-12-21T22:37:02+00:00https://techdrabble.com/citrix/citrix-optimizer-community-template-marketplace<p>Recently, <a href="https://twitter.com/MartinZugec">Martin Zugec</a> from Citrix released version 2.0 of the popular <a href="https://support.citrix.com/article/CTX224676">Citrix Optimizer tool</a> and one of the cool new features added is the ability to add custom template marketplaces. Once added a marketplace can allow users to choose then download and upgrade specific templates all from the Optimizer GUI. Out of the box, Optimizer includes a marketplace for all the Citrix maintained templates as you can see below</p>
<p><img src="/assets/images/content/marketplace/base.png" alt="base.png" /></p>
<p>After playing with the new version I thought it would be nice if there was a central location for the community to be able to submit, view and share their own custom templates. I ended up creating a home for a marketplace on Github and this post goes over the introduction of the marketplace and how I hope the community can utilize it.</p>
<h2 id="how-to-use-the-marketplace">How to use the marketplace</h2>
<p>Citrix Optimizer is designed to use <a href="https://ctxsym.citrix.com/supportabilitytools/citrixoptimizer/citrixmarketplace.xml">XML</a> for the marketplace data, including the URL location of a template and any metadata to display to the user such as author and version. This makes it easy enough to simply point Optimizer to a custom XML URL and Optimizer will be able to render the information and allow templates to be downloaded or updated. Let’s go over the steps to add the community marketplace to Optimizer.</p>
<p><img src="/assets/images/content/marketplace/add.png" alt="add.png" /></p>
<ul>
<li>From Citrix Optimizer select <strong>Template Marketplace</strong> from the left</li>
<li>Select <strong>Add New Marketplace</strong></li>
<li>Add the URL:
<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code> https://github.com/ryancbutler/Citrix_Optimizer_Community_Template_Marketplace/releases/latest/download/communitymarketplace.xml
</code></pre></div> </div>
</li>
<li>and select <strong>Done</strong>
<img src="/assets/images/content/marketplace/add.png" alt="add.png" /></li>
<li>
<p>Now on the left, you should see <strong>Citrix Community Marketplace</strong></p>
<p><img src="/assets/images/content/marketplace/market.png" alt="market.png" /></p>
</li>
<li>Templates available to download will appear on the right</li>
<li>As the marketplace grows this will allow users to download and update existing templates right from Optimizer</li>
</ul>
<h2 id="submit-templates">Submit Templates</h2>
<p>This marketplace is worthless without templates. If you have created custom templates I want them! This section will go over how to submit your templates to the marketplace so they can be shared.</p>
<p><img src="/assets/images/content/marketplace/Cm2.jpg" alt="cm2" height="75%" width="75%" /></p>
<h3 id="template-format-rules">Template Format Rules</h3>
<p>Before submitting there are a couple of rules that must be followed or else your submission\pull request will fail.</p>
<ul>
<li>Only templates created with Optimizer 2.0 will be allowed. (1.x will need to be converted)</li>
<li>Only unique display names and ids will be allowed</li>
<li>Template author name must match the directory name of the template
<img src="/assets/images/content/marketplace/rule.png" alt="rule" /></li>
</ul>
<h3 id="submit-via-git">Submit via GIT</h3>
<p>This is the preferred method. If you haven’t used GIT before this is a perfect situation to learn. Don’t get discouraged!</p>
<ul>
<li>First, you’ll need a <a href="https://github.com/join">Github account</a></li>
<li>Go to the Github repo at https://github.com/ryancbutler/Citrix_Optimizer_Community_Template_Marketplace and <strong>fork</strong> to your own repo
<img src="/assets/images/content/marketplace/fork.png" alt="fork.png" />
<ul>
<li>If you aren’t familiar with this process I can’t stress the benefits of at least learning the basics of GIT can be. There are plenty of <a href="https://help.github.com/articles/fork-a-repo/">guides</a> out there to help. Keep going!</li>
</ul>
</li>
<li>Within your fork go to the <strong>templates</strong> directory and create a new folder named the same as the author of the templates.</li>
<li>Copy your template(s) to the newly created folder</li>
<li><strong>(Optional but highly recommended)</strong> Create a <a href="https://help.github.com/articles/basic-writing-and-formatting-syntax/">readme</a> within your directory explaining your templates along with any other information you would like. Feel free to include your contact info, twitter or whatever.</li>
<li>An example of the layout can be found in the <strong>templates\Ryan Butler</strong> directory
<img src="/assets/images/content/marketplace/example.png" alt="example" /></li>
<li>Once you’re ready to submit to the marketplace you’ll want to submit a <a href=""https://help.github.com/articles/creating-a-pull-request/"">pull request</a>.</li>
<li>If all the tests pass I’ll be able to review and approve the PR making it part of the marketplace!</li>
</ul>
<p class="notice--warning"><strong>Note:</strong> There is no need to edit the <strong>communitymarketplace.xml</strong> file. This will automatically be re-generated for each submission.</p>
<h3 id="other">OTHER</h3>
<p>If you aren’t comfortable with the GIT process feel free to reach out to me on <a href="https://twitter.com/Ryan_C_Butler">Twitter</a> or wherever else and I’ll be happy to add them or help you get them submitted.</p>Ryan ButlerRecently, Martin Zugec from Citrix released version 2.0 of the popular Citrix Optimizer tool and one of the cool new features added is the ability to add custom template marketplaces.