blob: ba54a3380c7d225382671439069f36fc215d350e [file] [log] [blame]
Siobhan Tully00353f72013-10-08 21:53:27 -04001from django.db import models
Scott Baker008a9962015-04-15 20:58:20 -07002from core.models import PlCoreBase,SingletonModel,PlCoreBaseManager
Tony Mackd84b1ff2015-03-09 13:03:56 -04003from core.models.plcorebase import StrippedCharField
Scott Bakerd921e1c2015-04-20 14:24:29 -07004from xos.exceptions import *
Scott Baker618a4892015-07-06 14:27:31 -07005from operator import attrgetter
Scott Bakerf57e5592015-04-14 17:18:51 -07006import json
Siobhan Tully00353f72013-10-08 21:53:27 -04007
Scott Baker2461bec2015-08-14 09:10:11 -07008COARSE_KIND="coarse"
9
Scott Baker82498c52015-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 Baker74404fe2015-07-13 13:54:06 -070034 @classmethod
35 def setup_simple_attributes(cls):
36 for (attrname, default) in cls.simple_attributes:
Scott Baker096dce82015-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 Baker74404fe2015-07-13 13:54:06 -070041
Scott Baker82498c52015-07-13 13:07:27 -070042class Service(PlCoreBase, AttributeMixin):
Scott Baker008a9962015-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 Baker008a9962015-04-15 20:58:20 -070048 kind = StrippedCharField(max_length=30, help_text="Kind of service", default=KIND)
Tony Mackd84b1ff2015-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 Mackd84b1ff2015-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 Baker68944742015-04-30 14:30:56 -070054 public_key = models.TextField(null=True, blank=True, max_length=1024, help_text="Public key string")
Scott Bakerf60c0102015-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 Baker2f0828e2015-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 Baker008a9962015-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 Baker16573d32015-07-24 15:36:02 -070070 @classmethod
Scott Baker542cd6f2015-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 Baker16573d32015-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
Siobhan Tully00353f72013-10-08 21:53:27 -040086 def __unicode__(self): return u'%s' % (self.name)
87
Tony Mack9d2ea092015-04-29 12:23:10 -040088 def can_update(self, user):
89 return user.can_update_service(self, allow=['admin'])
Scott Baker618a4892015-07-06 14:27:31 -070090
Scott Baker98436732015-05-11 16:36:41 -070091 def get_scalable_nodes(self, slice, max_per_node=None, exclusive_slices=[]):
92 """
93 Get a list of nodes that can be used to scale up a slice.
94
95 slice - slice to scale up
Tony Mack3de59e32015-08-19 11:58:18 -040096 max_per_node - maximum numbers of instances that 'slice' can have on a single node
Scott Baker98436732015-05-11 16:36:41 -070097 exclusive_slices - list of slices that must have no nodes in common with 'slice'.
98 """
99
Tony Mack3de59e32015-08-19 11:58:18 -0400100 from core.models import Node, Instance # late import to get around order-of-imports constraint in __init__.py
Scott Baker98436732015-05-11 16:36:41 -0700101
102 nodes = list(Node.objects.all())
103
Tony Mack3de59e32015-08-19 11:58:18 -0400104 conflicting_instances = Instance.objects.filter(slice__in = exclusive_slices)
105 conflicting_nodes = Node.objects.filter(instances__in = conflicting_instances)
Scott Baker98436732015-05-11 16:36:41 -0700106
107 nodes = [x for x in nodes if x not in conflicting_nodes]
108
Tony Mack3de59e32015-08-19 11:58:18 -0400109 # If max_per_node is set, then limit the number of instances this slice
Scott Baker98436732015-05-11 16:36:41 -0700110 # can have on a single node.
111 if max_per_node:
112 acceptable_nodes = []
113 for node in nodes:
Tony Mack3de59e32015-08-19 11:58:18 -0400114 existing_count = node.instances.filter(slice=slice).count()
Scott Baker98436732015-05-11 16:36:41 -0700115 if existing_count < max_per_node:
116 acceptable_nodes.append(node)
117 nodes = acceptable_nodes
118
119 return nodes
120
121 def pick_node(self, slice, max_per_node=None, exclusive_slices=[]):
122 # Pick the best node to scale up a slice.
123
124 nodes = self.get_scalable_nodes(slice, max_per_node, exclusive_slices)
Tony Mack3de59e32015-08-19 11:58:18 -0400125 nodes = sorted(nodes, key=lambda node: node.instances.all().count())
Scott Baker98436732015-05-11 16:36:41 -0700126 if not nodes:
127 return None
128 return nodes[0]
129
130 def adjust_scale(self, slice_hint, scale, max_per_node=None, exclusive_slices=[]):
Tony Mack3de59e32015-08-19 11:58:18 -0400131 from core.models import Instance # late import to get around order-of-imports constraint in __init__.py
Scott Baker98436732015-05-11 16:36:41 -0700132
133 slices = [x for x in self.slices.all() if slice_hint in x.name]
134 for slice in slices:
Tony Mack3de59e32015-08-19 11:58:18 -0400135 while slice.instances.all().count() > scale:
136 s = slice.instances.all()[0]
137 # print "drop instance", s
Scott Baker98436732015-05-11 16:36:41 -0700138 s.delete()
139
Tony Mack3de59e32015-08-19 11:58:18 -0400140 while slice.instances.all().count() < scale:
Scott Baker98436732015-05-11 16:36:41 -0700141 node = self.pick_node(slice, max_per_node, exclusive_slices)
142 if not node:
143 # no more available nodes
144 break
145
146 image = slice.default_image
147 if not image:
148 raise XOSConfigurationError("No default_image for slice %s" % slice.name)
149
150 flavor = slice.default_flavor
151 if not flavor:
152 raise XOSConfigurationError("No default_flavor for slice %s" % slice.name)
153
Tony Mack3de59e32015-08-19 11:58:18 -0400154 s = Instance(slice=slice,
Scott Baker98436732015-05-11 16:36:41 -0700155 node=node,
156 creator=slice.creator,
157 image=image,
158 flavor=flavor,
159 deployment=node.site_deployment.deployment)
160 s.save()
161
Tony Mack3de59e32015-08-19 11:58:18 -0400162 # print "add instance", s
Tony Mack9d2ea092015-04-29 12:23:10 -0400163
Siobhan Tully00353f72013-10-08 21:53:27 -0400164class ServiceAttribute(PlCoreBase):
165 name = models.SlugField(help_text="Attribute Name", max_length=128)
Tony Mackd84b1ff2015-03-09 13:03:56 -0400166 value = StrippedCharField(help_text="Attribute Value", max_length=1024)
Siobhan Tully00353f72013-10-08 21:53:27 -0400167 service = models.ForeignKey(Service, related_name='serviceattributes', help_text="The Service this attribute is associated with")
168
Tony Mack9d2ea092015-04-29 12:23:10 -0400169class ServiceRole(PlCoreBase):
170 ROLE_CHOICES = (('admin','Admin'),)
171 role = StrippedCharField(choices=ROLE_CHOICES, unique=True, max_length=30)
172
173 def __unicode__(self): return u'%s' % (self.role)
174
175class ServicePrivilege(PlCoreBase):
176 user = models.ForeignKey('User', related_name='serviceprivileges')
177 service = models.ForeignKey('Service', related_name='serviceprivileges')
178 role = models.ForeignKey('ServiceRole',related_name='serviceprivileges')
179
180 class Meta:
Tony Mack9ec754e2015-05-13 12:21:28 -0400181 unique_together = ('user', 'service', 'role')
Tony Mack9d2ea092015-04-29 12:23:10 -0400182
183 def __unicode__(self): return u'%s %s %s' % (self.service, self.user, self.role)
184
185 def can_update(self, user):
186 if not self.service.enabled:
187 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
188 return self.service.can_update(user)
189
190 def save(self, *args, **kwds):
191 if not self.service.enabled:
192 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
193 super(ServicePrivilege, self).save(*args, **kwds)
194
195 def delete(self, *args, **kwds):
196 if not self.service.enabled:
197 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
Scott Baker4587b822015-07-01 18:29:08 -0700198 super(ServicePrivilege, self).delete(*args, **kwds)
199
Scott Baker16573d32015-07-24 15:36:02 -0700200 @classmethod
201 def select_by_user(cls, user):
Tony Mack9d2ea092015-04-29 12:23:10 -0400202 if user.is_admin:
Scott Baker16573d32015-07-24 15:36:02 -0700203 qs = cls.objects.all()
Tony Mack9d2ea092015-04-29 12:23:10 -0400204 else:
Scott Baker16573d32015-07-24 15:36:02 -0700205 qs = cls.objects.filter(user=user)
Scott Baker4587b822015-07-01 18:29:08 -0700206 return qs
207
Scott Baker82498c52015-07-13 13:07:27 -0700208class TenantRoot(PlCoreBase, AttributeMixin):
Scott Baker4587b822015-07-01 18:29:08 -0700209 """ A tenantRoot is one of the things that can sit at the root of a chain
210 of tenancy. This object represents a node.
211 """
212
213 KIND= "generic"
214 kind = StrippedCharField(max_length=30, default=KIND)
Scott Baker618a4892015-07-06 14:27:31 -0700215 name = StrippedCharField(max_length=255, help_text="name", blank=True, null=True)
Scott Baker4587b822015-07-01 18:29:08 -0700216
Scott Bakerefcec632015-07-07 12:12:42 -0700217 service_specific_attribute = models.TextField(blank=True, null=True)
218 service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
Scott Baker4587b822015-07-01 18:29:08 -0700219
Scott Bakerdb66fd32015-07-07 17:59:44 -0700220 def __init__(self, *args, **kwargs):
221 # for subclasses, set the default kind appropriately
222 self._meta.get_field("kind").default = self.KIND
223 super(TenantRoot, self).__init__(*args, **kwargs)
224
Scott Baker618a4892015-07-06 14:27:31 -0700225 def __unicode__(self):
226 if not self.name:
227 return u"%s-tenant_root-#%s" % (str(self.kind), str(self.id))
228 else:
229 return self.name
230
231 def can_update(self, user):
232 return user.can_update_tenant_root(self, allow=['admin'])
233
Scott Bakerefcec632015-07-07 12:12:42 -0700234 def get_subscribed_tenants(self, tenant_class):
235 ids = self.subscribed_tenants.filter(kind=tenant_class.KIND)
236 return tenant_class.objects.filter(id__in = ids)
237
238 def get_newest_subscribed_tenant(self, kind):
239 st = list(self.get_subscribed_tenants(kind))
240 if not st:
241 return None
242 return sorted(st, key=attrgetter('id'))[0]
243
244 @classmethod
245 def get_tenant_objects(cls):
246 return cls.objects.filter(kind = cls.KIND)
247
Scott Baker16573d32015-07-24 15:36:02 -0700248 @classmethod
249 def get_tenant_objects_by_user(cls, user):
250 return cls.select_by_user(user).filter(kind = cls.KIND)
251
252 @classmethod
253 def select_by_user(cls, user):
254 if user.is_admin:
255 return cls.objects.all()
256 else:
257 tr_ids = [trp.tenant_root.id for trp in TenantRootPrivilege.objects.filter(user=user)]
258 return cls.objects.filter(id__in=tr_ids)
259
Scott Baker82498c52015-07-13 13:07:27 -0700260class Tenant(PlCoreBase, AttributeMixin):
Scott Baker8103d0f2015-04-10 16:42:26 -0700261 """ A tenant is a relationship between two entities, a subscriber and a
Scott Baker4587b822015-07-01 18:29:08 -0700262 provider. This object represents an edge.
Scott Baker8103d0f2015-04-10 16:42:26 -0700263
264 The subscriber can be a User, a Service, or a Tenant.
265
266 The provider is always a Service.
Scott Baker4587b822015-07-01 18:29:08 -0700267
268 TODO: rename "Tenant" to "Tenancy"
Scott Baker8103d0f2015-04-10 16:42:26 -0700269 """
Scott Baker008a9962015-04-15 20:58:20 -0700270
Scott Baker925a8fa2015-04-26 20:30:40 -0700271 CONNECTIVITY_CHOICES = (('public', 'Public'), ('private', 'Private'), ('na', 'Not Applicable'))
272
Scott Baker008a9962015-04-15 20:58:20 -0700273 # when subclassing a service, redefine KIND to describe the new service
274 KIND = "generic"
275
276 kind = StrippedCharField(max_length=30, default=KIND)
Scott Baker4587b822015-07-01 18:29:08 -0700277 provider_service = models.ForeignKey(Service, related_name='provided_tenants')
278
279 # The next four things are the various type of objects that can be subscribers of this Tenancy
280 # relationship. One and only one can be used at a time.
281 subscriber_service = models.ForeignKey(Service, related_name='subscribed_tenants', blank=True, null=True)
282 subscriber_tenant = models.ForeignKey("Tenant", related_name='subscribed_tenants', blank=True, null=True)
283 subscriber_user = models.ForeignKey("User", related_name='subscribed_tenants', blank=True, null=True)
284 subscriber_root = models.ForeignKey("TenantRoot", related_name="subscribed_tenants", blank=True, null=True)
285
286 # Service_specific_attribute and service_specific_id are opaque to XOS
Scott Baker76934d82015-05-06 19:49:31 -0700287 service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
288 service_specific_attribute = models.TextField(blank=True, null=True)
Scott Baker4587b822015-07-01 18:29:08 -0700289
290 # Connect_method is only used by Coarse tenants
Scott Baker925a8fa2015-04-26 20:30:40 -0700291 connect_method = models.CharField(null=False, blank=False, max_length=30, choices=CONNECTIVITY_CHOICES, default="na")
Scott Baker8103d0f2015-04-10 16:42:26 -0700292
Scott Baker008a9962015-04-15 20:58:20 -0700293 def __init__(self, *args, **kwargs):
294 # for subclasses, set the default kind appropriately
295 self._meta.get_field("kind").default = self.KIND
296 super(Tenant, self).__init__(*args, **kwargs)
297
Scott Baker8103d0f2015-04-10 16:42:26 -0700298 def __unicode__(self):
Scott Bakerf996b762015-05-20 20:42:04 -0700299 return u"%s-tenant-%s" % (str(self.kind), str(self.id))
Scott Baker8103d0f2015-04-10 16:42:26 -0700300
Scott Baker008a9962015-04-15 20:58:20 -0700301 @classmethod
302 def get_tenant_objects(cls):
303 return cls.objects.filter(kind = cls.KIND)
304
Scott Bakere7fc9f52015-05-05 17:52:03 -0700305 @classmethod
Scott Baker16573d32015-07-24 15:36:02 -0700306 def get_tenant_objects_by_user(cls, user):
307 return cls.select_by_user(user).filter(kind = cls.KIND)
308
309 @classmethod
Scott Bakere7fc9f52015-05-05 17:52:03 -0700310 def get_deleted_tenant_objects(cls):
311 return cls.deleted_objects.filter(kind = cls.KIND)
312
Scott Bakerd921e1c2015-04-20 14:24:29 -0700313 # helper function to be used in subclasses that want to ensure service_specific_id is unique
314 def validate_unique_service_specific_id(self):
315 if self.pk is None:
316 if self.service_specific_id is None:
317 raise XOSMissingField("subscriber_specific_id is None, and it's a required field", fields={"service_specific_id": "cannot be none"})
318
319 conflicts = self.get_tenant_objects().filter(service_specific_id=self.service_specific_id)
320 if conflicts:
321 raise XOSDuplicateKey("service_specific_id %s already exists" % self.service_specific_id, fields={"service_specific_id": "duplicate key"})
322
Scott Baker618a4892015-07-06 14:27:31 -0700323 def save(self, *args, **kwargs):
324 subCount = sum( [1 for e in [self.subscriber_service, self.subscriber_tenant, self.subscriber_user, self.subscriber_root] if e is not None])
325 if (subCount > 1):
326 raise XOSConflictingField("Only one of subscriber_service, subscriber_tenant, subscriber_user, subscriber_root should be set")
327
328 super(Tenant, self).save(*args, **kwargs)
329
330 def get_subscribed_tenants(self, tenant_class):
331 ids = self.subscribed_tenants.filter(kind=tenant_class.KIND)
332 return tenant_class.objects.filter(id__in = ids)
333
334 def get_newest_subscribed_tenant(self, kind):
335 st = list(self.get_subscribed_tenants(kind))
336 if not st:
337 return None
338 return sorted(st, key=attrgetter('id'))[0]
339
Scott Baker534dd2a2015-11-18 20:58:08 -0800340class Scheduler(object):
341 # XOS Scheduler Abstract Base Class
342 # Used to implement schedulers that pick which node to put instances on
343
344 def __init__(self, slice):
345 self.slice = slice
346
347 def pick(self):
348 # this method should return a tuple (node, parent)
349 # node is the node to instantiate on
350 # parent is for container_vm instances only, and is the VM that will
351 # hold the container
352
353 raise Exception("Abstract Base")
354
355class LeastLoadedNodeScheduler(Scheduler):
356 # This scheduler always return the node with the fewest number of instances.
357
358 def __init__(self, slice):
359 super(LeastLoadedNodeScheduler, self).__init__(slice)
360
361 def pick(self):
362 from core.models import Node
363 nodes = list(Node.objects.all())
364 # TODO: logic to filter nodes by which nodes are up, and which
365 # nodes the slice can instantiate on.
366 nodes = sorted(nodes, key=lambda node: node.instances.all().count())
367 return [nodes[0], None]
368
369class ContainerVmScheduler(Scheduler):
370 # This scheduler picks a VM in the slice with the fewest containers inside
371 # of it. If no VMs are suitable, then it creates a VM.
372
373 # this is a hack and should be replaced by something smarter...
374 LOOK_FOR_IMAGES=["ubuntu-vcpe4", # ONOS demo machine -- preferred vcpe image
375 "Ubuntu 14.04 LTS", # portal
376 "Ubuntu-14.04-LTS", # ONOS demo machine
377 "trusty-server-multi-nic", # CloudLab
378 ]
379
380 MAX_VM_PER_CONTAINER = 10
381
382 def __init__(self, slice):
383 super(ContainerVmScheduler, self).__init__(slice)
384
385 @property
386 def image(self):
387 from core.models import Image
388
389 look_for_images = self.LOOK_FOR_IMAGES
390 for image_name in look_for_images:
391 images = Image.objects.filter(name = image_name)
392 if images:
393 return images[0]
394
395 raise XOSProgrammingError("No ContainerVM image (looked for %s)" % str(look_for_images))
396
397 def make_new_instance(self):
398 from core.models import Instance, Flavor
399
400 flavors = Flavor.objects.filter(name="m1.small")
401 if not flavors:
402 raise XOSConfigurationError("No m1.small flavor")
403
404 (node,parent) = LeastLoadedNodeScheduler(self.slice).pick()
405
406 instance = Instance(slice = self.slice,
407 node = node,
408 image = self.image,
409 creator = self.slice.creator,
410 deployment = node.site_deployment.deployment,
411 flavor = flavors[0],
412 isolation = "vm",
413 parent = parent)
414 instance.save()
415 # We rely on a special naming convention to identify the VMs that will
416 # hole containers.
417 instance.name = "%s-outer-%s" % (instance.slice.name, instance.id)
418 instance.save()
419 return instance
420
421 def pick(self):
422 from core.models import Instance, Flavor
423
424 for vm in self.slice.instances.filter(isolation="vm"):
425 avail_vms = []
426 if (vm.name.startswith("%s-outer-" % self.slice.name)):
427 container_count = Instance.objects.filter(parent=vm).count()
428 if (container_count < self.MAX_VM_PER_CONTAINER):
429 avail_vms.append( (vm, container_count) )
430 # sort by least containers-per-vm
431 avail_vms = sorted(avail_vms, key = lambda x: x[1])
432 print "XXX", avail_vms
433 if avail_vms:
434 instance = avail_vms[0][0]
435 return (instance.node, instance)
436
437 instance = self.make_new_instance()
438 return (instance.node, instance)
439
Scott Bakere458afd2015-09-09 16:36:06 -0700440class TenantWithContainer(Tenant):
441 """ A tenant that manages a container """
442
443 # this is a hack and should be replaced by something smarter...
Scott Baker7288c522015-09-09 16:43:39 -0700444 LOOK_FOR_IMAGES=["ubuntu-vcpe4", # ONOS demo machine -- preferred vcpe image
Scott Bakere458afd2015-09-09 16:36:06 -0700445 "Ubuntu 14.04 LTS", # portal
446 "Ubuntu-14.04-LTS", # ONOS demo machine
Scott Baker7288c522015-09-09 16:43:39 -0700447 "trusty-server-multi-nic", # CloudLab
Scott Bakere458afd2015-09-09 16:36:06 -0700448 ]
449
Scott Bakercfaf0d32015-11-16 22:51:02 -0800450 LOOK_FOR_CONTAINER_IMAGES=["andybavier/docker-vcpe"]
451
Scott Bakere458afd2015-09-09 16:36:06 -0700452 class Meta:
453 proxy = True
454
455 def __init__(self, *args, **kwargs):
456 super(TenantWithContainer, self).__init__(*args, **kwargs)
Tony Mack6a782f92015-09-13 22:50:39 +0000457 self.cached_instance=None
458 self.orig_instance_id = self.get_initial_attribute("instance_id")
Scott Bakere274f6b2015-11-02 20:54:28 -0800459
Scott Bakere458afd2015-09-09 16:36:06 -0700460 @property
Tony Mack6a782f92015-09-13 22:50:39 +0000461 def instance(self):
462 from core.models import Instance
463 if getattr(self, "cached_instance", None):
464 return self.cached_instance
465 instance_id=self.get_attribute("instance_id")
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600466 if not instance_id:
467 return None
468 instances=Instance.objects.filter(id=instance_id)
469 if not instances:
470 return None
471 instance=instances[0]
Tony Mack6a782f92015-09-13 22:50:39 +0000472 instance.caller = self.creator
473 self.cached_instance = instance
474 return instance
Scott Bakere458afd2015-09-09 16:36:06 -0700475
Tony Mack6a782f92015-09-13 22:50:39 +0000476 @instance.setter
477 def instance(self, value):
Scott Bakere458afd2015-09-09 16:36:06 -0700478 if value:
479 value = value.id
Tony Mack6a782f92015-09-13 22:50:39 +0000480 if (value != self.get_attribute("instance_id", None)):
481 self.cached_instance=None
482 self.set_attribute("instance_id", value)
Scott Bakere458afd2015-09-09 16:36:06 -0700483
Scott Bakere274f6b2015-11-02 20:54:28 -0800484 @property
Scott Bakere458afd2015-09-09 16:36:06 -0700485 def creator(self):
486 from core.models import User
487 if getattr(self, "cached_creator", None):
488 return self.cached_creator
489 creator_id=self.get_attribute("creator_id")
490 if not creator_id:
491 return None
492 users=User.objects.filter(id=creator_id)
493 if not users:
494 return None
495 user=users[0]
496 self.cached_creator = users[0]
497 return user
498
499 @creator.setter
500 def creator(self, value):
501 if value:
502 value = value.id
503 if (value != self.get_attribute("creator_id", None)):
504 self.cached_creator=None
505 self.set_attribute("creator_id", value)
506
507 @property
508 def image(self):
509 from core.models import Image
510 # Implement the logic here to pick the image that should be used when
511 # instantiating the VM that will hold the container.
Scott Bakercfaf0d32015-11-16 22:51:02 -0800512
513 slice = self.provider_service.slices.all()
514 if not slice:
515 raise XOSProgrammingError("provider service has no slice")
516 slice = slice[0]
517
518 if slice.default_isolation in ["container", "container_vm"]:
519 look_for_images = self.LOOK_FOR_CONTAINER_IMAGES
520 else:
521 look_for_images = self.LOOK_FOR_IMAGES
522
523 for image_name in look_for_images:
Scott Bakere458afd2015-09-09 16:36:06 -0700524 images = Image.objects.filter(name = image_name)
525 if images:
526 return images[0]
527
Scott Baker534dd2a2015-11-18 20:58:08 -0800528 raise XOSProgrammingError("No VPCE image (looked for %s)" % str(look_for_images))
Scott Bakere274f6b2015-11-02 20:54:28 -0800529
530 @creator.setter
531 def creator(self, value):
532 if value:
533 value = value.id
534 if (value != self.get_attribute("creator_id", None)):
535 self.cached_creator=None
536 self.set_attribute("creator_id", value)
537
Scott Bakercfaf0d32015-11-16 22:51:02 -0800538 def save_instance(self, instance):
539 # Override this function to do custom pre-save or post-save processing,
540 # such as creating ports for containers.
541 instance.save()
Scott Bakere274f6b2015-11-02 20:54:28 -0800542
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600543 def pick_least_loaded_instance_in_slice(self, slices):
544 for slice in slices:
545 if slice.instances.all().count() > 0:
546 for instance in slice.instances.all():
547 #Pick the first instance that has lesser than 5 tenants
548 if self.count_of_tenants_of_an_instance(instance) < 5:
549 return instance
550 return None
551
552 #TODO: Ideally the tenant count for an instance should be maintained using a
553 #many-to-one relationship attribute, however this model being proxy, it does
554 #not permit any new attributes to be defined. Find if any better solutions
555 def count_of_tenants_of_an_instance(self, instance):
556 tenant_count = 0
557 for tenant in self.get_tenant_objects().all():
558 if tenant.get_attribute("instance_id", None) == instance.id:
559 tenant_count += 1
560 return tenant_count
561
Scott Bakercfaf0d32015-11-16 22:51:02 -0800562 def manage_container(self):
Tony Mack6a782f92015-09-13 22:50:39 +0000563 from core.models import Instance, Flavor
Scott Bakere458afd2015-09-09 16:36:06 -0700564
565 if self.deleted:
566 return
567
Tony Mack6a782f92015-09-13 22:50:39 +0000568 if (self.instance is not None) and (self.instance.image != self.image):
569 self.instance.delete()
570 self.instance = None
Scott Bakere458afd2015-09-09 16:36:06 -0700571
Tony Mack6a782f92015-09-13 22:50:39 +0000572 if self.instance is None:
Scott Bakere458afd2015-09-09 16:36:06 -0700573 if not self.provider_service.slices.count():
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600574 raise XOSConfigurationError("The service has no slices")
Scott Bakere458afd2015-09-09 16:36:06 -0700575
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600576 new_instance_created = False
577 instance = None
578 if self.get_attribute("use_same_instance_for_multiple_tenants", default=False):
579 #Find if any existing instances can be used for this tenant
580 slices = self.provider_service.slices.all()
581 instance = self.pick_least_loaded_instance_in_slice(slices)
Scott Bakere458afd2015-09-09 16:36:06 -0700582
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600583 if not instance:
584 flavors = Flavor.objects.filter(name="m1.small")
585 if not flavors:
586 raise XOSConfigurationError("No m1.small flavor")
587
Srikanth Vavilapalli71120a92015-11-17 13:41:38 -0600588 slice = self.provider_service.slices.all()[0]
Scott Bakercfaf0d32015-11-16 22:51:02 -0800589
Srikanth Vavilapalli71120a92015-11-17 13:41:38 -0600590 if slice.default_isolation == "container_vm":
Scott Baker534dd2a2015-11-18 20:58:08 -0800591 (node, parent) = ContainerVmScheduler(slice).pick()
Srikanth Vavilapalli71120a92015-11-17 13:41:38 -0600592 else:
Scott Baker534dd2a2015-11-18 20:58:08 -0800593 (node, parent) = LeastLoadedNodeScheduler(slice).pick()
Scott Bakercfaf0d32015-11-16 22:51:02 -0800594
Srikanth Vavilapalli71120a92015-11-17 13:41:38 -0600595 instance = Instance(slice = slice,
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600596 node = node,
597 image = self.image,
598 creator = self.creator,
599 deployment = node.site_deployment.deployment,
Srikanth Vavilapalli71120a92015-11-17 13:41:38 -0600600 flavor = flavors[0],
601 isolation = slice.default_isolation,
602 parent = parent)
603 self.save_instance(instance)
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600604 new_instance_created = True
Scott Bakere458afd2015-09-09 16:36:06 -0700605
606 try:
Tony Mack6a782f92015-09-13 22:50:39 +0000607 self.instance = instance
Scott Bakere458afd2015-09-09 16:36:06 -0700608 super(TenantWithContainer, self).save()
609 except:
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600610 if new_instance_created:
611 instance.delete()
Scott Bakere458afd2015-09-09 16:36:06 -0700612 raise
613
614 def cleanup_container(self):
Tony Mack6a782f92015-09-13 22:50:39 +0000615 if self.instance:
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600616 if self.get_attribute("use_same_instance_for_multiple_tenants", default=False):
617 #Delete the instance only if this is last tenant in that instance
618 tenant_count = self.count_of_tenants_of_an_instance(self.instance)
619 if tenant_count == 0:
620 self.instance.delete()
621 else:
622 self.instance.delete()
Tony Mack6a782f92015-09-13 22:50:39 +0000623 self.instance = None
Scott Baker618a4892015-07-06 14:27:31 -0700624
Scott Baker925a8fa2015-04-26 20:30:40 -0700625class CoarseTenant(Tenant):
Scott Baker4587b822015-07-01 18:29:08 -0700626 """ TODO: rename "CoarseTenant" --> "StaticTenant" """
Scott Baker925a8fa2015-04-26 20:30:40 -0700627 class Meta:
628 proxy = True
Siobhan Tully00353f72013-10-08 21:53:27 -0400629
Scott Baker2461bec2015-08-14 09:10:11 -0700630 KIND = COARSE_KIND
Scott Baker925a8fa2015-04-26 20:30:40 -0700631
632 def save(self, *args, **kwargs):
633 if (not self.subscriber_service):
634 raise XOSValidationError("subscriber_service cannot be null")
635 if (self.subscriber_tenant or self.subscriber_user):
636 raise XOSValidationError("subscriber_tenant and subscriber_user must be null")
637
638 super(CoarseTenant,self).save()
Scott Baker4587b822015-07-01 18:29:08 -0700639
640class Subscriber(TenantRoot):
641 """ Intermediate class for TenantRoots that are to be Subscribers """
642
643 class Meta:
644 proxy = True
645
646 KIND = "Subscriber"
647
648class Provider(TenantRoot):
649 """ Intermediate class for TenantRoots that are to be Providers """
650
651 class Meta:
652 proxy = True
653
654 KIND = "Provider"
655
Scott Baker462a1d92015-10-15 15:59:19 -0700656class TenantAttribute(PlCoreBase):
Scott Baker907f9912015-10-20 17:12:36 -0700657 name = models.CharField(help_text="Attribute Name", max_length=128)
Scott Baker462a1d92015-10-15 15:59:19 -0700658 value = models.TextField(help_text="Attribute Value")
659 tenant = models.ForeignKey(Tenant, related_name='tenantattributes', help_text="The Tenant this attribute is associated with")
660
Scott Baker4587b822015-07-01 18:29:08 -0700661class TenantRootRole(PlCoreBase):
Scott Baker5116b302015-07-24 15:48:03 -0700662 ROLE_CHOICES = (('admin','Admin'), ('access','Access'))
Scott Baker4587b822015-07-01 18:29:08 -0700663
664 role = StrippedCharField(choices=ROLE_CHOICES, unique=True, max_length=30)
665
666 def __unicode__(self): return u'%s' % (self.role)
667
668class TenantRootPrivilege(PlCoreBase):
669 user = models.ForeignKey('User', related_name="tenant_root_privileges")
670 tenant_root = models.ForeignKey('TenantRoot', related_name="tenant_root_privileges")
671 role = models.ForeignKey('TenantRootRole', related_name="tenant_root_privileges")
672
673 class Meta:
674 unique_together = ('user', 'tenant_root', 'role')
675
Scott Baker618a4892015-07-06 14:27:31 -0700676 def __unicode__(self): return u'%s %s %s' % (self.tenant_root, self.user, self.role)
Scott Baker4587b822015-07-01 18:29:08 -0700677
678 def save(self, *args, **kwds):
679 if not self.user.is_active:
680 raise PermissionDenied, "Cannot modify role(s) of a disabled user"
Scott Baker335882a2015-07-24 10:15:31 -0700681 super(TenantRootPrivilege, self).save(*args, **kwds)
Scott Baker4587b822015-07-01 18:29:08 -0700682
683 def can_update(self, user):
Scott Baker335882a2015-07-24 10:15:31 -0700684 return user.can_update_tenant_root_privilege(self)
Scott Baker4587b822015-07-01 18:29:08 -0700685
Scott Baker16573d32015-07-24 15:36:02 -0700686 @classmethod
687 def select_by_user(cls, user):
Scott Baker4587b822015-07-01 18:29:08 -0700688 if user.is_admin:
Scott Baker5116b302015-07-24 15:48:03 -0700689 return cls.objects.all()
Scott Baker4587b822015-07-01 18:29:08 -0700690 else:
Scott Baker5116b302015-07-24 15:48:03 -0700691 # User can see his own privilege
692 trp_ids = [trp.id for trp in cls.objects.filter(user=user)]
693
694 # A slice admin can see the SlicePrivileges for his Slice
695 for priv in cls.objects.filter(user=user, role__role="admin"):
696 trp_ids.extend( [trp.id for trp in cls.objects.filter(tenant_root=priv.tenant_root)] )
697
698 return cls.objects.filter(id__in=trp_ids)
699
Scott Baker618a4892015-07-06 14:27:31 -0700700