Usando o POX com REST em um ambiente SDN

Neste post ensinaremos como oferecer uma interface REST informando a quantidade de switches conectados ao controlador POX, além de informar qual a tabela MAC – interface de cada switch conectado. Estas informações estarão disponíveis por meio de uma interface REST, e outros dispositivos poderão acessá-las através de requisições GET. Este tutorial foi feito em uma VM Mininet no VirtualBox (pode ser baixada neste link), uma vez que ela possui ferramentas para simular uma rede controlada pelo POX e também possui códigos POX.

Utilizaremos neste tutorial o código l2_learning.py presente no diretório pox (pox/pox/forwarding/l2_learning.py). Este código faz com que o controlador dinamicamente saiba as relações entre mac e interface dos hosts conectados a ele, além de inserir as regras nos switches pelo protocolo OpenFlow. O código desta aplicação está disponível no GitHub.

Para criarmos a interface REST definiremos inicialmente duas URLs onde as informações serão recuperadas: a primeira informa o dpid dos switches conectados ao controlador, através do endereço http://IP_DO_POX:8000/switches. O IP_DO_POX é o endereço IP do computador onde o POX está sendo executado. A segunda URL compreende as informações da tabela MAC-Interface de cada switch, e pode ser acessada pelo endereço http://IP_DO_POX:8000/switches/<dpid_switch&lt; . Este dpid pode ser recuperado pela URL.

Para inserir as informações nas URLs, precisaremos de listas ou dicionários para disponibilizarmos as informações por um JSON. Deste modo, precisamos receber as informações em um código que verifica os switches conectados e cria a tabela MAC-Interface. O código do l2_learning_rest.py está resumido a seguir:


from pox.core import core
import pox.openflow.libopenflow_01 as of
from pox.lib.util import dpid_to_str
from pox.lib.util import str_to_bool
import time

from SocketServer import ThreadingMixIn
from BaseHTTPServer import *
from time import sleep
import select
import threading

import random
import hashlib
import base64

import os
import posixpath
import urllib
import cgi
import errno

...

class LearningSwitch (object):
    ...
    def __init__ (self, connection, transparent):
    ...

    def _handle_PacketIn (self, event):
    ...

    def flood (message = None):
    ...

    def drop (duration = None):
    ...

class l2_learning (object):

    def __init__ (self, transparent):
    ...

    def _handle_ConnectionUp (self, event):
    ...

    def _setAttribs (parent, child):
    ...

class CoreHandler (SplitRequestHandler):
    ...
    def do_GET (self):
    ...

    def do_HEAD (self):
    ...

    def do_content (self, is_get):
    ...

    def send_info (self, is_get = False):
    ...

class SplitThreadedServer(ThreadingMixIn, HTTPServer):
    ...

def launch (transparent=False, hold_down=_flood_delay, address='', port=8000, static=False):

    try:
        global _flood_delay
        _flood_delay = int(str(hold_down), 10)
        assert _flood_delay &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt;= 0
   except:
        raise RuntimeError(&amp;amp;quot;Expected hold-down to be a number&amp;amp;quot;)

   core.registerNew(l2_learning, str_to_bool(transparent))
   httpd = SplitThreadedServer((address, int(port)), SplitterRequestHandler)
   core.register(&amp;amp;quot;WebServer&amp;amp;quot;, httpd)
   httpd.set_handler(&amp;amp;quot;/&amp;amp;quot;, CoreHandler, httpd, True)

   def run ():
      try:
         log.debug(&amp;amp;quot;Listening on %s:%i&amp;amp;quot; % httpd.socket.getsockname())
         httpd.serve_forever()
   except:
         pass
         log.info(&amp;amp;quot;Server quit&amp;amp;quot;)

   thread = threading.Thread(target=run)
   thread.daemon = True
   thread.start()

O launch, em resumo, inicia o l2_learning_rest. A linha core.registerNew(l2_learning,str_to_bool(transparent)) registra a classe l2_learning, definida mais acima, e esta classe começa a ser executada por seu método __init__. Nela há um método handle_connectionUp() que é executado cada vez que um novo switch se conecta ao controlador, e assim uma instância da classe LearningSwitch é criada para tratar requisições Openflow para aquele switch. A requisição Openflow PACKETIN é tratada pelo método chamado _handle_PacketIn(), que, além de outras verificações específicas, implementa o aprendizado da tabela do switch e a criação de regras no switch para o encaminhamento deste pacote.

O core.register(“WebServer”,httpd) registra o servidor web da classe SplitThreadedServer, e o método run() inicia-o. As URLs são recuperadas através do método DO_GET, que chama o método send_content que envia o conteúdo requisitado através da URL desejada.

Iremos editar inicialmente dois métodos: o handle_connectionUp() que irá salvar uma lista de switches que estão conectados ao controlador, enquanto o handle_packetIn() terá de salvar a tabela que relaciona interface e MAC de cada switch.

Para que ambas as classes possam compartilhar as informações entre si, criaremos um dicionário, chamado idTableSwitches, que armazena cada switch com a chave sendo o dpid do switch e o valor associado à chave sendo a tabela MAC-Interface.

#IMPORTS...
...
log = core.getLogger()
try:
weblog = log.getChild(&amp;amp;quot;server&amp;amp;quot;)
except:
weblog = core.getLogger(&amp;amp;quot;webcore.server&amp;amp;quot;)

#Adding list to be shared between controller and rest
idTableSwitches={}
_flood_delay = 0

class LearningSwitch(object):
...

No método _handle_ConnectionUp() iremos salvar as informações dos switches que se conectam no controlador nas tabelas, como mostrado na linha 6 abaixo:

...
class l2_learning(object)
...
def _handle_ConnectionUp (self, event):
    log.debug(&amp;amp;quot;Connection %s&amp;amp;quot; % (event.connection,))
    idTableSwitches[str(event.connection.dpid)]={}
    LearningSwitch(event.connection, self.transparent)

Como dito anteriormente, este método é chamado quando o switch se conecta ao controlador. Nesta parte do código criamos uma chave com o número do dpid do switch cujo cujo valor será o dicionário onde estão inseridas as relações entre MAC e interfaces.

No método handle_packetIn() da classe LearningSwitch iremos salvar a tabela MAC-Interface no dicionário do switch que possui aquela tabela. No código herdado do l2_learning.py, os autores criam um dicionário chamado macToPort, onde port é a interface que corresponde aquele MAC. Neste tutorial sempre referenciaremos da forma MAC-Interface para evitar confusões com portas da camada de transporte:

...
class LearningSwitch(object)
...

  def _handle_PacketIn(self,event):
  ...

    self.macToPort[packet.src] = event.port # 1

   ...

    log.debug(&amp;amp;quot;installing flow for %s.%i -&amp;amp;amp;amp;amp;gt; %s.%i&amp;amp;quot; %
                  (packet.src, event.port, packet.dst, port))
    idTableSwitches[str(event.connection.dpid)]=self.macToPort
...

Esta parte do código indica no debug que o fluxo será inserido do MAC – interface de origem para o MAC – interface de destino. A tabela macToPort associa o MAC do host (chave) com a interface física em que este está ligado (valor). Após a linha do debug, inserimos (linha 14)  a tabela macToPort no dicionário idTableSwitches.

Com esta mudança, a parte do l2_learning está concluída, vamos ao web server.

Editaremos a classe CoreHandler, que controla as chamadas referentes às URLs pesquisadas. Um destes métodos é o send_content. Nele se uma URL é acessada, este método é chamado e caso a URL bata com alguma das condições, a resposta daquela URL é montada. Iremos, então, criar duas URLs: /switches e /switches/dpid_switch.

...
class CoreHandler(SplitRequestHandler):
...

    def do_content (self, is_get):
    ...
    #adição em relação ao código default
    elif self.path == &amp;amp;quot;/switches&amp;amp;quot; or self.path == &amp;amp;quot;/switches/&amp;amp;quot;:
        self.send_switches(is_get)
    elif self.path.startswith(&amp;amp;quot;/switches/&amp;amp;quot;) and self.path[10:] in idTableSwitches.keys():
        self.send_switch(self.path[10:],is_get)
    else:
      self.send_error(404, &amp;amp;quot;File not found on CoreHandler&amp;amp;quot;)

Abaixo do comentário feito na linha 7, adicionamos duas condições: uma caso a URL acessada seja “/switches” ou “/switches/” e a outra se for “/switches/dpid_switch”. O primeiro caso chama o método send_switches(), indicando que a requisição é GET, enquanto o segundo caso primeiramente verifica se a url começa com switches e se a string da posição 10 em diante está na idTableSwitches. Caso esteja, é chamado o método send_switch(), passando como parâmetro o dpid e indicando que a requisição é GET.

Abaixo estão definidos os métodos send_switches() e send_switch().

...
class CoreHandler(SplitRequestHandler):
...
    def send_switches(self,is_get=True):
        switchesList={'switches':[]}
        for switch_id in idTableSwitches.keys():
            switchesList['switches'].append({'dpid':switch_id})
        self.send_response(200)
        self.send_header(&amp;amp;quot;Content-type&amp;amp;quot;, &amp;amp;quot;application/json&amp;amp;quot;)
        self.end_headers()
        if is_get:
          self.wfile.write(switchesList)

    def send_switch(self,path,is_get=True):
        switchTable={'table':[]}
        tempTable=[]
        switchTable['table']=idTableSwitches[path]
        for rule in switchTable['table']:
            new_rule={'mac':str(rule),'interface':switchTable['table'][rule]}
            tempTable.append(new_rule)
        switchTable['table']=tempTable
        self.send_response(200)
        self.send_header(&amp;amp;quot;Content-type&amp;amp;quot;, &amp;amp;quot;application/json&amp;amp;quot;)
        self.end_headers()
        if is_get:
          self.wfile.write(switchTable)

O primeiro método manipula o dicionário idTableSwitches, preparando o JSON de resposta da URL que será adicionado pelo método self.wfile.write() ao corpo da mensagem HTTP 200 OK de retorno.

O segundo método salva inicialmente a tabela MAC-interface na chave “table” do dicionário switchTable. Após isso, é feito um loop apenas para melhorar a exibição: para cada entrada da tabela MAC-interface, cria-se um objeto new_rule que dispõe a informação de forma mais amigável, com uma chave ‘mac’ para o endereço MAC do host e uma chave ‘interface’ para a interface relacionada àquele MAC. O objeto new_rule é inserido numa temp_table, que ao final do for é inserida no dicionário switchTable[‘table’]. Após isso, montamos a resposta JSON como no método anterior e escrevemos os dados pelo self.wfile.write().

Desta forma, se salvarmos o arquivo, inserirmos ele na pasta ~/pox/pox/forwarding e executarmos o comando ./pox.py forwarding.l2_learning_rest, o controlador passará a funcionar como um switch L2 com uma interface REST que pode ser acessada a partir de um browser comum.

Para testar o controlador e a interface REST recém criada, podemos na VM Mininet executar o comando mn para emularmos uma rede virtual. Para isso, podemos executar o comando

sudo mn --topo tree,2 --controller remote.

A topologia será basicamente desta forma, com os swiches conectados ao controlador:plataforma_reas_virtuais

Caso seja acessada a url http://IP_DO_POX:8000/switches no browser, será mostrado um json contendo os dpids dos três switches. Se, por exemplo, executarmos um ping de h1 para h2 no mininet pelo comando h1 ping h2, apenas o switch 2 terá uma tabela MAC – interface, que poderá ser acessada pela url http://IP_DO_POX:8000/switches/2. Caso seja executado um ping de h1 para h3, os três switches possuirão tabelas MAC – Interface. A partir daí é só verificar nas urls definidas para observar as tabelas de cada switch.

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s