<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>TechColumnist</title>
    <link>https://www.techcolumnist.com/</link>
    <description>Directives from a Director</description>
    <language>en-us</language>
    <lastBuildDate>Sun, 20 Sep 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://www.techcolumnist.com/feed.xml" rel="self" type="application/rss+xml" />
    <image>
      <url>https://www.techcolumnist.com/og-default.png</url>
      <title>TechColumnist</title>
      <link>https://www.techcolumnist.com/</link>
    </image>
    <item>
      <title>PowerShell: Autotask – Enable Client Portal for All Users</title>
      <link>https://www.techcolumnist.com/2022/10/12/autotask-powershell-enable-client-portal-for-all-users/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2022/10/12/autotask-powershell-enable-client-portal-for-all-users/</guid>
      <pubDate>Wed, 12 Oct 2022 14:13:37 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>This is a quick one, it’s been forever since I’ve posted here. After moving back to Autotask, there’s still a ton of things to automate.</description>
      <content:encoded><![CDATA[<p>This is a quick one, it’s been forever since I’ve posted here. After moving back to Autotask, there’s still a ton of things to automate. One of the things that was bugging me was the fact you can’t set the client portal to default. Well, here’s a script you can run periodically to enable all the users to have the simple version of the client portal.</p>
<h2>Requirements</h2>
<ul><li>PowerShell 3.0 or later (the script uses <code>Invoke-RestMethod</code>).</li>
<li>An Autotask REST API user with permission to read companies and contacts and to create client portal users.</li>
<li>The Autotask REST API base URL for your zone, the API integration code, and the API username and secret, supplied as environment variables (see Parameters).</li>
<li>Network access to the Autotask REST API.</li>
</ul>
<h2>Parameters</h2>
<p>The script has no <code>param</code> block. It reads its settings from these environment variables, which must be set in the session before it runs.</p>
<table><thead><tr><th>Name</th><th>Type</th><th>Required</th><th>Description</th></tr>
</thead><tbody><tr><td><code>at_uri</code></td><td>Environment variable (string)</td><td>Yes</td><td>Base URL of your Autotask REST API zone, without <code>/v1.0</code>.</td></tr>
<tr><td><code>at_integrationcode</code></td><td>Environment variable (string)</td><td>Yes</td><td>API integration code (sent as the <code>ApiIntegrationcode</code> header).</td></tr>
<tr><td><code>at_username</code></td><td>Environment variable (string)</td><td>Yes</td><td>Autotask API user name.</td></tr>
<tr><td><code>at_secret</code></td><td>Environment variable (string)</td><td>Yes</td><td>Secret for the Autotask API user.</td></tr>
</tbody></table>
<p>Two values in the script are specific to the author&apos;s Autotask instance and must be adjusted before you run it: the <code>companyCategoryID</code> filter and <code>Select-Object -Skip 3</code>. See Notes.</p>
<h2>Usage</h2>
<p>Set the environment variables for the current session, then run the script. It lists the contacts it enables as it goes.</p>
<pre><code class="language-powershell">$env:at_uri = &quot;&lt;autotask-rest-url&gt;&quot;
$env:at_integrationcode = &quot;&lt;integration-code&gt;&quot;
$env:at_username = &quot;&lt;api-username&gt;&quot;
$env:at_secret = &quot;&lt;api-secret&gt;&quot;

.\Enable-AutotaskClientPortal.ps1
</code></pre>
<h2>Script</h2>
<pre><code class="language-powershell">&lt;#
.SYNOPSIS
    Enables the simple client portal for every active Autotask contact that does not have it yet.
.DESCRIPTION
    Queries Autotask companies (filtered by company category), then the active
    contacts of each company, and compares them with the existing client portal
    users. For every contact without a client portal user it creates one, with
    the contact&apos;s e-mail address as the user name and a random 10-character
    password. Settings are read from environment variables, not parameters.
.PARAMETER at_uri
    Environment variable. Base URL of your Autotask REST API zone, without /v1.0.
.PARAMETER at_integrationcode
    Environment variable. API integration code (sent as the ApiIntegrationcode header).
.PARAMETER at_username
    Environment variable. Autotask API user name.
.PARAMETER at_secret
    Environment variable. Secret for the Autotask API user.
.EXAMPLE
    $env:at_uri = &quot;&lt;autotask-rest-url&gt;&quot;
    $env:at_integrationcode = &quot;&lt;integration-code&gt;&quot;
    $env:at_username = &quot;&lt;api-username&gt;&quot;
    $env:at_secret = &quot;&lt;api-secret&gt;&quot;
    .\Enable-AutotaskClientPortal.ps1
.NOTES
    Author  : Thomas Lasswell (https://www.techcolumnist.com)
    Version : 1.0 (2022-10-12)
    Requires: PowerShell 3.0 or later, Autotask REST API user
    Adjust  : Select-Object -Skip 3 skips the first three companies returned and
              companyCategoryID 101 is a category ID from the author&apos;s Autotask
              instance. Change or remove both before running.
    Limit   : Only companies with 500 or fewer contacts are handled.
#&gt;
function New-SecurePassword {
    $Password = &quot;!?@#$%^&amp;*0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz&quot;.ToCharArray()
    ($Password | Get-Random -Count 10) -join &apos;&apos;
}

$at_uri = $($env:at_uri)
$at_integrationcode = $($env:at_integrationcode)
$at_username = $($env:at_username)
$at_secret = $($env:at_secret)

# Autotask headers.
$headers = New-Object &quot;System.Collections.Generic.Dictionary[[String],[String]]&quot;
$headers.Add(&quot;ApiIntegrationcode&quot;, &quot;$at_integrationcode&quot;)
$headers.Add(&quot;Content-Type&quot;, &apos;application/json&apos;)
$headers.Add(&quot;UserName&quot;, &quot;$at_username&quot;)
$headers.Add(&quot;Secret&quot;, &quot;$at_secret&quot;)

# Get the companies to process.
$companies = $(Invoke-RestMethod -Uri $($at_uri + &apos;/v1.0/Companies/query?search={&quot;IncludeFields&quot;: [&quot;id&quot;, &quot;companyName&quot;,&quot;companyNumber&quot;,&quot;isActive&quot;],&quot;filter&quot;:[{&quot;op&quot;:&quot;eq&quot;,&quot;field&quot;:&quot;companyCategoryID&quot;,&quot;value&quot;:&quot;101&quot;}]}&apos;) -Headers $headers -Method Get).items

foreach ($company in $companies | Select-Object -Skip 3) {
    # Get the active contacts of the company.
    $contacts = $(Invoke-RestMethod -Uri $($at_uri + &apos;/v1.0/Contacts/query?search={&quot;IncludeFields&quot;: [&quot;id&quot;, &quot;firstName&quot;,&quot;lastName&quot;,&quot;isActive&quot;,&quot;emailAddress&quot;],&quot;filter&quot;:[{&quot;op&quot;:&quot;and&quot;,&quot;items&quot;:[{&quot;op&quot;:&quot;eq&quot;,&quot;field&quot;:&quot;companyID&quot;,&quot;value&quot;:&quot;&apos; + $($company.id) + &apos;&quot;},{&quot;op&quot;:&quot;eq&quot;,&quot;field&quot;:&quot;isActive&quot;,&quot;value&quot;:&quot;true&quot;}]}]}&apos;) -Headers $headers -Method Get).items
    $query = $null
    $x = 0
    $y = 0
    $clientportal = @()

    # Get the existing client portal users, 100 contacts per query.
    do {
        foreach ($contact in $contacts) {
            if ($query) {
                $query += &apos;,{&quot;op&quot;:&quot;eq&quot;,&quot;field&quot;:&quot;contactID&quot;,&quot;value&quot;:&quot;&apos; + $($contact.id) + &apos;&quot;}&apos;
            }
            if (!$query) {
                $query = &apos;{&quot;op&quot;:&quot;eq&quot;,&quot;field&quot;:&quot;contactID&quot;,&quot;value&quot;:&quot;&apos; + $($contact.id) + &apos;&quot;}&apos;
            }
            $y++
            $x++
            if ($x -eq $contacts.Count) {
                $y = 100
            }
            if ($y -eq 100) {
                $postbody = &apos;{&quot;filter&quot;:[{&quot;op&quot;:&quot;or&quot;,&quot;items&quot;:[&apos; + $query + &apos;]}]}&apos;
                $clientportal += $(Invoke-RestMethod -Uri $($at_uri + &apos;/v1.0/ClientPortalUsers/query&apos;) -Body $postbody -Headers $headers -Method Post).items
                $query = $null
                $y = 0
            }
        }
    }
    while ($x -lt $contacts.Count)

    # Create client portal users for the contacts that are missing one.
    if ($clientportal.Count -ne $contacts.Count) {
        Write-Host &quot;Contacts: $($contacts.Count)&quot;
        Write-Host &quot;Enabled: $($clientportal.Count)&quot;
        $missing = $null
        $missing = $contacts.id | Where-Object { $_ -notin $clientportal.contactId }
        foreach ($miss in $missing) {
            $contact = $null
            $contact = $contacts | Where-Object { $miss -eq $_.id }
            Write-Host &quot;$($contact.emailAddress)&quot;
            $json = [PSObject]@{
                contactID            = $($contact.id)
                userName             = &quot;$($contact.emailAddress)&quot;
                securityLevel        = 1
                password             = &quot;$(New-SecurePassword)&quot;
                numberFormat         = 22
                dateFormat           = 1
                timeFormat           = 1
                isClientPortalActive = $true
            }
            $json = $json | ConvertTo-Json
            Start-Sleep -Milliseconds 10
            Invoke-RestMethod -Uri $($at_uri + &apos;/v1.0/ClientPortalUsers&apos;) -Method POST -Body $json -Headers $headers
        }
    }
}
</code></pre>
<h2>Notes</h2>
<ul><li>There are a few places you might want to update. I use a filter for a specific customer category, so where your <code>$companies</code> variable is, you may want to change or remove this part of the query:<pre><code class="language-json">{&quot;op&quot;:&quot;eq&quot;,&quot;field&quot;:&quot;companyCategoryID&quot;,&quot;value&quot;:&quot;101&quot;}
</code></pre>
</li>
<li>Category <code>101</code> and the <code>Select-Object -Skip 3</code> (which skips the first three companies returned) are specific to my Autotask instance. Adjust or remove both for yours.</li>
<li>The script currently only handles companies with 500 or fewer contacts. I’ll update it with a loop to get all the contacts as well.</li>
</ul>
]]></content:encoded>
      <category>PowerShell</category>
      <category>Scripts</category>
    </item>
    <item>
      <title>PowerShell: Get Exchange Mailboxes Over XXGB</title>
      <link>https://www.techcolumnist.com/2021/02/18/powershell-get-exchange-mailboxes-over-xxgb/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2021/02/18/powershell-get-exchange-mailboxes-over-xxgb/</guid>
      <pubDate>Thu, 18 Feb 2021 14:46:29 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>Simple command turned crazy. I ended up coming up with this due to the fact we have duplicate display names and needed to update for Exchange Online to get mailbox sizes.</description>
      <content:encoded><![CDATA[<p>Simple command turned crazy. I ended up coming up with this due to the fact we have duplicate display names and needed to update for Exchange Online to get mailbox sizes.</p>
<h2>Requirements</h2>
<ul><li>An Exchange Online PowerShell session, for example from the ExchangeOnlineManagement module with <code>Connect-ExchangeOnline</code>.</li>
<li>Permission to run <code>Get-Mailbox</code> and <code>Get-MailboxStatistics</code> against every mailbox you want to check.</li>
</ul>
<h2>Parameters</h2>
<p>The command has no parameters. The one value you change is the size threshold.</p>
<table><thead><tr><th>Name</th><th>Type</th><th>Required</th><th>Description</th></tr>
</thead><tbody><tr><td><code>45</code> (in <code>-gt 45</code>)</td><td>number (GB)</td><td>Yes</td><td>Mailboxes larger than this many GB are listed. Edit it in the <code>Where-Object</code> stage of the command.</td></tr>
</tbody></table>
<h2>Walkthrough</h2>
<p>Here is a breakdown of the code, one stage of the pipeline at a time.</p>
<h3>Get-Mailbox</h3>
<p><code>Get-Mailbox</code> gets all the mailboxes available, there’s no search filter on this one.</p>
<pre><code class="language-powershell">Get-Mailbox -ResultSize Unlimited
</code></pre>
<h3>Select-Object for Identity</h3>
<p>The <code>Select-Object</code> statement is used for doing a select expression where we can transform the Identity parameter that is being used as pipeline input for the <code>Get-MailboxStatistics</code>. I did this mainly because we have duplicate display names and differing email addresses in this specific tenant.</p>
<pre><code class="language-powershell">| Select-Object @{ Name = &apos;Identity&apos;; Expression = { $_.PrimarySmtpAddress } }
</code></pre>
<h3>Get-MailboxStatistics</h3>
<p><code>Get-MailboxStatistics</code> accept pipeline input of the default variable Identity. By doing the select statement above, we’re now using <code>-Identity</code> via the pipeline using the primary SMTP email address. This command outputs the data about a mailbox.</p>
<pre><code class="language-powershell">| Get-MailboxStatistics
</code></pre>
<h3>Select-Object for TotalItemSize</h3>
<p>This next select statement gives us the TotalItemSize formatted for use with comparison.</p>
<pre><code class="language-powershell">| Select-Object DisplayName, @{ Name = &quot;TotalItemSize&quot;; Expression = { [math]::Round($($_.TotalItemSize.Value.ToString().Replace(&quot;,&quot;, &quot;&quot;).Split(&quot;(&quot;)[1].Split(&quot; bytes&quot;)[0]) / 1GB, 2) } }
</code></pre>
<p>This part of the select statement is broken down as follows:</p>
<p>The first part is the beginning of a select expression, the <code>{</code> after <code>Expression =</code> is the start of the expression that will now become TotalItemSize.</p>
<pre><code class="language-powershell">@{ Name = &quot;TotalItemSize&quot;; Expression = {} }
</code></pre>
<p>Next up, we have the <code>[math]</code> function, we use this because by doing the simple math, we’d have a large amount of decimal places, so we round it.</p>
<pre><code class="language-powershell">[math]::Round()
</code></pre>
<p>Inside the <code>()</code> for the Round, we have this expression</p>
<pre><code class="language-powershell">$($_.TotalItemSize.Value.ToString().Replace(&quot;,&quot;, &quot;&quot;).Split(&quot;(&quot;)[1].Split(&quot; bytes&quot;)[0]) / 1GB
</code></pre>
<p>The TotalItemSize is modified to become a string</p>
<pre><code class="language-powershell">$_.TotalItemSize.Value.ToString()
</code></pre>
<p>then we replace the commas in the string</p>
<pre><code class="language-powershell">.Replace(&quot;,&quot;, &quot;&quot;)
</code></pre>
<p>split it at the first <code>(</code> and grab the second part of the array</p>
<pre><code class="language-powershell">.Split(&quot;(&quot;)[1]
</code></pre>
<p>then split again at bytes and grab the first part of that array</p>
<pre><code class="language-powershell">.Split(&quot; bytes&quot;)[0]
</code></pre>
<p>Finally we finish the Round() statement with a <code>,2</code> which gives us 2 decimal places rounded.</p>
<h3>Where-Object</h3>
<p>The next pipeline sets the <code>Where-Object</code> and we’re only concerned about mailboxes over 45 GB in this example. You can change this to whatever you’d like to filter based on.</p>
<pre><code class="language-powershell">| Where-Object { $_.TotalItemSize -gt 45 }
</code></pre>
<h3>Format-Table</h3>
<p>The last stage, <code>Format-Table -AutoSize</code>, prints the result as a table sized to fit its content.</p>
<pre><code class="language-powershell">| Format-Table -AutoSize
</code></pre>
<h2>Usage</h2>
<p>Connect to Exchange Online, then run the command from the Script section below.</p>
<pre><code class="language-powershell">Connect-ExchangeOnline
</code></pre>
<p>It prints each mailbox over the threshold with its display name and size in GB.</p>
<pre><code class="language-text">DisplayName    TotalItemSize
-----------    -------------
&lt;display-name&gt;         52.31
</code></pre>
<p>To list mailboxes over 100 GB instead, change the <code>45</code> in the <code>Where-Object</code> stage.</p>
<pre><code class="language-powershell">| Where-Object { $_.TotalItemSize -gt 100 }
</code></pre>
<h2>Script</h2>
<p>The full command, expanded across lines. Each line ends with a pipe so it can be pasted straight into an Exchange Online session.</p>
<pre><code class="language-powershell">&lt;#
.SYNOPSIS
    Lists Exchange Online mailboxes larger than a size threshold.
.DESCRIPTION
    Gets all mailboxes, passes each primary SMTP address to Get-MailboxStatistics (so
    duplicate display names do not matter), converts TotalItemSize to GB and keeps the
    mailboxes over 45 GB.
.EXAMPLE
    .\Get-MailboxOverSize.ps1
.NOTES
    Author  : Thomas Lasswell (https://www.techcolumnist.com)
    Version : 1.0 (2021-02-18)
    Requires: Exchange Online PowerShell session; permission to run Get-Mailbox and Get-MailboxStatistics
#&gt;

Get-Mailbox -ResultSize Unlimited |
    Select-Object @{ Name = &apos;Identity&apos;; Expression = { $_.PrimarySmtpAddress } } |
    Get-MailboxStatistics |
    Select-Object DisplayName, @{
        Name       = &quot;TotalItemSize&quot;
        Expression = {
            [math]::Round($($_.TotalItemSize.Value.ToString().Replace(&quot;,&quot;, &quot;&quot;).Split(&quot;(&quot;)[1].Split(&quot; bytes&quot;)[0]) / 1GB, 2)
        }
    } |
    Where-Object { $_.TotalItemSize -gt 45 } |
    Format-Table -AutoSize
</code></pre>
]]></content:encoded>
      <category>PowerShell</category>
      <category>Scripts</category>
    </item>
    <item>
      <title>PowerShell: ConnectWise Documents API, Uploading a Document or Attachment to a Ticket</title>
      <link>https://www.techcolumnist.com/2019/01/09/powershell-connectwise-documents-api-uploading-a-document-or-attachment-to-a-ticket/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2019/01/09/powershell-connectwise-documents-api-uploading-a-document-or-attachment-to-a-ticket/</guid>
      <pubDate>Wed, 09 Jan 2019 09:46:57 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>PowerShell: upload a document or attachment to a ConnectWise ticket through the form-based Documents API.</description>
      <content:encoded><![CDATA[<p><img src="https://www.techcolumnist.com/uploads/2019/01/image.png" alt="PowerShell: ConnectWise Documents API, Uploading a Document or Attachment to a Ticket"></p>
<p>Phew, this one took a minute to figure out. ConnectWise has a form based documents API (technically not really API, but it’s the way you get yourself a document into a CW ticket). First is the really amazing documentation that CW provides around the documents API</p>
<p><img alt="ConnectWise Document API documentation showing the uploadsample GET endpoint and its Upload a document form" src="https://www.techcolumnist.com/uploads/2019/01/image.png"></p>
<p>Second is then working with PowerShell to handle streamed encoding correctly, build a multipart form data payload, and then getting it to actually send the correct thing. Ultimately there were some good learning steps here. Mainly on how to construct a proper content type of “multipart/form-data”. I’m writing this in hopes that many of you that are out there that are facing a similar challenge on getting documents to upload into CW via PowerShell aren’t faced with the same 2 day challenge I just had.</p>
<h2>Requirements</h2>
<ul><li>PowerShell 5.1 or later, with <code>Invoke-RestMethod</code>.</li>
<li>A ConnectWise API member with its public key, private key and your company identifier. Creating one is covered in <a href="https://www.techcolumnist.com/2018/12/27/powershell-connectwise-rest-api-query-contacts-by-email-address/">Query Contacts by Email Address</a>. The member&apos;s security role must be allowed to add documents to the record you upload to.</li>
<li>Outbound HTTPS to the ConnectWise documents endpoint on <code>na.myconnectwise.net</code>. Use the regular host here, not the <code>api-</code> one.</li>
<li>The file to upload, readable from the machine running the script, and the ID of the record (for example a ticket) it should be attached to.</li>
</ul>
<h2>Parameters</h2>
<p>The script has no parameters. Edit these values in it before running.</p>
<table><thead><tr><th>Name</th><th>Type</th><th>Required</th><th>Description</th></tr>
</thead><tbody><tr><td><code>$global:CWcompany</code></td><td>string</td><td>Yes</td><td>Your CW company identifier, <code>&lt;company-id&gt;</code>.</td></tr>
<tr><td><code>$global:CWprivate</code></td><td>string</td><td>Yes</td><td>The API member&apos;s private key, <code>&lt;private-key&gt;</code>.</td></tr>
<tr><td><code>$global:CWpublic</code></td><td>string</td><td>Yes</td><td>The API member&apos;s public key, <code>&lt;public-key&gt;</code>.</td></tr>
<tr><td><code>$global:CWserver</code></td><td>string</td><td>Yes</td><td>The documents endpoint URL, <code>https://na.myconnectwise.net/v4_6_release/apis/3.0/system/documents</code>. Despite the name it is the full endpoint, and it must not use the <code>api-</code> host.</td></tr>
<tr><td><code>$FilePath</code></td><td>string</td><td>Yes</td><td>Full path of the file to upload, for example <code>C:\path\to\file.jpg</code>.</td></tr>
<tr><td><code>recordType</code></td><td>string</td><td>Yes</td><td>Form field in <code>$bodyLines</code>: the kind of record to attach to. <code>Ticket</code> in this script.</td></tr>
<tr><td><code>recordId</code></td><td>string</td><td>Yes</td><td>Form field in <code>$bodyLines</code>: the ID of that record, <code>&lt;record-id&gt;</code>.</td></tr>
<tr><td><code>Title</code></td><td>string</td><td>Yes</td><td>Form field in <code>$bodyLines</code>: the title of the document in CW, <code>&lt;document-title&gt;</code>.</td></tr>
</tbody></table>
<h2>Walkthrough</h2>
<h3>Encoding Issues</h3>
<p>Mainly the Encoding Issues were around reading a file into PowerShell and then using Invoke-RestMethod to send it off. Typically you’d work in UTF-8, while that’s great in PS when working, sending that encoding via the Invoke-RestMethod seems to break things a little and none of the characters are correct, thus resulting in a data stream sent to your destination being garbled.</p>
<p><img alt="Two Notepad windows of multipart form data: readable JPEG headers on the left, garbled characters on the right" src="https://www.techcolumnist.com/uploads/2019/01/image-1.png"><em>Left – Proper Data | Right – Garbled Data</em></p>
<p>I happened to stumble, and by stumble, I’ve been searching the Google masters for quite a while trying to understand why the encoding wasn’t working correctly, upon this article:</p>
<p><a href="https://social.technet.microsoft.com/Forums/en-US/26f6a32e-e0e0-48f8-b777-06c331883555/invokewebrequest-encoding?forum=winserverpowershell">https://social.technet.microsoft.com/Forums/en-US/26f6a32e-e0e0-48f8-b777-06c331883555/invokewebrequest-encoding?forum=winserverpowershell</a></p>
<p>which nicely pointed me here:</p>
<p><a href="https://windowsserver.uservoice.com/forums/301869-powershell/suggestions/13685217-invoke-restmethod-and-invoke-webrequest-encoding-b">https://windowsserver.uservoice.com/forums/301869-powershell/suggestions/13685217-invoke-restmethod-and-invoke-webrequest-encoding-b</a></p>
<p>Taking from this, I modified the following from:</p>
<pre><code class="language-powershell">$fileEnc = [System.Text.Encoding]::GetEncoding(&apos;UTF-8&apos;).GetString($fileBytes)
</code></pre>
<p>To using the ISO 8859-1 encoding type of 28591. Converting this line to:</p>
<pre><code class="language-powershell">$fileEnc = [System.Text.Encoding]::GetEncoding(28591).GetString($fileBytes)
</code></pre>
<h3>Multipart boundaries</h3>
<p>The rest of the time was learning to deal with boundaries in a multipart/form-data payload. Essentially finding this article:</p>
<p><a href="https://gist.github.com/weipah/19bfdb14aab253e3f109">https://gist.github.com/weipah/19bfdb14aab253e3f109</a></p>
<p>This taught me a bit about the boundaries that need to be set and more-so having to use <code>`r`n</code> in different places, you’ll see this referenced the same way as in the link in my script below using the <code>$LF</code> variable.</p>
<h2>Usage</h2>
<p>Edit the values listed above, save the script as <code>Add-CWDocument.ps1</code> and run it from a PowerShell prompt.</p>
<pre><code class="language-powershell">.\Add-CWDocument.ps1
</code></pre>
<p><code>Invoke-RestMethod</code> prints whatever the CW documents API returns for the upload. To attach to a different record, change <code>recordType</code> and <code>recordId</code> in <code>$bodyLines</code>.</p>
<h2>Script</h2>
<p>Enjoy, here’s the full code layout:</p>
<pre><code class="language-powershell">&lt;#
.SYNOPSIS
    Uploads a file to a ConnectWise record (for example a ticket) through the Documents API.
.DESCRIPTION
    Reads the file at $FilePath, re-encodes its bytes as ISO 8859-1 (code page 28591) so
    Invoke-RestMethod does not garble them, builds a multipart/form-data body by hand
    (recordType, recordId, Title and the file) and posts it to the ConnectWise
    system/documents endpoint using an API member&apos;s keys.
.EXAMPLE
    .\Add-CWDocument.ps1
.NOTES
    Author  : Thomas Lasswell (https://www.techcolumnist.com)
    Version : 1.1 (2026-09-20)
    History : 1.0 2019-01-09 Initial version.
              1.1 2026-09-20 The uploaded file name now comes from $FilePath.
    Requires: PowerShell 5.1 or later; ConnectWise API member with permission to add documents
.LINK
    https://gist.github.com/weipah/19bfdb14aab253e3f109
#&gt;

# Initializations
$global:CWcompany    = &quot;&lt;company-id&gt;&quot;
$global:CWprivate    = &quot;&lt;private-key&gt;&quot;
$global:CWpublic     = &quot;&lt;public-key&gt;&quot;
# Don&apos;t use the api- URL here for the server.
$global:CWserver     = &quot;https://na.myconnectwise.net/v4_6_release/apis/3.0/system/documents&quot;

# CW auth string
[string]$Authstring  = $CWcompany + &apos;+&apos; + $CWpublic + &apos;:&apos; + $CWprivate
$encodedAuth         = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(($Authstring)))

# CW headers
$headers = New-Object &quot;System.Collections.Generic.Dictionary[[String],[String]]&quot;
$headers.Add(&quot;Authorization&quot;, &quot;Basic $encodedAuth&quot;)

# Read the file and build the multipart body
$FilePath = &apos;C:\path\to\file.jpg&apos;
$fileName = [System.IO.Path]::GetFileName($FilePath)
$fileBytes = [System.IO.File]::ReadAllBytes($FilePath)
$fileEnc = [System.Text.Encoding]::GetEncoding(28591).GetString($fileBytes)
$boundary = [System.Guid]::NewGuid().ToString()
$LF = &quot;`r`n&quot;

$bodyLines = (
    &quot;--$boundary&quot;,
    &quot;Content-Disposition: form-data; name=`&quot;recordType`&quot;$LF&quot;,
    &quot;Ticket&quot;,
    &quot;--$boundary&quot;,
    &quot;Content-Disposition: form-data; name=`&quot;recordId`&quot;$LF&quot;,
    &quot;&lt;record-id&gt;&quot;,
    &quot;--$boundary&quot;,
    &quot;Content-Disposition: form-data; name=`&quot;Title`&quot;$LF&quot;,
    &quot;&lt;document-title&gt;&quot;,
    &quot;--$boundary&quot;,
    &quot;Content-Disposition: form-data; name=`&quot;file`&quot;; filename=`&quot;$fileName`&quot;&quot;,
    &quot;Content-Type: application/octet-stream$LF&quot;,
    $fileEnc,
    &quot;--$boundary--$LF&quot;
) -join $LF

# Upload
Invoke-RestMethod -Uri $CWserver -Method Post -ContentType &quot;multipart/form-data; boundary=`&quot;$boundary`&quot;&quot; -Body $bodyLines -Headers $headers
</code></pre>
<h2>Notes</h2>
<ul><li>Update 2026-09-20 (v1.1): the file name in the multipart body was hard-coded as <code>image001.jpg</code>, so every upload was named that way whatever <code>$FilePath</code> pointed at. It is now taken from <code>$FilePath</code>.</li>
</ul>
]]></content:encoded>
      <category>PowerShell</category>
      <category>Scripts</category>
    </item>
    <item>
      <title>PowerShell: ConnectWise REST API Query Contacts by Email Address</title>
      <link>https://www.techcolumnist.com/2018/12/27/powershell-connectwise-rest-api-query-contacts-by-email-address/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2018/12/27/powershell-connectwise-rest-api-query-contacts-by-email-address/</guid>
      <pubDate>Thu, 27 Dec 2018 10:56:24 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>A PowerShell script that queries ConnectWise contacts by email address through the REST API.</description>
      <content:encoded><![CDATA[<p><img src="https://www.techcolumnist.com/uploads/2018/12/image.png" alt="PowerShell: ConnectWise REST API Query Contacts by Email Address"></p>
<p>I’ve found myself at a new job, recreating many of the processes that I spent the last few years putting together, tweaking, modifying, building a new managed services provider with an exciting new company. One of those challenges lead me to Email parsing and ConnectWise (CW). Previously I had the opportunity to use Autotask and email2ticket, however with the modifications to the API that CW did a few years ago, email2ticket is no longer supported for CW. With my found love of Azure Functions and looking at existing mail based parsing tools (<a href="https://mailparser.io/">https://mailparser.io/</a>, <a href="https://www.thinkautomation.com/">https://www.thinkautomation.com/</a>) which have amazing feature sets, they didn’t do quite what I was looking for and to replace the functionality that I once had with email2ticket.</p>
<p>That is leading me to this PowerShell series for how to utilize the CW REST API and the things that took some understanding and digging in a little to determine how to do simple queries. The CW developer portal has some great resources, and I stumbled upon the forums that ultimately made it possible to finally build a wildcard query via PowerShell to identify whether a contact exists in CW.</p>
<h2>Requirements</h2>
<ul><li>PowerShell 5.1 or later, with <code>Invoke-RestMethod</code>.</li>
<li>A CW API Access Account (an API member on the Members tab) with its public key, private key and your company identifier. First things first, you have to authenticate to the CW REST API, and you need access to the Admin Setup tables to create the account. The member&apos;s security role must be able to read contacts.</li>
<li>An Azure Functions PowerShell function with an HTTP trigger binding. The script reads the <code>email</code> query string parameter from <code>$req_query_email</code> and writes its JSON response to the output binding file path in <code>$res</code>.</li>
<li>Outbound HTTPS from the function to your CW API host (<code>api-na.myconnectwise.net</code> for the North America cloud).</li>
</ul>
<p><img alt="ConnectWise Manage Members screen with the API Members tab highlighted by a red arrow" src="https://www.techcolumnist.com/uploads/2018/12/image.png"><em>CW Members Tab – API Members</em></p>
<h2>Parameters</h2>
<table><thead><tr><th>Name</th><th>Type</th><th>Required</th><th>Description</th></tr>
</thead><tbody><tr><td><code>email</code></td><td>string</td><td>Yes</td><td>Full or partial email address to search for, for example <code>@xyz.corp</code> for every contact in a domain. Passed in the query string and read from <code>$req_query_email</code>.</td></tr>
<tr><td><code>$global:CWcompany</code></td><td>string</td><td>Yes</td><td>Value to edit: your CW company identifier, <code>&lt;company-id&gt;</code>.</td></tr>
<tr><td><code>$global:CWprivate</code></td><td>string</td><td>Yes</td><td>Value to edit: the API member&apos;s private key, <code>&lt;private-key&gt;</code>.</td></tr>
<tr><td><code>$global:CWpublic</code></td><td>string</td><td>Yes</td><td>Value to edit: the API member&apos;s public key, <code>&lt;public-key&gt;</code>.</td></tr>
<tr><td><code>$global:CWserver</code></td><td>string</td><td>Yes</td><td>Value to edit: the base URL of your CW API host, for example <code>https://api-na.myconnectwise.net</code>.</td></tr>
</tbody></table>
<h2>Walkthrough</h2>
<p>Once you have an integration setup you can proceed with creating a PowerShell script to handle the automation. I build in Azure Functions mostly, so there will be some pieces in here that relate to that, I’ll breakdown each section (and eventually move some of these pieces to linked articles).</p>
<h3>Authentication</h3>
<p>Authentication to the CW rest API is fairly simple. It requires your public and private keys and your company identifier.</p>
<p>First set your variables for your credentials.</p>
<pre><code class="language-powershell">$global:CWcompany    = &quot;&lt;company-id&gt;&quot;
$global:CWprivate    = &quot;&lt;private-key&gt;&quot;
$global:CWpublic     = &quot;&lt;public-key&gt;&quot;
$global:CWserver     = &quot;https://api-na.myconnectwise.net&quot;
</code></pre>
<p>Second configure the authentication string and setup the standard headers for your GET request.</p>
<pre><code class="language-powershell">[string]$Accept      = &quot;application/vnd.connectwise.com+json; version=3.0&quot;
[string]$ContentType = &apos;application/json&apos;
[string]$Authstring  = $CWcompany + &apos;+&apos; + $CWpublic + &apos;:&apos; + $CWprivate
$encodedAuth         = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(($Authstring)))

$headers = New-Object &quot;System.Collections.Generic.Dictionary[[String],[String]]&quot;
$headers.Add(&quot;Authorization&quot;, &quot;Basic $encodedAuth&quot;)
$headers.Add(&quot;Content-Type&quot;, &apos;application/json&apos;)
$headers.Add(&quot;Accept&quot;, $Accept)
</code></pre>
<h3>Request URI</h3>
<p>Then for the URI, this consists of a few parts, your base URL, your query parameters, and your target for your request.</p>
<p>Query String – this is the conditions you’re going to pass to do the lookup. In this case I’m looking for the email address of a contact. This requires the use of CW’s childconditions parameters. Initially the communicationItems is used to determine the value of the email address, then forcing it to only look for the type of email address to speed the query result. The “%” is used as the wildcard for the “like” operator. I spent some time trying to determine what would work best, contains and in both resulted in invalid syntax so using the like operator was the eventual conclusion.</p>
<pre><code class="language-powershell">[string]$query       = &apos;?childconditions=communicationItems/value like &quot;%&apos; + $email + &apos;%&quot; AND communicationItems/communicationType=&quot;Email&quot;&apos;
</code></pre>
<p>Putting it all together, you have a target of <a href="https://developer.connectwise.com/products/manage/rest?a=Company&amp;e=Contacts&amp;o=GET">/company/contacts</a> and use the query string. <code>$email</code> is the full or partial email address you’re searching for. Benefits of using a partial, such as <code>@xyz.corp</code>, would yield all contacts for that domain and you can use some logic there to determine what company they belong to (more to come on that subject).</p>
<pre><code class="language-powershell">[string]$TargetUri   = &apos;/company/contacts&apos;
[string]$query       = &apos;?childconditions=communicationItems/value like &quot;%&apos; + $email + &apos;%&quot; AND communicationItems/communicationType=&quot;Email&quot;&apos;
[string]$BaseUri     = &quot;$CWserver&quot; + &quot;/v4_6_release/apis/3.0&quot; + $TargetUri + $query
</code></pre>
<h3>Sending the request</h3>
<p>Finally, send the Invoke-RestMethod command to get the results. This returns a JSON table that Invoke-RestMethod converts to a PS Object.</p>
<pre><code class="language-powershell">$JSONResponse = Invoke-RestMethod -Uri $BaseUri -Headers $headers -ContentType $ContentType -Method Get
</code></pre>
<h2>Usage</h2>
<p>Once the function is deployed, request it with the full or partial email address in the query string.</p>
<pre><code class="language-text">https://&lt;function-app&gt;.azurewebsites.net/api/&lt;function-name&gt;?email=&lt;email&gt;
</code></pre>
<p>The same call from PowerShell:</p>
<pre><code class="language-powershell">Invoke-RestMethod -Uri &quot;https://&lt;function-app&gt;.azurewebsites.net/api/&lt;function-name&gt;?email=&lt;email&gt;&quot; -Method Get
</code></pre>
<p>If the function&apos;s authorization level is not anonymous, add <code>&amp;code=&lt;function-key&gt;</code> to the URL. A matching contact comes back in this shape:</p>
<pre><code class="language-text">{
    &quot;id&quot;:  1234,
    &quot;firstName&quot;:  &quot;&lt;first-name&gt;&quot;,
    &quot;lastName&quot;:  &quot;&lt;last-name&gt;&quot;,
    &quot;emails&quot;:  &quot;&lt;email&gt;;&quot;,
    &quot;company&quot;:  &quot;&lt;company-name&gt;&quot;,
    &quot;companyid&quot;:  56,
    &quot;companyidentifier&quot;:  &quot;&lt;company-identifier&gt;&quot;
}
</code></pre>
<p>If nothing matches, the script returns <code>$false</code> and writes nothing to <code>$res</code>.</p>
<h2>Script</h2>
<p>Here’s the full code that I’m using to query for a specific contact’s email address.</p>
<pre><code class="language-powershell">&lt;#
.SYNOPSIS
    Queries ConnectWise for contacts by full or partial email address.
.DESCRIPTION
    Runs as an Azure Functions PowerShell HTTP trigger. Reads the email address from the
    query string ($req_query_email), authenticates to the ConnectWise REST API with an API
    member&apos;s keys, searches /company/contacts with a wildcard &quot;like&quot; condition on the
    contact&apos;s email communication items, and writes the matching contacts (id, name,
    emails and company details) as JSON to the output binding file path in $res.
.PARAMETER email
    Full or partial email address to search for. Passed in the query string and read from $req_query_email.
.EXAMPLE
    Invoke-RestMethod -Uri &quot;https://&lt;function-app&gt;.azurewebsites.net/api/&lt;function-name&gt;?email=&lt;email&gt;&quot; -Method Get
.NOTES
    Author  : Thomas Lasswell (https://www.techcolumnist.com)
    Version : 1.1 (2026-09-20)
    History : 1.0 2018-12-27 Initial version.
              1.1 2026-09-20 The Accept header value no longer includes an &quot;Accept: &quot; prefix.
    Requires: PowerShell 5.1 or later; Azure Functions HTTP trigger ($req_query_email, $res); ConnectWise API member
.LINK
    https://developer.connectwise.com/products/manage/rest?a=Company&amp;e=Contacts&amp;o=GET
#&gt;

# GET method: each querystring parameter is its own variable
if ($req_query_email) {
    $email = $req_query_email
}

# Initializations
$global:CWcompany    = &quot;&lt;company-id&gt;&quot;
$global:CWprivate    = &quot;&lt;private-key&gt;&quot;
$global:CWpublic     = &quot;&lt;public-key&gt;&quot;
$global:CWserver     = &quot;https://api-na.myconnectwise.net&quot;

# CW auth string
[string]$Accept      = &quot;application/vnd.connectwise.com+json; version=3.0&quot;
[string]$Authstring  = $CWcompany + &apos;+&apos; + $CWpublic + &apos;:&apos; + $CWprivate
[string]$ContentType = &apos;application/json&apos;
$encodedAuth         = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(($Authstring)))

# CW headers
$headers = New-Object &quot;System.Collections.Generic.Dictionary[[String],[String]]&quot;
$headers.Add(&quot;Authorization&quot;, &quot;Basic $encodedAuth&quot;)
$headers.Add(&quot;Content-Type&quot;, &apos;application/json&apos;)
$headers.Add(&quot;Accept&quot;, $Accept)

# CW query
[string]$TargetUri   = &apos;/company/contacts&apos;
[string]$query       = &apos;?childconditions=communicationItems/value like &quot;%&apos; + $email + &apos;%&quot; AND communicationItems/communicationType=&quot;Email&quot;&apos;
[string]$BaseUri     = &quot;$CWserver&quot; + &quot;/v4_6_release/apis/3.0&quot; + $TargetUri + $query

# Get response
$JSONResponse = Invoke-RestMethod -Uri $BaseUri -Headers $headers -ContentType $ContentType -Method Get

# Parse contact info to usable short table
$contactInfo = @()
foreach ($contact in $JSONResponse) {
    $email = $null
    $emails = $null
    $obj = New-Object PSObject
    $obj | Add-Member -MemberType NoteProperty -Name &quot;id&quot; -Value $contact.id
    $obj | Add-Member -MemberType NoteProperty -Name &quot;firstName&quot; -Value $contact.firstName
    $obj | Add-Member -MemberType NoteProperty -Name &quot;lastName&quot; -Value $contact.lastName
    foreach ($commtype in $contact.communicationItems) {
        $email = $($commtype | Where-Object { $_.communicationType -eq &quot;Email&quot; }).value
        if ($email.length -gt 2) {
            $emails += $email + &quot;;&quot;
        }
    }
    $obj | Add-Member -MemberType NoteProperty -Name &quot;emails&quot; -Value $emails
    $obj | Add-Member -MemberType NoteProperty -Name &quot;company&quot; -Value $contact.company.name
    $obj | Add-Member -MemberType NoteProperty -Name &quot;companyid&quot; -Value $contact.company.id
    $obj | Add-Member -MemberType NoteProperty -Name &quot;companyidentifier&quot; -Value $contact.company.identifier
    $contactInfo += $obj
}

if ($contactInfo) {
    Out-File -Encoding Ascii -FilePath $res -InputObject $($contactInfo | ConvertTo-Json)
} else {
    return $false
}
</code></pre>
<h2>Notes</h2>
<ul><li>Update 2026-09-20 (v1.1): <code>$Accept</code> used to be <code>&quot;Accept: application/vnd.connectwise.com+json; version=3.0&quot;</code>, which put a second <code>Accept: </code> inside the header value. It is now just the media type.</li>
</ul>
]]></content:encoded>
      <category>PowerShell</category>
      <category>Scripts</category>
    </item>
    <item>
      <title>First post in a long time — changing hosting providers</title>
      <link>https://www.techcolumnist.com/2018/03/28/first-post-in-a-long-time-changing-hosting-providers/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2018/03/28/first-post-in-a-long-time-changing-hosting-providers/</guid>
      <pubDate>Wed, 28 Mar 2018 22:59:53 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>Wow, it’s been a while since I’ve done a real post on this site. I’ve got many interesting things to discuss, but it has been a great last few years.</description>
      <content:encoded><![CDATA[<p>Wow, it’s been a while since I’ve done a real post on this site. I’ve got many interesting things to discuss, but it has been a great last few years. More to come, but mainly, I’m just moved hosting providers and onto a VPS server.</p>
]]></content:encoded>

    </item>
    <item>
      <title>PowerShell: Connect to LogicMonitor’s REST API</title>
      <link>https://www.techcolumnist.com/2016/02/22/powershell-connect-to-logicmonitors-rest-api/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2016/02/22/powershell-connect-to-logicmonitors-rest-api/</guid>
      <pubDate>Mon, 22 Feb 2016 20:53:26 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>It’s been a while since I’ve posted. Way too long. I’ve had this script for quite a while that I wanted to share with the world.</description>
      <content:encoded><![CDATA[<p>It’s been a while since I’ve posted. Way too long. I’ve had this script for quite a while that I wanted to share with the world. LogicMonitor is releasing a new REST API which requires some session based login. This script helps you obtain that session and download the audit log for the last hour. The start time is calculated in UTC, so no timezone settings need changing.</p>
<h2>Requirements</h2>
<ul><li>Windows PowerShell 3.0 or later (<code>Invoke-RestMethod</code>).</li>
<li>A LogicMonitor user that is allowed to read the access (audit) logs.</li>
<li>HTTPS access to <code>&lt;account&gt;.logicmonitor.com</code>.</li>
</ul>
<h2>Parameters</h2>
<p>The script takes no parameters. Edit these values before running it.</p>
<table><thead><tr><th>Name</th><th>Type</th><th>Required</th><th>Description</th></tr>
</thead><tbody><tr><td><code>$user</code></td><td>String</td><td>Yes</td><td>LogicMonitor username. Replace <code>&lt;username&gt;</code>.</td></tr>
<tr><td><code>$pass</code></td><td>String</td><td>Yes</td><td>Password for that user. Replace <code>&lt;password&gt;</code>.</td></tr>
<tr><td><code>&lt;account&gt;</code></td><td>String</td><td>Yes</td><td>Your LogicMonitor account (portal) name, in the <code>$uri</code> host name.</td></tr>
<tr><td><code>$hours</code></td><td>Int</td><td>No</td><td>How many hours back the audit log starts. Defaults to <code>1</code>.</td></tr>
<tr><td><code>$filter</code></td><td>String</td><td>No</td><td>Access log filter. Defaults to <code>_all~update</code>; see the LogicMonitor documentation.</td></tr>
<tr><td><code>$fields</code></td><td>String</td><td>No</td><td>Comma-separated fields to return. Defaults to <code>username,happenedOnLocal,description</code>.</td></tr>
</tbody></table>
<h2>Usage</h2>
<p>Edit the variables at the top of the script, then run it.</p>
<pre><code class="language-powershell">.\Get-LogicMonitorAuditLog.ps1
</code></pre>
<p>To keep the result, redirect the output to a file.</p>
<pre><code class="language-powershell">.\Get-LogicMonitorAuditLog.ps1 | Out-File .\logicmonitor-audit.txt
</code></pre>
<h2>Script</h2>
<pre><code class="language-powershell">&lt;#
.SYNOPSIS
    Downloads the LogicMonitor audit (access) log for the last hour through the REST API.
.DESCRIPTION
    Builds the start time as UTC epoch seconds, builds a Basic authentication
    header from the user name and password, and calls the /santaba/rest/setting/accesslogs endpoint of
    your LogicMonitor account. The events are returned with the username, local time and description
    fields and displayed.
.EXAMPLE
    .\Get-LogicMonitorAuditLog.ps1
.NOTES
    Author  : Thomas Lasswell (https://www.techcolumnist.com)
    Version : 1.1 (2026-09-20)
    History : 1.0 2016-02-22 Initial version.
              1.1 2026-09-20 Start time is computed in UTC instead of a fixed EST offset;
                  removed the unused end time.
    Requires: Windows PowerShell 3.0+ (Invoke-RestMethod), a LogicMonitor user with access log rights
#&gt;
$user = &quot;&lt;username&gt;&quot;
$pass = &quot;&lt;password&gt;&quot;

# How many hours back to read the audit log.
$hours = 1

# Get the start time as UTC epoch seconds, rounded to not have decimals.
$epoch = New-Object DateTime 1970, 1, 1, 0, 0, 0, ([DateTimeKind]::Utc)
$epochStart = [math]::Round(((Get-Date).ToUniversalTime().AddHours(-$hours) - $epoch).TotalSeconds)

# Check the LogicMonitor documentation on filters.
$filter = &quot;_all~update&quot;
$fields = &quot;username,happenedOnLocal,description&quot;

# Build the URI for the access logs.
$uri = &quot;https://&lt;account&gt;.logicmonitor.com/santaba/rest/setting/accesslogs?sort=-happenedOn&amp;filter=$filter,happenedOn&gt;:$epochStart&amp;fields=$fields&quot;

# Build the base64 authentication string for the header.
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes((&quot;{0}:{1}&quot; -f $user, $pass)))

# Get the events.
$events = Invoke-RestMethod -Headers @{Authorization = (&quot;Basic {0}&quot; -f $base64AuthInfo)} -Uri $uri

# Display the events that were gathered.
$events
</code></pre>
<h2>Notes</h2>
<ul><li>Update 2026-09-20 (v1.1): the start time used to be the local time plus a hard-coded 4 hours (EST, and wrong during daylight saving), and an end time was calculated but never used. The start is now computed in UTC from <code>$hours</code>, so it works in any timezone.</li>
</ul>
]]></content:encoded>
      <category>PowerShell</category>
      <category>Scripts</category>
    </item>
    <item>
      <title>PowerShell: Autotask – Get Picklist Values</title>
      <link>https://www.techcolumnist.com/2015/02/06/powershell-autotask-get-picklist-values/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2015/02/06/powershell-autotask-get-picklist-values/</guid>
      <pubDate>Fri, 06 Feb 2015 18:11:32 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>When using AutoTask’s API it’s required to lookup a various amount of picklist values that are used in updating you’re web request.</description>
      <content:encoded><![CDATA[<p>When using Autotask’s API it’s required to lookup a various amount of picklist values that are used in updating your web request. This is a PowerShell way to pull those picklist values. The first part of the script validates your AT URI, the second part gets the entity data, in this case I was looking for “ticket” related fields.</p>
<p>Edit: Updated to give a more friendly output</p>
<h2>Requirements</h2>
<ul><li>Windows PowerShell 2.0 or later. <code>New-WebServiceProxy</code> is not available in PowerShell 6 and later.</li>
<li>An Autotask user with API access (username and password).</li>
<li>HTTPS access to the Autotask web services (<code>webservices1.autotask.net</code> and the zone URL that <code>getZoneInfo</code> returns).</li>
</ul>
<h2>Parameters</h2>
<p>The script takes no parameters. Edit these values before running it.</p>
<table><thead><tr><th>Name</th><th>Type</th><th>Required</th><th>Description</th></tr>
</thead><tbody><tr><td><code>$ATurl</code></td><td>String</td><td>Yes</td><td>Starting WSDL used to look up your zone. It is replaced with the zone URL that <code>getZoneInfo</code> returns.</td></tr>
<tr><td><code>$ATusername</code></td><td>String</td><td>Yes</td><td>Autotask API username. Replace <code>&lt;autotask-username&gt;</code>.</td></tr>
<tr><td><code>$ATpassword</code></td><td>String</td><td>Yes</td><td>Password for that user. Replace <code>&lt;autotask-password&gt;</code>.</td></tr>
<tr><td><code>&quot;Ticket&quot;</code></td><td>String</td><td>No</td><td>Entity passed to <code>getFieldInfo</code>. Change it to list the picklists of another entity.</td></tr>
</tbody></table>
<h2>Usage</h2>
<p>Fill in the username and password, then run the script.</p>
<pre><code class="language-powershell">.\Get-AutotaskPicklistValues.ps1
</code></pre>
<p>For each field in the entity it prints the name, label and description, followed by the picklist values (<code>Label</code>, <code>Value</code>, <code>IsActive</code>). It ends with your API threshold and usage message.</p>
<p>To look at another entity, change the entity name in the <code>getFieldInfo</code> call.</p>
<pre><code class="language-powershell">$entity = $atws.getFieldInfo(&quot;Account&quot;)
</code></pre>
<h2>Script</h2>
<pre><code class="language-powershell">&lt;#
.SYNOPSIS
    Lists the picklist values for an Autotask entity (Ticket by default) through the Autotask SOAP API.
.DESCRIPTION
    Connects to the Autotask web service with the given credentials, looks up the zone for the user
    and reconnects to the zone WSDL. It then calls getFieldInfo for the Ticket entity and prints each
    field with its picklist values, and finishes by printing the API threshold and usage message.
.EXAMPLE
    .\Get-AutotaskPicklistValues.ps1
.NOTES
    Author  : Thomas Lasswell (https://www.techcolumnist.com)
    Version : 1.0 (2015-02-06)
    Requires: Windows PowerShell 2.0+ (New-WebServiceProxy), an Autotask API user
#&gt;
# Username and password for Autotask.
$ATurl = &quot;https://webservices1.autotask.net/atservices/1.5/atws.wsdl&quot;
$ATusername = &quot;&lt;autotask-username&gt;&quot;
$ATpassword = ConvertTo-SecureString &quot;&lt;autotask-password&gt;&quot; -AsPlainText -Force
$ATcredentials = New-Object System.Management.Automation.PSCredential($ATusername, $ATpassword)

# Look up the zone for this user and connect to it.
$atws = New-WebServiceProxy -Uri $ATurl -Credential $ATcredentials
$zoneInfo = $atws.getZoneInfo($ATusername)
$ATurl = $zoneInfo.URL.Replace(&quot;.asmx&quot;, &quot;.wsdl&quot;)
$atws = New-WebServiceProxy -Uri $ATurl -Credential $ATcredentials

# Get the field information for the entity.
$entity = $atws.getFieldInfo(&quot;Ticket&quot;)

foreach ($picklist in $entity) {
    $picklist | Select-Object Name, Label, Description | Format-Table
    foreach ($values in $picklist.PicklistValues) {
        $values | Select-Object Label, Value, IsActive
    }
}

# Show the API threshold and usage information.
$output = $atws.getThresholdAndUsageInfo()
$output.EntityReturnInfoResults.message
</code></pre>
]]></content:encoded>
      <category>PowerShell</category>
      <category>Scripts</category>
    </item>
    <item>
      <title>PowerShell: NetApp – Gather Volume Information</title>
      <link>https://www.techcolumnist.com/2014/04/28/powershell-netapp-gather-volume-information/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2014/04/28/powershell-netapp-gather-volume-information/</guid>
      <pubDate>Mon, 28 Apr 2014 12:57:58 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>This is a simple script to gather volume information including dedupe schedule and autogrow settings.</description>
      <content:encoded><![CDATA[<p>This is a simple script to gather volume information including dedupe schedule and autogrow settings. I’m going to combine this with my snapshot script in the future to make a recommended dedupe schedule based on the average snapshot times.</p>
<h2>Requirements</h2>
<ul><li>PowerShell 2.0 or later.</li>
<li>NetApp Data ONTAP PowerShell Toolkit 1.2 or later (the <code>DataONTAP</code> module). The script imports it if it is not already loaded and stops with an error if the version is older.</li>
<li>NetApp 7-Mode controllers reachable over HTTPS.</li>
<li>An account on each controller that can read volume, autosize and dedupe (SIS) information.</li>
</ul>
<h2>Parameters</h2>
<table><thead><tr><th>Name</th><th>Type</th><th>Required</th><th>Description</th></tr>
</thead><tbody><tr><td><code>nodes</code></td><td>array</td><td>Yes</td><td>One or more controllers to connect to. When prompted, enter one per line and press Enter on an empty line to continue.</td></tr>
<tr><td><code>username</code></td><td>string</td><td>Yes</td><td>Account used to connect to every controller, for example <code>&lt;domain&gt;\&lt;username&gt;</code>.</td></tr>
<tr><td><code>password</code></td><td>SecureString</td><td>Yes</td><td>Password for the account. Prompted for as a secure string when not supplied.</td></tr>
<tr><td><code>IsVerbose</code></td><td>switch</td><td>No</td><td>Display the collected property table for each volume as it is gathered.</td></tr>
</tbody></table>
<p>The same credentials are used for every node in <code>nodes</code>.</p>
<h2>Usage</h2>
<p>Pass the controllers and account on the command line. You are prompted for the password.</p>
<pre><code class="language-powershell">&amp; &apos;.\NetApp-Gather Volume Information.ps1&apos; -nodes &quot;&lt;filer-1&gt;&quot;,&quot;&lt;filer-2&gt;&quot; -username &quot;&lt;domain&gt;\&lt;username&gt;&quot; -IsVerbose
</code></pre>
<p>Or run the script with no parameters and answer the prompts.</p>
<pre><code class="language-powershell">&amp; &apos;.\NetApp-Gather Volume Information.ps1&apos;
</code></pre>
<pre><code class="language-text">cmdlet NetApp-Gather Volume Information.ps1 at command pipeline position 1
Supply values for the following parameters:
nodes[0]: &lt;filer-1&gt;
nodes[1]: &lt;filer-2&gt;
nodes[2]:
username: &lt;domain&gt;\&lt;username&gt;
password: *************
</code></pre>
<p>The results are written to <code>GatherVolumeInformation_&lt;timestamp&gt;_Detail.csv</code> in the same folder as the script.</p>
<h2>Script</h2>
<pre><code class="language-powershell">&lt;#
.SYNOPSIS
    Gathers NetApp 7-Mode volume information, including dedupe schedule and autogrow settings.
.DESCRIPTION
    Connects to each node with the Data ONTAP PowerShell Toolkit and collects
    details for every online volume that is not read-only: aggregate, total
    size, used, available, dedupe (SIS) status and schedule, and autogrow
    status, maximum size and increment size. The results are written to
    GatherVolumeInformation_&lt;timestamp&gt;_Detail.csv in the script folder.
.PARAMETER nodes
    One or more controllers to connect to. When prompted, enter one per line and press Enter on an empty line to continue.
.PARAMETER username
    Account used to connect to every controller, for example &lt;domain&gt;\&lt;username&gt;.
.PARAMETER password
    Password for the account. Prompted for as a secure string when not supplied.
.PARAMETER IsVerbose
    Display the collected property table for each volume as it is gathered.
.EXAMPLE
    &amp; &apos;.\NetApp-Gather Volume Information.ps1&apos; -nodes &quot;&lt;filer-1&gt;&quot;,&quot;&lt;filer-2&gt;&quot; -username &quot;&lt;domain&gt;\&lt;username&gt;&quot; -IsVerbose
.EXAMPLE
    &amp; &apos;.\NetApp-Gather Volume Information.ps1&apos;

    cmdlet NetApp-Gather Volume Information.ps1 at command pipeline position 1
    Supply values for the following parameters:
    nodes[0]: &lt;filer-1&gt;
    nodes[1]: &lt;filer-2&gt;
    nodes[2]:
    username: &lt;domain&gt;\&lt;username&gt;
    password: *************
.NOTES
    Author  : Thomas Lasswell (https://www.techcolumnist.com)
    Version : 1.1 (2026-09-20)
    Requires: Data ONTAP PowerShell Toolkit 1.2 or later, NetApp 7-Mode
    History : 1.0 2014-04-28 Initial version.
              1.1 2026-09-20 Fixed the timestamp in the CSV file name (it used minutes
                  where the month belongs) and the volume progress message.
#&gt;
param (
    [Parameter(Mandatory = $true)]
    [Array]$nodes,
    [Parameter(Mandatory = $true)]
    [String]$username,
    [Parameter(Mandatory = $true, ParameterSetName = &apos;Secret&apos;)]
    [Security.SecureString]$password,
    [switch]$IsVerbose
)

# Create outfile information.
$exedir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$currentDate = (Get-Date -Format yyyyMMdd.HHmmss)
$outdetails = ($exedir + &quot;\&quot; + &quot;GatherVolumeInformation_&quot; + $currentDate + &quot;_Detail.csv&quot;)

# Load ONTAP PowerShell Toolkit.
$module = Get-Module DataONTAP
if ($module -eq $null) {
    Import-Module DataONTAP
}

try {
    $requiredVersion = New-Object System.Version(1.2)
    if ((Get-NaToolkitVersion).CompareTo($requiredVersion) -lt 0) {
        throw
    }
} catch [Exception] {
    Write-Host &quot;`nThis script requires Data ONTAP PowerShell Toolkit 1.2 or higher`n&quot; -ForegroundColor Red
    return
}

$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $username, $password

# Declare object arrays.
$objDetail = @()

# Connect to each node individually.
foreach ($node in $nodes) {
    Write-Host &quot;connecting to node $node...&quot;
    $conn = Connect-NaController -Name $node -HTTPS -Credential $cred

    if ($conn -ne $null) {
        Write-Host &quot;node connected, continuing on to volume calculations...&quot;
        Write-Host &quot;gathering node volumes...&quot;
        $vols = Get-NaVol | Where-Object { $_.state -eq &quot;online&quot; -and $_.raidstatus -notmatch &quot;read-only&quot; }
        if ($vols -ne $null) {
            foreach ($vol in $vols) {
                Write-Host &quot;`ngathering volume data for volume $vol...&quot;
                $nasis = $null

                # Get volume details.
                Write-Host &quot;    ... gathering volume details&quot;
                $navol = Get-NaVol -Name $vol
                Write-Host &quot;    ... gathering AutoSize details&quot;
                $navolautosize = Get-NaVolAutosize -Name $vol
                Write-Host &quot;    ... gathering Dedupe (SIS) details&quot;
                if ($navol.Dedupe -eq &quot;True&quot;) {
                    $nasis = Get-NaSis -Name $vol
                }
                if ($nasis -eq $null) {
                    $sissched = &quot;None&quot;
                }
                if ($nasis -ne $null) {
                    $sissched = $nasis.Schedule
                }

                # Format numbers.
                $ftotalsize = ConvertTo-FormattedNumber $navol.TotalSize DataSize &quot;0.0&quot;
                $favailable = ConvertTo-FormattedNumber $navol.Available DataSize &quot;0.0&quot;
                $fmaxsize = ConvertTo-FormattedNumber $navolautosize.MaximumSize DataSize &quot;0.0&quot;
                $fincrement = ConvertTo-FormattedNumber $navolautosize.IncrementSize DataSize &quot;0.0&quot;

                # Build array for details.
                $detailprop = @{
                    &apos;Node&apos;            = $node
                    &apos;Volume&apos;          = $vol
                    &apos;Aggregate&apos;       = $navol.Aggregate
                    &apos;TotalSize&apos;       = $ftotalsize
                    &apos;Used&apos;            = $navol.Used
                    &apos;Available&apos;       = $favailable
                    &apos;DedupeEnabled&apos;   = $navol.Dedupe
                    &apos;DedupeSchedule&apos;  = $sissched
                    &apos;AutogrowEnabled&apos; = $navolautosize.IsEnabled
                    &apos;MaxSize&apos;         = $fmaxsize
                    &apos;IncrementSize&apos;   = $fincrement
                }
                $objectD = New-Object -TypeName PSObject -Property $detailprop
                $objDetail += $objectD
                if ($IsVerbose) {
                    Write-Host &quot;`n`nShowing Verbose Output...`n`n&quot;
                    $detailprop
                }
            }
        }
    }
}

# Save files.
$objDetail | Select-Object Node, Volume, Aggregate, TotalSize, Used, Available, DedupeEnabled, DedupeSchedule, AutogrowEnabled, MaxSize, IncrementSize | Export-Csv -Path $outdetails -NoTypeInformation
</code></pre>
<h2>Notes</h2>
<ul><li>Update 2026-09-20 (v1.1): the timestamp in the output file name was <code>yyyymmdd.Hm.s</code>, where <code>mm</code> is minutes, so it never contained the month. It is now <code>yyyyMMdd.HHmmss</code>, for example <code>GatherVolumeInformation_20140428.140530_Detail.csv</code>.</li>
</ul>
]]></content:encoded>
      <category>Data ONTAP PowerShell Toolkit</category>
      <category>PowerShell</category>
      <category>Scripts</category>
    </item>
    <item>
      <title>PowerShell: OnCommand Core – Delete All Information Events</title>
      <link>https://www.techcolumnist.com/2014/02/10/oncommand-core-delete-all-information-events/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2014/02/10/oncommand-core-delete-all-information-events/</guid>
      <pubDate>Mon, 10 Feb 2014 11:56:00 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>Found a solution on the NetApp communities for deleting the Informational events that plague OnCommand Core.</description>
      <content:encoded><![CDATA[<p>Found a solution on the NetApp communities for deleting the Informational events that plague OnCommand Core.</p>
<h2>Requirements</h2>
<ul><li>The Data ONTAP <code>dfm</code> command-line tool, available on the OnCommand Core server.</li>
<li>A PowerShell prompt on the OnCommand Core server, and an account allowed to list and delete events.</li>
</ul>
<h2>Usage</h2>
<p>Run this in PowerShell on the OnCommand Core server. It lists the IDs of all Information events and deletes them one at a time.</p>
<pre><code class="language-powershell">dfm event list -q -S information |
    Select-String -Pattern &quot;(?&lt;ID&gt;[0-9]+)&quot; |
    Select-Object @{Name = &quot;ID&quot;; Expression = { $_.Matches[0].Groups[&quot;ID&quot;].Value }} |
    ForEach-Object { dfm event delete $_.ID }
</code></pre>
<h2>Notes</h2>
<ul><li><code>dfm event list -q -S information</code> is the key part of this line. It prints only the IDs of the events whose severity is Information.</li>
<li><code>Select-String</code> picks the numeric event ID out of each line, <code>Select-Object</code> turns it into an <code>ID</code> property, and <code>ForEach-Object</code> calls <code>dfm event delete</code> with each ID.</li>
<li>If you’d want to delete all events, remove the <code>-S information</code> from the line. This deletes events of every severity, so use it with care.</li>
</ul>
<h2>Source</h2>
<p><a href="https://communities.netapp.com/message/94591#94591">https://communities.netapp.com/message/94591#94591</a></p>
]]></content:encoded>
      <category>PowerShell</category>
      <category>Scripts</category>
    </item>
    <item>
      <title>PowerShell: NetApp – Gather Snapshot Details</title>
      <link>https://www.techcolumnist.com/2014/01/21/powershell-gather-netapp-snapshot-details/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2014/01/21/powershell-gather-netapp-snapshot-details/</guid>
      <pubDate>Tue, 21 Jan 2014 16:15:38 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>Gathering snapshot statistics is a tedious task when looking at autosupports and cli output.</description>
      <content:encoded><![CDATA[<p>Gathering snapshot statistics is a tedious task when looking at autosupports and cli output. I needed to gather information about oldest snapshot, average number of snaps per day, total snapshots, and other various information.</p>
<p>This PowerShell script uses the Data ONTAP PowerShell Toolkit to collect that information for every online volume on each 7-Mode controller and writes a summary CSV and a detail CSV.</p>
<h2>Requirements</h2>
<ul><li>Data ONTAP PowerShell Toolkit 1.2 or higher (the <code>DataONTAP</code> module)</li>
<li>NetApp 7-Mode controllers reachable over HTTPS</li>
<li>An account that can list volumes and snapshots on each controller</li>
<li>Write access to the folder the script runs from (the CSV files are saved next to the script)</li>
</ul>
<h2>Parameters</h2>
<table><thead><tr><th>Name</th><th>Type</th><th>Required</th><th>Description</th></tr>
</thead><tbody><tr><td><code>nodes</code></td><td>Array</td><td>Yes</td><td>Controllers to query (host names or IPs). If omitted, PowerShell prompts for one per line; press Enter on an empty line to continue.</td></tr>
<tr><td><code>username</code></td><td>String</td><td>Yes</td><td>Account used to connect, for example <code>domain\username</code>.</td></tr>
<tr><td><code>password</code></td><td>SecureString</td><td>Yes</td><td>Password for the account. Prompted for if omitted.</td></tr>
<tr><td><code>IsVerbose</code></td><td>Switch</td><td>No</td><td>Also prints a detail table of every snapshot per volume.</td></tr>
</tbody></table>
<h2>Usage</h2>
<p>Pass the controllers and account on the command line. The password is prompted for:</p>
<pre><code class="language-powershell">&amp; &apos;.\NetApp-Gather Snapshot Information v1.1.ps1&apos; -nodes filer1.company.biz,filer2.company.biz -username &quot;domain\username&quot; -IsVerbose
</code></pre>
<p>Or run it with no parameters and answer the prompts:</p>
<pre><code class="language-powershell">&amp; &apos;.\NetApp-Gather Snapshot Information v1.1.ps1&apos;
</code></pre>
<pre><code class="language-text">cmdlet NetApp-Gather Snapshot Information v1.1.ps1 at command pipeline position 1
Supply values for the following parameters:
nodes[0]: filer1.company.biz
nodes[1]: filer2.company.biz
nodes[2]:
username: domain\username
password: *************
</code></pre>
<h2>Script</h2>
<h3>Original (v1.1)</h3>
<pre><code class="language-powershell">&lt;#
.SYNOPSIS
    NetApp-Gather Snapshot Information
.DESCRIPTION
    Collects snapshot information, summary and detail, for every online volume
    on each 7-Mode controller, including created time, total snapshots, total
    days, average per day and oldest snapshot. Saves a Summary and a Detail CSV
    next to the script.
.PARAMETER nodes
    Controllers to query, one per entry.
.PARAMETER username
    Account used to connect, for example domain\username.
.PARAMETER password
    Password for the account, as a SecureString.
.PARAMETER IsVerbose
    Display snapshot detail for each volume.
.EXAMPLE
    &amp; &apos;.\NetApp-Gather Snapshot Information v1.1.ps1&apos; -nodes filer1.company.biz,filer2.company.biz -username &quot;domain\username&quot; -IsVerbose
.EXAMPLE
    &amp; &apos;.\NetApp-Gather Snapshot Information v1.1.ps1&apos;
.NOTES
    Author  : Thomas Lasswell (https://www.techcolumnist.com)
    Version : 1.2 (2026-09-20)
    Requires: Data ONTAP PowerShell Toolkit 1.2 or higher
    History : 1.0 2014-01-17 Initial version.
              1.1 2014-01-21 Added CSV export, days old column fixed.
              1.2 2026-09-20 Fixed the timestamp in the CSV file names (it used minutes
                  where the month belongs).
#&gt;
# Parameters: nodes is each node; when finished, press Enter and it will continue.
param(
    [Parameter(Mandatory = $true)]
    [Array]$nodes,
    [Parameter(Mandatory = $true)]
    [String]$username,
    [Parameter(Mandatory = $true, ParameterSetName = &apos;Secret&apos;)]
    [Security.SecureString]$password,
    [switch]$IsVerbose
)

# Create output file names
$exedir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$currentDate = (Get-Date -Format yyyyMMdd.HHmmss)
$outsummary = ($exedir + &quot;\&quot; + &quot;GatherSnapshotInformation_&quot; + $currentDate + &quot;_Summary.csv&quot;)
$outdetails = ($exedir + &quot;\&quot; + &quot;GatherSnapshotInformation_&quot; + $currentDate + &quot;_Detail.csv&quot;)

# Load the ONTAP PowerShell Toolkit
$module = Get-Module DataONTAP
if ($module -eq $null) {
    Import-Module DataONTAP
}

try {
    $requiredVersion = New-Object System.Version(1.2)
    if ((Get-NaToolkitVersion).CompareTo($requiredVersion) -lt 0) { throw }
} catch [Exception] {
    Write-Host &quot;`nThis script requires Data ONTAP PowerShell Toolkit 1.2 or higher`n&quot; -ForegroundColor Red
    return
}
$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $username, $password

# Calculate the difference between two dates
function Get-DateDiff {
    param (
        [CmdletBinding()]
        [parameter(Mandatory = $true)]
        [datetime]$date1,
        [parameter(Mandatory = $true)]
        [datetime]$date2
    )
    if ($date2 -gt $date1) {
        $diff = $date2 - $date1
    } else {
        $diff = $date1 - $date2
    }
    $diff
}

# Declare object arrays
$objDetail = @()
$objSummary = @()

# Connect to each node individually
foreach ($node in $nodes) {
    Write-Host &quot;connecting to node $node...&quot;
    $conn = Connect-NaController -Name $node -HTTPS -Credential $cred

    if ($conn -ne $null) {
        Write-Host &quot;node connected, continuing on to snapshot calculations...&quot;
        Write-Host &quot;gathering node volumes...&quot;
        $vols = Get-NaVol | Where-Object { $_.state -eq &quot;online&quot; -and $_.raidstatus -notmatch &quot;read-only&quot; }
        if ($vols -ne $null) {
            foreach ($vol in $vols) {
                Write-Host &quot;gathering snapshots for volume $vol...&quot;
                # Get snapshots
                $snaps = Get-NaSnapshot -TargetName $vol

                # Group to count snapshots per day
                $snapsdatecount = $snaps | Group-Object { ((Get-Date) - $_.Created).Days } -NoElement | Sort-Object Name -Descending
                # Measure to get count and average; the average is snapshots per day
                $totalavgdays = $snapsdatecount | Measure-Object Count -Average | Select-Object Count, Average
                $avgdays = $totalavgdays.Average
                $totaldays = $totalavgdays.Count

                # Sum snapshots for total snapshot consumption
                $totalsize = $snaps | Measure-Object Total -Sum | Select-Object Count, Sum
                $ftotalsize = ConvertTo-FormattedNumber $totalsize.sum DataSize &quot;0.0&quot;

                # Get oldest snapshot
                $foldestsnap = $snaps | Sort-Object Created -Descending | Select-Object -Last 1

                # Build array for summary
                if ($avgdays -ne $null) {
                    $summaryprop = @{
                        &apos;Node&apos;       = $node
                        &apos;Volume&apos;     = $vol
                        &apos;TotalSnaps&apos; = $snaps.length
                        &apos;TotalDays&apos;  = $totaldays
                        &apos;AvgPerDay&apos;  = $avgdays
                        &apos;TotalSize&apos;  = $ftotalsize
                        &apos;Oldest&apos;     = $foldestsnap.created
                    }
                    $objectS = New-Object -TypeName PSObject -Property $summaryprop
                    $objSummary += $objectS
                    $objectS

                    foreach ($snap in $snaps) {
                        $daysold = Get-DateDiff (Get-Date) $snap.created
                        $ftotal = ConvertTo-FormattedNumber $snap.total DataSize &quot;0.0&quot;
                        $fcumulative = ConvertTo-FormattedNumber $snap.CumulativeTotal DataSize &quot;0.0&quot;

                        # Build array for details
                        $detailprop = @{
                            &apos;Node&apos;            = $node
                            &apos;Volume&apos;          = $vol
                            &apos;Name&apos;            = $snap.name
                            &apos;Created&apos;         = $snap.created
                            &apos;DaysOld&apos;         = $daysold.days
                            &apos;TotalSize&apos;       = $ftotal
                            &apos;CumulativeTotal&apos; = $fcumulative
                        }
                        $objectD = New-Object -TypeName PSObject -Property $detailprop
                        $objDetail += $objectD
                    }
                    if ($IsVerbose) {
                        $snaps | Format-Table `
                            @{Expression = {$node}; Label = &quot;Node name&quot;; Width = 20},`
                            @{Expression = {$vol}; Label = &quot;Volume&quot;; Width = 40},`
                            @{Expression = {$_.Name}; Label = &quot;Name&quot;; Width = 150},`
                            @{Expression = {$_.Created.ToShortDateString()}; Label = &quot;Created&quot;; Width = 12},`
                            @{Expression = {&apos;{0} Days&apos; -f (Get-DateDiff (Get-Date) $_.created).days}; Label = &quot;Days Old&quot;; Width = 20},`
                            @{Expression = {ConvertTo-FormattedNumber $_.Total DataSize &quot;0.0&quot;}; Label = &quot;Total&quot;; Width = 15},`
                            @{Expression = {ConvertTo-FormattedNumber $_.CumulativeTotal DataSize &quot;0.0&quot;}; Label = &quot;Cumulative&quot;; Width = 10} -AutoSize `
                            | Out-String -Width 1000 | Write-Host
                    }
                }
                if ($avgdays -eq $null) { Write-Host &quot;`tNo Snapshots Exist on $vol...&quot; }
            }
        }
    }
}

# Save files
$objSummary | Select-Object Node, Volume, TotalSnaps, TotalDays, AvgPerDay, TotalSize, Oldest | Export-Csv -Path $outsummary -NoTypeInformation
$objDetail | Select-Object Node, Volume, Name, Created, DaysOld, TotalSize, CumulativeTotal | Export-Csv -Path $outdetails -NoTypeInformation
</code></pre>
<h3>ChatGPT rewrite</h3>
<p>I was curious to see what ChatGPT code interpreter would do with this code and this is its output.</p>
<pre><code class="language-powershell">&lt;#
.SYNOPSIS
    NetApp-Gather Snapshot Information
.DESCRIPTION
    Collects snapshot information, summary and detail, including created time,
    total snapshots, total days, average per day and oldest snapshot.
.PARAMETER nodes
    Controllers to query, one per entry.
.PARAMETER username
    Account used to connect, for example domain\username.
.PARAMETER password
    Password for the account, as a SecureString.
.PARAMETER IsVerbose
    Display snapshot detail for each volume.
.NOTES
    Author  : Thomas Lasswell (https://www.techcolumnist.com)
    Version : 1.2 (2026-09-20), rewritten by ChatGPT
    Requires: Data ONTAP PowerShell Toolkit 1.2 or higher
#&gt;
param(
    [Parameter(Mandatory = $true)]
    [Array]$nodes,
    [Parameter(Mandatory = $true)]
    [String]$username,
    [Parameter(Mandatory = $true, ParameterSetName = &apos;Secret&apos;)]
    [Security.SecureString]$password,
    [switch]$IsVerbose
)

$exedir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$currentDate = (Get-Date -Format &quot;yyyyMMdd.HHmmss&quot;)
$outsummary = -join ($exedir, &quot;\GatherSnapshotInformation_&quot;, $currentDate, &quot;_Summary.csv&quot;)
$outdetails = -join ($exedir, &quot;\GatherSnapshotInformation_&quot;, $currentDate, &quot;_Detail.csv&quot;)

Import-Module DataONTAP -ErrorAction SilentlyContinue

try {
    $requiredVersion = New-Object System.Version(1.2)
    if ((Get-NaToolkitVersion).CompareTo($requiredVersion) -lt 0) { throw &quot;This script requires Data ONTAP PowerShell Toolkit 1.2 or higher.&quot; }
} catch {
    Write-Host &quot;`nThis script requires Data ONTAP PowerShell Toolkit 1.2 or higher`n&quot; -ForegroundColor Red
    return
}

$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $username, $password

function Get-DateDiff {
    param (
        [parameter(Mandatory = $true)]
        [datetime]$date1,
        [parameter(Mandatory = $true)]
        [datetime]$date2
    )
    return [Math]::Abs(($date2 - $date1).Days)
}

$objDetail = @()
$objSummary = @()

foreach ($node in $nodes) {
    Write-Host &quot;Connecting to node $node...&quot;
    $conn = Connect-NaController -Name $node -HTTPS -Credential $cred

    if ($conn) {
        Write-Host &quot;Node connected, continuing on to snapshot calculations...&quot;
        $vols = Get-NaVol | Where-Object { $_.state -eq &quot;online&quot; -and $_.raidstatus -notmatch &quot;read-only&quot; }

        foreach ($vol in $vols) {
            $snaps = Get-NaSnapshot -TargetName $vol
            $snapsdatecount = $snaps | Group-Object { ((Get-Date) - $_.Created).Days } -NoElement | Sort-Object Name -Descending
            $totalavgdays = $snapsdatecount | Measure-Object Count -Average | Select-Object Count, Average
            $totalsize = $snaps | Measure-Object Total -Sum | Select-Object Count, Sum
            $foldestsnap = $snaps | Sort-Object Created -Descending | Select-Object -Last 1

            if ($totalavgdays.Average) {
                $summaryprop = @{
                    &apos;Node&apos;       = $node
                    &apos;Volume&apos;     = $vol
                    &apos;TotalSnaps&apos; = $snaps.length
                    &apos;TotalDays&apos;  = $totalavgdays.Count
                    &apos;AvgPerDay&apos;  = $totalavgdays.Average
                    &apos;TotalSize&apos;  = [System.Math]::Round($totalsize.Sum, 2)
                    &apos;Oldest&apos;     = $foldestsnap.created
                }
                $objSummary += New-Object -TypeName PSObject -Property $summaryprop

                foreach ($snap in $snaps) {
                    $daysold = Get-DateDiff (Get-Date) $snap.created
                    $detailprop = @{
                        &apos;Node&apos;            = $node
                        &apos;Volume&apos;          = $vol
                        &apos;Name&apos;            = $snap.name
                        &apos;Created&apos;         = $snap.created
                        &apos;DaysOld&apos;         = $daysold
                        &apos;TotalSize&apos;       = [System.Math]::Round($snap.total, 2)
                        &apos;CumulativeTotal&apos; = [System.Math]::Round($snap.CumulativeTotal, 2)
                    }
                    $objDetail += New-Object -TypeName PSObject -Property $detailprop
                }
            } else {
                Write-Host &quot;`tNo Snapshots Exist on $vol...&quot;
            }
        }
    }
}

$objSummary | Export-Csv -Path $outsummary -NoTypeInformation
$objDetail | Export-Csv -Path $outdetails -NoTypeInformation
</code></pre>
<h2>Notes</h2>
<ul><li>Output is two CSV files next to the script: <code>GatherSnapshotInformation_&lt;date&gt;_Summary.csv</code> (one row per volume) and <code>GatherSnapshotInformation_&lt;date&gt;_Detail.csv</code> (one row per snapshot).</li>
<li>The ChatGPT version is a straight rewrite of the original and has not been run against a filer. It is not a drop-in replacement: it drops the <code>-IsVerbose</code> table output and formats sizes with <code>[Math]::Round</code> instead of <code>ConvertTo-FormattedNumber</code>.</li>
<li>Update 2026-09-20 (v1.2): the CSV file name timestamp was <code>yyyymmdd.Hm.s</code>, where <code>mm</code> is minutes, so it never contained the month. It is now <code>yyyyMMdd.HHmmss</code>. The ChatGPT rewrite also grouped snapshots by <code>$_.Created.Days</code>, which does not exist on a date, so every snapshot landed in one group; it now groups by age in days like the original.</li>
</ul>
]]></content:encoded>
      <category>Data ONTAP PowerShell Toolkit</category>
      <category>PowerShell</category>
      <category>Scripts</category>
    </item>
    <item>
      <title>cisco WS-C4900M: Set SSH</title>
      <link>https://www.techcolumnist.com/2014/01/14/cisco-ws-c4900m-set-ssh/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2014/01/14/cisco-ws-c4900m-set-ssh/</guid>
      <pubDate>Tue, 14 Jan 2014 16:55:49 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>Note: You can leave the “telnet” off vty transport, but if you do ensure that you’ve tested SSH first!</description>
      <content:encoded><![CDATA[<p>Just a quick note about configuring SSH on a Cisco Catalyst 4900M Switch:</p>
<pre><code class="language-text">Cisco IOS Software, Catalyst 4500 L3 Switch Software (cat4500e-ENTSERVICESK9-M), Version 12.2(54)SG, RELEASE SOFTWARE (fc3)
# conf t
(config)#crypto key zeroize rsa
% No Signature RSA Keys found in configuration.
(config)# crypto key generate rsa general-keys label ssh modulus 1024
The name for the keys will be: ssh

% The key modulus size is 1024 bits
% Generating 1024 bit RSA keys, keys will be non-exportable...[OK]
(config)# ip ssh authentication-retries 5
(config)# ip ssh version 2
(config)# line vty 0 4
(config)# transport input ssh telnet
</code></pre>
<p>Note: You can leave the “telnet” off vty transport, but if you do ensure that you’ve tested SSH first!</p>
]]></content:encoded>
      <category>Engineering</category>
    </item>
    <item>
      <title>PowerShell: Download VMX File, Rename, Upload, Add to Inventory</title>
      <link>https://www.techcolumnist.com/2013/10/25/powershell-download-vmx-file-rename-upload-add-to-inventory/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2013/10/25/powershell-download-vmx-file-rename-upload-add-to-inventory/</guid>
      <pubDate>Fri, 25 Oct 2013 14:41:24 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>1 environment. This created a slight issue due to the fact that environment ran vShield.</description>
      <content:encoded><![CDATA[<p>I was tasked with migrating a VMware 4.1 to 5.1 environment. This created a slight issue due to the fact that environment ran vShield. For those of you not familiar, vShield on 4.1 had 2 additional lines in the VMX file that was manually added. These VFILE lines caused the VM not to boot if it was moved to an environment where vShield 1.0 was not present. So to resolve this a script was needed to download all these VMs, remove the lines, then reupload the file with a different name. Well, here you go.</p>
<h2>Requirements</h2>
<ul><li>PowerShell 2.0 or later.</li>
<li>VMware vSphere PowerCLI 5.x, loaded as the <code>VMware.VimAutomation.Core</code> snap-in (the script calls <code>Add-PSSnapin</code>).</li>
<li>Network access from the machine running the script to both the vCenter 4.1 and vCenter 5.1 servers.</li>
<li>Permission to browse and upload files on the datastores, and to register (<code>New-VM</code>) virtual machines in vCenter 5.1.</li>
<li>The datastores holding the VMs must be visible to the vCenter 5.1 server and to the ESX host you import to.</li>
<li>The two local working folders and the import folder in vCenter 5.1 must already exist (see the values to edit below).</li>
</ul>
<h2>Parameters</h2>
<table><thead><tr><th>Name</th><th>Type</th><th>Required</th><th>Description</th></tr>
</thead><tbody><tr><td><code>vcServer4</code></td><td>string</td><td>Yes</td><td>The vCenter 4.1 server to connect to.</td></tr>
<tr><td><code>vcServer5</code></td><td>string</td><td>Yes</td><td>The vCenter 5.1 server to connect to.</td></tr>
</tbody></table>
<p>The script exits with a message if either server is missing. Edit these variables near the top of the script before running it:</p>
<table><thead><tr><th>Variable</th><th>Value in the script</th><th>Description</th></tr>
</thead><tbody><tr><td><code>$outvmxdir</code></td><td><code>C:\scripts\vmxfiles</code></td><td>Local folder that receives the downloaded VMX files. Must exist.</td></tr>
<tr><td><code>$outvmxnewdir</code></td><td><code>C:\scripts\vmxfiles-new</code></td><td>Local folder for the modified VMX files. Must exist.</td></tr>
<tr><td><code>$vCluster</code></td><td><code>NA Cluster</code></td><td>Cluster in vCenter 4.1 whose powered-on VMs are processed.</td></tr>
<tr><td><code>$VMFolderName</code></td><td><code>Import</code></td><td>VM folder in vCenter 5.1 the VMs are imported into. Must exist.</td></tr>
<tr><td><code>$ESXHost</code></td><td><code>&lt;esx-host&gt;</code></td><td>ESX host in vCenter 5.1 the VMs are imported to.</td></tr>
<tr><td><code>$ResPool</code></td><td><code>Infrastructure</code></td><td>Resource pool in vCenter 5.1 the VMs are imported into.</td></tr>
</tbody></table>
<h2>Usage</h2>
<p>Pass the vCenter 4.1 server first and the vCenter 5.1 server second.</p>
<pre><code class="language-powershell">.\VM-GatherVMX.ps1 -vcServer4 &quot;&lt;vcenter-4-server&gt;&quot; -vcServer5 &quot;&lt;vcenter-5-server&gt;&quot;
</code></pre>
<h2>Script</h2>
<pre><code class="language-powershell">&lt;#
.SYNOPSIS
    Downloads, updates and uploads VMX files with PowerCLI, then adds the VMs to the vCenter 5.1 inventory.
.DESCRIPTION
    Downloads the VMX file of every &quot;PoweredOn&quot; VM in the vCenter 4.1 cluster,
    removes the vShield VFILE lines, and uploads the result to the same
    datastore folder under a new name (&lt;name&gt;-51.vmx). Once uploaded, each VM is
    imported into the new vCenter 5.1 server.
.PARAMETER vcServer4
    The vCenter 4.1 server to connect to.
.PARAMETER vcServer5
    The vCenter 5.1 server to connect to.
.EXAMPLE
    .\VM-GatherVMX.ps1 -vcServer4 &quot;&lt;vcenter-4-server&gt;&quot; -vcServer5 &quot;&lt;vcenter-5-server&gt;&quot;
.NOTES
    Author  : Thomas Lasswell (https://www.techcolumnist.com)
    Version : 1.1 (2026-09-20)
    Requires: VMware vSphere PowerCLI 5.x (VMware.VimAutomation.Core snap-in)
    History : 1.0 2013-10-25 Initial version.
              1.1 2026-09-20 Fixed VM folder lookup (now after connecting to vCenter 5.1)
                  and a variable clash that overwrote it, VMX names that contain
                  &quot;.&quot;, &quot;v&quot;, &quot;m&quot; or &quot;x&quot;, and the unassigned $VMFullPath output.
#&gt;
param(
    [string]$vcServer4,
    [string]$vcServer5
)

Clear-Host

if (!$vcServer4) {
    Write-Host &quot;No vCenter 4 Server Specified&quot;
    exit
}
if (!$vcServer5) {
    Write-Host &quot;No vCenter 5 Server Specified&quot;
    exit
}

# Load PowerCLI.
Add-PSSnapin VMware.VimAutomation.Core
$host.ui.rawui.WindowTitle = &quot;PowerShell [PowerCLI Snap-in Loaded]&quot;

# Download VMX file location.
$outvmxdir = &quot;C:\scripts\vmxfiles&quot;
# Modified VMX file location.
$outvmxnewdir = &quot;C:\scripts\vmxfiles-new&quot;
# Cluster name in vCenter 4.1 to gather PoweredOn VMs.
$vCluster = &quot;NA Cluster&quot;
# VM folder name in vCenter 5.1. This folder needs to exist.
$VMFolderName = &quot;Import&quot;
# ESX host in vCenter 5.1 to import VM.
$ESXHost = &quot;&lt;esx-host&gt;&quot;
# Resource pool in vCenter 5.1 to import VM.
$ResPool = &quot;Infrastructure&quot;

# Connect to vCenter 4.1 server.
Connect-VIServer $vcServer4

$aVM = Get-VM -Location $vCluster | Where-Object { $_.PowerState -eq &quot;PoweredOn&quot; } | Select-Object Name, PowerState
$aVMInfo = @()
foreach ($vm in $aVM) {
    $vname = $vm.name
    Write-Host $vname

    # Process VMX information.
    $VMView = Get-VM $vname | Get-View
    $VMPathName = $VMView.Config.Files.VmPathName
    Write-Host $VMPathName

    # Download VMX file.
    $dsname = $VMPathName.Split(&quot; &quot;)[0].TrimStart(&quot;[&quot;).TrimEnd(&quot;]&quot;)
    $vmRelativePath = $VMPathName.Split(&apos;]&apos;)[1].TrimStart(&apos; &apos;)
    Remove-PSDrive -Name fromds -ErrorAction SilentlyContinue
    New-PSDrive -Name fromds -Location (Get-Datastore $dsname) -PSProvider VimDatastore -Root &apos;/&apos; | Out-Null
    $dlvmx = $outvmxdir + (&quot;/&quot;) + $vmRelativePath.Split(&apos;/&apos;)[1]
    $checkforoutfile = Test-Path $dlvmx
    if ($checkforoutfile) {
        Remove-Item $dlvmx
    }
    Copy-DatastoreItem -Item fromds:\$vmRelativePath -Destination $outvmxdir -Force

    # Update VMX file to -51 file without vShield lines.
    $vmxfile = $VMPathName.Split(&quot;/&quot;)[1]
    $vmxsource = $outvmxdir + &quot;\&quot; + $vmxfile
    $vmxdest = $outvmxnewdir + &quot;\&quot; + [System.IO.Path]::GetFileNameWithoutExtension($vmxfile) + &quot;-51.vmx&quot;
    $checkforoutfile = Test-Path $vmxdest
    if ($checkforoutfile) {
        Remove-Item $vmxdest
    }
    Get-Content $vmxsource | Where-Object { $_ -notmatch &apos;VFILE.globaloptions&apos; -and $_ -notmatch &apos;scsi0:0.filters = &quot;VFILE&quot;&apos; } | Set-Content $vmxdest
    Add-Content $vmxdest &quot;`n#Modified to remove VFILE Lines&quot;

    # Upload new VMX file to datastore.
    Write-Host $vmxdest
    $vmxupload = ($vmRelativePath -replace &apos;\.vmx$&apos;, &apos;&apos;) + &quot;-51.vmx&quot;
    Write-Host $vmxupload
    Copy-DatastoreItem -Item $vmxdest -Destination fromds:\$vmxupload -Force

    # Set array for addition of VMs.
    $obj = New-Object PSObject
    $obj | Add-Member -MemberType NoteProperty -Name &quot;VMName&quot; -Value $vname
    $obj | Add-Member -MemberType NoteProperty -Name &quot;VMDatastore&quot; -Value $dsname
    $obj | Add-Member -MemberType NoteProperty -Name &quot;VMPathName&quot; -Value $vmxupload
    $aVMInfo += $obj
}

Remove-PSDrive -Name fromds -ErrorAction SilentlyContinue
Disconnect-VIServer $vcServer4 -Force -Confirm:$false
Start-Sleep 5

# Connect to vCenter 5.1 server.
Connect-VIServer $vcServer5
$VMFolder = Get-Folder $VMFolderName

# Import VM to vCenter 5.1.
foreach ($vm in $aVMInfo) {
    $vname = $vm.VMName
    $datastore = $vm.VMDatastore
    $VMPath = $vm.VMPathName
    Write-Host $vname
    Write-Host $datastore
    Write-Host $VMPath
    $VMFullPath = &quot;[&quot; + $datastore + &quot;] &quot; + $VMPath
    Write-Host $VMFullPath
    New-VM -VMFilePath $VMFullPath -VMHost $ESXHost -Location $VMFolder -ResourcePool $ResPool | Out-Null
}

Disconnect-VIServer $vcServer5 -Force -Confirm:$false
</code></pre>
<h2>Notes</h2>
<ul><li>The original VMX files are left untouched. The modified copy is uploaded next to each original as <code>&lt;name&gt;-51.vmx</code>, and that copy is what gets registered in vCenter 5.1.</li>
<li>The script expects each VM to live in a single datastore folder of the form <code>[datastore] &lt;folder&gt;/&lt;name&gt;.vmx</code>.</li>
<li>Update 2026-09-20 (v1.1): the import folder is now looked up after connecting to vCenter 5.1 (it previously ran before any connection, and its variable was overwritten inside the download loop, so <code>New-VM -Location</code> received a path string instead of the folder). VMX file names that contain <code>.</code>, <code>v</code>, <code>m</code> or <code>x</code> are also no longer truncated, and the <code>fromds:</code> drive is removed at the end.</li>
</ul>
]]></content:encoded>
      <category>PowerShell</category>
      <category>Scripts</category>
    </item>
    <item>
      <title>PowerShell: List All VMs with Datastore</title>
      <link>https://www.techcolumnist.com/2013/10/14/script-list-all-vms-with-datastore/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2013/10/14/script-list-all-vms-with-datastore/</guid>
      <pubDate>Mon, 14 Oct 2013 17:10:37 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>Script to list all the VMs that exist within your vCenter Server with their datastore.</description>
      <content:encoded><![CDATA[<p>Script to list all the VMs that exist within your vCenter Server with their datastore.</p>
<h2>Requirements</h2>
<ul><li>Windows PowerShell with VMware vSphere PowerCLI installed. The script loads the <code>VMware.VimAutomation.Core</code> snap-in, so it needs a PowerCLI release that ships it (5.x era).</li>
<li>A vCenter account that can read datacenters, datastores and VMs. <code>Connect-VIServer</code> prompts for credentials.</li>
<li>Network access to the vCenter Server.</li>
<li>A <code>D:\scripts</code> folder, or change the export path.</li>
</ul>
<h2>Parameters</h2>
<table><thead><tr><th>Name</th><th>Type</th><th>Required</th><th>Description</th></tr>
</thead><tbody><tr><td><code>vcServer</code></td><td>String</td><td>Yes</td><td>Name or address of the vCenter Server. The script exits with a message if it is missing.</td></tr>
<tr><td><code>D:\scripts\datastore-output.csv</code></td><td>Path</td><td>No</td><td>Edit in the script: where the CSV is written.</td></tr>
</tbody></table>
<h2>Usage</h2>
<p>Pass the vCenter Server name and sign in when prompted.</p>
<pre><code class="language-powershell">.\Get-VMDatastore.ps1 -vcServer vcenter.example.com
</code></pre>
<p>The results are written to <code>D:\scripts\datastore-output.csv</code>, one row per VM and datastore, with <code>Name</code>, <code>DataStore</code>, <code>VMHost</code>, <code>PowerState</code>, <code>Version</code> and <code>Folder</code> columns.</p>
<h2>Script</h2>
<pre><code class="language-powershell">&lt;#
.SYNOPSIS
    Lists all VMs in a vCenter Server together with their datastore.
.DESCRIPTION
    Connects to the vCenter Server, walks every datastore in every datacenter and collects the VMs
    stored on it (name, datastore, host, power state, hardware version and folder). The result is
    written to D:\scripts\datastore-output.csv and the session is disconnected.
.PARAMETER vcServer
    Name or address of the vCenter Server. The script exits with a message if it is missing.
.EXAMPLE
    .\Get-VMDatastore.ps1 -vcServer vcenter.example.com
.NOTES
    Author  : Thomas Lasswell (https://www.techcolumnist.com)
    Version : 1.1 (2026-09-20)
    History : 1.0 2013-10-14 Initial version.
              1.1 2026-09-20 Disconnects without a confirmation prompt.
    Requires: VMware PowerCLI with the VMware.VimAutomation.Core snap-in
#&gt;
param(
    [string]$vcServer
)

Clear-Host

if (!$vcServer) {
    Write-Host &quot;No vCenter Server Specified&quot;
    exit
}

Add-PSSnapin VMware.VimAutomation.Core
$host.UI.RawUI.WindowTitle = &quot;PowerShell [PowerCLI Module Loaded]&quot;

Connect-VIServer $vcServer

$aDatastoreVM = Get-Datacenter | Get-Datastore | ForEach-Object {
    $ds = $_.Name
    $_ | Get-VM | Select-Object Name, @{n = &apos;DataStore&apos;; e = { $ds } }, VMHost, PowerState, Version, Folder
}

$aDatastoreVM | Export-Csv &quot;D:\scripts\datastore-output.csv&quot; -NoTypeInformation -UseCulture
Disconnect-VIServer $vcServer -Confirm:$false
</code></pre>
<h2>Notes</h2>
<ul><li>Update 2026-09-20 (v1.1): <code>Disconnect-VIServer</code> now uses <code>-Confirm:$false</code>, so an unattended run no longer stops at a confirmation prompt.</li>
</ul>
]]></content:encoded>
      <category>PowerShell</category>
      <category>Scripts</category>
    </item>
    <item>
      <title>Windows Folder Locations</title>
      <link>https://www.techcolumnist.com/2013/10/01/windows-folder-locations/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2013/10/01/windows-folder-locations/</guid>
      <pubDate>Tue, 01 Oct 2013 10:23:06 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>A quick reference to Windows folder locations and the environment variables that point to them, such as %ALLUSERSPROFILE%.</description>
      <content:encoded><![CDATA[<p>Because I always lose them:</p>
<p>Remember —- %ALLUSERSPROFILE%</p>
]]></content:encoded>

    </item>
    <item>
      <title>Create bootable USB from ISO – Rufus</title>
      <link>https://www.techcolumnist.com/2013/09/17/create-bootable-usb-from-iso-rufus/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2013/09/17/create-bootable-usb-from-iso-rufus/</guid>
      <pubDate>Tue, 17 Sep 2013 09:42:19 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>A quick bookmark for Rufus, a free tool for creating a bootable USB drive from an ISO image.</description>
      <content:encoded><![CDATA[<p>A quick bookmark for later:</p>
<p><a href="http://rufus.akeo.ie/">http://rufus.akeo.ie/</a></p>
]]></content:encoded>
      <category>Engineering</category>
    </item>
    <item>
      <title>PowerShell: Find Installed NetApp Products</title>
      <link>https://www.techcolumnist.com/2013/09/03/powershell-find-installed-netapp-products/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2013/09/03/powershell-find-installed-netapp-products/</guid>
      <pubDate>Tue, 03 Sep 2013 17:32:50 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>A slightly modified version of a scriptingGuy post to find NetApp software installed from a csv file (export from active directory)</description>
      <content:encoded><![CDATA[<p>A slightly modified version of a scriptingGuy post to find NetApp software installed from a csv file (export from active directory)</p>
<h2>Requirements</h2>
<ul><li>Windows PowerShell 2.0 or later.</li>
<li>The Remote Registry service running on every computer in the list.</li>
<li>An account with administrative rights on those computers, and network access to them.</li>
<li>A <code>computerlist.csv</code> file in the same folder as the script, with a <code>computername</code> column (for example an Active Directory export).</li>
</ul>
<h2>Parameters</h2>
<p>The script takes no parameters. Edit these values before running it.</p>
<table><thead><tr><th>Name</th><th>Type</th><th>Required</th><th>Description</th></tr>
</thead><tbody><tr><td><code>$LikePublisher</code></td><td>String</td><td>Yes</td><td>Publisher name to search for. Matched with <code>-like &quot;*value*&quot;</code>; defaults to <code>NetApp</code>.</td></tr>
<tr><td><code>computerlist.csv</code></td><td>File</td><td>Yes</td><td>Input file in the script folder. Needs a <code>computername</code> column with one computer per row.</td></tr>
</tbody></table>
<h2>Usage</h2>
<p>Create <code>computerlist.csv</code> next to the script.</p>
<pre><code class="language-text">computername
SERVER01
SERVER02
</code></pre>
<p>Then run the script. Matches are shown as a table and also written to <code>searchpublisher-NetApp_&lt;ddMMyyyy&gt;.csv</code> in the script folder (an existing file with the same name is deleted first).</p>
<pre><code class="language-powershell">.\Find-InstalledNetAppProducts.ps1
</code></pre>
<p>To search for another vendor, change <code>$LikePublisher</code> at the top of the script.</p>
<h2>Script</h2>
<pre><code class="language-powershell">&lt;#
.SYNOPSIS
    Finds software from a given publisher (NetApp by default) installed on the computers in a CSV file.
.DESCRIPTION
    Reads computerlist.csv from the script folder (it needs a computername column), opens the
    remote registry on each computer and walks HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall,
    skipping KB entries. Entries whose publisher matches $LikePublisher are displayed as a table and
    exported to searchpublisher-&lt;publisher&gt;_&lt;ddMMyyyy&gt;.csv in the script folder.
.EXAMPLE
    .\Find-InstalledNetAppProducts.ps1
.NOTES
    Author  : Thomas Lasswell (https://www.techcolumnist.com)
    Version : 1.1 (2026-09-20)
    History : 1.0 2013-09-03 Initial version.
              1.1 2026-09-20 The CSV no longer starts with a #TYPE line.
    Requires: Windows PowerShell 2.0+, Remote Registry service on the target computers
    Based on a Scripting Guy post.
#&gt;
$ErrorActionPreference = &apos;silentlycontinue&apos;
$exedir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent

# Variables
$LikePublisher = &quot;NetApp&quot;

$infile = ($exedir + &quot;\computerlist.csv&quot;)
Write-Host &quot;$infile&quot;
$computers = Import-Csv $infile
$totalComputers = $computers.Count
$currentDate = (Get-Date -UFormat &quot;%d%m%Y&quot;)
$outfile = ($exedir + &quot;\&quot; + &quot;searchpublisher-&quot; + $LikePublisher + &quot;_&quot; + $currentDate + &quot;.csv&quot;)
Write-Host &quot;$outfile&quot;
$checkforoutfile = Test-Path $outfile
if ($checkforoutfile) {
    Remove-Item $outfile
}

$counter1 = 0
$array = @()
foreach ($pc in $computers) {
    $subkeys = $null
    $computername = $pc.computername
    $counter1++
    Write-Progress -Activity &quot;Processing&quot; -Status &quot;Processing computer $counter1 of $totalComputers&quot; -PercentComplete (100 * ($counter1 / $computers.Count)) -Id 1
    Write-Progress &quot;Initializing&quot; &quot;Connecting to the Windows System: $computername&quot; -ParentId 1

    # Define the variable to hold the location of currently installed programs.
    $UninstallKey = &quot;SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall&quot;

    # Create an instance of the registry object and open the HKLM base key.
    $reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey(&apos;LocalMachine&apos;, $computername)

    # Drill down into the Uninstall key using the OpenSubKey method.
    $regkey = $reg.OpenSubKey($UninstallKey)

    # Retrieve an array of strings that contain all the subkey names.
    $subkeys = $regkey.GetSubKeyNames()

    # Open each subkey and use the GetValue method to return the required values for each.
    $counter2 = 0
    foreach ($key in $subkeys) {
        $counter2++
        if ($subkeys.Count -eq $null) {
            Write-Host &quot;Could not connect to $computername continuing ...&quot;
        }
        if (($subkeys.Count -ne $null) -and ($key -notlike &quot;*KB*&quot;)) {
            Write-Progress -Activity &quot;Gathering Key information on $computername&quot; -Status &quot;Processing Key $key&quot; -PercentComplete (100 * ($counter2 / $subkeys.Count)) -ParentId 1
            $thisKey = $UninstallKey + &quot;\\&quot; + $key
            $thisSubKey = $reg.OpenSubKey($thisKey)
            $obj = New-Object PSObject
            $obj | Add-Member -MemberType NoteProperty -Name &quot;ComputerName&quot; -Value $computername
            $obj | Add-Member -MemberType NoteProperty -Name &quot;DisplayName&quot; -Value $($thisSubKey.GetValue(&quot;DisplayName&quot;))
            $obj | Add-Member -MemberType NoteProperty -Name &quot;DisplayVersion&quot; -Value $($thisSubKey.GetValue(&quot;DisplayVersion&quot;))
            $obj | Add-Member -MemberType NoteProperty -Name &quot;InstallLocation&quot; -Value $($thisSubKey.GetValue(&quot;InstallLocation&quot;))
            $obj | Add-Member -MemberType NoteProperty -Name &quot;Publisher&quot; -Value $($thisSubKey.GetValue(&quot;Publisher&quot;))
            $array += $obj
        }
    }
}

# Show and export the matches.
$array | Where-Object { $_.DisplayName -and $_.Publisher -like &quot;*$LikePublisher*&quot; } | Select-Object ComputerName, DisplayName, DisplayVersion, Publisher | Format-Table -AutoSize
$array | Where-Object { $_.DisplayName -and $_.Publisher -like &quot;*$LikePublisher*&quot; } | Select-Object ComputerName, DisplayName, DisplayVersion, Publisher | Export-Csv $outfile -NoTypeInformation
</code></pre>
<h2>Notes</h2>
<ul><li>Update 2026-09-20 (v1.1): the export now uses <code>-NoTypeInformation</code>. Without it, Windows PowerShell 5.1 and earlier write a <code>#TYPE ...</code> line at the top of the CSV that breaks most importers.</li>
</ul>
]]></content:encoded>
      <category>PowerShell</category>
      <category>Scripts</category>
    </item>
    <item>
      <title>vol maxfiles</title>
      <link>https://www.techcolumnist.com/2013/08/13/vol-maxfiles/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2013/08/13/vol-maxfiles/</guid>
      <pubDate>Tue, 13 Aug 2013 15:03:15 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>I was asked the other day what happens with maxfiles. Maxfiles has to do with inodes on the NetApp.</description>
      <content:encoded><![CDATA[<p>I was asked the other day what happens with maxfiles. Maxfiles has to do with inodes on the NetApp. This can get full due to many small files in a volume and you can still have plenty of space but not have enough inodes.</p>
<p>Basically, increasing the maxfiles cannot be reversed, however when you grow a volume the maxfiles grows as well. When you shrink a volume the maxfiles is still set at what it was and is not shrunk.</p>
<p>The maxfiles also grows if you’ve set a higher maxfiles than what the volume would originally specify but you grow the volume and the new maxfiles for that volume size is higher than what you’ve previously set.</p>
<pre><code class="language-text">na-ifas-01: vol create testvol1 aggr0 10g
Creation of volume &apos;testvol1&apos; with size 10g on containing aggregate
&apos;aggr0&apos; has completed.
na-ifas-01: maxfiles testvol1
Volume testvol1: maximum number of files is currently 311280 (96 used).
na-ifas-01: maxfiles testvol1 411280

The new maximum number of files specified is more than twice as big as
it needs to be, based on current usage patterns. This invocation of the
operation on the specified volume will allow disk space consumption for
files to grow up to the new limit depending on your workload. The maxfiles
setting cannot be lowered below the point of any such additional disk space
consumption and any additional disk space consumed can never be reclaimed.
Also, such consumption of additional disk space could result in less
available memory after an upgrade.
The new maximum number of files will be rounded to 411270.

Are you sure you want to change the maximum number of files? y
na-ifas-01: maxfiles testvol1
Volume testvol1: maximum number of files is currently 411270 (96 used).
na-ifas-01: vol size testvol1 20g
vol size: Flexible volume &apos;testvol1&apos; size set to 20g.
na-ifas-01: maxfiles testvol1
Volume testvol1: maximum number of files is currently 622580 (96 used).

na-ifas-01: vol create testvol2 aggr0 10g
Creation of volume &apos;testvol2&apos; with size 10g on containing aggregate
&apos;aggr0&apos; has completed.
na-ifas-01: vol size testvol2 20g
vol size: Flexible volume &apos;testvol2&apos; size set to 20g.
na-ifas-01: maxfiles testvol2
Volume testvol2: maximum number of files is currently 622580 (96 used).
na-ifas-01: vol size testvol2 10g
vol size: Flexible volume &apos;testvol2&apos; size set to 10g.
na-ifas-01: maxfiles testvol2
Volume testvol2: maximum number of files is currently 622580 (96 used).
</code></pre>
]]></content:encoded>
      <category>Engineering</category>
    </item>
    <item>
      <title>NetApp: cli vol create – match SM2.2 vols</title>
      <link>https://www.techcolumnist.com/2013/08/01/netapp-cli-vol-create-match-sm2-2-vols/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2013/08/01/netapp-cli-vol-create-match-sm2-2-vols/</guid>
      <pubDate>Thu, 01 Aug 2013 17:12:34 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>2 creates as a vol type but via CLI, some of us still batch out our commands in notepad so here yah go: SAN: NAS:</description>
      <content:encoded><![CDATA[<p>This is to match what System Manger 2.2 creates as a vol type but via CLI, some of us still batch out our commands in notepad so here yah go:</p>
<p>SAN:</p>
<pre><code class="language-bash">nosnap=on, nosnapdir=off, minra=off, no_atime_update=off, nvfail=off, ignore_inconsistent=off, snapmirrored=off, create_ucode=on, convert_ucode=on, maxdirsize=xxxx, schedsnapname=ordinal, fs_size_fixed=off, guarantee=volume, svo_enable=off, svo_checksum=off, svo_allow_rman=off, svo_reject_errors=off, no_i2p=on, fractional_reserve=0, extent=off, try_first=volume_grow, read_realloc=off, snapshot_clone_dependency=off, dlog_hole_reserve=off, nbu_archival_snap=off

vol create {volname} {aggr} {size}g
vol options {volname} nosnap on
vol options {volname} convert_ucode on
vol options {volname} no_i2p on
vol options {volname} fractional_reserve 0
snap reserve {volname} 0
vol autosize {volname} -m {size+20%} -i {50g}
snap autodelete {volname} on
</code></pre>
<p>NAS:</p>
<pre><code class="language-bash">nosnap=off, nosnapdir=off, minra=off, no_atime_update=off, nvfail=off, ignore_inconsistent=off, snapmirrored=off, create_ucode=on, convert_ucode=on, maxdirsize=xxxx, schedsnapname=ordinal, fs_size_fixed=off, guarantee=volume, svo_enable=off, svo_checksum=off, svo_allow_rman=off, svo_reject_errors=off, no_i2p=off, fractional_reserve=100, extent=off, try_first=volume_grow, read_realloc=off, snapshot_clone_dependency=off, dlog_hole_reserve=off, nbu_archival_snap=off

vol create {volname} {aggr} {size}g
vol options {volname} convert_ucode on
snap reserve {volname} 5
</code></pre>
]]></content:encoded>
      <category>Engineering</category>
    </item>
    <item>
      <title>Android: Push file to sdcard with adb</title>
      <link>https://www.techcolumnist.com/2013/07/29/android-push-file-to-sdcard-with-adb/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2013/07/29/android-push-file-to-sdcard-with-adb/</guid>
      <pubDate>Mon, 29 Jul 2013 16:47:08 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>I ran into the issue where Windows was not mounting the Phone’s SD card within windows, so I had to revert back to using adb.</description>
      <content:encoded><![CDATA[<p>I ran into the issue where Windows was not mounting the Phone’s SD card within windows, so I had to revert back to using adb. This method requires that you’ve download the drivers for your phone, along with the android sdk (<a href="http://developer.android.com/sdk/index.html">http://developer.android.com/sdk/index.html</a>). The SDK will download and you extract to any location of your choosing. adb is located in {directory}\sdk\platform-tools\</p>
<p>In case you have an issue with getting your remote device to show up for /sdcard/ the following commands will help you out:</p>
<pre><code class="language-text">adb devices
adb push filename.zip /sdcard/
</code></pre>
<p><strong>Example:</strong></p>
<pre><code class="language-text">D:\and\sdk\platform-tools\adb devices
List of devices attached
9132ce46b device

D:\and\sdk\platform-tools\adb push gapps-jb-20130301-signed.zip /sdcard/
3280 KB/s (95417279 bytes in 28.403s)
</code></pre>
<p>To install a new .apk run the following (such as installing a new beta software, etc):</p>
<pre><code class="language-text">D:\and\sdk\platform-tools\adb&lt;filename.apk&gt;
</code></pre>
<p>or to force the install</p>
<pre><code class="language-text">D:\and\sdk\platform-tools\adb -r &lt;filename.apk&gt;
</code></pre>
]]></content:encoded>
      <category>Engineering</category>
    </item>
    <item>
      <title>VMware dump collector and remote syslog</title>
      <link>https://www.techcolumnist.com/2013/07/12/vmware-dump-collector-and-remote-syslog/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2013/07/12/vmware-dump-collector-and-remote-syslog/</guid>
      <pubDate>Fri, 12 Jul 2013 11:12:57 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>The esxcli commands to configure a VMware ESXi network core dump collector, kept here because I always forget them.</description>
      <content:encoded><![CDATA[<p>Because I always forget them, here are the commands:</p>
<pre><code class="language-text">esxcli system coredump network get
esxcli system coredump network set --interface-name vmk0 --server-ipv4 10.0.1.11 --server-port 6500
esxcli system coredump network set --enable true
esxcli system coredump network get
esxcli system coredump network check

esxcli system syslog config get
esxcli system syslog config set --loghost=&quot;tcp://10.0.1.11:514&quot;;
esxcli network firewall ruleset set --ruleset-id=syslog --enabled=true
esxcli network firewall refresh
esxcli system syslog reload
</code></pre>
]]></content:encoded>
      <category>Engineering</category>
    </item>
    <item>
      <title>Update Disk Label</title>
      <link>https://www.techcolumnist.com/2013/06/22/update-disk-label/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2013/06/22/update-disk-label/</guid>
      <pubDate>Sat, 22 Jun 2013 14:24:10 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>Have you ever had a system where you were running 7.3.x and got a new disk shelf intended for an 8.x release, and the disks show as broken?</description>
      <content:encoded><![CDATA[<p>Have you ever had a system where you were running 7.3.x and got a new disk shelf intended for an 8.x release, and the disks show as broken?</p>
<pre><code class="language-text">Broken disks

RAID Disk Device HA SHELF BAY CHAN Pool Type RPM Used (MB/blks) Phys (MB/blks)
--------- ------ ------------- ---- ---- ---- ----- -------------- --------------
label version 0d.01.0 0d 1 0 SA:B - BSAS 7200 1695466/3472315904 1695759/3472914816
label version 0d.01.2 0d 1 2 SA:B - BSAS 7200 1695466/3472315904 1695759/3472914816
label version 0d.01.3 0d 1 3 SA:B - BSAS 7200 1695466/3472315904 1695759/3472914816
</code></pre>
<p>Here’s the fix:</p>
<pre><code class="language-text">disk assign
priv set diag
labelmaint isolate
label wipe
label wipev1
label makespare
labelmaint unisolate
priv set
</code></pre>
]]></content:encoded>
      <category>Engineering</category>
    </item>
    <item>
      <title>Unown Disks – Remove Disk Ownership</title>
      <link>https://www.techcolumnist.com/2013/04/24/unown-disks-remove-disk-ownership/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2013/04/24/unown-disks-remove-disk-ownership/</guid>
      <pubDate>Wed, 24 Apr 2013 12:51:21 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>A quick command that I always forget to remember when you want to unown disks from a NetApp where nether filer owns the disks.</description>
      <content:encoded><![CDATA[<p>A quick command that I always forget to remember when you want to unown disks from a NetApp where nether filer owns the disks. This can happen when you move a shelf from one controller pair to another.</p>
<pre><code class="language-text">disk assign {diskid} -s unowned -f
</code></pre>
]]></content:encoded>
      <category>Engineering</category>
    </item>
    <item>
      <title>SnapManager for Exchange: Backup Tasks</title>
      <link>https://www.techcolumnist.com/2013/02/06/snapmanager-for-exchange-backup-tasks/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2013/02/06/snapmanager-for-exchange-backup-tasks/</guid>
      <pubDate>Wed, 06 Feb 2013 14:40:34 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>Notes on SnapManager for Exchange backup tasks, and why the task-creation wizard is so frustrating.</description>
      <content:encoded><![CDATA[<p>There’s quite a lot of information out there on backup tasks, my biggest frustration is that the wizard is not very standard on creating the backup tasks, it’s much easier if you know what you’re doing to create the tasks manually and leave the backup wizard outta the picture.</p>
<p>Here’s the common command line arguments that come with the new-backup cmdlet</p>
<pre><code class="language-text">new-backup -Clusteraware &apos;True|False&apos; -lcr &apos;True|False&apos; -VerifyOnDestVolumes &apos;src_storage_system_list:src_vol:dest_storage_system:dest_vol&apos; -Verify &apos;True|False&apos; -Server &apos;server_name&apos; -StorageGroup &apos;storage_grp1, storage_grp2, ...&apos; -ManagementGroup &apos;Standard|Weekly|Daily&apos; -ActiveDatabaseOnly &apos;True|False&apos; -PassiveDatabaseOnly &apos;True|False&apos; -BackupTargetServer &apos;server name&apos; -ActivationPreference &apos;ActivationPreferenceNum&apos; -UpdateMirror &apos;True|False&apos; -VerDestVolume &apos;True|False&apos; -NoUTMRestore &apos;True|False&apos; -NoTruncateLogs &apos;False&apos; -Throttle &apos;throttle_val&apos; -VerificationServer &apos;server_name&apos; -UseMountPoint &apos;True|False&apos; -CCRActiveNode Boolean &apos;True|False&apos; -MountPointDir &apos;mountpoint_dir&apos; -RetainBackups &apos;no_of_days_to_retain_backup&apos; -RetainDays &apos;no_of_days_delete_backup&apos; -Command &apos;True|False&apos; -RunCommand &apos;win_path_and_script_name&apos; -GenericNaming &apos;True|False&apos; -BackupCopyRemoteCCRNode Boolean &apos;True|False&apos; -RecoveryPoint &apos;win_path_and_script_name&apos; -ReportProgress &apos;True|False&apos; -ArchiveBackup &apos;True|False&apos; -ArchiveBackupCopyRemoteCCRNode &apos;True|False&apos; -ArchivedBackupRetention &apos;Hourly|Monthly|Daily|Weekly|Unlimited&apos; -RetainUtmBackups &apos;no_of_log_backups_to_retain&apos;
</code></pre>
<p>I’ve found this one to be useful to backup all members of a DAG, active databases are full backups, secondary/passive databases are copy based backups, no up to the minute backups:</p>
<pre><code class="language-text">new-backup –Server &apos;dagname&apos; –ClusterAware –ManagementGroup &apos;Standard/Daily/Weekly&apos; –RetainDays xx –NoUTMRestore –ActiveDatabaseOnly -UseMountPoint –MountPointDir &apos;C:\Program Files\NetApp\Snap Manager for Exchange\SnapMgrMountPoint&apos; –RemoteAdditionalCopyBackup $True –RetainRemoteAdditionalCopyBackupDays xx
</code></pre>
<p>This one requires an <strong>individual task</strong> on <strong>every server</strong> in the DAG however this task will only run on one of the servers, the server that holds the cluster role. This allows for all databases to be backed up and still be able to backup your databases if your nodes fail. This command also allows you to add databases to your exchange environment and not have to modify your backup jobs.</p>
]]></content:encoded>
      <category>Engineering</category>
    </item>
    <item>
      <title>Check for queued autosupports</title>
      <link>https://www.techcolumnist.com/2013/01/14/check-for-queued-autosupports/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2013/01/14/check-for-queued-autosupports/</guid>
      <pubDate>Mon, 14 Jan 2013 18:34:57 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>NetApp has released some commands in the 8.1.x code line release to check autosupport information.</description>
      <content:encoded><![CDATA[<p>I’ve always wondered where the autosupports were held on the NetApp, specifically to see if my autosupports are being processed on new client sites. I’m not sure about other releases, but on 8.x releases (they’re most likely in the same location), they’re stored here:</p>
<p>\\{netapp}\etc$\log\autosupport</p>
<p>NetApp has released some commands in the 8.1.x code line release to check autosupport information.</p>
<pre><code class="language-text">netapp&gt; autosupport
autosupport destinations
autosupport history
autosupport manifest
autosupport trigger
</code></pre>
<p>You will get some outputs like this:</p>
<pre><code class="language-text">netapp&gt; autosupport history show
Seq                                    Attempt Last
Num   Destination Status               Count   Update
----- ----------- -------------------- ------- --------------------
66
      smtp        ignore               1       2/6/2013 09:35:46
      http        sent-successful      1       2/6/2013 09:35:49
      noteto      ignore               1       2/6/2013 09:35:46
65
      smtp        collection-failed    -       2/6/2013 09:34:51
      http        collection-failed    -       2/6/2013 09:34:51
      noteto      collection-failed    -       2/6/2013 09:34:51
</code></pre>
]]></content:encoded>
      <category>Engineering</category>
    </item>
    <item>
      <title>VMware 5.x, set drives to SSD</title>
      <link>https://www.techcolumnist.com/2012/11/01/vmware-5-x-set-drives-to-ssd/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2012/11/01/vmware-5-x-set-drives-to-ssd/</guid>
      <pubDate>Thu, 01 Nov 2012 14:48:24 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>Quick post, sometimes VMware doesn’t detect that you have SSD locally and you you need to force it to enable SSD.</description>
      <content:encoded><![CDATA[<p>Quick post, sometimes VMware doesn’t detect that you have SSD locally and you you need to force it to enable SSD. I’ll expand on this more and update this post with some screenshots later.</p>
<pre><code class="language-text">esxcli storage nmp device list
esxcli storage nmp satp rule add -s VMW_SATP_LOCAL -d naa.630f70d99950c0001822f33e055d5d4b -o=enable_ssd
esxcli storage nmp satp rule list | grep enable_ssd
esxcli storage core claimrule load
esxcli storage core claimrule run
esxcli storage core claiming reclaim -d naa.630f70d99950c0001822f33e055d5d4b
esxcli storage core device list -d naa.630f70d99950c0001822f33e055d5d4b
</code></pre>
]]></content:encoded>
      <category>Engineering</category>
    </item>
    <item>
      <title>300k IOPS – NetApp Flash Pools</title>
      <link>https://www.techcolumnist.com/2012/10/25/300k-iops-netapp-flash-pools/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2012/10/25/300k-iops-netapp-flash-pools/</guid>
      <pubDate>Thu, 25 Oct 2012 16:45:05 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>1, Flash Pools. You basically create an aggregate with two different disk types, SSD and SAS or SATA.</description>
      <content:encoded><![CDATA[<p>Had some fun today with a new feature in OnTap 8.1.1, Flash Pools. You basically create an aggregate with two different disk types, SSD and SAS or SATA. In doing this you create another pool of flash just for that specific aggregate. Very cool feature that will bring NetApp a long way in the proving grounds of being able to do spindle effective VDI solutions and other read intensive operations. One thing that also benefits from Flash Pools is the ability to write data to them. The normal Flash Cache does not allow for writes and only serves up reads. Continue on to see an Iometer screenshot.</p>
<p>The configuration for this achievement was:</p>
<ul><li>NetApp FAS6210</li>
<li>1x Flash Pool with 10x SSD, 11x 600GB SAS</li>
<li>One 10G iSCSI connection</li>
<li>Cisco UCS C-class server</li>
</ul>
<p>Here’s a fun picture of the outcome:</p>
]]></content:encoded>
      <category>Engineering</category>
    </item>
    <item>
      <title>NetApp DS4486 Shelf Released in June</title>
      <link>https://www.techcolumnist.com/2012/07/03/netapp-ds4486-shelf-released-in-june/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2012/07/03/netapp-ds4486-shelf-released-in-june/</guid>
      <pubDate>Tue, 03 Jul 2012 23:00:03 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>NetApp quietly released the DS4486 shelf in June: the first fourth-generation shelf, carrying 48 disks in a 4U chassis.</description>
      <content:encoded><![CDATA[<p>NetApp silently released their DS4486 shelf at the beginning of June. This shelf marks the first of the 4th generation shelves carrying a whopping 48 disks in a 4U chassis. This new shelf only supports 3TB disks, but with this type of density you’ll be able to pack quite a punch in a single rack.</p>
<p>Some additional information about the shelves:</p>
<ul><li>Supported on the following systems:
<ul><li>FAS/V3240</li>
<li>FAS/V3270</li>
<li>FAS/V6000 Series</li>
<li>FAS/V6200 Series</li>
</ul>
</li>
<li>Requires Data OnTap 8.1.1RC1 or higher</li>
<li>No MetroCluster support</li>
<li>Requires it’s own stack, meaning no mixing and matching DS4243 shelves, but this is the case with all the disk shelf models that are SAS.</li>
<li>Recommended to have four (4) spare disks instead of two (2), this accommodates the evacuation of two disks that are needed to replace a drive.</li>
<li>You replace 2 disks at once, not just one disk at a time.</li>
<li>Maximum shelves per stack: 5</li>
</ul>
<p>The DS4486 is <a href="http://www.netapp.com/us/products/storage-systems/disk-shelves-and-storage-media/disk-shelves-tech-specs.html">currently available</a> to order via your NetApp sales representative.</p>
<p>Disk Shelf Photo:</p>
<p>Disk Tray Photo:</p>
]]></content:encoded>
      <category>Engineering</category>
    </item>
    <item>
      <title>OnCommand Core – Changing default http port and report images not working</title>
      <link>https://www.techcolumnist.com/2012/03/09/oncommand-core-changing-default-http-port-and-report-images-not-working/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2012/03/09/oncommand-core-changing-default-http-port-and-report-images-not-working/</guid>
      <pubDate>Fri, 09 Mar 2012 11:04:06 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>Fixing NetApp OnCommand Core 5.0 after changing its default HTTP port, which broke the report images.</description>
      <content:encoded><![CDATA[<p>Ran into an issue the other day on where we had to change the default http port on OnCommand Core 5.0 package from NetApp. Basically when changing the port the system prompts you to do a:</p>
<pre><code class="language-powershell">dfm service stop http
dfm service start http
</code></pre>
<p>If you only do this then the images still point to the old http port. What actually needs to be run is:</p>
<pre><code class="language-powershell">dfm service stop
dfm service start
</code></pre>
<p>to fully restart oncommand.</p>
]]></content:encoded>
      <category>Engineering</category>
    </item>
    <item>
      <title>NetApp Disk Shelf Model Breakdown</title>
      <link>https://www.techcolumnist.com/2012/01/17/netapp-disk-shelf-model-breakdown/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2012/01/17/netapp-disk-shelf-model-breakdown/</guid>
      <pubDate>Tue, 17 Jan 2012 00:10:48 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>Just came across some interesting information that I didn’t realize before, but here’s some information on why disk shelves from NetApp are named the way they are.</description>
      <content:encoded><![CDATA[<p>Just came across some interesting information that I didn’t realize before, but here’s some information on why disk shelves from NetApp are named the way they are.</p>
<p>DS{U}{# Disks}{SAS Speed}</p>
<p>For example, the DS4243:<br>
DS – Disk Shelf, 4 – 4U, 24 – the number of disks, 3 – 3Gb/s SAS interface</p>
<p>and the DS2246<br>
DS – Disk Shelf, 2 – 2U, 24 – the number of disks, 6 – 6Gb/s SAS interface</p>
<p>and the DS4486<br>
DS – Disk Shelf, 4 – 4U, 48 – the number of disks, 6 – 6Gb/s SAS interface</p>
]]></content:encoded>
      <category>Engineering</category>
    </item>
    <item>
      <title>Update: New Job! Systems Engineer at CDW</title>
      <link>https://www.techcolumnist.com/2011/12/22/update-new-job-systems-engineer-at-cdw/</link>
      <guid isPermaLink="true">https://www.techcolumnist.com/2011/12/22/update-new-job-systems-engineer-at-cdw/</guid>
      <pubDate>Thu, 22 Dec 2011 17:31:09 GMT</pubDate>
      <dc:creator>Tom Lasswell</dc:creator>
      <description>Look forward to new and exciting blog posts as I take my adventures into specializing in NetApp.</description>
      <content:encoded><![CDATA[<p><img src="https://www.techcolumnist.com/uploads/external/eef9991b2bc2.gif" alt="Update: New Job! Systems Engineer at CDW"></p>
<p>Look forward to new and exciting blog posts as I take my adventures into specializing in NetApp. I’ll most likely be more active with tips and tricks that will be tailored around new experiences. Stay tuned!</p>
<p>Keep in mind that this is my personal blog. The views and opinions expressed on this site do not represent my employer.</p>
<p><img alt="CDW" src="https://www.techcolumnist.com/uploads/external/eef9991b2bc2.gif"></p>
]]></content:encoded>

    </item>
  </channel>
</rss>
