1 # -*- coding: utf-8 -*- vim:fileencoding=utf-8:
2 # vim: tabstop=4:shiftwidth=4:softtabstop=4:expandtab
4 # Copyright (C) 2010-2014 GRNET S.A.
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 from ncclient import manager
22 from ncclient.transport.errors import AuthenticationError, SSHError
23 from lxml import etree as ET
24 from django.conf import settings
26 from django.core.cache import cache
28 from celery.exceptions import TimeLimitExceeded, SoftTimeLimitExceeded
33 LOG_FILENAME = os.path.join(settings.LOG_FILE_LOCATION, 'celery_jobs.log')
35 #FORMAT = '%(asctime)s %(levelname)s: %(message)s'
36 #logging.basicConfig(format=FORMAT)
37 formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s')
39 logger = logging.getLogger(__name__)
40 logger.setLevel(logging.DEBUG)
41 handler = logging.FileHandler(LOG_FILENAME)
42 handler.setFormatter(formatter)
43 logger.addHandler(handler)
45 def fod_unknown_host_cb(host, fingerprint):
48 class Retriever(object):
49 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):
51 self.username = username
52 self.password = password
57 self.filter = settings.ROUTE_FILTER%route_name
60 with manager.connect(host=self.device, port=self.port, username=self.username, password=self.password, hostkey_verify=False) as m:
61 xmlconfig = m.get_config(source='running', filter=('subtree',self.filter)).data_xml
64 def proccess_xml(self):
68 xmlconfig = self.fetch_xml()
70 parser.confile = xmlconfig
71 device = parser.export()
74 def fetch_device(self):
75 device = cache.get("device")
76 logger.info("[CACHE] hit! got device")
80 device = self.proccess_xml()
81 if device.routing_options:
82 cache.set("device", device, 3600)
83 logger.info("[CACHE] miss, setting device")
88 class Applier(object):
89 def __init__(self, route_objects = [], route_object=None, device=settings.NETCONF_DEVICE, username=settings.NETCONF_USER, password=settings.NETCONF_PASS, port=settings.NETCONF_PORT):
90 self.route_object = route_object
91 self.route_objects = route_objects
93 self.username = username
94 self.password = password
97 def to_xml(self, operation=None):
98 logger.info("Operation: %s"%operation)
100 logger.info("Generating XML config")
101 route_obj = self.route_object
105 flow.routes.append(route)
106 device.routing_options.append(flow)
107 route.name = route_obj.name
108 if operation == "delete":
109 logger.info("Requesting a delete operation")
110 route.operation = operation
111 device = device.export(netconf_config=True)
112 return ET.tostring(device)
114 route.match['source'].append(route_obj.source)
115 if route_obj.destination:
116 route.match['destination'].append(route_obj.destination)
118 if route_obj.protocol:
119 for protocol in route_obj.protocol.all():
120 route.match['protocol'].append(protocol.protocol)
125 for port in route_obj.port.all():
126 route.match['port'].append(port.port)
130 if route_obj.destinationport:
131 for port in route_obj.destinationport.all():
132 route.match['destination-port'].append(port.port)
136 if route_obj.sourceport:
137 for port in route_obj.sourceport.all():
138 route.match['source-port'].append(port.port)
141 if route_obj.icmpcode:
142 route.match['icmp-code'].append(route_obj.icmpcode)
143 if route_obj.icmptype:
144 route.match['icmp-type'].append(route_obj.icmptype)
145 if route_obj.tcpflag:
146 route.match['tcp-flags'].append(route_obj.tcpflag)
149 for dscp in route_obj.dscp.all():
150 route.match['dscp'].append(dscp.dscp)
155 if route_obj.fragmenttype:
156 for frag in route_obj.fragmenttype.all():
157 route.match['fragment'].append(frag.fragmenttype)
161 for thenaction in route_obj.then.all():
162 if thenaction.action_value:
163 route.then[thenaction.action] = thenaction.action_value
165 route.then[thenaction.action] = True
166 if operation == "replace":
167 logger.info("Requesting a replace operation")
168 route.operation = operation
169 device = device.export(netconf_config=True)
170 return ET.tostring(device)
174 def delete_routes(self):
175 if self.route_objects:
176 logger.info("Generating XML config")
179 for route_object in self.route_objects:
180 route_obj = route_object
182 flow.routes.append(route)
183 route.name = route_obj.name
184 route.operation = 'delete'
185 device.routing_options.append(flow)
186 device = device.export(netconf_config=True)
187 return ET.tostring(device)
191 def apply(self, configuration = None, operation=None):
193 if not configuration:
194 configuration = self.to_xml(operation=operation)
195 edit_is_successful = False
196 commit_confirmed_is_successful = False
197 commit_is_successful = False
199 with manager.connect(host=self.device, port=self.port, username=self.username, password=self.password, hostkey_verify=False) as m:
200 assert(":candidate" in m.server_capabilities)
201 with m.locked(target='candidate'):
204 edit_response = m.edit_config(target='candidate', config=configuration, test_option='test-then-set')
205 edit_is_successful, reason = is_successful(edit_response)
206 logger.info("Successfully edited @ %s" % self.device)
207 if not edit_is_successful:
209 except SoftTimeLimitExceeded:
213 except TimeLimitExceeded:
217 except Exception as e:
218 cause="Caught edit exception: %s %s" %(e,reason)
219 cause=cause.replace('\n', '')
223 if edit_is_successful:
225 commit_confirmed_response = m.commit(confirmed=True, timeout=settings.COMMIT_CONFIRMED_TIMEOUT)
226 commit_confirmed_is_successful, reason = is_successful(commit_confirmed_response)
228 if not commit_confirmed_is_successful:
231 logger.info("Successfully confirmed committed @ %s" % self.device)
232 if not settings.COMMIT:
233 return True, "Successfully confirmed committed"
234 except SoftTimeLimitExceeded:
238 except TimeLimitExceeded:
242 except Exception as e:
243 cause="Caught commit confirmed exception: %s %s" %(e,reason)
244 cause=cause.replace('\n', '')
249 if edit_is_successful and commit_confirmed_is_successful:
251 commit_response = m.commit(confirmed=False)
252 commit_is_successful, reason = is_successful(commit_response)
253 logger.info("Successfully committed @ %s" % self.device)
254 newconfig = m.get_config(source='running', filter=('subtree',settings.ROUTES_FILTER)).data_xml
255 retrieve = Retriever(xml=newconfig)
256 logger.info("[CACHE] caching device configuration")
257 cache.set("device", retrieve.proccess_xml(), 3600)
259 if not commit_is_successful:
262 logger.info("Successfully cached device configuration")
263 return True, "Successfully committed"
264 except SoftTimeLimitExceeded:
268 except TimeLimitExceeded:
272 except Exception as e:
273 cause="Caught commit exception: %s %s" %(e,reason)
274 cause=cause.replace('\n', '')
278 return False, "No configuration was supplied"
280 def is_successful(response):
281 from StringIO import StringIO
282 doc = parsexml_(StringIO(response))
283 rootNode = doc.getroot()
284 success_list = rootNode.xpath("//*[local-name()='ok']")
285 if len(success_list)>0:
289 reason_list = rootNode.xpath("//*[local-name()='error-message']")
290 for reason in reason_list:
291 reason_return = "%s %s" %(reason_return, reason.text)
292 return False, reason_return
295 def parsexml_(*args, **kwargs):
296 if 'parser' not in kwargs:
297 kwargs['parser'] = ET.ETCompatXMLParser()
298 doc = ET.parse(*args, **kwargs)