<?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>Python on 0x2142 | Networking Nonsense</title>
    <link>https://0x2142.com/tags/python/</link>
    <description>Recent content in Python 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>Sat, 12 Mar 2022 15:21:35 +0000</lastBuildDate>
    <atom:link href="https://0x2142.com/tags/python/index.xml" rel="self" type="application/rss+xml" />
    <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>Where is all the Automation?</title>
      <link>https://0x2142.com/where-is-all-the-automation/</link>
      <pubDate>Tue, 24 Jul 2018 10:00:03 +0000</pubDate>
      <guid>https://0x2142.com/where-is-all-the-automation/</guid>
      <description>Learning Python for network automation doesn&amp;rsquo;t have to be scary. Let&amp;rsquo;s look at how to get started</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><em>The future is APIs! SD-EVERYTHING! Automation! Orchestration! Artificial Intelligence and Machine Learning!</em>
Sound familiar? It&rsquo;s all part of the messaging going around in just about everything IT-related. With as much as you keep hearing about it, you might think that it&rsquo;s all anyone is doing anymore. Yet it still just seems like not a whole lot of people are really getting into it in my area. Every vendor event I&rsquo;ve gone to this year has asked attendees the same questions: &ldquo;How many of you are leveraging the APIs in your network hardware/software?&rdquo;. And every time the same answer - maybe two or three people in a room of 40 raise their hands.</p>
<p>So where is the problem? Is all of this just marketing fluff or am I just talking to the wrong people?</p>
<p>Let&rsquo;s think about this from a typical network admin&rsquo;s perspective. Shifting from traditional CLI to automation and APIs can seem difficult or overwhelming. Let&rsquo;s say I want to automate a new VLAN deployment. <em>Oh, you&rsquo;re telling me I need to stop and learn vendor APIs… but before that I need to understand how to write scripts. But I&rsquo;ve never even programmed something before. There are dozens of languages - how do I pick one? How much fundamental programming knowledge do I really need to have before starting? I don&rsquo;t want to be a developer!</em></p>
<p>Okay, okay - just stop there for a second. No one is asking you to drop networking and write code for a living. The end goal of all this programmability stuff isn&rsquo;t to turn networkers into developers - It&rsquo;s to enable network/systems admins to be more efficient at their jobs. Why  copy/paste the same config change to 100+ devices, if you can mass-deploy the change via an API? That&rsquo;s a lot of time savings that could be used toward educating yourself on new products, planning other projects, or thinking about your ideal network design.</p>
<p>I&rsquo;ve heard a lot of the same things over the past few years:</p>
<blockquote>
<p><em>&ldquo;Programming is difficult&rdquo; or &ldquo;I don&rsquo;t know where to start&rdquo;</em></p></blockquote>
<p>Try learning Python. It&rsquo;s simple to get started and you can build from there.</p>
<blockquote>
<p><em>&ldquo;I don&rsquo;t know what an API is or how to use it&rdquo;</em></p></blockquote>
<p>Don&rsquo;t worry about that yet - start with learning the basics and APIs will make sense later.</p>
<blockquote>
<p><em>&ldquo;I&rsquo;m not a developer&rdquo;</em></p></blockquote>
<p>No one is asking you to be one! But learning the basics of scripting and automation gives you a whole new toolset to solve problems.</p>
<p>For me personally - I would never want to be a developer. I can&rsquo;t stand the thought of coming into work every day and just writing code. Some people might enjoy that, but for me it doesn&rsquo;t sound like fun. However - I enjoy writing scripts to solve problems, especially when it ends up making my job easier. I think that&rsquo;s the part where some people tend to get stuck though. A lot of automation sounds like I need to be able to develop a huge 10,000+ line application to pull data from 15 sources and aggregate it to make intelligent network changes. Ehhh&hellip; Nope, not really. But what about just a quick script that runs every 5 minutes to check an interface statistic, and email you when a particular threshold is exceeded? Realistically that could be done in less than 50-100 lines of a script and maybe 30 minutes worth of work.</p>
<p>Still not interested? That&rsquo;s okay too. Traditional networking isn&rsquo;t going away any time soon, and over time the vendors will write all of that automation for you. They will package it up in a pretty GUI and sell it off to companies that want it. In fact, this has already happening and has been for quite some time. This isn&rsquo;t a bad thing - vendors need to make money, and not all companies will have the time or skilled resources to automate all the things. However, a network admin who can write their own scripts/automation won&rsquo;t be exclusively tied to a vendor to help them - and instead they will be empowered to solve more problems themselves.</p>
<p>Where do you get started? I already wrote a bit earlier this year on a few resources for learning Python - which you can find <a href="/you-should-automate-something-this-year/">here</a>. I also wanted to point out some other great resources that are a bit more specific to using those skills for network automation:</p>
<ul>
<li>
<p><a href="https://pynet.twb-tech.com/email-signup.html">Python For Network Engineers</a> - Don&rsquo;t know anything about Python yet? Start here! This is a free course provided by Kirk Byers for anyone who is interested in using Python for network automation. Once a week you&rsquo;ll get an email with all the great free content, but it will be up to you to spend time going through it. Go sign up, and set aside an hour or two each week to practice.</p>
</li>
<li>
<p><a href="https://developer.cisco.com">Cisco DevNet</a> - There is a ton of great content here. While DevNet does offer some tutorials on basic Python fundamentals,  the real value here is examples on how to use some network APIs (NX-OS, Meraki, UCS, etc). Also - one of the best parts about DevNet is the sandboxes they offer. Want to write scripts against the FirePower Management Center, but you don&rsquo;t have one to test with? Well with DevNet you can get access to one!  Get familiar with your Python basics, then come here to see where you can start using those skills with your existing infrastructure.</p>
</li>
<li>
<p><a href="https://amzn.to/2L7EiL1">Network Programmability and Automation</a> - This is a fantastic book. Not free, but it is well worth the ~$30. Once you have a good handle on how to write some basic network automation with Python, I highly recommend picking this up. While Python is covered here, the book does a great job of introducing you to all of the other toolsets available. Curious about how Linux or Ansible fit into network automation? You can find out here - and learn about APIs and source control systems too!</p>
</li>
</ul>
<hr>
<p>So - What are you waiting for? Go get started, and see what you can accomplish. Learn the basics - and keep an open mind for opportunities to use those skills.</p>
<p>Have suggestions on where else to learn? Comment below!</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>You Should Automate Something This Year</title>
      <link>https://0x2142.com/you-should-automate-something-this-year/</link>
      <pubDate>Tue, 09 Jan 2018 08:00:06 +0000</pubDate>
      <guid>https://0x2142.com/you-should-automate-something-this-year/</guid>
      <description>Need a push to start learning network automation?</description>
      <content:encoded><![CDATA[<p>Maybe 2018 isn&rsquo;t off to quite the best start. Recent processor vulnerabilities have people scrambling to patch and update systems. Stuff like this ends up being a fairly large sink of time for any systems/network administrator. The worst part is that we have practically no control of when this stuff happens or how much time it&rsquo;s going to take to resolve. What we <em>do</em>have control over, however, is our ability to make our own lives easier through automation.</p>
<p>A lot of people take the beginning of the year to make new resolutions and goals for the coming months. So this year, I&rsquo;m urging you to add one more to your list: Try and automate something that will make your life easier.</p>
<h2 id="where-to-start">Where to Start</h2>
<p>What you choose to automate doesn&rsquo;t need to be extremely complex or elaborate, just anything that will save you a little bit of time. Never used a scripting language? I can&rsquo;t recommend enough using <a href="https://learnpythonthehardway.org/book/">Learn Python The Hard Way</a> to start learning. This site is what I used about five years ago to get into scripting. Another great resource is <a href="https://www.codecademy.com/catalog/language/python">CodeAcademy</a>, where they have web-based interactive tutorials (also check out their specific module on <a href="https://www.codecademy.com/ru/courses/python-intermediate-en-6zbLp/0/1?curriculum_id=50ecb8cb058fd2ebda00003b">Python and APIs</a>).</p>
<p>Once you get a good handle on the basics, start thinking about repeatable tasks that are great candidates for automation. Start with something simple - maybe a script that prompts the user for information, then generates the command-line entries to configure new switch ports. Then someone can easily copy and paste the commands from the script output to achieve their desired configuration. Something like this might not immediately seem like a huge time savings, but it gives you a place to start and get familiar with what is possible. Once you get something like that working, it&rsquo;s not too difficult to extend the script later and actually include calls to the switch APIs to automate the changes.</p>
<p>Is Python/scripting your only option? Not at all. There are also automation toolsets like Ansible, which can abstract the code layer a bit. For quite a number of systems that I deploy to an average datacenter, I already have Ansible playbooks written to handle that work. My actual time involved in deploying standard network monitoring applications and tools to a new datacenter went from hours to less than five minutes. For the purposes of this post, I&rsquo;ll be speaking more to the Python/Scripting side. However, the important point is not necessarily which toolset you choose - it&rsquo;s the fact that you try to use any one of these tools or others to automate something.</p>
<h2 id="stick-with-it">Stick with it</h2>
<p>Learning a scripting language at first might seem like a very unnecessary and time consuming task. However, this is something that will pay off in the long run. When I started learning Python, all I wanted to do was parse data from several CSV files and combine the necessary data into one large file. Stupid simple script, but it saved me half an hour each day for a previously manual task.</p>
<p>I&rsquo;m not at all a fantastic developer by any means, nor would I want to write code for a profession. I just really enjoy problem solving, and sometimes the best way to solve a problem is with a bit of custom scripting. What gets me excited is the process of finding something that wouldn&rsquo;t normally be possible and knowing that I have the skills and ability to make it happen. Over the past five years, my basic level Python abilities have enabled me to work my way through a number of problems - or write various scripts to make my job easier.</p>
<p>You&rsquo;ll need to dedicate some time and put in the effort up front to learn a new skill, but trust me it will be worth it.</p>
<h2 id="look-for-new-opportunities">Look for New Opportunities</h2>
<p>Once you have learned the basics, start looking for ways to use your new skills. It&rsquo;s a different way of thinking in some cases, and will likely take a bit of adjustment. Whats that? Your load balancer doesn&rsquo;t have built-in reporting functionality to tell you how many server pools you have (and how many are actually fully functioning vs degraded)? Yep, they probably have an API which would be easy enough to pull that data from.</p>
<p>Over the years, I&rsquo;ve built scripts to automate load balancer configurations, generate reports, alert on BGP peering changes, auto-remediate IPSec VPN disconnects, and even a full <a href="/building-a-vpn-dashboard-using-django-and-junos-pyez-part-1-initial-thoughts/">IPSec VPN dashboard</a> (since the vendor doesn&rsquo;t supply one). As a network administrator, having the automation skills in Python has allowed me to accomplish many tasks that my co-workers have stated aren&rsquo;t possible (solely based on the functionality not being native to a product). Sure, I spend a bit of time up front writing and testing out scripts - but it not only saves me time/effort, but also my peers who I share the scripts with. For example, my team used to have a maintenance task that would take a full hour to complete on a monthly basis. About a week worth of my own effort to write a script, and all of that work is now automated into a 30 second process.</p>
<p>Think about how automation can help not just you, but your whole team.</p>
<h2 id="the-future-of-networking">The Future of Networking</h2>
<p>If you follow practically any news source for computer networking, I&rsquo;m sure you&rsquo;ve heard this already. Over the next few years the role of a traditional network administrator can and will change. Businesses are evolving more rapidly to meet customer demands, and we need to ensure that our networks can keep up. The only way this is going to be possible is through automation, or hiring an ridiculous number of people.</p>
<p>Practically all major networking vendors are integrating APIs into the new iterations of their device platforms. Some are fantastic, and some are less than ideal - but they&rsquo;re all working on it. In some way or another these new APIs will become a part of your job - whether you&rsquo;re writing the code to perform tasks or just using a script written by someone else. Does this mean we have an end to our careers in the future? No - the CLI will take a while to completely disappear. Even if it does go away completely, the role of a network admin will not be replaced, but evolve into something a bit different from what we know today. Even today, having the skills to automate tasks out of your daily job can allow you to spend your time on more important things (like a new network design, or that big project you haven&rsquo;t had time to look at yet).</p>
<p>You can probably get away with not learning scripting and automation for quite some time yet - but don&rsquo;t you want to make your life easier and be prepared for the future of your career? I know I do.</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>
