blob: 7cc5cb7ec3ec5c4b10ede9cb422366c16313f1fd [file] [log] [blame]
Siobhan Tully00353f72013-10-08 21:53:27 -04001from django.db import models
Scott Baker0d306722015-04-15 20:58:20 -07002from core.models import PlCoreBase,SingletonModel,PlCoreBaseManager
Tony Mack50e12212015-03-09 13:03:56 -04003from core.models.plcorebase import StrippedCharField
Scott Baker7f8ef8f2015-04-20 14:24:29 -07004from xos.exceptions import *
Scott Bakerb2385622015-07-06 14:27:31 -07005from operator import attrgetter
Scott Baker7211f5b2015-04-14 17:18:51 -07006import json
Siobhan Tully00353f72013-10-08 21:53:27 -04007
Scott Bakerc24f86d2015-08-14 09:10:11 -07008COARSE_KIND="coarse"
9
Scott Baker9d1c6d92015-07-13 13:07:27 -070010class AttributeMixin(object):
11 # helper for extracting things from a json-encoded service_specific_attribute
12 def get_attribute(self, name, default=None):
13 if self.service_specific_attribute:
14 attributes = json.loads(self.service_specific_attribute)
15 else:
16 attributes = {}
17 return attributes.get(name, default)
18
19 def set_attribute(self, name, value):
20 if self.service_specific_attribute:
21 attributes = json.loads(self.service_specific_attribute)
22 else:
23 attributes = {}
24 attributes[name]=value
25 self.service_specific_attribute = json.dumps(attributes)
26
27 def get_initial_attribute(self, name, default=None):
28 if self._initial["service_specific_attribute"]:
29 attributes = json.loads(self._initial["service_specific_attribute"])
30 else:
31 attributes = {}
32 return attributes.get(name, default)
33
Scott Bakereb098e62015-07-13 13:54:06 -070034 @classmethod
35 def setup_simple_attributes(cls):
36 for (attrname, default) in cls.simple_attributes:
Scott Bakere2879d32015-07-13 14:27:51 -070037 setattr(cls, attrname, property(lambda self, attrname=attrname, default=default: self.get_attribute(attrname, default),
38 lambda self, value, attrname=attrname: self.set_attribute(attrname, value),
39 None,
40 attrname))
Scott Bakereb098e62015-07-13 13:54:06 -070041
Scott Baker9d1c6d92015-07-13 13:07:27 -070042class Service(PlCoreBase, AttributeMixin):
Scott Baker0d306722015-04-15 20:58:20 -070043 # when subclassing a service, redefine KIND to describe the new service
44 KIND = "generic"
45
Siobhan Tully00353f72013-10-08 21:53:27 -040046 description = models.TextField(max_length=254,null=True, blank=True,help_text="Description of Service")
47 enabled = models.BooleanField(default=True)
Scott Baker0d306722015-04-15 20:58:20 -070048 kind = StrippedCharField(max_length=30, help_text="Kind of service", default=KIND)
Tony Mack50e12212015-03-09 13:03:56 -040049 name = StrippedCharField(max_length=30, help_text="Service Name")
50 versionNumber = StrippedCharField(max_length=30, help_text="Version of Service Definition")
Siobhan Tullycf04fb62014-01-11 11:25:57 -050051 published = models.BooleanField(default=True)
Tony Mack50e12212015-03-09 13:03:56 -040052 view_url = StrippedCharField(blank=True, null=True, max_length=1024)
53 icon_url = StrippedCharField(blank=True, null=True, max_length=1024)
Scott Baker5b044612015-04-30 14:30:56 -070054 public_key = models.TextField(null=True, blank=True, max_length=1024, help_text="Public key string")
Scott Bakerdc63fb32015-11-12 16:22:52 -080055 private_key_fn = StrippedCharField(blank=True, null=True, max_length=1024)
Siobhan Tully00353f72013-10-08 21:53:27 -040056
Scott Bakerb9040e92015-07-13 12:33:28 -070057 # Service_specific_attribute and service_specific_id are opaque to XOS
58 service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
59 service_specific_attribute = models.TextField(blank=True, null=True)
60
Scott Baker0d306722015-04-15 20:58:20 -070061 def __init__(self, *args, **kwargs):
62 # for subclasses, set the default kind appropriately
63 self._meta.get_field("kind").default = self.KIND
64 super(Service, self).__init__(*args, **kwargs)
65
66 @classmethod
67 def get_service_objects(cls):
68 return cls.objects.filter(kind = cls.KIND)
69
Scott Baker27de6012015-07-24 15:36:02 -070070 @classmethod
Scott Bakercd32ad02015-10-19 21:18:53 -070071 def get_deleted_service_objects(cls):
72 return cls.deleted_objects.filter(kind = cls.KIND)
73
74 @classmethod
Scott Baker27de6012015-07-24 15:36:02 -070075 def get_service_objects_by_user(cls, user):
76 return cls.select_by_user(user).filter(kind = cls.KIND)
77
78 @classmethod
79 def select_by_user(cls, user):
80 if user.is_admin:
81 return cls.objects.all()
82 else:
83 service_ids = [sp.slice.id for sp in ServicePrivilege.objects.filter(user=user)]
84 return cls.objects.filter(id__in=service_ids)
85
Scott Bakerbcea8cf2015-12-07 22:20:40 -080086 @property
87 def serviceattribute_dict(self):
88 attrs = {}
89 for attr in self.serviceattributes.all():
90 attrs[attr.name] = attr.value
91 return attrs
92
Siobhan Tully00353f72013-10-08 21:53:27 -040093 def __unicode__(self): return u'%s' % (self.name)
94
Tony Mack950b4492015-04-29 12:23:10 -040095 def can_update(self, user):
96 return user.can_update_service(self, allow=['admin'])
Scott Bakerb2385622015-07-06 14:27:31 -070097
Scott Baker25757222015-05-11 16:36:41 -070098 def get_scalable_nodes(self, slice, max_per_node=None, exclusive_slices=[]):
99 """
100 Get a list of nodes that can be used to scale up a slice.
101
102 slice - slice to scale up
Tony Mackd8515472015-08-19 11:58:18 -0400103 max_per_node - maximum numbers of instances that 'slice' can have on a single node
Scott Baker25757222015-05-11 16:36:41 -0700104 exclusive_slices - list of slices that must have no nodes in common with 'slice'.
105 """
106
Tony Mackd8515472015-08-19 11:58:18 -0400107 from core.models import Node, Instance # late import to get around order-of-imports constraint in __init__.py
Scott Baker25757222015-05-11 16:36:41 -0700108
109 nodes = list(Node.objects.all())
110
Tony Mackd8515472015-08-19 11:58:18 -0400111 conflicting_instances = Instance.objects.filter(slice__in = exclusive_slices)
112 conflicting_nodes = Node.objects.filter(instances__in = conflicting_instances)
Scott Baker25757222015-05-11 16:36:41 -0700113
114 nodes = [x for x in nodes if x not in conflicting_nodes]
115
Tony Mackd8515472015-08-19 11:58:18 -0400116 # If max_per_node is set, then limit the number of instances this slice
Scott Baker25757222015-05-11 16:36:41 -0700117 # can have on a single node.
118 if max_per_node:
119 acceptable_nodes = []
120 for node in nodes:
Tony Mackd8515472015-08-19 11:58:18 -0400121 existing_count = node.instances.filter(slice=slice).count()
Scott Baker25757222015-05-11 16:36:41 -0700122 if existing_count < max_per_node:
123 acceptable_nodes.append(node)
124 nodes = acceptable_nodes
125
126 return nodes
127
128 def pick_node(self, slice, max_per_node=None, exclusive_slices=[]):
129 # Pick the best node to scale up a slice.
130
131 nodes = self.get_scalable_nodes(slice, max_per_node, exclusive_slices)
Tony Mackd8515472015-08-19 11:58:18 -0400132 nodes = sorted(nodes, key=lambda node: node.instances.all().count())
Scott Baker25757222015-05-11 16:36:41 -0700133 if not nodes:
134 return None
135 return nodes[0]
136
137 def adjust_scale(self, slice_hint, scale, max_per_node=None, exclusive_slices=[]):
Tony Mackd8515472015-08-19 11:58:18 -0400138 from core.models import Instance # late import to get around order-of-imports constraint in __init__.py
Scott Baker25757222015-05-11 16:36:41 -0700139
140 slices = [x for x in self.slices.all() if slice_hint in x.name]
141 for slice in slices:
Tony Mackd8515472015-08-19 11:58:18 -0400142 while slice.instances.all().count() > scale:
143 s = slice.instances.all()[0]
144 # print "drop instance", s
Scott Baker25757222015-05-11 16:36:41 -0700145 s.delete()
146
Tony Mackd8515472015-08-19 11:58:18 -0400147 while slice.instances.all().count() < scale:
Scott Baker25757222015-05-11 16:36:41 -0700148 node = self.pick_node(slice, max_per_node, exclusive_slices)
149 if not node:
150 # no more available nodes
151 break
152
153 image = slice.default_image
154 if not image:
155 raise XOSConfigurationError("No default_image for slice %s" % slice.name)
156
157 flavor = slice.default_flavor
158 if not flavor:
159 raise XOSConfigurationError("No default_flavor for slice %s" % slice.name)
160
Tony Mackd8515472015-08-19 11:58:18 -0400161 s = Instance(slice=slice,
Scott Baker25757222015-05-11 16:36:41 -0700162 node=node,
163 creator=slice.creator,
164 image=image,
165 flavor=flavor,
166 deployment=node.site_deployment.deployment)
167 s.save()
168
Tony Mackd8515472015-08-19 11:58:18 -0400169 # print "add instance", s
Tony Mack950b4492015-04-29 12:23:10 -0400170
Scott Baker012c54b2015-12-08 19:27:50 -0800171 def get_vtn_dependencies(self):
172 provider_net_ids = []
173 for tenant in self.subscribed_tenants.all():
174 if tenant.provider_service:
175 for slice in tenant.provider_service.slices.all():
176 for ns in slice.networkslices.all():
177 if ns.network:
178 if not (ns.network.id) in provider_net_ids:
179 provider_net_ids.append(ns.network_id)
180 return provider_net_ids
181
182
Siobhan Tully00353f72013-10-08 21:53:27 -0400183class ServiceAttribute(PlCoreBase):
Scott Bakerbcea8cf2015-12-07 22:20:40 -0800184 name = models.CharField(help_text="Attribute Name", max_length=128)
Tony Mack50e12212015-03-09 13:03:56 -0400185 value = StrippedCharField(help_text="Attribute Value", max_length=1024)
Siobhan Tully00353f72013-10-08 21:53:27 -0400186 service = models.ForeignKey(Service, related_name='serviceattributes', help_text="The Service this attribute is associated with")
187
Tony Mack950b4492015-04-29 12:23:10 -0400188class ServiceRole(PlCoreBase):
189 ROLE_CHOICES = (('admin','Admin'),)
190 role = StrippedCharField(choices=ROLE_CHOICES, unique=True, max_length=30)
191
192 def __unicode__(self): return u'%s' % (self.role)
193
194class ServicePrivilege(PlCoreBase):
195 user = models.ForeignKey('User', related_name='serviceprivileges')
196 service = models.ForeignKey('Service', related_name='serviceprivileges')
197 role = models.ForeignKey('ServiceRole',related_name='serviceprivileges')
198
199 class Meta:
Tony Mack02683de2015-05-13 12:21:28 -0400200 unique_together = ('user', 'service', 'role')
Tony Mack950b4492015-04-29 12:23:10 -0400201
202 def __unicode__(self): return u'%s %s %s' % (self.service, self.user, self.role)
203
204 def can_update(self, user):
205 if not self.service.enabled:
206 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
207 return self.service.can_update(user)
208
209 def save(self, *args, **kwds):
210 if not self.service.enabled:
211 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
212 super(ServicePrivilege, self).save(*args, **kwds)
213
214 def delete(self, *args, **kwds):
215 if not self.service.enabled:
216 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
Scott Bakera86489f2015-07-01 18:29:08 -0700217 super(ServicePrivilege, self).delete(*args, **kwds)
218
Scott Baker27de6012015-07-24 15:36:02 -0700219 @classmethod
220 def select_by_user(cls, user):
Tony Mack950b4492015-04-29 12:23:10 -0400221 if user.is_admin:
Scott Baker27de6012015-07-24 15:36:02 -0700222 qs = cls.objects.all()
Tony Mack950b4492015-04-29 12:23:10 -0400223 else:
Scott Baker27de6012015-07-24 15:36:02 -0700224 qs = cls.objects.filter(user=user)
Scott Bakera86489f2015-07-01 18:29:08 -0700225 return qs
226
Scott Baker9d1c6d92015-07-13 13:07:27 -0700227class TenantRoot(PlCoreBase, AttributeMixin):
Scott Bakera86489f2015-07-01 18:29:08 -0700228 """ A tenantRoot is one of the things that can sit at the root of a chain
229 of tenancy. This object represents a node.
230 """
231
232 KIND= "generic"
233 kind = StrippedCharField(max_length=30, default=KIND)
Scott Bakerb2385622015-07-06 14:27:31 -0700234 name = StrippedCharField(max_length=255, help_text="name", blank=True, null=True)
Scott Bakera86489f2015-07-01 18:29:08 -0700235
Scott Baker29415a82015-07-07 12:12:42 -0700236 service_specific_attribute = models.TextField(blank=True, null=True)
237 service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
Scott Bakera86489f2015-07-01 18:29:08 -0700238
Scott Baker126ad472015-07-07 17:59:44 -0700239 def __init__(self, *args, **kwargs):
240 # for subclasses, set the default kind appropriately
241 self._meta.get_field("kind").default = self.KIND
242 super(TenantRoot, self).__init__(*args, **kwargs)
243
Scott Bakerb2385622015-07-06 14:27:31 -0700244 def __unicode__(self):
245 if not self.name:
246 return u"%s-tenant_root-#%s" % (str(self.kind), str(self.id))
247 else:
248 return self.name
249
250 def can_update(self, user):
251 return user.can_update_tenant_root(self, allow=['admin'])
252
Scott Baker29415a82015-07-07 12:12:42 -0700253 def get_subscribed_tenants(self, tenant_class):
254 ids = self.subscribed_tenants.filter(kind=tenant_class.KIND)
255 return tenant_class.objects.filter(id__in = ids)
256
257 def get_newest_subscribed_tenant(self, kind):
258 st = list(self.get_subscribed_tenants(kind))
259 if not st:
260 return None
261 return sorted(st, key=attrgetter('id'))[0]
262
263 @classmethod
264 def get_tenant_objects(cls):
265 return cls.objects.filter(kind = cls.KIND)
266
Scott Baker27de6012015-07-24 15:36:02 -0700267 @classmethod
268 def get_tenant_objects_by_user(cls, user):
269 return cls.select_by_user(user).filter(kind = cls.KIND)
270
271 @classmethod
272 def select_by_user(cls, user):
273 if user.is_admin:
274 return cls.objects.all()
275 else:
276 tr_ids = [trp.tenant_root.id for trp in TenantRootPrivilege.objects.filter(user=user)]
277 return cls.objects.filter(id__in=tr_ids)
278
Scott Baker9d1c6d92015-07-13 13:07:27 -0700279class Tenant(PlCoreBase, AttributeMixin):
Scott Baker91e85882015-04-10 16:42:26 -0700280 """ A tenant is a relationship between two entities, a subscriber and a
Scott Bakera86489f2015-07-01 18:29:08 -0700281 provider. This object represents an edge.
Scott Baker91e85882015-04-10 16:42:26 -0700282
283 The subscriber can be a User, a Service, or a Tenant.
284
285 The provider is always a Service.
Scott Bakera86489f2015-07-01 18:29:08 -0700286
287 TODO: rename "Tenant" to "Tenancy"
Scott Baker91e85882015-04-10 16:42:26 -0700288 """
Scott Baker0d306722015-04-15 20:58:20 -0700289
Scott Bakeref58a842015-04-26 20:30:40 -0700290 CONNECTIVITY_CHOICES = (('public', 'Public'), ('private', 'Private'), ('na', 'Not Applicable'))
291
Scott Baker0d306722015-04-15 20:58:20 -0700292 # when subclassing a service, redefine KIND to describe the new service
293 KIND = "generic"
294
295 kind = StrippedCharField(max_length=30, default=KIND)
Scott Bakera86489f2015-07-01 18:29:08 -0700296 provider_service = models.ForeignKey(Service, related_name='provided_tenants')
297
298 # The next four things are the various type of objects that can be subscribers of this Tenancy
299 # relationship. One and only one can be used at a time.
300 subscriber_service = models.ForeignKey(Service, related_name='subscribed_tenants', blank=True, null=True)
301 subscriber_tenant = models.ForeignKey("Tenant", related_name='subscribed_tenants', blank=True, null=True)
302 subscriber_user = models.ForeignKey("User", related_name='subscribed_tenants', blank=True, null=True)
303 subscriber_root = models.ForeignKey("TenantRoot", related_name="subscribed_tenants", blank=True, null=True)
304
305 # Service_specific_attribute and service_specific_id are opaque to XOS
Scott Baker1b7c6f12015-05-06 19:49:31 -0700306 service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
307 service_specific_attribute = models.TextField(blank=True, null=True)
Scott Bakera86489f2015-07-01 18:29:08 -0700308
309 # Connect_method is only used by Coarse tenants
Scott Bakeref58a842015-04-26 20:30:40 -0700310 connect_method = models.CharField(null=False, blank=False, max_length=30, choices=CONNECTIVITY_CHOICES, default="na")
Scott Baker91e85882015-04-10 16:42:26 -0700311
Scott Baker0d306722015-04-15 20:58:20 -0700312 def __init__(self, *args, **kwargs):
313 # for subclasses, set the default kind appropriately
314 self._meta.get_field("kind").default = self.KIND
315 super(Tenant, self).__init__(*args, **kwargs)
316
Scott Baker91e85882015-04-10 16:42:26 -0700317 def __unicode__(self):
Scott Bakerfe91f622015-05-20 20:42:04 -0700318 return u"%s-tenant-%s" % (str(self.kind), str(self.id))
Scott Baker91e85882015-04-10 16:42:26 -0700319
Scott Baker0d306722015-04-15 20:58:20 -0700320 @classmethod
321 def get_tenant_objects(cls):
322 return cls.objects.filter(kind = cls.KIND)
323
Scott Bakereb50ee32015-05-05 17:52:03 -0700324 @classmethod
Scott Baker27de6012015-07-24 15:36:02 -0700325 def get_tenant_objects_by_user(cls, user):
326 return cls.select_by_user(user).filter(kind = cls.KIND)
327
328 @classmethod
Scott Bakereb50ee32015-05-05 17:52:03 -0700329 def get_deleted_tenant_objects(cls):
330 return cls.deleted_objects.filter(kind = cls.KIND)
331
Scott Bakerbcea8cf2015-12-07 22:20:40 -0800332 @property
333 def tenantattribute_dict(self):
334 attrs = {}
335 for attr in self.tenantattributes.all():
336 attrs[attr.name] = attr.value
337 return attrs
338
Scott Baker7f8ef8f2015-04-20 14:24:29 -0700339 # helper function to be used in subclasses that want to ensure service_specific_id is unique
340 def validate_unique_service_specific_id(self):
341 if self.pk is None:
342 if self.service_specific_id is None:
343 raise XOSMissingField("subscriber_specific_id is None, and it's a required field", fields={"service_specific_id": "cannot be none"})
344
345 conflicts = self.get_tenant_objects().filter(service_specific_id=self.service_specific_id)
346 if conflicts:
347 raise XOSDuplicateKey("service_specific_id %s already exists" % self.service_specific_id, fields={"service_specific_id": "duplicate key"})
348
Scott Bakerb2385622015-07-06 14:27:31 -0700349 def save(self, *args, **kwargs):
350 subCount = sum( [1 for e in [self.subscriber_service, self.subscriber_tenant, self.subscriber_user, self.subscriber_root] if e is not None])
351 if (subCount > 1):
352 raise XOSConflictingField("Only one of subscriber_service, subscriber_tenant, subscriber_user, subscriber_root should be set")
353
354 super(Tenant, self).save(*args, **kwargs)
355
356 def get_subscribed_tenants(self, tenant_class):
357 ids = self.subscribed_tenants.filter(kind=tenant_class.KIND)
358 return tenant_class.objects.filter(id__in = ids)
359
360 def get_newest_subscribed_tenant(self, kind):
361 st = list(self.get_subscribed_tenants(kind))
362 if not st:
363 return None
364 return sorted(st, key=attrgetter('id'))[0]
365
Scott Bakerc8914bf2015-11-18 20:58:08 -0800366class Scheduler(object):
367 # XOS Scheduler Abstract Base Class
368 # Used to implement schedulers that pick which node to put instances on
369
370 def __init__(self, slice):
371 self.slice = slice
372
373 def pick(self):
374 # this method should return a tuple (node, parent)
375 # node is the node to instantiate on
376 # parent is for container_vm instances only, and is the VM that will
377 # hold the container
378
379 raise Exception("Abstract Base")
380
381class LeastLoadedNodeScheduler(Scheduler):
382 # This scheduler always return the node with the fewest number of instances.
383
384 def __init__(self, slice):
385 super(LeastLoadedNodeScheduler, self).__init__(slice)
386
387 def pick(self):
388 from core.models import Node
389 nodes = list(Node.objects.all())
Scott Bakerbcea8cf2015-12-07 22:20:40 -0800390
Scott Bakerc8914bf2015-11-18 20:58:08 -0800391 # TODO: logic to filter nodes by which nodes are up, and which
392 # nodes the slice can instantiate on.
393 nodes = sorted(nodes, key=lambda node: node.instances.all().count())
394 return [nodes[0], None]
395
396class ContainerVmScheduler(Scheduler):
397 # This scheduler picks a VM in the slice with the fewest containers inside
398 # of it. If no VMs are suitable, then it creates a VM.
399
400 # this is a hack and should be replaced by something smarter...
401 LOOK_FOR_IMAGES=["ubuntu-vcpe4", # ONOS demo machine -- preferred vcpe image
402 "Ubuntu 14.04 LTS", # portal
403 "Ubuntu-14.04-LTS", # ONOS demo machine
404 "trusty-server-multi-nic", # CloudLab
405 ]
406
407 MAX_VM_PER_CONTAINER = 10
408
409 def __init__(self, slice):
410 super(ContainerVmScheduler, self).__init__(slice)
411
412 @property
413 def image(self):
414 from core.models import Image
415
416 look_for_images = self.LOOK_FOR_IMAGES
417 for image_name in look_for_images:
418 images = Image.objects.filter(name = image_name)
419 if images:
420 return images[0]
421
422 raise XOSProgrammingError("No ContainerVM image (looked for %s)" % str(look_for_images))
423
424 def make_new_instance(self):
425 from core.models import Instance, Flavor
426
427 flavors = Flavor.objects.filter(name="m1.small")
428 if not flavors:
429 raise XOSConfigurationError("No m1.small flavor")
430
431 (node,parent) = LeastLoadedNodeScheduler(self.slice).pick()
432
433 instance = Instance(slice = self.slice,
434 node = node,
435 image = self.image,
436 creator = self.slice.creator,
437 deployment = node.site_deployment.deployment,
438 flavor = flavors[0],
439 isolation = "vm",
440 parent = parent)
441 instance.save()
442 # We rely on a special naming convention to identify the VMs that will
443 # hole containers.
444 instance.name = "%s-outer-%s" % (instance.slice.name, instance.id)
445 instance.save()
446 return instance
447
448 def pick(self):
449 from core.models import Instance, Flavor
450
451 for vm in self.slice.instances.filter(isolation="vm"):
452 avail_vms = []
453 if (vm.name.startswith("%s-outer-" % self.slice.name)):
454 container_count = Instance.objects.filter(parent=vm).count()
455 if (container_count < self.MAX_VM_PER_CONTAINER):
456 avail_vms.append( (vm, container_count) )
457 # sort by least containers-per-vm
458 avail_vms = sorted(avail_vms, key = lambda x: x[1])
459 print "XXX", avail_vms
460 if avail_vms:
461 instance = avail_vms[0][0]
462 return (instance.node, instance)
463
464 instance = self.make_new_instance()
465 return (instance.node, instance)
466
Scott Bakerc1584b82015-09-09 16:36:06 -0700467class TenantWithContainer(Tenant):
468 """ A tenant that manages a container """
469
470 # this is a hack and should be replaced by something smarter...
Scott Bakerf05c4972015-09-09 16:43:39 -0700471 LOOK_FOR_IMAGES=["ubuntu-vcpe4", # ONOS demo machine -- preferred vcpe image
Scott Bakerc1584b82015-09-09 16:36:06 -0700472 "Ubuntu 14.04 LTS", # portal
473 "Ubuntu-14.04-LTS", # ONOS demo machine
Scott Bakerf05c4972015-09-09 16:43:39 -0700474 "trusty-server-multi-nic", # CloudLab
Scott Bakerc1584b82015-09-09 16:36:06 -0700475 ]
476
Scott Bakera759fe32015-11-16 22:51:02 -0800477 LOOK_FOR_CONTAINER_IMAGES=["andybavier/docker-vcpe"]
478
Scott Bakerc1584b82015-09-09 16:36:06 -0700479 class Meta:
480 proxy = True
481
482 def __init__(self, *args, **kwargs):
483 super(TenantWithContainer, self).__init__(*args, **kwargs)
Tony Mack32010062015-09-13 22:50:39 +0000484 self.cached_instance=None
485 self.orig_instance_id = self.get_initial_attribute("instance_id")
Scott Baker5c125e42015-11-02 20:54:28 -0800486
Scott Bakerc1584b82015-09-09 16:36:06 -0700487 @property
Tony Mack32010062015-09-13 22:50:39 +0000488 def instance(self):
489 from core.models import Instance
490 if getattr(self, "cached_instance", None):
491 return self.cached_instance
492 instance_id=self.get_attribute("instance_id")
Srikanth Vavilapalli17b5a3c2015-11-17 12:21:02 -0600493 if not instance_id:
494 return None
495 instances=Instance.objects.filter(id=instance_id)
496 if not instances:
497 return None
498 instance=instances[0]
Tony Mack32010062015-09-13 22:50:39 +0000499 instance.caller = self.creator
500 self.cached_instance = instance
501 return instance
Scott Bakerc1584b82015-09-09 16:36:06 -0700502
Tony Mack32010062015-09-13 22:50:39 +0000503 @instance.setter
504 def instance(self, value):
Scott Bakerc1584b82015-09-09 16:36:06 -0700505 if value:
506 value = value.id
Tony Mack32010062015-09-13 22:50:39 +0000507 if (value != self.get_attribute("instance_id", None)):
508 self.cached_instance=None
509 self.set_attribute("instance_id", value)
Scott Bakerc1584b82015-09-09 16:36:06 -0700510
Scott Baker5c125e42015-11-02 20:54:28 -0800511 @property
Scott Bakerc1584b82015-09-09 16:36:06 -0700512 def creator(self):
513 from core.models import User
514 if getattr(self, "cached_creator", None):
515 return self.cached_creator
516 creator_id=self.get_attribute("creator_id")
517 if not creator_id:
518 return None
519 users=User.objects.filter(id=creator_id)
520 if not users:
521 return None
522 user=users[0]
523 self.cached_creator = users[0]
524 return user
525
526 @creator.setter
527 def creator(self, value):
528 if value:
529 value = value.id
530 if (value != self.get_attribute("creator_id", None)):
531 self.cached_creator=None
532 self.set_attribute("creator_id", value)
533
534 @property
535 def image(self):
536 from core.models import Image
537 # Implement the logic here to pick the image that should be used when
538 # instantiating the VM that will hold the container.
Scott Bakera759fe32015-11-16 22:51:02 -0800539
540 slice = self.provider_service.slices.all()
541 if not slice:
542 raise XOSProgrammingError("provider service has no slice")
543 slice = slice[0]
544
545 if slice.default_isolation in ["container", "container_vm"]:
546 look_for_images = self.LOOK_FOR_CONTAINER_IMAGES
547 else:
548 look_for_images = self.LOOK_FOR_IMAGES
549
550 for image_name in look_for_images:
Scott Bakerc1584b82015-09-09 16:36:06 -0700551 images = Image.objects.filter(name = image_name)
552 if images:
553 return images[0]
554
Scott Bakerc8914bf2015-11-18 20:58:08 -0800555 raise XOSProgrammingError("No VPCE image (looked for %s)" % str(look_for_images))
Scott Baker5c125e42015-11-02 20:54:28 -0800556
557 @creator.setter
558 def creator(self, value):
559 if value:
560 value = value.id
561 if (value != self.get_attribute("creator_id", None)):
562 self.cached_creator=None
563 self.set_attribute("creator_id", value)
564
Scott Bakera759fe32015-11-16 22:51:02 -0800565 def save_instance(self, instance):
566 # Override this function to do custom pre-save or post-save processing,
567 # such as creating ports for containers.
568 instance.save()
Scott Baker5c125e42015-11-02 20:54:28 -0800569
Srikanth Vavilapalli17b5a3c2015-11-17 12:21:02 -0600570 def pick_least_loaded_instance_in_slice(self, slices):
571 for slice in slices:
572 if slice.instances.all().count() > 0:
573 for instance in slice.instances.all():
574 #Pick the first instance that has lesser than 5 tenants
575 if self.count_of_tenants_of_an_instance(instance) < 5:
576 return instance
577 return None
578
579 #TODO: Ideally the tenant count for an instance should be maintained using a
580 #many-to-one relationship attribute, however this model being proxy, it does
581 #not permit any new attributes to be defined. Find if any better solutions
582 def count_of_tenants_of_an_instance(self, instance):
583 tenant_count = 0
584 for tenant in self.get_tenant_objects().all():
585 if tenant.get_attribute("instance_id", None) == instance.id:
586 tenant_count += 1
587 return tenant_count
588
Scott Bakera759fe32015-11-16 22:51:02 -0800589 def manage_container(self):
Tony Mack32010062015-09-13 22:50:39 +0000590 from core.models import Instance, Flavor
Scott Bakerc1584b82015-09-09 16:36:06 -0700591
592 if self.deleted:
593 return
594
Tony Mack32010062015-09-13 22:50:39 +0000595 if (self.instance is not None) and (self.instance.image != self.image):
596 self.instance.delete()
597 self.instance = None
Scott Bakerc1584b82015-09-09 16:36:06 -0700598
Tony Mack32010062015-09-13 22:50:39 +0000599 if self.instance is None:
Scott Bakerc1584b82015-09-09 16:36:06 -0700600 if not self.provider_service.slices.count():
Srikanth Vavilapalli17b5a3c2015-11-17 12:21:02 -0600601 raise XOSConfigurationError("The service has no slices")
Scott Bakerc1584b82015-09-09 16:36:06 -0700602
Srikanth Vavilapalli17b5a3c2015-11-17 12:21:02 -0600603 new_instance_created = False
604 instance = None
605 if self.get_attribute("use_same_instance_for_multiple_tenants", default=False):
606 #Find if any existing instances can be used for this tenant
607 slices = self.provider_service.slices.all()
608 instance = self.pick_least_loaded_instance_in_slice(slices)
Scott Bakerc1584b82015-09-09 16:36:06 -0700609
Srikanth Vavilapalli17b5a3c2015-11-17 12:21:02 -0600610 if not instance:
611 flavors = Flavor.objects.filter(name="m1.small")
612 if not flavors:
613 raise XOSConfigurationError("No m1.small flavor")
614
Srikanth Vavilapalli3406fbb2015-11-17 13:41:38 -0600615 slice = self.provider_service.slices.all()[0]
Scott Bakera759fe32015-11-16 22:51:02 -0800616
Srikanth Vavilapalli3406fbb2015-11-17 13:41:38 -0600617 if slice.default_isolation == "container_vm":
Scott Bakerc8914bf2015-11-18 20:58:08 -0800618 (node, parent) = ContainerVmScheduler(slice).pick()
Srikanth Vavilapalli3406fbb2015-11-17 13:41:38 -0600619 else:
Scott Bakerc8914bf2015-11-18 20:58:08 -0800620 (node, parent) = LeastLoadedNodeScheduler(slice).pick()
Scott Bakera759fe32015-11-16 22:51:02 -0800621
Srikanth Vavilapalli3406fbb2015-11-17 13:41:38 -0600622 instance = Instance(slice = slice,
Srikanth Vavilapalli17b5a3c2015-11-17 12:21:02 -0600623 node = node,
624 image = self.image,
625 creator = self.creator,
626 deployment = node.site_deployment.deployment,
Srikanth Vavilapalli3406fbb2015-11-17 13:41:38 -0600627 flavor = flavors[0],
628 isolation = slice.default_isolation,
629 parent = parent)
630 self.save_instance(instance)
Srikanth Vavilapalli17b5a3c2015-11-17 12:21:02 -0600631 new_instance_created = True
Scott Bakerc1584b82015-09-09 16:36:06 -0700632
633 try:
Tony Mack32010062015-09-13 22:50:39 +0000634 self.instance = instance
Scott Bakerc1584b82015-09-09 16:36:06 -0700635 super(TenantWithContainer, self).save()
636 except:
Srikanth Vavilapalli17b5a3c2015-11-17 12:21:02 -0600637 if new_instance_created:
638 instance.delete()
Scott Bakerc1584b82015-09-09 16:36:06 -0700639 raise
640
641 def cleanup_container(self):
Tony Mack32010062015-09-13 22:50:39 +0000642 if self.instance:
Srikanth Vavilapalli17b5a3c2015-11-17 12:21:02 -0600643 if self.get_attribute("use_same_instance_for_multiple_tenants", default=False):
644 #Delete the instance only if this is last tenant in that instance
645 tenant_count = self.count_of_tenants_of_an_instance(self.instance)
646 if tenant_count == 0:
647 self.instance.delete()
648 else:
649 self.instance.delete()
Tony Mack32010062015-09-13 22:50:39 +0000650 self.instance = None
Scott Bakerb2385622015-07-06 14:27:31 -0700651
Scott Bakeref58a842015-04-26 20:30:40 -0700652class CoarseTenant(Tenant):
Scott Bakera86489f2015-07-01 18:29:08 -0700653 """ TODO: rename "CoarseTenant" --> "StaticTenant" """
Scott Bakeref58a842015-04-26 20:30:40 -0700654 class Meta:
655 proxy = True
Siobhan Tully00353f72013-10-08 21:53:27 -0400656
Scott Bakerc24f86d2015-08-14 09:10:11 -0700657 KIND = COARSE_KIND
Scott Bakeref58a842015-04-26 20:30:40 -0700658
659 def save(self, *args, **kwargs):
660 if (not self.subscriber_service):
661 raise XOSValidationError("subscriber_service cannot be null")
662 if (self.subscriber_tenant or self.subscriber_user):
663 raise XOSValidationError("subscriber_tenant and subscriber_user must be null")
664
665 super(CoarseTenant,self).save()
Scott Bakera86489f2015-07-01 18:29:08 -0700666
667class Subscriber(TenantRoot):
668 """ Intermediate class for TenantRoots that are to be Subscribers """
669
670 class Meta:
671 proxy = True
672
673 KIND = "Subscriber"
674
675class Provider(TenantRoot):
676 """ Intermediate class for TenantRoots that are to be Providers """
677
678 class Meta:
679 proxy = True
680
681 KIND = "Provider"
682
Scott Baker1e7e3482015-10-15 15:59:19 -0700683class TenantAttribute(PlCoreBase):
Scott Baker3ab4db82015-10-20 17:12:36 -0700684 name = models.CharField(help_text="Attribute Name", max_length=128)
Scott Baker1e7e3482015-10-15 15:59:19 -0700685 value = models.TextField(help_text="Attribute Value")
686 tenant = models.ForeignKey(Tenant, related_name='tenantattributes', help_text="The Tenant this attribute is associated with")
687
Scott Bakera86489f2015-07-01 18:29:08 -0700688class TenantRootRole(PlCoreBase):
Scott Baker1729e342015-07-24 15:48:03 -0700689 ROLE_CHOICES = (('admin','Admin'), ('access','Access'))
Scott Bakera86489f2015-07-01 18:29:08 -0700690
691 role = StrippedCharField(choices=ROLE_CHOICES, unique=True, max_length=30)
692
693 def __unicode__(self): return u'%s' % (self.role)
694
695class TenantRootPrivilege(PlCoreBase):
696 user = models.ForeignKey('User', related_name="tenant_root_privileges")
697 tenant_root = models.ForeignKey('TenantRoot', related_name="tenant_root_privileges")
698 role = models.ForeignKey('TenantRootRole', related_name="tenant_root_privileges")
699
700 class Meta:
701 unique_together = ('user', 'tenant_root', 'role')
702
Scott Bakerb2385622015-07-06 14:27:31 -0700703 def __unicode__(self): return u'%s %s %s' % (self.tenant_root, self.user, self.role)
Scott Bakera86489f2015-07-01 18:29:08 -0700704
705 def save(self, *args, **kwds):
706 if not self.user.is_active:
707 raise PermissionDenied, "Cannot modify role(s) of a disabled user"
Scott Bakerc8e947a2015-07-24 10:15:31 -0700708 super(TenantRootPrivilege, self).save(*args, **kwds)
Scott Bakera86489f2015-07-01 18:29:08 -0700709
710 def can_update(self, user):
Scott Bakerc8e947a2015-07-24 10:15:31 -0700711 return user.can_update_tenant_root_privilege(self)
Scott Bakera86489f2015-07-01 18:29:08 -0700712
Scott Baker27de6012015-07-24 15:36:02 -0700713 @classmethod
714 def select_by_user(cls, user):
Scott Bakera86489f2015-07-01 18:29:08 -0700715 if user.is_admin:
Scott Baker1729e342015-07-24 15:48:03 -0700716 return cls.objects.all()
Scott Bakera86489f2015-07-01 18:29:08 -0700717 else:
Scott Baker1729e342015-07-24 15:48:03 -0700718 # User can see his own privilege
719 trp_ids = [trp.id for trp in cls.objects.filter(user=user)]
720
721 # A slice admin can see the SlicePrivileges for his Slice
722 for priv in cls.objects.filter(user=user, role__role="admin"):
723 trp_ids.extend( [trp.id for trp in cls.objects.filter(tenant_root=priv.tenant_root)] )
724
725 return cls.objects.filter(id__in=trp_ids)
726
Scott Bakerb2385622015-07-06 14:27:31 -0700727