# -*- coding: utf-8 -*-
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import json
from enum import Enum
import requests
from requests.auth import HTTPBasicAuth
from requests.exceptions import HTTPError
from django.core.exceptions import ObjectDoesNotExist
from networkapi.plugins import exceptions
from networkapi.plugins.SDN.base import BaseSdnPlugin
from networkapi.equipamento.models import EquipamentoAcesso
from networkapi.plugins.SDN.ODL.flows.acl import AclFlowBuilder
log = logging.getLogger(__name__)
[docs]class FlowTypes(Enum):
""" Inner class that holds the Enumeration of flow types """
ACL = 0
[docs]class ODLPlugin(BaseSdnPlugin):
"""
Plugin base para interação com controlador ODL
"""
versions = ["BERYLLIUM", "BORON", "CARBON", "NITROGEN"]
def __init__(self, **kwargs):
super(ODLPlugin, self).__init__(**kwargs)
try:
if not isinstance(self.equipment_access, EquipamentoAcesso):
msg = 'equipment_access is not of EquipamentoAcesso type'
log.info(msg)
raise TypeError(msg)
except (AttributeError, TypeError):
# If AttributeError raised, equipment_access do not exists
self.equipment_access = self._get_equipment_access()
if self.version not in self.versions:
log.error("Invalid version at ODL Controller initialization")
raise exceptions.ValueInvalid(msg="Invalid version at ODL Controller initialization")
[docs] def add_flow(self, data=None, flow_id=0, flow_type=FlowTypes.ACL, nodes_ids=[]):
if flow_type == FlowTypes.ACL:
builder = AclFlowBuilder(data, self.environment, version=self.version)
flows_set = builder.build()
try:
for flows in flows_set:
for flow in flows['flow']:
self._flow(flow_id=flow['id'],
method='put',
data=json.dumps({'flow': [flow]}),
nodes_ids=nodes_ids)
except HTTPError as e:
raise exceptions.CommandErrorException(
msg=self._parse_errors(e.response.json()))
[docs] def del_flow(self, flow_id=0, nodes_ids=[]):
return self._flow(flow_id=flow_id, method='delete', nodes_ids=nodes_ids)
[docs] def update_all_flows(self, data, flow_type=FlowTypes.ACL):
current_flows = self.get_flows()
for node in current_flows.keys():
log.info("Starting update all flows for node %s"%node)
if flow_type == FlowTypes.ACL:
builder = AclFlowBuilder(data, self.environment, version=self.version)
new_flows_set = builder.build()
#Makes a diff
operations = self._diff_flows(current_flows[node], new_flows_set)
try:
for flow in operations["delete"]:
self.del_flow(flow_id=flow['id'], nodes_ids=[node])
for flow in operations["insert"]:
self._flow(flow_id=flow['id'],
method='put',
data=json.dumps({'flow': [flow]}),
nodes_ids=[node])
except Exception as e:
message = self._parse_errors(e.response.json())
log.error("ERROR while updating all flows: %s" % message)
raise exceptions.CommandErrorException(msg=message)
[docs] def flush_flows(self):
nodes_ids = self._get_nodes_ids()
# if len(nodes_ids) < 1:
# raise exceptions.ControllerInventoryIsEmpty(msg="No nodes found")
for node_id in nodes_ids:
try:
path = "/restconf/config/opendaylight-inventory:nodes/node/" \
"%s/flow-node-inventory:table/0/" % node_id
self._request(
method="delete", path=path, contentType='json'
)
except HTTPError as e:
if e.response.status_code == 404:
pass
else:
raise exceptions.CommandErrorException(
msg=self._parse_errors(e.response.json()))
except Exception as e:
raise e
def _parse_errors(self, err_json):
""" Generic message creator to format errors """
sep = ""
msg = ""
for error in err_json["errors"]["error"]:
msg = msg + sep + error["error-message"]
sep = ". "
return msg
[docs] def get_flow(self, flow_id=0):
""" HTTP GET method to request flows by id """
return self._flow(flow_id=flow_id, method='get')
def _flow(self, flow_id=0, method='', data=None, nodes_ids=[]):
""" Generic implementation of the plugin communication with the
remote controller through HTTP requests
"""
allowed_methods = ["get", "put", "delete"]
if flow_id < 1 or method not in allowed_methods:
log.error("Invalid parameters in OLDPlugin flow handler")
raise exceptions.ValueInvalid()
if nodes_ids==[]:
nodes_ids = self._get_nodes_ids()
# if len(nodes_ids) < 1:
# raise exceptions.ControllerInventoryIsEmpty(msg="No nodes found")
return_flows = []
for node_id in nodes_ids:
path = "/restconf/config/opendaylight-inventory:nodes/node/%s/" \
"flow-node-inventory:table/0/flow/%s" % (node_id, flow_id)
return_flows.append(
self._request(
method=method, path=path, data=data, contentType='json'
)
)
return return_flows
[docs] def get_flows(self):
""" Returns All flows for table 0 of all switches of a environment """
nodes_ids = self._get_nodes_ids()
# if len(nodes_ids) < 1:
# raise exceptions.ControllerInventoryIsEmpty(msg="No nodes found")
flows_list = {}
for node_id in nodes_ids:
try:
path = "/restconf/config/opendaylight-inventory:nodes/node/" \
"%s/flow-node-inventory:table/0/" % (node_id)
inventory = self._request(
method="get",
path=path,
contentType='json'
)
flows_list[node_id] = inventory["flow-node-inventory:table"]
except HTTPError as e:
if e.response.status_code == 404:
flows_list[node_id] = []
else:
raise exceptions.CommandErrorException(
msg=self._parse_errors(e.response.json()))
except Exception as e:
raise e
return flows_list
def _get_nodes_ids(self):
#TODO: We need to check on newer versions (later to Berylliun) if the
# check on both config and operational is still necessary
path1 = "/restconf/config/network-topology:network-topology/topology/flow:1/"
path2 = "/restconf/config/opendaylight-inventory:nodes/"
path3 = "/restconf/operational/network-topology:network-topology/topology/flow:1/"
nodes_ids={}
try:
nodes = self._request(method='get', path=path2, contentType='json')['nodes']
if nodes.has_key('node'):
for node in nodes['node']:
if node["id"].find("openflow:")==0:
nodes_ids[node["id"]] = 1
except HTTPError as e:
if e.response.status_code != 404:
raise e
try:
topo1=self._request(method='get', path=path1, contentType='json')['topology'][0]
if topo1.has_key('node'):
for node in topo1['node']:
if node["node-id"].find("openflow:") == 0:
nodes_ids[node["node-id"]] = 1
except HTTPError as e:
if e.response.status_code!=404:
raise e
try:
topo2 = self._request(method='get', path=path3, contentType='json')['topology'][0]
if topo2.has_key('node'):
for node in topo2['node']:
if node["node-id"].find("openflow:") == 0:
nodes_ids[node["node-id"]] = 1
except HTTPError as e:
if e.response.status_code!=404:
raise e
nodes_ids_list = nodes_ids.keys()
nodes_ids_list.sort()
return nodes_ids_list
def _request(self, **kwargs):
""" Sends request to controller """
# Params and default values
params = {
'method': 'get',
'path': '',
'data': None,
'contentType': 'json',
'verify': False
}
# Setting params via kwargs or use the defaults
for param in params:
if param in kwargs:
params[param] = kwargs.get(param)
headers = self._get_headers(contentType=params["contentType"])
uri = self._get_uri(path=params["path"])
log.debug(
"Starting %s request to controller %s at %s. Data to be sent: %s" %
(params["method"], self.equipment.nome, uri, params["data"])
)
try:
# Raises AttributeError if method is not valid
func = getattr(requests, params["method"])
request = func(
uri,
auth=self._get_auth(),
headers=headers,
verify=params["verify"],
data=params["data"]
)
request.raise_for_status()
if request.status_code==200 and request.content=='':
return
try:
return json.loads(request.text)
except Exception as exception:
log.error("Response received from uri '%s': \n%s",
uri, request.text)
log.error("Can't serialize as Json: %s" % exception)
return
except AttributeError:
log.error('Request method must be valid HTTP request. '
'ie: GET, POST, PUT, DELETE')
def _get_auth(self):
return self._basic_auth()
def _basic_auth(self):
""" Create a HTTP Basic Authentication object """
return HTTPBasicAuth(
self.equipment_access.user,
self.equipment_access.password
)
def _o_auth(self):
pass
def _get_headers(self, contentType):
""" Creates HTTP headers needed by the plugin """
types = {
'json': 'application/yang.data+json',
'xml': 'application/xml',
'text': 'text/plain'
}
return {'content-type': types[contentType],
'Accept': types[contentType]}
def _get_equipment_access(self):
""" Tries to get the equipment access """
try:
access = None
try:
access = EquipamentoAcesso.search(
None, self.equipment, 'https').uniqueResult()
except ObjectDoesNotExist:
access = EquipamentoAcesso.search(
None, self.equipment, 'http').uniqueResult()
return access
except Exception:
log.error('Access type %s not found for equipment %s.' %
('https', self.equipment.nome))
raise exceptions.InvalidEquipmentAccessException()
def _diff_flows(self, current_data, new_data):
#This function compares the current applied data with the desired new data
#returning a dict containing
# operation: the action that should be taken (delete or insert)
# flow: the flow that should be manipulated
if current_data != []:
current_data=current_data[0]['flow']
#turn lists into dicts and merge ids
ids_merged = []
new = {}
current = {}
for new_flows in new_data:
for new_flow in new_flows['flow']:
new[new_flow['id']] = new_flow
ids_merged.append(new_flow['id'])
for current_flow in current_data:
current[current_flow['id']] = current_flow
if ids_merged.count(current_flow['id'])==0:
ids_merged.append(current_flow['id'])
operations={"delete":[], "insert":[]} #update is also an insertion
ids_merged.sort()
for id in ids_merged:
if not id in new:
operations["delete"].append(current[id])
log.debug("flow id %s will be deleted"%id)
else:
if not id in current:
operations["insert"].append(new[id])
log.debug("flow id %s will be inserted" % id)
elif self.assertDictsEqual(new[id], current[id])==False:
operations["insert"].append(new[id])
log.debug("flow id %s will be updated" % id)
return operations
[docs] def assertDictsEqual(self, d1, d2):
for k in d1.keys():
if not d2.has_key(k):
log.debug("%s key missing" % k)
return False
else:
if type(d1[k]) is dict:
if self.assertDictsEqual(d1[k], d2[k]) == False:
return False
else:
#TODO: ignore cookie is a workaround for a unknown problem when
# diffing cookies
if "%s"%k=="cookie":
# ignoring cookie for now"
pass
elif type(d1[k]) == int or type(d2[k])==int:
if "%s"%d1[k] != "%s"%d2[k]:
log.debug("%s differs: %s - %s" % (k, d1[k], d2[k]))
return False
else:
if d1[k] != d2[k]:
log.debug ("%s differs: %s - %s"%(k, d1[k], d2[k]))
return False
return True