Last week of vacation

Haven’t done much labbing during my vacation and instead focused on reading/doing flashcards now and then, guess it’s important to unplug as well but at the same time I don’t want to lose all momentum… 🙂 Think i’ve managed to find a pretty good balance but the downside is that I haven’t had any interesting labs to posts about. I have however stumbled upon some pretty cool communities and other stuff that I thought I could share:

  • RouterGods – https://www.meetup.com/routergods
    A large community full of really talented & friendly people, comes with a large chat room full of CCIE/NP/NA study groups, lab-sessions, work help and much more. Highly recommended!
  • ACM – https://www.acm.org/
    “The world’s largest educational and scientific computing society”. One of the major benefits of becoming a member for $99 is that it also includes a membership to Safari Books (which originally costs $399!). There’s no secret that to become a CCIE you’ll have to read, a LOT, and Safari has pretty much everything you could ever ask for.
  • Dmitry Figols Network Programmability Lab on Youtube
    Dmitry has an excellent youtube-series i’ve been trying to catch up on where he shows how to build automation tools in Python using things like netmiko, YAML, Ansible, asyncio, NAPALM, NSO and much much more.
  • Anki Flashcards – https://apps.ankiweb.net/
    Wish I would have found this earlier instead of using Cram.com as my main resource for making flashcards during my earlier study sessions. Anki is free and really, really good. I’d recommend you always write your own flashcards to improve memorization but I also found an excellent resource written by Jedaiah Casey over at neckercube.com (3,500 high quality flashcards for the CCIE R&S 5.1). I’m sure there’s some benefit alternating your own flashcards with these to make sure you just haven’t missed some major points.

Finally finished my project!

With just a one day to go before vacation I finally finished the webportal i’ve been working on last few weeks/months and it’s now in production at the company I work at. Time to celebrate with a new whisky I think… 🙂

Purpose of the website is to semi-automate the building process and simplify config-generation (+ verification & deployment steps) for new links, nodes & services dynamically with just a few clicks. It will also rebuild existing solutions to newer architecture by just copy/paste’ing the current config. It’s written entirely in Python & Flask with some simple shellscripts running in the background to fetch statistics and some other minor stuff.

I’ve hosted a demo-page with very limited functionality and redacted config-templates if anyone is interested at https://www.deployment-tools.se/home. Spent the evening redesigning my own personal site as well which was very overdue.

Had a blast working on this project,  it also feels like most things in our field is slowly moving in the direction of more software-based networks and automation so i’m looking forward to dive deeper and learn more.

Deployment-tools

I’ve been busy these last few days working on migrating the website I created over at deployment-tools.se to Telias datacenter as it soon will be deployed for official use in the company, fun stuff! 🙂 It’s like an NSO-light which creates configuration (initial, full config & verification) for deployment of aggregation nodes, new links, services etc based on Python & Flask.

I’ve written a few posts showing how to do things like automate config for DHCP, firewalls & IOS etc that can be found here, here, here, here & here (in swedish though).  My weakest point is for sure design & CSS so i’m having a pretty rough time renewing the design (& HTML5) making it more dynamic but it’s slowly coming together now. I’ll host a demo-page the next few days when i’m closer to finishing it.

Moving from my own Raspberry to a very secured and locked down release of RHEL7 was a challenge as well hehe.. I’ve been trying to keep up with the reading in the mornings at least and i’m soon done with Internet Routing Architectures, it’s an old book but a very good read, highly recommended as a good intro to BGP!

Python & Flask – Chatterbot

Tänkte skriva ett litet inlägg om hur jag implementerade Chatterbot på min Flask/Python-webbsida, krävdes en hel del trial & error innan jag väl fick det att fungera så kanske kan vara intressant för någon mer. Ordförrådet fick dock hållas väldigt begränsat då Raspberryn inte riktigt orkar med om det blev för stora databaser att jobba med (svarstider på +20 sek).

Python

Börja med att installera Chatterbot-paketet, rekommenderar även att läsa igenom den officiella dokumentationen först för att förstå hur allt hänger ihop.

pip install chatterbot

Vi importerar sedan biblioteket i vår python-fil och skapar vår egen Chatbot-instans, här pekar vi även ut vilken databas vi ska använda, i mitt fall en Postgres-databas som ligger lokalt på servern:

from chatterbot import ChatBot
from chatterbot.trainers import ChatterBotCorpusTrainer

english_bot = ChatBot("jcAI", 
  storage_adapter="chatterbot.storage.SQLStorageAdapter",
  database_uri="postgres://user:password@localhost:5432/chatbot")

Nästa steg blir att “träna” vår chatbot, antingen via fördefinierade Corpus-filer som följer med vid installationen eller så skapar vi egna, i mitt fall gjorde jag både och. Filerna laddas härifrån:

/www/flaskenv/lib/python3.5/site-packages/chatterbot_corpus/data$ ls -l
totalt 68
drwxr-xr-x 2 joco02 joco02 4096 mar 7 13:29 bangla
drwxr-xr-x 2 joco02 joco02 4096 mar 7 13:29 chinese
drwxr-xr-x 2 joco02 joco02 4096 mar 9 22:58 custom
drwxr-xr-x 2 joco02 joco02 4096 mar 9 23:31 english
drwxr-xr-x 2 joco02 joco02 4096 mar 7 13:29 french
drwxr-xr-x 2 joco02 joco02 4096 mar 7 13:29 german
drwxr-xr-x 2 joco02 joco02 4096 mar 7 13:29 hebrew
drwxr-xr-x 2 joco02 joco02 4096 mar 7 13:29 hindi
drwxr-xr-x 2 joco02 joco02 4096 mar 7 13:29 indonesia
drwxr-xr-x 2 joco02 joco02 4096 mar 7 13:29 italian
drwxr-xr-x 2 joco02 joco02 4096 mar 7 13:29 marathi
drwxr-xr-x 2 joco02 joco02 4096 mar 7 13:29 portuguese
drwxr-xr-x 2 joco02 joco02 4096 mar 7 13:29 russian
drwxr-xr-x 2 joco02 joco02 4096 mar 7 13:29 spanish
drwxr-xr-x 2 joco02 joco02 4096 mar 21 15:00 swedish
drwxr-xr-x 2 joco02 joco02 4096 mar 7 13:29 tchinese
drwxr-xr-x 2 joco02 joco02 4096 mar 7 13:29 telugu

Några exempel från min egna Corpus-fil (sparad i custom), den inledande meningen/frågan börjar alltid med “- -” och efterföljande svar med “-“:

- - Dagens lunch
  - Please use ex. !lunch KG13 or !lunch pannbiff
- - DHCP
  - DCHP-documents is found <a href="http://link.link/DHCP/">here.</a>
- - Edge
  - Edge & Core Workroom is found <a href="http://link.link/core_agg/">here.</a>

Vi pekar sedan på önskade Corpus-filer i scriptet (detta behövs endast köras en gång och kan sedan kommenteras bort, verifiera via apache2-loggen att allt gick bra).

english_bot.set_trainer(ChatterBotCorpusTrainer)
english_bot.train("chatterbot.corpus.swedish", "chatterbot.corpus.english", "chatterbot.corpus.custom")

Sedan återstår endast att presentera användarinput & vår Chatbots svar på en webbsida:

@app.route("/chatbot")
def chatbot():
  return render_template("chatbot.html")

@app.route("/get")
def get_bot_response():
  userText = request.args.get('msg')

  return str(english_bot.get_response(userText))

HTML

Först skapar vi en “Chatbox”, använder Boostrap för att höger/vänsterjustera texten mellan bot/användare samt färgval.

<div class="row">
 <div class="col-md-6">
  <div class="panel panel-default">
   <div class="panel-heading"><span>AI Chat</span></div>
   <div class="panel-body">
    <p>Ask me something!<br></p>
    <div id="chatbox"></div>
   </div>
 <div class="panel-footer">
  <div id="userInput">
  <input id="textInput" type="text" name="msg" placeholder="">
  <input id="buttonInput" type="submit" value="Send" class="btn btn-success btn-xs">
  </div>
 </div>
 </div>
</div>

Sedan använder vi oss av ett script för att pusha/hämta info och presentera i ovanstående chatbox:

<script>
 function getBotResponse() {
 var rawText = $("#textInput").val();
 var userHtml = '<p class="text-success text-right"><span>' + rawText + '</span></p>';
 $("#textInput").val("");
 $("#chatbox").append(userHtml);
 document.getElementById('userInput').scrollIntoView({block: 'start', behavior: 'smooth'});
 $.get("/get", { msg: rawText }).done(function(data) {
 var botHtml = '<p>' + data + '</p>';
 $("#chatbox").append(botHtml);
 document.getElementById('userInput').scrollIntoView({block: 'start', behavior: 'smooth'});
 });
 }
 $("#textInput").keypress(function(e) {
 if(e.which == 13) {
 getBotResponse();
 }
 });
 $("#buttonInput").click(function() {
 getBotResponse();
 })
 </script>
</div>

Kompletterande även med en liten “infobox” (tips på användbara kommandon etc):

<div class="col-md-4">
 <div class="panel panel-default">
 <div class="panel-body">
  <h4>Some helpful commands:</h4><br>
  <small><strong>Jönköping:</strong></small><br>
  <small>!lunch <em>restaurant</em> (ex: <em><strong>!lunch KG13 or !lunch Vy</strong>)</em></small><br>
  <small>!lunch <em>food</em> (ex: <em><strong>!lunch kyckling</strong>)</em></small><br><br>

  <small><strong>Solna:</strong></small><br>
  <small>!solna <em>(shows todays menu at Modis)</em></small><br><br>

  <small><strong>Göteborg:</strong></small><br>
  <small>!gbg <em>(shows todays menu at Vällagat)</em></small><br><br>

  <small><strong>Other commands:</strong></small><br> 
  <small>!callguide (ex: <em><strong>!callguide or !callguide v12)</strong>)</em></small><br>
  <small>!sdp <em>node</em> (ex: <em><strong>!sdp j-sec1 or !sdp get-hsr1)</strong>)</em></small><br>
  <small>DHCP</small><br>
  <small>Common Services</small><br>
  <small>Standardporch</small><br>
  <small>etc..</small><br><br> 
  <small>I'm programmed with a few keywords so feel free to try anything, i'll try my best to respond. Let joco02 know if something else should be added for easy access.</small>
 </div>
 </div>
</div>

Som synes har jag byggt in ytterligare några funktioner här som använder samma chatbox men kringgår Chatterbot och presenterar resultat från andra scriptkörningar, exempelvis:

  • !lunch / !solna / !gbg – Använder Beautifulsoup4 för webscraping av menyer från lunchrestauranger i Jönköping/Solna/Göteborg (i närheten av Telia)
  • !callguide – Använder en schemafil och presenterar ansvarig person för aktuell/angiven vecka

Kanske återkommer med ett inlägg eller två om hur jag löste ovanstående framöver när det finns tid över.. 🙂

Python – Automatisera konfig med indata från Excel mha openpyxl

Fortsätter på temat Python ett tag till då de böcker jag läst nu senast (Interconnections – Bridges, Routers, Switches and Internetworking Protocols samt TCP/IP Illustrated) inte direkt inspirerar till att skriva några inlägg om. Det är dock väldigt bra böcker med mycket matnyttig info som jag rekommenderar alla som satsar mot CCIE att läsa för att ge en bättre inblick i “the fundamentals”.

Över till Python, i detta lilla script tänkte jag ge ett exempel på hur vi kan inhämta data från ett excelblad och sedan skapa konfiguration utifrån fördefinierade templates. I mitt fall används excelbladet som ett orderunderlag som sedan kommer producera konfiguration till ASR9K för x antal switchar. Mallen jag använder ser ut enligt följande:

Det vi frågar användaren efter är de “dynamiska” värdena vi behöver till våra konfig-templates. En mycket förkortad variant av en template jag använder idag skulle kunna se ut såhär:

!!!!!!!!!!!!! $BFNAME !!!!!!!!!!!!!

interface Te$BFINT
 description To $BFNAME;ETHAGG;$BFFB;;{LAG:BE$BID} 
 bundle id $BID mode on
 lacp period short
 load-interval 30
 transceiver permit pid all
 no shut
! 
interface Bundle-Ether$BID
 description To $BFNAME;ETHAGG;$BFFB;;
 bundle maximum-active links 1
 load-interval 30
 mtu 9216
!
multicast-routing
 address-family ipv4
 !
 interface Bundle-Ether$BID.243
 enable
!
router igmp
 interface Bundle-Ether$BID.243
 version 2
!
router pim
 address-family ipv4
 interface Bundle-Ether$BID.243
 enable
 !

Här har jag använt mig av ett antal variablar: $BFNAME,  $BFINT, $BFFB & $BID men detta går ju att bygga vidare på i all oändlighet (förutom att det kommer ta längre tid att fylla i excel-bladet).. 🙂 Jag använder mig fortfarande av flask som backend för att hämta in datan men detta kan ju göras på valfritt sett. I mitt fall ser HTML-koden ut enligt följande:

<form action="/bfmulti" class="form-horizontal" method="POST" role="form" enctype="multipart/form-data">
 <fieldset>
  <div class="form-group">
   <blockquote class="blockquote">
   <p>Upload file for new BF-switches.</p><br>
   <img src="/static/images/bfswitch.jpg"><br>
   <footer class="blockquote-footer">File required to be in <cite title="Source Title">*.xlsx format.</cite></footer>
   </blockquote>
   <small class="form-text text-muted col-md-2">Download template <a href="/static/download/bygga_bf.xlsx">here.</a></small>
 </div>
 <label class="btn btn-default btn-file col-md-1">Select file:
  <input type="file" style="display: none;" accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" name="file"><br></label>
  <label class="control-label" for="send"></label>
  <button id="send" name="send" class="btn btn-success" type="submit">Send</button>
 </fieldset>
</form>

I python har jag använt mig av biblioteket “openpyxl” för att läsa in datan från excel. Först läser jag in filen från användaren och använder sedan openpyxl för att hämta data från det aktiva bladet (har endast ett i min excel-mall). Vi läser även in antal rader (max_row) så vi vet hur många gånger scriptet ska loopa. För att skapa unika filnamn använder jag mig av datetime.

import openpyxl
from datetime import datetime
@app.route('/bfmulti', methods=['POST'])
def bf_multi():
 file = request.files['file']
 if file:

 wb = openpyxl.load_workbook(file)
 ws = wb.active
 maxRow = ws.max_row
 bfconfig_time = datetime.now().strftime("%Y%m%d-%H:%M:%S")
 filename = '/static/download/cfg-multi-%s.txt' % (bfconfig_time)

Vi loopar sedan alla rader vi hittat (maxRow) och läser in data från respektive cell. Observera att vi börjar på rad 3 i detta fall då vi ej vill loopa över våra rubriker etc:

for row in range(3, maxRow):
  checkempty = ws['A' + str(row)].value
  if checkempty is not None:
    bfname = ws['A' + str(row)].value
    bffb = ws['B' + str(row)].value
    bfbid = ws['C' + str(row)].value
    bfif = ws['D' + str(row)].value
    bfif = bfif.replace("-", "/")
    link = ws['E' + str(row)].value
    kotype = ws['F' + str(row)].value

  if "10G" in str(link):
    linktype = "Te"
  elif "1G" in str(link):
    linktype = "Gi"

Då våra templates ser annorlunda ut beroende på om det är ett 1G- eller 10G-interface sätter vi linktype efter vad vi hittar under str(link). Vi skapar sedan en dictionary med de variablar vi läst in från excel-bladet och matchar dessa med de förutbestämda namnen i vår template-konfig enligt följande:

replacements = {'$BID':str(bfbid), '$BFINT':str(bfif), '$BFFB':str(bffb), '$BFNAME':str(bfname)}

Nu återstår det bara att läsa in vår template-konfig, appenda rad för rad och ersätta våra variablar med det vi hittat i excel.

if linktype == "Gi":
  with open('/bfconfig/bf_1g_config.txt') as infile, open('/website/static/bfconfig/cfg-multi-%s.txt' % (bfconfig_time), 'a') as outfile:
    for line in infile:
      for src, target in replacements.items():
        line = line.replace(src, target)
        outfile.write(line)

När vi läst in alla rader återstår det endast att läsa in resultatet och antingen visa det för användaren eller kanske direkt pusha ut konfigurationen beroende på användingsområde?

with open('/website/static/bfconfig/cfg-multi-%s.txt' % (bfconfig_time), 'r') as f:
 content = f.read()

return render_template("results_bfmulti.html", content = content, filename = filename)

Svårare än så är det inte! 🙂

Python & Flask del 2 – Views & Forms

I del två tänkte jag bygga vidare på vår lilla “Hello World!”-sida och skapa ett formulär där våra användare kan fylla i vissa parametrar, detta skickas vidare till ett litet python-script som sedan presenterar resultatet för användaren. I detta exempel blir det konfiguration för driftsättning av ett nytt interface till en switch efter en fördefinierad konfigurationsmall.

Vi börjar med att skapa vårat formulär, för detta behöver vi först en ny route i vår test.py-fil. En route matchar på adressen vi/apache skickar från webbläsaren, @app.route(‘/’) matchar dvs på vår “root”-katalog, ex. min domän www.jonascollen.se/.  För att presentera en webbsida istället för en enkel string-variabel som “Hello World!” behöver vi först importera ytterligare en flask-modul, “render_template”, och för att hämta data från formulären importerar vi “request”. Vi skapar sedan får nya ‘view’ med namnet newconfig.

from flask import Flask, render_template, request
app = Flask(__name__)

# route for default-paget
@app.route('/')
def hello_world():
    return 'Hello World!'

# route for new config-page
@app.route('/newconfig')
def newconfig():
    return render_template('newconfig.html')

if __name__ == '__main__':
    app.run()

Vi skapar sedan en ny html-fil under /templates-katalogen, newconfig.html med valfritt formulär. Rekommenderar starkt användandet av Bootstrap för att snygga till det hela, kommer dock inte lägga någon tid på att förklara html/css, där finns det vettigare sidor att vända sig till…  Mitt formulär ser ut enligt följande:

newconfig.html

<div class="panel-heading"> <a data-toggle="collapse" href="#noBundle">New switch, no bundle</a></div>
 <div class="panel-body collapse in" id="noBundle">
 <form action="/newconfig" class="form-horizontal" method="POST" role="form">
 <fieldset>

<div class="form-group">
 <label class="col-md-1 control-label" for="swname">Switch:</label>
 <div class="col-md-2">
 <input id="swname" name="swname" class="form-control input-md" type="text">
 </div>
 </div>

<div class="form-group">
 <label class="col-md-1 control-label" for="swif">Interface:</label>
 <div class="col-md-2">
 <input id="swif" name="swif" class="form-control input-md" type="text">
 </div>
 </div>

<div class="form-group">
 <label class="col-md-1 control-label" for="swid">ID-number:</label>
 <div class="col-md-2">
 <input id="swid" name="swid" class="form-control input-md" type="text">
 </div>
 </div>

<div class="form-group">
 <label class="col-md-1 control-label" for="swbid">Bundle-ID:</label>
 <div class="col-md-2">
 <input id="swbid" name="swbid" class="form-control input-md" type="text">
 </div>
 </div>

<div class="form-group">
 <label class="col-md-1 control-label" for="userid">User:</label>
 <div class="col-md-2">
 <input id="userid" name="userid" class="form-control input-md" type="text">
 </div>
 </div>

<div class="form-group">
 <label class="col-md-1 control-label" for="send"></label>
 <div class="col-md-2">
 <button id="send" name="send" class="btn btn-info" type="submit">Generate config</button>
 </div>
 </div>
</fieldset>
 </form>
 </div>
 </div>

Det viktiga här är egentligen dessa rader:

  • <form action=”/newconfig” class=”form-horizontal” method=”POST” role=”form”> – Här matchar vi mot den route vi kommer definiera i vårat script för att presentera config-resultatet senare. Som synes går det att använda samma route som vi redan använder till vårat formulär, mer om detta strax.
  • input id=”x”– Detta blir namnet på variablerna vi vill hämta från formuläret i vårat python-script.

 

 

Surfar vi nu in på sidan /newconfig, fyller i fälten och klickar “Generate config” kommer du få ett felmeddelande, varför? När vi klickar send/generate config skickas formulärdatan med metoden ‘POST’, detta accepteras dock inte per default i vår /newconfig-route – och det vill vi ju inte heller. Vi vill fånga in datan och presentera det på en annan sida, låt oss därför skapa en ny route där vi explicit tilltåter just ‘POST’, hämtar in datan och spar ner till variabler.

# route for switch config results
@app.route('/newconfig', methods=['POST'])
def sw_config():
    swname = request.form['swname']
    swif = request.form['swif']
    swid = request.form['swid']
    swbid = request.form['swbid']
    user = request.form['userid']

Har användaren fyllt i “Core-SW1” under “Switch:” kommer det skrivas ut om vi kör ex. print(swname). Allt bra så långt, vi behöver nu en konfigmall som vi sedan slår ihop med infon vår användare skickat in. Detta kan hämtas från vilken mapp som helst på servern, i detta exempel skapar jag en liten textfil direkt i ~.

nano sw-default.txt

interface Te$SWIF
 description To $SWNAME;$SWID;BE$SWBID;{work+in+progress+$USERID}
 bundle id $SWBID mode on
 no shut
 !
interface Bundle-Ether$SWBID
 description To $SWNAME;$SWID;BE$SWBID;;{work+in+progress+$USERID}
 bundle maximum-active links 1
 load-interval 30

Tillbaka till vår lilla python-fil. För att ersätta variablerna i vår konfigmall gjorde jag på detta vis:

# Spar ner våra variabler till en dictionary, här matchar vi $SWBID i vår konfigmall med swbid från användaren
replacements = {'$SWBID':swbid, '$SWIF':swif, '$SWID':swid, '$SWNAME':swname, '$USERID':user}

# Läser in vår konfigmall och skapar en kopia med namnet "cfg-$swname-$user.txt" - W = Write
with open('/home/joco02/sw-default.txt') as infile, open('/home/joco02/cfg-%s-%s-%s.txt' % (swname, user), 'w') as outfile:
    for line in infile:
        # Byter ut orden som matchar med vår dictionary 'replacements'
        for src, target in replacements.items():
            line = line.replace(src, target)
        outfile.write(line)

# Läser in vår nya configfil till variabeln swconfig - R = Read
with open('/home/joco02/cfg-%s-%s-%s.txt' % (swname, user), 'r') as f:
    swconfig = f.read()

# Skickar resultatet till results.html med tillhörande variabler: content (innehåller allt konfig), samt swname (variabel från användaren)
return render_template("results.html", content = swconfig, swname = swname)

Vi behöver nu bara en skapa en html-sida att presentera resultatet på, results.html. I mitt fall ser den ut enligt följande:

<div class="panel panel-default">
 <div class="panel-heading"> <a data-toggle="collapse" href="#noBundle">Configuration for {{ swname }}</a></div>
 <div class="panel-body collapse in" id="noBundle">
 <pre> {{ content }} </pre>
 </div>
</div>

{{ swname }} kommer bytas ut mot namnet vår användare angav i formuläret, och content med innehållet från den konfigfil vi precis har skapat. Testar vi nu återigen vår sidan /newconfig och fyller i formuläret bör vår nya konfig presenteras när vi klickar på send.

 

It works! 🙂

Python & Flask del 1 – Dynamisk webbsida

1022 dagar sedan senaste inlägget så kändes som det kanske var dags att ge den här sidan lite kärlek igen.. Liten uppdatering – inget CCIE-cert ännu, däremot nytt jobb som IP Specialist på Telia sedan ~1 år tillbaka. Med SDN på intåg samt att certifieringar i det stora hela börjar kännas allt mer icke-relevanta pga braindumps m.m. så vet jag inte riktigt hur jag ska göra framöver. Just nu är det mer lärande för lärandets skull.. 🙂

Har haft som sidoprojekt senaste tiden att automatisera arbetsuppgifter/-moment via (shell)script, men tänkte försöka gå över till att använda Python istället i samband med lansering av en ny scriptserver på jobbet. Så i väntan på lanseringen har jag börjat kika lite på små pythonscript, men hade även en idé att försöka lyfta ur vissa bef. script som inte kräver någon nätaccess till en webbserver istället (och samtidigt skriva dessa i python istället) för att göra det mer användarvänligt/lättillgängligt.

logo-full

Då jag ville använde mig av Python3 som “backend” verkade Flask intressant, webbservern driver jag just nu på en raspberry (rasbian)/apache2. Efter x antal timmar börjar det nu faktiskt lira helt ok så tänkte det kunde vara på sin plats att dokumentera ner det hela lite grann.

För att komma igång:

Skapar sedan en katalog för min “hemsida” under /var/www/html/ (default för debian):

$ mkdir website
# För bilder/css/js etc
$ mkdir website/static
# För html-filer
$ mkdir website/templates
$ cd website
# Skapar en Virtual enviroment för Py3 med namnet flaskenv
$ virtualenv --python=python3 flaskenv
# Aktivera VE
$ source flaskenv/bin/activate

Fungerar allt som det ska bör prompten se ut likt detta (min prompt ser kanske lite udda ut men är endast för jag skapat en symlänk mellan hemkatalogen & /var/www/html/):

(flaskenv) joco02@webdev:~/www/website$
(flaskenv) joco02@webdev:~/www/website$ python -V
Python 3.5.3
# Installera Flask:
(flaskenv) joco02@webdev:~/www/website$ pip install flask

Vi kan nu testa slänga ihop ett litet script:

(flaskenv) joco02@webdev:~/www/website$ nano test.py

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
 return 'Hello World!'

if __name__ == '__main__':
 app.run()

Starta sedan Flasks inbyggda webbserver med:

(flaskenv) joco02@webdev:~/www/website$  python test.py
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

I default-läget är dock Flasks webbserver endast nåbar från localhost, vi kan alltid verifiera mer curl om det fungerar men mer lämpligt är väl att öppna upp så du kan testa från andra enheter:

(flaskenv) joco02@webdev:~/www/website$ export LC_ALL=C.UTF-8
(flaskenv) joco02@webdev:~/www/website$ export LANG=C.UTF-8
(flaskenv) joco02@webdev:~/www/website$ export FLASK_APP=test.py
(flaskenv) joco02@webdev:~/www/website$ flask run --host=0.0.0.0
 * Serving Flask app "test"
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

# Alt. så justerar vi vår test.py enligt följande:
if __name__ == "__main__":
    app.run(host='0.0.0.0', port=5000, debug=True)

Surfar vi nu in till vår server på port 5000 bör du mötas av en fin “Hello World!”. Men för att få det att fungera med apache2 krävs det ytterligare en hel del jobb.

Vi behöver först installera WSGI för Python3:

(flaskenv) joco02@webdev:~/www/website$ deactivate
joco02@webdev:~/www/website$ sudo apt-get install libapache2-mod-wsgi-py3
joco02@webdev:~/www/website$ # Aktivera mod_wsgi och starta om apache2
joco02@webdev:~/www/website$ a2enmod wsgi 
joco02@webdev:~/www/website$ sudo /etc/init.d/apache2 restart

Skapa sedan en Virtual Host i Apache2:

joco02@webdev:/var/www/html/website$ sudo nano /etc/apache2/sites-available/website.conf

Min fil ser ut enligt följande (observera att WSGIProcessGroup måste matcha din WSGI-fil du skapar i nästa steg):

<VirtualHost *:80>
 WSGIDaemonProcess website user=xx group=xx threads=5
 WSGIScriptAlias / /var/www/html/website/website.wsgi

ServerName x.se
 ServerAdmin x@x
 <Directory /var/www/html/website/>
 WSGIProcessGroup website
 WSGIApplicationGroup %{GLOBAL}
 WSGIScriptReloading On

Require all granted

</Directory>
 Alias /static /var/www/html/website/static
 <Directory /var/www/html/website/static/>
 Order allow,deny
 Allow from all
 </Directory>
 ErrorLog ${APACHE_LOG_DIR}/error.log
 LogLevel warn
 CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

Vi akviterar den sedan med:

sudo a2ensite website

Vi närmar oss.. Nu behövs en WSGI-fil som vi skapar i vår “website”-mapp. Efter en hel del trixande/trial & error ser min fil tillslut ut enligt följande:

joco02@webdev:/etc/apache2/sites-available$ cd /var/www/html/website/
joco02@webdev:/var/www/html/website$ nano website.wsgi


#!/usr/bin/python
import sys
import logging
import site

site.addsitedir('/var/www/website/flaskenv/lib/python3.5/site-packages')

logging.basicConfig(stream=sys.stderr)
sys.path.insert(0,"/var/www/html/website/")

from test import app as application

Starta sedan om apache och du ska *förhoppningsvis* få upp din hemsida om du surfar in på http://*serverip*, det var en del svett, blod & tårar innan jag kom såhär långt själv… 🙂

Kommer ytterligare något inlägg om hur vi bygger vidare på applikationen framöver, min egna lilla server rullar fortfarande på: www.jonascollen.se.

 

Python & MySQL

Fortsättning på det tidigare inlägget om Python & SNMP, tänkte komplettera med att spara ner resultatet till en databas istället för att endast skriva ut till terminalen.

Först installerar vi MySQL via “sudo apt-get install mysql” och python-plugin:et “python-mysqldb”.

Databasen vi vill sätta upp ser ut enligt följande:

database

När installationen är klar logga in med användaren du skapade:

mysql -h localhost -u root -p

Vi skapar sedan databasen my_network:

CREATE DATABASE my_network;

Och sedan tabellerna unit & interface:

USE my_network;


CREATE TABLE unit

(

id int unsigned NOT NULL auto_increment,

ip_address VARCHAR(16),

name VARCHAR(40),

model VARCHAR(40),

PRIMARY KEY (id),

UNIQUE (ip_address)

);

CREATE TABLE interface

(

id int unsigned NOT NULL auto_increment,

unit_id int unsigned NOT NULL,

name VARCHAR(40),

mac_address VARCHAR(40),

ip_address VARCHAR(16),

mask VARCHAR(16),

PRIMARY KEY (id)

);

Vilket ger följande resultat:

mysql> SHOW TABLES;

+----------------------+

| Tables_in_my_network |

+----------------------+

| interface            |

| unit                 |

+----------------------+

2 rows in set (0.00 sec)

mysql> DESCRIBE unit;

+------------+------------------+------+-----+---------+----------------+

| Field      | Type             | Null | Key | Default | Extra          |

+------------+------------------+------+-----+---------+----------------+

| id         | int(10) unsigned | NO   | PRI | NULL    | auto_increment |

| ip_address | varchar(16)      | YES  | UNI | NULL    |                |

| name       | varchar(40)      | YES  |     | NULL    |                |

| model      | varchar(40)      | YES  |     | NULL    |                |

+------------+------------------+------+-----+---------+----------------+

4 rows in set (0.00 sec)

mysql> DESCRIBE interface;

+-------------+------------------+------+-----+---------+----------------+

| Field       | Type             | Null | Key | Default | Extra          |

+-------------+------------------+------+-----+---------+----------------+

| id          | int(10) unsigned | NO   | PRI | NULL    | auto_increment |

| unit_id     | int(10) unsigned | NO   | MUL | NULL    |                |

| name        | varchar(40)      | YES  |     | NULL    |                |

| mac_address | varchar(40)      | YES  |     | NULL    |                |

| ip_address  | varchar(16)      | YES  |     | NULL    |                |

| mask        | varchar(16)      | YES  |     | NULL    |                |

+-------------+------------------+------+-----+---------+----------------+

6 rows in set (0.00 sec)

Vi kompletterar sedan koden med fetmarkerad text:

 

#!/usr/bin/env python
#Python
import netsnmp
import ipaddr
import sys
import subprocess
import MySQLdb
import prettytable
from prettytable import PrettyTable
 
checkarg = sys.argv[1:]
#Database
conn = MySQLdb.connect("localhost", "root", "netadm01", "my_network")
cursor = conn.cursor()
 
#Checks arguments and saves to variables
if len(checkarg) == 2:
    cCommunity = checkarg[0]
    cIP = checkarg[1]
    print cCommunity, cIP
    if isinstance(cCommunity, str):
        try:
            #Checks for valid IPv4-address
            ccIP = ipaddr.IPv4Address(cIP)
         
            try:
                #Modelname
                #Gets the Model-ID, returns numeric
                cModelInt = subprocess.Popen([r"snmpwalk","-v2c","-Oqv","-c",cCommunity,cIP,"sysObjectID"],stdout=subprocess.PIPE).communicate()[0]
                cModelSplit = cModelInt.split("::")
                cModelSplit = cModelSplit[1].rstrip()
 
                #Translate numeric Model-ID to string
                cModel = subprocess.Popen([r"snmptranslate","-m","CISCO-PRODUCTS-MIB","-IR",cModelSplit],stdout=subprocess.PIPE).communicate()[0]
                cModel = cModel.split("::")    
                       
                #Name
                oid = netsnmp.Varbind('sysName.0')
                cName = netsnmp.snmpget(oid, Version = 2, DestHost = cIP, Community = cCommunity)
 
                #Description
                oid = netsnmp.Varbind('sysDescr.0')
                cDesc = netsnmp.snmpget(oid, Version = 2, DestHost = cIP, Community = cCommunity)              
 
                #Output
                print ""
                print "Device info:"
                print ""               
                print "Host:", ccIP
                print "Model:", cModel[1].rstrip()
                print "Name:", cName[0]
                print cDesc[0]
                print ""
 
                try:
                   
                    cursor.execute("INSERT INTO unit(ip_address,name,model) VALUES(%s,%s,%s)", (ccIP,cName[0],cModel[1].rstrip()))
                    conn.commit()
                    cID = cursor.lastrowid
                    print "Highest ID:",cID
 
                except:
                    cursor.execute("SELECT id FROM unit WHERE ip_address=%s", (ccIP))
                    dbexists = cursor.fetchone()[0]
                    if cursor.rowcount > 0:
                        print ccIP, " Already exists in the database, cleaning up. ID: ", dbexists
                        try:
                            cursor.execute("DELETE FROM unit WHERE unit.id=%s", (dbexists))
                            conn.commit()
                            cursor.execute("DELETE FROM interface WHERE interface.unit_id=%s", (dbexists))
                            conn.commit()
 
                            try:
                                cursor.execute("INSERT INTO unit(ip_address,name,model) VALUES(%s,%s,%s)", (ccIP,cName[0],cModel[1].rstrip()))
                                conn.commit()
                                cID = cursor.lastrowid
                                print "Highest ID:",cID
                            except:
                                print "Unable to insert data"
                        except:
                           
                            print "Delete failed"
                            pass
                           
 
                x = PrettyTable(["Interface", "IP", "Subnet", "MAC"])
                x.align["Interface"] = "l"
                x.align["IP"] = "l"
                x.align["Subnet"] = "l"
               
                #Index of all the interfaces
                oid = netsnmp.Varbind("ifIndex")
                cIndex = netsnmp.snmpwalk(oid, Version = 2, DestHost = cIP, Community = cCommunity)
                               
                #Index of all the interfaces with IP
                oid = netsnmp.Varbind("ipAdEntIfIndex")
                cIndexIP = netsnmp.snmpwalk(oid, Version = 2, DestHost = cIP, Community = cCommunity)
                               
                #IP-Adress list
                oid = netsnmp.Varbind("ipAdEntAddr")    
                ipAdd = netsnmp.snmpwalk(oid, Version = 2, DestHost = cIP, Community = cCommunity)
               
                #MAC for all interfaces
                oid = netsnmp.Varbind("ifPhysAddress")
                cMAC = netsnmp.snmpwalk(oid, Version = 2, DestHost = cIP, Community = cCommunity)
               
                #Subnetmask for all IP interfaces
                oid = netsnmp.Varbind("ipAdEntNetMask")
                cNetmask = netsnmp.snmpwalk(oid, Version = 2, DestHost = cIP, Community = cCommunity)
               
                looped = 0
                               
                #Prints out interface-details
                for i in cIndex:
               
                    #Name of every interface
                    oid = netsnmp.Varbind("ifDescr."+i)
                    cIntName = netsnmp.snmpget(oid, Version = 2, DestHost = cIP, Community = cCommunity)
                   
                    try:
                        #Matches interface-list with interface+IP-list      
                        IPindex = cIndexIP.index(i)
                       
                        try:
                       
                            #Hex to readable
                            mac = cMAC[looped]
                            mac = ":".join( [ '%x'%(ord(c)) for c in mac ] )
                           
                            #Prints row
                            x.add_row([cIntName[0], ipAdd[IPindex], cNetmask[IPindex], mac])
                            cursor.execute("INSERT INTO interface(unit_id,name,ip_address,mask,mac_address) VALUES(%s,%s,%s,%s,%s)", (cID,cIntName[0],ipAdd[IPindex],cNetmask[IPindex],mac))
                            conn.commit()
 
                        except:
                            pass
 
                        looped += 1
       
                    except:
 
                        try:
                            #Hex to readable
                            mac = cMAC[looped]
                            mac = ":".join( [ '%x'%(ord(c)) for c in mac ] )
                            #Prints row w/o IP
                            x.add_row([cIntName[0], '', '', mac])
                            cursor.execute("INSERT INTO interface(unit_id,name,mac_address) VALUES(%s,%s,%s)", (cID,cIntName[0],mac))
                            conn.commit()
                        except:
                                       
                            #Prints row w/o MAC & IP
                            x.add_row([cIntName[0], '', '', ''])
                            cursor.execute("INSERT INTO interface(unit_id,name) VALUES(%s,%s)", (cID,cIntName[0]))
                            conn.commit()
                        looped += 1
                print x
                #Close database
                cursor.close()
                conn.close()
 
                try:
                    foutput = x.get_string()
                    f = open(cIP, "w")
                    f.write("Device info: " + cIP + "\nCommunity: " + cCommunity + "\n")
                    f.write("\nHost: " + cIP)
                    f.write("\nModel: " + cModel[1].rstrip())
                    f.write("\nName:" + cName[0])
                    f.write("\n" + cDesc[0])
                    f.write("\n" + foutput + "\n")
                   # f.writelines(x)
                except:
                    print "Failed to write file"       
                finally:
                    f.close()
            except:
                print "SNMP-polling failed"
                print x
        except:
            print "Invalid IP given:", cIP
    else:
        print "Invalid Community given (must be string)"
else:
    print "Error - you must set both Community and IP (ex ./3a.py public localhost):"

Exempel på output:

Device info: 192.168.152.9
Community: public

Host: 192.168.152.9
Model: catalyst355024
Name:3550-1
Cisco Internetwork Operating System Software 
IOS (tm) C3550 Software (C3550-I5Q3L2-M), Version 12.1(22)EA1a, RELEASE SOFTWARE (fc1)
Copyright (c) 1986-2004 by cisco Systems, Inc.
Compiled Fri 20-Aug-04 00:44 by yenanh
+--------------------+----------------+-----------------+-----------------+
| Interface          | IP             | Subnet          |       MAC       |
+--------------------+----------------+-----------------+-----------------+
| FastEthernet0/1    | 192.168.152.2  | 255.255.255.252 | 0:a:b7:9c:93:80 |
| FastEthernet0/2    |                |                 | 0:a:b7:9c:93:82 |
| FastEthernet0/3    |                |                 | 0:a:b7:9c:93:83 |
| FastEthernet0/4    |                |                 | 0:a:b7:9c:93:84 |
| FastEthernet0/5    |                |                 | 0:a:b7:9c:93:85 |
| FastEthernet0/6    |                |                 | 0:a:b7:9c:93:86 |
| FastEthernet0/7    |                |                 | 0:a:b7:9c:93:87 |
| FastEthernet0/8    |                |                 | 0:a:b7:9c:93:88 |
| FastEthernet0/9    |                |                 | 0:a:b7:9c:93:89 |
| FastEthernet0/10   |                |                 | 0:a:b7:9c:93:8a |
| FastEthernet0/11   |                |                 | 0:a:b7:9c:93:8b |
| FastEthernet0/12   |                |                 | 0:a:b7:9c:93:8c |
| FastEthernet0/13   |                |                 | 0:a:b7:9c:93:8d |
| FastEthernet0/14   |                |                 | 0:a:b7:9c:93:8e |
| FastEthernet0/15   |                |                 | 0:a:b7:9c:93:8f |
| FastEthernet0/16   |                |                 | 0:a:b7:9c:93:90 |
| FastEthernet0/17   |                |                 | 0:a:b7:9c:93:91 |
| FastEthernet0/18   |                |                 | 0:a:b7:9c:93:92 |
| FastEthernet0/19   |                |                 | 0:a:b7:9c:93:93 |
| FastEthernet0/20   |                |                 | 0:a:b7:9c:93:94 |
| FastEthernet0/21   |                |                 | 0:a:b7:9c:93:95 |
| FastEthernet0/22   |                |                 | 0:a:b7:9c:93:96 |
| FastEthernet0/23   |                |                 | 0:a:b7:9c:93:97 |
| FastEthernet0/24   | 192.168.152.5  | 255.255.255.252 | 0:a:b7:9c:93:80 |
| GigabitEthernet0/1 |                |                 | 0:a:b7:9c:93:99 |
| GigabitEthernet0/2 |                |                 | 0:a:b7:9c:93:9a |
| Null0              |                |                 |                 |
| Vlan1              |                |                 | 0:a:b7:9c:93:80 |
| Vlan2              | 192.168.152.9  | 255.255.255.248 | 0:a:b7:9c:93:80 |
| Vlan10             | 192.168.152.17 | 255.255.255.240 | 0:a:b7:9c:93:80 |
| Vlan12             | 192.168.152.97 | 255.255.255.224 | 0:a:b7:9c:93:80 |
| Vlan15             | 192.168.152.65 | 255.255.255.224 | 0:a:b7:9c:93:80 |
| Vlan20             | 192.168.152.49 | 255.255.255.240 | 0:a:b7:9c:93:80 |
| Vlan25             | 192.168.152.33 | 255.255.255.240 | 0:a:b7:9c:93:80 |
| Vlan153            | 192.168.153.1  | 255.255.255.0   | 0:a:b7:9c:93:80 |
+--------------------+----------------+-----------------+-----------------+

Ska försöka hitta tillbaka till Cisco snart men eventuellt kommer det några kortare inlägg om Cacti/Nagios & EEM här framöver först. Fullt upp i jobbsökandet dessutom, den 18-19:e mars ska jag till Oslo för lite intervjuer som verkar riktigt spännande! 🙂

Python & SNMP

Tänkte fortsätta på samma tema med SNMP och kombinera detta med lite enklare script i Python. Är långt ifrån någon programmerare så detta är nog inte direkt någon vacker lösning, men det fungerar åtminstone. 🙂

För att göra SNMP-querys kan vi använda exempelvis biblioteken subprocess eller netsnmp enligt följande:

Subprocess

#!/usr/bin/env python
import subprocess
checkarg = sys.argv[1:]
#Checks arguments and saves to variables
if len(checkarg) == 2:
    cCommunity = checkarg[0]
    cIP = checkarg[1]
#Modelname
#Gets the Model-ID, returns numeric
cModelInt = subprocess.Popen([r"snmpwalk","-v2c","-Oqv","-c",cCommunity,cIP,"sysObjectID"],stdout=subprocess.PIPE).communicate()[0]

Alternativt netsnmp:

#!/usr/bin/env python
import subprocess
checkarg = sys.argv[1:]
#Checks arguments and saves to variables
if len(checkarg) == 2:
    cCommunity = checkarg[0]
    cIP = checkarg[1]
oid = netsnmp.Varbind('sysName.0')
cName = netsnmp.snmpget(oid, Version = 2, DestHost = cIP, Community = cCommunity)

Vi testar sedan scriptet med “./script.py community ip-adress”.

Här ett exempel där jag hämtar hem lite grundläggande information från en cisco router/switch:

#!/usr/bin/env python
import netsnmp
import subprocess
import ipaddr
import sys
import prettytable
from prettytable import PrettyTable

checkarg = sys.argv[1:]
#Checks arguments and saves to variables
if len(checkarg) == 2:
    cCommunity = checkarg[0]
    cIP = checkarg[1]
if isinstance(cCommunity, str):
    try:
        #Checks for valid IPv4-address
        ccIP = ipaddr.IPv4Address(cIP)

        try:
            #Modelname
            #Gets the Model-ID, returns numeric
            cModelInt = subprocess.Popen([r"snmpwalk","-v2c","-Oqv","-c",cCommunity,cIP,"sysObjectID"],stdout=subprocess.PIPE).communicate()[0]
            cModelSplit = cModelInt.split("::")
            cModelSplit = cModelSplit[1].rstrip()
            #Translate numeric Model-ID to string
            cModel = subprocess.Popen([r"snmptranslate","-m","CISCO-PRODUCTS-MIB","-IR",cModelSplit],stdout=subprocess.PIPE).communicate()[0]
            cModel = cModel.split("::") 

            #Name
            oid = netsnmp.Varbind('sysName.0')
            cName = netsnmp.snmpget(oid, Version = 2, DestHost = cIP, Community = cCommunity)

            #Description
            oid = netsnmp.Varbind('sysDescr.0')
            cDesc = netsnmp.snmpget(oid, Version = 2, DestHost = cIP, Community = cCommunity)
            #Output
            print ""
            print "Device info:"
            print "" 
            print "Host:", ccIP
            print "Model:", cModel[1].rstrip()
            print "Name:", cName[0]
            print cDesc[0]
	    x = PrettyTable(["Interface", "IP", "Subnet", "MAC"])
            x.align["Interface"] = "l"

	    #Index of all the interfaces
	    oid = netsnmp.Varbind("ifIndex")
	    cIndex = netsnmp.snmpwalk(oid, Version = 2, DestHost = cIP, Community = cCommunity)

	    #Index of all the interfaces with IP
	    oid = netsnmp.Varbind("ipAdEntIfIndex")
	    cIndexIP = netsnmp.snmpwalk(oid, Version = 2, DestHost = cIP, Community = cCommunity)

	    #IP-Adress list
	    oid = netsnmp.Varbind("ipAdEntAddr")    
	    ipAdd = netsnmp.snmpwalk(oid, Version = 2, DestHost = cIP, Community = cCommunity)

	    #MAC for all interfaces
	    oid = netsnmp.Varbind("ifPhysAddress")
	    cMAC = netsnmp.snmpwalk(oid, Version = 2, DestHost = sys.argv[2], Community = sys.argv[1])

	    #Subnetmask for all IP interfaces
	    oid = netsnmp.Varbind("ipAdEntNetMask")
	    cNetmask = netsnmp.snmpwalk(oid, Version = 2, DestHost = cIP, Community = cCommunity)

	    looped = 0

	    #Prints out interface-details
	    for i in cIndex:

	        #Name of every interface
	        oid = netsnmp.Varbind("ifDescr."+i)
	        cInterfacename = netsnmp.snmpget(oid, Version = 2, DestHost = cIP, Community = cCommunity)

	        try:
	            #Matches interface-list with interface+IP-list       
	            IPindex = cIndexIP.index(i)

	            try:
	                #Hex to readable
	                mac = cMAC[looped]
	                mac = ":".join( [ '%x'%(ord(c)) for c in mac ] )

	                #Prints row
	                x.addrow(cInterfacename[0],ipAdd[IPindex],cNetmask[IPindex],mac)

	            except:
		        pass
		    looped += 1

	        except:
	            try:
	                #Hex to readable
	                mac = cMAC[looped]
	                mac = ":".join( [ '%x'%(ord(c)) for c in mac ] )
	                #Prints row w/o IP
	                x.addrow(cInterfacename[0],mac)

	            except:

	                #Prints row w/o MAC & IP
	                x.addrow(cInterfacename[0])

	            looped += 1

		 except:
		     print "SNMP-polling failed"

	     except: 
                 print "Invalid IP given:", cIP
     else:
         print "Invalid Community given (must be string)"
else:
    print "Error - you must set both Community and IP (ex ./3a.py public localhost):"

Programmering/scripting kan vara väldigt skoj har jag märkt! Kan verkligen rekommendera exempelvis https://www.khanacademy.org/science/computer-science-subject/computer-science för att få en liten introduktion till programmering i just python. Till nästa inlägg tänkte jag komplettera scriptet med att spara ner informationen i en MySQL-databas eller fil istället för att endast skriva ut på skärmen.