1 '''
2 Created on Apr 26, 2010
3
4 @author: jnaous
5 '''
6
7 import re
8 from django.db import models
9 from django.conf import settings
10 from expedient.clearinghouse.resources import models as resource_models
11 from expedient.clearinghouse.slice import models as slice_models
12 from expedient.clearinghouse.aggregate import models as aggregate_models
13 from expedient.common.xmlrpc_serverproxy.models import PasswordXMLRPCServerProxy
14 from django.core.urlresolvers import reverse
15 from django.utils.datetime_safe import datetime
16 from autoslug.fields import AutoSlugField
17 from django.db.models import signals
18 from django.contrib.sites.models import Site
19 import logging
20 from expedient.common.utils import create_or_update, modelfields
21 from expedient.clearinghouse.slice.models import Slice
22 from django.db.models.aggregates import Count
23 from django.core.exceptions import ValidationError
24 from expedient.common.timer.models import Job
25
26 logger = logging.getLogger("OpenflowModels")
27 parse_logger = logging.getLogger("OpenflowModelsParsing")
31
32 cntrlr_url_re = re.compile(r"^((tcp)|(ssl)):(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9]):(?P<port>\d+)$")
34 def error():
35 raise ValidationError(
36 u"Invalid controller URL. The format is "
37 "tcp:<hostname>:<port> or ssl:<hostname>:<port>. Port must "
38 "be less than %s" % (2**16),
39 code="invalid",
40 )
41
42 m = cntrlr_url_re.match(value)
43 if m:
44 port = m.group("port")
45 if not port:
46 error()
47 else:
48 port = int(port)
49 if port > 2**16-1:
50 error()
51 else:
52 error()
53
64
66 information = \
67 """
68 OpenFlow is an open standard that allows network switches to be controlled by
69 a remote controller. It enables researchers to run experimental protocols in
70 production networks, and is currently deployed in several universities.
71 """
72
73 client = models.OneToOneField(PasswordXMLRPCServerProxy)
74 usage_agreement = models.TextField()
75
78
80 self.client.install_trusted_ca()
81
82
83
84 if base_uri.endswith("/"): base_uri = base_uri[:-1]
85 try:
86 logger.debug("Registering topology callback at %s%s" % (
87 base_uri, reverse("openflow_open_xmlrpc")))
88 err = self.client.proxy.register_topology_callback(
89 "%s%s" % (base_uri, reverse("openflow_open_xmlrpc")),
90 "%s" % self.pk,
91 )
92 if err: return err
93 except Exception as ret_exception:
94 import traceback
95 logger.info("XML RPC call failed to aggregate %s" % self.name)
96 traceback.print_exc()
97 return str(ret_exception)
98
99
100 Job.objects.schedule(
101 settings.OPENFLOW_TOPOLOGY_UPDATE_PERIOD,
102 self.update_topology(),
103 )
104
105 err = self.update_topology()
106 if err: return err
107
109 '''
110 Get the topology for this aggregate as a set of links.
111 '''
112 return get_raw_topology(self)
113
115 '''
116 Update the set of available switches.
117 '''
118
119 current_switches = OpenFlowSwitch.objects.filter(aggregate=self)
120
121 switches = {}
122 for switch_info in active_switches_raw:
123 dpid, info = switch_info
124
125
126 portstrs = info["portList"].split(",")
127
128 ports = [int(p) for p in portstrs if p != ""]
129
130 switches[dpid] = ports
131
132 create_or_update_switches(self, switches)
133
135 '''
136 Get the available links and update the network connections.
137 '''
138 create_or_update_links(self, active_links_raw)
139
141 '''
142 Read the topology from the OM and FV, parse it, and store it.
143 '''
144 try:
145 switches = self.client.proxy.get_switches()
146 links = self.client.proxy.get_links()
147 except:
148 import traceback
149 traceback.print_exc()
150 raise
151 self.parse_switches(switches)
152 self.parse_links(links)
153
155 """
156 Get a slice id to use when creating slices at the OM.
157 """
158 return "%s_%s" % (Site.objects.get_current().domain, slice.id)
159
161 """
162 Get the set of slivers in the slice for this aggregate in a format
163 that the OM understands.
164
165 @param slice: The slice to get slivers from
166 @type slice: L{expedient.clearinghouse.slice.models.Slice}
167 @return: C{dict} containing a mapping from datapath ids in the aggregate
168 to flowspaces on that switch.
169 @rtype: C{dict}
170 """
171
172 ifaces = OpenFlowInterface.objects.filter(
173 slice_set=slice, aggregate__id=self.id).select_related(
174 'flowspacerule_set', 'switch')
175
176
177 dpids = set(ifaces.values_list('switch__datapath_id', flat=True))
178
179 sw_slivers = []
180
181 for dpid in dpids:
182 d = {}
183 d['datapath_id'] = dpid
184 d['flowspace'] = []
185 for iface in ifaces.filter(switch__datapath_id=dpid):
186 sliver = iface.sliver_set.get(slice=slice).as_leaf_class()
187 for fs in sliver.flowspacerule_set.all():
188 fsd = {"port_num_start": iface.port_num,
189 "port_num_end": iface.port_num}
190 for f in fs._meta.fields:
191 if f.name != "slivers":
192 v = getattr(fs, f.name)
193 if v is not None and v != "":
194 fsd[f.name] = v
195 else:
196 fsd[f.name] = "*"
197 d['flowspace'].append(fsd)
198 sw_slivers.append(d)
199
200 return sw_slivers
201
202
203
204
205
206 @classmethod
209
212
229
239
245
268
270 """Connection to/from an OpenFlow Interface to a non-OpenFlow Resource"""
271
272 of_iface = models.ForeignKey(
273 "OpenFlowInterface",
274 verbose_name="OpenFlow Interface",
275 related_name="nonopenflow_connections")
276
277 resource = models.ForeignKey(
278 resource_models.Resource,
279 verbose_name="Non-OpenFlow Resource",
280 related_name="openflow_connections")
281
284
288
311
326 signals.post_save.connect(OpenFlowInterfaceSliver.check_save,
327 OpenFlowInterfaceSliver)
330 slivers = models.ManyToManyField(
331 OpenFlowInterfaceSliver,
332 help_text="Select the interfaces to apply the flowspace rule to.",
333 )
334
335 dl_src_start = modelfields.MACAddressField(
336 'Link layer source address range start', null=True, blank=True)
337 dl_dst_start = modelfields.MACAddressField(
338 'Link layer destination address range start', null=True, blank=True)
339 dl_type_start = modelfields.LimitedIntegerField(
340 'Link layer type range start',
341 max_value=2**16-1, min_value=0, blank=True, null=True)
342 vlan_id_start = modelfields.LimitedIntegerField(
343 'VLAN ID range start',
344 max_value=2**12-1, min_value=0, blank=True, null=True)
345 nw_src_start = models.IPAddressField(
346 'Network source address range start', blank=True, null=True)
347 nw_dst_start = models.IPAddressField(
348 'Network destination address range start', blank=True, null=True)
349 nw_proto_start = modelfields.LimitedIntegerField(
350 'Network protocol range start',
351 max_value=2**8-1, min_value=0, blank=True, null=True)
352 tp_src_start = modelfields.LimitedIntegerField(
353 'Transport source port range start',
354 max_value=2**16-1, min_value=0, blank=True, null=True)
355 tp_dst_start = modelfields.LimitedIntegerField(
356 'Transport destination port range start',
357 max_value=2**16-1, min_value=0, blank=True, null=True)
358
359 dl_src_end = modelfields.MACAddressField(
360 'Link layer source address range end', null=True, blank=True)
361 dl_dst_end = modelfields.MACAddressField(
362 'Link layer destination address range end', null=True, blank=True)
363 dl_type_end = modelfields.LimitedIntegerField(
364 'Link layer type range end',
365 max_value=2**16-1, min_value=0, blank=True, null=True)
366 vlan_id_end = modelfields.LimitedIntegerField(
367 'VLAN ID range end',
368 max_value=2**12-1, min_value=0, blank=True, null=True)
369 nw_src_end = models.IPAddressField(
370 'Network source address range end', blank=True, null=True)
371 nw_dst_end = models.IPAddressField(
372 'Network destination address range end', blank=True, null=True)
373 nw_proto_end = modelfields.LimitedIntegerField(
374 'Network protocol range end',
375 max_value=2**8-1, min_value=0, blank=True, null=True)
376 tp_src_end = modelfields.LimitedIntegerField(
377 'Transport source port range end',
378 max_value=2**16-1, min_value=0, blank=True, null=True)
379 tp_dst_end = modelfields.LimitedIntegerField(
380 'Transport destination port range end',
381 max_value=2**16-1, min_value=0, blank=True, null=True)
382
384 """Create or update the switches in aggregate C{aggregate} with switches in C{switches}.
385
386 C{switches} is a dict mapping datapath ids to list of ports.
387
388 """
389
390 active_switch_ids = []
391 active_iface_ids = []
392 for dpid, ports in switches.items():
393 switch, _ = create_or_update(
394 OpenFlowSwitch,
395 filter_attrs=dict(
396 datapath_id=dpid,
397 ),
398 new_attrs=dict(
399 aggregate=aggregate,
400 name=dpid,
401 available=True,
402 status_change_timestamp=datetime.now(),
403 )
404 )
405 active_switch_ids.append(switch.id)
406
407 for port in ports:
408
409 iface, _ = create_or_update(
410 OpenFlowInterface,
411 filter_attrs=dict(
412 switch__datapath_id=dpid,
413 port_num=port,
414 ),
415 new_attrs=dict(
416 aggregate=aggregate,
417 name="Port %s" % port,
418 switch=switch,
419 available=True,
420 status_change_timestamp=datetime.now(),
421 ),
422 )
423 active_iface_ids.append(iface.id)
424
425
426 OpenFlowInterface.objects.filter(
427 aggregate=aggregate).exclude(id__in=active_iface_ids).update(
428 available=False, status_change_timestamp=datetime.now())
429
430 OpenFlowSwitch.objects.filter(
431 aggregate=aggregate).exclude(id__in=active_switch_ids).update(
432 available=False, status_change_timestamp=datetime.now())
433
435 """Create or update the links in aggregate C{aggregate}.
436
437 @param aggregate: the aggregate with openflow links and switches.
438 @param links: a tuple (src dpid, src port, dst dpid, dst port, attrs)
439
440 """
441 active_cnxn_ids = []
442
443 for src_dpid, src_port, dst_dpid, dst_port, attrs in links:
444 parse_logger.debug("parsing link %s:%s - %s:%s" % (
445 src_dpid, src_port, dst_dpid, dst_port))
446 try:
447 src_iface = OpenFlowInterface.objects.get(
448 switch__datapath_id=src_dpid,
449 port_num=src_port,
450 )
451 except OpenFlowInterface.DoesNotExist:
452 logger.warn(
453 "Tried to add connection for non-existing source "
454 "interface %s:%s" % (src_dpid, src_port))
455 continue
456
457 try:
458 dst_iface = OpenFlowInterface.objects.get(
459 switch__datapath_id=dst_dpid,
460 port_num=dst_port,
461 )
462 except OpenFlowInterface.DoesNotExist:
463 logger.warn(
464 "Tried to add connection for non-existing destination "
465 "interface %s:%s" % (dst_dpid, dst_port))
466 continue
467
468 cnxn, _ = OpenFlowConnection.objects.get_or_create(
469 src_iface=src_iface,
470 dst_iface=dst_iface,
471 )
472 active_cnxn_ids.append(cnxn.id)
473
474
475 OpenFlowConnection.objects.filter(
476
477
478 src_iface__switch__aggregate=aggregate,
479 dst_iface__switch__aggregate=aggregate).exclude(
480
481 id__in=active_cnxn_ids).delete()
482
493
497 empty_fs = FlowSpaceRule.objects.annotate(
498 num_slivers=Count("slivers")).filter(
499 num_slivers=0)
500 logger.debug("Deleting %s flowspaces" % empty_fs.count())
501 empty_fs.delete()
502
507
508 signals.post_delete.connect(delete_empty_flowspace, Slice)
509 signals.post_delete.connect(delete_empty_flowspace, OpenFlowInterfaceSliver)
510 signals.m2m_changed.connect(
511 check_fs_change, FlowSpaceRule.slivers.through)
512