<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:blogger='http://schemas.google.com/blogger/2008' xmlns:georss='http://www.georss.org/georss' xmlns:gd="http://schemas.google.com/g/2005" xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-9129463197233057363</id><updated>2026-03-26T19:36:08.969-05:00</updated><category term="How To"/><category term="NetQoS"/><category term="NPC"/><category term="My Tools"/><category term="NetVoyant"/><category term="Reviews"/><category term="SuperAgent ADA"/><category term="Mobile"/><category term="Throw Back"/><category term="Just for fun"/><category term="Mini"/><category term="Raspberry Pi"/><category term="ReporterAnalyzer NFA"/><category term="GXMLG"/><category term="LogicMonitor"/><category term="Social Networking"/><category term="NPC Browser View"/><category term="bittorrent"/><category term="iOS Apps"/><category term="Excel"/><category term="ODBC"/><category term="Web"/><category term="DataSource"/><category term="Python"/><category term="Docker"/><category term="Graph"/><category term="JavaScript"/><category term="Postman"/><category term="Powershell"/><category term="QRCode"/><category term="Videos"/><category term="XML"/><category term="3D Printing"/><category term="Ansible"/><category term="Cloud"/><category term="Grammar"/><category term="Groovy"/><category term="SNMP"/><category term="SSH"/><category term="SharePoint"/><category term="Ubuntu"/><title type='text'>Stuart&#39;s Semi-Professional Blog</title><subtitle type='html'>I&#39;m an engineer who doesn&#39;t care for a lot of fluff for fluff&#39;s sake.</subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://stuart.weenig.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default?redirect=false'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><link rel='next' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default?start-index=26&amp;max-results=25&amp;redirect=false'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>226</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-3534398351250282148</id><published>2026-01-15T13:03:00.003-06:00</published><updated>2026-01-15T13:03:20.408-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="LogicMonitor"/><title type='text'>LogicMonitor and Postman</title><content type='html'>&lt;div&gt;In LM&lt;/div&gt;&lt;ol style=&quot;text-align: left;&quot;&gt;&lt;li&gt;Generate a bearer token.&lt;/li&gt;&lt;/ol&gt;&lt;div&gt;In Postman&lt;/div&gt;&lt;ol style=&quot;text-align: left;&quot;&gt;&lt;li&gt;Click Import and paste https://www.logicmonitor.com/swagger-ui-master/api-v3/dist/swagger.json. This should start the import process.&amp;nbsp;&lt;/li&gt;&lt;li&gt;Before clicking the import button, click the gear to view import settings.&amp;nbsp;&lt;/li&gt;&lt;li&gt;&amp;nbsp;Make sure &quot;Always inherit authentication&quot; is checked on.&amp;nbsp;&lt;/li&gt;&lt;li&gt;Go to the collection root and select the auth tab.&amp;nbsp;&lt;/li&gt;&lt;li&gt;Change the auth type to &quot;Bearer Token&quot; and put {{bearer}} as the token.&amp;nbsp;&lt;/li&gt;&lt;li&gt;Go to the scripts tab and add this to the pre-request script: pm.request.headers.add({key: &#39;X-Version&#39;, value: &#39;3&#39;})&amp;nbsp;&lt;/li&gt;&lt;li&gt;Save the collection.&amp;nbsp;&lt;/li&gt;&lt;li&gt;Create a new environment with the following variables. You just need one for the bearer token. You should set the type to &#39;secret&#39; for sensitive credentials.&lt;/li&gt;&lt;ol&gt;&lt;li&gt;url – https://&amp;lt;portalname&amp;gt;.logicmonitor.com/santaba/rest&lt;/li&gt;&lt;li&gt;bearer - set to type &quot;secret&quot; and use the token generated (without prepending &quot;bearer &quot;)&lt;/li&gt;&lt;/ol&gt;&lt;/ol&gt;</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/3534398351250282148/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2026/01/logicmonitor-and-postman.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/3534398351250282148'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/3534398351250282148'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2026/01/logicmonitor-and-postman.html' title='LogicMonitor and Postman'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-6070398169560615687</id><published>2025-12-08T10:12:00.002-06:00</published><updated>2025-12-08T10:12:15.726-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="DataSource"/><category scheme="http://www.blogger.com/atom/ns#" term="LogicMonitor"/><title type='text'>Tracking LogicMonitor Version</title><content type='html'>&lt;p&gt;&amp;nbsp;I like to know when my LM portal has been upgraded. I use this inside a Datasource:&lt;/p&gt;
&lt;div style=&quot;background-color: black; color: #6aa84f; font-family: courier; text-align: left;&quot;&gt;&lt;b&gt;import com.santaba.agent.util.Settings&lt;br /&gt;import groovy.json.*&lt;br /&gt;def account = hostProps.get(&quot;lmaccount&quot;)?:Settings.getSetting(Settings.AGENT_COMPANY)&lt;br /&gt;def jsondata = new URL(&quot;https://${account}.logicmonitor.com/santaba/rest/version&quot;).getText()&lt;br /&gt;def versiondata = new JsonSlurper().parseText(jsondata)&lt;br /&gt;println(versiondata.data.version.major)&lt;/b&gt;&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;&lt;p&gt;I then set a delta &amp;gt; 0 alert so that I get an alert whenever the version changes. You can play with the alert clear interval so that the alert stays open long enough for you to notice it (otherwise the alert clears on the next poll where the new version doesn&#39;t differ from the new version).&amp;nbsp;&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/6070398169560615687/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2025/12/tracking-logicmonitor-version.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/6070398169560615687'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/6070398169560615687'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2025/12/tracking-logicmonitor-version.html' title='Tracking LogicMonitor Version'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-8706630296416335512</id><published>2025-05-16T10:23:00.003-05:00</published><updated>2025-05-16T10:23:31.783-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Powershell"/><title type='text'>Touch in Powershell</title><content type='html'>&lt;p&gt;Linux has Touch. I needed touch today in PowerShell. This is what I came up with.&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;span style=&quot;color: #6aa84f; font-family: courier;&quot;&gt;&lt;b style=&quot;background-color: black;&quot;&gt;function touch {&lt;br /&gt;&amp;nbsp; &amp;nbsp; param (&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; [Parameter(Mandatory=$true, ValueFromRemainingArguments=$true)]&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; [string[]]$Paths&lt;br /&gt;&amp;nbsp; &amp;nbsp; )&lt;br /&gt;&amp;nbsp; &amp;nbsp; foreach ($Path in $Paths) {&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; $dir = Split-Path $Path&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (-not (Test-Path $dir)) {&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; New-Item -ItemType Directory -Path $dir -Force | Out-Null&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (-not (Test-Path $Path)) {&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; New-Item -ItemType File -Path $Path -Force | Out-Null&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;br /&gt;&amp;nbsp; &amp;nbsp; }&lt;br /&gt;}&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Add it to your PowerShell profile by pasting the function into your current PowerShell window then:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot;background-color: black; color: #6aa84f; font-family: courier;&quot;&gt;&lt;b&gt;touch $PROFILE&lt;br /&gt;notepad $PROFILE&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Paste the function into the text file and save it. Then all future PowerShell sessions can use:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot;background-color: black; color: #6aa84f; font-family: courier;&quot;&gt;&lt;b&gt;touch &amp;lt;path to your file&amp;gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;One thing I built into it was to build out the intermediary folders in case they don&#39;t exist.&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/8706630296416335512/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2025/05/touch-in-powershell.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/8706630296416335512'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/8706630296416335512'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2025/05/touch-in-powershell.html' title='Touch in Powershell'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-5628535761322432646</id><published>2025-05-05T11:23:00.010-05:00</published><updated>2025-05-05T11:23:58.084-05:00</updated><title type='text'>Using Screen in Linux</title><content type='html'>&lt;h3 style=&quot;text-align: left;&quot;&gt;Create session:&lt;/h3&gt;&lt;p&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;screen -S &amp;lt;sessionname&amp;gt;&lt;/span&gt;&lt;/p&gt;&lt;h3 style=&quot;text-align: left;&quot;&gt;Disconnect from session&lt;/h3&gt;&lt;p&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;Ctrl+A+D&lt;/span&gt;&lt;/p&gt;&lt;h3 style=&quot;text-align: left;&quot;&gt;Reconnect to session&lt;/h3&gt;&lt;p&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;screen -r &amp;lt;sessionname&amp;gt;&lt;/span&gt;&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/5628535761322432646/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2025/05/using-screen-in-linux.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/5628535761322432646'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/5628535761322432646'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2025/05/using-screen-in-linux.html' title='Using Screen in Linux'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-3681226883551778457</id><published>2025-04-28T10:44:00.001-05:00</published><updated>2025-04-28T10:44:25.253-05:00</updated><title type='text'>Python Progress Bar</title><content type='html'>&lt;p&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;&lt;b&gt;pip install tqdm&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;&lt;b&gt;from tqdm import tqdm&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;&lt;b&gt;from time import sleep&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;&lt;b&gt;for i in tqdm(range(1,10+1)):&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;&lt;b&gt;&amp;nbsp; sleep(0.5)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/3681226883551778457/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2025/04/python-progress-bar.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/3681226883551778457'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/3681226883551778457'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2025/04/python-progress-bar.html' title='Python Progress Bar'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-6703305401400168285</id><published>2025-04-16T10:31:00.004-05:00</published><updated>2025-04-16T10:31:44.789-05:00</updated><title type='text'>Running one Docker container like another</title><content type='html'>&lt;p&gt;&amp;nbsp;I found &lt;a href=&quot;https://github.com/lavie/runlike&quot; target=&quot;_blank&quot;&gt;this tool&lt;/a&gt; and it was handy.&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/6703305401400168285/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2025/04/running-one-docker-container-like.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/6703305401400168285'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/6703305401400168285'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2025/04/running-one-docker-container-like.html' title='Running one Docker container like another'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-2984759536783273299</id><published>2025-03-12T16:37:00.001-05:00</published><updated>2025-03-12T16:45:54.530-05:00</updated><title type='text'>Ansible Blocks with Nested When Statements</title><content type='html'>&lt;table style=&quot;width:100%&quot;&gt;
    &lt;tr&gt;&lt;td&gt;inventory.yml&lt;/td&gt;&lt;/tr&gt;
    &lt;tr style=&quot;color: #6aa84f; font-family: courier;background-color: black;&quot;&gt;
        &lt;td&gt;&lt;pre&gt;all:
  hosts:
    device1:
      interfaces:
        GigabitEthernet0:
          description: Internet Uplink
        GigabitEthernet0/0/0:
        GigabitEthernet0/0/1:&lt;/pre&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;playbook.yml&lt;/td&gt;&lt;/tr&gt;
    &lt;tr style=&quot;color: #6aa84f; font-family: courier;background-color: black;&quot;&gt;
        &lt;td&gt;&lt;pre&gt;---
  - name: Main Play
    hosts: all
    gather_facts: false
    vars:
      ansible_connection: network_cli
      ansible_network_os: ios
    tasks:
      - name: Gather facts
        cisco.ios.ios_facts:
          gather_subset: all
      - name: Configure Interfaces
        when: interfaces is defined
        block:
          - name: Set interface descriptions
            loop: &quot;{{ interfaces | dict2items }}&quot;
            when: item.value.description is defined
            cisco.ios.ios_config:
              lines:
                - &quot;description {{ item.value.description }}&quot;
              parents: &quot;interface {{ item.key }}&quot;&lt;/pre&gt;&lt;/td&gt;
    &lt;/tr&gt;
&lt;/table&gt;</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/2984759536783273299/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2025/03/ansible-blocks-with-nested-when.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/2984759536783273299'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/2984759536783273299'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2025/03/ansible-blocks-with-nested-when.html' title='Ansible Blocks with Nested When Statements'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-5277309003065869488</id><published>2025-02-03T16:19:00.005-06:00</published><updated>2025-02-03T16:19:23.508-06:00</updated><title type='text'>Python For Loops with Complex Values</title><content type='html'>&lt;p&gt;&amp;nbsp;I found myself needing to loop through a dictionary that looked like this:&lt;/p&gt;&lt;div style=&quot;text-align: left;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: black; color: #6aa84f;&quot;&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;data = {&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;&amp;nbsp; &amp;nbsp; &#39;key1&#39;: [&#39;id1&#39;, &#39;subnet1&#39;],&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;&amp;nbsp; &amp;nbsp; &#39;key2&#39;: [&#39;id2&#39;, &#39;subnet2&#39;],&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;&amp;nbsp; &amp;nbsp; &#39;key3&#39;: [&#39;id3&#39;, &#39;subnet3&#39;]&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;p&gt;Normally, I&#39;d do this:&lt;/p&gt;&lt;div style=&quot;text-align: left;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: black; color: #6aa84f;&quot;&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;for key, v in data.items():&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;&amp;nbsp; id = v[0]&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;&amp;nbsp; subnet = v[1]&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;&amp;nbsp; print(f&quot;{key}: {id} {subnet}&quot;)&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;p&gt;But today I figured I&#39;d let AI suggest an efficiency and it came through. Instead of the above, I can do just this:&lt;/p&gt;&lt;div style=&quot;text-align: left;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: black; color: #6aa84f;&quot;&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;for key, (id, subnet) in data.items():&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;&amp;nbsp; print(f&quot;{key}: {id} {subnet}&quot;)&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/5277309003065869488/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2025/02/python-for-loops-with-complex-values.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/5277309003065869488'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/5277309003065869488'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2025/02/python-for-loops-with-complex-values.html' title='Python For Loops with Complex Values'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-3472773604366946125</id><published>2025-01-09T11:13:00.002-06:00</published><updated>2026-03-26T14:29:15.423-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="How To"/><category scheme="http://www.blogger.com/atom/ns#" term="Powershell"/><category scheme="http://www.blogger.com/atom/ns#" term="SSH"/><title type='text'>Using SSH on Windows with Powershell</title><content type='html'>&lt;p&gt;I&#39;ve come to prefer CLIs over GUIs. Rather than install Putty (or grab the portal version and copy it to the server I want to use), I prefer to SSH directly from Powershell since Microsoft started including a client in the Windows Features.&lt;/p&gt;&lt;p&gt;First, make sure it&#39;s installed. From a Powershell prompt running with elevated privileges:&lt;/p&gt;&lt;div style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;background-color: black; color: #6aa84f; font-family: courier;&quot;&gt;&lt;b&gt;if (-not (Get-WindowsCapability -Online | Where-Object { $_.Name -like &#39;OpenSSH.Client*&#39; -and $_.State -eq &#39;Installed&#39; })) {&lt;br /&gt;&amp;nbsp; &amp;nbsp; Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0&lt;br /&gt;}&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;This will check if the SSH client is installed and if not, it will install it.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Using SSH is pretty much the same as it is on Linux then:&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #6aa84f; font-family: courier;&quot;&gt;&lt;b style=&quot;background-color: black;&quot;&gt;ssh &amp;lt;username&amp;gt;@&amp;lt;hostname/fqdn/ip&amp;gt; -p &amp;lt;port&amp;gt;&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;However, if you do that, you&#39;ll be prompted for the password of the user. You could enable key based login, which would essentially use a huge alternate password stored in a file on your computer to connect to the target.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;First make sure your system has a key. If you already have keys, this will overwrite them, so use carefully. You can always change the name to something unique to make sure it doesn&#39;t get overwritten. Just update the name in the rest of the commands here.&amp;nbsp;&lt;/div&gt;&lt;div&gt;On Linux&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span style=&quot;color: #6aa84f; font-family: courier;&quot;&gt;&lt;b style=&quot;background-color: black;&quot;&gt;ssh-keygen -f ~/.ssh/id_rsa -b 4096 -N &quot;&quot;&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #6aa84f; font-family: courier;&quot;&gt;&lt;b style=&quot;background-color: black;&quot;&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;On Windows&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #6aa84f; font-family: courier;&quot;&gt;&lt;b style=&quot;background-color: black;&quot;&gt;ssh-keygen -f ~/.ssh/id_rsa -b 4096 -N &#39;&quot;&quot;&#39;&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Then:&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;For Linux&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #6aa84f; font-family: courier;&quot;&gt;&lt;b style=&quot;background-color: black;&quot;&gt;ssh-copy-id -i ~/.ssh/id_rsa.pub -p 22 &amp;lt;username&amp;gt;@&amp;lt;hostname/fqdn/ip&amp;gt;&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;For Windows&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #6aa84f; font-family: courier;&quot;&gt;&lt;b style=&quot;background-color: black;&quot;&gt;type ~\.ssh\id_rsa.pub | ssh &amp;lt;username&amp;gt;@&amp;lt;hostname/fqdn/ip&amp;gt; &quot;cat &amp;gt;&amp;gt; ~/.ssh/authorized_keys&quot;&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #6aa84f; font-family: courier;&quot;&gt;&lt;b style=&quot;background-color: black;&quot;&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;Now you should be able to SSH from Powershell using the same command above:&lt;div&gt;&lt;div&gt;&lt;span style=&quot;color: #6aa84f; font-family: courier;&quot;&gt;&lt;b style=&quot;background-color: black;&quot;&gt;ssh &amp;lt;username&amp;gt;@&amp;lt;hostname/fqdn/ip&amp;gt; -p &amp;lt;port&amp;gt;&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;Except that now, the client in Powershell will attempt to pass your key to the target to authenticate with. Since you key contains a super-long alternate password, you will be logged in. One cool thing is that this survives password changes on the target account.&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/3472773604366946125/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2025/01/using-ssh-on-windows-with-powershell.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/3472773604366946125'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/3472773604366946125'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2025/01/using-ssh-on-windows-with-powershell.html' title='Using SSH on Windows with Powershell'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-4468867891008697992</id><published>2024-12-19T14:10:00.003-06:00</published><updated>2024-12-19T14:10:00.116-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Grammar"/><title type='text'>Premises vs. Premise</title><content type='html'>&lt;p&gt;&lt;/p&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;&lt;span style=&quot;color: #4a4a4a; font-family: Helvetica, Arial, sans-serif; font-size: 1.125em;&quot;&gt;One theory or idea is a&lt;/span&gt;&lt;span style=&quot;color: #4a4a4a; font-family: Helvetica, Arial, sans-serif; font-size: 1.125em;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;em style=&quot;box-sizing: inherit; color: #4a4a4a; font-family: Helvetica, Arial, sans-serif; font-size: 1.125em;&quot;&gt;premise&lt;/em&gt;&lt;span style=&quot;color: #4a4a4a; font-family: Helvetica, Arial, sans-serif; font-size: 1.125em;&quot;&gt;.&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;&lt;span style=&quot;color: #4a4a4a; font-family: Helvetica, Arial, sans-serif; font-size: 1.125em;&quot;&gt;Two or more theories or ideas are&lt;/span&gt;&lt;span style=&quot;color: #4a4a4a; font-family: Helvetica, Arial, sans-serif; font-size: 1.125em;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;em style=&quot;box-sizing: inherit; color: #4a4a4a; font-family: Helvetica, Arial, sans-serif; font-size: 1.125em;&quot;&gt;premises&lt;/em&gt;&lt;span style=&quot;color: #4a4a4a; font-family: Helvetica, Arial, sans-serif; font-size: 1.125em;&quot;&gt;.&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;&lt;em style=&quot;box-sizing: inherit; color: #4a4a4a; font-family: Helvetica, Arial, sans-serif; font-size: 1.125em;&quot;&gt;Premises&amp;nbsp;&lt;/em&gt;&lt;span style=&quot;color: #4a4a4a; font-family: Helvetica, Arial, sans-serif; font-size: 1.125em;&quot;&gt;is also a plural noun referring to a piece of land with a set of buildings.&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/h2&gt;&lt;p&gt;&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/4468867891008697992/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2024/12/premises-vs-premise.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/4468867891008697992'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/4468867891008697992'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2024/12/premises-vs-premise.html' title='Premises vs. Premise'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-8302403607830373736</id><published>2024-12-12T15:42:00.001-06:00</published><updated>2024-12-12T15:42:00.183-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="How To"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><title type='text'>Using Python Virtual Environments</title><content type='html'>&lt;div&gt;tl;dr:&lt;/div&gt;&lt;h4 style=&quot;text-align: left;&quot;&gt;Setup:&lt;/h4&gt;&lt;div&gt;&lt;span style=&quot;color: #6aa84f; font-family: courier;&quot;&gt;&lt;b style=&quot;background-color: black;&quot;&gt;mkdir my-new-project&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #6aa84f; font-family: courier;&quot;&gt;&lt;b style=&quot;background-color: black;&quot;&gt;cd my-new-project&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #6aa84f; font-family: courier;&quot;&gt;&lt;b style=&quot;background-color: black;&quot;&gt;python -m venv env&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;&lt;h4 style=&quot;text-align: left;&quot;&gt;Linux Usage:&lt;/h4&gt;&lt;div&gt;&lt;span style=&quot;background-color: black; color: #6aa84f; font-family: courier;&quot;&gt;&lt;b&gt;source env/bin/activate&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;&lt;h4 style=&quot;text-align: left;&quot;&gt;Powershell Usage:&lt;/h4&gt;&lt;div&gt;&lt;span style=&quot;background-color: black; color: #6aa84f; font-family: courier;&quot;&gt;&lt;b&gt;./env/Scripts/Activate.ps1&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;&lt;h4 style=&quot;text-align: left;&quot;&gt;Either usage&lt;/h4&gt;&lt;div&gt;&lt;span style=&quot;background-color: black; color: #6aa84f; font-family: courier;&quot;&gt;&lt;b&gt;pip install requests # and anything else you need&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;&lt;h4 style=&quot;text-align: left;&quot;&gt;Exit&lt;/h4&gt;&lt;div&gt;&lt;span style=&quot;color: #6aa84f; font-family: courier;&quot;&gt;&lt;span style=&quot;background-color: black;&quot;&gt;&lt;b&gt;deactivate&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;span&gt;&lt;a name=&#39;more&#39;&gt;&lt;/a&gt;&lt;/span&gt;&lt;div&gt;Imagine you have a bunch of different school projects, and each one needs its own set of supplies. If you mix all the supplies together, it can get really messy and confusing. Python virtual environments help keep things organized, just like having separate boxes for each project.&lt;/div&gt;&lt;br /&gt;Here are some benefits of using Python virtual environments:&lt;br /&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;Organization: Each project gets its own “box” (virtual environment) with all the tools and libraries it needs. This way, different projects don’t mess with each other.&lt;/li&gt;&lt;li&gt;Avoiding Conflicts: Sometimes, different projects need different versions of the same tool. Virtual environments make sure each project uses the right version without causing problems.&lt;/li&gt;&lt;li&gt;Easy Sharing: If you want to share your project with a friend, you can include the virtual environment. This way, your friend gets all the right tools and versions to run your project without any issues.&lt;/li&gt;&lt;li&gt;Safety: You can try out new tools or updates in a virtual environment without worrying about breaking other projects. It’s like having a safe space to experiment.&lt;/li&gt;&lt;li&gt;Clean Up: When you’re done with a project, you can easily delete its virtual environment. This keeps your computer neat and tidy.&lt;/li&gt;&lt;/ul&gt;&lt;br /&gt;In short, Python virtual environments help you stay organized, avoid problems, and make it easier to share and manage your projects.&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;More here:&amp;nbsp;&lt;a href=&quot;https://www.raspberrypi.com/news/using-python-with-virtual-environments-the-magpi-148&quot;&gt;https://www.raspberrypi.com/news/using-python-with-virtual-environments-the-magpi-148&lt;/a&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/8302403607830373736/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2024/12/using-python-virtual-environments.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/8302403607830373736'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/8302403607830373736'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2024/12/using-python-virtual-environments.html' title='Using Python Virtual Environments'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-120704388464256093</id><published>2024-12-06T15:39:00.001-06:00</published><updated>2024-12-06T15:39:47.888-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="How To"/><category scheme="http://www.blogger.com/atom/ns#" term="LogicMonitor"/><category scheme="http://www.blogger.com/atom/ns#" term="Postman"/><title type='text'>Accessing LM with a bearer token through Postman</title><content type='html'>I&#39;m a day late on this one, but at least it&#39;s good info:&lt;br /&gt;&lt;br /&gt;&lt;a href=&quot;https://community.logicmonitor.com/discussions/product-discussions/accessing-the-logicmonitor-rest-api-with-postman-and-bearer-token-authentication/15828&quot;&gt;Original content is here&lt;/a&gt; for now.&lt;div&gt;&lt;ol style=&quot;text-align: left;&quot;&gt;&lt;li&gt;Download and install &lt;a href=&quot;https://www.getpostman.com/&quot;&gt;Postman&lt;/a&gt;, or use &lt;a href=&quot;http://postman.com/&quot;&gt;http://postman.com&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Launch Postman and create a new collection that will be used for all LogicMonitor API requests:&lt;/li&gt;&lt;ol&gt;&lt;li style=&quot;box-sizing: border-box;&quot;&gt;In Postman, click Import and paste&amp;nbsp;&lt;a class=&quot;lia-external-url&quot; href=&quot;https://www.logicmonitor.com/swagger-ui-master/api-v3/dist/swagger.json&quot; rel=&quot;noopener noreferrer&quot; style=&quot;box-sizing: border-box; font-style: var(--lia-forum-message-link-font-style); font-weight: var(--lia-forum-message-link-font-weight); text-decoration: var(--lia-forum-message-link-decoration); touch-action: manipulation; word-break: break-word;&quot; target=&quot;_blank&quot;&gt;https://www.logicmonitor.com/swagger-ui-master/api-v3/dist/swagger.json&lt;/a&gt;. This should start the import process.&amp;nbsp;&lt;/li&gt;&lt;li style=&quot;box-sizing: border-box;&quot;&gt;Before clicking the import button, click the gear to view import settings.&amp;nbsp;&lt;/li&gt;&lt;li style=&quot;box-sizing: border-box;&quot;&gt;Make sure &quot;Always inherit authentication&quot; is checked on.&lt;/li&gt;&lt;/ol&gt;&lt;li&gt;Configure Authentication:&lt;/li&gt;&lt;ol&gt;&lt;li style=&quot;box-sizing: border-box;&quot;&gt;Go to the collection root and select the auth tab.&lt;/li&gt;&lt;li style=&quot;box-sizing: border-box;&quot;&gt;Change the auth type to &quot;Bearer Token&quot; and put &lt;span style=&quot;background-color: black; color: #6aa84f; font-family: courier;&quot;&gt;&lt;b&gt;{{bearer}}&lt;/b&gt;&lt;/span&gt; as the token.&lt;/li&gt;&lt;li style=&quot;box-sizing: border-box;&quot;&gt;Go to the scripts tab and add this to the pre-request script:&lt;br /&gt;&lt;span style=&quot;color: #6aa84f; font-family: courier;&quot;&gt;&lt;b style=&quot;background-color: black;&quot;&gt;pm.request.headers.add({key: &#39;X-Version&#39;, value: &#39;3&#39;})&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;&lt;li style=&quot;box-sizing: border-box;&quot;&gt;Save the collection.&lt;/li&gt;&lt;/ol&gt;&lt;li&gt;Create a new environment with the following variables. You just need one for the bearer token. You should set the type to &#39;secret&#39; for sensitive credentials.&lt;/li&gt;&lt;ol&gt;&lt;li&gt;url – https://&amp;lt;portalname&amp;gt;.logicmonitor.com/santaba/rest&lt;/li&gt;&lt;ol&gt;&lt;li&gt;If you want to work with the &lt;a href=&quot;https://www.logicmonitor.com/support/lm-logs/sending-logs-to-the-lm-logs-ingestion-api&quot;&gt;LM Ingestion API&lt;/a&gt;, duplicate this environment and change the url to &#39;https://&amp;lt;portalname&amp;gt;.logicmonitor.com/rest&#39; (without &quot;santaba&quot;)&lt;/li&gt;&lt;/ol&gt;&lt;li&gt;bearer – secret – For the current value, be sure to prepend the token with &quot;bearer &quot; (with space)&lt;/li&gt;&lt;/ol&gt;&lt;/ol&gt;&lt;div&gt;And that should do it. You should be able to open any request already defined in the collection you imported and run it.&lt;/div&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/120704388464256093/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2024/12/accessing-lm-with-bearer-token-through.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/120704388464256093'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/120704388464256093'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2024/12/accessing-lm-with-bearer-token-through.html' title='Accessing LM with a bearer token through Postman'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-1876329494932329236</id><published>2024-11-28T13:04:00.002-06:00</published><updated>2024-11-28T13:04:00.237-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="How To"/><category scheme="http://www.blogger.com/atom/ns#" term="Ubuntu"/><title type='text'>Adding Space to an Ubuntu VM</title><content type='html'>&lt;p&gt;I ran out of space on an Ubuntu VM today and had to go through the process of expanding the hard drive.&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;ol style=&quot;text-align: left;&quot;&gt;&lt;li&gt;First, we shut down the VM and reconfigured VMware to let it have a larger hard drive.&amp;nbsp;&lt;/li&gt;&lt;li&gt;Another thing to note is that we used the default settings when configuring the disk when installing Ubuntu.&lt;/li&gt;&lt;li&gt;We downloaded the &lt;a href=&quot;https://gparted.org/livecd.php&quot; target=&quot;_blank&quot;&gt;ISO for GParted&lt;/a&gt;. This is an entirely self contained OS with Gparted installed (along with a few other tools) and mounted it in the optical drive of the VM and booted it up.&lt;/li&gt;&lt;li&gt;When it finished booting, we used GParted to expand the partition to use the extra space on the drive.&lt;/li&gt;&lt;li&gt;Then we ejected the ISO and booted up as normal.&lt;/li&gt;&lt;li&gt;Since we used the default options when installing Ubuntu, it uses a logical volume. We expanded the logical volume to encompass the new expanded size of the partition on the physical (virtual) drive using this:&lt;br /&gt;&lt;span style=&quot;background-color: black; color: #93c47d; font-family: courier;&quot;&gt;&lt;b&gt;sudo lvextend -l 100%VG ubuntu-vg-/ubuntu-lv&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;&lt;li&gt;Then we increased the size of the file system to use the available space in the logical volume using this:&lt;br /&gt;&lt;span style=&quot;background-color: black; color: #93c47d; font-family: courier;&quot;&gt;&lt;b&gt;sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;&lt;div&gt;A simple df -h later and we could see that the drive now had access to the extended space.&lt;/div&gt;&lt;p&gt;&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/1876329494932329236/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2024/11/adding-space-to-ubuntu-vm.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/1876329494932329236'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/1876329494932329236'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2024/11/adding-space-to-ubuntu-vm.html' title='Adding Space to an Ubuntu VM'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-3574510156644926345</id><published>2024-11-21T11:00:00.000-06:00</published><updated>2024-11-21T11:00:00.116-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Docker"/><title type='text'>Freeing up Disk Space when using Docker</title><content type='html'>&lt;p&gt;&amp;nbsp;Turns out there&#39;s a lot of temporary data that is used by Docker. To clean it up, try the following (&lt;a href=&quot;https://stackoverflow.com/a/77062393&quot; target=&quot;_blank&quot;&gt;courtesy Mr Davron&lt;/a&gt;):&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;ol style=&quot;text-align: left;&quot;&gt;&lt;li&gt;Open an elevated Powershell or CMD prompt&lt;/li&gt;&lt;li&gt;`docker system prune --all`&lt;/li&gt;&lt;li&gt;Right mouse click on docker desktop in system tray -&amp;gt; quit&lt;/li&gt;&lt;li&gt;`wsl --shutdown`&lt;/li&gt;&lt;li&gt;`Optimize-VHD -Path &quot;$env:LOCALAPPDATA\Docker\wsl\data\ext4.vhdx&quot; -Mode Full`&lt;/li&gt;&lt;li&gt;Reboot&lt;/li&gt;&lt;/ol&gt;&lt;div&gt;This freed up about 25GB on my machine.&lt;/div&gt;&lt;p&gt;&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/3574510156644926345/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2024/11/freeing-up-disk-space-when-using-docker.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/3574510156644926345'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/3574510156644926345'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2024/11/freeing-up-disk-space-when-using-docker.html' title='Freeing up Disk Space when using Docker'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-4833654264597584243</id><published>2024-11-14T11:39:00.047-06:00</published><updated>2024-11-14T11:39:00.119-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Ansible"/><category scheme="http://www.blogger.com/atom/ns#" term="Docker"/><title type='text'>Using Ansible in Docker (without installing Ansible)</title><content type='html'>&lt;p&gt;tl;dr:&lt;/p&gt;&lt;p&gt;Powershell:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot;background-color: black; color: #93c47d; font-family: courier;&quot;&gt;docker run --rm -v ${PWD}:/ansible/playbooks sweenig/ansible-docker playbook.yml -i inventory&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Linux:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot;background-color: black; color: #93c47d; font-family: courier;&quot;&gt;docker run --rm -v $(pwd):/ansible/playbooks sweenig/ansible-docker playbook.yml -i inventory&lt;/span&gt;&lt;/p&gt;&lt;p&gt;I love Ansible. I love Docker. Running Ansible in Docker not only makes me melt but it means I don&#39;t have to install anything but Docker to run Ansible. Anywhere Docker works, Ansible works. And since I already have Docker installed on any machine I call mine...&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/sweenig/docker-ansible-playbook/tree/master/ansible_lab&quot;&gt;I built a lab&lt;/a&gt; that shows how this can be used. The lab spins up 4 Ubuntu servers, then uses Ansible in a docker container to install a few things. Here&#39;s the shortcut to get everything up and running if you already have Docker installed:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot;background-color: black;&quot;&gt;&lt;span style=&quot;color: #93c47d;&quot;&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;&amp;gt; docker run -it --rm -v ${HOME}:/root -v ${PWD}:/git alpine/git clone&amp;nbsp;https://github.com/sweenig/docker-ansible-playbook.git&lt;br /&gt;&amp;gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;cd .\docker-ansible-playbook\ansible_lab\&lt;br /&gt;&amp;gt; docker compose up -d&lt;br /&gt;&amp;gt;&amp;nbsp;docker run --rm -v ${PWD}:/ansible/playbooks --network=ansible_lab_default sweenig/ansible-docker playbook.yml -i inventory&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style=&quot;background-color: white; color: #333333;&quot;&gt;&lt;span style=&quot;font-family: inherit;&quot;&gt;With these four commands, you:&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;&lt;span style=&quot;background-color: white; color: #333333;&quot;&gt;&lt;span style=&quot;font-family: inherit;&quot;&gt;Pull down the lab files&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;background-color: white; color: #333333;&quot;&gt;&lt;span style=&quot;font-family: inherit;&quot;&gt;Switch into the lab directory&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;background-color: white; color: #333333;&quot;&gt;&lt;span style=&quot;font-family: inherit;&quot;&gt;Start up 4 containers (the equivalent of starting up 4 micro VMs)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;background-color: white; color: #333333;&quot;&gt;&lt;span style=&quot;font-family: inherit;&quot;&gt;Run the Ansible playbook&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;&lt;span style=&quot;color: #333333;&quot;&gt;You used Git and Ansible and had neither installed.&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #333333;&quot;&gt;To shut down the lab, do:&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;background-color: black; color: #93c47d; font-family: courier;&quot;&gt;docker compose down&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #333333;&quot;&gt;In the ansible-lab directory.&lt;/span&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/4833654264597584243/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2024/11/using-ansible-in-docker-without.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/4833654264597584243'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/4833654264597584243'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2024/11/using-ansible-in-docker-without.html' title='Using Ansible in Docker (without installing Ansible)'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-1268758368781810964</id><published>2024-11-07T16:40:00.007-06:00</published><updated>2024-11-08T11:19:45.555-06:00</updated><title type='text'>Using Git without Installing it (through Docker)</title><content type='html'>&lt;p&gt;If you follow this blog, you might already know that I&#39;m a Docker fanboy. Docker containers are like micro-VMs, just lighter and faster. Git is version control software. The nice thing about version control software, or more specifically distributed version control software like Git, is that it not only allows for the storage of blobs of text or bytes but it also allows you to build workflows that enable people to contribute edits to the stored text along with approval and multiple branches.&lt;/p&gt;&lt;p&gt;Installing Git isn&#39;t always needed. Sometimes I just need to clone a repo. If I have Docker installed, I do this (in Powershell):&lt;/p&gt;&lt;p&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;docker run -it --rm -v ${HOME}:/root -v ${PWD}:/git alpine/git clone&amp;nbsp;https://github.com/sweenig/docker-ansible-playbook.git&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Linux/Mac is just as easy:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;docker run -it --rm -v ${HOME}:/root -v $(pwd):/git alpine/git clone&amp;nbsp;https://github.com/sweenig/docker-ansible-playbook.git&lt;/span&gt;&lt;/p&gt;&lt;div&gt;If you want, you can even setup an alias in Bash:&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Linux:&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;alias git=&quot;docker run -ti --rm -v $(pwd):/git -v $HOME/.ssh:/root/.ssh alpine/git&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Or in Windows with Powershell:&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;function git {&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;&amp;nbsp; $allArgs = $PsBoundParameters.values + $args&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;&amp;nbsp; docker run --rm -it -v ${PWD}:/git -v ${HOME}:/root alpine/git $allArgs&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;With Powershell, since the underlying container is running Linux, make sure that the path to the playbook and inventory files doesn&#39;t use the backslash.&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/1268758368781810964/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2024/11/using-git-without-installing-it-through.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/1268758368781810964'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/1268758368781810964'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2024/11/using-git-without-installing-it-through.html' title='Using Git without Installing it (through Docker)'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-8902867581857458080</id><published>2024-10-29T09:18:00.003-05:00</published><updated>2024-10-29T09:18:33.630-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Postman"/><title type='text'>Working with Beyond Trust&#39;s Privileged Remote Access API</title><content type='html'>&lt;p&gt;&amp;nbsp;I recently started a trial with Beyond Trust for their Privileged Remote Access product (fka: Bomgar). It&#39;s an RMM. As with any tool I have, I&#39;m looking to automate it. We have a system of record (SOR) where our targets reside. PRA requires that each of these have a record in PRA in order to use PRA to remote into the target. I&#39;ll be attempting to automate synchronization of our devices from our SOR to PRA using the API. Our trial involved the SaaS version of PRA.&lt;/p&gt;&lt;p&gt;Naturally, my first step was to download their collection into Postman and get started. Actually, the first thing I did was generate the API credentials, which came in the form of an ID and secret. Then I imported the collection into Postman. Unfortunately, I found it a little lacking, so I decided to enhance it using some techniques I&#39;ve learned. This is not a slight against Beyond Trust. Postman is not their product and I didn&#39;t expect their collection to be any more than it was. However, that doesn&#39;t mean it couldn&#39;t be improved. ;-)&lt;/p&gt;&lt;p&gt;First things first, I created an environment. In it I created the ClientID, ClientSecret, and baseUrl variables. It looks like the collection file is dynamically generated from my trial portal, because the collection had a variable called baseUrl which pointed specifically to my trial portal. Because customer data should be in the environment and the collection should reference it using variables, I moved the value to the baseUrl environment variable and deleted the collection variable so that the environment variable would be used instead.&amp;nbsp;&lt;/p&gt;&lt;p&gt;BTPRA uses OAuth2.0, so to make any requests you have to first generate an ephemeral token which will be used as a bearer token in any subsequent requests. The collection didn&#39;t contain a request to obtain this ephemeral token, so I built one called &quot;START HERE&quot;.&amp;nbsp;&lt;/p&gt;&lt;p&gt;The documentation states to make a POST request to https://access.beyondtrustcloud.com/oauth2/token. Unfortunately, this URL is smaller than the baseUrl, so I created a new environment variable called authURL and give it the value of &lt;i&gt;https://access.example.com&lt;/i&gt;. Obviously, not access.example.com, but the URL to my portal.&amp;nbsp;&lt;/p&gt;&lt;p&gt;For the &quot;START HERE&quot; request, I have to include a basic authorization header. I also have to include a grant_type in the body of my post request. The other thing I want to do is parse the response and store the ephemeral access token in a new environment variable. Here&#39;s how I did it.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;ol style=&quot;text-align: left;&quot;&gt;&lt;li&gt;Create a new POST request&lt;/li&gt;&lt;li&gt;Set the url to &lt;i&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;{{authURL}}/oauth2/token&lt;/span&gt;&lt;/i&gt;&lt;/li&gt;&lt;li&gt;On the Authorization tab&lt;/li&gt;&lt;ol&gt;&lt;li&gt;Set the Auth Type to &quot;Basic Auth&quot;&lt;/li&gt;&lt;li&gt;Set the Username to &lt;i&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;{{ClientID}}&lt;/span&gt;&lt;/i&gt;&lt;/li&gt;&lt;li&gt;Set the Password to &lt;i&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;{{ClientSecret}}&lt;/span&gt;&lt;/i&gt;&lt;/li&gt;&lt;/ol&gt;&lt;li&gt;On the Headers tab, add a header:&lt;/li&gt;&lt;ol&gt;&lt;li&gt;&quot;Accept&quot; : &quot;application/json&quot;&lt;br /&gt;This tells Postman to expect the response to be JSON, which we need it to be.&lt;/li&gt;&lt;/ol&gt;&lt;li&gt;On the Body tab:&lt;/li&gt;&lt;ol&gt;&lt;li&gt;Pick &quot;x-www-form-urlencoded&quot; (there are other ways to do this, I know, but this works fine)&lt;/li&gt;&lt;li&gt;Add &quot;grant_type&quot; : &quot;client_credentials&quot;&lt;/li&gt;&lt;/ol&gt;&lt;li&gt;On the Scripts tab, we&#39;re going to write a script that will parse the response and set an environment variable containing our ephemeral access token.&lt;/li&gt;&lt;ol&gt;&lt;li&gt;Select &quot;Post-response&quot; and enter the following script:&lt;br /&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;try {&lt;br /&gt;&amp;nbsp; &amp;nbsp; var json = JSON.parse(pm.response.text());&lt;br /&gt;&amp;nbsp; &amp;nbsp; pm.environment.set(&quot;bearToken&quot;, json.access_token);&lt;br /&gt;} catch (e) {console.log(e);}&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/ol&gt;Save and run your request. If you set everything up right, you should see a response containing your token. If you check your environment, you should see a new variable called bearToken, with the value of the access_token in the response.&amp;nbsp;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;One thing remains: we need to tell all requests in the collection to use this token. Luckily this is pretty easy to do since all the requests already inherit the authorization from their parent, the collection. Opening the collection, I went to the Authorization tab and set the &quot;Auth Type&quot; to &quot;Bearer Token&quot;. Then in the Token field, I put &lt;i&gt;&lt;span style=&quot;font-family: courier;&quot;&gt;{{bearToken}}&lt;/span&gt;&lt;/i&gt;.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;And that&#39;s it. Now you should be able to open any request from the collection and run it, providing any parameters the request requires.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;This configuration should keep everything about the API separate from the my specific settings meaning I could delete and reimport the collection (don&#39;t delete the START HERE request).&amp;nbsp;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/8902867581857458080/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2024/10/working-with-beyond-trusts-privileged.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/8902867581857458080'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/8902867581857458080'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2024/10/working-with-beyond-trusts-privileged.html' title='Working with Beyond Trust&#39;s Privileged Remote Access API'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-6872211441463347305</id><published>2024-10-24T12:21:00.004-05:00</published><updated>2024-11-06T08:24:53.056-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><title type='text'>Favorite way to troubleshoot Python scripts</title><content type='html'>&lt;p&gt;I recently discovered a great way to make sure that Python scripts give you the information you need when there&#39;s a failure. I often run Python scripts inside Docker containers. They either log locally to a file or send logs to a log aggregator (LM Logs). As such, there&#39;s not always someone monitoring the stdout pipe of the Python script. If it fails, often the best piece of information is captured using a try/except block. You can have extra data printed out to stdout or even sent out to the log aggregator. This would look something like this:&lt;/p&gt;&lt;pre style=&quot;background-color: #595959; color: white; font-family: &amp;quot;Courier New&amp;quot;, Courier, monospace; margin: 10px; padding: 10px;&quot;&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; try:&lt;br /&gt;...&amp;nbsp; &amp;nbsp;{}[&quot;shrubbery&quot;]&lt;br /&gt;... except Exception as e:&lt;br /&gt;...&amp;nbsp; &amp;nbsp;print(e)&lt;br /&gt;...&lt;br /&gt;&#39;shrubbery&#39;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now that wasn&#39;t helpful was it? If the only logs we had seen were logs about successful operation then suddenly a log that says &quot;shrubbery&quot;, we really wouldn&#39;t know what was going on. Luckily, there are a few things we can add to the exception output that clarify things:&lt;/p&gt;&lt;pre style=&quot;background-color: #595959; margin: 10px; padding: 10px;&quot;&gt;&lt;span style=&quot;color: white;&quot;&gt;&amp;gt;&amp;gt;&amp;gt; import sys
&amp;gt;&amp;gt;&amp;gt; try:
...   {}[&quot;shrubbery&quot;]
... except Exception as e:
...   print(f&quot;There was an unexpected error: {e}: \nError on line {sys.exc_info()[-1].tb_lineno}&quot;)
...
There was an unexpected error: &#39;shrubbery&#39;:
Error on line 2&lt;/span&gt;&lt;/pre&gt;&lt;p&gt;If we import the &quot;sys&quot; library, it gives us some options, one of which being the line number on which the failure happened, the failure that popped us out of our try block into the except block. This still doesn&#39;t give us everything we might want, but it provides the line number where the error happened. That gives us a great place to start looking at our code to see what happened.&lt;/p&gt;&lt;p&gt;We can do better:&lt;/p&gt;
&lt;pre style=&quot;background-color: #595959; margin: 10px; padding: 10px;&quot;&gt;&lt;span style=&quot;color: white;&quot;&gt;&amp;gt;&amp;gt;&amp;gt; import sys
&amp;gt;&amp;gt;&amp;gt; try:
...   {}[&quot;shrubbery&quot;]
... except Exception as e:
...   print(f&quot;There was an unexpected {type(e).__name__} error: {e}: \nError on line {sys.exc_info()[-1].tb_lineno}&quot;)
...
There was an unexpected KeyError error: &#39;shrubbery&#39;:
Error on line 2&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;Ah, very nice. Now we know the type of error, a KeyError, we know the key that caused the error, and we know the line in our code where the error is happening.&lt;/p&gt;&lt;p&gt;There are more options for outputting more data. However, I haven&#39;t found more data to be that useful. With this information, I have just what I need and no extra fluff to work through.&amp;nbsp;&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/6872211441463347305/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2024/10/favorite-way-to-troubleshoot-python.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/6872211441463347305'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/6872211441463347305'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2024/10/favorite-way-to-troubleshoot-python.html' title='Favorite way to troubleshoot Python scripts'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-204866854581810436</id><published>2024-10-03T12:13:00.006-05:00</published><updated>2025-01-30T14:30:22.724-06:00</updated><title type='text'>Capturing packets on a Windows server without installing anything</title><content type='html'>&lt;p&gt;&amp;nbsp;Ever wanted to do a pcap on a Windows server, but didn&#39;t have permission to install an app like Wireshark? Here&#39;s how you do it:&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;ol style=&quot;text-align: left;&quot;&gt;&lt;li&gt;Start an elevated command prompt or powershell terminal.&lt;/li&gt;&lt;li&gt;Run `netsh trace start capture=yes stopWhenFull=&lt;span class=&quot;hljs-built_in&quot; style=&quot;color: var(--cib-color-syntax-orange); font-size: var(--cib-type-body1-font-size); font-variation-settings: var(--cib-type-subtitle1-font-variation-settings); font-weight: var(--cib-type-body1-font-weight);&quot;&gt;yes&amp;nbsp;&lt;/span&gt;tracefile=C:\temp\packetcapture.etl&quot;&lt;/li&gt;&lt;li&gt;Wait until you believe the desired packets have been captured or reproduce the issue you want to capture.&lt;/li&gt;&lt;li&gt;Run `netsh trace stop`&lt;/li&gt;&lt;li&gt;Your packet capture file will be in c:\temp called packetcapture.etl. You&#39;ll need to convert this into a file that Wireshark can open. In the past, you could open it with Microsoft Message Analyzer, but it isn&#39;t available anymore. You can use &lt;a href=&quot;https://github.com/microsoft/etl2pcapng/releases&quot; target=&quot;_blank&quot;&gt;this tool&lt;/a&gt; to convert it. Simply download the release and run:&lt;br /&gt;`etl2pcapng.exe in.etl out.pcapng`&lt;br /&gt;Where in.etl points to the file output from your trace and out.pcapng points to the place where you want your output file to go.&amp;nbsp;&lt;/li&gt;&lt;/ol&gt;&lt;div&gt;There are filters you can apply to the netsh command if needed. But I&#39;ve found the filtering in Wireshark to be easier/better.&amp;nbsp;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/204866854581810436/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2024/10/capturing-packets-on-windows-server.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/204866854581810436'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/204866854581810436'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2024/10/capturing-packets-on-windows-server.html' title='Capturing packets on a Windows server without installing anything'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-7981915649224552741</id><published>2019-12-10T12:50:00.001-06:00</published><updated>2019-12-10T12:50:25.684-06:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="LogicMonitor"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><title type='text'>Sending sysLog via Python</title><content type='html'>I had someone ask me the other day if we could ingest alarms into LogicMonitor from their app. For example, if the app had an exception raised, they&#39;d like to send a message to LogicMonitor to open an alarm. The easiest way I could think of doing this was to have a standalone Python script that sends data via Syslog to LM. This is one way this could be done. I haven&#39;t tested this, but it&#39;s simple enough to see that it should work:&lt;br /&gt;
&lt;br /&gt;
import logging&lt;br /&gt;
import logging.handlers&lt;br /&gt;
import sys&lt;br /&gt;
my_logger = logging.getLogger(&#39;MyLogger&#39;)&lt;br /&gt;
my_logger.setLevel(logging.INFO)&lt;br /&gt;
destIpAddress = sys.argv[1]&lt;br /&gt;
destPort = sys.argv[2]&lt;br /&gt;
handler = logging.handlers.SysLogHandler(address = (ipAddress,514))&lt;br /&gt;
my_logger.addHandler(handler)&lt;br /&gt;
my_logger.info(sys.argv[3:].join(&quot; &quot;))&lt;br /&gt;
&lt;br /&gt;
You&#39;d call it like this:&lt;br /&gt;
&amp;gt; send_syslog.py 192.168.25.64 514 My application had an error in the widget creator service.&lt;br /&gt;
&lt;div&gt;
&lt;br /&gt;&lt;/div&gt;
</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/7981915649224552741/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2019/12/sending-syslog-via-python.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/7981915649224552741'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/7981915649224552741'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2019/12/sending-syslog-via-python.html' title='Sending sysLog via Python'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-3366550192750330892</id><published>2019-11-01T12:36:00.000-05:00</published><updated>2020-03-27T16:35:54.657-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="DataSource"/><category scheme="http://www.blogger.com/atom/ns#" term="Graph"/><category scheme="http://www.blogger.com/atom/ns#" term="LogicMonitor"/><category scheme="http://www.blogger.com/atom/ns#" term="My Tools"/><title type='text'>Counting Resources Per Group</title><content type='html'>This post is now deprecated. The updated content is found &lt;a href=&quot;https://github.com/sweenig/lmcommunity/tree/master/Billing&quot;&gt;here&lt;/a&gt;.&lt;br /&gt;
&lt;br /&gt;
&lt;a name=&#39;more&#39;&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
This week I was able to revisit something I&#39;d made for a previous customer, and this time I think I made it even better.&lt;br /&gt;
&lt;br /&gt;
In both cases, the customers were MSPs who wanted to generate some statistics around how many objects were in particular groups. They wanted to easily bill their customers for services delivered. The first iteration worked just fine, but ended up being difficult to implement for the other customer because the groups were not only named different, but the group structure itself was nothing similar.&lt;br /&gt;
&lt;br /&gt;
&lt;h2&gt;
&lt;a href=&quot;https://www.blogger.com/null&quot; id=&quot;weenig_warning&quot; name=&quot;weenig_warning&quot;&gt;The Datasource Itself&lt;/a&gt;&lt;/h2&gt;
You&#39;ll need the datsource file imported into your portal for any of this to work for you. I&#39;m not publishing the datasource configuration file, nor the dashboard configuration file yet, for several reasons. Once I do, I&#39;ll update it here.&lt;br /&gt;
&lt;br /&gt;
The second iteration was an opportunity to innovate so I stuck to the &quot;S&quot; principle in &lt;a href=&quot;https://scotch.io/bar-talk/s-o-l-i-d-the-first-five-principles-of-object-oriented-design&quot;&gt;SOLID&lt;/a&gt;: Single responsibility. I decided that my datasource shouldn&#39;t try to do very specific things regarding groups. The end result is that I started by creating a datasource that pulls a list of all groups and gets the counts for every group. This would put all the data in the hands of the user and let them filter out the things that aren&#39;t needed.&lt;br /&gt;
&lt;br /&gt;
&lt;h4&gt;
Filtering to only report on certain groups&lt;/h4&gt;
In order to make filtering easier, I added two instance level properties to the datasource definition: depth and fullPath.&amp;nbsp; Before I go into that, let&#39;s see what an example folder structure might look like. This example will be referenced throughout this post.&lt;br /&gt;
&lt;br /&gt;
&lt;h4&gt;
Example Folder Structure&lt;/h4&gt;
&lt;pre&gt;/ (root)
├── Clients
│&amp;nbsp;&amp;nbsp; ├── Acme
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── Collectors
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── Network
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── Security
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── Server
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; └── Virtual Machines
│&amp;nbsp;&amp;nbsp; └── NetQoS
│&amp;nbsp;&amp;nbsp;     ├── Collectors
│&amp;nbsp;&amp;nbsp;     ├── Network
│&amp;nbsp;&amp;nbsp;     ├── Server
│&amp;nbsp;&amp;nbsp;     └── Virtual Machines
├── Devices By Type
├── Minimal Monitoring
└── My Devices
    ├── Office A
    └── Office B&lt;/pre&gt;
&lt;br /&gt;
&lt;h4&gt;
fullPath&lt;/h4&gt;
fullPath contains the actual path of the folder. This can be used in a filter to only include folders under certain other folders. For example, since I know my customers&#39; devices are sorted into the Acme and NetQoS folders, I could add a filter like this:&lt;br /&gt;
&lt;pre&gt;&lt;/pre&gt;
&lt;pre&gt;auto.fullPath CONTAIN &quot;Clients/&quot;&lt;/pre&gt;
&lt;br /&gt;
This would limit discovery to only those folders containing the string &quot;Clients/&quot;, which are the following:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;Clients/Acme
Clients/Acme/Collectors
Clients/Acme/Network
Clients/Acme/Security
Clients/Acme/Server
Clients/Acme/Virtual Machines
Clients/NetQoS/
Clients/NetQoS/Collectors
Clients/NetQoS/Network
Clients/NetQoS/Server
Clients/NetQoS/Virtual Machines&lt;/pre&gt;
&lt;br /&gt;
This gets us halfway to where we want to be. It included the main client folders (Acme and NetQoS), but also included the groups under them (children of Acme/NetQoS, grandchildren of Clients, and descendants of the root).&lt;br /&gt;
&lt;br /&gt;
&lt;h4&gt;
Depth&lt;/h4&gt;
Depth refers to the distance from the root of each folder. Setting a filter like this:&lt;br /&gt;
&lt;br /&gt;
auto.depth == 2&lt;br /&gt;
&lt;br /&gt;
would get the following folders:&lt;br /&gt;
&lt;br /&gt;
Clients/Acme&lt;br /&gt;
Clients/NetQoS&lt;br /&gt;
My Devices/Office A&lt;br /&gt;
My Devices/Office B&lt;br /&gt;
&lt;br /&gt;
Combining the two folders would get only the main client folders (Acme &amp;amp; NetQoS).&amp;nbsp; At any rate, any combination of these two filters should allow the user to filter down to just the folders for which we want counts.&amp;nbsp; If no filters were applied, it would give data on each and every group.&lt;br /&gt;
&lt;br /&gt;
&lt;h3&gt;
The Data&lt;/h3&gt;
The data comes from the LM API. Normally, data in LM is collected from a target and stored under the resource in LM that corresponds to that target. This can be most directly/simply done by adding your LM portal to LM as a device. In my case, since my portal is lmstuartweenig, I added &quot;lmstuartweenig.logicmonitor.com&quot; as a device, using the expert mode. I then disabled everything that was discovered by default (because I don&#39;t really need to monitor that, to each his own).&lt;br /&gt;
&lt;br /&gt;
Alternatively, you could avoid adding another resource into your portal by having the datasource collect the data from the API, but storing/associating the data with one of the collectors. This saves you 1 license.&amp;nbsp; Either way, you need to have a resource in your resource tree to which which the data can be associated. In my case, I added my portal as a resource. The only difference is whether or not you burn a license.&lt;br /&gt;
&lt;br /&gt;
Once you&#39;ve identified which resource with which the data will be associated, you need to add a few properties. These properties serve two purposes: 1) it signals LM to enable the datasource and where to associate the data and 2) it provides some key information that the datasource will need to access the API. &lt;a href=&quot;https://www.logicmonitor.com/support/devices/adding-managing-devices/device-properties/&quot;&gt;Add the following properties&lt;/a&gt; to the device:&lt;br /&gt;
&lt;center&gt;
&lt;table style=&quot;border-collapse: collapse; border: 1px solid black;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;billing_api.account&lt;/td&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;This is the account name for your portal. In my case, since my portal address is lmstuartweenig.logicmonitor.com, I&#39;ll store &quot;lmstuartweenig&quot; in this property.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;billing_api.id&lt;/td&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;&lt;a href=&quot;https://www.logicmonitor.com/support/settings/users-and-roles/api-tokens/&quot;&gt;Generate an API token&lt;/a&gt; and store the ID here.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;billing_api.pass&lt;/td&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;Store the API Key here.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/center&gt;
&lt;br /&gt;
Once this is done, you can wait 15 minutes and the datasource should have discovered the groups and started collecting data. At this point, you may want to go to the datasource definition and tweak the filters for your purposes.&lt;br /&gt;
&lt;br /&gt;
&lt;h4&gt;
Datapoints&lt;/h4&gt;
The following datapoints are gathered for each folder:&lt;br /&gt;
&lt;center&gt;
&lt;table style=&quot;border-collapse: collapse; border: 1px solid black;&quot;&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;Datapoint&lt;/td&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;Description&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;host_count&lt;/td&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;The total count of all hosts in the group (including hosts in sub-groups)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;gcp_count&lt;/td&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;How many GCP devices (not sure if it includes sub-groups, I think it does)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;azure_count&lt;/td&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;How many Azure devices (not sure if it includes sub-groups, I think it does)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;aws_count&lt;/td&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;How many AWS devices (not sure if it includes sub-groups, I think it does)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;Total_Count&lt;/td&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;Simply the sum of host_count, gcp_count, azure_count, aws_count&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/center&gt;
&lt;br /&gt;
&lt;h4&gt;
Adding Billing Information&lt;/h4&gt;
Since one of the original purposes of this data source was to bill customers, it made sense to build in some billing capability. This component is optional and will show &quot;No Data&quot; until it is configured (which I&#39;ll detail below).&lt;br /&gt;
Since the resulting set of folders normally would represent customers, and since some customers sometimes have different prices for services (gold customers may pay more, for example), and since different type of things could have different costs, you can add a few properties to have the data source calculate some costs as part of the data source with high flexibility.&lt;br /&gt;
These properties can be added at the device level. Doing so, would apply the same unit cost to every customer unless a unit cost was applied at the instance level. &lt;b&gt;Let me be clear:&lt;/b&gt;&amp;nbsp;the device and instances I&#39;m talking about here are the ones to which the data is associated. Putting these properties on the groups you want to track will have no effect. In my case I added these properties to the lmstuartweenig.logicmonitor.com resource, then added some overrides on the instances under the &quot;Resource Group Member Counts&quot; and &quot;Website Group Member Counts&quot; datasources.&lt;br /&gt;
&lt;br /&gt;
Specifically, you can add the following properties:&lt;br /&gt;
&lt;center&gt;
&lt;table style=&quot;border-collapse: collapse; border: 1px solid black;&quot;&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;Property&lt;/td&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;Description&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;billing_api.aws_cost&lt;/td&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;Unit cost of AWS instances&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;billing_api.azure_cost&lt;/td&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;Unit cost of Azure instances&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;billing_api.gcp_cost&lt;/td&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;Unit cost of GCP instances&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;billing_api.resource_cost&lt;/td&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;Unit cost of devices/resources&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;billing_api.website_cost&lt;/td&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;Unit cost of a website check&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/center&gt;
&lt;br /&gt;
Once any number of these properties are added, the value of these properties will feed into new datapoints, populated using the formulas shown below:&lt;br /&gt;
&lt;center&gt;
&lt;table style=&quot;border-collapse: collapse; border: 1px solid black;&quot;&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;Datapoint&lt;/td&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;Formula&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;aws_total_cost&lt;/td&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;aws_count X billing_api.aws_cost&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;azure_total_cost&lt;/td&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;azure_count X billing_api.azure_cost&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;gcp_total_cost&lt;/td&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;gcp_count X billing_api.gcp_cost&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;host_total_cost&lt;/td&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;host_count X billing_api.resource_cost&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;website_cost&lt;/td&gt;&lt;td style=&quot;border: 1px solid black;&quot;&gt;numOfWebsites X billing_api.website_cost&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/center&gt;
&lt;br /&gt;
Using these newly populated datapoints, you can create a dashboard to display all these metrics along with their corresponding cost. Additionally or alternatively, you could create reports containing all the billing data. I recommend dashboards sent out as reports because they are prettier and easier to digest than a flat table of data. Dashboards also allow you to simplify the verbiage.&lt;br /&gt;
&lt;br /&gt;
One other little caveat: this datasource by default only pulls 50 groups. If you have more than 50 groups in your resource and/or website structure, you&#39;ll need to add a property called billing_api.query_limit with a value greater than the total number of folders in the larger structure, recursively.&lt;br /&gt;
&lt;br /&gt;
&lt;h3&gt;
Creating the Dashboard&lt;/h3&gt;
There&#39;s a lot that goes into creating a dashboard. Luckily, I&#39;ve done all the work for you so you can just &lt;a href=&quot;https://www.blogger.com/blogger.g?blogID=9129463197233057363#weenig_warning&quot;&gt;import&lt;/a&gt; the dashboard. However, when importing, you need to understand how the data is filtered down to one customer. The dashboard makes use of &lt;a href=&quot;https://www.logicmonitor.com/support/dashboards-and-widgets/managing-dashboards/how-are-dashboards-created/&quot;&gt;tokens&lt;/a&gt;, which are just variables that you create on the dashboard level. Particularly, you need to create the ##customer## token where the value is the name of the customer. It&#39;s important that the value of this token match the name of the folder containing this customer&#39;s resources &lt;b&gt;and&lt;/b&gt; websites. Meaning that in order for website data to show up along with resource data, the folders should be named the same (technically they don&#39;t have to be the same because the filter looks for any discovered folders &lt;i&gt;containing&lt;/i&gt; the value of ##customer##).&lt;br /&gt;
&lt;br /&gt;
In my case, my website folder structure is a bit simpler than my resource group structure:&lt;br /&gt;
&lt;pre&gt;Websites/
├── Acme
├── My\ Stuff
└── NetQoS&lt;/pre&gt;
&lt;br /&gt;
However, since I have a folder for Acme and a folder for NetQoS and the ##customer## token can cover both my resources and my websites.&amp;nbsp; After selecting the dashboard definition file to import, All I do is change the dashboard name and update the token. As long as the data is there, it should show everything. Every widget (and you can see the configuration of each one) makes reference to the token so they all filter to only show data concerning the customer of interest.&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZ9PoYV4wwW1vtvRyLcZaHIHcJSYhZHxfGLmYtAst14v8rPpMatSasPD_dShIqj_DtxfWY0wwVMqq87tPDILWmAOvvLnaQztF2yWP2b7YM_oL5mRqIeaehPooLHLZ5L5R9-lLZtEvZakzM/s1600/Screen+Shot+2019-11-01+at+12.04.17+PM.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;896&quot; data-original-width=&quot;696&quot; height=&quot;320&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZ9PoYV4wwW1vtvRyLcZaHIHcJSYhZHxfGLmYtAst14v8rPpMatSasPD_dShIqj_DtxfWY0wwVMqq87tPDILWmAOvvLnaQztF2yWP2b7YM_oL5mRqIeaehPooLHLZ5L5R9-lLZtEvZakzM/s320/Screen+Shot+2019-11-01+at+12.04.17+PM.png&quot; width=&quot;248&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: left;&quot;&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;h3 style=&quot;text-align: left;&quot;&gt;
Additional (Accidental) Uses&lt;/h3&gt;
&lt;div style=&quot;text-align: left;&quot;&gt;
One interesting discovery is that if you&#39;re only presenting the data through the dashboard, you can do all the filtering on the dashboard level. Meaning: you could actually discover and track every group&#39;s metrics, but by properly using the dashboard token, you can filter out only to the one group you&#39;re interested in showing.&lt;/div&gt;
&lt;div style=&quot;text-align: left;&quot;&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: left;&quot;&gt;
Another discovery: If you choose your token wisely, you can get multiple customer&#39;s data together on one dashboard to get a breakdown of the counts per customer.&amp;nbsp; The token would have to be one that would match on all the customer groups that I want to show. Luckily in my case, I have my customer groups under &quot;Clients&quot;. The token would then be &quot;Clients-&quot; (the &quot;/&quot; in the path is replaced with a &quot;-&quot; when the instance is named).&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiTF7hplc_XajFT0pEl9uuupg0_qu4n3HeBwVtcs5uiWfyi4ynEszjKnwnID-Gotk6F4WzVO2K41pHJUMgWgL8cOfp9c6q-gd0qN8g38aV9KetOND6pwJS-hvUZE46LGy-4huxWNIAiUY4F/s1600/Screen+Shot+2019-11-01+at+12.16.28+PM.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;879&quot; data-original-width=&quot;1544&quot; height=&quot;364&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiTF7hplc_XajFT0pEl9uuupg0_qu4n3HeBwVtcs5uiWfyi4ynEszjKnwnID-Gotk6F4WzVO2K41pHJUMgWgL8cOfp9c6q-gd0qN8g38aV9KetOND6pwJS-hvUZE46LGy-4huxWNIAiUY4F/s640/Screen+Shot+2019-11-01+at+12.16.28+PM.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: left;&quot;&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: left;&quot;&gt;
Notice in the legends of the pie charts, that it shows one slice for NetQoS&#39; resources and a different slice for Acme&#39;s resources.&amp;nbsp; FYI: ignore the oddness in the top trend plots as a configuration change right before this snapshot caused some historical data to be lost and start gathering again).&amp;nbsp;&lt;/div&gt;
</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/3366550192750330892/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2019/11/counting-resources-per-group.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/3366550192750330892'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/3366550192750330892'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2019/11/counting-resources-per-group.html' title='Counting Resources Per Group'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZ9PoYV4wwW1vtvRyLcZaHIHcJSYhZHxfGLmYtAst14v8rPpMatSasPD_dShIqj_DtxfWY0wwVMqq87tPDILWmAOvvLnaQztF2yWP2b7YM_oL5mRqIeaehPooLHLZ5L5R9-lLZtEvZakzM/s72-c/Screen+Shot+2019-11-01+at+12.04.17+PM.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-6547409036856606320</id><published>2019-09-03T15:12:00.001-05:00</published><updated>2019-10-08T10:23:26.209-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="DataSource"/><category scheme="http://www.blogger.com/atom/ns#" term="Groovy"/><category scheme="http://www.blogger.com/atom/ns#" term="LogicMonitor"/><category scheme="http://www.blogger.com/atom/ns#" term="SNMP"/><title type='text'>Groovy SNMPwalk Helper Functions</title><content type='html'>tl;dr - &lt;a href=&quot;https://github.com/sweenig/monitoring-recipes/tree/master/DataSources/Groovy/SNMP/Examples&quot;&gt;Go here.&lt;/a&gt;&lt;br /&gt;
&lt;br /&gt;
I&#39;ve been doing a lot of SNMP polling through Groovy lately. One of the methods I use most often is Snmp.walkAsMap(), which returns a map that looks like this:&lt;br /&gt;
&lt;pre&gt;[8.6:2, 4.10:1500, 8.7:1, 8.8:1, 4.12:1500, 4.14:1500, 4.16:1500, 3.44:6, 22.5:0.0, 22.4:0.0, 
22.3:0.0, 22.2:0.0, 22.1:0.0, 22.8:0.0, 22.7:0.0, 22.6:0.0, 17.16:164136, 16.44:6639007, 17.12:8634086, 
18.8:0, 17.14:8745523, 18.7:0, 17.10:21076, 10.6:0, 10.5:0, 10.4:0, 10.3:0, 10.2:3968863511, 10.1:8980402, 
22.44:0.0, 18.6:0, 18.5:0, 7.1:1, 18.4:0, 7.2:1, 18.3:0, 7.3:1, 18.2:0, 7.4:1, 18.1:0, 7.5:1, 10.8:1684285, 
7.6:2, 10.7:4181064938, 7.7:1, 7.8:1, 15.44:0, 16.12:2114735865, 16.10:4077101, 16.16:16467729, 
16.14:2361323502, 22.14:0.0, 22.16:0.0, 9.44:2 days, 14:54:42.04, 21.6:0, 21.5:0, 21.4:0, 21.3:0, 
21.2:0, 21.1:0, 22.10:0.0, 22.12:0.0, 21.44:0, 21.8:0, 21.7:0, 5.10:4294967295, 4.44:1500, 5.12:4294967295, 
5.14:4294967295, 11.10:0, 5.16:4294967295, 10.44:1620014, 11.12:12487957, 17.8:30812, 11.14:12673362, 
17.7:17502386, 6.1:, 17.6:0, 6.2:90:e6:ba:59:1b:18, 11.16:136053, 17.5:0, 6.3:00:50:56:c0:00:01, 
17.4:20226, 6.4:00:50:56:c0:00:08, 17.3:20229, 6.5:52:54:00:9b:4b:e0, 17.2:37200831, 6.6:52:54:00:9b:4b:e0, 
17.1:44466, 6.7:02:42:39:1b:cc:e3, 6.8:02:42:55:44:3f:06, 10.10:0, 20.7:0, 20.6:0, 20.5:0, 20.4:0, 20.3:0, 
20.2:0, 20.1:0, 10.12:30849041, 10.14:131456967, 10.16:77954842, 20.8:0, 5.1:10000000, 16.8:17045376, 
5.2:100000000, 16.7:189459674, 5.3:0, 16.6:0, 5.4:0, 16.5:0, 5.5:0, 16.4:0, 5.6:10000000, 16.3:0, 5.7:0, 
16.2:1009509757, 5.8:0, 16.1:8980402, 6.12:4e:c1:4e:e6:07:90, 5.44:4294967295, 6.14:0a:2c:4f:3a:eb:5c, 
6.16:12:db:e0:82:ca:a2, 19.44:0, 6.10:06:31:50:31:a5:fb, 15.12:0, 14.44:0, 15.10:0, 21.16:0, 15.16:0, 
21.14:0, 15.14:0, 20.44:0, 21.12:0, 1.12:12, 1.10:10, 15.1:0, 4.1:65536, 4.2:1500, 4.3:1500, 15.8:0, 
21.10:0, 4.4:1500, 15.7:0, 4.5:1500, 15.6:0, 4.6:1500, 15.5:0, 4.7:1500, 15.4:0, 4.8:1500, 15.3:0, 
15.2:0, 14.10:0, 20.16:0, 14.14:0, 20.14:0, 13.44:0, 14.12:0, 20.12:0, 1.16:16, 1.14:14, 20.10:0, 14.16:0, 
6.44:62:d0:3f:14:ef:85, 7.12:1, 7.14:1, 7.16:1, 7.10:1, 14.2:0, 14.1:0, 3.1:24, 3.2:6, 3.3:6, 3.4:6, 3.5:6, 
14.8:0, 3.6:6, 14.7:0, 3.7:6, 14.6:0, 3.8:6, 14.5:0, 14.4:0, 14.3:0, 2.14:veth9a64fa1, 2.12:veth2bc8fbd, 
1.44:44, 2.10:veth92ca0b0, 19.14:0, 19.16:0, 19.10:0, 19.12:0, 18.44:0, 13.3:0, 13.2:0, 13.1:0, 2.1:lo, 
2.2:NVIDIA Corporation MCP77 Ethernet, 2.16:veth497df7f, 2.3:vmnet1, 2.4:vmnet8, 2.5:virbr0, 2.6:virbr0-nic, 
2.7:br-6a2604a91ac1, 13.8:0, 2.8:docker0, 13.7:0, 13.6:0, 13.5:0, 13.4:0, 18.16:0, 8.16:1, 8.14:1, 
17.44:18857, 18.12:0, 18.14:0, 18.10:0, 8.12:1, 7.44:1, 8.10:1, 13.10:0, 12.44:0, 13.14:0, 13.12:0, 
3.14:6, 2.44:vethd608288, 3.12:6, 3.10:6, 12.4:0, 12.3:0, 12.2:2662, 12.1:0, 1.1:1, 1.2:2, 1.3:3, 1.4:4, 
1.5:5, 1.6:6, 1.7:7, 1.8:8, 13.16:0, 9.1:0:00:00.00, 12.8:0, 9.2:0:00:00.00, 12.7:0, 9.3:0:00:06.31, 12.6:0, 
9.4:0:00:06.31, 12.5:0, 9.5:0:00:09.32, 9.6:0:00:12.32, 9.7:3:03:43.67, 9.8:0:00:18.32, 12.10:0, 11.44:4506, 
12.12:0, 3.16:6, 12.14:0, 12.16:0, 9.16:3:03:43.67, 9.14:3:03:43.67, 19.8:0, 19.7:0, 19.6:0, 8.44:1, 
9.12:3:03:43.67, 9.10:0:00:18.32, 11.5:0, 11.4:0, 11.3:0, 11.2:28692938, 11.1:44466, 19.5:0, 19.4:0, 
19.3:0, 8.1:1, 19.2:0, 8.2:1, 19.1:0, 8.3:1, 11.8:6882, 8.4:1, 11.7:25297372, 8.5:2, 11.6:0]&lt;/pre&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
This great and usable, but I found myself constantly wanting to display the data easier. I also wanted the data to be structured a little more hierarchically, grouping by row in the SNMP table.&amp;nbsp; I also wanted to make it easier to address individual pieces of the data. I built a couple helper functions that transform the data and make it easier to address. They can be found &lt;a href=&quot;https://github.com/sweenig/monitoring-recipes/tree/master/DataSources/Groovy/SNMP/Examples&quot;&gt;here&lt;/a&gt;.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
The output of the snmpMapToTable() function looks like this:&lt;br /&gt;
&lt;pre&gt;[6:[8:2, 22:0.0, 10:0, 18:0, 7:2, 21:0, 17:0, 6:52:54:00:9b:4b:e0, 20:0, 16:0, 5:10000000, 15:0, 4:1500, 
3:6, 14:0, 2:virbr0-nic, 13:0, 1:6, 12:0, 9:0:00:12.32, 19:0, 11:0], 10:[4:1500, 17:21076, 16:4077101, 22:0.0, 
5:4294967295, 11:0, 10:0, 6:06:31:50:31:a5:fb, 15:0, 1:10, 21:0, 14:0, 20:0, 7:1, 2:veth92ca0b0, 19:0, 18:0, 
8:1, 13:0, 3:6, 12:0, 9:0:00:18.32], 7:[8:1, 22:0.0, 18:0, 10:4181064938, 7:1, 21:0, 17:17502386, 
6:02:42:39:1b:cc:e3, 20:0, 16:189459674, 5:0, 15:0, 4:1500, 14:0, 3:6, 2:br-6a2604a91ac1, 13:0, 1:7, 
12:0, 9:3:03:43.67, 19:0, 11:25297372], 8:[8:1, 22:0.0, 18:0, 10:1684285, 7:1, 21:0, 17:30812, 
6:02:42:55:44:3f:06, 20:0, 16:17045376, 5:0, 15:0, 4:1500, 14:0, 3:6, 13:0, 2:docker0, 1:8, 12:0, 
9:0:00:18.32, 19:0, 11:6882], 12:[4:1500, 17:8634086, 16:2114735865, 22:0.0, 5:4294967295, 
11:12487957, 10:30849041, 6:4e:c1:4e:e6:07:90, 15:0, 21:0, 1:12, 14:0, 20:0, 7:1, 2:veth2bc8fbd, 
19:0, 18:0, 8:1, 13:0, 3:6, 12:0, 9:3:03:43.67], 14:[4:1500, 17:8745523, 16:2361323502, 22:0.0, 
5:4294967295, 11:12673362, 10:131456967, 6:0a:2c:4f:3a:eb:5c, 21:0, 15:0, 14:0, 20:0, 1:14, 7:1, 
2:veth9a64fa1, 19:0, 8:1, 18:0, 13:0, 3:6, 12:0, 9:3:03:43.67], 16:[4:1500, 17:164136, 16:16467729, 
22:0.0, 5:4294967295, 11:136053, 10:77954842, 6:12:db:e0:82:ca:a2, 21:0, 15:0, 20:0, 1:16, 14:0, 
7:1, 19:0, 2:veth497df7f, 18:0, 8:1, 13:0, 3:6, 12:0, 9:3:03:43.67], 44:[3:6, 16:6639007, 22:0.0, 
15:0, 9:2 days, 14:54:42.04, 21:0, 4:1500, 10:1620014, 5:4294967295, 19:0, 14:0, 20:0, 
13:0, 6:62:d0:3f:14:ef:85, 1:44, 18:0, 17:18857, 7:1, 12:0, 2:vethd608288, 11:4506, 8:1], 5:[22:0.0, 
10:0, 18:0, 7:1, 21:0, 17:0, 6:52:54:00:9b:4b:e0, 20:0, 16:0, 5:0, 4:1500, 15:0, 3:6, 14:0, 2:virbr0, 
13:0, 1:5, 12:0, 9:0:00:09.32, 11:0, 19:0, 8:2], 4:[22:0.0, 10:0, 18:0, 7:1, 21:0, 17:20226, 
6:00:50:56:c0:00:08, 20:0, 5:0, 16:0, 4:1500, 15:0, 3:6, 14:0, 2:vmnet8, 13:0, 12:0, 1:4, 9:0:00:06.31, 
11:0, 19:0, 8:1], 3:[22:0.0, 10:0, 18:0, 7:1, 21:0, 6:00:50:56:c0:00:01, 17:20229, 20:0, 5:0, 16:0, 
4:1500, 15:0, 3:6, 14:0, 13:0, 2:vmnet1, 12:0, 1:3, 9:0:00:06.31, 11:0, 19:0, 8:1], 2:[22:0.0, 
10:3968863511, 7:1, 18:0, 21:0, 6:90:e6:ba:59:1b:18, 17:37200831, 20:0, 5:100000000, 16:1009509757, 
4:1500, 15:0, 14:0, 3:6, 13:0, 2:NVIDIA Corporation MCP77 Ethernet, 12:2662, 1:2, 9:0:00:00.00, 
11:28692938, 19:0, 8:1], 1:[22:0.0, 10:8980402, 7:1, 18:0, 21:0, 6:, 17:44466, 20:0, 5:10000000, 
16:8980402, 15:0, 4:65536, 14:0, 3:24, 13:0, 2:lo, 12:0, 1:1, 9:0:00:00.00, 11:44466, 8:1, 19:0]]&lt;/pre&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
It may not look much better, but if you add some carriage returns and tabs you get this:&lt;br /&gt;
&lt;pre&gt;[
6:[
  8:2, 22:0.0, 10:0, 18:0, 7:2, 21:0, 17:0, 6:52:54:00:9b:4b:e0, 
  20:0, 16:0, 5:10000000, 15:0, 4:1500, 3:6, 14:0, 2:virbr0-nic, 
  13:0, 1:6, 12:0, 9:0:00:12.32, 19:0, 11:0], 
10:[
  4:1500, 17:21076, 16:4077101, 22:0.0, 5:4294967295, 11:0, 10:0, 
  6:06:31:50:31:a5:fb, 15:0, 1:10, 21:0, 14:0, 20:0, 7:1, 2:veth92ca0b0, 
  19:0, 18:0, 8:1, 13:0, 3:6, 12:0, 9:0:00:18.32], 
7:[
  8:1, 22:0.0, 18:0, 10:4181064938, 7:1, 21:0, 17:17502386, 6:02:42:39:1b:cc:e3, 
  20:0, 16:189459674, 5:0, 15:0, 4:1500, 14:0, 3:6, 2:br-6a2604a91ac1, 13:0, 1:7, 
  12:0, 9:3:03:43.67, 19:0, 11:25297372], 
8:[
  8:1, 22:0.0, 18:0, 10:1684285, 7:1, 21:0, 17:30812, 6:02:42:55:44:3f:06, 20:0, 
  16:17045376, 5:0, 15:0, 4:1500, 14:0, 3:6, 13:0, 2:docker0, 1:8, 12:0, 9:0:00:18.32, 
  19:0, 11:6882], 
12:[
  4:1500, 17:8634086, 16:2114735865, 22:0.0, 5:4294967295, 11:12487957, 10:30849041, 
  6:4e:c1:4e:e6:07:90, 15:0, 21:0, 1:12, 14:0, 20:0, 7:1, 2:veth2bc8fbd, 19:0, 18:0, 8:1, 
  13:0, 3:6, 12:0, 9:3:03:43.67], 
14:[
  4:1500, 17:8745523, 16:2361323502, 22:0.0, 5:4294967295, 11:12673362, 10:131456967, 
  6:0a:2c:4f:3a:eb:5c, 21:0, 15:0, 14:0, 20:0, 1:14, 7:1, 2:veth9a64fa1, 19:0, 8:1, 18:0, 
  13:0, 3:6, 12:0, 9:3:03:43.67], 
16:[
  4:1500, 17:164136, 16:16467729, 22:0.0, 5:4294967295, 11:136053, 10:77954842, 
  6:12:db:e0:82:ca:a2, 21:0, 15:0, 20:0, 1:16, 14:0, 7:1, 19:0, 2:veth497df7f, 18:0, 
  8:1, 13:0, 3:6, 12:0, 9:3:03:43.67], 
44:[
  3:6, 16:6639007, 22:0.0, 15:0, 9:2 days, 14:54:42.04, 21:0, 4:1500, 10:1620014, 
  5:4294967295, 19:0, 14:0, 20:0, 13:0, 6:62:d0:3f:14:ef:85, 1:44, 18:0, 17:18857, 
  7:1, 12:0, 2:vethd608288, 11:4506, 8:1], 
5:[
  22:0.0, 10:0, 18:0, 7:1, 21:0, 17:0, 6:52:54:00:9b:4b:e0, 20:0, 16:0, 5:0, 4:1500, 
  15:0, 3:6, 14:0, 2:virbr0, 13:0, 1:5, 12:0, 9:0:00:09.32, 11:0, 19:0, 8:2], 
4:[
  22:0.0, 10:0, 18:0, 7:1, 21:0, 17:20226, 6:00:50:56:c0:00:08, 20:0, 5:0, 16:0, 
  4:1500, 15:0, 3:6, 14:0, 2:vmnet8, 13:0, 12:0, 1:4, 9:0:00:06.31, 11:0, 19:0, 8:1], 
3:[
  22:0.0, 10:0, 18:0, 7:1, 21:0, 6:00:50:56:c0:00:01, 17:20229, 20:0, 5:0, 16:0, 4:1500, 
  15:0, 3:6, 14:0, 13:0, 2:vmnet1, 12:0, 1:3, 9:0:00:06.31, 11:0, 19:0, 8:1], 
2:[
  22:0.0, 10:3968863511, 7:1, 18:0, 21:0, 6:90:e6:ba:59:1b:18, 17:37200831, 20:0, 
  5:100000000, 16:1009509757, 4:1500, 15:0, 14:0, 3:6, 13:0, 
  2:NVIDIA Corporation MCP77 Ethernet, 12:2662, 1:2, 9:0:00:00.00, 11:28692938, 19:0, 8:1], 
1:[
  22:0.0, 10:8980402, 7:1, 18:0, 21:0, 6:, 17:44466, 20:0, 5:10000000, 16:8980402, 15:0, 
  4:65536, 14:0, 3:24, 13:0, 2:lo, 12:0, 1:1, 9:0:00:00.00, 11:44466, 8:1, 19:0]
]&lt;/pre&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
As you can see, it&#39;s getting easier to see what&#39;s going on. At this point, it&#39;d be nice to order by instance ID and also order by metric ID.&amp;nbsp; One of the helper functions I built does just that, pprintSnmpWalkTable:&lt;br /&gt;
&lt;pre&gt;Wildvalue: 1:
Data sorted by column ID
  1.##WILDVALUE##: 1
  2.##WILDVALUE##: lo
  3.##WILDVALUE##: 24
  4.##WILDVALUE##: 65536
  5.##WILDVALUE##: 10000000
  6.##WILDVALUE##: 
  7.##WILDVALUE##: 1
  8.##WILDVALUE##: 1
  9.##WILDVALUE##: 0:00:00.00
  10.##WILDVALUE##: 8980402
  11.##WILDVALUE##: 44466
  12.##WILDVALUE##: 0
  13.##WILDVALUE##: 0
  14.##WILDVALUE##: 0
  15.##WILDVALUE##: 0
  16.##WILDVALUE##: 8980402
  17.##WILDVALUE##: 44466
  18.##WILDVALUE##: 0
  19.##WILDVALUE##: 0
  20.##WILDVALUE##: 0
  21.##WILDVALUE##: 0
  22.##WILDVALUE##: 0.0
Wildvalue: 2:
Data sorted by column ID
  1.##WILDVALUE##: 2
  2.##WILDVALUE##: NVIDIA Corporation MCP77 Ethernet
  3.##WILDVALUE##: 6
  4.##WILDVALUE##: 1500
  5.##WILDVALUE##: 100000000
  6.##WILDVALUE##: 90:e6:ba:59:1b:18
  7.##WILDVALUE##: 1
  8.##WILDVALUE##: 1
  9.##WILDVALUE##: 0:00:00.00
  10.##WILDVALUE##: 3968863511
  11.##WILDVALUE##: 28692938
  12.##WILDVALUE##: 2662
  13.##WILDVALUE##: 0
  14.##WILDVALUE##: 0
  15.##WILDVALUE##: 0
  16.##WILDVALUE##: 1009509757
  17.##WILDVALUE##: 37200831
  18.##WILDVALUE##: 0
  19.##WILDVALUE##: 0
  20.##WILDVALUE##: 0
  21.##WILDVALUE##: 0
  22.##WILDVALUE##: 0.0
Wildvalue: 3:
Data sorted by column ID
  1.##WILDVALUE##: 3
  2.##WILDVALUE##: vmnet1
  3.##WILDVALUE##: 6
  4.##WILDVALUE##: 1500
  5.##WILDVALUE##: 0
  6.##WILDVALUE##: 00:50:56:c0:00:01
  7.##WILDVALUE##: 1
  8.##WILDVALUE##: 1
  9.##WILDVALUE##: 0:00:06.31
  10.##WILDVALUE##: 0
  11.##WILDVALUE##: 0
  12.##WILDVALUE##: 0
  13.##WILDVALUE##: 0
  14.##WILDVALUE##: 0
  15.##WILDVALUE##: 0
  16.##WILDVALUE##: 0
  17.##WILDVALUE##: 20229
  18.##WILDVALUE##: 0
  19.##WILDVALUE##: 0
  20.##WILDVALUE##: 0
  21.##WILDVALUE##: 0
  22.##WILDVALUE##: 0.0
Wildvalue: 4:
Data sorted by column ID
  1.##WILDVALUE##: 4
  2.##WILDVALUE##: vmnet8
  3.##WILDVALUE##: 6
  4.##WILDVALUE##: 1500
  5.##WILDVALUE##: 0
  6.##WILDVALUE##: 00:50:56:c0:00:08
  7.##WILDVALUE##: 1
  8.##WILDVALUE##: 1
  9.##WILDVALUE##: 0:00:06.31
  10.##WILDVALUE##: 0
  11.##WILDVALUE##: 0
  12.##WILDVALUE##: 0
  13.##WILDVALUE##: 0
  14.##WILDVALUE##: 0
  15.##WILDVALUE##: 0
  16.##WILDVALUE##: 0
  17.##WILDVALUE##: 20226
  18.##WILDVALUE##: 0
  19.##WILDVALUE##: 0
  20.##WILDVALUE##: 0
  21.##WILDVALUE##: 0
  22.##WILDVALUE##: 0.0
Wildvalue: 5:
Data sorted by column ID
  1.##WILDVALUE##: 5
  2.##WILDVALUE##: virbr0
  3.##WILDVALUE##: 6
  4.##WILDVALUE##: 1500
  5.##WILDVALUE##: 0
  6.##WILDVALUE##: 52:54:00:9b:4b:e0
  7.##WILDVALUE##: 1
  8.##WILDVALUE##: 2
  9.##WILDVALUE##: 0:00:09.32
  10.##WILDVALUE##: 0
  11.##WILDVALUE##: 0
  12.##WILDVALUE##: 0
  13.##WILDVALUE##: 0
  14.##WILDVALUE##: 0
  15.##WILDVALUE##: 0
  16.##WILDVALUE##: 0
  17.##WILDVALUE##: 0
  18.##WILDVALUE##: 0
  19.##WILDVALUE##: 0
  20.##WILDVALUE##: 0
  21.##WILDVALUE##: 0
  22.##WILDVALUE##: 0.0
Wildvalue: 6:
Data sorted by column ID
  1.##WILDVALUE##: 6
  2.##WILDVALUE##: virbr0-nic
  3.##WILDVALUE##: 6
  4.##WILDVALUE##: 1500
  5.##WILDVALUE##: 10000000
  6.##WILDVALUE##: 52:54:00:9b:4b:e0
  7.##WILDVALUE##: 2
  8.##WILDVALUE##: 2
  9.##WILDVALUE##: 0:00:12.32
  10.##WILDVALUE##: 0
  11.##WILDVALUE##: 0
  12.##WILDVALUE##: 0
  13.##WILDVALUE##: 0
  14.##WILDVALUE##: 0
  15.##WILDVALUE##: 0
  16.##WILDVALUE##: 0
  17.##WILDVALUE##: 0
  18.##WILDVALUE##: 0
  19.##WILDVALUE##: 0
  20.##WILDVALUE##: 0
  21.##WILDVALUE##: 0
  22.##WILDVALUE##: 0.0
Wildvalue: 7:
Data sorted by column ID
  1.##WILDVALUE##: 7
  2.##WILDVALUE##: br-6a2604a91ac1
  3.##WILDVALUE##: 6
  4.##WILDVALUE##: 1500
  5.##WILDVALUE##: 0
  6.##WILDVALUE##: 02:42:39:1b:cc:e3
  7.##WILDVALUE##: 1
  8.##WILDVALUE##: 1
  9.##WILDVALUE##: 3:03:43.67
  10.##WILDVALUE##: 4181064938
  11.##WILDVALUE##: 25297372
  12.##WILDVALUE##: 0
  13.##WILDVALUE##: 0
  14.##WILDVALUE##: 0
  15.##WILDVALUE##: 0
  16.##WILDVALUE##: 189459674
  17.##WILDVALUE##: 17502386
  18.##WILDVALUE##: 0
  19.##WILDVALUE##: 0
  20.##WILDVALUE##: 0
  21.##WILDVALUE##: 0
  22.##WILDVALUE##: 0.0
Wildvalue: 8:
Data sorted by column ID
  1.##WILDVALUE##: 8
  2.##WILDVALUE##: docker0
  3.##WILDVALUE##: 6
  4.##WILDVALUE##: 1500
  5.##WILDVALUE##: 0
  6.##WILDVALUE##: 02:42:55:44:3f:06
  7.##WILDVALUE##: 1
  8.##WILDVALUE##: 1
  9.##WILDVALUE##: 0:00:18.32
  10.##WILDVALUE##: 1684285
  11.##WILDVALUE##: 6882
  12.##WILDVALUE##: 0
  13.##WILDVALUE##: 0
  14.##WILDVALUE##: 0
  15.##WILDVALUE##: 0
  16.##WILDVALUE##: 17045376
  17.##WILDVALUE##: 30812
  18.##WILDVALUE##: 0
  19.##WILDVALUE##: 0
  20.##WILDVALUE##: 0
  21.##WILDVALUE##: 0
  22.##WILDVALUE##: 0.0
Wildvalue: 10:
Data sorted by column ID
  1.##WILDVALUE##: 10
  2.##WILDVALUE##: veth92ca0b0
  3.##WILDVALUE##: 6
  4.##WILDVALUE##: 1500
  5.##WILDVALUE##: 4294967295
  6.##WILDVALUE##: 06:31:50:31:a5:fb
  7.##WILDVALUE##: 1
  8.##WILDVALUE##: 1
  9.##WILDVALUE##: 0:00:18.32
  10.##WILDVALUE##: 0
  11.##WILDVALUE##: 0
  12.##WILDVALUE##: 0
  13.##WILDVALUE##: 0
  14.##WILDVALUE##: 0
  15.##WILDVALUE##: 0
  16.##WILDVALUE##: 4077101
  17.##WILDVALUE##: 21076
  18.##WILDVALUE##: 0
  19.##WILDVALUE##: 0
  20.##WILDVALUE##: 0
  21.##WILDVALUE##: 0
  22.##WILDVALUE##: 0.0
Wildvalue: 12:
Data sorted by column ID
  1.##WILDVALUE##: 12
  2.##WILDVALUE##: veth2bc8fbd
  3.##WILDVALUE##: 6
  4.##WILDVALUE##: 1500
  5.##WILDVALUE##: 4294967295
  6.##WILDVALUE##: 4e:c1:4e:e6:07:90
  7.##WILDVALUE##: 1
  8.##WILDVALUE##: 1
  9.##WILDVALUE##: 3:03:43.67
  10.##WILDVALUE##: 30849041
  11.##WILDVALUE##: 12487957
  12.##WILDVALUE##: 0
  13.##WILDVALUE##: 0
  14.##WILDVALUE##: 0
  15.##WILDVALUE##: 0
  16.##WILDVALUE##: 2114735865
  17.##WILDVALUE##: 8634086
  18.##WILDVALUE##: 0
  19.##WILDVALUE##: 0
  20.##WILDVALUE##: 0
  21.##WILDVALUE##: 0
  22.##WILDVALUE##: 0.0
Wildvalue: 14:
Data sorted by column ID
  1.##WILDVALUE##: 14
  2.##WILDVALUE##: veth9a64fa1
  3.##WILDVALUE##: 6
  4.##WILDVALUE##: 1500
  5.##WILDVALUE##: 4294967295
  6.##WILDVALUE##: 0a:2c:4f:3a:eb:5c
  7.##WILDVALUE##: 1
  8.##WILDVALUE##: 1
  9.##WILDVALUE##: 3:03:43.67
  10.##WILDVALUE##: 131456967
  11.##WILDVALUE##: 12673362
  12.##WILDVALUE##: 0
  13.##WILDVALUE##: 0
  14.##WILDVALUE##: 0
  15.##WILDVALUE##: 0
  16.##WILDVALUE##: 2361323502
  17.##WILDVALUE##: 8745523
  18.##WILDVALUE##: 0
  19.##WILDVALUE##: 0
  20.##WILDVALUE##: 0
  21.##WILDVALUE##: 0
  22.##WILDVALUE##: 0.0
Wildvalue: 16:
Data sorted by column ID
  1.##WILDVALUE##: 16
  2.##WILDVALUE##: veth497df7f
  3.##WILDVALUE##: 6
  4.##WILDVALUE##: 1500
  5.##WILDVALUE##: 4294967295
  6.##WILDVALUE##: 12:db:e0:82:ca:a2
  7.##WILDVALUE##: 1
  8.##WILDVALUE##: 1
  9.##WILDVALUE##: 3:03:43.67
  10.##WILDVALUE##: 77954842
  11.##WILDVALUE##: 136053
  12.##WILDVALUE##: 0
  13.##WILDVALUE##: 0
  14.##WILDVALUE##: 0
  15.##WILDVALUE##: 0
  16.##WILDVALUE##: 16467729
  17.##WILDVALUE##: 164136
  18.##WILDVALUE##: 0
  19.##WILDVALUE##: 0
  20.##WILDVALUE##: 0
  21.##WILDVALUE##: 0
  22.##WILDVALUE##: 0.0
Wildvalue: 44:
Data sorted by column ID
  1.##WILDVALUE##: 44
  2.##WILDVALUE##: vethd608288
  3.##WILDVALUE##: 6
  4.##WILDVALUE##: 1500
  5.##WILDVALUE##: 4294967295
  6.##WILDVALUE##: 62:d0:3f:14:ef:85
  7.##WILDVALUE##: 1
  8.##WILDVALUE##: 1
  9.##WILDVALUE##: 2 days, 14:54:42.04
  10.##WILDVALUE##: 1620014
  11.##WILDVALUE##: 4506
  12.##WILDVALUE##: 0
  13.##WILDVALUE##: 0
  14.##WILDVALUE##: 0
  15.##WILDVALUE##: 0
  16.##WILDVALUE##: 6639007
  17.##WILDVALUE##: 18857
  18.##WILDVALUE##: 0
  19.##WILDVALUE##: 0
  20.##WILDVALUE##: 0
  21.##WILDVALUE##: 0
  22.##WILDVALUE##: 0.0&lt;/pre&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
For me, this is pretty easy to read and find individual values that I&#39;m looking for. The three lines that resulted in the above output were these:&lt;br /&gt;
&lt;pre&gt;walkResult = Snmp.walkAsMap(host, Oid, props, timeout)
entryRaw = snmpMapToTable(walkResult)
pprintSnmpWalkTable(entryRaw)
&lt;/pre&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Accessing the data is pretty easy now:&lt;br /&gt;
&lt;pre&gt;ifEntryRaw.each {wildvalue, data -&amp;gt;
 println(&quot;&quot;&quot;Interface ${wildvalue}:
 ifAlias: ${ifXEntryRaw[wildvalue][&quot;1&quot;]}
 ifDescr: ${data[&quot;2&quot;]}
 ifType: ${data[&quot;3&quot;]}
    &quot;&quot;&quot;)
}&lt;/pre&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
This gives us the following output:&lt;br /&gt;
&lt;pre&gt;Interface 6:
 ifAlias: virbr0-nic
 ifDescr: virbr0-nic
 ifType: 6

Interface 10:
 ifAlias: veth92ca0b0
 ifDescr: veth92ca0b0
 ifType: 6

Interface 7:
 ifAlias: br-6a2604a91ac1
 ifDescr: br-6a2604a91ac1
 ifType: 6

Interface 8:
 ifAlias: docker0
 ifDescr: docker0
 ifType: 6

Interface 12:
 ifAlias: veth2bc8fbd
 ifDescr: veth2bc8fbd
 ifType: 6

Interface 14:
 ifAlias: veth9a64fa1
 ifDescr: veth9a64fa1
 ifType: 6

Interface 16:
 ifAlias: veth497df7f
 ifDescr: veth497df7f
 ifType: 6

Interface 44:
 ifAlias: vethd608288
 ifDescr: vethd608288
 ifType: 6

Interface 5:
 ifAlias: virbr0
 ifDescr: virbr0
 ifType: 6

Interface 4:
 ifAlias: vmnet8
 ifDescr: vmnet8
 ifType: 6

Interface 3:
 ifAlias: vmnet1
 ifDescr: vmnet1
 ifType: 6

Interface 2:
 ifAlias: enp0s10
 ifDescr: NVIDIA Corporation MCP77 Ethernet
 ifType: 6

Interface 1:
 ifAlias: lo
 ifDescr: lo
 ifType: 24&lt;/pre&gt;
&lt;br /&gt;</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/6547409036856606320/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2019/09/groovy-snmpwalk-helper-functions.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/6547409036856606320'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/6547409036856606320'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2019/09/groovy-snmpwalk-helper-functions.html' title='Groovy SNMPwalk Helper Functions'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-7811654837714061491</id><published>2019-07-05T09:05:00.000-05:00</published><updated>2019-07-05T09:05:29.823-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="DataSource"/><category scheme="http://www.blogger.com/atom/ns#" term="LogicMonitor"/><title type='text'> Discovering Enumerated Properties</title><content type='html'>tl;dr - the scripts are&amp;nbsp;&lt;a href=&quot;https://github.com/logicmonitor/monitoring-recipes/blob/master/DataSources/Groovy/SNMP/Examples/ActiveDiscovery_with_properties_with_enumeration.groovy&quot;&gt;here&lt;/a&gt;&amp;nbsp;and &lt;a href=&quot;https://github.com/logicmonitor/monitoring-recipes/blob/master/PropertySources/Examples/PropertySource%20with%20enumeration.groovy&quot;&gt;here&lt;/a&gt;.&lt;br /&gt;
&lt;br /&gt;
In the same vein as the &lt;a href=&quot;https://stuart.weenig.com/2019/07/visualizing-status-codes.html&quot;&gt;previous post&lt;/a&gt;, this post will talk about SNMP enumeration. While the &lt;a href=&quot;https://stuart.weenig.com/2019/07/visualizing-status-codes.html&quot;&gt;previous post talked about polling enumerated values and how to display them as intuitively as possible&lt;/a&gt;, this post will talk about polling more static values and using them as properties.&lt;br /&gt;
&lt;br /&gt;
There are two levels of properties that can be obtained via SNMP: device level properties (that pertain to the whole device) and instance level properties (that pertain to a particular thing on the device, of which there may be more than one).&amp;nbsp; Polling those properties is easy; this post will go over how to improve the quality of the data stored so that the data can be intuitively used.&lt;br /&gt;
&lt;h4&gt;
Polling properties vs. data points&lt;/h4&gt;
&lt;div&gt;
Polling properties is easy. Knowing which properties to poll as properties and which to poll as data points is a separate discussion entirely. Suffice it to say that things that represent a characteristic about an object should be properties and things that represent a behavior should be data points. Depending on the tool, only data points can be alerted upon, so that may influence a decision to make something that would normally be a property also a data point.&amp;nbsp; In other cases, sometimes data points can&#39;t be used to influence enablement of monitoring; so sometimes data points need to be properties too.&lt;/div&gt;
&lt;div&gt;
Assuming you&#39;re past the point where you&#39;ve looked at the MIB and figured out which OIDs should be properties and which should be data points, the next thing to look at is whether or not the OIDs you will be storing as properties have enumerations.&lt;/div&gt;
&lt;div&gt;
&lt;h4&gt;
Enumerations&lt;/h4&gt;
&lt;/div&gt;
&lt;div&gt;
An enumeration is used in SNMP to keep the &lt;b&gt;simple&lt;/b&gt; in &lt;b&gt;Simple&lt;/b&gt; Network Management Protocol. Instead of passing back a string comprised of ASCII characters, a map is created that connects a meaningful string to a single integer, which is passed back to the NMS. Passing back a single integer is much simpler than passing back a string of characters of varying length.&lt;/div&gt;
&lt;div&gt;
For example, the &lt;a href=&quot;http://www.net-snmp.org/docs/mibs/interfaces.html#ifAdminStatus&quot;&gt;admin status of an interface&lt;/a&gt; is found at &lt;a href=&quot;http://oid-info.com/get/1.3.6.1.2.1.2.2.1.7&quot;&gt;1.3.6.1.2.1.2.2.1.7&lt;/a&gt;. It&#39;s not a particularly good example of a property since it lends itself more to a data point, but having it as a property can allow for advanced filtering which may only be available using properties.&lt;br /&gt;
&lt;pre class=&quot;code&quot;&gt;ifAdminStatus OBJECT-TYPE
    SYNTAX  INTEGER {
                up(1),       -- ready to pass packets
                down(2),
                testing(3)   -- in some test mode
            }
    MAX-ACCESS  read-write
    STATUS      current
    DESCRIPTION
            &quot;The desired state of the interface.  The testing(3) state
            indicates that no operational packets can be passed.  When a
            managed system initializes, all interfaces start with
            ifAdminStatus in the down(2) state.  As a result of either
            explicit management action or per configuration information
            retained by the managed system, ifAdminStatus is then
            changed to either the up(1) or testing(3) states (or remains
            in the down(2) state).&quot;
    ::= { ifEntry 7 }
&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
You can see in the definition of the OID that it has a custom syntax that allows for one of three values. When this OID is polled, either a 1, a 2, or a 3 is returned. It&#39;s up to the NMS to interpret the different values to understand the meaning. If the NMS tool has MIB compilation, this may have a certain level of automation. If it doesn&#39;t, interpretation is up to you. Obviously, storing a 1, 2, or 3 as a property could be sufficient, but it would be better to store the interpreted meaning making use by humans easier and more intuitive.&lt;br /&gt;
&lt;h4&gt;
Interpretation of an instance property enumeration&lt;/h4&gt;
So how do we interpret this? It&#39;s actually pretty easy, but it involves some scripting to process the returned data. It also involves some research into the MIB to find out all the meanings. Let&#39;s start with a simple case of interfaces. In most cases a MIB will contain a &quot;Table&quot; OID with an &quot;Entry&quot; child OID containing all the instances with whatever OIDs go along with the instances. In the case of interfaces (ignoring the ifXTable), the table is called &lt;a href=&quot;http://oid-info.com/get/1.3.6.1.2.1.2.2&quot;&gt;&quot;ifTable&quot; at 1.3.6.1.2.1.2.2&lt;/a&gt;. You&#39;ll see that there is a child OID called &lt;a href=&quot;http://oid-info.com/cgi-bin/display?tree=1.3.6.1.2.1.2.2.1&quot;&gt;&quot;ifEntry&quot; at 1.3.6.1.2.1.2.2.1&lt;/a&gt;.&amp;nbsp; This kind of table typically has an index along with perhaps some properties and data points as columns. Each row is an instance. We&#39;re going to ignore the data points for now and focus on the OIDs that would do well stored as properties. MTU(4), speed(5), and MAC address(6) are good items to store as properties. They require no interpretation. However, ifType(3) and ifAdminStatus(7) require some interpretation to be useful.&lt;br /&gt;
LogicMonitor&#39;s multi-instance datasource can be configured to use a &lt;a href=&quot;https://groovy-lang.org/&quot;&gt;groovy script (yes, it&#39;s a real thing)&lt;/a&gt;&amp;nbsp;to do auto-discovery, which is the mechanism that discovers poll instances and sets properties per poll instance. The &lt;a href=&quot;https://www.logicmonitor.com/support/datasources/active-discovery/script-active-discovery/&quot;&gt;concept&lt;/a&gt; is pretty simple, use the groovy SNMP libraries to retrieve the data, use groovy to interpret the data, then just print the data to standard output, one line per poll instance.&lt;br /&gt;
I recently wrote &lt;a href=&quot;https://github.com/logicmonitor/monitoring-recipes/blob/master/DataSources/Groovy/SNMP/Examples/ActiveDiscovery_with_properties_with_enumeration.groovy&quot;&gt;a script to do this&lt;/a&gt;. It&#39;s all self documented with explanations and sample output and everything. Some points to consider at the following lines:&lt;br /&gt;
&lt;ol&gt;
&lt;li value=&quot;11&quot;&gt;This line defines the address of the Entry table. Everything we will be polling happens to live under this branch of the OID tree.&lt;/li&gt;
&lt;li value=&quot;14&quot;&gt;This is where we define which column of the ifEntry table contains the name that we should be using for each of our instances.&lt;/li&gt;
&lt;li value=&quot;32&quot;&gt;This line shows the information from the MIB added to the script so that the script can interpret the meaning from the returned value for ifAdminStatus&lt;/li&gt;
&lt;li&gt;This line shows the information from the MIB added to the script so that the script can interpret the meaning from the returned value for ifOperStatus&lt;/li&gt;
&lt;li value=&quot;40&quot;&gt;This line shows that we&#39;re still polling ifMtu as a property, but there is no interpretation available for the value. We could alternatively put [&quot;1500&quot;:&quot;Default (1500)&quot;] instead of [:] to tell the script to add some meaning to the most common value of MTU&lt;/li&gt;
&lt;li&gt;ifPhysAddress is the MAC address and needs no interpretation&lt;/li&gt;
&lt;li value=&quot;51&quot;&gt;This line shows an interpretation that isn&#39;t defined in the MIB. Instead of just passing the raw value through, we can provide our own interpretation of the speed to give some more intuitive values for common speeds. If the speed of the interface isn&#39;t in our map, the speed itself will be stored as the value. If it does happen to match on eof&lt;/li&gt;
&lt;li value=&quot;59&quot;&gt;This is where we start to define the enumeration for ifType. Turns out ifType has over 200 different enumerated values. Each of these is defined in the script so that the proper type name can be stored as a property. &lt;/li&gt;
&lt;li value=&quot;102&quot;&gt;Here&#39;s where you can see some sample output against a device here in my house. Notice that there&#39;s one line for each port (both physical and logical). &lt;/li&gt;
&lt;li value=&quot;117&quot;&gt;This line is a good example showing the interpreted values of ifAdminStatus, ifOperStatus, ifSpeed, and ifType.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;
Interpretation of device level properties&lt;/h4&gt;
Polling and storing device level properties is a bit simpler mainly because looping through instances is not required. We do have to provide each OID, the name we want the property stored under, and the interpretation, if any. All of this comes from the MIB. The script to do this is &lt;a href=&quot;https://github.com/logicmonitor/monitoring-recipes/blob/master/PropertySources/Examples/PropertySource%20with%20enumeration.groovy&quot;&gt;here&lt;/a&gt;. This example comes from the mGuard MIB, which is from some work I did recently. However, the OIDs can be replaced with any OID from any MIB (notice there&#39;s not a baseOID), as long as the OID returns a single value because the script does an SNMP get on that OID.&lt;br /&gt;
&lt;h4&gt;
Conclusion&lt;/h4&gt;
That&#39;s about it. These two scripts can be used to add real meaning to device and instance level properties. The only things that have to change are the data that come from the MIB itself. Perhaps one of these days I&#39;ll get around to writing a MIB parser that will output this information for all OIDs in the MIB. Yeah, when I have time.&lt;/div&gt;
</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/7811654837714061491/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2019/07/discovering-enumerated-properties.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/7811654837714061491'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/7811654837714061491'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2019/07/discovering-enumerated-properties.html' title=' Discovering Enumerated Properties'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-8436723751981369192</id><published>2019-07-02T11:32:00.000-05:00</published><updated>2019-08-23T11:09:34.210-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="DataSource"/><category scheme="http://www.blogger.com/atom/ns#" term="Graph"/><category scheme="http://www.blogger.com/atom/ns#" term="LogicMonitor"/><title type='text'>Visualizing Status Codes</title><content type='html'>If you follow me on &lt;a href=&quot;http://www.linkedin.com/in/sweenig&quot; target=&quot;_blank&quot;&gt;LinkedIn&lt;/a&gt;, you will have noticed that I changed jobs and moved from Houston to Austin. I&#39;m now working for &lt;a href=&quot;http://www.logicmonitor.com/&quot; target=&quot;_blank&quot;&gt;LogicMonitor&lt;/a&gt; as a &lt;a href=&quot;https://sweenig.github.io/resume/#current&quot; target=&quot;_blank&quot;&gt;Sales/Monitoring Engineer&lt;/a&gt;. It&#39;s a great job and I&#39;m enjoying it a lot more than previous jobs. One of the advantages of this job is that I should have much more opportunity, desire, and content for blog posts.&lt;br /&gt;
&lt;br /&gt;
If this is your first time here, know that this blog is not written for you. It&#39;s written for me. I increasingly need more and more reminders of how to do things. That goes especially for things that I devise since no one else knows it unless I tell them. This blog is primarily a place for me to keep those things written down.&lt;br /&gt;
&lt;br /&gt;
Anyway, on to this blog post. LogicMonitor monitors IT infrastructure. After collecting data through various mechanisms, it stores the data in a big database in the cloud and then provides a cloud hosted front end website to display the data. Part of the display is graphs. Many times, the metrics being graphed lend themselves to being plotted on a &lt;a href=&quot;https://en.wikipedia.org/wiki/Cartesian_coordinate_system&quot; target=&quot;_blank&quot;&gt;Cartesian coordinated&lt;/a&gt; graph. However, sometimes the metric being polled is a status code. A good example of this is license status on Sophos&#39; XG Firewall. This metric is found at &lt;a href=&quot;http://oid-info.com/get/1.3.6.1.4.1.21067.2.1.3.4.1&quot;&gt;.1.3.6.1.4.1.21067.2.1.3.4.1&lt;/a&gt;.&lt;br /&gt;
&lt;pre class=&quot;code&quot;&gt;asSubStatus OBJECT-TYPE
    SYNTAX          SubscriptionStatusType
    MAX-ACCESS      read-only
    STATUS          current
    DESCRIPTION     &quot; &quot;
    ::= { liAntispam 1 }
&lt;/pre&gt;
The syntax is &quot;SubscriptionStatusType&quot;, which is an enumerated type meaning that only a number is returned, but that number has a meaning depending on the different values returned. Looking at the syntax definition in the MIB will help illustrate:&lt;br /&gt;
&lt;pre class=&quot;code&quot;&gt;SubscriptionStatusType ::= TEXTUAL-CONVENTION
        STATUS            current
        DESCRIPTION       &quot;enumerated type for subscription status&quot;
        SYNTAX INTEGER {
                 trial          ( 1 ),
                 unsubscribed   ( 2 ),
                 subscribed     ( 3 ),
                 expired        ( 4 )
        }
&lt;/pre&gt;
So each different value returned indicates a particular state of the license subscription. It&#39;s not like a percentage where 100% is good and 0% is bad and there might be values in between. It&#39;s not like a rate, where a high number is fast and a low number is slow. It only has discreet values and values in between don&#39;t actually have any meaning.&lt;br /&gt;
Normally, without putting in much effort, someone might easily create a graph that just plots this number, putting time on the x-axis and the value retuned on the y-axis. This results in what you see here:&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZw-14xVXCR1sDnyK2nGzIKdlTRPgykaEXeg41zkAJRitqoQqHjx1pbpZtU0qwdjd4ZC9LZWwHpoBqp42M8xWCS9bFRc0_mazSwzm4L0XibRoaAZoVlRCQEuGxzCGFlUMfTZPPmdNzYhCH/s1600/Screen+Shot+2019-07-02+at+10.18.51+AM.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;780&quot; data-original-width=&quot;1288&quot; height=&quot;386&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZw-14xVXCR1sDnyK2nGzIKdlTRPgykaEXeg41zkAJRitqoQqHjx1pbpZtU0qwdjd4ZC9LZWwHpoBqp42M8xWCS9bFRc0_mazSwzm4L0XibRoaAZoVlRCQEuGxzCGFlUMfTZPPmdNzYhCH/s640/Screen+Shot+2019-07-02+at+10.18.51+AM.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
As you can see, it&#39;s not very helpful. It&#39;s a flat line because the status has been the same for the entire time range. That&#39;s ok. However, there&#39;s no real indicator of meaning. Some effort was made to add the meaning to the data point description (which appears in the tooltip). However, the enumeration is so long that it doesn&#39;t really fit in the tooltip. What does a 3 mean? Also, it&#39;s not illustrated here, but what happens if the value changes from 3 to 4? There would be two flat lines, one at 3 before the change and one at 4 after the change. But what would be shown at the change? Would it be a vertical line from 3 to 4? Would it be slightly slanted? Also not illustrated here, but what happens when larger timeframes are chosen and values are aggregated together (most often using an average)? Imagine that line at 3 that transitions to 4. What if that happened in middle of the quarter and you viewed it at the end of the quarter? If this status was polled every hour, that would mean 2190 data points to display! &lt;a href=&quot;https://guides.library.duke.edu/datavis/topten&quot; target=&quot;_blank&quot;&gt;That&#39;s too many&lt;/a&gt;. Almost every graphing solution would attempt to decrease the data points by grouping points and averaging every group. In the case of a quarterly timeframe, it might simplify by averaging all data points for a single day together. This could be fine for most days, except for the one where there was a change. That would show an average of 3&#39;s and 4&#39;s, yielding a value of 3.5. WTH does 3.5 mean? It gets worse if you have a 2 that transitions to a 3 which then later transitions to a 4. You could end up with an average of 3, indicating no problem at all!?!?&amp;nbsp; It&#39;s not intuitive; and&amp;nbsp;&lt;a href=&quot;https://en.wikipedia.org/wiki/The_Design_of_Everyday_Things&quot; target=&quot;_blank&quot;&gt;graphs need to be intuitive&lt;/a&gt;.&lt;br /&gt;
&lt;h4&gt;
So, what do we do?&lt;/h4&gt;
Well, we might be tempted to normalize the data. This is actually a very good idea. Let me explain: normalizing the data transforms it into a scale that is more intuitive. For example, we might say that we will normalize the data using the following rules:&lt;br /&gt;
&lt;ol&gt;
&lt;li&gt;A value of 3 is good, so we&#39;ll call that 1&lt;/li&gt;
&lt;li&gt;Any other value is bad, so we&#39;ll call that 0&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;
Pretty cool. That&#39;s a pretty good one. Any time everything is ok, we would plot a 1. Any other status is undesirable and we would plot a 0. Transitions are still ugly and potentially troublesome. If we make one tweak, it could allow us to put some context around the resulting values. What if we changed it to 100% instead of 1 and 0% instead of 0? If we did that, we could actually put some additional meaning behind the resulting values. Thing about it, if everything is good, you&#39;re plotting 100%. What does that mean? It means that for 100% of the timeframe displayed, the status was good. If the status is 4, we&#39;d see a line down at 0% meaning that for 100% of the timeframe displayed, the status was not good, or conversely: the status was good 0% of the timeframe. We could also create another data point which is the inverse of our normalized data:&lt;/div&gt;
&lt;div&gt;
&lt;ol&gt;
&lt;li&gt;If value is 3, plot 100, else plot 0&lt;/li&gt;
&lt;li&gt;Plot (100 - the value from above)&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div&gt;
Doing this would also let us use a more intuitive graph called a stacked area graph. We could plot our normalized data using a pleasant color like blue or green and plot the inverse data using a warning color like red or orange. This would give us two series of data that compliment each other and when plotted as a stacked graph would look like the second graph &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1li0OORuHWwawmYxo7N0wnAT_jo9-BZboA3AM-s30xGE/edit?usp=sharing&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt; (the first graph is a status code plot for reference):&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj9KbVkZuhUnWiyn1E1cUQ2-aqB4WjNpaRUFvrk6GKSpo5JlWvzrWU5mEMxqR5xONH4kzjgdvUqIjM8VuE0n97cngftd-enF-ayJE2lwEvkbyEHXlq2WN9SvuiFq3I2vUEhFAEly5I9NVYm/s1600/Screen+Shot+2019-07-02+at+11.07.37+AM.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1526&quot; data-original-width=&quot;1224&quot; height=&quot;640&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj9KbVkZuhUnWiyn1E1cUQ2-aqB4WjNpaRUFvrk6GKSpo5JlWvzrWU5mEMxqR5xONH4kzjgdvUqIjM8VuE0n97cngftd-enF-ayJE2lwEvkbyEHXlq2WN9SvuiFq3I2vUEhFAEly5I9NVYm/s640/Screen+Shot+2019-07-02+at+11.07.37+AM.png&quot; width=&quot;512&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;
See how much more intuitive that is? The first graph in the above picture is a plot of the raw status code. How easy is it to know when things aren&#39;t good (without reading the axis labels)? Doable but not instantly intuitive. Imagine you had 12 of these kinds of graphs on a single dashboard on a wall. Would you want to take the time and effort to read the axis labels of each one to know how things are going? No. Now look at the second graph. It&#39;s pretty easy to tell that there were two different problems between 8pm and 11am and 1pm and 5pm.&amp;nbsp; We could even remove the y-axis labels and you&#39;d still probably be able to tell with a glance how things are doing. Imagine 12 different graphs like this. How easy is it to see if there&#39;s a problem? Just look for the red!&lt;/div&gt;
&lt;div&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;div&gt;
There are only two drawbacks. Have you noticed? We see that there are two problems, but are they the same problem? Actually, they&#39;re not. Also, are they the same severity of problem? The morning problem is that status goes from &quot;subscribed&quot; to &quot;expired&quot;. The afternoon problem is that the status goes from &quot;subscribed&quot; (did you notice that it returned to a good status at noon?) to &quot;trial&quot;. The morning problem is worse than the afternoon problem.&amp;nbsp; There&#39;s a way to visualize this so that it all looks good. Let me explain:&lt;/div&gt;
&lt;div&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;div&gt;
Essentially we want to normalize the data but still keep as much detail as possible. We&#39;ll need to have four series, each with its own color. For any one data point, we&#39;ll only have a value of 100% in one of the series. All the others will be 0%. Meaning that for the timeframe that data point represents, whichever series has a value of 100% indicates the status for that moment. Let&#39;s look at the normalization rules:&lt;/div&gt;
&lt;div&gt;
&lt;ol&gt;
&lt;li&gt;If the status code is 1, return 100, else return nothing (100 here means Trial status)&lt;/li&gt;
&lt;li&gt;If the status code is 2, return 100, else return nothing (100 here means Unsubscribed status)&lt;/li&gt;
&lt;li&gt;If the status code is 3, return 100, else return nothing (100 here means Subscribed status)&lt;/li&gt;
&lt;li&gt;If the status code is 4, return 100, else return nothing (100 here means Expired status)&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;
&lt;a href=&quot;https://docs.google.com/spreadsheets/d/1li0OORuHWwawmYxo7N0wnAT_jo9-BZboA3AM-s30xGE/edit?usp=sharing&quot; target=&quot;_blank&quot;&gt;This&lt;/a&gt; is what it would look like (graph on the right, first two shown for reference):&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhtfo6XPE6t7CfWym69U_yIgSG_PTOG34Mm9k-cWSTMcvXMNPIUtNy21h_45k_NaoZtI-PSlZf7VcrjYZRk24K0qryVS4dLUuCbocrpJ4ENc2AOl7ueY9n9devIj5naVkR7gThi_g5ENM3E/s1600/Screen+Shot+2019-07-02+at+11.16.47+AM.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;992&quot; data-original-width=&quot;1600&quot; height=&quot;396&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhtfo6XPE6t7CfWym69U_yIgSG_PTOG34Mm9k-cWSTMcvXMNPIUtNy21h_45k_NaoZtI-PSlZf7VcrjYZRk24K0qryVS4dLUuCbocrpJ4ENc2AOl7ueY9n9devIj5naVkR7gThi_g5ENM3E/s640/Screen+Shot+2019-07-02+at+11.16.47+AM.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;
Notice how the problem in the morning is highlighted with a red and the problem in the afternoon is highlighted with a yellow? Easy to tell that there are two problems, that they are different, and that the morning problem is the more severe.&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;div&gt;
This is what the final version would look like in the LogicMonitor web gui (not interesting I know since the status code didn&#39;t change the whole time I was building this):&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhDhxiCjpEhCp0cNrmEUOtZ6Z9X-zpRMBXY2hg0BPTH9W3LiXGXxWFkhNDteofcP9agu55SkVkca4254IF5So8I24gN2eXbInzOcWZuOvFn6X3ou6NmHFps_KoR74nmvUf_EecIt8scnLJy/s1600/Screen+Shot+2019-07-02+at+11.26.35+AM.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;780&quot; data-original-width=&quot;1288&quot; height=&quot;386&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhDhxiCjpEhCp0cNrmEUOtZ6Z9X-zpRMBXY2hg0BPTH9W3LiXGXxWFkhNDteofcP9agu55SkVkca4254IF5So8I24gN2eXbInzOcWZuOvFn6X3ou6NmHFps_KoR74nmvUf_EecIt8scnLJy/s640/Screen+Shot+2019-07-02+at+11.26.35+AM.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;
&lt;br /&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEitEJMMGwno0imPlnC4OxmVqaS3DTppMRnyV7WgpzifVu5cZTdHuI0oauCtAVubuARmv4i-vGTexk9eblYeEjMqf0Ic6x-pTID3NX1Y9QdHMGOQU9LjQfZqqAOLL4PNlc_zrbg1flsvhjLO/s1600/Screen+Shot+2019-08-23+at+10.24.06+AM.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;497&quot; data-original-width=&quot;1600&quot; height=&quot;196&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEitEJMMGwno0imPlnC4OxmVqaS3DTppMRnyV7WgpzifVu5cZTdHuI0oauCtAVubuARmv4i-vGTexk9eblYeEjMqf0Ic6x-pTID3NX1Y9QdHMGOQU9LjQfZqqAOLL4PNlc_zrbg1flsvhjLO/s640/Screen+Shot+2019-08-23+at+10.24.06+AM.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;div&gt;
Here&#39;s how it&#39;s built in the GUI:&lt;br /&gt;
Notes on the screenshot:&lt;br /&gt;
&lt;br /&gt;
&lt;ul&gt;
&lt;li&gt;it shows line types of &quot;Area&quot; but they should be &quot;Stacked&quot; to display properly&lt;/li&gt;
&lt;li&gt;the formulas should be `if(in(StatusCode,3),100,unkn())`&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiLlAt4Rt625QI-PHwVXl1RbgEx9lmjcYOJ9NcENfQztpKqEoIopH0Z2nWH0qwt_y_-oUYbk2apwvEmfGPDzb4AVjvEUjBIzTWPAKiJtQa-YSMkpbrauFnJmBQHXm6Fq7YcW8eumhmqTCZs/s1600/Screen+Shot+2019-07-02+at+11.30.07+AM.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1118&quot; data-original-width=&quot;1600&quot; height=&quot;447&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiLlAt4Rt625QI-PHwVXl1RbgEx9lmjcYOJ9NcENfQztpKqEoIopH0Z2nWH0qwt_y_-oUYbk2apwvEmfGPDzb4AVjvEUjBIzTWPAKiJtQa-YSMkpbrauFnJmBQHXm6Fq7YcW8eumhmqTCZs/s640/Screen+Shot+2019-07-02+at+11.30.07+AM.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;
&lt;br /&gt;&lt;/div&gt;
</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/8436723751981369192/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2019/07/visualizing-status-codes.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/8436723751981369192'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/8436723751981369192'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2019/07/visualizing-status-codes.html' title='Visualizing Status Codes'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZw-14xVXCR1sDnyK2nGzIKdlTRPgykaEXeg41zkAJRitqoQqHjx1pbpZtU0qwdjd4ZC9LZWwHpoBqp42M8xWCS9bFRc0_mazSwzm4L0XibRoaAZoVlRCQEuGxzCGFlUMfTZPPmdNzYhCH/s72-c/Screen+Shot+2019-07-02+at+10.18.51+AM.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-9129463197233057363.post-4169411586184731230</id><published>2019-06-03T09:30:00.001-05:00</published><updated>2019-06-03T09:30:21.032-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="bittorrent"/><title type='text'>How to share a ton of stuff with someone else</title><content type='html'>&lt;br /&gt;
&lt;h2&gt;
If you have the stuff to share:&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Download and install &lt;a href=&quot;https://www.resilio.com/individuals-sync/?&quot; target=&quot;_blank&quot;&gt;Resilio Sync&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;After opening the app, click the plus sign in the top left corner and select &quot;Standard Folder&quot;&lt;/li&gt;
&lt;li&gt;Browse to the folder you want to share with someone else and click &quot;Open&quot;&lt;/li&gt;
&lt;li&gt;A new entry will appear in your list of folders. At the right end of this entry will appear three dots (when you mouseover the row). Click the three dots and select &quot;Copy Read Only key&quot; or &quot;Copy Read &amp;amp; Write key&quot; depending on whether or not you want the sharer to be able to change what&#39;s in the shared folder. They key is now in your clipboard.&lt;/li&gt;
&lt;li&gt;Send the key to the person you want to share with.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
If you have received a key:&lt;/h2&gt;
&lt;br /&gt;
&lt;div&gt;
&lt;ol&gt;
&lt;li&gt;Download and install&amp;nbsp;&lt;a href=&quot;https://www.resilio.com/individuals-sync/?&quot; target=&quot;_blank&quot;&gt;Resilio Sync&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;After opening the app, click the plus sign in the top left corner and select &quot;Enter key or link&quot;&lt;/li&gt;
&lt;li&gt;Paste in the key that was sent to you&lt;/li&gt;
&lt;li&gt;Browse to the folder you want to synchronize and select Open.&lt;/li&gt;
&lt;li&gt;Go grab a coffee and chips and wait until the synchronization finishes.&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;br /&gt;&lt;/div&gt;
&lt;div&gt;
Disclaimer: using any protocol to transmit/receive data that you are not legally allowed to transmit/receive is obviously illegal. I&#39;m not responsible if you use this to do something illegal.&lt;/div&gt;
</content><link rel='replies' type='application/atom+xml' href='http://stuart.weenig.com/feeds/4169411586184731230/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://stuart.weenig.com/2019/06/how-to-share-ton-of-stuff-with-someone.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/4169411586184731230'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/9129463197233057363/posts/default/4169411586184731230'/><link rel='alternate' type='text/html' href='http://stuart.weenig.com/2019/06/how-to-share-ton-of-stuff-with-someone.html' title='How to share a ton of stuff with someone else'/><author><name>Stuart Weenig</name><uri>http://www.blogger.com/profile/08727259847102590099</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry></feed>