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 – Automatisera DHCP-scopes, IPTables- & interface-config

Tänkte fixa ihop ett litet script för att automatiskt generera DHCP-scopes, IPTables- & router-config, går finfint att antingen bygga ihop med Flask och ta in näten via ett formulär från användaren eller hämta från valfri källa. Vi räknar med att näten som kommer in är i formatet “192.168.0.0/24” med radbrytning mellan varje nät.  Vi separerar även typen av nät genom att lägga till filnamnet vi hämtat det från. Exempelvis:

IPTV.txt
192.168.0.0/24
192.168.1.0/24
192.168.2.0/24
192.168.4.0/24

VOIP.txt
172.16.0.0/24
172.16.2.0/24
172.16.3.0/24
172.16.4.0/24

SURF.txt
10.10.0.0/24
10.10.1.0/24
10.10.2.0/24
10.10.3.0/24

OFFNET.txt
132.10.0.0/24
147.2.0.0/24

Börjar med att importa biblioteket netaddr & datetime samt att vi sparar ner ovanstående till variabeln “content”på valfritt vis, skapa sedan listor som vi kommer använda för att sortera ut näten.

from netaddr import *
from datetime import datetime

iptv = []
voip = []
surf = []
offnet = []

Vi loopar sedan genom variabeln ord för ord och sätter en tag för respektive nättyp när vi får träff. Scriptet vet på vis att efterföljande ipnät tillhör ex. IPTV, och när den matchar mot VOIP sparas de i “voip” etc.

 for line in content.split():
  # Tagging types of networks
  if "IPTV.txt" in line:
    net = "iptv"
  elif "VOIP.txt" in line:
    net = "voip"
  elif "SURF.txt" in line:
    net = "surf"
  elif "OFFNET.txt" in line:
    net = "offnet"
 
  # Saving networks to separate lists
  if net == "iptv":
    if "0/24" in line:
      iptv.append(line)
  elif net == "voip":
    if "0/24" in line:
      voip.append(line)
  elif net == "surf":
    if "0/24" in line:
      surf.append(line)
  elif net == "offnet":
    if "0/24" in line:
    offnet.append(line)

För validering kan vi även summera hur många nät vi hittat i respektive kategori.

validation = '''Found the following networks:
 IPTV: {iptv}
 VoIP: {voip}
 SURF: {surf}
 OFFNET: {offnet}'''.format(iptv=len(iptv), voip=len(voip), surf=len(surf), offnet=len(offnet))

Nu har vi allt vi behöver för att börja skapa lite filer, IP-tables till att börja med. I detta fall vill jag endast skapa firewall-öppningar för näten IPTV, VOIP & Surf, vi måste därför separera dem från OFFNET-näten. Vi fixar även en tidsstämpel vi kan använda till att skapa unika filnamn.

 # Creates a list of lists - all networks that needs firewall opening 
 fire_nets = [iptv, voip, surf]

 # Used for unique filename
 tfconfig_time = datetime.now().strftime("%Y%m%d-%H:%M:%S")

I IPTables vill vi exempelvis öppna för UDP-förfrågningar på port 67 (DHCP), vi loopar därför genom fire_nets och lägger in detta i en fördefinierad iptables-rad. Vi behöver även en funktion för att traversa listor i listor, utan detta blir outputen [192.168.0.0/24, 192.168.1.0/24…], [172.16.0.0/24, 172.16.1.0/24..], [10.10.0.0/24, 10.10.1.0/24…] vilket vi inte vill ha.

def traverse(o, tree_types=(list, tuple)):
  if isinstance(o, tree_types):
    for value in o:
      for subvalue in traverse(value, tree_types):
        yield subvalue
      else:
       yield o

 # Appends a statement for each network in list that needs a firewall opening
 firewall = []
 for network in traverse(fire_nets):
   firewall.append('-A INPUT -p udp -s {NET} --dport 67 -j ACCEPT'.format(NET=network))

Vi spar sedan ner ovanstående nät i en fil.

with open('/firewall/iptables-{time}.txt'.format(time=tfconfig_time), 'w') as outfile:
# Prints firewall-statements to file, uses print to get newline per statement - instead of insterting /n to text
 for fire in firewall:
   print(fire, file=outfile)

Så enkelt har vi skapat vår IP-tables configuration, nästa steg blir att skapa routerconfig. Vi kontrollerar så att respektive lista inte är tom (isf behöver vi ingen interface-config) och använder sedan modulen netaddr vi importerade tidigare för att hämta ut första giltiga ip-adress och subnätmask ur respektive nät.

with open('/loopbacks/interfaces-{time}.txt'.format(time=tfconfig_time), 'w') as outfile:

  if iptv:
    print("\ninterface Lo500", file=outfile)
    for network in iptv:
      loopaddr = IPNetwork(network) # Saves IP-info (uses netaddr)
      print(' ipv4 address ' + str(loopaddr[1]) + ' ' + str(loopaddr.netmask) + ' secondary', file=outfile) # Prints 1st available hostadress + netmask from variable

  if voip:
   print("\ninterface Lo600", file=outfile)
   for network in voip:
     loopaddr = IPNetwork(network)
     print(' ipv4 address ' + str(loopaddr[1]) + ' ' + str(loopaddr.netmask) + ' secondary', file=outfile)

  if surf:
   print("\ninterface Lo700", file=outfile)
   for network in surf:
     loopaddr = IPNetwork(network)
     print(' ipv4 address ' + str(loopaddr[1]) + ' ' + str(loopaddr.netmask) + ' secondary', file=outfile)

  if offnet:
   print("\ninterface Lo800", file=outfile)
   for network in offnet:
     loopaddr = IPNetwork(network)
     print(' ipv4 address ' + str(loopaddr[1]) + ' ' + str(loopaddr.netmask) + ' secondary', file=outfile)

Sista steget blir att skapa DHCP-scope filer, detta blir något krångligare men förhoppningsvis är det inte allt för rörigt. Vi skapar först en textfil för respektive nät vilket kommer användas som mall för våra scopes. Exempelvis:

/templates/voip-scope.txt

subnet $NET netmask $MASK {
 pool {
  allow members of "voip";
  deny members of "notallowed";
  range $FIRST $LAST;
  option routers $OPTROUTER;
  option broadcast-address $BROADCAST;
  option subnet-mask $MASK;
  }
}

Skapa liknande filer för alla nättyper, vi använder sedan netaddr igen för att få ut all info vi behöver till scope-filen från respektive nät genom att loopa varje lista.  Vi öppnar även vår scope-mall och ersätter ex. $FIRST $LAST med infon vi redan hämtat ut genom användandet av dictionarys. Supersmidigt! Glöm inte att appenda infon så vi inte skriver över filen för varje loop-körning.

 # Generate scopes for VoIP
 for network in voip:
   ip = IPNetwork(network)
   net = str(ip[0])
   mask = str(ip.netmask)
   gwip = str(ip[1])
   bcip = str(ip.broadcast)
   first_ip = str(ip[2])
   last_ip = str(ip.broadcast - 1)
   # Testing
   #print('Net: {net}, Mask: {mask}, GW: {gwip}, BC: {bcip}, host {first_ip}, last useable: {last_ip}'.format(net=net, mask=mask, gwip=gwip, bcip=bcip, first_ip=first_ip, last_ip=last_ip))

  replacements = {'$NET':net, '$MASK':mask, '$FIRST':first_ip, '$LAST':last_ip, '$OPTROUTER':gwip, '$BROADCAST':bcip}

  with open('/templates/voip-scope.txt') as infile, open('/dhcp-scopes/voip-{time}.txt'.format(time=tfconfig_time), 'a') as outfile:
    for line in infile:
      for src, target in replacements.items():
        line = line.replace(src, target)
        outfile.write(line)

Vi gör sedan precis likadant för respektive scope-fil. Done!

#### Surf DHCP-Scope Networks ####
 
  subnet 10.10.0.0 netmask 255.255.255.0 {
    pool {
         allow members of "surf";
         deny members of "notallowed";
         range 10.10.0.2 10.10.0.254;
         option routers 10.10.0.1;
         option broadcast-address 10.10.0.255;
         option subnet-mask 255.255.255.0;
         }
  }
  subnet 10.10.1.0 netmask 255.255.255.0 {
    pool {
         allow members of "surf";
         deny members of "notallowed";
         range 10.10.1.2 10.10.1.254;
         option routers 10.10.1.1;
         option broadcast-address 10.10.1.255;
         option subnet-mask 255.255.255.0;
         }
  }
  subnet 10.10.2.0 netmask 255.255.255.0 {
    pool {
         allow members of "surf";
         deny members of "notallowed";
         range 10.10.2.2 10.10.2.254;
         option routers 10.10.2.1;
         option broadcast-address 10.10.2.255;
         option subnet-mask 255.255.255.0;
         }
  }
  subnet 10.10.3.0 netmask 255.255.255.0 {
    pool {
         allow members of "surf";
         deny members of "notallowed";
         range 10.10.3.2 10.10.3.254;
         option routers 10.10.3.1;
         option broadcast-address 10.10.3.255;
         option subnet-mask 255.255.255.0;
         }
  }

#### Interface config ####

interface Lo700
 ipv4 address 10.10.0.1 255.255.255.0 secondary
 ipv4 address 10.10.1.1 255.255.255.0 secondary
 ipv4 address 10.10.2.1 255.255.255.0 secondary
 ipv4 address 10.10.3.1 255.255.255.0 secondary

#### Firewall config ####

-A INPUT -p udp -s 10.10.0.0/24 --dport 67 -j ACCEPT
-A INPUT -p udp -s 10.10.1.0/24 --dport 67 -j ACCEPT
-A INPUT -p udp -s 10.10.2.0/24 --dport 67 -j ACCEPT
-A INPUT -p udp -s 10.10.3.0/24 --dport 67 -j ACCEPT

Har implementerat något liknande på jobbet om än med betydligt fler funktioner då vi hanterar en rätt stor mängd nät mer eller mindre dagligen och det har helt klart förenklat arbetet. Kanske kan leda till lite inspiration för någon annan.. 🙂

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.