Package openflow :: Package plugin :: Module models
[hide private]
[frames] | no frames]

Source Code for Module openflow.plugin.models

  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") 
28 29 -def as_is_slugify(value):
30 return value
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+)$")
33 -def validate_controller_url(value):
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
54 -class OpenFlowSliceInfo(models.Model):
55 slice = models.OneToOneField(slice_models.Slice) 56 controller_url = models.CharField( 57 "OpenFlow controller URL", max_length=100, 58 validators=[validate_controller_url], 59 help_text="e.g. tcp:beirut.stanford.edu:6633") 60 # TODO: It is not a good idea to store the password in the clear. 61 password = models.CharField( 62 max_length=64, help_text="This is the password to use when accessing\ 63 the slice directly at the flowvisors.")
64
65 -class OpenFlowAggregate(aggregate_models.Aggregate):
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
76 - class Meta:
77 verbose_name = "OpenFlow Aggregate"
78
79 - def setup_new_aggregate(self, base_uri):
80 self.client.install_trusted_ca() 81 # TODO: re-enable this for security. Currently disabled for testing. 82 # err = self.client.change_password() 83 # if err: return err 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 # schedule a job to automatically update resources 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
108 - def get_raw_topology(self):
109 ''' 110 Get the topology for this aggregate as a set of links. 111 ''' 112 return get_raw_topology(self)
113
114 - def parse_switches(self, active_switches_raw):
115 ''' 116 Update the set of available switches. 117 ''' 118 # switches already in the DB. 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 # The ports is a comma-separated set of values (see flowvisor API). 126 portstrs = info["portList"].split(",") 127 # turn into port numbers 128 ports = [int(p) for p in portstrs if p != ""] 129 130 switches[dpid] = ports 131 132 create_or_update_switches(self, switches)
133 139
140 - def update_topology(self):
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
154 - def _get_slice_id(self, slice):
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
160 - def _get_slivers(self, slice):
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 # get all interfaces in this slice 172 ifaces = OpenFlowInterface.objects.filter( 173 slice_set=slice, aggregate__id=self.id).select_related( 174 'flowspacerule_set', 'switch') 175 176 # get the list of dpids in the slice 177 dpids = set(ifaces.values_list('switch__datapath_id', flat=True)) 178 179 sw_slivers = [] 180 # For each dpid, get the flowspace by looking at the interface slivers 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 # Following are overrides from aggregate_models.Aggregate # 204 ################################################################### 205 206 @classmethod
207 - def get_url_name_prefix(cls):
208 return "openflow"
209
210 - def check_status(self):
211 return self.available and self.client.is_available()
212
213 - def start_slice(self, slice):
214 super(OpenFlowAggregate, self).start_slice(slice) 215 sw_slivers = self._get_slivers(slice) 216 try: 217 return self.client.proxy.create_slice( 218 self._get_slice_id(slice), slice.project.name, 219 slice.project.description, 220 slice.name, slice.description, 221 slice.openflowsliceinfo.controller_url, 222 slice.owner.email, 223 slice.openflowsliceinfo.password, sw_slivers) 224 except Exception as ret_exception: 225 import traceback 226 logger.info("XML RPC call to aggregate %s failed." % self.name) 227 logger.error(traceback.format_exc()) 228 raise
229
230 - def stop_slice(self, slice):
231 super(OpenFlowAggregate, self).stop_slice(slice) 232 try: 233 self.client.proxy.delete_slice(self._get_slice_id(slice)) 234 except Exception as e: 235 import traceback 236 logger.info("XML RPC call failed to aggregate %s" % self.name) 237 traceback.print_exc() 238 raise
239
240 -class OpenFlowSwitch(resource_models.Resource):
241 datapath_id = models.CharField(max_length=100, unique=True) 242
243 - def __unicode__(self):
244 return "OpenFlow Switch %s" % self.datapath_id
245
246 -class OpenFlowConnection(models.Model):
247 '''Connection between two interfaces''' 248 src_iface = models.ForeignKey("OpenFlowInterface", 249 related_name="ingress_connections") 250 dst_iface = models.ForeignKey("OpenFlowInterface", 251 related_name="egress_connections") 252 slug = AutoSlugField( 253 populate_from=lambda instance: "%s_%s_%s_%s" % ( 254 instance.src_iface.switch.datapath_id, 255 instance.src_iface.port_num, 256 instance.dst_iface.switch.datapath_id, 257 instance.dst_iface.port_num, 258 ), 259 slugify=as_is_slugify, 260 editable=False, 261 ) 262
263 - class Meta:
264 unique_together=(("src_iface", "dst_iface"),)
265
266 - def __unicode__(self):
267 return "%s to %s" % (self.src_iface, self.dst_iface)
268
269 -class NonOpenFlowConnection(models.Model):
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
282 - class Meta:
283 unique_together=(("of_iface", "resource"),)
284
285 - def __unicode__(self):
286 return "to/from %s from/to %s" % ( 287 self.of_iface, self.resource.as_leaf_class())
288
289 -class OpenFlowInterface(resource_models.Resource):
290 port_num = models.IntegerField() 291 switch = models.ForeignKey(OpenFlowSwitch) 292 ingress_neighbors = models.ManyToManyField( 293 'self', symmetrical=False, 294 related_name="egress_neighbors", 295 through=OpenFlowConnection, 296 ) 297 slug = AutoSlugField( 298 populate_from=lambda instance: "%s_%s" % ( 299 instance.switch.datapath_id, instance.port_num), 300 slugify=as_is_slugify, 301 editable=False, 302 ) 303
304 - class Meta:
305 unique_together=(("switch", "port_num"),) 306 verbose_name = "OpenFlow Interface"
307
308 - def __unicode__(self):
309 return "Aggregate %s: Port %s on %s" % ( 310 self.aggregate.name, self.port_num, self.switch)
311
312 -class OpenFlowInterfaceSliver(resource_models.Sliver):
313 - class TooManySliversPerSlicePerInterface(Exception): pass
314 315 @classmethod
316 - def check_save(cls, sender, **kwargs):
317 """ 318 Make sure there is only one OpenFlowInterfaceSliver per 319 slice per interface. 320 """ 321 instance = kwargs["instance"] 322 if kwargs["created"]: 323 if OpenFlowInterfaceSliver.objects.filter( 324 slice=instance.slice, resource=instance.resource).count() > 1: 325 raise cls.TooManySliversPerSlicePerInterface()
326 signals.post_save.connect(OpenFlowInterfaceSliver.check_save, 327 OpenFlowInterfaceSliver)
328 329 -class FlowSpaceRule(models.Model):
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
383 -def create_or_update_switches(aggregate, switches):
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 # update the interfaces for this switch 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 # make all inactive switches and interfaces unavailable. 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 482
483 -def get_raw_topology(aggregate):
484 """Get the openflow toplogy as a set of links in the aggregate.""" 485 links = set() 486 for iface in OpenFlowInterface.objects.filter( 487 aggregate=aggregate).select_related("switch", "ingress_neighbors__switch"): 488 for in_ngbr in iface.ingress_neighbors.all(): 489 links.add((iface.switch.datapath_id, iface.port_num, 490 in_ngbr.switch.datapath_id, in_ngbr.port_num)) 491 492 return links
493
494 495 # when a slice is deleted, make sure all its flowspace is deleted too 496 -def delete_empty_flowspace(sender, **kwargs):
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
503 -def check_fs_change(sender, **kwargs):
504 if kwargs["action"] == "post_remove": 505 logger.debug("m2m changed with %s" % kwargs["action"]) 506 delete_empty_flowspace(sender, **kwargs)
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