1 # -*- coding: utf-8 -*- vim:fileencoding=utf-8:
2 # vim: tabstop=4:shiftwidth=4:softtabstop=4:expandtab
4 # Copyright © 2011-2014 Greek Research and Technology Network (GRNET S.A.)
5 # Copyright © 2011-2014 Leonidas Poulopoulos (@leopoul)
7 # Permission to use, copy, modify, and/or distribute this software for any
8 # purpose with or without fee is hereby granted, provided that the above
9 # copyright notice and this permission notice appear in all copies.
11 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
12 # TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
13 # FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
14 # CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
15 # DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
16 # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
20 from ncclient import manager
21 from ncclient.transport.errors import AuthenticationError, SSHError
22 from lxml import etree as ET
23 from django.conf import settings
25 from django.core.cache import cache
31 LOG_FILENAME = os.path.join(settings.LOG_FILE_LOCATION, 'celery_jobs.log')
33 #FORMAT = '%(asctime)s %(levelname)s: %(message)s'
34 #logging.basicConfig(format=FORMAT)
35 formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s')
37 logger = logging.getLogger(__name__)
38 logger.setLevel(logging.DEBUG)
39 handler = logging.FileHandler(LOG_FILENAME)
40 handler.setFormatter(formatter)
41 logger.addHandler(handler)
43 def fod_unknown_host_cb(host, fingerprint):
46 class Retriever(object):
47 def __init__(self, device=settings.NETCONF_DEVICE, username=settings.NETCONF_USER, password=settings.NETCONF_PASS, filter=settings.ROUTES_FILTER, port=settings.NETCONF_PORT, route_name=None, xml=None):
49 self.username = username
50 self.password = password
55 self.filter = settings.ROUTE_FILTER%route_name
58 with manager.connect(host=self.device, port=self.port, username=self.username, password=self.password, unknown_host_cb=fod_unknown_host_cb) as m:
59 xmlconfig = m.get_config(source='running', filter=('subtree',self.filter)).data_xml
62 def proccess_xml(self):
66 xmlconfig = self.fetch_xml()
68 parser.confile = xmlconfig
69 device = parser.export()
72 def fetch_device(self):
73 device = cache.get("device")
74 logger.info("[CACHE] hit! got device")
78 device = self.proccess_xml()
79 if device.routing_options:
80 cache.set("device", device, 3600)
81 logger.info("[CACHE] miss, setting device")
86 class Applier(object):
87 def __init__(self, route_objects = [], route_object=None, device=settings.NETCONF_DEVICE, username=settings.NETCONF_USER, password=settings.NETCONF_PASS, port=settings.NETCONF_PORT):
88 self.route_object = route_object
89 self.route_objects = route_objects
91 self.username = username
92 self.password = password
95 def to_xml(self, operation=None):
96 logger.info("Operation: %s"%operation)
98 logger.info("Generating XML config")
99 route_obj = self.route_object
103 flow.routes.append(route)
104 device.routing_options.append(flow)
105 route.name = route_obj.name
106 if operation == "delete":
107 logger.info("Requesting a delete operation")
108 route.operation = operation
109 device = device.export(netconf_config=True)
110 return ET.tostring(device)
112 route.match['source'].append(route_obj.source)
113 if route_obj.destination:
114 route.match['destination'].append(route_obj.destination)
116 if route_obj.protocol:
117 for protocol in route_obj.protocol.all():
118 route.match['protocol'].append(protocol.protocol)
123 for port in route_obj.port.all():
124 route.match['port'].append(port.port)
128 if route_obj.destinationport:
129 for port in route_obj.destinationport.all():
130 route.match['destination-port'].append(port.port)
134 if route_obj.sourceport:
135 for port in route_obj.sourceport.all():
136 route.match['source-port'].append(port.port)
139 if route_obj.icmpcode:
140 route.match['icmp-code'].append(route_obj.icmpcode)
141 if route_obj.icmptype:
142 route.match['icmp-type'].append(route_obj.icmptype)
143 if route_obj.tcpflag:
144 route.match['tcp-flags'].append(route_obj.tcpflag)
147 for dscp in route_obj.dscp.all():
148 route.match['dscp'].append(dscp.dscp)
153 if route_obj.fragmenttype:
154 for frag in route_obj.fragmenttype.all():
155 route.match['fragment'].append(frag.fragmenttype)
159 for thenaction in route_obj.then.all():
160 if thenaction.action_value:
161 route.then[thenaction.action] = thenaction.action_value
163 route.then[thenaction.action] = True
164 if operation == "replace":
165 logger.info("Requesting a replace operation")
166 route.operation = operation
167 device = device.export(netconf_config=True)
168 return ET.tostring(device)
172 def delete_routes(self):
173 if self.route_objects:
174 logger.info("Generating XML config")
177 for route_object in self.route_objects:
178 route_obj = route_object
180 flow.routes.append(route)
181 route.name = route_obj.name
182 route.operation = 'delete'
183 device.routing_options.append(flow)
184 device = device.export(netconf_config=True)
185 return ET.tostring(device)
189 def apply(self, configuration = None, operation=None):
191 if not configuration:
192 configuration = self.to_xml(operation=operation)
193 edit_is_successful = False
194 commit_confirmed_is_successful = False
195 commit_is_successful = False
197 with manager.connect(host=self.device, port=830, username=self.username, password=self.password, unknown_host_cb=fod_unknown_host_cb) as m:
198 assert(":candidate" in m.server_capabilities)
199 with m.locked(target='candidate'):
202 edit_response = m.edit_config(target='candidate', config=configuration, test_option='test-then-set')
203 edit_is_successful, reason = is_successful(edit_response)
204 logger.info("Successfully edited @ %s" % self.device)
205 if not edit_is_successful:
207 except Exception as e:
208 cause="Caught edit exception: %s %s" %(e,reason)
209 cause=cause.replace('\n', '')
213 if edit_is_successful:
215 commit_confirmed_response = m.commit(confirmed=True, timeout=settings.COMMIT_CONFIRMED_TIMEOUT)
216 commit_confirmed_is_successful, reason = is_successful(commit_confirmed_response)
218 if not commit_confirmed_is_successful:
221 logger.info("Successfully confirmed committed @ %s" % self.device)
222 if not settings.COMMIT:
223 return True, "Successfully confirmed committed"
224 except Exception as e:
225 cause="Caught commit confirmed exception: %s %s" %(e,reason)
226 cause=cause.replace('\n', '')
230 if edit_is_successful and commit_confirmed_is_successful:
232 commit_response = m.commit(confirmed=False)
233 commit_is_successful, reason = is_successful(commit_response)
234 logger.info("Successfully committed @ %s" % self.device)
235 newconfig = m.get_config(source='running', filter=('subtree',settings.ROUTES_FILTER)).data_xml
236 retrieve = Retriever(xml=newconfig)
237 logger.info("[CACHE] caching device configuration")
238 cache.set("device", retrieve.proccess_xml(), 3600)
240 if not commit_is_successful:
243 logger.info("Successfully cached device configuration")
244 return True, "Successfully committed"
245 except Exception as e:
246 cause="Caught commit exception: %s %s" %(e,reason)
247 cause=cause.replace('\n', '')
251 return False, "No configuration was supplied"
253 def is_successful(response):
254 from StringIO import StringIO
255 doc = parsexml_(StringIO(response))
256 rootNode = doc.getroot()
257 success_list = rootNode.xpath("//*[local-name()='ok']")
258 if len(success_list)>0:
262 reason_list = rootNode.xpath("//*[local-name()='error-message']")
263 for reason in reason_list:
264 reason_return = "%s %s" %(reason_return, reason.text)
265 return False, reason_return
268 def parsexml_(*args, **kwargs):
269 if 'parser' not in kwargs:
270 kwargs['parser'] = ET.ETCompatXMLParser()
271 doc = ET.parse(*args, **kwargs)