<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Network Automation on 0x2142 | Networking Nonsense</title>
    <link>https://0x2142.com/categories/network-automation/</link>
    <description>Recent content in Network Automation on 0x2142 | Networking Nonsense</description>
    <image>
      <title>0x2142 | Networking Nonsense</title>
      <url>https://0x2142.com/logo.jpg</url>
      <link>https://0x2142.com/logo.jpg</link>
    </image>
    <generator>Hugo -- 0.143.1</generator>
    <language>en-us</language>
    <lastBuildDate>Tue, 21 Feb 2023 19:52:12 +0000</lastBuildDate>
    <atom:link href="https://0x2142.com/categories/network-automation/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Getting Started with Cisco YANG Suite</title>
      <link>https://0x2142.com/getting-started-with-cisco-yang-suite/</link>
      <pubDate>Tue, 21 Feb 2023 19:52:12 +0000</pubDate>
      <guid>https://0x2142.com/getting-started-with-cisco-yang-suite/</guid>
      <description>An introduction to setting up Cisco YANG Suite for exploring and testing network device automation via NETCONF &amp;amp; RESTCONF - including an example of automating wireless pre-shared key rotation.</description>
      <content:encoded><![CDATA[<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/nnd4KqeeqIw?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<p>Back when I started diving into studying for the Cisco DevNet certifications, I thought a lot of the REST API stuff was fairly easy. But I always struggled a bit with wrapping my head around YANG models &amp; how exactly NETCONF/RESTCONF worked. A lot of it seemed more abstract, and browsing through YANG models isn&rsquo;t quite as intuitive as most REST API documentation.</p>
<p>These days I end up working quite a bit with NETCONF - and if there&rsquo;s one tool that&rsquo;s helped me more than anything, it&rsquo;s certainly YANG suite. So in this post, I wanted to spend a little bit of time walking through how to get set up with this tool - and walk through a quick example of how it can be used.</p>
<hr>
<h2 id="whats-yang-suite">What&rsquo;s YANG Suite?</h2>
<p>To put it simply: YANG Suite is a web-based tool that allows you to easily browse through YANG models &amp; find what you need. It also has a bunch of tools to run test queries against devices, which makes developing NETCONF automation easier. It can also auto-generate code snippets - and who doesn&rsquo;t like that? 😄</p>
<p>YANG suite is an open source tool maintained by Cisco &amp; the GitHub repo <a href="https://github.com/CiscoDevNet/yangsuite">here</a>.</p>
<h2 id="initial-setup">Initial Setup</h2>
<p>YANG suite is a containerized application, so we&rsquo;ll need a container host to run it on. You could use something like Docker Desktop to run it on your local PC, but I&rsquo;ll be using a dedicated Ubuntu machine in my lab with Docker installed.</p>
<blockquote>
<p>Note: YANG suite now supports a local Python pip-based install. Check out the <a href="https://github.com/CiscoDevNet/yangsuite">GitHub</a> repo for more info on that.</p></blockquote>
<h3 id="deploy-with-docker">Deploy with Docker</h3>
<p>First thing we&rsquo;ll do is pull down the code from GitHub:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">matt@yangsuite:~/yangsuite/docker$ git clone https://github.com/CiscoDevNet/yangsuite.git
</span></span></code></pre></div><p>Next, we can execute the setup script to help provision the YANG Suite containers, which is located at <code>yangsuite/docker/start_yang_suite.sh</code>. The script will prompt you to set up an admin user and generate self-signed certificates:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">matt@yangsuite:~/yangsuite/docker$ <span class="nb">cd</span> yangsuite/docker
</span></span><span class="line"><span class="cl">matt@yangsuite:~/yangsuite/docker$ ./start_yang_suite.sh
</span></span><span class="line"><span class="cl">Hello, please setup YANG Suite admin user.
</span></span><span class="line"><span class="cl">username: matt
</span></span><span class="line"><span class="cl">password:
</span></span><span class="line"><span class="cl">confirm password:
</span></span><span class="line"><span class="cl">email: matt@0x2142.local
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Setup <span class="nb">test</span> certificates? <span class="o">(</span>y/n<span class="o">)</span>: y
</span></span><span class="line"><span class="cl">  -- output omitted -- 
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">-----
</span></span><span class="line"><span class="cl">You are about to be asked to enter information that will be incorporated
</span></span><span class="line"><span class="cl">into your certificate request.
</span></span><span class="line"><span class="cl">What you are about to enter is what is called a Distinguished Name or a DN.
</span></span><span class="line"><span class="cl">There are quite a few fields but you can leave some blank
</span></span><span class="line"><span class="cl">For some fields there will be a default value,
</span></span><span class="line"><span class="cl">If you enter <span class="s1">&#39;.&#39;</span>, the field will be left blank.
</span></span><span class="line"><span class="cl">-----
</span></span><span class="line"><span class="cl">Country Name <span class="o">(</span><span class="m">2</span> letter code<span class="o">)</span> <span class="o">[</span>AU<span class="o">]</span>: US
</span></span><span class="line"><span class="cl">State or Province Name <span class="o">(</span>full name<span class="o">)</span> <span class="o">[</span>Some-State<span class="o">]</span>:
</span></span><span class="line"><span class="cl">Locality Name <span class="o">(</span>eg, city<span class="o">)</span> <span class="o">[]</span>:
</span></span><span class="line"><span class="cl">Organization Name <span class="o">(</span>eg, company<span class="o">)</span> <span class="o">[</span>Internet Widgits Pty Ltd<span class="o">]</span>:
</span></span><span class="line"><span class="cl">Organizational Unit Name <span class="o">(</span>eg, section<span class="o">)</span> <span class="o">[]</span>:
</span></span><span class="line"><span class="cl">Common Name <span class="o">(</span>e.g. server FQDN or YOUR name<span class="o">)</span> <span class="o">[]</span>:
</span></span><span class="line"><span class="cl">Email Address <span class="o">[]</span>:
</span></span><span class="line"><span class="cl">Certificates generated...
</span></span><span class="line"><span class="cl">Building docker containers...
</span></span></code></pre></div><p>Once all of the above info is provided, the script will build the YANG Suite contianer images &amp; launch them. However, by default it will launch in the foreground - so we can kill the script with <code>Ctrl-C</code> &amp; relaunch the containers with <code>docker-compose up -d</code>.</p>
<h3 id="allow-access-from-remote-ips">Allow Access from Remote IPs</h3>
<p>We may need to make a quick change depending on how we&rsquo;ll be accessing YANG Suite.</p>
<p>By default, YANG Suite will only permit access to the web UI from <code>localhost</code> - so if you&rsquo;re running Docker on your local PC, then no change is necessary.</p>
<p>However, if you&rsquo;re running Docker on a dedicated VM like I am, then we&rsquo;ll need to allow external IPs to access the YANG Suite UI. This is controlled by the <code>DJANGO_ALLOWED_HOSTS</code> environment variable that gets supplied to the container.</p>
<p>As long as we&rsquo;ve already run the setup script once - our environment variables will be saved to <code>yangsuite/docker/yangsuite/setup.env</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">DJANGO_SETTINGS_MODULE</span><span class="o">=</span>yangsuite.settings.production
</span></span><span class="line"><span class="cl"><span class="nv">MEDIA_ROOT</span><span class="o">=</span>/ys-data/
</span></span><span class="line"><span class="cl"><span class="nv">STATIC_ROOT</span><span class="o">=</span>/ys-static/
</span></span><span class="line"><span class="cl"><span class="nv">DJANGO_STATIC_ROOT</span><span class="o">=</span>/ys-static/
</span></span><span class="line"><span class="cl"><span class="nv">DJANGO_ALLOWED_HOSTS</span><span class="o">=</span>localhost
</span></span><span class="line"><span class="cl"><span class="nv">YS_ADMIN_USER</span><span class="o">=</span>&lt;removed&gt;
</span></span><span class="line"><span class="cl"><span class="nv">YS_ADMIN_PASS</span><span class="o">=</span>&lt;removed&gt;
</span></span><span class="line"><span class="cl"><span class="nv">YS_ADMIN_EMAIL</span><span class="o">=</span>&lt;removed&gt;
</span></span></code></pre></div><p>We&rsquo;ll need to change <code>DJANGO_ALLOWED_HOSTS=localhost</code> to the IP address <strong>of the Docker host</strong>. So if your Docker host is reachable at <code>192.168.1.1</code>, then we&rsquo;ll set: <code>DJANGO_ALLOWED_HOSTS=192.168.1.1</code>.</p>
<p>After which, we can bring up the containers again with <code>docker-compose up -d</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">matt@yangsuite:~/yangsuite/docker$ docker-compose up -d
</span></span><span class="line"><span class="cl">Recreating docker-yangsuite-1 ... <span class="k">done</span>
</span></span><span class="line"><span class="cl">Recreating docker-backup-1    ... <span class="k">done</span>
</span></span><span class="line"><span class="cl">Recreating docker-nginx-1     ... <span class="k">done</span>
</span></span></code></pre></div><p>Once everything is running, the web UI can be reached at <code>https://&lt;ip address&gt;:8443</code>.</p>
<h3 id="loading-yang-models">Loading YANG Models</h3>
<p>After we get logged in, we&rsquo;ll need to load the appropriate YANG models for whatever devices we&rsquo;re using.</p>
<p>For example, I&rsquo;ll be using Cisco IOS-XE devices running version 17.6.x code - so I&rsquo;ll need to download the YANG models from: <code>https://github.com/YangModels/yang/tree/main/vendor/cisco/xe/1761</code>.</p>
<p>Within YANG Suite, we&rsquo;ll navigate to <strong>Setup &gt; YANG files and repositories</strong>. Then click the button to add a <strong>New repository</strong>:</p>
<p><img alt="add-yang-repo-name-1" loading="lazy" src="/content/images/2022/12/add-yang-repo-name-1.png#center"></p>
<p>You can name the repo whatever you like. In my case, I named it <code>ios-xe-1761</code> which describes the models it will hold.</p>
<p>Next we can tell YANG Suite where we want to pull our models from. There are a couple of options, including a direct upload from your local PC or querying models directly from a device - but I&rsquo;ll choose <code>Git</code>.</p>
<p>After which, we&rsquo;ll need to fill in the following info:</p>
<p><img alt="add-yang-repo-from-git" loading="lazy" src="/content/images/2022/12/add-yang-repo-from-git.png#center"></p>
<p>The <strong>Repository URL</strong> will be the base Git repo that we&rsquo;re pulling from. In this case, that&rsquo;s <code>https://github.com/YangModels/yang.git</code>.</p>
<p>We&rsquo;ll also need to specify which <strong>Git branch</strong> to pull from, which for the <strong>YangModels</strong> repo will be <code>main</code>.</p>
<p>Then we&rsquo;ll specify which directory to import. The <strong>YangModels</strong> repository holds models for a large number of vendors &amp; devices - so we only want to pull in what is necessary. In my case, I&rsquo;ve set this to <code>vendor/cisco/xe/1761</code> - which will only pull in the YANG models for Cisco IOS-XE 17.6.x devices. I&rsquo;ve also checked the box for <strong>Include subdirectories</strong>.</p>
<p>Once that&rsquo;s all set, we can hit <strong>Import YANG files</strong>. Depending on how many models are being downloaded &amp; how quick your Docker host is, this may take a few minutes.</p>
<p>After it finishes, mine displayed a message showing that 827 models had been added:</p>
<p><img alt="add-yang-repo-complete" loading="lazy" src="/content/images/2022/12/add-yang-repo-complete.png#center"></p>
<h3 id="selecting-yang-module-sets">Selecting YANG Module Sets</h3>
<p>Once we have all of our models downloaded into YANG Suite, it&rsquo;s time to set up some module sets. You can think of these as filters for the YANG models.</p>
<p>For example, later on we&rsquo;ll show how to change a wireless PSK with NETCONF. So since I already know what type of information I&rsquo;ll be working with, I can create a module set to only contain those models.</p>
<p>YANG Suite will require at least one module set to be configured, even if you don&rsquo;t want to filter anything out.</p>
<p>To do this, we&rsquo;ll navigate to <strong>Setup &gt; YANG model sets</strong>. Then click <strong>New YANG set</strong>:</p>
<p><img alt="add-yang-module-set" loading="lazy" src="/content/images/2023/02/add-yang-module-set.png#center"></p>
<p>Again, since we&rsquo;ll be working with wireless data in a bit, I&rsquo;ll name my model set <code>wireless</code>. We&rsquo;ll also have to select which YANG repository this will pull from, which will be <code>ios-xe-1761</code>.</p>
<p>Next, we&rsquo;ll get to select which YANG models we want to include in our module set. To make things easy, I&rsquo;ll filter the models by <code>wireless</code> and then click <strong>Add selected</strong>.</p>
<p><img alt="add-wireless-models" loading="lazy" src="/content/images/2023/02/add-wireless-models.png#center"></p>
<p>Once those are added, you&rsquo;ll likely see an error about missing dependencies. Some of the models we selected may have sub-models or data types that are located in other models.</p>
<p>In order to quickly add these, YANG Suite provides a one-click-button to <strong>Locate and add missing dependencies</strong>:</p>
<p><img alt="add-dependencies" loading="lazy" src="/content/images/2023/02/add-dependencies.png#center"></p>
<p>That&rsquo;s it - our model set is ready to use.</p>
<h3 id="adding-devices">Adding Devices</h3>
<p>Okay, so now that we have all of our necessary YANG models set up, let&rsquo;s take a add a device that we can use for testing. I&rsquo;ll be adding a Catalyst 9800 Wireless controller.</p>
<p>To do this, we&rsquo;ll head over to <strong>Setup &gt; Device Profiles</strong>. Then click on <strong>Create new device</strong>.</p>
<p><img alt="add-device" loading="lazy" src="/content/images/2023/02/add-device.png#center"></p>
<p>Here we&rsquo;ll fill in a name for our device profile, along with the device&rsquo;s IP/FQDN and login credentials.</p>
<p>Scrolling down a bit, we&rsquo;ll also need to make sure we enable the appropriate management protocols. So I&rsquo;ll be checking the box for both NETCONF and RESTCONF, since I have both enabled. I&rsquo;ll also add SSH, which isn&rsquo;t shown in the screenshot below.</p>
<p><img alt="add-device-enable-netconf" loading="lazy" src="/content/images/2023/02/add-device-enable-netconf.png#center"></p>
<p>Please note, you&rsquo;ll likely want to check the box for <em>Skip SSH key validation for this device</em> under the NETCONF settings.</p>
<p>After that&rsquo;s all done, we can click <strong>Create Profile</strong> at the bottom.</p>
<p>Then, to validate everything works - we&rsquo;ll select our device &amp; click the button for <strong>Check selected device&rsquo;s reachability</strong>.</p>
<p>With any luck, you should see a similar result as shown below:</p>
<p><img alt="check-device-reachability" loading="lazy" src="/content/images/2023/02/check-device-reachability.png#center"></p>
<h2 id="example-changing-wireless-psk">Example: Changing Wireless PSK</h2>
<p>Alright, now let&rsquo;s take a look at how we might use YANG Suite to test NETCONF / RESTCONF calls to a wireless controller.</p>
<p>In this example, perhaps we&rsquo;re developing automation to rotate the guest wireless network password on a regular basis. We need to figure out how specifically to accomplish that by finding the right YANG models &amp; understanding what data is sent and received with those calls.</p>
<p>We&rsquo;ll explore how to do this with both NETCONF and RESTCONF.</p>
<h3 id="via-netconf">Via NETCONF</h3>
<p>To start off, we&rsquo;ll navigate to <strong>Protocols &gt; NETCONF</strong>. On this page, we&rsquo;ll have full access to explore models, build XML payloads, and test them against our configured device.</p>
<h4 id="get-config">Get-config</h4>
<p>First thing we&rsquo;ll need is to select our <strong>YANG set</strong> &amp; <strong>Modules</strong>. In my case, the <strong>YANG set</strong> will be <code>wireless</code>, and I&rsquo;ll select the module: <code>Cisco-IOS-XE-wireless-wlan-cfg</code>.</p>
<p>Then click the button to <strong>Load Modules</strong>:</p>
<p><img alt="netconf-select-models" loading="lazy" src="/content/images/2023/02/netconf-select-models.png#center"></p>
<p>By default, our <strong>NETCONF Operation</strong> will be set to <code>get-config</code>, which will work for now since we only want to retrieve configuration and not change anything yet.</p>
<p>Under the <strong>Nodes</strong> pane, we can expand the tree a bit &amp; dig in to find which call we need.</p>
<p>For our example, we&rsquo;ll need to examine the current configured WLAN profiles. So we&rsquo;ll check the box next to <code>wlan-cfg-entries</code>:</p>
<p><img alt="netconf-nodes" loading="lazy" src="/content/images/2023/02/netconf-nodes.png#center"></p>
<p>This will retrieve all configured WLAN profiles on the controller. For now, let&rsquo;s do that so we can figure out what data we&rsquo;ll get back.</p>
<p>We can now click the button to <strong>Build RPC</strong>, and YANG Suite will automatically build the appropriate XML payload for us:</p>
<p><img alt="netconf-build-rpc" loading="lazy" src="/content/images/2023/02/netconf-build-rpc.png#center"></p>
<p>You&rsquo;ll also notice that I selected the <strong>catalyst 9800</strong> as the target device. Let&rsquo;s go ahead and run this with the <strong>Run RPC(s)</strong> button, which should pop open a new window &amp; establish a connection to our device.</p>
<p><img alt="netconf-rpc-response-1" loading="lazy" src="/content/images/2023/02/netconf-rpc-response-1.png#center"></p>
<p>We can see in the response above, that we have two WLAN profiles configured: <code>test-lab-network</code> and <code>test-guest-network</code>. For the purposes of this demo, I have opted to have the guest network PSK stored in plain text, so we can validate our changes more easily.</p>
<p>Okay, so what if we wanted to refine our query XML to only retrieve data on the <code>test-guest-network</code>? Maybe we also want to filter our the additional information returned, and only see the current PSK?</p>
<p>Back on the payload editor, we can expand <code>wlan-cfg-entries</code> and input our desired <code>profile-name</code>:</p>
<p><img alt="netconf-build-rpc-filtered-1" loading="lazy" src="/content/images/2023/02/netconf-build-rpc-filtered-1.png#center"></p>
<p>We&rsquo;ll also find the <code>psk</code> item further down in the list. Now we want to pull back this field, regardless of what the PSK is currently set to. So we&rsquo;ll click the value field, but leave it blank:</p>
<p><img alt="netconf-build-rpc-filtered-psk" loading="lazy" src="/content/images/2023/02/netconf-build-rpc-filtered-psk.png#center"></p>
<p>Once we click <strong>Build RPC</strong>, we can see our payload now shows the additional XML filters asking only for a single profile &amp; it&rsquo;s associated PSK:</p>
<p><img alt="netconf-rpc-payload-filtered" loading="lazy" src="/content/images/2023/02/netconf-rpc-payload-filtered.png#center"></p>
<p>If we run that RPC call, we&rsquo;ll now see that we only get back a limited set of data:</p>
<p><img alt="netconf-filtered-rpc-response" loading="lazy" src="/content/images/2023/02/netconf-filtered-rpc-response.png#center"></p>
<p>Now we know exactly what payload to send if we want to get back the current configured PSK for any specific WLAN profile.</p>
<p>Let&rsquo;s try changing it!</p>
<h4 id="edit-config">Edit-config</h4>
<p>Lucky for us, we&rsquo;re already nearly set up for that. We&rsquo;ll need to make two quick changes. First, set the <strong>NETCONF Operation</strong> to <code>edit-config</code>:</p>
<p><img alt="netconf-op-edit-config" loading="lazy" src="/content/images/2023/02/netconf-op-edit-config.png#center"></p>
<p>Then, under the YANG tree, we&rsquo;ll set a new PSK:</p>
<p><img alt="netconf-new-psk" loading="lazy" src="/content/images/2023/02/netconf-new-psk.png#center"></p>
<p>Again, we&rsquo;ll click <strong>Build RPC</strong> - and take a quick look at the proposed change:</p>
<p><img alt="netconf-new-psk-payload" loading="lazy" src="/content/images/2023/02/netconf-new-psk-payload.png#center"></p>
<p>Look good to you? Okay, let&rsquo;s send it:</p>
<p><img alt="netconf-new-psk-response" loading="lazy" src="/content/images/2023/02/netconf-new-psk-response.png#center"></p>
<p>Sure enough, the new configuration was sent to the wireless controller and we got a response of: <strong>ok</strong>.</p>
<p>But of course, we can check quickly by switching our operation back to <code>get-config</code> and re-running our previous XML payload:</p>
<p><img alt="netconf-new-psk-validation" loading="lazy" src="/content/images/2023/02/netconf-new-psk-validation.png#center"></p>
<h4 id="auto-generated-snippets">Auto-generated snippets</h4>
<p>Well if you&rsquo;re new to NETCONF, you might be asking yourself &ldquo;Okay great, but how do I get this into Python?!?!&rdquo;</p>
<p>YANG Suite has some built in tools to auto-generate Python code or Ansible playbooks, which can help us get started on our code more quickly.</p>
<p>To generate these, click the <strong>Replays</strong> button:</p>
<p><img alt="netconf-python-ansible" loading="lazy" src="/content/images/2023/02/netconf-python-ansible.png#center"></p>
<p>For example, I selected to auto-generate a Python script. Here&rsquo;s what that looks like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="ch">#! /usr/bin/env python</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">traceback</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">lxml.etree</span> <span class="k">as</span> <span class="nn">et</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">argparse</span> <span class="kn">import</span> <span class="n">ArgumentParser</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">ncclient</span> <span class="kn">import</span> <span class="n">manager</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">ncclient.operations</span> <span class="kn">import</span> <span class="n">RPCError</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">payload</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl"><span class="s1">&#39;&#39;&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">&lt;get-config xmlns=&#34;urn:ietf:params:xml:ns:netconf:base:1.0&#34;&gt;
</span></span></span><span class="line"><span class="cl"><span class="s1">    &lt;source&gt;
</span></span></span><span class="line"><span class="cl"><span class="s1">      &lt;running/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s1">    &lt;/source&gt;
</span></span></span><span class="line"><span class="cl"><span class="s1">    &lt;filter&gt;
</span></span></span><span class="line"><span class="cl"><span class="s1">      &lt;wlan-cfg-data xmlns=&#34;http://cisco.com/ns/yang/Cisco-IOS-XE-wireless-wlan-cfg&#34;&gt;
</span></span></span><span class="line"><span class="cl"><span class="s1">        &lt;wlan-cfg-entries&gt;
</span></span></span><span class="line"><span class="cl"><span class="s1">          &lt;wlan-cfg-entry&gt;
</span></span></span><span class="line"><span class="cl"><span class="s1">            &lt;profile-name&gt;test-guest-network&lt;/profile-name&gt;
</span></span></span><span class="line"><span class="cl"><span class="s1">            &lt;psk/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s1">          &lt;/wlan-cfg-entry&gt;
</span></span></span><span class="line"><span class="cl"><span class="s1">        &lt;/wlan-cfg-entries&gt;
</span></span></span><span class="line"><span class="cl"><span class="s1">      &lt;/wlan-cfg-data&gt;
</span></span></span><span class="line"><span class="cl"><span class="s1">    &lt;/filter&gt;
</span></span></span><span class="line"><span class="cl"><span class="s1">  &lt;/get-config&gt;
</span></span></span><span class="line"><span class="cl"><span class="s1">
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;&#39;&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">&#39;__main__&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">parser</span> <span class="o">=</span> <span class="n">ArgumentParser</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s1">&#39;Usage:&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># script arguments</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s1">&#39;-a&#39;</span><span class="p">,</span> <span class="s1">&#39;--host&#39;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">str</span><span class="p">,</span> <span class="n">required</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">help</span><span class="o">=</span><span class="s2">&#34;Device IP address or Hostname&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s1">&#39;-u&#39;</span><span class="p">,</span> <span class="s1">&#39;--username&#39;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">str</span><span class="p">,</span> <span class="n">required</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">help</span><span class="o">=</span><span class="s2">&#34;Device Username (netconf agent username)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s1">&#39;-p&#39;</span><span class="p">,</span> <span class="s1">&#39;--password&#39;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">str</span><span class="p">,</span> <span class="n">required</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">help</span><span class="o">=</span><span class="s2">&#34;Device Password (netconf agent password)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s1">&#39;--port&#39;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">int</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">830</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">help</span><span class="o">=</span><span class="s2">&#34;Netconf agent port&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">args</span> <span class="o">=</span> <span class="n">parser</span><span class="o">.</span><span class="n">parse_args</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># connect to netconf agent</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">manager</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">host</span><span class="o">=</span><span class="n">args</span><span class="o">.</span><span class="n">host</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                         <span class="n">port</span><span class="o">=</span><span class="n">args</span><span class="o">.</span><span class="n">port</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                         <span class="n">username</span><span class="o">=</span><span class="n">args</span><span class="o">.</span><span class="n">username</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                         <span class="n">password</span><span class="o">=</span><span class="n">args</span><span class="o">.</span><span class="n">password</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                         <span class="n">timeout</span><span class="o">=</span><span class="mi">90</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                         <span class="n">hostkey_verify</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                         <span class="n">device_params</span><span class="o">=</span><span class="p">{</span><span class="s1">&#39;name&#39;</span><span class="p">:</span> <span class="s1">&#39;csr&#39;</span><span class="p">})</span> <span class="k">as</span> <span class="n">m</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># execute netconf operation</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">rpc</span> <span class="ow">in</span> <span class="n">payload</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="n">response</span> <span class="o">=</span> <span class="n">m</span><span class="o">.</span><span class="n">dispatch</span><span class="p">(</span><span class="n">et</span><span class="o">.</span><span class="n">fromstring</span><span class="p">(</span><span class="n">rpc</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">                <span class="n">data</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="n">xml</span>
</span></span><span class="line"><span class="cl">            <span class="k">except</span> <span class="n">RPCError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="n">data</span> <span class="o">=</span> <span class="n">e</span><span class="o">.</span><span class="n">xml</span>
</span></span><span class="line"><span class="cl">                <span class="k">pass</span>
</span></span><span class="line"><span class="cl">            <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="n">traceback</span><span class="o">.</span><span class="n">print_exc</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">                <span class="n">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="c1"># beautify output</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">et</span><span class="o">.</span><span class="n">iselement</span><span class="p">(</span><span class="n">data</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">                <span class="n">data</span> <span class="o">=</span> <span class="n">et</span><span class="o">.</span><span class="n">tostring</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">pretty_print</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span><span class="o">.</span><span class="n">decode</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="n">out</span> <span class="o">=</span> <span class="n">et</span><span class="o">.</span><span class="n">tostring</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                    <span class="n">et</span><span class="o">.</span><span class="n">fromstring</span><span class="p">(</span><span class="n">data</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s1">&#39;utf-8&#39;</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">                    <span class="n">pretty_print</span><span class="o">=</span><span class="kc">True</span>
</span></span><span class="line"><span class="cl">                <span class="p">)</span><span class="o">.</span><span class="n">decode</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">            <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="n">traceback</span><span class="o">.</span><span class="n">print_exc</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">                <span class="n">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="nb">print</span><span class="p">(</span><span class="n">out</span><span class="p">)</span>
</span></span></code></pre></div><h3 id="via-restconf">Via RESTCONF</h3>
<p>What if we prefer working with REST &amp; JSON instead of XML? This time we&rsquo;ll navigate to <strong>Protocols &gt; RESTCONF</strong>.</p>
<p>Here, we&rsquo;ll select our YANG set &amp; modules again. In case you skipped the NETCONF section, we&rsquo;re working with the <code>wireless</code> YANG set &amp; the <code>Cisco-IOS-XE-wireless-wlan-cfg</code> module.</p>
<p>We&rsquo;ll also select a device to use, then click <strong>Load Module(s)</strong>:</p>
<p><img alt="restconf-nodes" loading="lazy" src="/content/images/2023/02/restconf-nodes.png#center"></p>
<p>Again, we&rsquo;ll see a similar structure to the NETCONF side. But this time, we&rsquo;ll utilize it quite a bit differently.</p>
<p>In the screenshot above, I&rsquo;ve already selected the <code>wlan-cfg-entries</code> entry. Next, we&rsquo;ll click the button to <strong>Generate API(s)</strong>:</p>
<p><img alt="restconf-wlan-all" loading="lazy" src="/content/images/2023/02/restconf-wlan-all.png#center"></p>
<p>We&rsquo;ll see auto-generated API documentation for all of the available calls. This list will contain every variation possible, so it can be quite long. Since we already went through the NETCONF method, we know what data we need to find / use which should make this easier.</p>
<p>To start with, I&rsquo;ll expand the <strong>GET</strong> for <code>/data/Cisco-IOS-XE-wireless-wlan-cfg:wlan-cfg-data/wlan-cfg-entries</code>. We&rsquo;ll have the ability to test the RESTCONF calls directly from this interface as well by clicking on <strong>Try it out</strong>:</p>
<p><img alt="restconf-example-query-wlan" loading="lazy" src="/content/images/2023/02/restconf-example-query-wlan.png#center"></p>
<p>In the screenshot above, I&rsquo;ve already submitted the test request &amp; gotten a response from the controller. While the screenshot is cut off a bit, we can see both of our wireless profile names.</p>
<p>If we wanted to see only the PSK, like we did with our previous example - we could scroll down quite a bit to find the <strong>GET</strong> request for <code>/data/Cisco-IOS-XE-wireless-wlan-cfg:wlan-cfg-data/wlan-cfg-entries/wlan-cfg-entry={wlan-cfg-entry-profile-name}/psk</code></p>
<p>In the screenshot below, I&rsquo;ve already input our <code>test-guest-network</code> profile name &amp; executed the API call:</p>
<p><img alt="restconf-example-query-psk" loading="lazy" src="/content/images/2023/02/restconf-example-query-psk.png#center"></p>
<p>Sure enough, we see the PSK that we had just configured via NETCONF a few minutes ago.</p>
<p>Now let&rsquo;s change it. We&rsquo;ll expand the <strong>PATCH</strong> API call for the same endpoint, and fill in the request body with our new PSK:</p>
<p><img alt="restconf-patch-psk" loading="lazy" src="/content/images/2023/02/restconf-patch-psk.png#center"></p>
<p>Executing that call, the wireless controller returned a status code of <strong>204</strong>.</p>
<p>So let&rsquo;s check again to see if that updated properly:</p>
<p><img alt="restconf-updated-psk" loading="lazy" src="/content/images/2023/02/restconf-updated-psk.png#center"></p>
<p>Now that we have all the appropriate REST calls, it should be much easier to start developing our automation.</p>
<hr>
<p>Okay, I think that&rsquo;s about all I wanted to cover in this post. I&rsquo;ve seen a lot of people struggle with NETCONF much like I used to, and I think the real key to getting familiar is just having the right tools &amp; spending enough time with it. YANG Suite has really helped me get more familiar with the various model structures and how to use them.</p>
<p>Hopefully this information helps others to get started. NETCONF &amp; YANG are certainly big hills to climb, but the right tools can make all the difference.</p>
]]></content:encoded>
    </item>
    <item>
      <title>[How To] Building a Simple Discord Bot using DiscordGo</title>
      <link>https://0x2142.com/how-to-discordgo-bot/</link>
      <pubDate>Wed, 19 Oct 2022 14:38:00 +0000</pubDate>
      <guid>https://0x2142.com/how-to-discordgo-bot/</guid>
      <description>A short tutorial for building a Discord chatbot with Golang.</description>
      <content:encoded><![CDATA[<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/G7A3nnMvfCk?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<p>Earlier this year I started trying out Golang as a new language to learn. Most of my prior experience is with Python, with a handful of other languages sprinkled in here &amp; there.</p>
<p>My experience with Go so far has been good &amp; I&rsquo;ve been having fun learning something new. It&rsquo;s been easy enough to pick up &amp; there have been quite a handful of things that I really appreciate about Go vs Python.</p>
<p>One of the best ways to learn something new is to find a good project to work on. Since I&rsquo;ve done quite a bit historically with Python &amp; building Webex chatbots - I figured one good way to learn Go would be trying to do something similar.</p>
<p>So in this blog post, I&rsquo;ll walk through how to build a simple Discord bot using a Golang module called <a href="https://github.com/bwmarrin/discordgo">DiscordGo</a>. We&rsquo;ll use the same project idea as my previous <a href="/how-to-building-a-basic-webex-chatbot/">Webex bot</a>, where the bot will leverage the OpenWeather APIs to retrieve weather information.</p>
<p>Since I&rsquo;ve been learning Go recently, I&rsquo;ll do my best to try to keep this simple - so that hopefully you can follow along if you&rsquo;re also new to Go 😊.</p>
<blockquote>
<p>Code repo for the examples below can be found <a href="https://github.com/0x2142/example-scripts/tree/master/simple-discord-chatbot">here</a></p></blockquote>
<hr>
<h1 id="setting-up-the-project">Setting up the Project</h1>
<p>So first we&rsquo;ll run through some quick setup. If you&rsquo;re familiar enough with Go already, you can likely skip this step &amp; just set up your project in the way you prefer.</p>
<p>I&rsquo;ve created a new project folder called <code>discord-weather-bot</code>. So first we&rsquo;ll initialize our Go module using the command <code>go mod init discord-weather-bot</code>.</p>
<p>Then, I&rsquo;ll create a <code>main.go</code> in this folder, as well as a sub-folder named <code>bot</code> which will contain a <code>bot.go</code> file.</p>
<p>Once we&rsquo;re done, our initial project layout should look like this:</p>
<p><img alt="project-layout" loading="lazy" src="/content/images/2022/08/project-layout.png#center"></p>
<p>We&rsquo;ll add on to this later, but this will work to start.</p>
<p>Next, we&rsquo;ll need to install the DiscordGo module, which we can do with one simple command: <code>go get github.com/bwmarrin/discordgo</code></p>
<h2 id="generating-api-keys">Generating API Keys</h2>
<p>Next we&rsquo;ll need to get our API keys for OpenWeather &amp; Discord.</p>
<p>OpenWeather is going to be the easier of the two - simply sign up for a free account <a href="https://openweathermap.org/price">here</a>. Then jump over to your account page &amp; create a new API key <a href="https://home.openweathermap.org/api_keys">here</a>. Free accounts are limited to only using their current weather API, which will be more than enough for our example.</p>
<p>Next, we&rsquo;ll look at the Discord side of things - which will be a few more hoops to jump though.</p>
<p>First we&rsquo;ll need to head over to the <a href="https://discord.com/developers/applications">Discord Applications</a> page &amp; create a new application/integration.</p>
<p><img alt="new-app" loading="lazy" src="/content/images/2022/08/new-app.png#center"></p>
<p>We&rsquo;ll be prompted to provide a name for our app (Note, this isn&rsquo;t the name of our bot - we&rsquo;ll get to that shortly):</p>
<p><img alt="new-app-name" loading="lazy" src="/content/images/2022/08/new-app-name.png#center"></p>
<p>Once we finish that, we&rsquo;ll be taken to a page to add additional information about our app - like tags, a description, an icon, or links to app resources. If you plan to publish your app publicly, you&rsquo;ll want to invest some time here. However, since we&rsquo;ll just be building the bot as a personal project, we don&rsquo;t need to do anything here.</p>
<p>Next, we&rsquo;ll want to jump down to the <strong>Bot</strong> section under <strong>Settings</strong>. This is where we can set up some basics for our bot &amp; get our bot API token.</p>
<p><img alt="side-menu" loading="lazy" src="/content/images/2022/08/side-menu.png#center"></p>
<p>We&rsquo;ll get a quick confirmation prompt, where we&rsquo;ll click on <strong>Yes, do it!</strong></p>
<p><img alt="add-bot-confirm" loading="lazy" src="/content/images/2022/08/add-bot-confirm.png#center"></p>
<p>Now we have our bot created! We can change the name of our bot here, to be separate from our app name if we want.</p>
<p><img alt="new-bot" loading="lazy" src="/content/images/2022/08/new-bot.png#center"></p>
<p>Scrolling down the page a bit - by default our bot is public, which means anyone could add it to their server. I&rsquo;ll switch this off for now, since this is intended to be a private example bot.</p>
<p><img alt="public-bot" loading="lazy" src="/content/images/2022/08/public-bot.png#center"></p>
<p>In order for our bot to receive &amp; process message content, we&rsquo;ll also need to enable the toggle for <strong>message content</strong>. Note that Discord says this is only allowed for servers under a certain size, after which they would need to be verified in order to use this intent. Since this bot will only be used as an example, we don&rsquo;t need to worry about that yet - but we will still need to enable the intent:</p>
<p><img alt="bot-intents" loading="lazy" src="/content/images/2022/09/bot-intents.png#center"></p>
<p>For now, this is all we need to set up on this page. Before we leave, we&rsquo;ll want to reset our bot access token - and save the token for later:</p>
<p><img alt="bot-token" loading="lazy" src="/content/images/2022/08/bot-token.png#center"></p>
<p>Last but not least, we&rsquo;ll need to add our Discord bot to a server. I have a server in Discord that I use for testing, and I&rsquo;ve created a private channel to experiment with this bot.</p>
<p>In order to do this, we&rsquo;ll need to create an OAuth authorization link that can be used to give the bot access.</p>
<p>Under our <strong>Settings</strong>, we&rsquo;ll click on <strong>OAuth2</strong> &gt; <strong>URL Generator</strong>.</p>
<p><img alt="oauth-url-gen-1" loading="lazy" src="/content/images/2022/08/oauth-url-gen-1.png#center"></p>
<p>In the screenshot above, I&rsquo;ve selected the <strong>Scope</strong> as <strong>bot</strong>. And checked off permissions for <strong>Read Messages</strong>, <strong>Send Messages</strong>, and <strong>Send Messages in Threads</strong>. For what we&rsquo;ll be doing in this example, that should be more than enough.</p>
<p>At the bottom of the screenshot, you can see that Discord will auto-generate a URL which we can now use to add the bot to our Discord Server.</p>
<p>If we visit that link while signed into Discord, we&rsquo;ll be prompted to add it to our server:</p>
<p><img alt="add-bot-to-server" loading="lazy" src="/content/images/2022/08/add-bot-to-server.png#center"></p>
<p>We&rsquo;ll also be prompted to confirm the permissions the bot will have:</p>
<p><img alt="confirm-bot-perms" loading="lazy" src="/content/images/2022/08/confirm-bot-perms.png#center"></p>
<p>Now our bot has been successfully added to our Discord server, but will show as offline since we haven&rsquo;t built it yet! Let&rsquo;s get started with that.</p>
<h1 id="building-the-bot---basic-interaction">Building the Bot - Basic Interaction</h1>
<p>So we&rsquo;ll start off by building out just enough of our bot code for it to send messages to our server.</p>
<h2 id="collecting-api-keys-via-environment-variables">Collecting API Keys via Environment Variables</h2>
<p>For the purpose of this project, I&rsquo;ll be storing both the Discord &amp; OpenWeather API keys as environment variables. So well keep our <code>main.go</code> simple &amp; just handle importing our API keys and kicking off the bot:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// snippet from: main.go</span>
</span></span><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">main</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl"> <span class="s">&#34;discord-weather-bot/bot&#34;</span>
</span></span><span class="line"><span class="cl"> <span class="s">&#34;log&#34;</span>
</span></span><span class="line"><span class="cl"> <span class="s">&#34;os&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// Load environment variables</span>
</span></span><span class="line"><span class="cl"> <span class="nx">botToken</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">os</span><span class="p">.</span><span class="nf">LookupEnv</span><span class="p">(</span><span class="s">&#34;BOT_TOKEN&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">log</span><span class="p">.</span><span class="nf">Fatal</span><span class="p">(</span><span class="s">&#34;Must set Discord token as env variable: BOT_TOKEN&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="nx">openWeatherToken</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">os</span><span class="p">.</span><span class="nf">LookupEnv</span><span class="p">(</span><span class="s">&#34;OPENWEATHER_TOKEN&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">log</span><span class="p">.</span><span class="nf">Fatal</span><span class="p">(</span><span class="s">&#34;Must set Open Weather token as env variable: OPENWEATHER_TOKEN&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> 
</span></span><span class="line"><span class="cl">    <span class="c1">// Start the bot</span>
</span></span><span class="line"><span class="cl"> <span class="nx">bot</span><span class="p">.</span><span class="nx">BotToken</span> <span class="p">=</span> <span class="nx">botToken</span>
</span></span><span class="line"><span class="cl"> <span class="nx">bot</span><span class="p">.</span><span class="nx">OpenWeatherToken</span> <span class="p">=</span> <span class="nx">openWeatherToken</span>
</span></span><span class="line"><span class="cl"> <span class="nx">bot</span><span class="p">.</span><span class="nf">Run</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>In the code above, we&rsquo;ll import <code>os</code> and <code>log</code> which we&rsquo;ll use to handle importing our environment variables &amp; generating errors if this fails. We&rsquo;ll also import our <code>bot</code> folder with <code>discord-weather-bot/bot</code>.</p>
<p>For our <code>main</code> function, we&rsquo;ll start by trying to find our environment variables. For each of the variables, we&rsquo;ll use a format of <code>&lt;varname&gt;, ok := os.LookupEnv(&quot;&lt;env name&gt;&quot;)</code>.</p>
<p>Using <code>os.LookupEnv</code>, we&rsquo;ll try and search for the environment variable by name - so in the first example looking for <code>BOT_TOKEN</code>. If the environment variable exists, it will be assigned to <code>botToken</code> - and <code>ok</code> will be <code>True</code>. Otherwise, we&rsquo;ll get no value for <code>botToken</code>, and <code>ok</code> will be <code>False</code>.</p>
<p>If either environment variable doesn&rsquo;t exist, we&rsquo;ll just print a log message reminding the user to set the variable. <code>log.Fatal</code> will also quit the program after displaying the log message.</p>
<p>Finally, assuming we have both of those API keys - we&rsquo;ll kick those keys over to the bot module &amp; start the bot with <code>bot.Run()</code>. Let&rsquo;s hop over &amp; build that piece now.</p>
<h2 id="connecting-to-discord">Connecting to Discord</h2>
<p>Next, we&rsquo;ll start out bot code with enough to receive the variables being passed by <code>main.go</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// snippet from: bot/bot.go</span>
</span></span><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">bot</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl"> <span class="s">&#34;fmt&#34;</span>
</span></span><span class="line"><span class="cl"> <span class="s">&#34;log&#34;</span>
</span></span><span class="line"><span class="cl"> <span class="s">&#34;os&#34;</span>
</span></span><span class="line"><span class="cl"> <span class="s">&#34;os/signal&#34;</span>
</span></span><span class="line"><span class="cl"> <span class="s">&#34;strings&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/bwmarrin/discordgo&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl"> <span class="nx">OpenWeatherToken</span> <span class="kt">string</span>
</span></span><span class="line"><span class="cl"> <span class="nx">BotToken</span>         <span class="kt">string</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">Run</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// Not implemented yet</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>In the snippet above, we&rsquo;ve defined two variables - <code>OpenWeatherToken</code> and <code>BotToken</code>. Note that both start with a capital letter, meaning that they have been <a href="https://go.dev/tour/basics/3">exported</a> &amp; available to code outside this module - which is how we can update these values via <code>main.go</code> by setting <code>bot.BotToken = botToken</code>.</p>
<p>Okay - Let&rsquo;s get started with connecting our bot to Discord. We&rsquo;ll update our <code>Run</code> function to the following:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// snippet from: bot/bot.go</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">Run</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// Create new Discord Session</span>
</span></span><span class="line"><span class="cl"> <span class="nx">discord</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">discordgo</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;Bot &#34;</span> <span class="o">+</span> <span class="nx">BotToken</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">log</span><span class="p">.</span><span class="nf">Fatal</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Add event handler</span>
</span></span><span class="line"><span class="cl"> <span class="nx">discord</span><span class="p">.</span><span class="nf">AddHandler</span><span class="p">(</span><span class="nx">newMessage</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Open session</span>
</span></span><span class="line"><span class="cl"> <span class="nx">discord</span><span class="p">.</span><span class="nf">Open</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"> <span class="k">defer</span> <span class="nx">discord</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Run until code is terminated</span>
</span></span><span class="line"><span class="cl"> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;Bot running...&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="nx">c</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="nx">os</span><span class="p">.</span><span class="nx">Signal</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="nx">signal</span><span class="p">.</span><span class="nf">Notify</span><span class="p">(</span><span class="nx">c</span><span class="p">,</span> <span class="nx">os</span><span class="p">.</span><span class="nx">Interrupt</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="o">&lt;-</span><span class="nx">c</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>To begin with, we create a new discord session with <code>discordgo.New()</code> &amp; passing our <code>BotToken</code>. Discord requires an HTTP <code>Authorization</code> header that contains <code>Bot &lt;token&gt;</code> or <code>Bearer &lt;token&gt;</code> for OAuth. The <code>discordgo.New()</code> function is just setting this header for us.</p>
<p>Next we&rsquo;ll register an event handler to our Discord client. In a moment, we&rsquo;ll create a new function called <code>newMessage</code> that will receive &amp; process any new messages that are created in our channel. More to come in just a moment!</p>
<p>Now we can open our websocket tunnel to Discord via <code>discord.Open()</code>. If you&rsquo;re not familiar with websockets, I wrote a little about them when I built a Webex bot in <a href="/how-to-building-a-basic-webex-chatbot/#webhook-vs-websocket-which-to-use">this post</a>. We&rsquo;ll also include a <code>defer discord.Close()</code>, which will ensure that we gracefully close the Discord session whenever the <code>Run()</code> function exits.</p>
<p>Lastly, we&rsquo;ll use Go&rsquo;s <code>os/signal</code> package to listen for any interrupt/process kill signals. This will allow our Discord bot to run in the background until we stop it. We do this by creating a new channel of type <code>os.Signal</code> where we will listen for termination signals. Then using <code>signal.Notify</code>, we tell the signal package where to send termination signals (our <code>c</code> channel) - and what signals we want to listen for (<code>os.Interrupt</code>). Then, using <code>&lt;-c</code>, our program will hold until something is received on the channel.</p>
<p>An operator like <code>&lt;-</code> is typically used for concurrency, where we have one function passing data to another via a channel. The <code>&lt;-</code> or <code>-&gt;</code> operator indicates which direction that data is being sent. So in this example, we&rsquo;re receiving data from channel <code>c</code> using the statement <code>&lt;-c</code>. Now the tricky thing here, is that we&rsquo;re not assigning that input to another variable (for example, <code>incomingSignal &lt;- c</code>), since we don&rsquo;t care what signal was sent - just that there was a signal received.</p>
<p>After we receive any termination signal, the code will continue to the end of the <code>Run()</code> function. This will then automatically execute our <code>defer discord.Close()</code> from earlier, which will tear down our Discord session.</p>
<h2 id="processing-incoming-messages">Processing Incoming Messages</h2>
<p>Okay, now that we have our Discord session establishment handled, let&rsquo;s build out our <code>newMessage()</code> function to handle receiving messages.</p>
<p>So for now, our code for this function will look like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// snippet from: bot/bot.go</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">newMessage</span><span class="p">(</span><span class="nx">discord</span> <span class="o">*</span><span class="nx">discordgo</span><span class="p">.</span><span class="nx">Session</span><span class="p">,</span> <span class="nx">message</span> <span class="o">*</span><span class="nx">discordgo</span><span class="p">.</span><span class="nx">MessageCreate</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Ignore bot messaage</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">message</span><span class="p">.</span><span class="nx">Author</span><span class="p">.</span><span class="nx">ID</span> <span class="o">==</span> <span class="nx">discord</span><span class="p">.</span><span class="nx">State</span><span class="p">.</span><span class="nx">User</span><span class="p">.</span><span class="nx">ID</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Respond to messages</span>
</span></span><span class="line"><span class="cl"> <span class="k">switch</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">Content</span><span class="p">,</span> <span class="s">&#34;weather&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">  <span class="nx">discord</span><span class="p">.</span><span class="nf">ChannelMessageSend</span><span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">ChannelID</span><span class="p">,</span> <span class="s">&#34;I can help with that!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">Content</span><span class="p">,</span> <span class="s">&#34;bot&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">  <span class="nx">discord</span><span class="p">.</span><span class="nf">ChannelMessageSend</span><span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">ChannelID</span><span class="p">,</span> <span class="s">&#34;Hi there!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Let&rsquo;s take a quick look at the code above.</p>
<p>The <code>newMessage()</code> function is our event handler that we are registering with our Discord client. It will need to receive two values - a <a href="https://go.dev/tour/moretypes/1">pointer</a> to our Discord session (<code>*discordgo.Session</code>), and the type of event we&rsquo;re listening for. In this case we&rsquo;re listening for new message creation events (<code>*discordgo.MessageCreate</code>). A full list of events that we could listen for are listed in the <a href="https://discord.com/developers/docs/topics/gateway#commands-and-events">Discord Gateway Documentation</a>.</p>
<p>First thing we&rsquo;ll do when processing the message is ensure that the bot ignores any message from itself. We wouldn&rsquo;t want our bot creating a loop by responding to it&rsquo;s own messages, which then generates a new message to respond to!</p>
<p>We accomplish this by checking the incoming message&rsquo;s Author ID (<code>message.Author.ID</code>) against our current Discord session&rsquo;s User ID (<code>discord.State.User.ID</code>). If these values match, then the incoming message was created by our bot code - so we&rsquo;ll just <code>return</code> and stop execution of this function.</p>
<p>If the incoming message is from another user, then we can go ahead and process it. There are a number of ways to do this depending on what you want to achieve. To keep things simple, I am using a quick <a href="https://go.dev/tour/flowcontrol/9">switch</a> statement to evaluate the message content.</p>
<p>Our bot then is listening for two keywords - &ldquo;weather&rdquo; and &ldquo;bot&rdquo;. If the incoming message content contains either of those two words, our bot will respond with a message by using <code>discord.ChannelMessageSend</code> - and passing the channel ID from the incoming message (<code>message.ChannelID</code>) and our message.</p>
<blockquote>
<p>A quick note: I used a switch/case here since it would be easier to add onto than a long list of if/else&hellip; but the example code here still isn&rsquo;t perfect. For instance, our second case is only looking for someone using the word <code>bot</code>. However this is a simple match, and would still catch on someone talking about a robot or a bottle - since both contain <code>bot</code> in them. If this was a production/public bot, we would want to clean that up a bit.</p></blockquote>
<h2 id="testing">Testing</h2>
<p>Okay, with that we can now give our bot a quick test.</p>
<p>Let&rsquo;s go ahead and start our bot with <code>go run main.go</code>. Don&rsquo;t forget to set your <code>BOT_TOKEN</code> &amp; <code>OPENWEATHER_TOKEN</code> environment variables first! (Or comment out the code for the <code>OPENWEATHER_TOKEN</code>, since we&rsquo;re not using it just yet)</p>
<p>Here&rsquo;s my test run:</p>
<p><img alt="basic-interaction" loading="lazy" src="/content/images/2022/08/basic-interaction.png#center"></p>
<p>As we can see, the bot responded just as we expected!</p>
<p>Another fun thing to point out, is that since we&rsquo;re using websockets for the connection (aka Discord Gateway) - our bot can also register presence events. So as long as our bot is connected, it will actually show as online:</p>
<p><img alt="bot-presence" loading="lazy" src="/content/images/2022/08/bot-presence.png#center"></p>
<h1 id="adding-basic-bot-commands">Adding Basic Bot Commands</h1>
<p>So far we&rsquo;ve gotten our bot connected to Discord, and responding to regular chat messages. Now we&rsquo;ll focus on actually adding functionality to our bot by leveraging the OpenWeather API.</p>
<p>For now, we&rsquo;ll implement this with very simple command matching. In a future blog post, I&rsquo;ll be showing how to accomplish this with Discord slash commands.</p>
<p>So first we&rsquo;ll modify our existing switch statement in the <code>newMessage()</code> function. We&rsquo;ll provide a little help text with our response to someone mentioning <code>weather</code> - and create a new command by catching messages with <code>!zip</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// snippet from: bot/bot.go</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Respond to messages</span>
</span></span><span class="line"><span class="cl"> <span class="k">switch</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">Content</span><span class="p">,</span> <span class="s">&#34;weather&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">  <span class="nx">discord</span><span class="p">.</span><span class="nf">ChannelMessageSend</span><span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">ChannelID</span><span class="p">,</span> <span class="s">&#34;I can help with that! Use &#39;!zip &lt;zip code&gt;&#39;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">Content</span><span class="p">,</span> <span class="s">&#34;bot&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">  <span class="nx">discord</span><span class="p">.</span><span class="nf">ChannelMessageSend</span><span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">ChannelID</span><span class="p">,</span> <span class="s">&#34;Hi there!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">Content</span><span class="p">,</span> <span class="s">&#34;!zip&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">  <span class="nx">currentWeather</span> <span class="o">:=</span> <span class="nf">getCurrentWeather</span><span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">Content</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="nx">discord</span><span class="p">.</span><span class="nf">ChannelMessageSendComplex</span><span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">ChannelID</span><span class="p">,</span> <span class="nx">currentWeather</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span></code></pre></div><p>Under the <code>!zip</code> command, we&rsquo;ll have a function called <code>getCurrentWeather()</code> which will return a Discord message. However, in this case we&rsquo;ll change things up a little - and use an embedded message so we can include some formatting. This also means that we&rsquo;ll change our response function to <code>discord.ChannelMessageSendComplex()</code>.</p>
<p>Where <code>discord.ChannelMessageSend()</code> needs the channel ID &amp; a simple string message, <code>discord.ChannelMessageSendComplex()</code> will require data of the type <code>discordgo.MessageSend</code> instead of a string. In our <code>getCurrentWeather()</code> function, we&rsquo;ll see how to assemble our response using this structure.</p>
<h2 id="querying-weather">Querying Weather</h2>
<p>So to start with, we&rsquo;ll need to query the OpenWeather API &amp; retrieve the current weather information for a given US ZIP code.</p>
<p>In our project, I&rsquo;ve created a new Go file called <code>command-weather.go</code> in the same <code>bot</code> subfolder which already contains our <code>bot.go</code> file. This new file will also be part of <code>package bot</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// snippet from: bot/command-weather.go</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">bot</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl"> <span class="s">&#34;encoding/json&#34;</span>
</span></span><span class="line"><span class="cl"> <span class="s">&#34;fmt&#34;</span>
</span></span><span class="line"><span class="cl"> <span class="s">&#34;io/ioutil&#34;</span>
</span></span><span class="line"><span class="cl"> <span class="s">&#34;net/http&#34;</span>
</span></span><span class="line"><span class="cl"> <span class="s">&#34;regexp&#34;</span>
</span></span><span class="line"><span class="cl"> <span class="s">&#34;strconv&#34;</span>
</span></span><span class="line"><span class="cl"> <span class="s">&#34;time&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/bwmarrin/discordgo&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></div><p>I&rsquo;ve included the list of imports above, in case you&rsquo;re following along.</p>
<p>To start with, I&rsquo;ll define two items that we&rsquo;ll need for our `getCurentWeather() function:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// snippet from: bot/command-weather.go</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">const</span> <span class="nx">URL</span> <span class="kt">string</span> <span class="p">=</span> <span class="s">&#34;https://api.openweathermap.org/data/2.5/weather?&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">WeatherData</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">Weather</span> <span class="p">[]</span><span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">Description</span> <span class="kt">string</span> <span class="s">`json:&#34;description&#34;`</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="s">`json:&#34;weather&#34;`</span>
</span></span><span class="line"><span class="cl"> <span class="nx">Main</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">Temp</span>     <span class="kt">float64</span> <span class="s">`json:&#34;temp&#34;`</span>
</span></span><span class="line"><span class="cl">  <span class="nx">Humidity</span> <span class="kt">int</span>     <span class="s">`json:&#34;humidity&#34;`</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="s">`json:&#34;main&#34;`</span>
</span></span><span class="line"><span class="cl"> <span class="nx">Wind</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">Speed</span> <span class="kt">float64</span> <span class="s">`json:&#34;speed&#34;`</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="s">`json:&#34;wind&#34;`</span>
</span></span><span class="line"><span class="cl"> <span class="nx">Name</span> <span class="kt">string</span> <span class="s">`json:&#34;name&#34;`</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>The first item is a constant for the base URL for OpenWeather&rsquo;s API. This should never need to change, which is why we use a constant here.</p>
<p>In addition, I&rsquo;ve defined a new <a href="https://go.dev/tour/moretypes/2">struct</a> called <code>WeatherData</code> that will be used to unpack our JSON response from OpenWeather&rsquo;s API. They list a current sample response payload on their <a href="https://openweathermap.org/current#current_JSON">documentation</a> page, which we can use to build our data structure. Since we won&rsquo;t be using all of the data provided in the response, I also don&rsquo;t need to define every value in our struct.</p>
<blockquote>
<p>Note: Need a quicker way to convert a sample JSON payload to a Golang struct? There are a handful of awesome websites that will do this conversion automatically for you. I like to use <a href="https://transform.tools/json-to-go">this one</a>, but that&rsquo;s only one of a handful of options!</p></blockquote>
<p>Next, we&rsquo;ll start building out our <code>getCurrentWeather()</code> function:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// snippet from: bot/command-weather.go</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">getCurrentWeather</span><span class="p">(</span><span class="nx">message</span> <span class="kt">string</span><span class="p">)</span> <span class="o">*</span><span class="nx">discordgo</span><span class="p">.</span><span class="nx">MessageSend</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// Match 5-digit US ZIP code</span>
</span></span><span class="line"><span class="cl"> <span class="nx">r</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">regexp</span><span class="p">.</span><span class="nf">Compile</span><span class="p">(</span><span class="s">`\d{5}`</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="nx">zip</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nf">FindString</span><span class="p">(</span><span class="nx">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// If ZIP not found, return an error</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">zip</span> <span class="o">==</span> <span class="s">&#34;&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="o">&amp;</span><span class="nx">discordgo</span><span class="p">.</span><span class="nx">MessageSend</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nx">Content</span><span class="p">:</span> <span class="s">&#34;Sorry that ZIP code doesn&#39;t look right&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span></code></pre></div><p>In our function definition, we&rsquo;ll be expecting a string input containing the user&rsquo;s message - for example: &ldquo;!zip 12345&rdquo; - and we&rsquo;ll be returning a <code>discordgo.MessageSend</code> object like I mentioned earlier.</p>
<p>Next, we&rsquo;ll do a quick (and very lazy) regular expression match against the incoming message &amp; attempt to pull out the ZIP code. This regex is only searching for a pattern of 5 digits. If the <code>r.FindString()</code> call finds a match, it will return the 5-digit ZIP code. If it doesn&rsquo;t match, it will return an empty string.</p>
<p>In case we don&rsquo;t match a ZIP code, we&rsquo;ll check to see if the <code>zip</code> variable is empty. If it is, we&rsquo;ll have Discord send a message back to the user saying it&rsquo;s invalid.</p>
<p>Here&rsquo;s where we&rsquo;ll see a simple example of using our <code>discordgo.MessageSend</code> object. This object is just a Go struct, so we can use it to store data in the format of <code>Key: Value</code>. In this case, we can still respond with a simple plain-text message - by using the <code>Content</code> key and supplying a string message. The full definition &amp; possible fields for this object are in the <a href="https://pkg.go.dev/github.com/bwmarrin/discordgo#MessageSend">discordgo docs</a>.</p>
<p>Next, we&rsquo;ll handle making our HTTP request to the OpenWeather API:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// snippet from: bot/command-weather.go</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="nx">weatherURL</span> <span class="o">:=</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;%szip=%s&amp;units=imperial&amp;appid=%s&#34;</span><span class="p">,</span> <span class="nx">URL</span><span class="p">,</span> <span class="nx">zip</span><span class="p">,</span> <span class="nx">OpenWeatherToken</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Create new HTTP client &amp; set timeout</span>
</span></span><span class="line"><span class="cl"> <span class="nx">client</span> <span class="o">:=</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Client</span><span class="p">{</span><span class="nx">Timeout</span><span class="p">:</span> <span class="mi">5</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Query OpenWeather API</span>
</span></span><span class="line"><span class="cl"> <span class="nx">response</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">client</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="nx">weatherURL</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="o">&amp;</span><span class="nx">discordgo</span><span class="p">.</span><span class="nx">MessageSend</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nx">Content</span><span class="p">:</span> <span class="s">&#34;Sorry, there was an error trying to get the weather&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span></code></pre></div><p>We&rsquo;ll start by generating the complete API URL. We&rsquo;ll use <code>fmt.Sprintf</code> to generate a formatted string &amp; inject variable values. So in this case, we&rsquo;re combining the base URL with the required parameters: zip, units, and appid. The <code>zip</code> value will contain the ZIP code we just collected from our user. <code>appid</code> will be our OpenWeather API key. Using <code>fmt.Sprintf</code>, we can inject variables using the <code>%s</code> placeholder for string values - then provide those values as inputs.</p>
<p>So for example, a full query URL for ZIP code 12345 may look similar to this: <code>https://api.openweathermap.org/data/2.5/weather?zip=12345&amp;units=imperial&amp;appid=ABCDEF12345</code></p>
<p>Then we create a new instance of an <code>http.Client</code> - just so we can modify the timeout value to 5 seconds. We then use this client to issue a HTTP GET request to the full <code>weatherURL</code>.</p>
<p>Assuming the call succeeds, we&rsquo;ll have our JSON response stored in the <code>response</code> variable. If not, we&rsquo;ll quickly check for an error &amp; send a Discord message back to the user.</p>
<h2 id="generating-a-discord-embed-message">Generating a Discord Embed Message</h2>
<p>Now we can parse our JSON response:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// snippet from: bot/command-weather.go</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Open HTTP response body</span>
</span></span><span class="line"><span class="cl"> <span class="nx">body</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">ioutil</span><span class="p">.</span><span class="nf">ReadAll</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">Body</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">defer</span> <span class="nx">response</span><span class="p">.</span><span class="nx">Body</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Convert JSON</span>
</span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nx">data</span> <span class="nx">WeatherData</span>
</span></span><span class="line"><span class="cl"> <span class="nx">json</span><span class="p">.</span><span class="nf">Unmarshal</span><span class="p">([]</span><span class="nb">byte</span><span class="p">(</span><span class="nx">body</span><span class="p">),</span> <span class="o">&amp;</span><span class="nx">data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Pull out desired weather info &amp; Convert to string if necessary</span>
</span></span><span class="line"><span class="cl"> <span class="nx">city</span> <span class="o">:=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">Name</span>
</span></span><span class="line"><span class="cl"> <span class="nx">conditions</span> <span class="o">:=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">Weather</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">Description</span>
</span></span><span class="line"><span class="cl"> <span class="nx">temperature</span> <span class="o">:=</span> <span class="nx">strconv</span><span class="p">.</span><span class="nf">FormatFloat</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">Main</span><span class="p">.</span><span class="nx">Temp</span><span class="p">,</span> <span class="sc">&#39;f&#39;</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">64</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="nx">humidity</span> <span class="o">:=</span> <span class="nx">strconv</span><span class="p">.</span><span class="nf">Itoa</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">Main</span><span class="p">.</span><span class="nx">Humidity</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="nx">wind</span> <span class="o">:=</span> <span class="nx">strconv</span><span class="p">.</span><span class="nf">FormatFloat</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">Wind</span><span class="p">.</span><span class="nx">Speed</span><span class="p">,</span> <span class="sc">&#39;f&#39;</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">64</span><span class="p">)</span>
</span></span></code></pre></div><p>We&rsquo;ll read in the HTTP response body using <code>ioutil.ReadAll()</code>. We&rsquo;ll need to close this object, so we&rsquo;ll immediately follow that read with a <code>defer response.Body.Close()</code> to make sure that happens.</p>
<p>Next we&rsquo;ll create a new variable called <code>data</code>, which will be of type <code>WeatherData</code> - our struct that we created earlier to store the JSON data. We can then convert that JSON response to our <code>data</code> object using <code>json.Unmarshal()</code> and passing our HTTP body &amp; the target variable to store the data in.</p>
<p>Lastly, we can start pulling out the information we need to use later. For clarity, I&rsquo;ve chosen to pull out each individual value here &amp; assign them to their own variable names. Since our Discord response can only be a string, we also handle type conversions here - from integer or float64 to a string.</p>
<p>Finally, we can generate our Discord <code>MessageSend</code> object:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// snippet from: bot/command-weather.go</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Build Discord embed response</span>
</span></span><span class="line"><span class="cl"> <span class="nx">embed</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">discordgo</span><span class="p">.</span><span class="nx">MessageSend</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">Embeds</span><span class="p">:</span> <span class="p">[]</span><span class="o">*</span><span class="nx">discordgo</span><span class="p">.</span><span class="nx">MessageEmbed</span><span class="p">{{</span>
</span></span><span class="line"><span class="cl">   <span class="nx">Type</span><span class="p">:</span>        <span class="nx">discordgo</span><span class="p">.</span><span class="nx">EmbedTypeRich</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nx">Title</span><span class="p">:</span>       <span class="s">&#34;Current Weather&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nx">Description</span><span class="p">:</span> <span class="s">&#34;Weather for &#34;</span> <span class="o">+</span> <span class="nx">city</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nx">Fields</span><span class="p">:</span> <span class="p">[]</span><span class="o">*</span><span class="nx">discordgo</span><span class="p">.</span><span class="nx">MessageEmbedField</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">     <span class="nx">Name</span><span class="p">:</span>   <span class="s">&#34;Conditions&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">     <span class="nx">Value</span><span class="p">:</span>  <span class="nx">conditions</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">     <span class="nx">Inline</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">     <span class="nx">Name</span><span class="p">:</span>   <span class="s">&#34;Temperature&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">     <span class="nx">Value</span><span class="p">:</span>  <span class="nx">temperature</span> <span class="o">+</span> <span class="s">&#34;°F&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">     <span class="nx">Inline</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">     <span class="nx">Name</span><span class="p">:</span>   <span class="s">&#34;Humidity&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">     <span class="nx">Value</span><span class="p">:</span>  <span class="nx">humidity</span> <span class="o">+</span> <span class="s">&#34;%&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">     <span class="nx">Inline</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">     <span class="nx">Name</span><span class="p">:</span>   <span class="s">&#34;Wind&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">     <span class="nx">Value</span><span class="p">:</span>  <span class="nx">wind</span> <span class="o">+</span> <span class="s">&#34; mph&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">     <span class="nx">Inline</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">   <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">embed</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>We&rsquo;ll create a new variable called <code>embed</code>, which will store a <code>discordgo.MessageSend</code> struct.</p>
<p>Earlier we used this to send a <code>Content:</code> value, but this time we&rsquo;ll be leveraging the <code>Embeds</code> key. <code>Embeds</code> is of the type <code>[]*discordgo.MessageEmbed</code>, which is again just a struct to store data. For reference, the format &amp; possible fields of the <code>MessageEmbed</code> object are listed <a href="https://pkg.go.dev/github.com/bwmarrin/discordgo#MessageEmbed">here</a></p>
<p>Within this <code>MessageEmbed</code> object, we can start filling in data about our message. So we fill in our message <code>Title</code> and <code>Description</code> - and tell Discord that this is going to be a richtext type of response.</p>
<p>We&rsquo;ll be displaying a box in discord that has a number of key/value pairs - which we&rsquo;ll use to show the measurement title &amp; value. To do this, we&rsquo;ll fill in the <code>Fields:</code> key - which takes in a list of structs of the type <code>[]*discordgo.MessageEmbedField</code>.</p>
<p>For each of the weather measurements we want to display, we&rsquo;ll provide a <code>Name:</code>, the <code>Value:</code>, and specify <code>Inline: true</code>. The <code>Inline</code> options tells Discord to try and list each key/value inline with eachother, rather than placing each pair on a new line.</p>
<p>Last, we&rsquo;ll return our <code>discordgo.MessageSend</code> object via <code>return embed</code>.</p>
<h2 id="testing-1">Testing</h2>
<p>Let&rsquo;s test this out. We can stop our currently running bot with <code>Ctrl+C</code>, then restart with <code>go run main.go</code>.</p>
<p>We should be able to ask our bot for the weather using our new <code>!zip</code> command:</p>
<p><img alt="basic-bot-command" loading="lazy" src="/content/images/2022/09/basic-bot-command.png#center"></p>
<p>Look at that! The bot quickly responds with the current weather, and formatted in an embedded message. There&rsquo;s a lot more we could do with embedded messages, but this is just an example to get started.</p>
<hr>
<p>Okay - I think that wraps up this blog post. I know it was a long one, so if you&rsquo;re still with me - Thank you! I hope this was helpful.</p>
<p>I am planning on a follow up post shortly, where I&rsquo;ll cover how to add Discord slash commands to our example bot. Check back soon or subscribe to the Blog or YouTube if you&rsquo;re interested!</p>
]]></content:encoded>
    </item>
    <item>
      <title>[How to] Webex Chatbot (Part 2): Digging in to Adaptive Cards</title>
      <link>https://0x2142.com/webex-chatbot-with-adaptivecards/</link>
      <pubDate>Sat, 12 Mar 2022 15:21:35 +0000</pubDate>
      <guid>https://0x2142.com/webex-chatbot-with-adaptivecards/</guid>
      <description>In this post, we&amp;rsquo;ll expand upon the previous Webex weather bot &amp;amp; add in AdaptiveCards to improve user interaction.</description>
      <content:encoded><![CDATA[<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/q4LaBvMePTw?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<p>A few months ago, I had written a blog post on building a simple chatbot with Webex. You can find that <a href="/how-to-building-a-basic-webex-chatbot/">here</a>.</p>
<p>At the time I was already thinking about some follow-up content, where I could walk through how to use AdaptiveCards to make the bot a little fancier - but I decided to wait a bit &amp; see how the first post performed first.</p>
<p>Well, now it&rsquo;s a few months later &amp; I suppose it&rsquo;s about time I write this 😄.</p>
<p>So in this post - We&rsquo;ll build on the <a href="https://github.com/0x2142/example-scripts/tree/master/simple-webex-chatbot">code</a> I used in the last post. We&rsquo;ll  show how to add cards to both display information, as well as take user input.</p>
<hr>
<h2 id="what-are-adaptivecards">What are AdaptiveCards?</h2>
<p><a href="https://adaptivecards.io/">AdaptiveCards</a> are actually not a Webex/Cisco-specific thing! They were actually created by Microsoft with the intention of being a platform-agnostic way of displaying information. So in theory, a card schema written for a Webex bot could be taken &amp; re-used on another messaging platform that implements AdaptiveCards - without any/much rework.</p>
<p>Hopefully this should mean a more consistent user experience for a bot that is supported across multiple platforms. The cards themselves can also enhance the experience by making your bot more dynamic &amp; interactive. Maybe a developer or network engineer is okay with issuing specific syntax-based commands to a bit - but what about someone who is less computer savvy? Using cards makes it easy to provide guided input with textboxes, buttons, and toggles.</p>
<p>AdaptiveCards are built on a standard JSON structure with a pre-defined <a href="https://adaptivecards.io/explorer/">schema</a> of card properties. Let&rsquo;s take a quick look at a simple example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;AdaptiveCard&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;$schema&#34;</span><span class="p">:</span> <span class="s2">&#34;http://adaptivecards.io/schemas/adaptive-card.json&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;version&#34;</span><span class="p">:</span> <span class="s2">&#34;1.2&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;body&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;TextBlock&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;text&#34;</span><span class="p">:</span> <span class="s2">&#34;Hello there! What&#39;s your name?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;wrap&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;Input.Text&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;placeholder&#34;</span><span class="p">:</span> <span class="s2">&#34;Your name here&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;user_name&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;ActionSet&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;actions&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">                <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;Action.Submit&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&#34;title&#34;</span><span class="p">:</span> <span class="s2">&#34;Submit&#34;</span>
</span></span><span class="line"><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>So in the JSON payload above, we&rsquo;re creating a card that asks for user input. We have a block of text that asks the user &ldquo;What&rsquo;s your name?&rdquo;, then we provide a text input field, and finally a submit button. Looks pretty straightforward, yeah?</p>
<p>We&rsquo;ll get into the specifics around how to create that card JSON in a bit, but you can always use Microsoft&rsquo;s <a href="https://adaptivecards.io/designer">AdaptiveCard Designer</a> to play around with card elements/structure.</p>
<p>So what would that card look like when rendered in Webex? Let&rsquo;s take a look:</p>
<p><img alt="sample-card" loading="lazy" src="/content/images/2022/02/sample-card.png#center"></p>
<p>That card seems a lot more user-friendly than trying to explain to your users that they need to remember some sytax like <code>/botcommand username set &lt;firstname&gt; &lt;lastname&gt;</code>. Instead, they can just enter the info on the card &amp; click submit.</p>
<h2 id="sending-a-static-default-card-from-a-json-file">Sending a Static, Default Card from a JSON File</h2>
<p>So first we&rsquo;ll start off with the easier scenario. We&rsquo;ll have a pre-defined JSON card built, and use the bot to load &amp; send this card response.</p>
<p>One note before we get started - I&rsquo;ll be building on my simple weather chatbot code that I mentioned earlier. If you&rsquo;re following along, you can pull down that code from <a href="https://github.com/0x2142/example-scripts/tree/master/simple-webex-chatbot">here</a>.</p>
<p>Okay, to start off - we&rsquo;ll be creating a card that allows a user to enter their desired zip code. Instead of having to remember the specific command syntax, a user will instead just issue the <code>weather</code> command &amp; our bot will prompt for information via a card.</p>
<p>I&rsquo;ve used the Microsoft Adaptive Card Designer to build out the basic card structure, which I&rsquo;ve kept simple for now. Here&rsquo;s what that looks like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;AdaptiveCard&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;$schema&#34;</span><span class="p">:</span> <span class="s2">&#34;http://adaptivecards.io/schemas/adaptive-card.json&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;version&#34;</span><span class="p">:</span> <span class="s2">&#34;1.2&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;body&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;TextBlock&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;text&#34;</span><span class="p">:</span> <span class="s2">&#34;Get Current Weather&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;wrap&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;size&#34;</span><span class="p">:</span> <span class="s2">&#34;Medium&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;weight&#34;</span><span class="p">:</span> <span class="s2">&#34;Bolder&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;TextBlock&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;text&#34;</span><span class="p">:</span> <span class="s2">&#34;Enter a Zip code:&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;wrap&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;Input.Number&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;placeholder&#34;</span><span class="p">:</span> <span class="s2">&#34;00000&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;zip_code&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;min&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;max&#34;</span><span class="p">:</span> <span class="mi">99950</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;ActionSet&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;actions&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">                <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;Action.Submit&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&#34;title&#34;</span><span class="p">:</span> <span class="s2">&#34;Get Weather&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&#34;data&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                        <span class="nt">&#34;callback_keyword&#34;</span><span class="p">:</span> <span class="s2">&#34;weather&#34;</span>
</span></span><span class="line"><span class="cl">                    <span class="p">}</span>
</span></span><span class="line"><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>This card looks pretty similar to the example I used earlier. We have an initial text block that displays the title &ldquo;Get Current Weather&rdquo;, and we&rsquo;ve added attributes for bold text &amp; a medium size.</p>
<p>Next, we have a normal text block prompting the user to enter their zip code. This is followed by a number input box for that data entry.</p>
<p>The input box has a &ldquo;min&rdquo; &amp; &ldquo;max&rdquo; value for the zip code from 1 to 99950 - which is the full range of US zip codes. We also need to assign any input a unique &ldquo;id&rdquo; value - which we&rsquo;ll see later is used to pull out the user input from the response.</p>
<p>Last, we have our actions - which similar to earlier. This will present as a simple submit button with the text &ldquo;Get Weather&rdquo;. The important note here is adding a <code>data</code> dictionary with a <code>callback_keyword</code> sub-element. We set the <code>callback_keyword</code> to the command we want this submission to be sent to. In our case, we want the input to be sent back to the weather command.</p>
<p>Now to implement. Back in our bot, we&rsquo;ll edit the <code>weather.py</code> file to load the card &amp; assign as the default command response. Let&rsquo;s take a look:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1">### Script snippet</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s2">&#34;./input-card.json&#34;</span><span class="p">,</span> <span class="s2">&#34;r&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">card</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">INPUT_CARD</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">load</span><span class="p">(</span><span class="n">card</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">WeatherByZIP</span><span class="p">(</span><span class="n">Command</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">command_keyword</span><span class="o">=</span><span class="s2">&#34;weather&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">help_message</span><span class="o">=</span><span class="s2">&#34;Get current weather conditions by ZIP code.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">card</span><span class="o">=</span><span class="n">INPUT_CARD</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl"><span class="c1">### Script snippet</span>
</span></span></code></pre></div><p>In the above snippet, I&rsquo;ve extracted just the pieces we&rsquo;ll be editing. For full examples, please see the final example repo <a href="https://github.com/0x2142/example-scripts/tree/master/simple-webex-chatbot-with-cards">here</a>.</p>
<p>I&rsquo;ve saved that JSON payload from earlier as <code>input-card.json</code>. Then we&rsquo;ll open that card file &amp; assign that JSON payload to a variable called <code>INPUT_CARD</code>.</p>
<p>Since we&rsquo;ll expect this card to be the primary method of collecting a zip code - we&rsquo;ll make this card the default response from our bot when the command is triggered. In my original code, we initialized the WeatherByZIP class with <code>card=None</code> - which instead passed our input directly to the <code>execute()</code> function. In this case, we&rsquo;ll set that to <code>card=INPUT_CARD</code> to send our form.</p>
<p>With all that done, let&rsquo;s test it out:</p>
<p><img alt="bot-input" loading="lazy" src="/content/images/2022/03/bot-input.png#center"></p>
<h2 id="collecting--processing-card-submissions">Collecting &amp; Processing Card Submissions</h2>
<p>Now that we have our input card, let&rsquo;s move on to processing the response.</p>
<p>In the original example, we expected the user to enter a command like <code>weather 12345</code> - and it was easy enough to just strip the 5-digit zip code from the response.</p>
<p>Since a card could have many input boxes, we instead receive that data back in a structured JSON format.</p>
<p>If we check out the logging in the webex_bot console, we can see the payload that gets returned to us when the card is submitted:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="mi">2022-03-03</span> <span class="mi">14</span><span class="err">:</span><span class="mi">59</span><span class="err">:</span><span class="mi">26</span>  <span class="p">[</span><span class="err">INFO</span><span class="p">]</span>  <span class="p">[</span><span class="err">webex_websocket_client.root._process_incoming_websocket_message</span><span class="p">]</span><span class="err">:</span><span class="mi">62</span> <span class="err">attachment_actions</span> <span class="err">from</span> <span class="err">message_base_</span><span class="mi">64</span><span class="err">_id:</span> <span class="err">Webex</span> <span class="err">Teams</span> <span class="err">AttachmentAction:</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;--removed--&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;submit&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;messageId&#34;</span><span class="p">:</span> <span class="s2">&#34;--removed--&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;inputs&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;callback_keyword&#34;</span><span class="p">:</span> <span class="s2">&#34;weather&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;zip_code&#34;</span><span class="p">:</span> <span class="s2">&#34;12345&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;personId&#34;</span><span class="p">:</span> <span class="s2">&#34;--removed--&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;roomId&#34;</span><span class="p">:</span> <span class="s2">&#34;--removed--&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;created&#34;</span><span class="p">:</span> <span class="s2">&#34;2022-03-03T19:59:24.820Z&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Using this output, we can see exactly what data is passed back to our bot. Under imports, we do see the correct <code>callback_keyword</code> that we defined in our card JSON. But we also see the user input - which is <code>12345</code>. This is listed under the key <code>zip_code</code> which is the <code>id</code> we assigned to our input field earlier.</p>
<p>Now we can easily dig through this response to pull out the data we need. This JSON payload is delivered to the <code>execute</code> function under the <code>attachment_actions</code> parameter.</p>
<p>So we&rsquo;ll make some minor modifications to our original code to pull that value out.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">,</span> <span class="n">attachment_actions</span><span class="p">,</span> <span class="n">activity</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">zip_code</span> <span class="o">=</span> <span class="n">attachment_actions</span><span class="o">.</span><span class="n">inputs</span><span class="p">[</span><span class="s1">&#39;zip_code&#39;</span><span class="p">]</span>
</span></span></code></pre></div><p>As a reminder, <code>zip_code</code> is the variable we&rsquo;re using inside the <code>execute()</code> function to store the requested zip code - which is then passed to the OpenWeather API.</p>
<p>Previously, we had just assigned the value of <code>message</code> to <code>zip_code</code>, which was the incoming text message from the user.</p>
<p>Instead, this time we&rsquo;ll fetch the zip code from the incoming card response, using <code>attachment_actions.inputs</code> &amp; pulling out the <code>zip_code</code> key.</p>
<p>With that simple change completed, we should be able to ask the bot for the weather, input our zip code, then receive the same text-based output as before.</p>
<p>Here&rsquo;s what that looks like now:</p>
<p><img alt="bot-input-standard-response-1" loading="lazy" src="/content/images/2022/03/bot-input-standard-response-1.png#center"></p>
<p>As we would expect, upon hitting <code>Get Weather</code> - the bot returns the text-based weather report.</p>
<p>Next, we&rsquo;ll take a look at dynamically building a card to display weather info.</p>
<h2 id="dynamically-building-cards">Dynamically Building Cards</h2>
<p>Now we get to the real fun part. We&rsquo;ll need to dynamically build an AdaptiveCard response, depending on what values we want to present to the user.</p>
<p>I will be using a Python module called <a href="https://github.com/ku222/AdaptiveCardBuilder">adaptivecardbuilder</a>. There are a handful of modules available that can help dynamically build cards, but this was the one I found to be most intuitive for me. While I will walk through an example here, I would also recommend reviewing their docs for additional context/examples.</p>
<p>You could also generate a JSON payload manually, or build one using the card designer &amp; try to edit the JSON dynamically to change card values - but I won&rsquo;t be doing that here.</p>
<p>So we&rsquo;ll start off by installing the adaptive card module:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install adaptivecardbuilder
</span></span></code></pre></div><p>Before we build out the full card response, let&rsquo;s take a quick look at how we can use this module.</p>
<p>In our <code>execute()</code> function, we&rsquo;ll create a simple card that shows what zip code was entered.</p>
<p>First, we&rsquo;ll need to add two new imports:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">webex_bot.models.response</span> <span class="kn">import</span> <span class="n">Response</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">adaptivecardbuilder</span> <span class="kn">import</span> <span class="o">*</span>
</span></span></code></pre></div><p>We&rsquo;ll import the <code>Response</code> module from <code>webex_bot</code>, which will be required to send any attachments / custom response to the client. We&rsquo;ll also import our <code>adaptivecardbuilder</code> module.</p>
<p>Next, we&rsquo;ll modify the <code>execute()</code> function to look like the following:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">,</span> <span class="n">attachment_actions</span><span class="p">,</span> <span class="n">activity</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">zip_code</span> <span class="o">=</span> <span class="n">attachment_actions</span><span class="o">.</span><span class="n">inputs</span><span class="p">[</span><span class="s1">&#39;zip_code&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="n">card</span> <span class="o">=</span> <span class="n">AdaptiveCard</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">card</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">TextBlock</span><span class="p">(</span><span class="n">text</span><span class="o">=</span><span class="sa">f</span><span class="s2">&#34;Weather for </span><span class="si">{</span><span class="n">zip_code</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="s2">&#34;Medium&#34;</span><span class="p">,</span> <span class="n">weight</span><span class="o">=</span><span class="s2">&#34;Bolder&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="n">card_data</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">asyncio</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">card</span><span class="o">.</span><span class="n">to_json</span><span class="p">()))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">card_payload</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;contentType&#34;</span><span class="p">:</span> <span class="s2">&#34;application/vnd.microsoft.card.adaptive&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;content&#34;</span><span class="p">:</span> <span class="n">card_data</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">response</span> <span class="o">=</span> <span class="n">Response</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">response</span><span class="o">.</span><span class="n">text</span> <span class="o">=</span> <span class="s2">&#34;Test Card&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">response</span><span class="o">.</span><span class="n">attachments</span> <span class="o">=</span> <span class="n">card_payload</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">response</span>
</span></span></code></pre></div><p>First we create a new <code>AdaptiveCard()</code> instance. With this new object we can add different card elements.</p>
<p>In this case, we add a single card element - a <code>TextBlock</code>. This has multiple attributes, including the actual text content, and we&rsquo;re specifying that we want a medium font size &amp; bold text.</p>
<p>Next we serialize the card payload to JSON using <code>asyncio.run</code>.</p>
<p>In order to prepare the card object to be attached to our response, we will need to set the appropriate headers. In this case we need to wrap the JSON structure with the <code>contentType</code> header. We could use the <code>response.attachments</code> object to send any number of different file types to the user, but in this case we&rsquo;ll set that value to <code>application/vnd.microsoft.card.adaptive</code>.</p>
<p>Then we can assemble the response to the client. We&rsquo;ll create a new instance of the <code>Response()</code> object, where we&rsquo;ll define some necessary attributes.</p>
<p>We&rsquo;ll set our <code>response.text</code> first. This will be the text message sent to the client. If the client supports adaptive cards, then this message won&rsquo;t be shown to the user - however, it may still appear in pop notifications. If the client does not support adaptive cards, only this message will be shown to the user.</p>
<p>Next we attach our complete card payload using <code>response.attachments</code>.</p>
<p>Last but not least, we can <code>return</code> that <code>response</code> object - which is then sent back to the user.</p>
<p><img alt="bot-response-simple-1" loading="lazy" src="/content/images/2022/03/bot-response-simple-1.png#center"></p>
<p>In this case, we can see the bot responds back to our request with a simple card that displays the zip code.</p>
<p>Let&rsquo;s move onto building out the remainder of the card. For the purposes of this example, I&rsquo;ll also be pulling out a few additional items from the OpenWeather API - including sunrise, sunset, pressure, and high/low temperatures.</p>
<p>I&rsquo;ll go ahead and show the full card build here:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">card</span> <span class="o">=</span> <span class="n">AdaptiveCard</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">card</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">TextBlock</span><span class="p">(</span><span class="n">text</span><span class="o">=</span><span class="sa">f</span><span class="s2">&#34;Weather for </span><span class="si">{</span><span class="n">city</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="s2">&#34;Medium&#34;</span><span class="p">,</span> <span class="n">weight</span><span class="o">=</span><span class="s2">&#34;Bolder&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="n">card</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">ColumnSet</span><span class="p">())</span>
</span></span><span class="line"><span class="cl"><span class="n">card</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">Column</span><span class="p">(</span><span class="n">width</span><span class="o">=</span><span class="s2">&#34;stretch&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="n">card</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">FactSet</span><span class="p">())</span>
</span></span><span class="line"><span class="cl"><span class="n">card</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">Fact</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s2">&#34;Temp&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">temperature</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="n">card</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">Fact</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s2">&#34;Conditions&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">conditions</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="n">card</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">Fact</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s2">&#34;Wind&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">wind</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="n">card</span><span class="o">.</span><span class="n">up_one_level</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">card</span><span class="o">.</span><span class="n">up_one_level</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">card</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">Column</span><span class="p">(</span><span class="n">width</span><span class="o">=</span><span class="s2">&#34;stretch&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="n">card</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">FactSet</span><span class="p">())</span>
</span></span><span class="line"><span class="cl"><span class="n">card</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">Fact</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s2">&#34;High&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">high_temp</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="n">card</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">Fact</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s2">&#34;Low&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">low_temp</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="n">card</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">Fact</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s2">&#34;Humidity&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">humidity</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="n">card_data</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">asyncio</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">card</span><span class="o">.</span><span class="n">to_json</span><span class="p">()))</span>
</span></span></code></pre></div><p>If this looks a little messy, it is. There is a cleaner way to do this, which I&rsquo;ll show in a moment.</p>
<p>The first thing to know is that the card JSON structure is hierarchical. So individual card items need to be added under the correct hierarchy - for example, you create a <code>ColumnSet</code> first, then add a <code>Column</code> underneath, then the items within that Column last.</p>
<p>See the below visualization of this card:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Top Level
</span></span><span class="line"><span class="cl"> ↳ Text Block
</span></span><span class="line"><span class="cl"> ↳ ColumnSet
</span></span><span class="line"><span class="cl">   ↳ Column
</span></span><span class="line"><span class="cl">     ↳ Fact Set
</span></span><span class="line"><span class="cl">       ↳ Fact
</span></span><span class="line"><span class="cl">       ↳ Fact
</span></span><span class="line"><span class="cl">       ↳ Fact
</span></span><span class="line"><span class="cl">    ↳ Column
</span></span><span class="line"><span class="cl">     ↳ Fact Set
</span></span><span class="line"><span class="cl">       ↳ Fact
</span></span><span class="line"><span class="cl">       ↳ Fact
</span></span><span class="line"><span class="cl">       ↳ Fact
</span></span></code></pre></div><p>So on our card, we first placed the title <code>TextBlock</code>. Underneath that we created a <code>ColumnSet</code>, which will contain two <code>Columns</code>.</p>
<p>Each one of those <code>Columns</code> contains a <code>FactSet</code>, which is a key/value pair listing of information. And of course, we add each <code>Fact</code> pair underneath the <code>FactSet</code>.</p>
<p>You&rsquo;ll notice after building one <code>Column</code> and <code>FactSet</code>, we use <code>card.up_one_level()</code> twice - to return back to the <code>ColumnSet</code> level, where we then add a second <code>Column</code>.</p>
<p>This can certainly get a little confusing at first, but I&rsquo;ll show a more intuitive method in a moment.</p>
<p>Let&rsquo;s take a peak at what this looks like now:</p>
<p><img alt="bot-response-full-1" loading="lazy" src="/content/images/2022/03/bot-response-full-1.png#center"></p>
<h2 id="sub-cards--better-card-building">Sub-cards &amp; Better Card Building</h2>
<p>In this section we&rsquo;ll do two quick things. First we&rsquo;ll take a look at a simpler, more visual way of building the cards. Then we&rsquo;ll also expand our example with sub-cards.</p>
<p>The <code>adaptivecardsbuilder</code> module supports a second way of building cards, which I personally find to be much easier to use.</p>
<p>In this method, we just create a single <code>card.add()</code> object - and feed it a list of all the items on the card.</p>
<p>For me, this makes it easier because you can add indentation to each list item - which helps keep track of where you are at in the hierarchical structure.</p>
<p>To navigate within the structure, we use <code>&quot;&lt;&quot;</code> to go up one level, or <code>&quot;^&quot;</code> to return to the top level.</p>
<p>I&rsquo;ve re-written the earlier example with this new structure below:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">card</span> <span class="o">=</span> <span class="n">AdaptiveCard</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">card</span><span class="o">.</span><span class="n">add</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="n">TextBlock</span><span class="p">(</span><span class="n">text</span><span class="o">=</span><span class="sa">f</span><span class="s2">&#34;Weather for </span><span class="si">{</span><span class="n">city</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="s2">&#34;Medium&#34;</span><span class="p">,</span> <span class="n">weight</span><span class="o">=</span><span class="s2">&#34;Bolder&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">ColumnSet</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">            <span class="n">Column</span><span class="p">(</span><span class="n">width</span><span class="o">=</span><span class="s2">&#34;stretch&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="n">FactSet</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">                    <span class="n">Fact</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s2">&#34;Temp&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">temperature</span><span class="si">}</span><span class="s2">F&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                    <span class="n">Fact</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s2">&#34;Conditions&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">conditions</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                    <span class="n">Fact</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s2">&#34;Wind&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">wind</span><span class="si">}</span><span class="s2"> mph&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                    <span class="s2">&#34;&lt;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;&lt;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">Column</span><span class="p">(</span><span class="n">width</span><span class="o">=</span><span class="s2">&#34;stretch&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="n">FactSet</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">                <span class="n">Fact</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s2">&#34;High&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">high_temp</span><span class="si">}</span><span class="s2">F&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="n">Fact</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s2">&#34;Low&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">low_temp</span><span class="si">}</span><span class="s2">F&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;^&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">ActionSet</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">            <span class="n">ActionShowCard</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s2">&#34;More Details&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="n">ColumnSet</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">                    <span class="n">Column</span><span class="p">(</span><span class="n">width</span><span class="o">=</span><span class="s2">&#34;stretch&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                        <span class="n">FactSet</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">                            <span class="n">Fact</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s2">&#34;Humidity&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">humidity</span><span class="si">}</span><span class="s2">%&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                            <span class="n">Fact</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s2">&#34;Pressure&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">pressure</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                            <span class="s2">&#34;&lt;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="s2">&#34;&lt;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="n">Column</span><span class="p">(</span><span class="n">width</span><span class="o">=</span><span class="s2">&#34;stretch&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                        <span class="n">FactSet</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">                            <span class="n">Fact</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s2">&#34;Sunrise&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">sunrise</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                            <span class="n">Fact</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s2">&#34;Sunset&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">sunset</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">card_data</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">asyncio</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">card</span><span class="o">.</span><span class="n">to_json</span><span class="p">()))</span>
</span></span></code></pre></div><p>Again, you&rsquo;re welcome to use whichever method suits you - but I find this method to be a bit cleaner &amp; easier to read/troubleshoot.</p>
<p>You can also see that I&rsquo;ve added a new <code>ActionSet</code>, with a single action: <code>ActionShowCard</code>. This adds a nice little button to our card where we can optionally display additional information.</p>
<p>Here&rsquo;s what that looks like now:</p>
<p><img alt="bot-response-full-pt2" loading="lazy" src="/content/images/2022/03/bot-response-full-pt2.png#center"></p>
<p>And if we expand that sub-card:</p>
<p><img alt="bot-response-full-pt2-expanded-2" loading="lazy" src="/content/images/2022/03/bot-response-full-pt2-expanded-2.png#center"></p>
<p>Sub-cards can be an easy way to hide excess information to keep the response clean &amp; focus on the important information.</p>
<hr>
<p>Well that&rsquo;s it for this blog post. There&rsquo;s a lot more you can do with cards, including adding images, video, tables, dropdown menus, etc. But I just wanted to show a few examples of building cards. They can be a simple way to make chatbots a little less boring!</p>
<p>Enjoy!</p>
]]></content:encoded>
    </item>
    <item>
      <title>[How To] Building a Simple Webex Chatbot with Python Websockets &amp; OpenWeather APIs</title>
      <link>https://0x2142.com/how-to-building-a-basic-webex-chatbot/</link>
      <pubDate>Thu, 18 Nov 2021 14:59:53 +0000</pubDate>
      <guid>https://0x2142.com/how-to-building-a-basic-webex-chatbot/</guid>
      <description>Let&amp;rsquo;s build a simple chatbot using Cisco Webex that can query the weather!</description>
      <content:encoded><![CDATA[<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/yZQjoe5XUYE?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<p>I&rsquo;ve been spending some of my time lately working on a new project that involves an interactive chatbot - where you can send commands to the bot &amp; ask for it to take certain actions or return information.</p>
<p>I didn&rsquo;t previously have a ton of experience building something like this. Mostly what I&rsquo;ve done before is just using Webex or Discord APIs to send alerts or messages to a room.</p>
<p>As with anything new - I&rsquo;ve stumbled my way through a couple of things, but have been enjoying the challenge of trying something different. Building out some of the more interactive parts of the bot hasn&rsquo;t been quite as difficult as I originally imagined, and some of the newer modules &amp; SDKs have made the process much simpler.</p>
<p>With all that being said - I wanted to throw together a quick guide on how to get started building a basic chatbot, in case anyone else is interested but worried that it may be too complicated.</p>
<blockquote>
<p>Repo <a href="https://github.com/0x2142/example-scripts/tree/master/simple-webex-chatbot">here</a> for all the sample code used below</p></blockquote>
<hr>
<h1 id="step-1-getting-api-keys">Step 1: Getting API Keys</h1>
<p>So the first thing we&rsquo;ll worry about is getting signed up for a Webex developer account &amp; generating API keys.</p>
<p>So head on over to the <a href="https://developer.webex.com/">Webex Developer</a> site &amp; log in (or sign up for a new account).</p>
<p>Once logged in, we&rsquo;ll go to the upper-right corner of the screen and click on our user icon - and select &ldquo;My Webex Apps&rdquo; from the drop down:</p>
<p><img alt="01&mdash;login" loading="lazy" src="/content/images/2021/09/01---login.png#center"></p>
<p>Then we&rsquo;ll be asked what type of app we&rsquo;re creating. In this case, we&rsquo;ll select <strong>bot</strong>:</p>
<p><img alt="02&mdash;appselection" loading="lazy" src="/content/images/2021/09/02---appselection.png#center"></p>
<p>Next we&rsquo;ll need to provide some details about our bot. This includes a display name, username, image, and some details. Once the bot is completed, we have the ability to publish it via the Webex App Hub where anyone could interact with it. If you&rsquo;re looking to do this, make sure you give a good description of your bot!</p>
<p><img alt="03&mdash;botdetails" loading="lazy" src="/content/images/2021/09/03---botdetails.png#center"></p>
<p>After we finish all that, we&rsquo;ll finally get our API key!</p>
<p><img alt="04&mdash;apikey" loading="lazy" src="/content/images/2021/09/04---apikey.png#center"></p>
<h1 id="webhook-vs-websocket-which-to-use">Webhook vs Websocket: Which to use?</h1>
<p>Next, I wanted to cover the two primary methods of sending &amp; receiving messages from the Webex APIs: Webhooks and websockets.</p>
<h3 id="webhooks">Webhooks</h3>
<p>Likely webhooks are the method most people are familiar with. Using this method, we need to run out bot code on a publicly reachable URL. When we run our bot, one of our first actions will be sending a request to the Webex APIs with our bot URL. This way, every time Webex receives a message from a user that wants to interact with our bot, the Webex cloud knows where to send that chat message.</p>
<p>Now, the potential downside of using webhooks is that we need to 1) manage a web server and 2) expose the bot publicly to the internet. Both of these could be overcome with easy workarounds, if needed. For example, we could use something like <a href="https://fastapi.tiangolo.com/">FastAPI</a> to quickly build our web listener &amp; handle incoming POST requests. We could also use a tunneling method (like <a href="https://ngrok.com/">ngrok</a>) to avoid having to directly open ports to our webserver.</p>
<h3 id="websockets">Websockets</h3>
<p>On the other hand, we could use websockets to accomplish the same function. With a websocket, we&rsquo;re instead opening a direct, persistent TCP connection to the Webex APIs. Through this persistent connection, we can send &amp; receive messages very quickly and easily.</p>
<p>This method offers a few advantages over webhooks. Primarily not having to expose our app publicly to the internet. Websockets act almost similar to something like ngrok, where we create an outbound tunnel - except in this case, only Webex knows about it and has access to it (where ngrok is still providing you a public URL that anyone could hit, if they knew about it). Additionally, this method removes the need for maintaining a web server &amp; having to handle all of the incoming HTTP requests in your bot&rsquo;s code. There may also be a handful of API calls which are only supported via websockets today as well - like having your bot show that they&rsquo;ve read a message, for example.</p>
<p>I&rsquo;ve also found that the websocket method seems to be much more responsive, likely because there is a persistent TCP connection open - and you no longer have to rely on a full HTTP session establishment &amp; teardown for every message to the bot.</p>
<p>Historically, websockets were only officially supported via the Webex JavaScript SDK. However, it seems recently that someone has built a Python equivalent earlier this year - which we&rsquo;ll be using throughout this blog post. You can find that package <a href="https://github.com/fbradyirl/webex_bot">here</a></p>
<h1 id="getting-started-registering-the-bot-with-webex">Getting started: Registering the Bot with Webex</h1>
<p>So we&rsquo;ll start with some basic functionality. Let&rsquo;s grab the modules we need, apply our API key, and see if we can get a response from our bot.</p>
<p>Luckily, the module we&rsquo;re using makes this super easy. So let&rsquo;s install via pip:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install webex_bot
</span></span></code></pre></div><p>Once that&rsquo;s installed, we&rsquo;ll create a new Python file - I&rsquo;ll call mine <code>bot.py</code></p>
<p>In that file, we&rsquo;ll start our script like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">webex_bot.webex_bot</span> <span class="kn">import</span> <span class="n">WebexBot</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">api_token</span> <span class="o">=</span> <span class="s2">&#34;&lt;TOKEN_HERE&gt;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">bot</span> <span class="o">=</span> <span class="n">WebexBot</span><span class="p">(</span><span class="n">api_token</span><span class="p">,</span> <span class="n">approved_domains</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;0x2142.com&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">run</span><span class="p">()</span>
</span></span></code></pre></div><p>Looks easy enough, yeah? We&rsquo;ll import our webex_bot module first, as always. Then we&rsquo;ll define our API key. Obviously this can be stored as an environmental variable or another more secure location, but we&rsquo;ll define it here for demonstration.</p>
<p>Then we create a new instance of <code>WebexBot</code>, by providing our API key and an optional <code>approved_domains</code> value. In my case, I&rsquo;ve provided the 0x2142.com domain - which means the bot will reject messages from anyone who isn&rsquo;t from that organization.</p>
<p>Last, we start our bot with <code>bot.run()</code>.</p>
<p>Let&rsquo;s go ahead and run our bot with <code>python bot.py</code> and see what happens!</p>
<p>You should see something similar to this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">matt@0x2142:~/demo-webex-bot$ python bot.py
</span></span><span class="line"><span class="cl">2021-10-25 15:42:52  <span class="o">[</span>INFO<span class="o">]</span>  <span class="o">[</span>webex_bot.webex_bot.webex_bot.__init__<span class="o">]</span>:39 Registering bot with Webex cloud
</span></span><span class="line"><span class="cl">2021-10-25 15:42:52  <span class="o">[</span>INFO<span class="o">]</span>  <span class="o">[</span>restsession.webexteamssdk.restsession.user_agent<span class="o">]</span>:167 User-Agent: webexteamssdk/0+unknown <span class="o">{</span><span class="s2">&#34;implementation&#34;</span>: <span class="o">{</span><span class="s2">&#34;name&#34;</span>: <span class="s2">&#34;CPython&#34;</span>, <span class="s2">&#34;version&#34;</span>: <span class="s2">&#34;3.8.10&#34;</span><span class="o">}</span>, <span class="s2">&#34;system&#34;</span>: <span class="o">{</span><span class="s2">&#34;name&#34;</span>: <span class="s2">&#34;Linux&#34;</span>, <span class="s2">&#34;release&#34;</span>: <span class="s2">&#34;5.10.16.3-microsoft-standard-WSL2&#34;</span><span class="o">}</span>, <span class="s2">&#34;cpu&#34;</span>: <span class="s2">&#34;x86_64&#34;</span><span class="o">}</span>
</span></span><span class="line"><span class="cl">2021-10-25 15:42:52  <span class="o">[</span>WARNING<span class="o">]</span>  <span class="o">[</span>command.webex_bot.models.command.__init__<span class="o">]</span>:33 no card actions data so no entry <span class="k">for</span> <span class="s1">&#39;callback_keyword&#39;</span> <span class="k">for</span> <span class="nb">echo</span>
</span></span><span class="line"><span class="cl">2021-10-25 15:42:52  <span class="o">[</span>INFO<span class="o">]</span>  <span class="o">[</span>command.webex_bot.models.command.set_default_card_callback_keyword<span class="o">]</span>:47 Added default action <span class="k">for</span> <span class="s1">&#39;echo&#39;</span> <span class="nv">callback_keyword</span><span class="o">=</span>callback___echo
</span></span><span class="line"><span class="cl">2021-10-25 15:42:53  <span class="o">[</span>INFO<span class="o">]</span>  <span class="o">[</span>webex_bot.webex_bot.webex_bot.get_me_info<span class="o">]</span>:74 Running as bot <span class="s1">&#39;0x2142 Bot&#39;</span> with email <span class="o">[</span><span class="s1">&#39;0x2142@webex.bot&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">2021-10-25 15:42:54  <span class="o">[</span>INFO<span class="o">]</span>  <span class="o">[</span>webex_websocket_client.root._connect_and_listen<span class="o">]</span>:151 Opening websocket connection to wss://mercury-connection-partition2-a.wbx2.com/v1/apps/wx2/registrations/06b5c858-174a-4977-a268-04530d777563/messages
</span></span><span class="line"><span class="cl">2021-10-25 15:42:54  <span class="o">[</span>INFO<span class="o">]</span>  <span class="o">[</span>webex_websocket_client.root._connect_and_listen<span class="o">]</span>:154 WebSocket Opened.
</span></span></code></pre></div><p>So long as we continue to run this script in the foreground - we&rsquo;ll be able to see the live log of anything that happens, including messages to our bot. So for now, we&rsquo;ll leave this up &amp; running.</p>
<p>Now we should be able to try sending our bot a message in Webex.</p>
<p>In the Webex app, if we do a quick search for our bot (by email or name), we should see it pop up pretty quickly:</p>
<p><img alt="bot" loading="lazy" src="/content/images/2021/10/bot.png#center"></p>
<p>With our new space, we can interact with our bot. I&rsquo;ll send a &ldquo;Hello&rdquo; message:</p>
<p><img alt="bot2" loading="lazy" src="/content/images/2021/10/bot2.png#center"></p>
<p>And the webex_bot module will automatically respond back with a built-in help card.</p>
<p>Currently the bot only supports one command: echo. We&rsquo;ll add our own commands shortly, but for now let&rsquo;s give that a try:</p>
<p><img alt="bot4" loading="lazy" src="/content/images/2021/10/bot4.png#center"></p>
<p>So in the example above, I used the <code>echo</code> command. The bot returns a card with an input field. Once you fill out the textbox &amp; hit submit, the bot will echo back whatever text was provided!</p>
<p>So now we have a running bot. But what about implementing our own commands? Let&rsquo;s take a look!</p>
<h1 id="creating-custom-bot-commands">Creating custom bot commands</h1>
<p><em>Now</em> we get to the fun part!! To demonstrate, let&rsquo;s add a command to ask our bot for the current weather conditions. To accomplish this, we&rsquo;ll use the <a href="https://openweathermap.org/api">OpenWeather</a> APIs.</p>
<p>So to begin - We&rsquo;ll probably want a separate Python file for each command we want to add. First we&rsquo;ll add <code>weather.py</code> to our current directory.</p>
<p>Next, we&rsquo;ll throw in a bit of boilerplate code that will be used for each command:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">webex_bot.models.command</span> <span class="kn">import</span> <span class="n">Command</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">logging</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">log</span> <span class="o">=</span> <span class="n">logging</span><span class="o">.</span><span class="n">getLogger</span><span class="p">(</span><span class="vm">__name__</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">WeatherByZIP</span><span class="p">(</span><span class="n">Command</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">command_keyword</span><span class="o">=</span><span class="s2">&#34;weather&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">help_message</span><span class="o">=</span><span class="s2">&#34;Get current weather conditions by ZIP code.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">card</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">,</span> <span class="n">attachment_actions</span><span class="p">,</span> <span class="n">activity</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="sa">f</span><span class="s2">&#34;Got the message: </span><span class="si">{</span><span class="n">message</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p>So first we&rsquo;ll import <code>Command</code> from our webex_bot module, which will be used to create our custom command class.</p>
<p>Next we create a new class, in this case <code>WeatherByZIP</code>, and inherit the <code>Command</code> class from webex_bot. Within this new class, we&rsquo;ll create our <code>__init__</code> function &amp; define some information about our command.</p>
<p>So using <code>super().__init__()</code>, we&rsquo;ll provide what keyword should trigger this command, any help message to be provided to the user, and an optional card. We saw how the card could work earlier with the <code>echo</code> command - but to keep this module simple, we&rsquo;ll go without a card for now.</p>
<p>After that - we just need to create an <code>execute</code> function. This is what gets called by webex_bot when our command is triggered. By default, webex_bot will pass us the user&rsquo;s message - and, in the case of an action (like a card submission), it will provide those details.</p>
<p>Two things to keep in mind with our <code>execute</code> function:</p>
<ol>
<li>the <code>message</code> variable will contain the user&rsquo;s message with the command keyword removed. So in our case, if the user&rsquo;s message was &ldquo;weather 12345&rdquo; - then the message variable would just contain &ldquo;12345&rdquo;.</li>
<li>Any text we return from our function will be sent via the bot as a chat response. If we wanted to provide extras, like cards or attachments, we would need to use the webex_bot <code>Response</code> object. I might cover this in a separate blog post later</li>
</ol>
<p>Okay! So right now our new command is configured to just echo back any test that is sent to the bot - similar to the <code>echo</code> command earlier, except without the card.</p>
<p>We&rsquo;ll add our weather functionality shortly - but let&rsquo;s first add our command to the bot &amp; give it a quick test.</p>
<p>In our <code>bot.py</code> file, we&rsquo;ll just need to import our command module and call <code>bot.add_command()</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">webex_bot.webex_bot</span> <span class="kn">import</span> <span class="n">WebexBot</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">weather</span> <span class="kn">import</span> <span class="n">WeatherByZIP</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">api_token</span> <span class="o">=</span> <span class="s2">&#34;&lt;TOKEN_HERE&gt;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">bot</span> <span class="o">=</span> <span class="n">WebexBot</span><span class="p">(</span><span class="n">api_token</span><span class="p">,</span> <span class="n">approved_domains</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;0x2142.com&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">add_command</span><span class="p">(</span><span class="n">WeatherByZIP</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">bot</span><span class="o">.</span><span class="n">run</span><span class="p">()</span>
</span></span></code></pre></div><p>That&rsquo;s it! Now we should be able to try it out. We&rsquo;ll restart our bot, and send the command <code>weather 12345</code>:</p>
<p><img alt="bot5" loading="lazy" src="/content/images/2021/10/bot5.png#center"></p>
<p>Sure enough, our bot gets the message, passes it to our custom command, and returns the stripped message - all as expected.</p>
<p>So let&rsquo;s add our weather query &amp; actually make this thing work.</p>
<p>Using the OpenWeather APIs, I&rsquo;ll create a new API key - then create a variable in the command module called <code>OPENWEATHER_KEY</code>. Then, I&rsquo;ve updated my <code>execute</code> function to look like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">,</span> <span class="n">attachment_actions</span><span class="p">,</span> <span class="n">activity</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Message may include spaces. Strip whitespace:</span>
</span></span><span class="line"><span class="cl">    <span class="n">zip_code</span> <span class="o">=</span> <span class="n">message</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Define our URL, with requested ZIP code &amp; API Key</span>
</span></span><span class="line"><span class="cl">    <span class="n">url</span> <span class="o">=</span> <span class="s2">&#34;https://api.openweathermap.org/data/2.5/weather?&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">url</span> <span class="o">+=</span> <span class="sa">f</span><span class="s2">&#34;zip=</span><span class="si">{</span><span class="n">zip_code</span><span class="si">}</span><span class="s2">&amp;units=imperial&amp;appid=</span><span class="si">{</span><span class="n">OPENWEATHER_KEY</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Query weather</span>
</span></span><span class="line"><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">weather</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Pull out desired info</span>
</span></span><span class="line"><span class="cl">    <span class="n">city</span> <span class="o">=</span> <span class="n">weather</span><span class="p">[</span><span class="s1">&#39;name&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">conditions</span> <span class="o">=</span> <span class="n">weather</span><span class="p">[</span><span class="s1">&#39;weather&#39;</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="s1">&#39;description&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">temperature</span> <span class="o">=</span> <span class="n">weather</span><span class="p">[</span><span class="s1">&#39;main&#39;</span><span class="p">][</span><span class="s1">&#39;temp&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">humidity</span> <span class="o">=</span> <span class="n">weather</span><span class="p">[</span><span class="s1">&#39;main&#39;</span><span class="p">][</span><span class="s1">&#39;humidity&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">wind</span> <span class="o">=</span> <span class="n">weather</span><span class="p">[</span><span class="s1">&#39;wind&#39;</span><span class="p">][</span><span class="s1">&#39;speed&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">response_message</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;In </span><span class="si">{</span><span class="n">city</span><span class="si">}</span><span class="s2">, it&#39;s currently </span><span class="si">{</span><span class="n">temperature</span><span class="si">}</span><span class="s2">F with </span><span class="si">{</span><span class="n">conditions</span><span class="si">}</span><span class="s2">. &#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">response_message</span> <span class="o">+=</span> <span class="sa">f</span><span class="s2">&#34;Wind speed is </span><span class="si">{</span><span class="n">wind</span><span class="si">}</span><span class="s2">mph. Humidity is </span><span class="si">{</span><span class="n">humidity</span><span class="si">}</span><span class="s2">%&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">response_message</span>
</span></span></code></pre></div><p>So because of the way that <code>message</code> is passed to us, we&rsquo;ll first need to strip whitespace. This is because when the user enters a command like &ldquo;weather 12345&rdquo;, webex_bot will only remove the &ldquo;weather&rdquo; portion - leaving us with &quot; 12345&quot;. So by stripping that value, we&rsquo;re left with just the numeric ZIP Code: &ldquo;12345&rdquo;.</p>
<p>Next we assemble our URL, supplying our parameters: zip, units, and appid. Zip is the numeric ZIP code provided by our user, units will be either imperial or metric, and appid is our API key.</p>
<p>After that, it&rsquo;s as simple as sending the GET request &amp; parsing the data. In the code above, I assemble the final text response into <code>response_message</code> which is then returned to the user.</p>
<p>All that being done - Let&rsquo;s try it out!!</p>
<p><img alt="bot6" loading="lazy" src="/content/images/2021/10/bot6.png#center"></p>
<p>In this example, I used the ZIP code for Richfield, OH - and our bot quickly returned that information to us.</p>
<hr>
<p>And that&rsquo;s it! In just a few steps we created a new Webex bot &amp; added custom commands. Once we have that process down, it&rsquo;s easy enough to continue extending our bot by adding additional commands.</p>
<p>There are obviously more advanced use cases - and a whole lot of <em>fun</em> that comes with using Adaptive Cards&hellip; But I hope this post was helpful in getting you started!</p>
<p><strong>Update 2022/03/12 -</strong> If you&rsquo;re interested in seeing additional content around Cards, please check out the new blog post &amp; video <a href="/webex-chatbot-with-adaptivecards/">here</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Network Engineering: Do I REALLY Need to Learn to Code?</title>
      <link>https://0x2142.com/do-i-really-need-to-learn-to-code/</link>
      <pubDate>Wed, 13 Oct 2021 17:43:47 +0000</pubDate>
      <guid>https://0x2142.com/do-i-really-need-to-learn-to-code/</guid>
      <description>Is learning Python really necessary for your networking career? Let&amp;rsquo;s investigate</description>
      <content:encoded><![CDATA[<p>I guess it&rsquo;s that time of year again, where I&rsquo;m starting to see the same discussion points pop up quite a bit once more:</p>
<p>&ldquo;As a network administrator, do I really need to learn to code?&rdquo;</p>
<p>&ldquo;How much to I need to learn?&rdquo;</p>
<p>&ldquo;This will <strong>NEVER</strong> be in enterprise networks&rdquo;</p>
<p>Well, today I&rsquo;m here to give some thoughts. As with all my content, I reserve the right to be wrong or not 100% in alignment with everyone else&rsquo;s view of the world.</p>
<hr>
<h2 id="do-i-need-to-learn-code">Do I need to learn code?</h2>
<p>Nope.</p>
<h2 id="wait-really">Wait really?!?</h2>
<p>Correct, no one can make you learn if you don&rsquo;t have an interest in it.</p>
<h2 id="okay-but-will-i-benefit-from-it">Okay, but will I benefit from it?</h2>
<p>100% yes you will. To what degree may vary, but I think it&rsquo;s worth a look.</p>
<h2 id="well-how-much-do-i-need-to-know">Well how much do I need to know?</h2>
<p>As the network architect might say, <em>it depends.</em></p>
<h2 id="is-automation-coming-for-my-job">Is automation coming for my job?</h2>
<p>¯\_(ツ)_/¯</p>
<hr>
<p>So what&rsquo;s the right answer here? It depends on what&rsquo;s right for you - what you&rsquo;re interested in, what you think will help you in your job, and what you think will help you succeed.</p>
<p>Yes, it&rsquo;s true that much of the networking industry has been shifting focus to automation &amp; programmability. Open APIs have become much more important in recent years as we seek to expand our capabilities through custom code &amp; integrations. I don&rsquo;t see this slowing down anytime soon.</p>
<p>But will that spell the end of the network engineer as we know it? I don&rsquo;t know that anyone can 100% say definitively, but I&rsquo;m willing to bet that network engineers aren&rsquo;t going anywhere anytime soon. The role will definitely change &amp; evolve over time (as it already has been, and will continue), but truly go away? I think not.</p>
<h3 id="the-benefits-of-programmability">The benefits of programmability</h3>
<p>I&rsquo;m sure most people are familiar with a lot of this by now, but why should we care about automation? Well there are lots of reasons! For example, here&rsquo;s a few:</p>
<ul>
<li>Reducing human error</li>
<li>Reducing time spent on repetitive tasks</li>
<li>Accomplishing things that aren&rsquo;t natively possible in a product</li>
<li>Adding custom functionality or integrations with other systems</li>
<li>And more!</li>
</ul>
<h3 id="how-much-you-should-learn">How much you should learn</h3>
<p>This one REAALLY depends. To throw out some ideas, here are some factors that could help determine how much you&rsquo;ll need to know:</p>
<ul>
<li>How much you enjoy it</li>
<li>How much your company cares or has any automation initiatives</li>
<li>The type (and age) of gear you&rsquo;re working with</li>
<li>Whether or not you have an in-house development or devops team</li>
</ul>
<p>So on one side, I think that most people who get into writing Python &amp; find that they truly enjoy it will likely end up transitioning to a more developer-focused role - whether that be a network-centric devops role or pure development role. Or simply just seeking out more ways to focus on programmability within their current network engineering role. These types of people are finding that they love solving problems &amp; being able to have the creative freedom to write whatever code they want to accomplish a task. They&rsquo;ve found something that they enjoy - Good for them!</p>
<p>On the other side, even if a network engineer isn&rsquo;t going to write code very often - just knowing the fundamentals can be hugely beneficial. For example, as a network engineer, have you ever had to learn how VMware works? Likely you didn&rsquo;t feel the need to become a VMware administrator overnight - but understanding how a vSwitch works has helped you support those teams. How much easier did it become to communicate with the VMware admins?</p>
<p>With programmability, it&rsquo;s much the same. If you&rsquo;re not going to dive in, that&rsquo;s okay. But you&rsquo;ll likely benefit from a good overall understanding of how things work. For me personally, it was easy to recognize the benefit when I was a network administrator. Application team puts in a ticket saying something isn&rsquo;t working right? Well guess who understands API calls, HTTP requests, and JSON/XML? It suddenly became much easier to help find root cause, and often easier to shift blame away from the network. Plus, when you speak with an application/development team &amp; you can use <em>their terms</em>, they feel like you understand them. This helps reduce friction between teams. In my experience, what I found was that those application teams would then ask for my help more often - because I was willing to go further than &ldquo;checked the firewall, it&rsquo;s not the network&rdquo;.</p>
<p>In my opinion, whether or not you learn to code &amp; how much you learn comes down to your goals. Can you keep being a network admin &amp; avoid code? Sure - just like you could continue being a network admin &amp; never learn design, business goals, or what other teams in your company do. But will learning those things expand your knowledge &amp; give you a better big-picture view? Yup. Would learning those things give you an advantage? Absolutely.</p>
<p>What happens when you run into a difficult challenge where manual effort would be reckless? Maybe you pass off the job to a development team to write a utility or some custom integration. But guess what happens next? That development team doesn&rsquo;t understand how your network gear operates. Again, this situation really benefits from having an individual who can speak both languages. Even if you don&rsquo;t understand functions &amp; classes, you might be able to read API docs for your network gear and help guide the developer through where to start.</p>
<p>What about that in-between state? Maybe you want to learn to code a bit, but not 100% of the time. What then? Well, could you pull &amp; terminate ethernet cables without a cable tester? Yes, but would you be more efficient with one? Also yes. Similarly, being able to code can just be as simple as adding another tool to your skill set. Would you automate a single port change? Nope. But what if you find yourself making that same port change several times a week? That&rsquo;s when you pull out your Python skills and write a quick script.</p>
<p>And for those who would rather just not do any of it? You&rsquo;re fine. How many parts of the Internet still run legacy protocols? Someone has to support that. And there are companies out there who want commercially supported products, so they have someone to blame. Vendors know this, which is why we end up with big, centralized controllers &amp; complex GUI apps to help slowly pull networking out of the CLI days. But you can&rsquo;t tell me that SD-Access, ACI, or EVPN are simple technologies - companies will still benefit from employing people who understand the underlying technologies &amp; protocols. Let&rsquo;s be honest: automated or not, things still break - and <em>someone</em> will need to have the knowledge to fix it.</p>
<hr>
<h3 id="so-whats-the-answer-here">So what&rsquo;s the answer here?</h3>
<p>¯\_(ツ)_/¯</p>
<p>It seems like these debates continue because everyone comes from different backgrounds &amp; has different experiences. Some people work in companies that would never entertain the idea of custom code, where others may find their team being pushed to become more agile &amp; learn Python to automate tasks. Neither approach is wrong, but of course our human instinct is that we have to assert that everyone else <em>must</em> be wrong, since their view doesn&rsquo;t match ours. The world is never a one-size-fits-all for every situation, yet for some reason we&rsquo;re all inclined to think it is.</p>
<p>All that being said, companies are going to adopt this stuff at different paces. Some find immediate value in it now, and some are scared or just set in their ways. Again, neither is wrong - but they both have their own advantages &amp; disadvantages.</p>
<p>And that&rsquo;s why I say - if you don&rsquo;t want to learn any of this, you have nothing to fear. There will always be organizations at different stages of the cycle.</p>
<p>But if you do learn? At worst, you gain knowledge outside of your primary domain. At best, you gain a new tool to leverage &amp; find ways to improve your work.</p>
<p>At the end of the day, you have to focus on what&rsquo;s right for you, your career, and your current employer. And that answer is going to be different for everyone.</p>
]]></content:encoded>
    </item>
    <item>
      <title>From Postman to Python: Your First GET Request</title>
      <link>https://0x2142.com/from-postman-to-python-your-first-get-request/</link>
      <pubDate>Thu, 11 Mar 2021 20:21:34 +0000</pubDate>
      <guid>https://0x2142.com/from-postman-to-python-your-first-get-request/</guid>
      <description>Let&amp;rsquo;s walk through a few simple Postman requests, and show how to do the same with Python</description>
      <content:encoded><![CDATA[<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/nOdIyrA2l5A?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<p>Maybe it&rsquo;s just me - but I feel like when we want to demonstrate product APIs to someone, we usually jump into Postman first. It&rsquo;s not without good reason though. Postman is a very easy to use platform for running API calls against REST endpoints &amp; see the nicely formatted output.</p>
<p>That being said, often times I&rsquo;ve seen that we stop there. We might mention Python as an advanced method of using our APIs, or maybe we talk about how &ldquo;if APIs are the new CLI, Postman is the new PuTTY&rdquo;. I&rsquo;ve seen some engineers who hear these statements and feel like Postman is being pushed on them - even when they&rsquo;re perfectly happy with an SSH-based CLI.</p>
<p>Network automation isn&rsquo;t usually accomplished with just one tool - it&rsquo;s a whole storage closet full of different tools &amp; utilities, each with their own use cases or specializations. In this particular example - learning Python allows you to move beyond one-off API calls in Postman, and into being able to accomplish much more complex automation.</p>
<p>So the purpose of this post is to explore how to get beyond just Postman, and into using Python for REST API calls. This isn&rsquo;t going to be a complete run-down of all the capabilities of both tools - but rather a few examples of simple GET requests using both methods.</p>
<p>If you don&rsquo;t have Postman yet, go ahead and snag the download <a href="https://www.postman.com/downloads/">here</a>.</p>
<p><em>Note: Much of the code below is minimal and does not contain any error handling. Therefore, it should not be used in a production network. Full example code <a href="https://github.com/0x2142/example-scripts/tree/master/postman-python">here</a>.</em></p>
<hr>
<h2 id="postman-simple-get-request">Postman: Simple GET Request</h2>
<p>So first, let&rsquo;s start off with an example of using Postman for a simple GET request.</p>
<p>In this example, we&rsquo;ll keep things simple &amp; use a non-authenticated API endpoint. We&rsquo;ll accomplish this using a free website called <a href="https://jsonplaceholder.typicode.com/">JSON Placeholder</a>.</p>
<p>Let&rsquo;s go ahead and start up Postman, and we&rsquo;ll see a blank workspace:</p>
<p><img alt="001&mdash;Postman-1" loading="lazy" src="/content/images/2021/03/001---Postman-1.PNG#center"></p>
<p>What we&rsquo;ll need to pay attention to first, is what type of HTTP request we&rsquo;re sending - as well as the URL we want to send our request to. If we expand the request type dropdown, we&rsquo;ll see a handful of options - but we&rsquo;ll only be using a GET request for now:</p>
<p><img alt="002&mdash;Postman" loading="lazy" src="/content/images/2021/03/002---Postman.PNG#center"></p>
<p>In order to create our first request, we&rsquo;ll just need to enter our API endpoint URL. We&rsquo;ll use the <strong>users</strong> endpoint offered by JSON Placeholder, which is located at: <a href="https://jsonplaceholder.typicode.com/users">https://jsonplaceholder.typicode.com/users</a></p>
<p><img alt="003&mdash;Postman" loading="lazy" src="/content/images/2021/03/003---Postman.png#center"></p>
<p>In the screenshot above, you&rsquo;ll see in the highlighted box that we entered our URL. Next, we just click the big <strong>Send</strong> button on the right side.</p>
<p>I already sent the GET request, so in the screenshot you&rsquo;ll also notice that we have our JSON response from the API. The mock data offered by this API provides us with a list of 10 different users, along with the data that&rsquo;s been entered for each user.</p>
<p>What if we only wanted to get data for one specific user? Well, our test API site supports using query parameters to filter our response. Let&rsquo;s say we want to find information for a user named <strong>Delphine</strong>. We would append <em>?username=Delphine</em> to our URL:</p>
<p><img alt="004&mdash;Postman" loading="lazy" src="/content/images/2021/03/004---Postman.PNG#center"></p>
<p>So as we can see above - now our JSON response only contains the data for a single user. If we needed to find a user&rsquo;s phone number, this could be an easy way to quickly filter our data &amp; get to what we need.</p>
<p>This is really where Postman is great. We&rsquo;re able to quickly &amp; visually test out an API call. We can see what the response data looks like, and understand the structure of requests &amp; responses.</p>
<p>But what about when we want to iterate through a number of users and pull only a specific few statistics? Or maybe we need a way to take a list of user information, and automatically upload or convert that data into another system.</p>
<p>For use cases like that, we&rsquo;ll jump over to Python.</p>
<h2 id="python-simple-get-request">Python: Simple GET Request</h2>
<p>In this section, we&rsquo;ll walk through the same example from above - but this time using Python.</p>
<p>First thing we&rsquo;ll need to do, is install the Python <em>requests</em> library. While there are a handful of libraries that can accomplish this work, <em>requests</em> is fairly popular &amp; easy to use.</p>
<p>So we&rsquo;ll kick things off by using pip to install our library:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">pip</span> <span class="n">install</span> <span class="n">requests</span>
</span></span></code></pre></div><p>Then, we&rsquo;ll create a very simple Python script to perform the same initial GET request from earlier. So we&rsquo;ll pull a list of ALL users from the API.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">requests</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">URL</span> <span class="o">=</span> <span class="s2">&#34;https://jsonplaceholder.typicode.com/users&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">URL</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
</span></span></code></pre></div><p>The code above should be pretty straightforward. First we&rsquo;ll import our <em>requests</em> library. Then, just to keep the code clean, we&rsquo;ll create a variable called <em>URL</em> to hold the URL for the API endpoint.</p>
<p>Next, we send that GET request, using <em>requests.get</em>. Last but not least, we&rsquo;ll go ahead and print out the text payload that we receive back. That output looks pretty similar:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">matt@DESKTOP:~/postman-python$ python3 simpleGET.py 
</span></span><span class="line"><span class="cl"><span class="o">[</span>
</span></span><span class="line"><span class="cl">  <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;id&#34;</span>: 1,
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;name&#34;</span>: <span class="s2">&#34;Leanne Graham&#34;</span>,
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;username&#34;</span>: <span class="s2">&#34;Bret&#34;</span>,
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;email&#34;</span>: <span class="s2">&#34;Sincere@april.biz&#34;</span>,
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;address&#34;</span>: <span class="o">{</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;street&#34;</span>: <span class="s2">&#34;Kulas Light&#34;</span>,
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;suite&#34;</span>: <span class="s2">&#34;Apt. 556&#34;</span>,
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;city&#34;</span>: <span class="s2">&#34;Gwenborough&#34;</span>,
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;zipcode&#34;</span>: <span class="s2">&#34;92998-3874&#34;</span>,
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;geo&#34;</span>: <span class="o">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;lat&#34;</span>: <span class="s2">&#34;-37.3159&#34;</span>,
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;lng&#34;</span>: <span class="s2">&#34;81.1496&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="o">}</span>
</span></span><span class="line"><span class="cl">    <span class="o">}</span>,
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;phone&#34;</span>: <span class="s2">&#34;1-770-736-8031 x56442&#34;</span>,
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;website&#34;</span>: <span class="s2">&#34;hildegard.org&#34;</span>,
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;company&#34;</span>: <span class="o">{</span>
</span></span><span class="line"><span class="cl">     -- output truncated --
</span></span></code></pre></div><p>Now, let&rsquo;s add a little extra logic to our next step. Rather than just specifying a static username that we want to search for, let&rsquo;s ask our user to specify a username:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">requests</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">URL</span> <span class="o">=</span> <span class="s2">&#34;https://jsonplaceholder.typicode.com/users&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Search by Username:&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">user</span> <span class="o">=</span> <span class="nb">input</span><span class="p">(</span><span class="s2">&#34;&gt; &#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">queryURL</span> <span class="o">=</span> <span class="n">URL</span> <span class="o">+</span> <span class="sa">f</span><span class="s2">&#34;?username=</span><span class="si">{</span><span class="n">user</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">queryURL</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
</span></span></code></pre></div><p>In the code above, we made just a few changes. One is printing out a prompt to &ldquo;Search by Username&rdquo;. Then we use the Python <em>input</em> function to collect input from our user, then store that in the <em>user</em> variable.</p>
<p>Next, we create a <em>queryURL</em> - which is similar to the URL we used in the Postman example. Except in this case, we&rsquo;re supplying a dynamic username input based on whatever the end user wants to search for. We use a Python <a href="https://realpython.com/python-f-strings/">f-string</a> to inject the username into our query parameters.</p>
<p>Let&rsquo;s see what that looks like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">matt@DESKTOP:~/postman-python$ python3 simpleGET.py 
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Search by Username:
</span></span><span class="line"><span class="cl">&gt; Delphine
</span></span><span class="line"><span class="cl"><span class="o">[</span>
</span></span><span class="line"><span class="cl">  <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;id&#34;</span>: 9,
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;name&#34;</span>: <span class="s2">&#34;Glenna Reichert&#34;</span>,
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;username&#34;</span>: <span class="s2">&#34;Delphine&#34;</span>,
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;email&#34;</span>: <span class="s2">&#34;Chaim_McDermott@dana.io&#34;</span>,
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;address&#34;</span>: <span class="o">{</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;street&#34;</span>: <span class="s2">&#34;Dayna Park&#34;</span>,
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;suite&#34;</span>: <span class="s2">&#34;Suite 449&#34;</span>,
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;city&#34;</span>: <span class="s2">&#34;Bartholomebury&#34;</span>,
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;zipcode&#34;</span>: <span class="s2">&#34;76495-3109&#34;</span>,
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;geo&#34;</span>: <span class="o">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;lat&#34;</span>: <span class="s2">&#34;24.6463&#34;</span>,
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;lng&#34;</span>: <span class="s2">&#34;-168.8889&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="o">}</span>
</span></span><span class="line"><span class="cl">    <span class="o">}</span>,
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;phone&#34;</span>: <span class="s2">&#34;(775)976-6794 x41206&#34;</span>,
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;website&#34;</span>: <span class="s2">&#34;conrad.com&#34;</span>,
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;company&#34;</span>: <span class="o">{</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;name&#34;</span>: <span class="s2">&#34;Yost and Sons&#34;</span>,
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;catchPhrase&#34;</span>: <span class="s2">&#34;Switchable contextually-based project&#34;</span>,
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;bs&#34;</span>: <span class="s2">&#34;aggregate real-time technologies&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="o">}</span>
</span></span><span class="line"><span class="cl">  <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="o">]</span>
</span></span></code></pre></div><p>Perfect! We get the result we expected, which is the single dataset from the user that we specified.</p>
<p>Let&rsquo;s take this one step further! Maybe we want to build a quick utility to search for a user&rsquo;s contact information. We can take the raw JSON output, pull out a few pieces of info, then apply some formatting to make it more user-friendly.</p>
<p>In the following example, we&rsquo;ll update our code to search that JSON response &amp; collect the user&rsquo;s name, email address, and phone number.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">requests</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">json</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">URL</span> <span class="o">=</span> <span class="s2">&#34;https://jsonplaceholder.typicode.com/users&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Search by Username:&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">user</span> <span class="o">=</span> <span class="nb">input</span><span class="p">(</span><span class="s2">&#34;&gt; &#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">queryURL</span> <span class="o">=</span> <span class="n">URL</span> <span class="o">+</span> <span class="sa">f</span><span class="s2">&#34;?username=</span><span class="si">{</span><span class="n">user</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">queryURL</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">userdata</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">text</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">name</span> <span class="o">=</span> <span class="n">userdata</span><span class="p">[</span><span class="s2">&#34;name&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">email</span> <span class="o">=</span> <span class="n">userdata</span><span class="p">[</span><span class="s2">&#34;email&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">phone</span> <span class="o">=</span> <span class="n">userdata</span><span class="p">[</span><span class="s2">&#34;phone&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s2"> can be reached via the following methods:&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Email: </span><span class="si">{</span><span class="n">email</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Phone: </span><span class="si">{</span><span class="n">phone</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span></code></pre></div><p>So a few things have changed in the above example. First thing you may notice, is that we&rsquo;ve added an additional import: the <strong>json</strong> library. We use this in line 11, where we convert the JSON output into a native python object using the <em>json.loads</em> function.</p>
<p>What this allows us to do is easily pull individual data values from the JSON output. In the original JSON data that we received, we saw a bunch of user data organized into key-value pairs. So once we load that JSON into a Python dictionary, we can pull out individual values and assign them to variables.</p>
<p>Finally - we can print all of that data out to our terminal:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">matt@DESKTOP:~/postman-python$ python3 simpleGET.py
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Search by Username:
</span></span><span class="line"><span class="cl">&gt; Delphine
</span></span><span class="line"><span class="cl">Glenna Reichert can be reached via the following methods:
</span></span><span class="line"><span class="cl">Email: Chaim_McDermott@dana.io
</span></span><span class="line"><span class="cl">Phone: <span class="o">(</span>775<span class="o">)</span>976-6794 x41206
</span></span></code></pre></div><p>Nothing super fancy here - but this could be the beginnings of building out a user search web page, or maybe importing certain data fields into another system. Once we get the basics of working with REST APIs out of the way, there are a ton of possibilities for what can be built.</p>
<h2 id="okay---what-about-network-automation">Okay - What About Network Automation?</h2>
<p>Many new network technologies are now API enabled. Nearly every cloud-hosted platform or management controller offers some method of interacting programmatically. In fact, most routers &amp; switches even offer a REST-based API that can be used for automated configuration or monitoring.</p>
<p>So let&rsquo;s take the following example: We have a couple of Meraki networks, and we&rsquo;ve been tasked with providing a report of how many total clients have connected to our network in the last 30 days. In addition, it would be nice to see a breakdown of the distribution of operating systems.</p>
<p>Let&rsquo;s take a look at the following screenshot from Postman:</p>
<p><img alt="005&mdash;Postman" loading="lazy" src="/content/images/2021/03/005---Postman.png#center"></p>
<p>Using Meraki&rsquo;s <a href="https://developer.cisco.com/meraki/api-v1/#!get-network-clients">API docs</a>, we can determine the exact URL endpoint we need to query. This URL requires that we know the exact network ID for the network we want to get info from, which we&rsquo;ll pretend we already know. Also not shown here, we have an additional HTTP header that includes our API key - which was generated in Meraki Dashboard.</p>
<p>In addition, we have a few query parameters to help make sure we get the data we need. By default, this API endpoint will return 10 devices. In this particular network, we already know we have more than 10 - so we use the <em>perPage</em> query parameter to request up to 100 devices. We also wanted to query the last 30 days of devices, so we use the <em>timespan</em> parameter. This parameter expects the lookback to be in seconds, so we specify 43,200 seconds.</p>
<p>So we got back a list of devices - and specifically in the example shown, it looks like we have an Android device on our network. Easy enough to spot that info, right?</p>
<p>Well - maybe we have more than a handful of devices. We don&rsquo;t want to manually sort through all of that data &amp; assemble a list. Or maybe this task needs to be run more than once. This is where we can use Python to automatically assemble that report with just a few lines of code.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">requests</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">json</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">URL</span> <span class="o">=</span> <span class="s2">&#34;https://api.meraki.com/api/v1&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">APIKEY</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;X-Cisco-Meraki-API-Key&#34;</span><span class="p">:</span> <span class="s2">&#34;xxxxxxxxxxxxxxxxx&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">---</span> <span class="n">Code</span> <span class="n">omitted</span> <span class="o">---</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">getClients</span><span class="p">(</span><span class="n">orgID</span><span class="p">,</span> <span class="n">networkList</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    Query clients for each network, return client list
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">clientCount</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl">    <span class="n">total</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Query Parameters: Return up to 100 devices seen in the past 43,200 seconds (30 days)</span>
</span></span><span class="line"><span class="cl">    <span class="n">q</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;perPage&#34;</span><span class="p">:</span> <span class="s2">&#34;100&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="s2">&#34;timespan&#34;</span><span class="p">:</span> <span class="s2">&#34;43200&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">network</span> <span class="ow">in</span> <span class="n">networkList</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Query clients for each network</span>
</span></span><span class="line"><span class="cl">        <span class="n">queryURL</span> <span class="o">=</span> <span class="n">URL</span> <span class="o">+</span> <span class="sa">f</span><span class="s2">&#34;/networks/</span><span class="si">{</span><span class="n">network</span><span class="si">}</span><span class="s2">/clients&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">queryURL</span><span class="p">,</span> <span class="n">params</span><span class="o">=</span><span class="n">q</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">APIKEY</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">data</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Grab client OS from each device &amp; append to clientCount dictionary</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">client</span> <span class="ow">in</span> <span class="n">data</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="n">clientCount</span><span class="p">[</span><span class="n">client</span><span class="p">[</span><span class="s2">&#34;os&#34;</span><span class="p">]]</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">            <span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="n">clientCount</span><span class="p">[</span><span class="n">client</span><span class="p">[</span><span class="s2">&#34;os&#34;</span><span class="p">]]</span> <span class="o">=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">            <span class="k">except</span> <span class="ne">TypeError</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="k">continue</span>
</span></span><span class="line"><span class="cl">            <span class="n">total</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Append final count of all devices &amp; return dict</span>
</span></span><span class="line"><span class="cl">    <span class="n">clientCount</span><span class="p">[</span><span class="s2">&#34;Total Devices&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">total</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">clientCount</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">printReport</span><span class="p">(</span><span class="n">clientOS</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    Print final output to terminal
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Count of clients by operating system:&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">OS</span> <span class="ow">in</span> <span class="n">clientOS</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">OS</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">clientOS</span><span class="p">[</span><span class="n">OS</span><span class="p">]</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl"><span class="o">---</span> <span class="n">Code</span> <span class="n">omitted</span> <span class="o">---</span>
</span></span></code></pre></div><p>To keep things simple - we&rsquo;ll only look at a snippet of the code. The entire example will be posted to <a href="https://github.com/0x2142/example-scripts/tree/master/postman-python">GitHub</a>, and contains two additional functions that will automatically find our Organization ID &amp; Network ID.</p>
<p>In our <em>getClients</em> function, we create an empty Python dictionary to hold our list of client operating systems - and we also create a <em>total</em> variable which we&rsquo;ll increment for every client in the list.</p>
<p>The HTTP GET request should look fairly similar to what we used previously. The big difference is that we&rsquo;re creating a dictionary called <em>q</em> to hold our <em>perPage</em> &amp; <em>timespan</em> query parameters. Then we include that in our GET request by adding <em>params=q</em> to the request.</p>
<p>Next - we convert the JSON response into a Python object, and walk through every device in that list. For each device in the list, we look at the &ldquo;os&rdquo; value to determine the operating system detected by Meraki. We then use a try/except block to see if we can increment the counter for that operating system. If we get a <strong>KeyError</strong>, then we know the OS isn&rsquo;t currently on the list &amp; we can just add it with a new value of 1.</p>
<p>After all that has been completed, we return our <em>clientCount</em> dictionary - which our primary function passes to the <em>printReport</em> function. This just prints out a header, then iterates through our <em>clientCount</em> dictionary and prints each operating system &amp; count.</p>
<p>Let&rsquo;s take a look at that output below:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">matt@DESKTOP:~/postman-python$ python getOScount.py
</span></span><span class="line"><span class="cl">Count of clients by operating system:
</span></span><span class="line"><span class="cl">None: <span class="m">9</span>
</span></span><span class="line"><span class="cl">Sailfish OS: <span class="m">2</span>
</span></span><span class="line"><span class="cl">Nokia: <span class="m">1</span>
</span></span><span class="line"><span class="cl">Windows 10: <span class="m">2</span>
</span></span><span class="line"><span class="cl">Raspberry Pi: <span class="m">2</span>
</span></span><span class="line"><span class="cl">Android: <span class="m">4</span>
</span></span><span class="line"><span class="cl">Meraki OS: <span class="m">1</span>
</span></span><span class="line"><span class="cl">Total Devices: <span class="m">21</span>
</span></span></code></pre></div><p>In the list above, we can see that we have a total of 21 devices in our network - and 7 different operating systems were found as well.</p>
<p>That output may not be the prettiest of reports, but it was accomplished with ~50 lines of Python &amp; takes less than a few seconds to run. If we wanted to format the data in another way, or include data from a non-Meraki source, we absolutely could.</p>
<p>That&rsquo;s part of what&rsquo;s exciting about using Python for network automation - most anything we could think of doing is possible.</p>
<h2 id="tip-use-postman-to-generate-python-code">TIP: Use Postman to Generate Python Code</h2>
<p>Okay - so I wanted to save this bit for last. Now that you&rsquo;ve seen examples in both tools, I wanted to make sure we cover a feature in postman that can save some time.</p>
<p>So over in the right-hand side of your Postman window, you&rsquo;ll see an icon that looks like this: <strong>&lt;/&gt;</strong></p>
<p><img alt="006&mdash;Postman" loading="lazy" src="/content/images/2021/03/006---Postman.png#center"></p>
<p>If you click that, you&rsquo;ll see a dropdown menu of various languages &amp; tools. Select one of those options and Postman will auto-generate code you can use. This auto-generated code will execute the same request you have built in the Postman GUI.</p>
<p>For example, our last Postman request we used to get Meraki clients:</p>
<p><img alt="007&mdash;Postman" loading="lazy" src="/content/images/2021/03/007---Postman.png#center"></p>
<p>I wanted to save this for last, because I feel like it&rsquo;s important to understand how to write the code manually first - then take advantages of shortcuts after having a good understanding of what the code is doing.</p>
<hr>
<p>That&rsquo;s about all I had for this example. Again, my intent here wasn&rsquo;t to create an all-inclusive resource for how to use Postman &amp; Python - but rather to give some examples for each one &amp; show where/why each would be used.</p>
<p>I&rsquo;ve met a handful of people over the years who have seen many API demos using Postman, but aren&rsquo;t sold yet on the value of learning Python &amp; getting into network automation.</p>
<p>Hopefully these examples help bridge that gap a little bit 😄</p>
]]></content:encoded>
    </item>
    <item>
      <title>Getting Started with IOS-XE Guestshell</title>
      <link>https://0x2142.com/getting-started-with-ios-xe-guestshell/</link>
      <pubDate>Tue, 26 Jan 2021 16:15:41 +0000</pubDate>
      <guid>https://0x2142.com/getting-started-with-ios-xe-guestshell/</guid>
      <description>In this post, we&amp;rsquo;ll explore how to set up Cisco IOX &amp;amp; Guestshell for on-box Python</description>
      <content:encoded><![CDATA[<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/KiVbDq0Czdg?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<p>This post has been on my backlog for a while&hellip; I&rsquo;ve been intermittently trying out the IOS-XE guestshell feature for a few different projects, and never feeling like any of them were good enough to write about.</p>
<p>So why not just write about guestshell itself? It&rsquo;s such a nifty tool.</p>
<hr>
<h2 id="what-is-guestshell">What is Guestshell?</h2>
<p>Now that Cisco&rsquo;s Catalyst platform has standardized on IOS-XE, we can take advantage of some of the features of the Linux-based platform. One of which being Linux Containers (LXC).</p>
<p>Most of the latest Catalyst routers, switches, and access points support what Cisco calls Application Hosting - which is a fancy way of just saying it can run containers on-box. Another acronym to be aware of is IOx - which is what Cisco calls their IOS-XE/Linux appliction framework.</p>
<p>Guestshell is a specific feature within IOS-XE, where a dedicated pre-built container is enabled for on-box access to a Linux shell.</p>
<h2 id="what-hardware-supports-guestshell">What Hardware Supports Guestshell?</h2>
<p>Okay - so for this example, I&rsquo;ll be using the new Catalyst 8000V platform (a virtual Catalyst router). I do also have a Catalyst 9200L on hand, but unfortunately it&rsquo;s one of the few Catalyst platforms that <em>does not</em> support Guestshell (due to lower-end HW specs).</p>
<p>You can also use the Catalyst 8000V to test this in a lab, or you could use most of the Catalyst 8000 / 9000 series platforms if you had them available (or the CSRv &amp; some ISR routers!).</p>
<p>You can find a list of supported platforms &amp; other hardware requirements <a href="https://www.cisco.com/c/en/us/td/docs/ios-xml/ios/prog/configuration/166/b_166_programmability_cg/guest_shell.html#id_45838">here</a>.</p>
<p>All that being said - let&rsquo;s get started!</p>
<h2 id="enabling-iox">Enabling IOx</h2>
<p>So before we can actually use the guestshell feature, there is some pre-configuration to be done.</p>
<p>First we&rsquo;ll need to ensure that the IOx application framework is enabled &amp; running. We can check this by using the <strong>show iox</strong> command:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">0x8KV01# show iox
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">IOx Infrastructure Summary:
</span></span><span class="line"><span class="cl">---------------------------
</span></span><span class="line"><span class="cl">IOx service (CAF)              : Not Running
</span></span><span class="line"><span class="cl">IOx service (HA)               : Not Supported
</span></span><span class="line"><span class="cl">IOx service (IOxman)           : Not Running
</span></span><span class="line"><span class="cl">IOx service (Sec storage)      : Not Supported
</span></span><span class="line"><span class="cl">Libvirtd 5.5.0                 : Running
</span></span></code></pre></div><p>And as we see above - most components are listed as &ldquo;Not Running&rdquo; or &ldquo;Not Supported&rdquo;.</p>
<p>So we&rsquo;ll enter config mode, and use the <em>iox</em> command to enable these services:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">0x8KV01# conf t
</span></span><span class="line"><span class="cl">Enter configuration commands, one per line.  End with CNTL/Z.
</span></span><span class="line"><span class="cl">0x8KV01(config)# iox 
</span></span><span class="line"><span class="cl">0x8KV01(config)#
</span></span><span class="line"><span class="cl">0x8KV01(config)# exit
</span></span><span class="line"><span class="cl">0x8KV01# show iox
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">IOx Infrastructure Summary:
</span></span><span class="line"><span class="cl">---------------------------
</span></span><span class="line"><span class="cl">IOx service (CAF)              : Running
</span></span><span class="line"><span class="cl">IOx service (HA)               : Not Supported
</span></span><span class="line"><span class="cl">IOx service (IOxman)           : Running
</span></span><span class="line"><span class="cl">IOx service (Sec storage)      : Not Supported
</span></span><span class="line"><span class="cl">Libvirtd 5.5.0                 : Running
</span></span></code></pre></div><h2 id="configuring-guestshell-networking">Configuring Guestshell Networking</h2>
<p>There&rsquo;s a good chance we&rsquo;ll want our container to have access to the outside world, so let&rsquo;s take a quick look at the network configuration.</p>
<p><strong>Note: The networking config syntax will differ if you are using a Catalyst router or a Catalyst switch. Since I am using a Catalyst 8000V - we will look at the config for a Catalyst router first.</strong></p>
<p>In order to allow external access to our container, we&rsquo;ll need to configure a gateway interface and NAT translations. Then we can provide a full network configuration to our container:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">## Enable NAT on our external interface - in my case, gigabitEthernet1
</span></span><span class="line"><span class="cl">0x8KV01(config)# interface gigabitEthernet 1
</span></span><span class="line"><span class="cl">0x8KV01(config-if)# ip nat outside
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## Create a virtual interface for the guestshell container
</span></span><span class="line"><span class="cl">0x8KV01(config-if)# int virtualportgroup0
</span></span><span class="line"><span class="cl">0x8KV01(config-if)# ip address 192.168.100.1 255.255.255.0
</span></span><span class="line"><span class="cl">0x8KV01(config-if)# ip nat inside
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## Create an ACL to match traffic from guestshell &amp; Enable NAT
</span></span><span class="line"><span class="cl">0x8KV01(config)# ip access-list standard IOX_NAT
</span></span><span class="line"><span class="cl">0x8KV01(config-std-nacl)# permit 192.168.100.0 0.0.0.255
</span></span><span class="line"><span class="cl">0x8KV01(config)# ip nat inside source list IOX_NAT interface gigabitEthernet 1 overload
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## Assign network configuration to our guestshell container
</span></span><span class="line"><span class="cl">0x8KV01(config)#app-hosting appid guestshell
</span></span><span class="line"><span class="cl">0x8KV01(config-app-hosting)# app-vnic gateway0 virtualportgroup 0 guest-interface 0
</span></span><span class="line"><span class="cl">0x8KV01(config-app-hosting-gateway0)# guest-ipaddress 192.168.100.5 netmask 255.255.255.0
</span></span><span class="line"><span class="cl">0x8KV01(config-app-hosting-gateway0)# app-default-gateway 192.168.100.1 guest-interface 0
</span></span><span class="line"><span class="cl">0x8KV01(config-app-hosting)# name-server0 8.8.8.8
</span></span></code></pre></div><p>Now, as I mentioned before - this config will look different if you&rsquo;re using a switch vs a router.</p>
<p>So let&rsquo;s take a quick look at what this would look like, if we were on a Catalyst Switch instead:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Cat9k(config)# interface AppGigabitEthernet 1/0/1
</span></span><span class="line"><span class="cl">Cat9k(config-if)# switchport mode trunk
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Cat9k(config)# app-hosting appid name
</span></span><span class="line"><span class="cl">Cat9k(config-app-hosting)# app-vnic AppGigabitEthernet trunk
</span></span><span class="line"><span class="cl">Cat9k(config-app-hosting-trunk)# vlan 10 guest-interface 0
</span></span><span class="line"><span class="cl">Cat9k(config-app-hosting-vlan-access-ip))# guest-ipaddress 192.168.100.1 netmask 255.255.255.0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Cat9k(config-app-hosting)# app-default-gateway 192.168.100.1 guest-interface 0
</span></span><span class="line"><span class="cl">Cat9k(config)# name-server 8.8.8.8 
</span></span></code></pre></div><p>Once all that config is done - Let&rsquo;s move on to getting our container started!!</p>
<h2 id="managing-the-guestshell-process">Managing the Guestshell Process</h2>
<p>In order to start a container on our Catalyst device, we&rsquo;ll need to go through a process of deploying the app, activating it, then starting it.</p>
<p>For guestshell, there is a shortcut command that will perform all of these steps for you - and make managing the guestshell process much easier.</p>
<p>So let&rsquo;s go ahead and spin up our guestshell container with the command <strong>guestshell enable</strong>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">0x8KV01# guestshell enable
</span></span><span class="line"><span class="cl">Interface will be selected if configured in app-hosting
</span></span><span class="line"><span class="cl">Please wait for completion
</span></span><span class="line"><span class="cl">guestshell installed successfully
</span></span><span class="line"><span class="cl">Current state is: DEPLOYED
</span></span><span class="line"><span class="cl">guestshell activated successfully
</span></span><span class="line"><span class="cl">Current state is: ACTIVATED
</span></span><span class="line"><span class="cl">guestshell started successfully
</span></span><span class="line"><span class="cl">Current state is: RUNNING
</span></span><span class="line"><span class="cl">Guestshell enabled successfully
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">0x8KV01# show app-hosting list
</span></span><span class="line"><span class="cl">App id                                   State
</span></span><span class="line"><span class="cl">---------------------------------------------------------
</span></span><span class="line"><span class="cl">guestshell                               RUNNING
</span></span></code></pre></div><p>And as you can see above, everything gets automatically deployed &amp; started. We can verify using the <strong>show app-hosting list</strong> command to see that our container has been started.</p>
<p>If we need to, we can also stop &amp; deactivate our container using the command <strong>guestshell disable</strong>.</p>
<p>We could also use the commands below if we wanted to perform these steps manually:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">0x8KV01# app-hosting activate appid guestshell
</span></span><span class="line"><span class="cl">0x8KV01# app-hosting start appid guestshell
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">0x8KV01# app-hosting stop appid guestshell
</span></span><span class="line"><span class="cl">0x8KV01# app-hosting deactivate appid guestshell
</span></span></code></pre></div><p>Before we move onto accessing the guestshell interface, I also wanted to show where you can get more detail about the guestshell container.</p>
<p>Using the <strong>show app-hosting detail appid guestshell</strong> command, we can see details around the container version, current MAC/IP config, and resource consumption:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">0x8KV01# show app-hosting detail appid guestshell
</span></span><span class="line"><span class="cl">App id                 : guestshell
</span></span><span class="line"><span class="cl">Owner                  : iox
</span></span><span class="line"><span class="cl">State                  : RUNNING
</span></span><span class="line"><span class="cl">Application
</span></span><span class="line"><span class="cl">  Type                 : lxc
</span></span><span class="line"><span class="cl">  Name                 : GuestShell
</span></span><span class="line"><span class="cl">  Version              : 3.2.0
</span></span><span class="line"><span class="cl">  Description          : Cisco Systems Guest Shell XE for x86_64
</span></span><span class="line"><span class="cl">  Path                 : /guestshell/:guestshell.tar
</span></span><span class="line"><span class="cl">  URL Path             :
</span></span><span class="line"><span class="cl">Activated profile name : custom
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Resource reservation
</span></span><span class="line"><span class="cl">  Memory               : 256 MB
</span></span><span class="line"><span class="cl">  Disk                 : 1 MB
</span></span><span class="line"><span class="cl">  CPU                  : 800 units
</span></span><span class="line"><span class="cl">  CPU-percent          : 3 %
</span></span><span class="line"><span class="cl">  VCPU                 : 1
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Attached devices
</span></span><span class="line"><span class="cl">  Type              Name               Alias
</span></span><span class="line"><span class="cl">  ---------------------------------------------
</span></span><span class="line"><span class="cl">  serial/shell     iox_console_shell   serial0
</span></span><span class="line"><span class="cl">  serial/aux       iox_console_aux     serial1
</span></span><span class="line"><span class="cl">  serial/syslog    iox_syslog          serial2
</span></span><span class="line"><span class="cl">  serial/trace     iox_trace           serial3
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Network interfaces
</span></span><span class="line"><span class="cl">   ---------------------------------------
</span></span><span class="line"><span class="cl">eth0:
</span></span><span class="line"><span class="cl">   MAC address         : 52:54:dd:d:bf:da
</span></span><span class="line"><span class="cl">   IPv4 address        : 192.168.100.5
</span></span><span class="line"><span class="cl">   IPv6 address        : ::
</span></span><span class="line"><span class="cl">   Network name        : VPG0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Port forwarding
</span></span><span class="line"><span class="cl">  Table-entry  Service  Source-port  Destination-port
</span></span><span class="line"><span class="cl">  ---------------------------------------------------
</span></span></code></pre></div><h2 id="accessing-the-guestshell-cli">Accessing the Guestshell CLI</h2>
<p>Now for the fun part! Let&rsquo;s get into our guestshell container.</p>
<p>We&rsquo;ll just need to use the <strong>guestshell</strong> command, and we&rsquo;ll be dropped into the Linux shell:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">0x8KV01# guestshell
</span></span><span class="line"><span class="cl">[guestshell@guestshell ~]$
</span></span><span class="line"><span class="cl">[guestshell@guestshell ~]$ uname -a
</span></span><span class="line"><span class="cl">Linux guestshell 5.4.40 #1 SMP Tue Oct 6 17:57:00 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
</span></span></code></pre></div><p>Let&rsquo;s also make sure our network &amp; NAT configuration worked:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">[guestshell@guestshell ~]$ ping 8.8.8.8
</span></span><span class="line"><span class="cl">PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
</span></span><span class="line"><span class="cl">64 bytes from 8.8.8.8: icmp_seq=1 ttl=64 time=3.57 ms
</span></span><span class="line"><span class="cl">64 bytes from 8.8.8.8: icmp_seq=2 ttl=64 time=2.64 ms
</span></span><span class="line"><span class="cl">64 bytes from 8.8.8.8: icmp_seq=3 ttl=64 time=2.54 ms
</span></span><span class="line"><span class="cl">^C
</span></span><span class="line"><span class="cl">--- 8.8.8.8 ping statistics ---
</span></span><span class="line"><span class="cl">3 packets transmitted, 3 received, 0% packet loss, time 4ms
</span></span><span class="line"><span class="cl">rtt min/avg/max/mdev = 2.543/2.916/3.567/0.465 ms
</span></span></code></pre></div><p>If needed, we can also run commands on our Catalyst device CLI without leaving the guestshell interface by using the <strong>dohost</strong> utility:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">[guestshell@guestshell ~]$ dohost &#34;show app-hosting list&#34;
</span></span><span class="line"><span class="cl">App id                                   State
</span></span><span class="line"><span class="cl">---------------------------------------------------------
</span></span><span class="line"><span class="cl">guestshell                               RUNNING
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">[guestshell@guestshell ~]$
</span></span></code></pre></div><p>Just a note, the reverse is also possible. Using the <strong>guestshell run</strong> command from our Catalyst CLI, we can run commands in our container:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">0x8KV01# guestshell run uname -a
</span></span><span class="line"><span class="cl">Linux guestshell 5.4.40 #1 SMP Tue Oct 6 17:57:00 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">0x8KV01#
</span></span></code></pre></div><p>It&rsquo;s also worth reiterating, that the guestshell container is a full Linux container (It runs CentOS). So if we need to install additional Linux packages, we can use our Linux package manager (YUM) to do so.</p>
<p>Anyways - Let&rsquo;s wrap this up by looking at the built-in Python modules &amp; capabilities</p>
<h2 id="using-python-in-guestshell">Using Python in Guestshell</h2>
<p>One of the more appealing use cases for guestshell is the ability to run native Python scripts &amp; code directly on your Catalyst device.</p>
<p>A couple of possible use cases could be:</p>
<ul>
<li>Troubleshoot intermittent issues - Automatically collect device state (routes, MAC changes, etc) on a regular basis to log locally or send via email</li>
<li>Automated resolution - If a certain type of state change is observed, we could run a pre-set list of commands to try and automatically fix the issue</li>
<li>Local monitoring / troubleshooting - We could run a series of ping, web page load tests, etc from a python script to help monitor performance from the switch&rsquo;s perspective</li>
<li>Change management / notification - A script could be written to monitor the device configuration for any changes made, then send an automated email when something was detected</li>
<li>Have a risky change that might sever your connection to a remote device? A script could execute the change for you, and if unsuccessful, revert to a working configuration (without doing a <em>reload after</em>)</li>
</ul>
<p>These are just a handful of ideas that came to mind while writing this blog post&hellip; But there are many more possibilities!</p>
<p>Within the guestshell container, there is a special <strong>cli</strong> python module that comes pre-loaded. This module allows us to run CLI commands on our catalyst device directly from a python script &amp; receive the command output for parsing.</p>
<p>Let&rsquo;s look at a quick example using the Python interactive interpreter to save the device configuration:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">[guestshell@guestshell ~]$ python3
</span></span><span class="line"><span class="cl">Python 3.6.8 (default, Aug 24 2020, 17:57:11)
</span></span><span class="line"><span class="cl">[GCC 8.3.1 20191121 (Red Hat 8.3.1-5)] on linux
</span></span><span class="line"><span class="cl">Type &#34;help&#34;, &#34;copyright&#34;, &#34;credits&#34; or &#34;license&#34; for more information.
</span></span><span class="line"><span class="cl">&gt;&gt;&gt; 
</span></span><span class="line"><span class="cl">&gt;&gt;&gt; import cli
</span></span><span class="line"><span class="cl">&gt;&gt;&gt; status = cli.execute(&#34;write memory&#34;)
</span></span><span class="line"><span class="cl">&gt;&gt;&gt; if &#34;OK&#34; in status: print(&#34;Device configuration was saved!&#34;)
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">Device configuration was saved!
</span></span></code></pre></div><p>The example above uses the <strong>cli.execute</strong> Python function, which will run commands in Cisco&rsquo;s exec mode.</p>
<p>What if we wanted to make configuration changes? In that case we would use the <strong>cli.configure</strong> function:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&gt;&gt;&gt; config_data = &#34;&#34;&#34;interface loopback200
</span></span><span class="line"><span class="cl">... ip address 172.16.99.10 255.255.255.0
</span></span><span class="line"><span class="cl">... &#34;&#34;&#34;
</span></span><span class="line"><span class="cl">&gt;&gt;&gt;
</span></span><span class="line"><span class="cl">&gt;&gt;&gt; cli.configure(config_data)
</span></span><span class="line"><span class="cl">&#39;Line 1 SUCCESS:  interface loopback200\nLine 2 SUCCESS:  ip address 172.16.99.10 255.255.255.0\n&#39;
</span></span><span class="line"><span class="cl">&gt;&gt;&gt;
</span></span><span class="line"><span class="cl">&gt;&gt;&gt; cli.execute(&#34;show run interface loopback200&#34;)
</span></span><span class="line"><span class="cl">&#39;Building configuration...\nCurrent configuration : 68 bytes\n!\ninterface Loopback200\n ip address 172.16.99.10 255.255.255.0\nend\n&#39;
</span></span></code></pre></div><p>In the above example, we used a multi-line comment to provide the list of commands to enter. The <strong>cli.configure</strong> function also supports a Python list - so we could provide the commands in that format as well.</p>
<p>You&rsquo;ll notice that the Python interpreter returns a status for each command that we attempted to run. This should allow us to error-check &amp; ensure that all of our intended configuration was accepted.</p>
<p>Lastly, we used the <strong>cli.execute</strong> command again to validate that our running configuration looks correct.</p>
<p>There&rsquo;s a bit more functionality built into the <strong>cli</strong> Python module, but I just wanted to cover some of the common use cases. More details can be found <a href="https://www.cisco.com/c/en/us/td/docs/ios-xml/ios/prog/configuration/1610/b_1610_programmability_cg/python_api.html">here</a></p>
<hr>
<h2 id="a-few-common-questions">A Few Common Questions</h2>
<p><strong>Does this give me access to the underlying IOS-XE Linux shell?</strong></p>
<p>Nope, anything running in guestshell is running in a container - which runs <em>on-top</em> of the IOS-XE image.</p>
<p><strong>What about resource conflicts?</strong></p>
<p>The linux containers running on the IOS-XE platform are given their own dedicated resources. Any resource constraints that affect the containers will not impact normal network functions.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Automating the CLI (Part 2): Building a Web Dashboard with Flask &amp; Bootstrap</title>
      <link>https://0x2142.com/web-dashboard-flask-and-bootstrap/</link>
      <pubDate>Mon, 12 Oct 2020 14:42:58 +0000</pubDate>
      <guid>https://0x2142.com/web-dashboard-flask-and-bootstrap/</guid>
      <description>Building on the last post, we&amp;rsquo;ll show how easy it an be to build a Flask dashboard</description>
      <content:encoded><![CDATA[<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/JGvVSxKTy74?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<p>In the <a href="/automating-the-cli-using-scrapli/">last post</a>, I covered a simple project that I&rsquo;m working on while studying for the Cisco DevNet certifications. This is part two of a series, which will continue on while I work through this project.</p>
<p>As a brief summary - I started using a combination of Scrapli &amp; Cisco Genie for a short network automation script. This script connects to a list of network switches &amp; outputs a spreadsheet of interface statistics. This provides a quick snapshot view into the current port capacity within your network.</p>
<p>In this post, I&rsquo;ll be showing off a bit of how to build a simple web frontend - which I&rsquo;ll put together using <a href="https://flask.palletsprojects.com/en/1.1.x/">Flask</a> and <a href="https://pythonhosted.org/Flask-Bootstrap/">Bootstrap</a>.</p>
<hr>
<h2 id="getting-started-with-flask">Getting Started with Flask</h2>
<p>Okay so I have a bad habit of making a lot of Python scripts which are essentially just command-line utilities.</p>
<p>In theory, this isn&rsquo;t necessarily problem. But what if I want to share some of the tools I build? Maybe not everyone wants to compile config files &amp; figure out what command-line arguments are needed. For some, they may prefer to have an easy-to-use web interface instead.</p>
<p>So for this project I opted to venture a little outside my typical comfort zone &amp; build a web dashboard. I&rsquo;ll admit I&rsquo;ve done this once or twice before, but certainly not something I would claim proficiency in!</p>
<p>The good thing is - building a web interface doesn&rsquo;t have to be complex. My intent with this post is to try &amp; break it down a bit - and hopefully encourage you to try it yourself!</p>
<h3 id="installing-flask--hello-world">Installing Flask &amp; Hello World</h3>
<p>Installing Flask is much like any other python module - we&rsquo;ll use pip:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install flask
</span></span></code></pre></div><p>Once installed, we can import the module into our python script:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">Flask</span>
</span></span></code></pre></div><p>Simple enough!</p>
<p>Now let&rsquo;s look at how easy it is to set up the web server &amp; return a simple web page:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="vm">__name__</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@app.route</span><span class="p">(</span><span class="s1">&#39;/&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="s2">&#34;Hello there!&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">&#39;__main__&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">app</span><span class="o">.</span><span class="n">run</span><span class="p">()</span>
</span></span></code></pre></div><p>First we create a new instance of Flask, using the syntax <em>app = Flask(<strong>name</strong>)</em>.</p>
<p>Next, we just create a quick python function to return the text <strong>Hello there!</strong> You&rsquo;ll notice that there is a decorator above the function - this is used by Flask to route incoming HTTP requests.</p>
<p>In this example, we use <em>@app.route(&rsquo;/&rsquo;)</em>. This tells Flask to register this python function for any HTTP calls to <em>http://&lt;url&gt;/</em>. If we wanted a function for any HTTP requests to <em>http://&lt;url&gt;/networking</em>, we would modify the decorator to <em>@app.route(&rsquo;/networking&rsquo;)</em>.</p>
<p>Lastly, we need to start the Flask web server when the script is run. This is accomplished using <em>app.run()</em></p>
<p>Let&rsquo;s go ahead and try to run the script - which should start the Flask server:</p>
<p><img alt="flask-runoutput" loading="lazy" src="/content/images/2020/10/flask-runoutput.PNG#center"></p>
<p>In the above screenshot, you&rsquo;ll see that by default Flask will start on <a href="http://127.0.0.1:5000">http://127.0.0.1:5000</a>. You may also notice that Flask will print out real-time logging of incoming HTTP requests.</p>
<p>If we visit our page, we&rsquo;ll see pretty much what we might expect:</p>
<p><img alt="Hello-world" loading="lazy" src="/content/images/2020/10/Hello-world.PNG#center"></p>
<p>Before we move onto the next section, I did want to show one more example.</p>
<p>Let&rsquo;s say that we have a python variable (or dictionary, etc). Could we pass that as part of our web page function?</p>
<p>Let&rsquo;s look at this modified example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">web_page_text</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;Hello&#34;</span><span class="p">:</span> <span class="s2">&#34;There!&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@app.route</span><span class="p">(</span><span class="s1">&#39;/&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">web_page_text</span>
</span></span></code></pre></div><p>If we reload our local web page, we&rsquo;ll now see the following:</p>
<p><img alt="hello-world-2" loading="lazy" src="/content/images/2020/10/hello-world-2.PNG#center"></p>
<p>Keep this in mind - We&rsquo;ll use this later!</p>
<h3 id="making-life-easier-with-html-templates">Making life easier with HTML templates</h3>
<p>Now then - you could build all of your HTML in python, then return the entire HTML page as a response to the function call. That being said, just because you can doesn&rsquo;t mean you should 🙃</p>
<p>Let&rsquo;s take a quick look at how we can use HTML templates to help build our web page.</p>
<p>First we&rsquo;ll create a new directory called <em>templates</em>, and create an HTML file. In this case, I named my template <em>main.html</em>:</p>
<p><img alt="dir" loading="lazy" src="/content/images/2020/10/dir.PNG#center"></p>
<p>To start with, we&rsquo;ll just add some simple text into the HTML file:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    Hello There - From the template!
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>On the Flask side, we&rsquo;ll have to modify our imports to include the <em>render_template</em> module. We&rsquo;ll also update what we&rsquo;re returning when an HTTP request is received:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">Flask</span><span class="p">,</span> <span class="n">render_template</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="vm">__name__</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@app.route</span><span class="p">(</span><span class="s1">&#39;/&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">render_template</span><span class="p">(</span><span class="s1">&#39;main.html&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">&#39;__main__&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">app</span><span class="o">.</span><span class="n">run</span><span class="p">()</span>
</span></span></code></pre></div><p>So now instead of returning plain text, we&rsquo;ll return <em>render_template(&lsquo;main.html&rsquo;)</em>.</p>
<p>Flask uses Jinja2 for templating, which we&rsquo;ll get into shortly. The above function call tells our app to use <em>main.html</em> as a base template for the content that is returned to the end user.</p>
<p>Let&rsquo;s check out the page:</p>
<p><img alt="hello-world-3" loading="lazy" src="/content/images/2020/10/hello-world-3.PNG#center"></p>
<p>Awesome - we receive our HTML file as a response this time.</p>
<h3 id="extending-functionality-with-jinja2">Extending functionality with Jinja2</h3>
<p>Now we can start getting into the fun stuff.</p>
<p>Manually writing out a bunch of HTML is fun and all - but what if we could auto-generate everything programmatically?</p>
<p>Jinja2 allows for exactly that. We&rsquo;ll be able to insert variables, if/then statements, and looping logic directly into our HTML template.</p>
<p>As an example, let&rsquo;s say we had the following python dictionary - which we wanted to display on a web page:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">switchList</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;C9200&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;serial&#34;</span><span class="p">:</span> <span class="s2">&#34;ABCD00010&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;ip&#34;</span><span class="p">:</span> <span class="s2">&#34;192.168.1.1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;check&#34;</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;total&#34;</span><span class="p">:</span> <span class="mi">28</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;up&#34;</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;down&#34;</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;disabled&#34;</span><span class="p">:</span> <span class="mi">13</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;capacity&#34;</span><span class="p">:</span> <span class="mi">35</span>       
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;C9300&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;serial&#34;</span><span class="p">:</span> <span class="s2">&#34;ABCD00011&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;ip&#34;</span><span class="p">:</span> <span class="s2">&#34;172.168.44.2&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;check&#34;</span><span class="p">:</span> <span class="kc">False</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;total&#34;</span><span class="p">:</span> <span class="mi">48</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;up&#34;</span><span class="p">:</span> <span class="mi">32</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;down&#34;</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;disabled&#34;</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span>  
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;capacity&#34;</span><span class="p">:</span> <span class="mi">67</span>       
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;C9400&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;serial&#34;</span><span class="p">:</span> <span class="s2">&#34;ABCD00012&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;ip&#34;</span><span class="p">:</span> <span class="s2">&#34;172.33.44.2&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;check&#34;</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;total&#34;</span><span class="p">:</span> <span class="mi">48</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;up&#34;</span><span class="p">:</span> <span class="mi">40</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;down&#34;</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;disabled&#34;</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>  
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;capacity&#34;</span><span class="p">:</span> <span class="mi">82</span>       
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span></code></pre></div><p>This dictionary is an example from the scrapli script I&rsquo;ve been working on (more detail in my <a href="/automating-the-cli-using-scrapli/">last post</a>).</p>
<p>One way for us to present this using an HTML template would be the following:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    {% block content %}
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    {% for switch in switches %}
</span></span><span class="line"><span class="cl">    Switch Name: {{ switch.name }} 
</span></span><span class="line"><span class="cl">      - Serial: {{ switch.serial }} 
</span></span><span class="line"><span class="cl">      - Reachable at: {{ switch.ip }} 
</span></span><span class="line"><span class="cl">      
</span></span><span class="line"><span class="cl">    {% endfor %}
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    {% endblock %}
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>As you can see - that&rsquo;s a bit of a change from what we were using in our template before.</p>
<p>The Jinja2 syntax above uses a combination of curly braces &amp; percentage signs to indicate which parts of our template are logic that should be processed before returning content to the user. Variables are represented with double curly braces.</p>
<p>Once our template receives the <em>switchList</em> dictionary, we can iterate through it similar to how we might in python. Using the <em>{% for switch in switches %}</em> syntax, we can iterate through each item - and pull out relevant data.</p>
<p>Within our for loop, we can reference the individual values of each item in the dictionary. Using the double braces, we can insert a placeholder for where a piece of content should be inserted. For example, in the above template we have a spot where we want to insert the name of the switch. We use <em>{{ switch.name }}</em> as a placeholder, where the value from the dictionary will be inserted once our template is processed.</p>
<p>Okay - so before we test this, we have one minor modification to our python function:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nd">@app.route</span><span class="p">(</span><span class="s1">&#39;/&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">render_template</span><span class="p">(</span><span class="s1">&#39;main.html&#39;</span><span class="p">,</span> <span class="n">switches</span><span class="o">=</span><span class="n">switchList</span><span class="p">)</span>
</span></span></code></pre></div><p>We&rsquo;ve changed our function <em>render_template</em>, but also include the python object that we&rsquo;ll be passing to our template.</p>
<p>Let&rsquo;s test this out:</p>
<p><img alt="switchdict" loading="lazy" src="/content/images/2020/10/switchdict.PNG#center"></p>
<p>Now there&rsquo;s some magic! Using a combination of python &amp; our Jinja2 templates - we can drastically reduce the amount of HTML code that we need to write.</p>
<h3 id="okay-but-thats-not-pretty">Okay, but that&rsquo;s not pretty</h3>
<p>Yes I know. But we needed to get the basics done first!</p>
<p>Here&rsquo;s where Bootstrap comes in.</p>
<p>I went out to Google and searched for good CSS templates. I&rsquo;m not a web developer, nor am I good at design - so I&rsquo;ll leave that work to someone else.</p>
<p>In my searching, I found quite a bunch of free templates&hellip; but I fell in love with one called Lux by <a href="https://bootswatch.com/">Bootswatch</a>.</p>
<p>They provide a great <a href="https://bootswatch.com/lux/">sample page</a> for the template, which shows off different variations. But the best part is that it also includes code samples!</p>
<p>After we pick &amp; download our CSS template, we&rsquo;ll place it in a new directory named <em>static</em>:</p>
<p><img alt="dir2" loading="lazy" src="/content/images/2020/10/dir2.PNG#center"></p>
<p>To get started, we&rsquo;ll need to install one additional python module:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install flask_bootstrap
</span></span></code></pre></div><p>Then we&rsquo;ll also need a quick modification to our base Flask app:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">flask_bootstrap</span> <span class="kn">import</span> <span class="n">Bootstrap</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">...</span> <span class="n">code</span> <span class="n">omitted</span> <span class="o">...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">&#39;__main__&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">Bootstrap</span><span class="p">(</span><span class="n">app</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">app</span><span class="o">.</span><span class="n">run</span><span class="p">()</span>
</span></span></code></pre></div><p>I removed some code to focus on just the two parts that change.</p>
<p>First, we need to import our new module. Second, we need to inject our bootstrap plugin into the Flask app. We do this with <em>Bootstrap(app)</em> prior to <em>app.run()</em>.</p>
<p>Let&rsquo;s move back over to our HTML template, since this is where the most of our changes will be.</p>
<p>First, we&rsquo;ll need to include a few things to make sure our CSS file is loaded:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl">{%- extends &#34;bootstrap/base.html&#34; %}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    {% block styles %}
</span></span><span class="line"><span class="cl">    {{super()}}
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">link</span> <span class="na">rel</span><span class="o">=</span><span class="s">&#34;stylesheet&#34;</span>
</span></span><span class="line"><span class="cl">            <span class="na">href</span><span class="o">=</span><span class="s">&#34;{{url_for(&#39;static&#39;, filename=&#39;bootstrap.css&#39;)}}&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    {% endblock %}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">head</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>I won&rsquo;t dive in too deep on this piece, as it&rsquo;s fairly straightforward. First, we&rsquo;ll need to include the base/default bootstrap template. Next, we&rsquo;ll include a reference to where our CSS file is located &amp; it&rsquo;s name - so that it gets loaded when our page is rendered.</p>
<p>Okay, now for the body of the page:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    {% block content %}
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">table</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;table table-hover&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">&lt;</span><span class="nt">thead</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">          <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="p">&lt;</span><span class="nt">th</span> <span class="na">scope</span><span class="o">=</span><span class="s">&#34;col&#34;</span><span class="p">&gt;</span>Name<span class="p">&lt;/</span><span class="nt">th</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="p">&lt;</span><span class="nt">th</span> <span class="na">scope</span><span class="o">=</span><span class="s">&#34;col&#34;</span><span class="p">&gt;</span>Current Capacity<span class="p">&lt;/</span><span class="nt">th</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">          <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">&lt;/</span><span class="nt">thead</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">&lt;</span><span class="nt">tbody</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">          {% for switch in switches %}
</span></span><span class="line"><span class="cl">            {% if switch.capacity &gt; 75 %}
</span></span><span class="line"><span class="cl">                <span class="p">&lt;</span><span class="nt">tr</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;table-danger&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">            {% else %}
</span></span><span class="line"><span class="cl">                <span class="p">&lt;</span><span class="nt">tr</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;table-success&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">            {% endif %}
</span></span><span class="line"><span class="cl">            <span class="p">&lt;</span><span class="nt">th</span> <span class="na">scope</span><span class="o">=</span><span class="s">&#34;row&#34;</span><span class="p">&gt;</span>{{ switch.name }}<span class="p">&lt;/</span><span class="nt">th</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>{{ switch.capacity }}%<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">          <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">          {% endfor %}
</span></span><span class="line"><span class="cl">        <span class="p">&lt;/</span><span class="nt">tbody</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;/</span><span class="nt">table</span><span class="p">&gt;</span> 
</span></span><span class="line"><span class="cl">    {% endblock %}
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>Now instead of just returning a plain-text list of all three switches, we&rsquo;ll use our CSS template to generate a colorful table. For the purpose of this example, I kept the data to just showing the switch name &amp; it&rsquo;s current capacity.</p>
<p>Most of the template is fairly simple. First we build our table header using the <em>&lt;thead&gt;</em> tags. Next we&rsquo;ll build the rows of our table using <em>&lt;tbody&gt;</em> and <em>&lt;tr&gt;</em>.</p>
<p>For the Jinja2 logic - We&rsquo;ll iterate through our list of switches, and add a new table row (<em>&lt;tr&gt;</em>) for each device. I also added logic to check the current capacity of the switch. If the switch capacity is greater than 75%, we create the table row using <em>&lt;tr class=&ldquo;table-danger&rdquo;&gt;</em> - which is a reference in our CSS stylesheet that will color the row red. If capacity is less than 75%, we use <em>table-success</em> for a green color.</p>
<p>Let&rsquo;s go ahead and check out what this looks like:</p>
<p><img alt="table" loading="lazy" src="/content/images/2020/10/table.PNG#center"></p>
<p>And as we expect - we get our nicely formatted table &amp; the correct colors on each row.</p>
<h2 id="bringing-it-all-together">Bringing it all together</h2>
<p>Now that we&rsquo;ve gotten some background on how to use Flask &amp; Bootstrap, let me show you a bit of what I&rsquo;ve been working on.</p>
<p>I won&rsquo;t cover much code in this section, but you can check out the <a href="https://github.com/0x2142/switchport-web-dashboard">GitHub repo</a> if you&rsquo;re interested in seeing the details.</p>
<p>The scrapli script that I wrote previously has been extended a little. The biggest change is that instead of writing its output to a spreadsheet, it will insert the data into a sqlite database.</p>
<p>The web frontend has been assembled using the same basic ideas covered in this blog (plus a lot of banging my head against the desk, trying to figure out why things don&rsquo;t format or align properly 🙂). Whenever a request is made to the web dashboard, it will query the sqlite database for the requested information.</p>
<p>So, without further delay - here it is!</p>
<p><img alt="dashboard-01" loading="lazy" src="/content/images/2020/10/dashboard-01.PNG#center"></p>
<p>I currently have one physical device running, plus a handful of virtual nexus 9k &amp; CSRv devices running in Cisco Modeling Labs. So all of the data shown in this dashboard is being pulled from real(ish) equipment - no mock data.</p>
<p>This was one of those projects where I started off thinking my dashboard was going to be really simple&hellip; but then I kept having new ideas and getting carried away with the design &amp; functionality.</p>
<p>The table above shows each switch, it&rsquo;s serial number, management IP, current software version, and the port inventory. Similar to the example earlier in this post, I have a small progress bar that shows switch capacity - and will change color depending on percentage of in-use ports.</p>
<p>If you click on any of the switch names, you&rsquo;ll be taken to a page with additional detail on that particular device:</p>
<p><img alt="dashboard-02" loading="lazy" src="/content/images/2020/10/dashboard-02.PNG#center"></p>
<p>On this first tab, you&rsquo;ll see some quick info - mostly the same that is on the main page of the dashboard. However, I added two additional tabs here - one for a detailed port breakdown &amp; one for a dump of the raw CLI output.</p>
<p>If we click on the port info:</p>
<p><img alt="dashboard-03" loading="lazy" src="/content/images/2020/10/dashboard-03.PNG#center"></p>
<p>We have a breakdown of how many ports are currently in use vs down, as well as our breakdown of port media &amp; operational speed. Since I account for switches that are anywhere from 10M up to 100G, the data for operational speeds will only show the speeds that are actually in-use on the switch.</p>
<p>Just in case it&rsquo;s needed, I added a tab to show the raw CLI output from the switch:</p>
<p><img alt="dashboard-04" loading="lazy" src="/content/images/2020/10/dashboard-04.PNG#center"></p>
<p>This could be a quick way to check port counters &amp; errors, or any other information that isn&rsquo;t already presented via the dashboard.</p>
<p>Lastly, I built a separate page to provide statistics on ALL devices in the network:</p>
<p><img alt="dashboard-05" loading="lazy" src="/content/images/2020/10/dashboard-05.PNG#center"></p>
<p>This page will show port count &amp; availability network-wide. However, I also added some spaces to show the top 5 hardware models &amp; software versions that are in-use across the network.</p>
<hr>
<p>I went into this project a bit worried because frontend development isn&rsquo;t my strength. That being said, I really enjoyed working on this project. It was a different challenge than I&rsquo;m normally used to - and it gave me a creative way to try and format &amp; display the data I am collecting.</p>
<p>I hope this post was useful to you. Please leave a comment below - and check out my <a href="https://github.com/0x2142/switchport-web-dashboard">Github repo</a> for the full code.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Automating the CLI: Using Scrapli &amp; Cisco Genie to Collect Interface Data</title>
      <link>https://0x2142.com/automating-the-cli-using-scrapli/</link>
      <pubDate>Tue, 08 Sep 2020 13:29:25 +0000</pubDate>
      <guid>https://0x2142.com/automating-the-cli-using-scrapli/</guid>
      <description>Let&amp;rsquo;s look at one way to automate routine CLI-based tasks using Scrapli &amp;amp; Cisco Genie</description>
      <content:encoded><![CDATA[<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/79WitHmGW9I?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<p>So I just scheduled an upcoming attempt at the <strong>Developing Applications using Cisco Core Platforms and APIs (DEVCOR 350-901)</strong> exam.</p>
<p>It&rsquo;s coming up quick in two weeks here &amp; I&rsquo;m starting to do some review / final prep before taking the exam.</p>
<p>Why does this matter? Well - I plan on doing a few write-ups &amp; videos on some of the content I&rsquo;m studying. There are a lot of people diving into the DevNet exams, and I want to do what I can to help other people succeed.</p>
<p>All that being said, check back here over the next few weeks - or subscribe on <a href="https://www.youtube.com/channel/UCTY24K3UQrc6xHFcLfGg1hg">YouTube</a> - to see the additional content that will follow. While this specific blog doesn&rsquo;t cover much from the exam blueprint, it&rsquo;s the beginning a simple project I&rsquo;ll be using in the further content.</p>
<hr>
<h2 id="getting-started-with-scrapli">Getting Started with Scrapli</h2>
<p>For this project I opted to start with scrapli, and possibly migrate to using RESTCONF later on in the process. Mostly this was an excuse for me to try out scrapli, as I&rsquo;ve heard a few people using it recently &amp; was interested.</p>
<p><a href="https://github.com/carlmontanari/scrapli">Scrapli</a> is a screen scraping module for Python. If you&rsquo;re not familiar with screen scraping, it&rsquo;s the process of connecting to something via telnet/ssh/etc, then literally scraping or dumping the contents of the screen. There are other modules that do this as well, like paramiko, netmiko, or expect scripting.</p>
<p>In networking, too many of our devices still rely on CLI and don&rsquo;t have proper API endpoints. We&rsquo;re working on it, but yet still a ways from having it everywhere. So in the meantime, we still need screen scraping for automation.</p>
<p>On the whole, this isn&rsquo;t necessarily a bad thing. For example, most network engineers are very familiar and competent with the CLI of any network operating system. It&rsquo;s easier to jump into the world of automation if you can start with something you know, the CLI. It&rsquo;s harder to force someone to start their automation journey by giving up everything they know and shoving REST APIs at them.</p>
<p>Okay - enough rambling. Let&rsquo;s get scrapli installed and see what it can do.</p>
<h3 id="installing-the-module">Installing the module</h3>
<p>Easiest part of the whole project. Install with pip:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install scrapli
</span></span></code></pre></div><p>Next we&rsquo;ll go ahead &amp; import into our script.</p>
<p>Scrapli supports quite a handful of operating systems (including a few non-Cisco platforms as well!). In the case of my project though, I&rsquo;m only using Cisco IOS-XE switches - so we&rsquo;ll only import the scrapli driver for that.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">scrapli.driver.core</span> <span class="kn">import</span> <span class="n">IOSXEDriver</span>
</span></span></code></pre></div><h3 id="connecting-to-a-device">Connecting to a Device</h3>
<p>Okay - now that we&rsquo;re all setup with the module, it&rsquo;s time to get working!</p>
<p>First thing, we need to authenticate to our device. Building off of the example code from the scrapli page - we&rsquo;ll create a short dictionary of authentication parameters first:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">switch</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;host&#34;</span><span class="p">:</span> <span class="s2">&#34;10.1.1.1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;auth_username&#34;</span><span class="p">:</span> <span class="s2">&#34;net_api&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;auth_password&#34;</span><span class="p">:</span> <span class="s2">&#34;net_api_pass&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;auth_strict_key&#34;</span><span class="p">:</span> <span class="kc">False</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">cli</span> <span class="o">=</span> <span class="n">IOSXEDriver</span><span class="p">(</span><span class="o">**</span><span class="n">switch</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">cli</span><span class="o">.</span><span class="n">open</span><span class="p">()</span>
</span></span></code></pre></div><p>Once we build out dictionary, we&rsquo;ll use <strong>**switch</strong> to unpack our key/value pairs into the IOSXEDriver object &amp; assign it to a variable called <em>cli</em>. Then, all we need to do is call <em>cli.open()</em> and scrapli will open a connection to our target device.</p>
<p>Now we can run any command we want by calling <em>cli.send_command()</em>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">sh_int</span> <span class="o">=</span> <span class="n">cli</span><span class="o">.</span><span class="n">send_command</span><span class="p">(</span><span class="s2">&#34;show interface&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">sh_int</span><span class="o">.</span><span class="n">output</span><span class="p">)</span>
</span></span></code></pre></div><p>So in the above example, I want to issue a <em>show interface</em> - then print the output.</p>
<p>What we get is shown below:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">GigabitEthernet1/0/1 is up, line protocol is up (connected)
</span></span><span class="line"><span class="cl">  Hardware is Gigabit Ethernet, address is 78bc.1a81.e101 (bia 78bc.1a81.e101)
</span></span><span class="line"><span class="cl">  Description: Test Port
</span></span><span class="line"><span class="cl">  MTU 1500 bytes, BW 1000000 Kbit/sec, DLY 10 usec,
</span></span><span class="line"><span class="cl">     reliability 255/255, txload 1/255, rxload 1/255
</span></span><span class="line"><span class="cl">  Encapsulation ARPA, loopback not set
</span></span><span class="line"><span class="cl">  Keepalive set (10 sec)
</span></span><span class="line"><span class="cl">  Full-duplex, 1000Mb/s, media type is 10/100/1000BaseTX
</span></span><span class="line"><span class="cl">  input flow-control is off, output flow-control is unsupported
</span></span><span class="line"><span class="cl">  ARP type: ARPA, ARP Timeout 04:00:00
</span></span><span class="line"><span class="cl">  Last input 00:00:11, output 00:00:03, output hang never
</span></span><span class="line"><span class="cl">  Last clearing of &#34;show interface&#34; counters never
</span></span><span class="line"><span class="cl">  Input queue: 0/2000/0/0 (size/max/drops/flushes); Total output drops: 198
</span></span><span class="line"><span class="cl">  Queueing strategy: fifo
</span></span><span class="line"><span class="cl">  Output queue: 0/40 (size/max)
</span></span><span class="line"><span class="cl">  5 minute input rate 9000 bits/sec, 11 packets/sec
</span></span><span class="line"><span class="cl">  5 minute output rate 2066000 bits/sec, 221 packets/sec
</span></span><span class="line"><span class="cl">     2647548 packets input, 371883264 bytes, 0 no buffer
</span></span><span class="line"><span class="cl">     Received 6985 broadcasts (6757 multicasts)
</span></span><span class="line"><span class="cl">     0 runts, 0 giants, 0 throttles
</span></span><span class="line"><span class="cl">     0 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored
</span></span><span class="line"><span class="cl">     0 watchdog, 6757 multicast, 0 pause input
</span></span><span class="line"><span class="cl">     0 input packets with dribble condition detected
</span></span><span class="line"><span class="cl">     50905390 packets output, 60134059101 bytes, 0 underruns
</span></span><span class="line"><span class="cl">     0 output errors, 0 collisions, 2 interface resets
</span></span><span class="line"><span class="cl">     0 unknown protocol drops
</span></span><span class="line"><span class="cl">     0 babbles, 0 late collision, 0 deferred
</span></span><span class="line"><span class="cl">     0 lost carrier, 0 no carrier, 0 pause output
</span></span><span class="line"><span class="cl">     0 output buffer failures, 0 output buffers swapped out
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">[**Output Truncated**]
</span></span></code></pre></div><p>What&rsquo;s that look like? Exactly the same output we would get if we entered <em>show interface</em> on the command line ourselves! (Only Gig1/0/1 shown here to keep this short &amp; clean)</p>
<h3 id="using-cisco-genie-to-parse-cli-output">Using Cisco Genie to Parse CLI Output</h3>
<p>So what if we wanted to pull a list of every interface on the switch? Maybe tally how many ports are connected vs down vs admin disabled? Or even check operational speeds &amp; media types?</p>
<p>Well that&rsquo;s what I&rsquo;m looking to do for this project.</p>
<p>Now at first it may seem like we have to use regular expressions to try &amp; match the content we need from the output. However, there is an easier way!</p>
<p>Cisco Genie is an open source project, and part of the larger <a href="https://developer.cisco.com/pyats/">PyATS</a> automated network testing suite. If you haven&rsquo;t looked at any of that yet, I would highly recommend you check it out.</p>
<p>So what&rsquo;s Genie do? It&rsquo;s a utility that handles all the output parsing for us. There are a ton of pre-existing parsers written, which handle all the hard work so we don&rsquo;t have to.</p>
<p>Best yet - scrapli has native integration into Genie. All we have to do is install one additional component:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install scrapli<span class="o">[</span>genie<span class="o">]</span>
</span></span></code></pre></div><p>And to make use of the power of Genie, we just need to pass our raw output to it.</p>
<p>So our new code will look like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">sh_int</span> <span class="o">=</span> <span class="n">cli</span><span class="o">.</span><span class="n">send_command</span><span class="p">(</span><span class="s2">&#34;show interface&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">sh_int_parsed</span> <span class="o">=</span> <span class="n">sh_int</span><span class="o">.</span><span class="n">genie_parse_output</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">sh_int_parsed</span><span class="p">)</span>
</span></span></code></pre></div><p>That&rsquo;s it. And now, instead of that raw output - we get a native Python dictionary that&rsquo;s ready to consume by our script:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;GigabitEthernet1/0/1&#34;</span><span class="p">:{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;port_channel&#34;</span><span class="p">:{</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;port_channel_member&#34;</span><span class="p">:</span><span class="err">False</span>
</span></span><span class="line"><span class="cl">      <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;enabled&#34;</span><span class="p">:</span><span class="err">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;line_protocol&#34;</span><span class="p">:</span><span class="s2">&#34;up&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;oper_status&#34;</span><span class="p">:</span><span class="s2">&#34;up&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;connected&#34;</span><span class="p">:</span><span class="err">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span><span class="s2">&#34;Gigabit Ethernet&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;mac_address&#34;</span><span class="p">:</span><span class="s2">&#34;78bc.1a81.e101&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;phys_address&#34;</span><span class="p">:</span><span class="s2">&#34;78bc.1a81.e101&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;description&#34;</span><span class="p">:</span><span class="s2">&#34;Test Port&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;delay&#34;</span><span class="p">:</span><span class="mi">10</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;mtu&#34;</span><span class="p">:</span><span class="mi">1500</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;bandwidth&#34;</span><span class="p">:</span><span class="mi">1000000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;reliability&#34;</span><span class="p">:</span><span class="s2">&#34;255/255&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;txload&#34;</span><span class="p">:</span><span class="s2">&#34;1/255&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;rxload&#34;</span><span class="p">:</span><span class="s2">&#34;1/255&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;encapsulations&#34;</span><span class="p">:{</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;encapsulation&#34;</span><span class="p">:</span><span class="s2">&#34;arpa&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;keepalive&#34;</span><span class="p">:</span><span class="mi">10</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;duplex_mode&#34;</span><span class="p">:</span><span class="s2">&#34;full&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;port_speed&#34;</span><span class="p">:</span><span class="s2">&#34;1000mb/s&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;media_type&#34;</span><span class="p">:</span><span class="s2">&#34;10/100/1000BaseTX&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;flow_control&#34;</span><span class="p">:{</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;receive&#34;</span><span class="p">:</span><span class="err">False</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;send&#34;</span><span class="p">:</span><span class="err">False</span>
</span></span><span class="line"><span class="cl">      <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;arp_type&#34;</span><span class="p">:</span><span class="s2">&#34;arpa&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;arp_timeout&#34;</span><span class="p">:</span><span class="s2">&#34;04:00:00&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;last_input&#34;</span><span class="p">:</span><span class="s2">&#34;00:00:05&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;last_output&#34;</span><span class="p">:</span><span class="s2">&#34;00:00:08&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;output_hang&#34;</span><span class="p">:</span><span class="s2">&#34;never&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;queues&#34;</span><span class="p">:{</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;input_queue_size&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;input_queue_max&#34;</span><span class="p">:</span><span class="mi">2000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;input_queue_drops&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;input_queue_flushes&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;total_output_drop&#34;</span><span class="p">:</span><span class="mi">198</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;queue_strategy&#34;</span><span class="p">:</span><span class="s2">&#34;fifo&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;output_queue_size&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;output_queue_max&#34;</span><span class="p">:</span><span class="mi">40</span>
</span></span><span class="line"><span class="cl">      <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;counters&#34;</span><span class="p">:{</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;rate&#34;</span><span class="p">:{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;load_interval&#34;</span><span class="p">:</span><span class="mi">300</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;in_rate&#34;</span><span class="p">:</span><span class="mi">8000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;in_rate_pkts&#34;</span><span class="p">:</span><span class="mi">11</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;out_rate&#34;</span><span class="p">:</span><span class="mi">1868000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;out_rate_pkts&#34;</span><span class="p">:</span><span class="mi">205</span>
</span></span><span class="line"><span class="cl">         <span class="p">},</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;last_clear&#34;</span><span class="p">:</span><span class="s2">&#34;never&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;in_pkts&#34;</span><span class="p">:</span><span class="mi">2658450</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;in_octets&#34;</span><span class="p">:</span><span class="mi">372811780</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;in_no_buffer&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;in_multicast_pkts&#34;</span><span class="p">:</span><span class="mi">6783</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;in_broadcast_pkts&#34;</span><span class="p">:</span><span class="mi">6783</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;in_runts&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;in_giants&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;in_throttles&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;in_errors&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;in_crc_errors&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;in_frame&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;in_overrun&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;in_ignored&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;in_watchdog&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;in_mac_pause_frames&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;in_with_dribble&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;out_pkts&#34;</span><span class="p">:</span><span class="mi">51057453</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;out_octets&#34;</span><span class="p">:</span><span class="mi">60309072263</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;out_underruns&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;out_errors&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;out_interface_resets&#34;</span><span class="p">:</span><span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;out_collision&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;out_unknown_protocl_drops&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;out_babble&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;out_late_collision&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;out_deferred&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;out_lost_carrier&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;out_no_carrier&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;out_mac_pause_frames&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;out_buffer_failure&#34;</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="nt">&#34;out_buffers_swapped&#34;</span><span class="p">:</span><span class="mi">0</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">   <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>This format makes our lives a lot easier. For example, let&rsquo;s say we wanted to determine the operational state of a port. Without the Genie integration, we would need to write some regex to comb through the raw output &amp; find what we needed.</p>
<p>However, with Genie involved - its as easy as this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">sh_int_parsed</span><span class="p">[</span><span class="s1">&#39;GigabitEthernet1/0/1&#39;</span><span class="p">][</span><span class="s1">&#39;oper_status&#39;</span><span class="p">])</span>
</span></span></code></pre></div><h3 id="collecting-the-useful-bits">Collecting the Useful Bits</h3>
<p>Now that we have everything in an easy-to-use format, all we need to do is write a few loops to run through our interface list &amp; collect data.</p>
<p>The purpose of my script is to automate the collection of port utilization. For example, think about performing a switch refresh or some form of capacity planning scenario. You might need to inventory how many switches you have, how many ports are utilized vs how many are available, or count the number of copper vs fiber ports.</p>
<p>For this first iteration, we&rsquo;ll just collect the data then dump it out to a CSV file. As I mentioned earlier, this is just the start of a small project I&rsquo;ll be using to study for the DEVCOR exam - so this will be evolving into a web service later on.</p>
<p>The full script can be found <a href="https://github.com/0x2142/example-scripts/tree/master/scrapli-and-genie-demo">here</a>. But I&rsquo;ve posted just a snippet of how we count the different interface characteristics:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Count all Ethernet interfaces</span>
</span></span><span class="line"><span class="cl"><span class="n">interfaceStats</span><span class="p">[</span><span class="s1">&#39;total&#39;</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Count admin-down interfaces</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="ow">not</span> <span class="n">sh_int_parsed</span><span class="p">[</span><span class="n">iface</span><span class="p">][</span><span class="s1">&#39;enabled&#39;</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="n">interfaceStats</span><span class="p">[</span><span class="s1">&#39;intdisabled&#39;</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Count not connected interfaces</span>
</span></span><span class="line"><span class="cl"><span class="k">elif</span> <span class="n">sh_int_parsed</span><span class="p">[</span><span class="n">iface</span><span class="p">][</span><span class="s1">&#39;enabled&#39;</span><span class="p">]</span> <span class="ow">and</span> <span class="n">sh_int_parsed</span><span class="p">[</span><span class="n">iface</span><span class="p">][</span><span class="s1">&#39;oper_status&#39;</span><span class="p">]</span> <span class="o">==</span> <span class="s1">&#39;down&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">interfaceStats</span><span class="p">[</span><span class="s1">&#39;intdown&#39;</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Count up / connected interfaces - Then collect current speeds</span>
</span></span><span class="line"><span class="cl"><span class="k">elif</span> <span class="n">sh_int_parsed</span><span class="p">[</span><span class="n">iface</span><span class="p">][</span><span class="s1">&#39;enabled&#39;</span><span class="p">]</span> <span class="ow">and</span> <span class="n">sh_int_parsed</span><span class="p">[</span><span class="n">iface</span><span class="p">][</span><span class="s1">&#39;connected&#39;</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="n">interfaceStats</span><span class="p">[</span><span class="s1">&#39;intup&#39;</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">    <span class="n">speed</span> <span class="o">=</span> <span class="n">sh_int_parsed</span><span class="p">[</span><span class="n">iface</span><span class="p">][</span><span class="s1">&#39;bandwidth&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">speed</span> <span class="o">==</span> <span class="mi">10_000</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">interfaceStats</span><span class="p">[</span><span class="s1">&#39;intop10m&#39;</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">speed</span> <span class="o">==</span> <span class="mi">100_000</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">interfaceStats</span><span class="p">[</span><span class="s1">&#39;intop100m&#39;</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">speed</span> <span class="o">==</span> <span class="mi">1_000_000</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">interfaceStats</span><span class="p">[</span><span class="s1">&#39;intop1g&#39;</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">speed</span> <span class="o">==</span> <span class="mi">10_000_000</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">interfaceStats</span><span class="p">[</span><span class="s1">&#39;intop10g&#39;</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Count number of interfaces by media type</span>
</span></span><span class="line"><span class="cl"><span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">media</span> <span class="o">=</span> <span class="n">sh_int_parsed</span><span class="p">[</span><span class="n">iface</span><span class="p">][</span><span class="s1">&#39;media_type&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="s1">&#39;1000BaseTX&#39;</span> <span class="ow">in</span> <span class="n">media</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">interfaceStats</span><span class="p">[</span><span class="s1">&#39;intmedcop&#39;</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">interfaceStats</span><span class="p">[</span><span class="s1">&#39;intmedsfp&#39;</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"><span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">interfaceStats</span><span class="p">[</span><span class="s1">&#39;intmedsfp&#39;</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span></code></pre></div><p>Once all that runs, we create a CSV file to dump all of the data to.</p>
<p>For example, running the complete script against my lab Catalyst 9200 switch would output the following:</p>
<table>
  <thead>
      <tr>
          <th>Hostname</th>
          <th>Model</th>
          <th>Serial Number</th>
          <th>Software Version</th>
          <th>Total Interfaces</th>
          <th>Interfaces UP (Total)</th>
          <th>Interfaces DOWN (Total)</th>
          <th>Interfaces Disabled</th>
          <th>Interface Operational Speed (10M)</th>
          <th>Interface Operational Speed (100M)</th>
          <th>Interface Operational Speed (1G)</th>
          <th>Interface Operational Speed (10G)</th>
          <th>Interface Media (Copper)</th>
          <th>Interface Media (SFP)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SW-C9200</td>
          <td>C9200L-24T-4X</td>
          <td>XX########X</td>
          <td>16.9.4</td>
          <td>28</td>
          <td>7</td>
          <td>8</td>
          <td>13</td>
          <td>0</td>
          <td>1</td>
          <td>6</td>
          <td>0</td>
          <td>24</td>
          <td>4</td>
      </tr>
  </tbody>
</table>
<p>And that&rsquo;s it! Using the combination of scrapli &amp; genie made this script very quick and easy to write - which means now I can focus more time on the next steps.</p>
<hr>
<p>As I&rsquo;ve mentioned a few times, this is hopefully the start of a short series of blog posts &amp; videos as I wrap up studying for the DEVCOR exam.</p>
<p>If you found this content helpful - or you&rsquo;re interested in the future content - please check back soon! Or consider subscribing to my <a href="https://www.youtube.com/channel/UCTY24K3UQrc6xHFcLfGg1hg">YouTube</a> channel, where new content will be coming shortly.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Cisco Firepower - Automating Cellular Failover</title>
      <link>https://0x2142.com/cisco-firepower-automating-cellular-failover/</link>
      <pubDate>Fri, 17 Jul 2020 20:41:09 +0000</pubDate>
      <guid>https://0x2142.com/cisco-firepower-automating-cellular-failover/</guid>
      <description>How I automated internet uplink monitoring &amp;amp; route injection on a Cisco Firepower Firewall</description>
      <content:encoded><![CDATA[<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/Mf5zIt9HFxk?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<p>In a <a href="/how-to-setting-up-google-fi-on-a-netgear-lte-modem/">recent post</a>, I walked through setting up a Netgear LTE cellular modem with Google Fi. Might seem random - but I had a plan for this!</p>
<p>In trying to ensure I keep a stable internet connection for work, I eventually led myself down the path of buying a cellular modem. That was part one. The next step was being able to somewhat-intelligently monitor my home internet connection, and then fail over to the cell modem when connectivity was poor (think lazy SDWAN).</p>
<p>At first I figured this would be easy&hellip;. but of course nothing is :)</p>
<p>I currently have a Cisco FirePower 1010 appliance that I&rsquo;m using for a home firewall. Unfortunately, while this thing does support IP SLA - It wouldn&rsquo;t quite accomplish what I wanted to do. And to be fair, this box is intended to be a <em>security</em> appliance - not SDWAN.</p>
<p>Anyways - I talked myself into just writing some Python automation to monitor my home internet, and dynamically inject/remove static routing entries. I needed a new project anyways</p>
<p><em>For those of you wanting to jump straight to the code, the GitHub repo is <a href="https://github.com/0x2142/fpwr-route-failover">here</a></em></p>
<hr>
<h2 id="part-1---monitoring-packet-loss--latency">Part 1 - Monitoring Packet Loss &amp; Latency</h2>
<p>So first thing - I regularly experience both complete internet outages (always at the worst times), as well as very high latency. Packet loss seems to be less common with my ISP - but hey, it happens sometimes too.</p>
<p>I opted to use the <a href="https://pypi.org/project/pythonping/">pythonping</a> module as an easy way to collect some of the info I needed.</p>
<p>After importing the module, it&rsquo;s as easy as specifying a destination, packet size, and number of ICMP messages to send:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pythonping</span> <span class="kn">import</span> <span class="n">ping</span>
</span></span><span class="line"><span class="cl"><span class="n">result</span> <span class="o">=</span> <span class="n">ping</span><span class="p">(</span><span class="s1">&#39;8.8.8.8&#39;</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">count</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span> <span class="n">verbose</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span></code></pre></div><p>To simplify at least one part of the operation, pythonping has a built in function to return the average round trip:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="o">&gt;&gt;&gt;</span> <span class="nb">print</span><span class="p">(</span><span class="n">result</span><span class="o">.</span><span class="n">rtt_avg_ms</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="mf">74.23</span>
</span></span></code></pre></div><p>The actual contents of our ping results are going to be in the following format:
<code>Reply from 8.8.8.8, 10 bytes in 43.82ms</code></p>
<p>So a quick way for me to figure out packet loss was to simply search for the &ldquo;Reply from&rdquo; text in each response:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">lost</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">packet</span> <span class="ow">in</span> <span class="n">result</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="s2">&#34;Reply from&#34;</span> <span class="ow">in</span> <span class="nb">str</span><span class="p">(</span><span class="n">packet</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">pass</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">lost</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span></code></pre></div><p>Easy enough - and we can use that to calculate the amount of packet loss against the 10 total packets sent:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">lossperc = 100 * lost / 10
</span></span></code></pre></div><p>Once all that is figured out, we can use a simple expression to determine whether or not the loss or latency thresholds have been exceeded:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">if</span> <span class="n">response</span> <span class="o">&gt;=</span> <span class="n">MAX_LATENCY</span> <span class="ow">or</span> <span class="n">loss</span> <span class="o">&gt;=</span> <span class="n">MAX_LOSS</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Loss/Latency violate thresholds.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Loss/Latency within thresholds.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kc">False</span>
</span></span></code></pre></div><p>The code I wrote then takes one of two paths.</p>
<ul>
<li>If the loss and latency is ABOVE our thresholds, call to the FirePower module to inject a static default route over the LTE modem</li>
<li>If the loss and latency is BELOW our thresholds, call to the FirePower module to delete the static default route - which returns traffic to the primary internet</li>
</ul>
<p>These functions within the FirePower module were written so that each change will only be made once. For example, if the script checks and finds that the loss/latency is bad, but the static route towards the LTE modem already exists - then no additional changes are made.</p>
<p>So next - Let&rsquo;s take a look at the FirePower module.</p>
<h2 id="part-2---authenticating-to-fdm--initial-checks">Part 2 - Authenticating to FDM &amp; Initial Checks</h2>
<p>This was my first time getting into the FirePower FDM APIs - but they ended up being fairly straightforward to use.</p>
<p>Turns out that FDM has built-in API documentation, which is extremely helpful. This can be found at <code>https://&lt;FDM IP&gt;/#/api-explorer</code></p>
<p><img alt="firepower-api-explorer" loading="lazy" src="/content/images/2020/07/firepower-api-explorer.PNG#center"></p>
<p>Okay - So first thing&rsquo;s first. Authenticating to the firewall:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">oauth_data</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;grant_type&#34;</span><span class="p">:</span> <span class="s2">&#34;password&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;username&#34;</span><span class="p">:</span> <span class="s2">&#34;admin&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;password&#34;</span><span class="p">:</span> <span class="s2">&#34;Cisco1234!&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="n">headers</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="s2">&#34;Content-Type&#34;</span><span class="p">:</span> <span class="s2">&#34;application/json&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="s2">&#34;Accept&#34;</span><span class="p">:</span> <span class="s2">&#34;application/json&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="n">baseurl</span> <span class="o">=</span> <span class="s2">&#34;https://&#34;</span> <span class="o">+</span> <span class="n">FDM</span> <span class="o">+</span> <span class="s2">&#34;/api/fdm/latest&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">authurl</span> <span class="o">=</span> <span class="n">baseurl</span> <span class="o">+</span> <span class="s2">&#34;/fdm/token&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Posting AUTH request to FDM&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># POST request to FDM with headers / oauth data</span>
</span></span><span class="line"><span class="cl"><span class="n">resp</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">s</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="n">authurl</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">headers</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                   <span class="n">data</span><span class="o">=</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">oauth_data</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                   <span class="n">verify</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># If success - only pull out &amp; return the auth token</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">resp</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Auth success - got token.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">resp</span><span class="o">.</span><span class="n">text</span><span class="p">)[</span><span class="s1">&#39;access_token&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Authentication Failed.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="n">resp</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
</span></span></code></pre></div><p>In the code above - we take the headers and some basic authentication info (username, password, grant type) and send it in an HTTP POST request to <code>https://&lt;FDM IP&gt;/api/fdm/latest/fdm/token</code></p>
<p>This returns an authentication token, which we&rsquo;ll need to include in any further HTTP request to the firewall. This can be done by sending a new set of headers in the future:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">headers</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="s2">&#34;Content-Type&#34;</span><span class="p">:</span> <span class="s2">&#34;application/json&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="s2">&#34;Accept&#34;</span><span class="p">:</span> <span class="s2">&#34;application/json&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="s2">&#34;Authorization&#34;</span><span class="p">:</span> <span class="s2">&#34;Bearer &#34;</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">token</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Next we need to figure out which routing table to insert the route into. Since I am only using the default routing table, the script will just grab the UID for the default global routing table:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">vr_url</span> <span class="o">=</span> <span class="n">baseurl</span> <span class="o">+</span> <span class="s2">&#34;/devices/default/routing/virtualrouters&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">routing_table</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">vr_url</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">headers</span><span class="p">)</span>
</span></span></code></pre></div><p>I mentioned earlier that the script will not make any changes if the firewall is already in the desired state. So we will take time now to check what state the firewall is in, before attempting to make any changes.</p>
<p>This is accomplished by making our first primary action scanning the routing table to see if our route already exists:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">route_url</span> <span class="o">=</span> <span class="n">baseurl</span> <span class="o">+</span> <span class="s2">&#34;/devices/default/routing/virtualrouters/&#34;</span> <span class="o">+</span> \
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">globalVR</span> <span class="o">+</span> <span class="s2">&#34;/staticrouteentries&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">route_data</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">route_url</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">headers</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Convert returned JSON object</span>
</span></span><span class="line"><span class="cl"><span class="n">current_routes</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">route_data</span><span class="p">)[</span><span class="s1">&#39;items&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Run through each route returned and see if it matches the one we&#39;re looking for</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">route</span> <span class="ow">in</span> <span class="n">current_routes</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># For each route, we need to manually go look up the uid of our gateway &amp; network objects</span>
</span></span><span class="line"><span class="cl">    <span class="n">gateway</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">getNetworkObject</span><span class="p">(</span><span class="n">route</span><span class="p">[</span><span class="s1">&#39;gateway&#39;</span><span class="p">][</span><span class="s1">&#39;id&#39;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">    <span class="n">dest_network</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">getNetworkObject</span><span class="p">(</span><span class="n">route</span><span class="p">[</span><span class="s1">&#39;networks&#39;</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="s1">&#39;id&#39;</span><span class="p">])</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s1">&#39;/&#39;</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Match based on route prefix &amp; upstream next hop gateway</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">gateway</span> <span class="o">==</span> <span class="n">GATEWAY</span> <span class="ow">and</span> <span class="n">dest_network</span> <span class="o">==</span> <span class="n">ROUTE</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s1">&#39;/&#39;</span><span class="p">)[</span><span class="mi">0</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Found route to </span><span class="si">%s</span><span class="s2"> via </span><span class="si">%s</span><span class="s2">&#34;</span> <span class="o">%</span> <span class="p">(</span><span class="n">dest_network</span><span class="p">,</span> <span class="n">gateway</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">route</span>
</span></span></code></pre></div><p>Depending on whether we were looking to add or remove a route, the script may proceed with doing so - or just quit if the action has already been taken previously.</p>
<h2 id="part-3---failover-add-static-route">Part 3 - Failover (Add Static Route)</h2>
<p>Assuming that we need to <strong>Add</strong> a route, we need to sent a POST request to <code>https://&lt;FDM IP&gt;/api/fdm/latest/devices/default/routing/virtualrouters/&lt;Virtual Router ID&gt;/staticrouteentries</code> with some required information (like subnet info, next-hop, etc)</p>
<p>Unfortunately, we can&rsquo;t just send a POST with the target subnet &amp; gateway. Those need to be created as network objects on the FirePower box beforehand. For each network object, we need to build a quick JSON object with the required info:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">host_data</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl"><span class="n">host_data</span><span class="p">[</span><span class="s1">&#39;name&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="n">name</span>
</span></span><span class="line"><span class="cl"><span class="n">host_data</span><span class="p">[</span><span class="s1">&#39;description&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&#34;Created by ISP Failover automation&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">host_data</span><span class="p">[</span><span class="s1">&#39;subType&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="n">subtype</span>
</span></span><span class="line"><span class="cl"><span class="n">host_data</span><span class="p">[</span><span class="s1">&#39;value&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="n">address</span>
</span></span><span class="line"><span class="cl"><span class="n">host_data</span><span class="p">[</span><span class="s1">&#39;type&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&#34;networkobject&#34;</span>
</span></span></code></pre></div><p>Then we can send a POST to the FDM API to create the object with our supplied parameters:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">posturl</span> <span class="o">=</span> <span class="n">baseurl</span> <span class="o">+</span> <span class="s2">&#34;/object/networks&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">requests</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="n">posturl</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">headers</span><span class="p">,</span> <span class="n">json</span><span class="o">=</span><span class="n">host_data</span><span class="p">)</span>
</span></span></code></pre></div><p>Once we have those objects created, we can compile all of that info into a JSON object with all the components needed to create a static route entry:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">routeobject</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl"><span class="n">routeobject</span><span class="p">[</span><span class="s1">&#39;name&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&#34;route_BACKUP&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">routeobject</span><span class="p">[</span><span class="s1">&#39;description&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&#34;Created by ISP Failover automation&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">routeobject</span><span class="p">[</span><span class="s1">&#39;iface&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl"><span class="n">routeobject</span><span class="p">[</span><span class="s1">&#39;iface&#39;</span><span class="p">][</span><span class="s1">&#39;id&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="n">iface_ID</span>
</span></span><span class="line"><span class="cl"><span class="n">routeobject</span><span class="p">[</span><span class="s1">&#39;iface&#39;</span><span class="p">][</span><span class="s1">&#39;type&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&#34;physicalinterface&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">routeobject</span><span class="p">[</span><span class="s1">&#39;iface&#39;</span><span class="p">][</span><span class="s1">&#39;name&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="n">iface_name</span>
</span></span><span class="line"><span class="cl"><span class="n">routeobject</span><span class="p">[</span><span class="s1">&#39;networks&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="p">[{}]</span>
</span></span><span class="line"><span class="cl"><span class="n">routeobject</span><span class="p">[</span><span class="s1">&#39;networks&#39;</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl"><span class="n">routeobject</span><span class="p">[</span><span class="s1">&#39;networks&#39;</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="s1">&#39;id&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="n">network_ID</span>
</span></span><span class="line"><span class="cl"><span class="n">routeobject</span><span class="p">[</span><span class="s1">&#39;networks&#39;</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="s1">&#39;type&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&#34;networkobject&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">routeobject</span><span class="p">[</span><span class="s1">&#39;networks&#39;</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="s1">&#39;name&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="n">network_name</span>
</span></span><span class="line"><span class="cl"><span class="n">routeobject</span><span class="p">[</span><span class="s1">&#39;gateway&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl"><span class="n">routeobject</span><span class="p">[</span><span class="s1">&#39;gateway&#39;</span><span class="p">][</span><span class="s1">&#39;id&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="n">gateway_ID</span>
</span></span><span class="line"><span class="cl"><span class="n">routeobject</span><span class="p">[</span><span class="s1">&#39;gateway&#39;</span><span class="p">][</span><span class="s1">&#39;type&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&#34;networkobject&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">routeobject</span><span class="p">[</span><span class="s1">&#39;gateway&#39;</span><span class="p">][</span><span class="s1">&#39;name&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="n">gateway_name</span>
</span></span><span class="line"><span class="cl"><span class="n">routeobject</span><span class="p">[</span><span class="s1">&#39;metricValue&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"><span class="n">routeobject</span><span class="p">[</span><span class="s1">&#39;ipType&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&#34;IPv4&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">routeobject</span><span class="p">[</span><span class="s1">&#39;type&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&#34;staticrouteentry&#34;</span>
</span></span></code></pre></div><p>Then pass all that into a POST request to create the route object:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">add_url</span> <span class="o">=</span> <span class="n">baseurl</span> <span class="o">+</span> <span class="s2">&#34;/devices/default/routing/virtualrouters/&#34;</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">globalVR</span> <span class="o">+</span> <span class="s2">&#34;/staticrouteentries</span>
</span></span><span class="line"><span class="cl"><span class="n">requests</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="n">add_url</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">headers</span><span class="p">,</span> <span class="n">json</span><span class="o">=</span><span class="n">routeobject</span><span class="p">)</span>
</span></span></code></pre></div><p>Success? Well not quite yet.</p>
<p>FirePower requires all changes to be deployed (or applied) to the system before they take effect.</p>
<p>So next we need to initiate a deployment task - and periodically check on it. Deployments can take a <em>while</em> to complete, depending on number of changes, processing power of the appliance, etc.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># First send a POST to the deployment endpoint:</span>
</span></span><span class="line"><span class="cl"><span class="n">deploy_url</span> <span class="o">=</span> <span class="n">baseurl</span> <span class="o">+</span> <span class="s2">&#34;/operational/deploy&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">deploy_response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="n">deploy_url</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">headers</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Grab the deployment task UID:</span>
</span></span><span class="line"><span class="cl"><span class="n">deploymentID</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">deploy_response</span><span class="o">.</span><span class="n">text</span><span class="p">)[</span><span class="s1">&#39;id&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Loop while deployment is running:</span>
</span></span><span class="line"><span class="cl"><span class="k">while</span> <span class="n">deployed</span> <span class="ow">is</span> <span class="kc">False</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Sleep for a few seconds:</span>
</span></span><span class="line"><span class="cl">    <span class="n">sleep</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Get list of all deployment tasks:</span>
</span></span><span class="line"><span class="cl">    <span class="n">tasklist</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">deploy_url</span><span class="p">)</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">task</span> <span class="ow">in</span> <span class="n">taskList</span><span class="p">[</span><span class="s1">&#39;items&#39;</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">task</span><span class="p">[</span><span class="s1">&#39;id&#39;</span><span class="p">]</span> <span class="o">==</span> <span class="n">deploymentID</span> <span class="ow">and</span> <span class="n">task</span><span class="p">[</span><span class="s1">&#39;state&#39;</span><span class="p">]</span> <span class="o">==</span> <span class="s1">&#39;DEPLOYED&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Deployment status is: &#34;</span> <span class="o">+</span> <span class="n">task</span><span class="p">[</span><span class="s1">&#39;state&#39;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">            <span class="n">deployed</span> <span class="o">=</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="n">task</span><span class="p">[</span><span class="s1">&#39;id&#39;</span><span class="p">]</span> <span class="o">==</span> <span class="n">deploymentID</span> <span class="ow">and</span> <span class="n">task</span><span class="p">[</span><span class="s1">&#39;state&#39;</span><span class="p">]</span> <span class="o">!=</span> <span class="s1">&#39;DEPLOYED&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># If changes not yet deployed, check again momentarily</span>
</span></span><span class="line"><span class="cl">            <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Deployment status is: &#34;</span> <span class="o">+</span> <span class="n">task</span><span class="p">[</span><span class="s1">&#39;state&#39;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">            <span class="n">deployed</span> <span class="o">=</span> <span class="kc">False</span>
</span></span></code></pre></div><p>So in the above block, we POST a request to deploy changes. The response of that POST request contains our deployment ID, which we keep to check the status later.</p>
<p>Then the script waits a few seconds, and pulls a list of all deployment tasks. Search through that list for our deployment ID - and check to see if it has been completed yet. If not, wait and run the loop again.</p>
<p>And that&rsquo;s it! Once we get confirmation that our changes have been deployed - we&rsquo;ve now routed traffic over the secondary connection (an LTE modem, in my case).</p>
<h2 id="part-4---fail-back-delete-static-route">Part 4 - Fail-Back (Delete Static Route)</h2>
<p>Okay - this section will be fairly quick. We&rsquo;ve already looked at authenticating, checking if our route already exists, and how to add a new route. So the only thing remaining is removing our route if we wanted to migrate traffic back to the primary connection.</p>
<p>Same logic applies as earlier. If our path monitoring script runs and finds that loss &amp; latency are within the thresholds we set, then we make a call to the firepower module. First we check to ensure there <em>is</em> a route for us to delete, and if so - proceed with deleting it.</p>
<p>Adding a route is a little more work, since we may need to create network objects. However, when we delete the route - we&rsquo;ll just leave those objects on the FirePower box. They&rsquo;ll be there for the next time we need them (which also speeds up deployment times).</p>
<p>To get started, we just need the UID for the route we want to delete.</p>
<p>Remember that code I showed above, where we check to see if the static route exists? And match on our intended subnet / gateway pair? Well, I just made that into a function that will return the UID of the route if it finds it. Easy enough!</p>
<p>Next, we just send an HTTP DELETE to the static routing endpoint - and reference that UID:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">del_url</span> <span class="o">=</span> <span class="n">baseurl</span> <span class="o">+</span> <span class="s2">&#34;/devices/default/routing/virtualrouters/&#34;</span> <span class="o">+</span> \
</span></span><span class="line"><span class="cl">          <span class="bp">self</span><span class="o">.</span><span class="n">globalVR</span> <span class="o">+</span> <span class="s2">&#34;/staticrouteentries/&#34;</span> <span class="o">+</span> <span class="n">route</span><span class="p">[</span><span class="s1">&#39;id&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">requests</span><span class="o">.</span><span class="n">delete</span><span class="p">(</span><span class="n">del_url</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">headers</span><span class="p">)</span>
</span></span></code></pre></div><p>Once that succeeds - we use the same logic as earlier to deploy the changes.</p>
<p>After that, our route is removed &amp; traffic should be flowing over our primary connection again.</p>
<hr>
<p>If it helps anyone - I also threw together a quick diagram to explain the flow of operations here:
<img alt="flow-diagram" loading="lazy" src="/content/images/2020/07/flow-diagram.jpg#center"></p>
<hr>
<p>Well - that&rsquo;s it. This was a fun side project to work on over the past week or two. I hadn&rsquo;t spent much time with the FirePower/FDM APIs just yet. While there was a bit of work in creating all the necessary network objects, the overall process was fairly simple.</p>
<p>It helped tremendously that the FDM API explorer exists, and is available on-box. This utility allows you to see all the available API calls, what parameters they require, and even run test calls from the web UI. This greatly reduced the time needed to figure this out.</p>
<p>Hope this was interesting. If you would like to see the whole project - check out the repo on my GitHub page.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Automated F5 Backups with CatTools</title>
      <link>https://0x2142.com/automated-f5-backups-with-cattools/</link>
      <pubDate>Tue, 27 Mar 2018 11:00:22 +0000</pubDate>
      <guid>https://0x2142.com/automated-f5-backups-with-cattools/</guid>
      <description>How to configure SolarWinds Kiwi CatTools to monitor &amp;amp; back up F5 load balancer configurations</description>
      <content:encoded><![CDATA[<p>We have some new F5 load balancers in our environment, which means I need a method of grabbing regular configuration backups. There are a number of methods out there, but I&rsquo;ve opted to use SolarWind&rsquo;s CatTools software since we already own it.</p>
<p>The config I used is based on <a href="https://lessonsintech.wordpress.com/2017/06/15/automate-f5-backups">this blog post</a>. It&rsquo;s a great write-up on how to back up F5 configurations using CatTools. I don&rsquo;t want to replicate what was written over there - but I did hit some issues that were specific to my use-case that I wanted to share.</p>
<p>While I was happy to find the article linked above, the immediate results didn&rsquo;t work so smooth for me. This may be due to some key configuration differences that I face in my network:</p>
<ul>
<li>All the F5&rsquo;s are LDAP integrated - so there isn&rsquo;t an easy way to provide LDAP users with direct bash access</li></li>
<li>All of my F5&rsquo;s are remote appliances, where the backup configuration is being copied across the WAN</li></li>
</ul>
<p>Getting around the first problem was my biggest challenge. CatTools is a very command/response-oriented application. Any remote LDAP authenticated users are immediately dropped into F5&rsquo;s shell: <strong>tmsh</strong>. To get from there to their &lsquo;advanced shell&rsquo; is as simple as typing <strong>bash</strong>. However, When the terminal prompt changes, it often throws CatTools into a state of &ldquo;I didn&rsquo;t receive the response prompt I expected, therefore kill the job - something went wrong&rdquo;. I spent a bit more time on this than I wanted to - but the underlying problem was that the &ldquo;<strong>F5.BigIP</strong>&rdquo; device type was specifically looking for the tmsh shell and couldn&rsquo;t handle the prompt change. The fix? Switch the device type to &ldquo;<strong>Linux.RedHat.Bash</strong>&rdquo;, then add the <strong>bash</strong> command to the first line of your backup script.</p>
<p>The next problem was using TFTP to copy the backup archives over the WAN. Even some of the new F5&rsquo;s with minimal configuration still generate a 10Mb file. Doesn&rsquo;t seem like much, but when you&rsquo;re copying that over a WAN between two datacenters, that turns into a ~5 minute file transfer. CatTools by default will only wait 30 seconds after executing a command before it expects a response. So every time I tried to run the job, CatTools would kill it only 30-seconds into the file transfer. Luckily enough, they support a utility command that can alter the normal timeout:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">%ctUM: Timeout <span class="m">600</span>
</span></span><span class="line"><span class="cl">tftp -m binary 192.168.1.10 -c put <span class="nv">$filename</span>
</span></span><span class="line"><span class="cl">%ctUM: Timeout <span class="m">0</span>
</span></span></code></pre></div><p>The command <code>%ctUM: Timeout 600</code> changes the timeout value to 600 seconds, or 10 minutes. The TFTP file transfer command is next, which is now permitted up to 10 minutes to finish. The last command resets the timeout back to the default (30 seconds).</p>
<p>I also realized that the original script doesn&rsquo;t purge the backup archive afterwards. For my use case, I would much rather automatically clean up the backup files once they&rsquo;ve been transferred to a central location.</p>
<p>So after all that, here is the version of that script that I&rsquo;m using:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">date</span><span class="o">=</span><span class="sb">`</span>date +<span class="s2">&#34;%y%m%d&#34;</span>​<span class="sb">`</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">filename</span><span class="o">=</span><span class="nv">$HOSTNAME</span>.<span class="nv">$date</span>.ucs
</span></span><span class="line"><span class="cl">tmsh save /sys ucs /var/local/ucs/<span class="nv">$filename</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /var/local/ucs
</span></span><span class="line"><span class="cl">%ctUM: Timeout <span class="m">600</span>
</span></span><span class="line"><span class="cl">tftp -m binary 192.168.1.10 -c put <span class="nv">$filename</span>
</span></span><span class="line"><span class="cl">%ctUM: Timeout <span class="m">0</span>
</span></span><span class="line"><span class="cl">rm -f <span class="nv">$filename</span>
</span></span></code></pre></div><hr>
<p>Thanks again to the <a href="https://lessonsintech.wordpress.com/2017/06/15/automate-f5-backups">original blog post</a> for getting me on the right track with this! I hope that my ramblings here are helpful to anyone with a similar deployment scenario.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Juniper SRX - Automated Route Monitoring</title>
      <link>https://0x2142.com/juniper-srx-automated-route-monitoring/</link>
      <pubDate>Tue, 13 Mar 2018 11:00:20 +0000</pubDate>
      <guid>https://0x2142.com/juniper-srx-automated-route-monitoring/</guid>
      <description>How I automated BGP routing table monitoring for Juniper SRX firewalls</description>
      <content:encoded><![CDATA[<p><sup><em>Note: I may receive commissions for purchases made through links in this post. This is to help support my blog and does not have any impact on my recommendations.</em></sup></p>
<hr>
<p>I have always said that I&rsquo;m not sure I could write code for a living, but I do really enjoy writing scripts that make my life easier. Today&rsquo;s post is a great example of that. The ease of use offered by the Juniper SRX firewalls and JunOS is something that I wish I had in all of my networking infrastructure. Even better, a brand new SRX 300 can be purchased on <a href="https://www.amazon.com/gp/product/B01ICEO2U4/ref=as_li_qf_asin_il_tl?ie=UTF8&amp;tag=0x2142-20&amp;creative=9325&amp;linkCode=as2&amp;creativeASIN=B01ICEO2U4&amp;linkId=35fbe8300af4e5d1e26e7a860782b3ca">Amazon</a> for less than $300 - which made a great addition to my home lab. Now I have a place to develop and test automation without breaking production 🙂</p>
<p>Anyways - I had a requirement to monitor my SRX clusters for route changes. Specifically I&rsquo;m using this with devices where I have implemented customer peering via BGP. The SRXs don&rsquo;t natively offer any form of monitoring and alerting for this, and the current monitoring applications at my disposal don&rsquo;t either. So I decided to write something myself, which took significantly less time than I had assumed.</p>
<p>Code has been posted up to my <a href="https://github.com/0x2142/juniper-srx-scripts/blob/master/checkBGP.py">GitHub</a> - but I&rsquo;m going to walk through some of it here. This script is intended to run as a cron job on a 5 or 10 minute interval.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Imports</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">jnpr.junos</span> <span class="kn">import</span> <span class="n">Device</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">jnpr.junos.op.routes</span> <span class="kn">import</span> <span class="n">RouteTable</span>
</span></span></code></pre></div><p>On my initial research to see how easy this would be to pull off, I found a nifty thing in the <a href="/getting-started-with-junos-pyez/">JunOS PyEZ</a> package. Turns out they already offer a module literally called RouteTable that can pull the information I need.</p>
<p>Originally, I spent a bit of time trying to figure out how to use the standard device API to pull this info, but this module made everything 10x easier.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1">#################</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Required values</span>
</span></span><span class="line"><span class="cl"><span class="c1">#################</span>
</span></span><span class="line"><span class="cl"><span class="n">deviceName</span> <span class="o">=</span> <span class="s1">&#39;SRXFirewall&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">deviceIP</span> <span class="o">=</span> <span class="s1">&#39;0.0.0.0&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">apiuser</span> <span class="o">=</span> <span class="s1">&#39;username&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">apipassword</span> <span class="o">=</span> <span class="s1">&#39;password&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">SMTP</span> <span class="o">=</span> <span class="s1">&#39;smtp-alias&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">fromName</span> <span class="o">=</span> <span class="s1">&#39;BGP Monitor&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">fromAddr</span> <span class="o">=</span> <span class="s1">&#39;bgpmonitor@domain.com&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">toName</span> <span class="o">=</span> <span class="s1">&#39;contactName&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">toAddr</span> <span class="o">=</span> <span class="s1">&#39;contact@domain.com&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">prefixDict</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;FriendlyName&#34;</span><span class="p">:</span> <span class="s2">&#34;Prefix&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Route-to-Inet&#34;</span><span class="p">:</span> <span class="s2">&#34;0.0.0.0/0&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="c1">################</span>
</span></span></code></pre></div><p>The section above contains a list of the required variables for this script to function. A lot of them are going to be self-explanatory - but I wanted to take a moment to look at the prefixDict. This is a Python dictionary that maps a friendly name to a route prefix, which is used when we check our routing table. For example, if you wanted to monitor the SRX device for a route for 10.10.10.0/24 which belongs to Customer1, then we would just add and entry in this dictionary for &ldquo;Customer1&rdquo;: &ldquo;10.10.10.0/24&rdquo;</p>
<p>Alright - now let&rsquo;s skip to the good stuff:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Function to check received BGP routes</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">checkBGP</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Open SRX session</span>
</span></span><span class="line"><span class="cl">        <span class="n">dev</span><span class="o">.</span><span class="n">open</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">except</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Pull device routing table, then keep only BGP originated routes</span>
</span></span><span class="line"><span class="cl">    <span class="n">allroutes</span> <span class="o">=</span> <span class="n">RouteTable</span><span class="p">(</span><span class="n">dev</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">bgp</span> <span class="o">=</span> <span class="n">allroutes</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">protocol</span><span class="o">=</span><span class="s2">&#34;bgp&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">keys</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Close SRX session</span>
</span></span><span class="line"><span class="cl">    <span class="n">dev</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span></code></pre></div><p>The section above is the beginning of the checkBGP function. Simple enough - just open an API session to the SRX and grab the entire routing table. That module I was talking about earlier makes it super easy! Next, we pull only the routes originating from BGP and assign them to a variable called <code>bgp</code>.</p>
<p>So in order to run this script repeatedly as a cron, I needed a place to persistently store the last retrieved routing info. For the time being, this is done by simply writing a temp file with the information:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">tempfile</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">tempfile</span><span class="p">,</span> <span class="s1">&#39;w+b&#39;</span><span class="p">)</span> <span class="k">as</span> <span class="n">a</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">a</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">bgp</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Local file used to keep track of BGP learned routes</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">tempfile</span><span class="p">,</span> <span class="s1">&#39;ab&#39;</span><span class="p">)</span> <span class="k">as</span> <span class="n">a</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">lastroutes</span> <span class="o">=</span> <span class="n">a</span><span class="o">.</span><span class="n">readlines</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Compare if routes are different</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nb">str</span><span class="p">(</span><span class="n">bgp</span><span class="p">)</span> <span class="o">==</span> <span class="nb">str</span><span class="p">(</span><span class="n">lastroutes</span><span class="p">[</span><span class="mi">0</span><span class="p">]):</span>
</span></span><span class="line"><span class="cl">            <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nb">str</span><span class="p">(</span><span class="n">bgp</span><span class="p">)</span> <span class="o">!=</span> <span class="nb">str</span><span class="p">(</span><span class="n">lastroutes</span><span class="p">[</span><span class="mi">0</span><span class="p">]):</span>
</span></span><span class="line"><span class="cl">            <span class="k">pass</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Delete file, then re-create with new route list</span>
</span></span><span class="line"><span class="cl">    <span class="c1">#os.remove(tempfile)</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">tempfile</span><span class="p">,</span> <span class="s1">&#39;w+b&#39;</span><span class="p">)</span> <span class="k">as</span> <span class="n">a</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">a</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">bgp</span><span class="p">))</span>
</span></span></code></pre></div><p>The logic in the comparison is simple enough. If the current string of routes pulled from the SRX equals what is in the temp file (from the last run), then we assume no changes have occurred - and the script ends. Otherwise, if they don&rsquo;t match then something has changed.</p>
<p>Once we know something has changed, we&rsquo;ll go ahead and find out exactly which route entry is missing:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl">    <span class="c1"># Create Status list, by checking received routes in bgp object</span>
</span></span><span class="line"><span class="cl">    <span class="n">status</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">name</span><span class="p">,</span><span class="n">prefix</span> <span class="ow">in</span> <span class="n">prefixDict</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">prefix</span> <span class="ow">in</span> <span class="n">bgp</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">status</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">%s</span><span class="s2"> - RECEIVED&#34;</span> <span class="o">%</span> <span class="n">name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="n">prefix</span> <span class="ow">in</span> <span class="n">bgp</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">status</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">%s</span><span class="s2"> - MISSING&#34;</span> <span class="o">%</span> <span class="n">name</span><span class="p">)</span>
</span></span></code></pre></div><p>This is where our prefixDict from earlier comes into play. We&rsquo;ll look at every BGP prefix that we defined in that dictionary, and see if it exists in the current SRX routing table. All of this information (both routes received and missing) get added to an alert email.</p>
<p>Lastly, we&rsquo;ll go ahead and compose our alert email to send out:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl">    <span class="c1"># Send alert message</span>
</span></span><span class="line"><span class="cl">    <span class="n">sendMail</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">lastroutes</span><span class="p">[</span><span class="mi">0</span><span class="p">]),</span> <span class="nb">str</span><span class="p">(</span><span class="n">bgp</span><span class="p">),</span> <span class="n">status</span><span class="p">)</span>
</span></span></code></pre></div><p>I&rsquo;m not going to cover the email function here, since it&rsquo;s pretty straightforward.</p>
<p>That&rsquo;s it! The existence of the JunOS RouteTables module made creating this script a piece of cake. I&rsquo;m considering adding onto this later with some additional functionality, so be sure to check my <a href="https://github.com/0x2142/juniper-srx-scripts">GitHub</a> repo if you&rsquo;re interested.</p>
<p>Hope this is useful to someone else out there - Let me know in the comments!</p>
]]></content:encoded>
    </item>
    <item>
      <title>Building a VPN Dashboard using Django and JunOS pyEZ (Part 4 – Polling the SRX)</title>
      <link>https://0x2142.com/building-a-vpn-dashboard-using-django-and-junos-pyez-part-4-polling-the-srx/</link>
      <pubDate>Tue, 04 Jul 2017 08:00:31 +0000</pubDate>
      <guid>https://0x2142.com/building-a-vpn-dashboard-using-django-and-junos-pyez-part-4-polling-the-srx/</guid>
      <description>This post focuses on the Juniper SRX side of my VPN dashboard - writing the Python scripting to query VPN status from each device</description>
      <content:encoded><![CDATA[<p>This is a multi-part series - If you just hit this page, please check out the prior posts first!</p>
<ul>
<li><a href="/building-a-vpn-dashboard-using-django-and-junos-pyez-part-1-initial-thoughts/">Part 1 - Initial Thoughts</a></li>
<li><a href="/building-a-vpn-dashboard-using-django-and-junos-pyez-part-2-learning-django/">Part 2 - Leaning Django</a></li>
<li><a href="/building-a-vpn-dashboard-using-django-and-junos-pyez-part-3-creating-the-dashboard/">Part 3 - Creating the Dashboard</a></li>
</ul>
<hr>
<p>Alright - We&rsquo;re finally at the last step to this project. We already have Django running with a somewhat decent-looking web dashboard. Now the only thing that remains is polling our SRX firewalls for VPN connections, then populating this information into the database.</p>
<p>For this to happen, I built a custom management command for Django - the cool thing here is that we can easily use this within a cron job to automatically schedule updates at regular intervals. So within our application (the vpn folder), we&rsquo;re going to create a <em>management</em> folder then a <em>commands</em> folder within that. In here you can name your script whatever you want, so I went with cronpoller.py.</p>
<p>The script that I wrote is extremely simplistic and could probably use quite a number of improvements - which I will slowly make over time. But for now, it is what it is - and it does exactly what I need it to.</p>
<p>First thing we need to do is import a few things we will need:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">django.core.management.base</span> <span class="kn">import</span> <span class="n">BaseCommand</span><span class="p">,</span> <span class="n">CommandError</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">vpn.models</span> <span class="kn">import</span> <span class="n">Firewall</span><span class="p">,</span> <span class="n">Datacenter</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">jnpr.junos</span> <span class="kn">import</span> <span class="n">Device</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">lxml</span> <span class="kn">import</span> <span class="n">etree</span>
</span></span></code></pre></div><p>We have some Django modules for treating this as a management command - and we&rsquo;re also importing our existing models into this script. This part is really cool to me, because it means that this cron script can just reference the existing objects that we created to get their attributes. Next, we import the JunOS stuff from their pyEZ packages (don&rsquo;t forget to <a href="/getting-started-with-junos-pyez/">install pyEZ</a>!). The last one is going to be used to parse the responses from our SRX into something we can use.</p>
<p>Alright - so in order to build a management command, we have to create a class called <em>Command,</em> then define our functions within that. Within the <em>Command</em> class, Django will look for a function called <em>handle</em> to execute. In my final script, I have that function plus one called <em>getVPNStatus</em>. Let&rsquo;s start with putting together <em>getVPNStatus:</em></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># This function will poll the device for status </span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">getVPNStatus</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">fw</span><span class="p">,</span> <span class="n">dc</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">connectedlist</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># So we generate a JunOS pyEZ device connection using the information about the </span>
</span></span><span class="line"><span class="cl">    <span class="c1"># firewall that we gather from the database object</span>
</span></span><span class="line"><span class="cl">    <span class="n">device</span> <span class="o">=</span> <span class="n">Device</span><span class="p">(</span><span class="n">fw</span><span class="o">.</span><span class="n">firewall_manageip</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="n">user</span><span class="o">=</span><span class="n">fw</span><span class="o">.</span><span class="n">firewall_user</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="n">password</span><span class="o">=</span><span class="n">fw</span><span class="o">.</span><span class="n">firewall_pass</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Try to open a connection out to the target device</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">dev</span><span class="o">.</span><span class="n">open</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">except</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># If for some reason this doesn&#39;t work, just return UNREACHABLE - which</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># we&#39;ll assume means the device is down</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="s2">&#34;UNREACHABLE&#34;</span>
</span></span><span class="line"><span class="cl"> 
</span></span><span class="line"><span class="cl">    <span class="c1"># Here is where we poll the SRX for a list of all IPSec Security Associations.</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># The equivalent of the &#39;show security ipsec sa&#39; command</span>
</span></span><span class="line"><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">etree</span><span class="o">.</span><span class="n">tostring</span><span class="p">(</span><span class="n">dev</span><span class="o">.</span><span class="n">rpc</span><span class="o">.</span><span class="n">get_security_associations_information</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># The SRX returns a response in XML, which we&#39;ll need to dig through</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Credit to the guys over at &lt;a href=&#34;http://packetpushers.net/parsing-junos-xml-python/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;Packet Pushers&lt;/a&gt; for a great post explaining how to</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># parse these responses</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">response</span><span class="p">)</span> <span class="k">as</span> <span class="n">a</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">xmldoc</span> <span class="o">=</span> <span class="n">etree</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">a</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">docroot</span> <span class="o">=</span> <span class="n">xmldoc</span><span class="o">.</span><span class="n">getroot</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">rootchildren</span> <span class="o">=</span> <span class="n">docroot</span><span class="o">.</span><span class="n">iter</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">child</span> <span class="ow">in</span> <span class="n">rootchildren</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># For each IPSec SA returned, we need to find the remote gateway IP, which </span>
</span></span><span class="line"><span class="cl">            <span class="c1"># we use to tie the connection back to the connected datacenter</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">child</span><span class="o">.</span><span class="n">tag</span> <span class="o">==</span> <span class="s2">&#34;sa-remote-gateway&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="n">connectedlist</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">child</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">   
</span></span><span class="line"><span class="cl">    <span class="c1"># Once we&#39;ve built our list, send it back!</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">connectedlist</span>
</span></span></code></pre></div><p>That already is major part of our script. It will connect out to each device, grab a list of every connected IPSec tunnel, then return back a list of connected gateways. Now we just need to write our <em>handle</em> function, which will do all of the remaining work.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">handle</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Let&#39;s quickly create two lists - based on our firewall and datacenter models</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># This actually polls the database for anything that&#39;s been created</span>
</span></span><span class="line"><span class="cl">    <span class="n">dclist</span> <span class="o">=</span> <span class="n">Datacenter</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">order_by</span><span class="p">(</span><span class="s1">&#39;datacenter_code&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">fwlist</span> <span class="o">=</span> <span class="n">Firewall</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">order_by</span><span class="p">(</span><span class="s1">&#39;firewall_name&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> 
</span></span><span class="line"><span class="cl">    <span class="c1"># This will loop through each firewall, then call the getVPNStatus function</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">fw</span> <span class="ow">in</span> <span class="n">Firewall</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">order_by</span><span class="p">(</span><span class="s1">&#39;firewall_name&#39;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">statuslist</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">getVPNStatus</span><span class="p">(</span><span class="n">fw</span><span class="p">,</span> <span class="n">dclist</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Once we have our response, we&#39;re going to check the returned list of connected</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># gateways against our list of datacenters from the database</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">dc</span> <span class="ow">in</span> <span class="n">dclist</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">remoteFW</span> <span class="ow">in</span> <span class="n">fwlist</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># If we find another datacenter in the connected gateway list, add </span>
</span></span><span class="line"><span class="cl">            <span class="c1"># that datacenter to the vpnstatus list as &#34;VPNUP&#34;, otherwise assume it&#39;s down</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">remoteFW</span><span class="o">.</span><span class="n">firewall_vpnip</span> <span class="ow">in</span> <span class="n">statuslist</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="n">vpnstatus</span><span class="p">[</span><span class="n">dc</span><span class="o">.</span><span class="n">datacenter_code</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&#34;VPNUP&#34;</span>
</span></span><span class="line"><span class="cl">                <span class="k">break</span>
</span></span><span class="line"><span class="cl">            <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="n">vpnstatus</span><span class="p">[</span><span class="n">dc</span><span class="o">.</span><span class="n">datacenter_code</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&#34;VPNDOWN&#34;</span>
</span></span><span class="line"><span class="cl"> 
</span></span><span class="line"><span class="cl">    <span class="c1"># After all that is done - we just save the new vpnstatus list to the firewall_vpnstatus</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># field in the database</span>
</span></span><span class="line"><span class="cl">    <span class="n">fw</span><span class="o">.</span><span class="n">firewall_vpnstatus</span> <span class="o">=</span> <span class="n">vpnstatus</span>
</span></span><span class="line"><span class="cl">    <span class="n">fw</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">update_fields</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;firewall_vpnstatus&#34;</span><span class="p">])</span>
</span></span></code></pre></div><p>I&rsquo;ve said this before, but I think it&rsquo;s worth noting again - This probably isn&rsquo;t the best or most efficient/reliable way of doing this. I think that over time I would like to revisit this and refine it a bit - but this is what I&rsquo;ve put together so far. In fact, I think it&rsquo;s worth stating that this will probably break in a fantastically horrific manner. This isn&rsquo;t a finished product, but pretty much just version 0.1 - the base functionality works, but it&rsquo;s in desperate need of refinement.</p>
<p>Alright - so now we just have to add a cron job in our system to call this script and run it. Depending on how many firewalls you have and how the latency is, you might need a different interval than I am using - but I went ahead and set mine to run once every 10 minutes. I would like to get this down to 5 minutes or less, but I might need to figure out a way to potentially multi-thread the cronpoller.py script. So here is what I did in /etc/cron.d/vpnstatuspoller:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">*/10  * * * * * python /root/junos-dashboard/manage.py cronpoller
</span></span></code></pre></div><p>After letting the script poll my firewalls - I ended up with a dashboard that looks like this:</p>
<p><img alt="dashboard-complete" loading="lazy" src="/content/images/2021/01/dashboard-complete.png#center"></p>
<h2 id="future-improvements">Future improvements</h2>
<p>Of course, I still have a few things I would like to add or improve on. I&rsquo;ve been keeping a list of some ideas that I&rsquo;ve had, which I&rsquo;ll share here:</p>
<ul>
<li>Add a timestamp to each firewall that contains the last poll time (in case something gets missed or hasn&rsquo;t updated yet)</li>
<li>Possibly add ability to click a &lsquo;down&rsquo; VPN cell to force clear SA</li>
<li>Set up email alerts from the cronpoller to automatically notify me of a failure</li>
<li>Ability to click on any VPN cell and get additional info - like VPN uptime, subnets routed across it, or maybe the IKE/IPSec negotiation parameters</li>
<li>Ability to click on any firewall name and get additional info - like uptime, JunOS version, CPU/Memory</li>
</ul>
<hr>
<p>I finally got around to getting myself a <a href="https://github.com/0x2142">GitHub</a> account, so I might put the final code up there once I&rsquo;m done. I also have a number of other JunOS scripts that are probably worth posting up there as well.</p>
<p>Well, I hope you enjoyed reading about this project - because I certainly enjoyed working on it. This was one of my first real projects using the JunOS pyEZ libraries, and I got to pick up and learn how to use Django as well. The experience of building this has given me ideas for other JunOS automation projects - so look out for some of those in the future!</p>
]]></content:encoded>
    </item>
    <item>
      <title>Building a VPN Dashboard using Django and JunOS pyEZ (Part 3 – Creating the Dashboard)</title>
      <link>https://0x2142.com/building-a-vpn-dashboard-using-django-and-junos-pyez-part-3-creating-the-dashboard/</link>
      <pubDate>Tue, 27 Jun 2017 08:00:19 +0000</pubDate>
      <guid>https://0x2142.com/building-a-vpn-dashboard-using-django-and-junos-pyez-part-3-creating-the-dashboard/</guid>
      <description>This post focuses on the web GUI side of the VPN dashboard - storing &amp;amp; displaying the information that gets collected</description>
      <content:encoded><![CDATA[<p>This is a multi-part series - If you just hit this page, please check out the prior posts first!</p>
<ul>
<li><a href="/building-a-vpn-dashboard-using-django-and-junos-pyez-part-1-initial-thoughts/">Part 1 - Initial Thoughts</a></li>
<li><a href="/building-a-vpn-dashboard-using-django-and-junos-pyez-part-2-learning-django/">Part 2 - Leaning Django</a></li>
</ul>
<hr>
<p>In the last post, we got Django working well enough to start serving up web requests. That&rsquo;s a great start - So now it&rsquo;s on to actually building the project. We&rsquo;re going to cover this in two parts: the front-end dashboard, and the back-end scripts. In this post, I&rsquo;m going to show you how I began putting everything together to assemble the front-end HTML dashboard.</p>
<p>As a quick refresher note, we created a project in the last post called junos-dashboard. Within that project, we created our app named vpn. This folder is primarily where we&rsquo;ll be spending our time today. There are two files in our app folder that will need a few edits:</p>
<p><strong>views.py</strong> - This is where we originally created our basic index page, which currently just spits out a message. So we&rsquo;ll need to add code in here to render our dashboard instead.</p>
<p><strong>models.py</strong> - In here we will define our objects and the characteristics about those objects that we want to store/retrieve in the sqlite database.</p>
<p>Let&rsquo;s go ahead and open up our models.py file. For my dashboard, I have two objects I want to track - Firewalls (the SRX hosting the VPN) and Datacenter (as a location tracking). So in order to create those objects, we just have to add two classes into the models.py file:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">__future__</span> <span class="kn">import</span> <span class="n">unicode_literals</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Datacenter</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">datacenter_code</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Firewall</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">firewall_name</span>
</span></span></code></pre></div><p>Easy enough, right? So here is the really cool thing about Django (or probably any web framework really) - These model objects are now something that we can reference in our code to change or query attributes about them. Even cooler is that Django automatically builds an admin page to our site, where we can create new instances of each model right there (which I&rsquo;ll cover later). So this means that all I have to do is define these two models and their attributes, then anyone on my team can log into the dashboard admin page and add new firewalls or datacenters.</p>
<p>Okay, so in order to actually make these models useful, we need to add our attributes. For the datacenter object, I only care about two things - What is the datacenter code (identifier), and is the datacenter an active location. So here is where we add those options under our datacenter class:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Datacenter</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">datacenter_code</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="s1">&#39;Datacenter Code&#39;</span><span class="p">,</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">datacenter_active</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">BooleanField</span><span class="p">(</span><span class="s1">&#39;Datacenter Active?&#39;</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span></code></pre></div><p>The firewall object gets a little more complicated because I want to be able to define quite a few things. The obvious attributes are: the firewall name, which datacenter it belongs to, and whether or not it is an active firewall. I&rsquo;ll also need to collect this firewall&rsquo;s VPN peer IP, so that I can compare it against other devices to check the connection. But wait - how am I actually checking that VPN status? Oh, I guess I&rsquo;ll also need to collect a username/password for the SRX API, as well as a valid management IP to reach the API.</p>
<p>Let&rsquo;s take a moment though - because I stopped here and realized that I&rsquo;ll need to collect user/password data in a form, then it&rsquo;s going to be stored in a sqlite database. Oh, and of course it will be unencrypted. I didn&rsquo;t really like that idea - so I did some research on how to encrypt one of the object attributes. Turns out, it&rsquo;s actually pretty easy since someone has already created a module to do exactly that.</p>
<p>So let&rsquo;s grab that module real quick:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">[root@0x2142-Centos vpn]# pip install django-encrypted-fields
</span></span></code></pre></div><p>Once we have the module, it requires us to generate some keys which will be used for encrypting our data. The following is based on the steps listed on the GitHub page for <a href="https://github.com/defrex/django-encrypted-fields">django-encrypted-fields</a>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">[root@0x2142-Centos junos-dashboard]# mkdir fieldkeys
</span></span><span class="line"><span class="cl">[root@0x2142-Centos junos-dashboard]# keyczart create --location=fieldkeys --purpose=crypt
</span></span><span class="line"><span class="cl">[root@0x2142-Centos junos-dashboard]# keyczart addkey --location=fieldkeys --status=primary --size=256
</span></span></code></pre></div><p>Next, we just need to add this keyfile into our settings.py:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">ENCRYPTED_FIELDS_KEYDIR = &#39;/root/junos-dashboard/fieldkeys&#39;
</span></span></code></pre></div><p>Alright - back to actually creating the attributes we need for our firewalls. Since we&rsquo;re using this new module, we&rsquo;ll need to add an additional import statement - otherwise, we are just adding the fields I covered earlier:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">encrypted_fields</span> <span class="kn">import</span> <span class="n">EncryptedCharField</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Firewall</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">firewall_name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="s1">&#39;Firewall Name&#39;</span><span class="p">,</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">50</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">firewall_location</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="s1">&#39;Datacenter&#39;</span><span class="p">,</span> <span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="o">.</span><span class="n">CASCADE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">firewall_active</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">BooleanField</span><span class="p">(</span><span class="s1">&#39;Firewall Active?&#39;</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">firewall_manageip</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">GenericIPAddressField</span><span class="p">(</span><span class="s1">&#39;Management IP&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">firewall_vpnip</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">GenericIPAddressField</span><span class="p">(</span><span class="s1">&#39;VPN Interface IP&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">firewall_user</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="s1">&#39;API User&#39;</span><span class="p">,</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">50</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">firewall_pass</span> <span class="o">=</span> <span class="n">EncryptedCharField</span><span class="p">(</span><span class="s1">&#39;API Pass&#39;</span><span class="p">,</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">50</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">firewall_vpnstatus</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">TextField</span><span class="p">(</span><span class="n">editable</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span></code></pre></div><p>A few things to note here - The firewall_location attribute will actually query the table of datacenter objects - so we&rsquo;re not creating any data twice. You might also notice that I added a firewall_vpnstatus text field, which is going to be hidden in the administrative console. Because I&rsquo;m a terrible programmer, I&rsquo;ll be storing all of the VPN status info in this text field in the database. Yes, there is probably a much more elegant way of handling this - but again, I&rsquo;m a network admin not a professional coder. For what I need, this gets the job done. If you have a better way of accomplishing this - let me know! I would be very interested in another method.</p>
<p>I also found out later that just because I encrypted the field doesn&rsquo;t mean that Django knows the field is a password. I wanted the field to be treated like a password input, so the data wasn&rsquo;t visible to anyone who just logged in. I found out that you can create a forms.py file within the vpn directory, and pretty much override the field type to account for this.</p>
<p>So here is what I threw into the forms.py file to handle password input:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">django.forms</span> <span class="kn">import</span> <span class="n">ModelForm</span><span class="p">,</span> <span class="n">PasswordInput</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">.models</span> <span class="kn">import</span> <span class="n">Firewall</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">FirewallForm</span><span class="p">(</span><span class="n">ModelForm</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">firewall_pass</span> <span class="o">=</span> <span class="n">CharField</span><span class="p">(</span><span class="n">widget</span><span class="o">=</span><span class="n">forms</span><span class="o">.</span><span class="n">PasswordInput</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">model</span> <span class="o">=</span> <span class="n">Firewall</span>
</span></span><span class="line"><span class="cl">        <span class="n">fields</span> <span class="o">=</span> <span class="p">[</span><span class="s1">&#39;firewall_pass&#39;</span><span class="p">]</span>
</span></span></code></pre></div><p>That was actually way easier than I had anticipated.</p>
<p>Awesome, now our work on the models is completely done - why don&rsquo;t we log into the administrative  web interface and take a look at how it all turned out?</p>
<p>Here is a view of the datacenter page - where we can add a new location:</p>
<p><img alt="image" loading="lazy" src="/content/images/2021/01/djangoadmin-datacenters.png#center"></p>
<p>And now for the firewall objects - with all the fields we need to get the job done:</p>
<p><img alt="image" loading="lazy" src="/content/images/2021/01/djangoadmin-firewalls.png#center"></p>
<p>Looks pretty good, huh? And we didn&rsquo;t even need to do any work to get the free admin functionality! This is honestly my favorite part of Django. Without posting a ton of additional screenshots, the admin page also provides audit logs - so we can see who changed what objects. Very helpful.</p>
<hr>
<p>Alright - now let&rsquo;s start work on our views.py file, so we can actually start putting all this together in our dashboard!</p>
<p>Within our views.py file, we already had our index function, which just returned a string of text. We&rsquo;ll be adding to that in a moment - but first I created a separate function called buildrows, which does exactly what you think. This function will pull each firewall and it&rsquo;s status, and build our little HTML table. I&rsquo;m not going to go into great detail on this - but check out the comments for more information on what&rsquo;s going on here:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># This is our function to build the HTML table - We&#39;re going to be expecting this function</span>
</span></span><span class="line"><span class="cl"><span class="c1"># to be fed a list of firewalls and datacenters</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">buildrows</span><span class="p">(</span><span class="n">firewall_list</span><span class="p">,</span> <span class="n">datacenter_list</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">firewallstatus</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># We will iterate through each firewall and compile each row for our table</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">fw</span> <span class="ow">in</span> <span class="n">firewall_list</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># The first cell in our row is going to be the firewall name and it&#39;s own VPN peer IP</span>
</span></span><span class="line"><span class="cl">        <span class="n">onerow</span> <span class="o">=</span> <span class="s2">&#34;&lt;td&gt;&lt;b&gt;</span><span class="si">%s</span><span class="s2">&lt;/b&gt;&lt;i&gt;</span><span class="si">%s</span><span class="s2">&lt;/i&gt;&lt;/td&gt;&#34;</span> <span class="o">%</span> <span class="p">(</span><span class="n">fw</span><span class="o">.</span><span class="n">firewall_name</span><span class="p">,</span> <span class="n">fw</span><span class="o">.</span><span class="n">firewall_vpnip</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Now we iterate through every datacenter, in order to see which ones we have a</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># connection to from this firewall</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">dc</span> <span class="ow">in</span> <span class="n">datacenter_list</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># First thing is first - If this firewall contains the name of the datacenter</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># then we&#39;ll print N/A, since we wouldn&#39;t expect a VPN to itself </span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="nb">str</span><span class="p">(</span><span class="n">dc</span><span class="o">.</span><span class="n">datacenter_code</span><span class="p">)</span> <span class="ow">in</span> <span class="nb">str</span><span class="p">(</span><span class="n">fw</span><span class="o">.</span><span class="n">firewall_name</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">                <span class="n">onerow</span> <span class="o">+=</span> <span class="p">(</span><span class="s2">&#34;&lt;td&gt;N/A&lt;/td&gt;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="k">continue</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># Also, if there is no VPN status in the database, then just print &#39;No Status&#39;</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="ow">not</span> <span class="n">fw</span><span class="o">.</span><span class="n">firewall_vpnstatus</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="n">onerow</span> <span class="o">+=</span> <span class="p">(</span><span class="s2">&#34;&lt;td&gt;No Status&lt;/td&gt;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="k">continue</span>
</span></span><span class="line"><span class="cl">     
</span></span><span class="line"><span class="cl">            <span class="c1"># This portion is pretty self-explanatory - We parse the vpnstatus field in the</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># database. If the state of the connection is &#39;VPNUP&#39;, then we print a cell for</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># that datacenter with the text &#39;UP&#39;. If the state is &#39;VPNDOWN&#39;, then we print</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># &#39;DOWN&#39;. And if nothing matches, print &#39;No Status&#39;</span>
</span></span><span class="line"><span class="cl">            <span class="n">statuslist</span> <span class="o">=</span> <span class="n">ast</span><span class="o">.</span><span class="n">literal_eval</span><span class="p">(</span><span class="n">fw</span><span class="o">.</span><span class="n">firewall_vpnstatus</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="n">status</span> <span class="o">=</span> <span class="n">statuslist</span><span class="p">[</span><span class="n">dc</span><span class="o">.</span><span class="n">datacenter_code</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="n">status</span> <span class="o">==</span> <span class="s2">&#34;VPNUP&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                    <span class="n">onerow</span> <span class="o">+=</span> <span class="p">(</span><span class="s2">&#34;&lt;td class=</span><span class="se">\&#34;</span><span class="s2">vpnup</span><span class="se">\&#34;</span><span class="s2">&gt;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                    <span class="n">onerow</span> <span class="o">+=</span> <span class="p">(</span><span class="s2">&#34;UP&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="k">elif</span> <span class="n">status</span> <span class="o">==</span> <span class="s2">&#34;VPNDOWN&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                    <span class="n">onerow</span> <span class="o">+=</span> <span class="p">(</span><span class="s2">&#34;&lt;td class=</span><span class="se">\&#34;</span><span class="s2">vpndown</span><span class="se">\&#34;</span><span class="s2">&gt;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                    <span class="n">onerow</span> <span class="o">+=</span> <span class="p">(</span><span class="s2">&#34;DOWN&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                    <span class="n">onerow</span> <span class="o">+=</span> <span class="p">(</span><span class="s2">&#34;&lt;td&gt;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                    <span class="n">onerow</span> <span class="o">+=</span> <span class="p">(</span><span class="s2">&#34;No Status&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                    <span class="n">onerow</span> <span class="o">+=</span> <span class="p">(</span><span class="s2">&#34;&lt;/td&gt;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="n">onerow</span> <span class="o">+=</span> <span class="p">(</span><span class="s2">&#34;&lt;td&gt;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="n">onerow</span> <span class="o">+=</span> <span class="p">(</span><span class="s2">&#34;No Status&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="n">onerow</span> <span class="o">+=</span> <span class="p">(</span><span class="s2">&#34;&lt;/td&gt;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Once complete, we append this row to the status list</span>
</span></span><span class="line"><span class="cl">        <span class="n">firewallstatus</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">onerow</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1"># All done! Return our list of statuses!</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">firewallstatus</span>
</span></span></code></pre></div><p>That block might not make a ton of sense at the moment - but it will shortly. Essentially I have a script that is connecting to each firewall via the SRX API and polling the VPN status for each peer. The script is then writing all of this information to a field in the database called vpnstatus in one long string, which kinda looks like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span><span class="err">u&#39;US-1&#39;:</span> <span class="err">&#39;VPNUP&#39;,</span> <span class="err">u&#39;US-2&#39;:</span> <span class="err">&#39;VPNUP&#39;,</span> <span class="err">u&#39;EU-1&#39;:</span> <span class="err">&#39;VPNUP&#39;,</span> <span class="err">u&#39;EU-2&#39;:</span> <span class="err">&#39;VPNUP&#39;,</span> <span class="err">u&#39;CN-1&#39;:</span> <span class="err">&#39;VPNDOWN&#39;</span><span class="p">}</span>
</span></span></code></pre></div><p>The buildrows function is then just grabbing each firewall and then parsing this data to figure out what it has connections to - then generates a row in our table.</p>
<p>With that done, our index function within views.py needs a little work to start displaying our table. You&rsquo;ll also notice we&rsquo;re importing the models we created, as well as a template loader - which I&rsquo;ll get to in a moment. So here is what that function will look like when we&rsquo;re done:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">.models</span> <span class="kn">import</span> <span class="n">Firewall</span><span class="p">,</span> <span class="n">Datacenter</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">django.template</span> <span class="kn">import</span> <span class="n">loader</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">sys</span><span class="o">,</span> <span class="nn">ast</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">index</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">firewall_list</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="n">datacenter_list</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="n">firewall_status</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># This will grab each datacenter object (if they&#39;re marked active) from the </span>
</span></span><span class="line"><span class="cl">    <span class="c1"># database and add them to a list</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">each</span> <span class="ow">in</span> <span class="n">Datacenter</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">order_by</span><span class="p">(</span><span class="s1">&#39;datacenter_code&#39;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">each</span><span class="o">.</span><span class="n">datacenter_active</span> <span class="o">==</span> <span class="kc">True</span><span class="p">:</span> <span class="n">datacenter_list</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">each</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Same thing here - grab our firewalls and append to a list</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">each</span> <span class="ow">in</span> <span class="n">Firewall</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">order_by</span><span class="p">(</span><span class="s1">&#39;firewall_name&#39;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">each</span><span class="o">.</span><span class="n">firewall_active</span> <span class="o">==</span> <span class="kc">True</span><span class="p">:</span> <span class="n">firewall_list</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">each</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Now we pass those lists to the buildrows function to assemble our HTML table</span>
</span></span><span class="line"><span class="cl">    <span class="n">firewall_status</span> <span class="o">=</span> <span class="n">buildrows</span><span class="p">(</span><span class="n">firewall_list</span><span class="p">,</span> <span class="n">datacenter_list</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># We&#39;re also going to apply a template to make the page look fancy</span>
</span></span><span class="line"><span class="cl">    <span class="n">template</span> <span class="o">=</span> <span class="n">loader</span><span class="o">.</span><span class="n">get_template</span><span class="p">(</span><span class="s1">&#39;web/index.html&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># This is taking all of our lists, and passing them into our HTML template</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># so that it can build the page semi-automatically</span>
</span></span><span class="line"><span class="cl">    <span class="n">context</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;datacenter_list&#39;</span><span class="p">:</span><span class="n">datacenter_list</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;firewall_list&#39;</span><span class="p">:</span> <span class="n">firewall_list</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;firewall_status&#39;</span><span class="p">:</span><span class="n">firewall_status</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">template</span><span class="o">.</span><span class="n">render</span><span class="p">(</span><span class="n">context</span><span class="p">,</span><span class="n">request</span><span class="p">))</span>
</span></span></code></pre></div><p>Alright - our views.py file is now complete. I wanted to keep the index function kind of simple, so we just have it pull data from the database, pass it to another function for processing, then return it to the browser for display.</p>
<p>So in the code above, you might have noticed that I was using an HTML template to make the page look fancier. I did this by going out to Google and finding a free CSS template for HTML tables. I won&rsquo;t post the CSS here, but there are plenty of free templates out there - just find whatever suits your taste. Once you have that CSS file, create a directory (within our app folder) called static and place the CSS file in there.</p>
<p>Then, it&rsquo;s one simple template HTML file to load our CSS and render our table. Within the app directory, I created a templates folder, then a folder called web within that. I added a new file called index.html - which looks like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">title</span><span class="p">&gt;</span>SRX VPN Dashboard<span class="p">&lt;/</span><span class="nt">title</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">align</span><span class="o">=</span><span class="s">&#34;center&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    {% load static %}
</span></span><span class="line"><span class="cl">    <span class="c">&lt;!-- Load the CSS template here --&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">link</span> <span class="na">rel</span><span class="o">=</span><span class="s">&#34;stylesheet&#34;</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text/css&#34;</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;{% static &#39;style.css&#39; %}&#34;</span> <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="c">&lt;!-- Add a header to the table --&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;table-title&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">&lt;</span><span class="nt">h3</span><span class="p">&gt;</span>SRX VPN Dashboard<span class="p">&lt;/</span><span class="nt">h3</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c">&lt;!-- Create our first row, which will be a list of each data center location --&gt;</span>
</span></span><span class="line"><span class="cl">    {% if firewall_list %}
</span></span><span class="line"><span class="cl">        <span class="p">&lt;</span><span class="nt">table</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;container&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">&lt;</span><span class="nt">th</span><span class="p">&gt;&lt;/</span><span class="nt">th</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        {% for datacenter in datacenter_list %}
</span></span><span class="line"><span class="cl">            <span class="p">&lt;</span><span class="nt">th</span><span class="p">&gt;</span>{{ datacenter.datacenter_code }}<span class="p">&lt;/</span><span class="nt">th</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        {% endfor %}
</span></span><span class="line"><span class="cl">        <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c">&lt;!-- Then we add a row for each firewall, followed by its status 
</span></span></span><span class="line"><span class="cl"><span class="c">             for each datacenter --&gt;</span>
</span></span><span class="line"><span class="cl">        {% for firewall in firewall_status %}
</span></span><span class="line"><span class="cl">            <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">            {{firewall|safe}}
</span></span><span class="line"><span class="cl">            <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        {%endfor%}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="p">&lt;/</span><span class="nt">table</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="c">&lt;!-- If something goes wrong, we just print an error --&gt;</span>
</span></span><span class="line"><span class="cl">    {% else %}
</span></span><span class="line"><span class="cl">        No firewalls were found.
</span></span><span class="line"><span class="cl">    {% endif %}
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>Guess what? The dashboard is complete! Now we can run our Django server using &lsquo;./manage.py runserver&rsquo; and take a look at what we have created.</p>
<p><img alt="image" loading="lazy" src="/content/images/2021/01/dashboard-nodata.png#center"></p>
<p>Pretty simplistic - but we&rsquo;re getting very close to what I wanted to accomplish. The dashboard loads and generates the table, but it doesn&rsquo;t have any data yet.</p>
<p>In the next post - We&rsquo;ll take a look at building the backend script to poll all of our SRX firewalls for VPN connections. I hope you enjoyed reading this - let me know what you think in the comments below!</p>
<hr>
<p>This is a multi-part series - Check out the other posts:</p>
<ul>
<li><a href="/building-a-vpn-dashboard-using-django-and-junos-pyez-part-1-initial-thoughts/">Part 1 - Initial Thoughts</a></li>
<li><a href="/building-a-vpn-dashboard-using-django-and-junos-pyez-part-2-learning-django/">Part 2 - Leaning Django</a></li>
<li><a href="/building-a-vpn-dashboard-using-django-and-junos-pyez-part-4-polling-the-srx/">Part 4 - Polling the SRX</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Building a VPN Dashboard using Django and JunOS pyEZ (Part 2 – Learning Django)</title>
      <link>https://0x2142.com/building-a-vpn-dashboard-using-django-and-junos-pyez-part-2-learning-django/</link>
      <pubDate>Tue, 13 Jun 2017 08:00:18 +0000</pubDate>
      <guid>https://0x2142.com/building-a-vpn-dashboard-using-django-and-junos-pyez-part-2-learning-django/</guid>
      <description>In order to build my Juniper SRX VPN dashboard, I opted to use Django. Let&amp;rsquo;s take a quick look at how I got started.</description>
      <content:encoded><![CDATA[<p>This is a multi-part series - If you just hit this page, please check out the prior post first!</p>
<ul>
<li><a href="/building-a-vpn-dashboard-using-django-and-junos-pyez-part-1-initial-thoughts/">Part 1 - Initial Thoughts</a></li>
</ul>
<hr>
<p>Once I figured out the overall idea behind what I wanted to do - the next big part was figuring out where to start. As I mentioned in part 1, I had never used Django before starting this project. So I figured that learning how to to use a new web framework might be the best place to begin.</p>
<p>The great thing, is that Django already has some great tutorials on how to get the packages installed and begin working on your first project. Their tutorials can be found here:</p>
<ul>
<li><a href="https://docs.djangoproject.com/en/1.11/intro/install/">Installing Django</a></li>
<li><a href="https://docs.djangoproject.com/en/1.11/intro/tutorial01/">Writing your first app</a> - Credit to this page for some parts of the examples below</li>
</ul>
<p>I used both of these tutorials when I wrote my dashboard, and pretty much just modified what they were doing to fit my needs. After I got a baseline down, it was pretty easy to keep going and complete what I wanted to do.</p>
<p><strong>Note:</strong> Just as a pure warning - I&rsquo;m a network admin, not a professional programmer. Python, Django, and the Junos PyEZ libraries make it easy enough for me to make my ideas into a reality. However, that doesn&rsquo;t mean I write perfect or even great code. (I learned Python from <a href="https://learnpythonthehardway.org/book/">Learn Python The Hard Way</a> - I highly recommend this site, as it was a great resource!)</p>
<p>Alright, with that out of the way - Let&rsquo;s get started!</p>
<h2 id="step-one---installing-django">Step One - Installing Django</h2>
<p>First thing is first, I learned Python on the 2.7.x chain, and I&rsquo;ve been terrible about forcing myself to switch to 3.x. So for the purposes of this tutorial, everything I&rsquo;ve written was intended for Python 2.7.5.
Installing the actual Django package is a super simple task (if you use <a href="https://pip.pypa.io/en/latest/installing/#installing-with-get-pip-py">pip</a>):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">[root@0x2142-Centos ~]# pip install django
</span></span><span class="line"><span class="cl">Collecting django
</span></span><span class="line"><span class="cl"> Downloading Django-1.11.2-py2.py3-none-any.whl (6.9MB)
</span></span><span class="line"><span class="cl"> 100% |████████████████████████████████| 7.0MB 140kB/s
</span></span><span class="line"><span class="cl">Collecting pytz (from django)
</span></span><span class="line"><span class="cl"> Downloading pytz-2017.2-py2.py3-none-any.whl (484kB)
</span></span><span class="line"><span class="cl"> 100% |████████████████████████████████| 491kB 1.9MB/s
</span></span><span class="line"><span class="cl">Installing collected packages: pytz, django
</span></span><span class="line"><span class="cl">Successfully installed django-1.11.2 pytz-2017.2
</span></span></code></pre></div><h2 id="step-two---get-junos-pyez-libraries">Step Two - Get JunOS pyEZ libraries</h2>
<p>I already wrote about this earlier in <a href="/getting-started-with-junos-pyez">Getting Started with JunOS PyEZ</a>. If you don&rsquo;t already have the JunOS packages installed, check out that page for the instructions.</p>
<h2 id="step-three---create-the-project">Step Three - Create the Project</h2>
<p>Django is pretty wonderful from my brief experiences with it. They package a number of tools that make starting a new project very simple.</p>
<p>So to start off with our project, we&rsquo;re going to use the django-admin tool:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">[root@0x2142-Centos ~]# django-admin startproject junos-dashboard
</span></span></code></pre></div><p>This will create a base directory structure for us to begin our project. We&rsquo;ll dive into what these files and folders are as we touch them - but for now, you&rsquo;ll need to make one minor edit to ~/junos-dashboard/junos-dashboard/settings.py. Open that file up in your favorite text editor, and find the ALLOWED_HOSTS setting. Add in the IP address of your linux VM, like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">ALLOWED_HOSTS = [&#39;10.2.32.90&#39;]
</span></span></code></pre></div><p>Once that&rsquo;s done, go to the root of your project (~/junos-dashboard/) and test out the web server:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">[root@0x2142-Centos junos-dashboard]# ./manage.py runserver 10.2.32.90:8000
</span></span></code></pre></div><p>Django includes it&rsquo;s own web server for development and testing, which is extremely helpful. Once you run that command, try to hit your page in a browser and just make sure it loads.</p>
<p>Once that&rsquo;s done, make sure you&rsquo;re in the root of your project - and we will create our dashboard application:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">[root@0x2142-Centos junos-dashboard]# django-admin startapp vpn
</span></span></code></pre></div><p>This command just creates <em>another</em> directory structure within your project. I think this is helpful though, because then I don&rsquo;t have to worry about which files I need to create or how they should be laid out - Django just handles that for us.</p>
<h2 id="step-four---getting-our-web-server-to-return-our-app-page">Step Four - Getting our web server to return our app page</h2>
<p>Okay - So next we need to make a couple of minor edits in a few files. Within our app directory, there are two files (views.py and urls.py) that handle the main parts of our web interface. Note - urls.py doesn&rsquo;t exist by default and you will have to create it!</p>
<p>Within views.py, we&rsquo;re going to create our dashboard homepage with a quick test response:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Import stuff</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">django.shortcuts</span> <span class="kn">import</span> <span class="n">render</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">django.http</span> <span class="kn">import</span> <span class="n">HttpResponse</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">django.template</span> <span class="kn">import</span> <span class="n">loader</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">sys</span><span class="o">,</span> <span class="nn">ast</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># This is our index page</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">index</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Return some plain text - just so we know it worked!</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="s2">&#34;This page will eventually be a magical dashboard!&#34;</span><span class="p">)</span>
</span></span></code></pre></div><p>This file is where we will eventually be putting in all the code for our dashboard page. When we hit our webserver, this file is executed to generate the view that we&rsquo;ll see.</p>
<p>Next, we need to create a urls.py file within the same folder as views.py. This file is just a way to direct traffic. If we receive traffic to a regex of ^$, which is no path, then we are going to redirect it to the index function in views.py:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Import stuff</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">django.conf.urls</span> <span class="kn">import</span> <span class="n">url</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">.</span> <span class="kn">import</span> <span class="n">views</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#Redirect to index function in views.py</span>
</span></span><span class="line"><span class="cl"><span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl"> <span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">&#39;^$&#39;</span><span class="p">,</span> <span class="n">views</span><span class="o">.</span><span class="n">index</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s1">&#39;index&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span></code></pre></div><p>Finally, we&rsquo;re going to grab the urls.py file in our <em>project</em> folder - not the one we just edited within the app. So this should be ~/junos-dashboard/junos-dashboard/urls.py. In this file, we just need to tell the web server to include the urls.py file we created when evaluating traffic. This file should already exist and have content in it for routing traffic to the admin page, so just add an entry under urlpatterns - should look like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl"> <span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">&#39;^$&#39;</span><span class="p">,</span> <span class="n">include</span><span class="p">(</span><span class="s1">&#39;vpn.urls&#39;</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl"> <span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">&#39;^admin/&#39;</span><span class="p">,</span> <span class="n">admin</span><span class="o">.</span><span class="n">site</span><span class="o">.</span><span class="n">urls</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span></code></pre></div><p>I just want to point out that in this case, we&rsquo;re catching traffic again with the ^$ regex. This is so that any traffic to the main page (in this case <a href="http://10.2.32.90:8000">http://10.2.32.90:8000</a>) is automatically sent to our vpn app. However, if you had multiple apps running, you might want to give each one it&rsquo;s own directory.</p>
<p>Go ahead and execute the runserver tool again, and validate that you&rsquo;re able to successfully hit the page and retrieve the test text we put in.</p>
<hr>
<p>Okay, I think we&rsquo;re going to stop here for this post. At this point we have Django installed and our project/app created. We also have the beginnings of our dashboard page. In the next post, we&rsquo;ll actually start building out the dashboard interface!</p>
<hr>
<p>This is a multi-part series - Check out the other posts:</p>
<ul>
<li><a href="/building-a-vpn-dashboard-using-django-and-junos-pyez-part-1-initial-thoughts/">Part 1 - Initial Thoughts</a></li>
<li><a href="/building-a-vpn-dashboard-using-django-and-junos-pyez-part-3-creating-the-dashboard/">Part 3 - Creating the Dashboard</a></li>
<li><a href="/building-a-vpn-dashboard-using-django-and-junos-pyez-part-4-polling-the-srx/">Part 4 - Polling the SRX</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Building a VPN Dashboard using Django and JunOS pyEZ (Part 1 - Initial thoughts)</title>
      <link>https://0x2142.com/building-a-vpn-dashboard-using-django-and-junos-pyez-part-1-initial-thoughts/</link>
      <pubDate>Wed, 07 Jun 2017 08:32:04 +0000</pubDate>
      <guid>https://0x2142.com/building-a-vpn-dashboard-using-django-and-junos-pyez-part-1-initial-thoughts/</guid>
      <description>I decided to build a web GUI to monitor Juniper SRX VPN tunnels. Here&amp;rsquo;s what I was thinking&amp;hellip;</description>
      <content:encoded><![CDATA[<p>So maybe you&rsquo;re like me - you&rsquo;ve done a bit of everything in the past, but now you&rsquo;ve specialized on something (like networking). Know what the best part of that is? Using all that knowledge and experience to make things happen. In this case, I&rsquo;m talking about some of my prior experiences in scripting in automation, paired with some pretty great network APIs.</p>
<p>One thing my current job has never had is a good way to view VPN tunnel status between our firewalls. Our Check Point firewalls don&rsquo;t really provide a good high-level view, which has unfortunately caused some confusion around whether or not a site-to-site VPN tunnel is currently established. Luckily, over the past year I&rsquo;ve had the opportunity to install over 20 new Juniper SRX firewalls - mostly made up of SRX 1500 and SRX 345 models. I&rsquo;ve written a bit before about the JunOS APIs and pyEZ (their Python library), but I&rsquo;m always really excited at a new use case for network automation.</p>
<p>So recently I decided that it would be great to build a web-based dashboard, which would query all of our SRX firewalls for currently connected VPN tunnels. Approximately 90% of the current tunnels are site-to-site between our own data center locations, with the other 10% being external to customers. My idea was that I would have a simple HTML table, which would show each data center along the top and bottom and whether or not each was connected to each other, kinda like this:</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th><strong>Location 1</strong></th>
          <th><strong>Location 2</strong></th>
          <th><strong>Location 3</strong></th>
          <th><strong>Location 4</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Location 1</strong></td>
          <td>N/A</td>
          <td>UP</td>
          <td>UP</td>
          <td>UP</td>
      </tr>
      <tr>
          <td><strong>Location 2</strong></td>
          <td>UP</td>
          <td>N/A</td>
          <td>UP</td>
          <td>DOWN!</td>
      </tr>
      <tr>
          <td><strong>Location 3</strong></td>
          <td>UP</td>
          <td>UP</td>
          <td>N/A</td>
          <td>UP</td>
      </tr>
      <tr>
          <td><strong>Location 4</strong></td>
          <td>UP</td>
          <td>DOWN!</td>
          <td>UP</td>
          <td>N/A</td>
      </tr>
  </tbody>
</table>
<p>I wasn&rsquo;t really concerned about making it look fancy - just dynamically updatable by whatever backend mechanism I used. Speaking of which, I had also assumed I would just be writing some simple Python script to pull VPN info from each device, then just write it to an HTML file. I haven&rsquo;t really done much web stuff with Python in the past, so I set out to learn a bit and figure out what the best approach might be.</p>
<p>I ended up settling on trying out <a href="https://www.djangoproject.com">Django</a> to write the frontend web stuff. I had never used it before, but I&rsquo;ve been interested in trying - and what&rsquo;s a better time to learn, than when you have something that needs to be accomplished? One thing that really pushed me towards Django for this project was the built-in administration page. This was huge for me, because it meant that I could easily have a way for other people to update the web dashboard. Whenever a new location came online, I wouldn&rsquo;t have to go update the script directly - anyone on my team could log into the admin page and make the changes.</p>
<p>In this post I just wanted to get through what my ideas were behind this project. In the next few weeks, I&rsquo;ll begin explaining how I built the dashboard using Django and use pyEZ to scrape VPN status from each firewall.</p>
<p>Thoughts? Drop a comment below!</p>
<hr>
<p>This is a multi-part series - Check out the other posts:</p>
<ul>
<li><a href="/building-a-vpn-dashboard-using-django-and-junos-pyez-part-2-learning-django/">Part 2 - Leaning Django</a></li>
<li><a href="/building-a-vpn-dashboard-using-django-and-junos-pyez-part-3-creating-the-dashboard/">Part 3 - Creating the Dashboard</a></li>
<li><a href="/building-a-vpn-dashboard-using-django-and-junos-pyez-part-4-polling-the-srx/">Part 4 - Polling the SRX</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Getting Started with JunOS PyEZ</title>
      <link>https://0x2142.com/getting-started-with-junos-pyez/</link>
      <pubDate>Tue, 24 Jan 2017 08:00:20 +0000</pubDate>
      <guid>https://0x2142.com/getting-started-with-junos-pyez/</guid>
      <description>Quickly get started using Juniper&amp;rsquo;s Python SDK &amp;amp; interact with JunOS devices</description>
      <content:encoded><![CDATA[<p><sup>Note: I may receive commissions for purchases made through links in this post. This is to help support my blog and does not have any impact on my recommendations.</sup></p>
<hr>
<blockquote>
<p>This guide is written for CentOS 7. If you&rsquo;re running another distro, find your dependencies <a href="https://www.juniper.net/techpubs/en_US/junos-pyez1.0/topics/task/installation/junos-pyez-server-installing.html">here</a>.</p></blockquote>
<p>Last year we had to begin migrating off of some of our older Juniper SSG firewalls since we were beginning to push them to their throughput limits. We evaluated a couple of vendors but ultimately decided to stay with Juniper and purchase SRX 1500 firewalls, which are capable of up to 10G throughput. After a while of working with these firewalls, I have to say they&rsquo;re pretty solid devices and I&rsquo;m extremely happy with them. One of the main reasons I like them so much is the ease of automation, which is what we&rsquo;re going to dive into today. If you need a device for lab/test - Amazon has the <a href="https://www.amazon.com/gp/product/B01ICEO2U4/ref=as_li_qf_asin_il_tl?ie=UTF8&amp;tag=0x2142-20&amp;creative=9325&amp;linkCode=as2&amp;creativeASIN=B01ICEO2U4&amp;linkId=35fbe8300af4e5d1e26e7a860782b3ca">SRX 300</a> for less than $300. I&rsquo;ll likely be picking one up in the near future for easier automation testing.</p>
<p>Juniper provides an awesome library for SRX management called PyEZ. Here is what we need to get the toolkit ready to use in our scripts:</p>
<h2 id="install-dependencies">Install dependencies</h2>
<p>I run CentOS here, so I just needed to grab the following packages:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">yum -y install python-devel libxml2-devel libxslt-devel gcc openssl openssl-devel libffi-devel
</span></span></code></pre></div><p>Just a quick note - Juniper&rsquo;s web page doesn&rsquo;t actually mention openssl-devel, but their tools will fail to install without it</p>
<h2 id="get-pip">Get pip</h2>
<p>If you don&rsquo;t have it already, then download the pip installer <a href="https://bootstrap.pypa.io/get-pip.py">here</a>. Then just run the following:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">python get-pip.py
</span></span></code></pre></div><h2 id="install-pyez">Install PyEZ</h2>
<p>Once everything else is set, this is the easy part:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pip install junos-eznc
</span></span></code></pre></div><p>After all that is done, we can get to the exciting part: automating something so you don&rsquo;t have to do it anymore! So here is what we&rsquo;re going to throw in our script just to get started:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Import the JunOS modules we need</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">jnpr.junos</span> <span class="kn">import</span> <span class="n">Device</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Define the login credentials and address for the target firewall</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Need to provide device address, user account, and password</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Note: Juniper also provides authentication via key-pair, which would be more secure than username/password</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">srx</span> <span class="o">=</span> <span class="n">Device</span><span class="p">(</span><span class="s1">&#39;10.10.10.10&#39;</span><span class="p">,</span> <span class="n">user</span><span class="o">=</span><span class="s1">&#39;deviceuser&#39;</span><span class="p">,</span> <span class="k">pass</span><span class="o">=</span><span class="s1">&#39;devicepass&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Open the connection</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">srx</span><span class="o">.</span><span class="n">open</span><span class="p">()</span>
</span></span></code></pre></div><p>Once you have that going, it&rsquo;s pretty easy to start making calls to collect data or change the configuration. In one of the first projects I used this for, I was making use of the API to reset VPN tunnels. This was due to an issue with a cross-vendor tunnel, which would occasionally break during VPN re-keys and only negotiate a uni-directional tunnel. So the script was written to detect a uni-directional flow of traffic, then log into the SRX and reset the VPN - which would force a renegotiation and fix the issue.</p>
<p>So in order to accomplish the SRX-side of that script, I used the following to reset the IKE and IPSec security associations:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Clear IKE security association for a given peer</span>
</span></span><span class="line"><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">srx</span><span class="o">.</span><span class="n">rpc</span><span class="o">.</span><span class="n">clear_ike_security_association</span><span class="p">(</span><span class="n">peer_address</span><span class="o">=</span><span class="s1">&#39;20.20.20.20&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check to see if command was accepted</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">response</span> <span class="o">==</span> <span class="kc">True</span><span class="p">:</span> <span class="nb">print</span> <span class="s2">&#34;IKE SA Cleared&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Clear IPSec security association for a given peer</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Note: In this case, you need to provide the index ID</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Get the index ID from running &#39;*show security ipsec sa&#39; *on the SRX</span>
</span></span><span class="line"><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">srx</span><span class="o">.</span><span class="n">rpc</span><span class="o">.</span><span class="n">clear_ipsec_security_association</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="s1">&#39;123456&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Same thing - Check to make sure the command succeeded</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">response</span> <span class="o">==</span> <span class="kc">True</span><span class="p">:</span> <span class="nb">print</span> <span class="s2">&#34;IPSec SA Cleared&#34;</span>
</span></span></code></pre></div><p>This provides a pretty basic example of how easy it is to control the SRX via a Python script. You might be wondering: This is great, but how do I find the names of those calls? Well, Juniper made that extremely simple as well! Just take any command on the SRX and pipe it through <code>display xml rpc</code> and the device will tell you exactly what you need. For example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl">root@SRX123&gt; show security ipsec sa | display xml rpc
</span></span><span class="line"><span class="cl"><span class="nt">&lt;rpc-reply</span> <span class="na">xmlns:junos=</span><span class="s">&#34;http://xml.juniper.net/junos/15.1X49/junos&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;rpc&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&lt;get-security-associations-information&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&lt;/get-security-associations-information&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;/rpc&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;cli&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&lt;banner&gt;</span>{primary:node0}<span class="nt">&lt;/banner&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;/cli&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/rpc-reply&gt;</span>
</span></span></code></pre></div><p>In this case, we are running the command which would normally print all of the connected IPSec tunnels. So the section under <em>rpc</em> is where we want to look. Now just take the get-security-associations-information, and change the dashes to underscores. So to use this in a script we would call srx.rpc.get_security_associations_information(). I&rsquo;ve actually used this command to build a VPN status dashboard using Python and Django, which I accomplished by just pulling all the IPSec tunnels and parsing them into a web table.</p>
<p>As a final note, I would <strong>highly</strong> recommend creating a separate service account on the SRX for scripting. A separate user account with limited permissions would be the recommended way to go here. It might seem like a pain to set up, but it&rsquo;s worth it in terms of security. Here is what I have configured on my SRX firewalls for the API service account:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="err">system</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="err">login</span> <span class="err">{</span>
</span></span><span class="line"><span class="cl">        <span class="err">class</span> <span class="err">api-class</span> <span class="err">{</span>
</span></span><span class="line"><span class="cl">            <span class="err">permissions</span> <span class="err">security-control;</span>
</span></span><span class="line"><span class="cl">            <span class="err">allow-commands</span> <span class="nt">&#34;(clear security ike)|(clear security ipsec)&#34;</span><span class="err">;</span>
</span></span><span class="line"><span class="cl">            <span class="err">deny-commands</span> <span class="s2">&#34;(clear)|(file)|(file show)|(help)|(load)|(monitor)|(op)|(request)|(save)|(set)|(start)|(test)&#34;</span><span class="err">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="err">user</span> <span class="err">apiuser</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="err">full-name</span> <span class="err">API_Service_Acct;</span>
</span></span><span class="line"><span class="cl">            <span class="err">uid</span> <span class="err">125;</span>
</span></span><span class="line"><span class="cl">            <span class="err">class</span> <span class="err">api-class;</span>
</span></span><span class="line"><span class="cl">            <span class="err">authentication</span> <span class="err">{</span>
</span></span><span class="line"><span class="cl">                <span class="err">encrypted-password</span> <span class="err">*&lt;encrypted</span> <span class="err">string</span> <span class="err">here&gt;*</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="err">}</span>
</span></span><span class="line"><span class="cl">    <span class="err">}</span>
</span></span><span class="line"><span class="cl"><span class="err">}</span>
</span></span></code></pre></div><p>So using the above example, you should only define the necessary commands in the <em>allow-commands</em> section. If the account is ever compromised, we are severely limiting the amount of damage that could be done. A little extra effort, but potentially a big payoff.</p>
<p>So what did you think of this tutorial? Helpful? Have questions? Let me know in the comments below!</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
