blob: 5d8fb3ddbbc072eb4d7e07586c5220ac4c182448 [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
Scott Bakercce158d2015-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 Mack9d2ea092015-04-29 12:23:10 -040095 def can_update(self, user):
96 return user.can_update_service(self, allow=['admin'])
Scott Baker618a4892015-07-06 14:27:31 -070097
Scott Baker98436732015-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 Mack3de59e32015-08-19 11:58:18 -0400103 max_per_node - maximum numbers of instances that 'slice' can have on a single node
Scott Baker98436732015-05-11 16:36:41 -0700104 exclusive_slices - list of slices that must have no nodes in common with 'slice'.
105 """
106
Tony Mack3de59e32015-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 Baker98436732015-05-11 16:36:41 -0700108
109 nodes = list(Node.objects.all())
110
Tony Mack3de59e32015-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 Baker98436732015-05-11 16:36:41 -0700113
114 nodes = [x for x in nodes if x not in conflicting_nodes]
115
Tony Mack3de59e32015-08-19 11:58:18 -0400116 # If max_per_node is set, then limit the number of instances this slice
Scott Baker98436732015-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 Mack3de59e32015-08-19 11:58:18 -0400121 existing_count = node.instances.filter(slice=slice).count()
Scott Baker98436732015-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 Mack3de59e32015-08-19 11:58:18 -0400132 nodes = sorted(nodes, key=lambda node: node.instances.all().count())
Scott Baker98436732015-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 Mack3de59e32015-08-19 11:58:18 -0400138 from core.models import Instance # late import to get around order-of-imports constraint in __init__.py
Scott Baker98436732015-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 Mack3de59e32015-08-19 11:58:18 -0400142 while slice.instances.all().count() > scale:
143 s = slice.instances.all()[0]
144 # print "drop instance", s
Scott Baker98436732015-05-11 16:36:41 -0700145 s.delete()
146
Tony Mack3de59e32015-08-19 11:58:18 -0400147 while slice.instances.all().count() < scale:
Scott Baker98436732015-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 Mack3de59e32015-08-19 11:58:18 -0400161 s = Instance(slice=slice,
Scott Baker98436732015-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 Mack3de59e32015-08-19 11:58:18 -0400169 # print "add instance", s
Tony Mack9d2ea092015-04-29 12:23:10 -0400170
Scott Baker8a67ae72015-12-09 22:54:52 -0800171 def get_vtn_src_nets(self):
172 nets=[]
173 for slice in self.slices.all():
174 for ns in slice.networkslices.all():
175 if not ns.network:
176 continue
Scott Bakerf63cf1c2016-01-12 19:59:12 -0800177# if ns.network.template.access in ["direct", "indirect"]:
178# # skip access networks; we want to use the private network
179# continue
Scott Baker8a67ae72015-12-09 22:54:52 -0800180 if ns.network.name in ["wan_network", "lan_network"]:
181 # we don't want to attach to the vCPE's lan or wan network
182 # we only want to attach to its private network
183 # TODO: fix hard-coding of network name
184 continue
185 for cn in ns.network.controllernetworks.all():
186 if cn.net_id:
187 net = {"name": ns.network.name, "net_id": cn.net_id}
188 nets.append(net)
189 return nets
190
Scott Bakercbd718e2015-12-08 21:31:18 -0800191 def get_vtn_nets(self):
192 nets=[]
193 for slice in self.slices.all():
194 for ns in slice.networkslices.all():
195 if not ns.network:
196 continue
Scott Baker8a67ae72015-12-09 22:54:52 -0800197 if ns.network.template.access not in ["direct", "indirect"]:
198 # skip anything that's not an access network
199 continue
Scott Bakercbd718e2015-12-08 21:31:18 -0800200 for cn in ns.network.controllernetworks.all():
201 if cn.net_id:
202 net = {"name": ns.network.name, "net_id": cn.net_id}
203 nets.append(net)
204 return nets
205
206 def get_vtn_dependencies_nets(self):
207 provider_nets = []
Scott Baker5380a442015-12-08 19:27:50 -0800208 for tenant in self.subscribed_tenants.all():
209 if tenant.provider_service:
Scott Bakercbd718e2015-12-08 21:31:18 -0800210 for net in tenant.provider_service.get_vtn_nets():
211 if not net in provider_nets:
212 provider_nets.append(net)
213 return provider_nets
214
215 def get_vtn_dependencies_ids(self):
216 return [x["net_id"] for x in self.get_vtn_dependencies_nets()]
217
218 def get_vtn_dependencies_names(self):
219 return [x["name"]+"_"+x["net_id"] for x in self.get_vtn_dependencies_nets()]
220
Scott Baker8a67ae72015-12-09 22:54:52 -0800221 def get_vtn_src_ids(self):
222 return [x["net_id"] for x in self.get_vtn_src_nets()]
Scott Bakercbd718e2015-12-08 21:31:18 -0800223
Scott Baker8a67ae72015-12-09 22:54:52 -0800224 def get_vtn_src_names(self):
225 return [x["name"]+"_"+x["net_id"] for x in self.get_vtn_src_nets()]
Scott Baker5380a442015-12-08 19:27:50 -0800226
227
Siobhan Tully00353f72013-10-08 21:53:27 -0400228class ServiceAttribute(PlCoreBase):
Scott Bakercce158d2015-12-07 22:20:40 -0800229 name = models.CharField(help_text="Attribute Name", max_length=128)
Tony Mackd84b1ff2015-03-09 13:03:56 -0400230 value = StrippedCharField(help_text="Attribute Value", max_length=1024)
Siobhan Tully00353f72013-10-08 21:53:27 -0400231 service = models.ForeignKey(Service, related_name='serviceattributes', help_text="The Service this attribute is associated with")
232
Tony Mack9d2ea092015-04-29 12:23:10 -0400233class ServiceRole(PlCoreBase):
234 ROLE_CHOICES = (('admin','Admin'),)
235 role = StrippedCharField(choices=ROLE_CHOICES, unique=True, max_length=30)
236
237 def __unicode__(self): return u'%s' % (self.role)
238
239class ServicePrivilege(PlCoreBase):
240 user = models.ForeignKey('User', related_name='serviceprivileges')
241 service = models.ForeignKey('Service', related_name='serviceprivileges')
242 role = models.ForeignKey('ServiceRole',related_name='serviceprivileges')
243
244 class Meta:
Tony Mack9ec754e2015-05-13 12:21:28 -0400245 unique_together = ('user', 'service', 'role')
Tony Mack9d2ea092015-04-29 12:23:10 -0400246
247 def __unicode__(self): return u'%s %s %s' % (self.service, self.user, self.role)
248
249 def can_update(self, user):
250 if not self.service.enabled:
251 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
252 return self.service.can_update(user)
253
254 def save(self, *args, **kwds):
255 if not self.service.enabled:
256 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
257 super(ServicePrivilege, self).save(*args, **kwds)
258
259 def delete(self, *args, **kwds):
260 if not self.service.enabled:
261 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
Scott Baker4587b822015-07-01 18:29:08 -0700262 super(ServicePrivilege, self).delete(*args, **kwds)
263
Scott Baker16573d32015-07-24 15:36:02 -0700264 @classmethod
265 def select_by_user(cls, user):
Tony Mack9d2ea092015-04-29 12:23:10 -0400266 if user.is_admin:
Scott Baker16573d32015-07-24 15:36:02 -0700267 qs = cls.objects.all()
Tony Mack9d2ea092015-04-29 12:23:10 -0400268 else:
Scott Baker16573d32015-07-24 15:36:02 -0700269 qs = cls.objects.filter(user=user)
Scott Baker4587b822015-07-01 18:29:08 -0700270 return qs
271
Scott Baker82498c52015-07-13 13:07:27 -0700272class TenantRoot(PlCoreBase, AttributeMixin):
Scott Baker4587b822015-07-01 18:29:08 -0700273 """ A tenantRoot is one of the things that can sit at the root of a chain
274 of tenancy. This object represents a node.
275 """
276
277 KIND= "generic"
278 kind = StrippedCharField(max_length=30, default=KIND)
Scott Baker618a4892015-07-06 14:27:31 -0700279 name = StrippedCharField(max_length=255, help_text="name", blank=True, null=True)
Scott Baker4587b822015-07-01 18:29:08 -0700280
Scott Bakerefcec632015-07-07 12:12:42 -0700281 service_specific_attribute = models.TextField(blank=True, null=True)
282 service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
Scott Baker4587b822015-07-01 18:29:08 -0700283
Scott Bakerdb66fd32015-07-07 17:59:44 -0700284 def __init__(self, *args, **kwargs):
285 # for subclasses, set the default kind appropriately
286 self._meta.get_field("kind").default = self.KIND
287 super(TenantRoot, self).__init__(*args, **kwargs)
288
Scott Baker618a4892015-07-06 14:27:31 -0700289 def __unicode__(self):
290 if not self.name:
291 return u"%s-tenant_root-#%s" % (str(self.kind), str(self.id))
292 else:
293 return self.name
294
295 def can_update(self, user):
296 return user.can_update_tenant_root(self, allow=['admin'])
297
Scott Bakerefcec632015-07-07 12:12:42 -0700298 def get_subscribed_tenants(self, tenant_class):
299 ids = self.subscribed_tenants.filter(kind=tenant_class.KIND)
300 return tenant_class.objects.filter(id__in = ids)
301
302 def get_newest_subscribed_tenant(self, kind):
303 st = list(self.get_subscribed_tenants(kind))
304 if not st:
305 return None
306 return sorted(st, key=attrgetter('id'))[0]
307
308 @classmethod
309 def get_tenant_objects(cls):
310 return cls.objects.filter(kind = cls.KIND)
311
Scott Baker16573d32015-07-24 15:36:02 -0700312 @classmethod
313 def get_tenant_objects_by_user(cls, user):
314 return cls.select_by_user(user).filter(kind = cls.KIND)
315
316 @classmethod
317 def select_by_user(cls, user):
318 if user.is_admin:
319 return cls.objects.all()
320 else:
321 tr_ids = [trp.tenant_root.id for trp in TenantRootPrivilege.objects.filter(user=user)]
322 return cls.objects.filter(id__in=tr_ids)
323
Scott Baker82498c52015-07-13 13:07:27 -0700324class Tenant(PlCoreBase, AttributeMixin):
Scott Baker8103d0f2015-04-10 16:42:26 -0700325 """ A tenant is a relationship between two entities, a subscriber and a
Scott Baker4587b822015-07-01 18:29:08 -0700326 provider. This object represents an edge.
Scott Baker8103d0f2015-04-10 16:42:26 -0700327
328 The subscriber can be a User, a Service, or a Tenant.
329
330 The provider is always a Service.
Scott Baker4587b822015-07-01 18:29:08 -0700331
332 TODO: rename "Tenant" to "Tenancy"
Scott Baker8103d0f2015-04-10 16:42:26 -0700333 """
Scott Baker008a9962015-04-15 20:58:20 -0700334
Scott Baker925a8fa2015-04-26 20:30:40 -0700335 CONNECTIVITY_CHOICES = (('public', 'Public'), ('private', 'Private'), ('na', 'Not Applicable'))
336
Scott Baker008a9962015-04-15 20:58:20 -0700337 # when subclassing a service, redefine KIND to describe the new service
338 KIND = "generic"
339
340 kind = StrippedCharField(max_length=30, default=KIND)
Scott Baker4587b822015-07-01 18:29:08 -0700341 provider_service = models.ForeignKey(Service, related_name='provided_tenants')
342
343 # The next four things are the various type of objects that can be subscribers of this Tenancy
344 # relationship. One and only one can be used at a time.
345 subscriber_service = models.ForeignKey(Service, related_name='subscribed_tenants', blank=True, null=True)
346 subscriber_tenant = models.ForeignKey("Tenant", related_name='subscribed_tenants', blank=True, null=True)
347 subscriber_user = models.ForeignKey("User", related_name='subscribed_tenants', blank=True, null=True)
348 subscriber_root = models.ForeignKey("TenantRoot", related_name="subscribed_tenants", blank=True, null=True)
349
350 # Service_specific_attribute and service_specific_id are opaque to XOS
Scott Baker76934d82015-05-06 19:49:31 -0700351 service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
352 service_specific_attribute = models.TextField(blank=True, null=True)
Scott Baker4587b822015-07-01 18:29:08 -0700353
354 # Connect_method is only used by Coarse tenants
Scott Baker925a8fa2015-04-26 20:30:40 -0700355 connect_method = models.CharField(null=False, blank=False, max_length=30, choices=CONNECTIVITY_CHOICES, default="na")
Scott Baker8103d0f2015-04-10 16:42:26 -0700356
Scott Baker008a9962015-04-15 20:58:20 -0700357 def __init__(self, *args, **kwargs):
358 # for subclasses, set the default kind appropriately
359 self._meta.get_field("kind").default = self.KIND
360 super(Tenant, self).__init__(*args, **kwargs)
361
Scott Baker8103d0f2015-04-10 16:42:26 -0700362 def __unicode__(self):
Scott Bakerf996b762015-05-20 20:42:04 -0700363 return u"%s-tenant-%s" % (str(self.kind), str(self.id))
Scott Baker8103d0f2015-04-10 16:42:26 -0700364
Scott Baker008a9962015-04-15 20:58:20 -0700365 @classmethod
366 def get_tenant_objects(cls):
367 return cls.objects.filter(kind = cls.KIND)
368
Scott Bakere7fc9f52015-05-05 17:52:03 -0700369 @classmethod
Scott Baker16573d32015-07-24 15:36:02 -0700370 def get_tenant_objects_by_user(cls, user):
371 return cls.select_by_user(user).filter(kind = cls.KIND)
372
373 @classmethod
Scott Bakere7fc9f52015-05-05 17:52:03 -0700374 def get_deleted_tenant_objects(cls):
375 return cls.deleted_objects.filter(kind = cls.KIND)
376
Scott Bakercce158d2015-12-07 22:20:40 -0800377 @property
378 def tenantattribute_dict(self):
379 attrs = {}
380 for attr in self.tenantattributes.all():
381 attrs[attr.name] = attr.value
382 return attrs
383
Scott Bakerd921e1c2015-04-20 14:24:29 -0700384 # helper function to be used in subclasses that want to ensure service_specific_id is unique
385 def validate_unique_service_specific_id(self):
386 if self.pk is None:
387 if self.service_specific_id is None:
388 raise XOSMissingField("subscriber_specific_id is None, and it's a required field", fields={"service_specific_id": "cannot be none"})
389
390 conflicts = self.get_tenant_objects().filter(service_specific_id=self.service_specific_id)
391 if conflicts:
392 raise XOSDuplicateKey("service_specific_id %s already exists" % self.service_specific_id, fields={"service_specific_id": "duplicate key"})
393
Scott Baker618a4892015-07-06 14:27:31 -0700394 def save(self, *args, **kwargs):
395 subCount = sum( [1 for e in [self.subscriber_service, self.subscriber_tenant, self.subscriber_user, self.subscriber_root] if e is not None])
396 if (subCount > 1):
397 raise XOSConflictingField("Only one of subscriber_service, subscriber_tenant, subscriber_user, subscriber_root should be set")
398
399 super(Tenant, self).save(*args, **kwargs)
400
401 def get_subscribed_tenants(self, tenant_class):
402 ids = self.subscribed_tenants.filter(kind=tenant_class.KIND)
403 return tenant_class.objects.filter(id__in = ids)
404
405 def get_newest_subscribed_tenant(self, kind):
406 st = list(self.get_subscribed_tenants(kind))
407 if not st:
408 return None
409 return sorted(st, key=attrgetter('id'))[0]
410
Scott Baker534dd2a2015-11-18 20:58:08 -0800411class Scheduler(object):
412 # XOS Scheduler Abstract Base Class
413 # Used to implement schedulers that pick which node to put instances on
414
415 def __init__(self, slice):
416 self.slice = slice
417
418 def pick(self):
419 # this method should return a tuple (node, parent)
420 # node is the node to instantiate on
421 # parent is for container_vm instances only, and is the VM that will
422 # hold the container
423
424 raise Exception("Abstract Base")
425
426class LeastLoadedNodeScheduler(Scheduler):
427 # This scheduler always return the node with the fewest number of instances.
428
429 def __init__(self, slice):
430 super(LeastLoadedNodeScheduler, self).__init__(slice)
431
432 def pick(self):
433 from core.models import Node
Pingping Linfa30bae2016-03-03 09:52:24 -0800434# nodes = list(Node.objects.all())
435 if not self.slice.default_node:
436 nodes = list(Node.objects.all())
437 nodes = sorted(nodes, key=lambda node: node.instances.all().count())
438 else:
439 nodes = list(Node.objects.filter(name = self.slice.default_node))
Scott Baker534dd2a2015-11-18 20:58:08 -0800440 # TODO: logic to filter nodes by which nodes are up, and which
441 # nodes the slice can instantiate on.
Pingping Linfa30bae2016-03-03 09:52:24 -0800442# nodes = sorted(nodes, key=lambda node: node.instances.all().count())
Scott Baker534dd2a2015-11-18 20:58:08 -0800443 return [nodes[0], None]
444
445class ContainerVmScheduler(Scheduler):
446 # This scheduler picks a VM in the slice with the fewest containers inside
447 # of it. If no VMs are suitable, then it creates a VM.
448
449 # this is a hack and should be replaced by something smarter...
450 LOOK_FOR_IMAGES=["ubuntu-vcpe4", # ONOS demo machine -- preferred vcpe image
451 "Ubuntu 14.04 LTS", # portal
452 "Ubuntu-14.04-LTS", # ONOS demo machine
453 "trusty-server-multi-nic", # CloudLab
454 ]
455
456 MAX_VM_PER_CONTAINER = 10
457
458 def __init__(self, slice):
459 super(ContainerVmScheduler, self).__init__(slice)
460
461 @property
462 def image(self):
463 from core.models import Image
464
465 look_for_images = self.LOOK_FOR_IMAGES
466 for image_name in look_for_images:
467 images = Image.objects.filter(name = image_name)
468 if images:
469 return images[0]
470
471 raise XOSProgrammingError("No ContainerVM image (looked for %s)" % str(look_for_images))
472
473 def make_new_instance(self):
474 from core.models import Instance, Flavor
475
476 flavors = Flavor.objects.filter(name="m1.small")
477 if not flavors:
478 raise XOSConfigurationError("No m1.small flavor")
479
480 (node,parent) = LeastLoadedNodeScheduler(self.slice).pick()
481
482 instance = Instance(slice = self.slice,
483 node = node,
484 image = self.image,
485 creator = self.slice.creator,
486 deployment = node.site_deployment.deployment,
487 flavor = flavors[0],
488 isolation = "vm",
489 parent = parent)
490 instance.save()
491 # We rely on a special naming convention to identify the VMs that will
492 # hole containers.
493 instance.name = "%s-outer-%s" % (instance.slice.name, instance.id)
494 instance.save()
495 return instance
496
497 def pick(self):
498 from core.models import Instance, Flavor
499
500 for vm in self.slice.instances.filter(isolation="vm"):
501 avail_vms = []
502 if (vm.name.startswith("%s-outer-" % self.slice.name)):
503 container_count = Instance.objects.filter(parent=vm).count()
504 if (container_count < self.MAX_VM_PER_CONTAINER):
505 avail_vms.append( (vm, container_count) )
506 # sort by least containers-per-vm
507 avail_vms = sorted(avail_vms, key = lambda x: x[1])
508 print "XXX", avail_vms
509 if avail_vms:
510 instance = avail_vms[0][0]
511 return (instance.node, instance)
512
513 instance = self.make_new_instance()
514 return (instance.node, instance)
515
Scott Bakere458afd2015-09-09 16:36:06 -0700516class TenantWithContainer(Tenant):
517 """ A tenant that manages a container """
518
519 # this is a hack and should be replaced by something smarter...
Scott Baker7288c522015-09-09 16:43:39 -0700520 LOOK_FOR_IMAGES=["ubuntu-vcpe4", # ONOS demo machine -- preferred vcpe image
Scott Bakere458afd2015-09-09 16:36:06 -0700521 "Ubuntu 14.04 LTS", # portal
522 "Ubuntu-14.04-LTS", # ONOS demo machine
Scott Baker7288c522015-09-09 16:43:39 -0700523 "trusty-server-multi-nic", # CloudLab
Scott Bakere458afd2015-09-09 16:36:06 -0700524 ]
525
Scott Baker0f908e02015-12-14 10:21:53 -0800526 LOOK_FOR_CONTAINER_IMAGES=["docker-vcpe"]
Scott Bakercfaf0d32015-11-16 22:51:02 -0800527
Scott Bakere458afd2015-09-09 16:36:06 -0700528 class Meta:
529 proxy = True
530
531 def __init__(self, *args, **kwargs):
532 super(TenantWithContainer, self).__init__(*args, **kwargs)
Tony Mack6a782f92015-09-13 22:50:39 +0000533 self.cached_instance=None
534 self.orig_instance_id = self.get_initial_attribute("instance_id")
Scott Bakere274f6b2015-11-02 20:54:28 -0800535
Scott Bakere458afd2015-09-09 16:36:06 -0700536 @property
Tony Mack6a782f92015-09-13 22:50:39 +0000537 def instance(self):
538 from core.models import Instance
539 if getattr(self, "cached_instance", None):
540 return self.cached_instance
541 instance_id=self.get_attribute("instance_id")
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600542 if not instance_id:
543 return None
544 instances=Instance.objects.filter(id=instance_id)
545 if not instances:
546 return None
547 instance=instances[0]
Tony Mack6a782f92015-09-13 22:50:39 +0000548 instance.caller = self.creator
549 self.cached_instance = instance
550 return instance
Scott Bakere458afd2015-09-09 16:36:06 -0700551
Tony Mack6a782f92015-09-13 22:50:39 +0000552 @instance.setter
553 def instance(self, value):
Scott Bakere458afd2015-09-09 16:36:06 -0700554 if value:
555 value = value.id
Tony Mack6a782f92015-09-13 22:50:39 +0000556 if (value != self.get_attribute("instance_id", None)):
557 self.cached_instance=None
558 self.set_attribute("instance_id", value)
Scott Bakere458afd2015-09-09 16:36:06 -0700559
Scott Bakere274f6b2015-11-02 20:54:28 -0800560 @property
Scott Bakerbdda0be2016-02-10 12:23:53 -0800561 def external_hostname(self):
562 return self.get_attribute("external_hostname", "")
563
564 @external_hostname.setter
565 def external_hostname(self, value):
566 self.set_attribute("external_hostname", value)
567
568 @property
569 def external_container(self):
570 return self.get_attribute("external_container", "")
571
572 @external_container.setter
573 def external_container(self, value):
574 self.set_attribute("external_container", value)
575
576 @property
Scott Bakere458afd2015-09-09 16:36:06 -0700577 def creator(self):
578 from core.models import User
579 if getattr(self, "cached_creator", None):
580 return self.cached_creator
581 creator_id=self.get_attribute("creator_id")
582 if not creator_id:
583 return None
584 users=User.objects.filter(id=creator_id)
585 if not users:
586 return None
587 user=users[0]
588 self.cached_creator = users[0]
589 return user
590
591 @creator.setter
592 def creator(self, value):
593 if value:
594 value = value.id
595 if (value != self.get_attribute("creator_id", None)):
596 self.cached_creator=None
597 self.set_attribute("creator_id", value)
598
599 @property
600 def image(self):
601 from core.models import Image
602 # Implement the logic here to pick the image that should be used when
603 # instantiating the VM that will hold the container.
Scott Bakercfaf0d32015-11-16 22:51:02 -0800604 slice = self.provider_service.slices.all()
605 if not slice:
606 raise XOSProgrammingError("provider service has no slice")
607 slice = slice[0]
608
609 if slice.default_isolation in ["container", "container_vm"]:
610 look_for_images = self.LOOK_FOR_CONTAINER_IMAGES
611 else:
612 look_for_images = self.LOOK_FOR_IMAGES
613
614 for image_name in look_for_images:
Pingping Linfa30bae2016-03-03 09:52:24 -0800615 images = Image.objects.filter(name = slice.default_image)
Scott Bakere458afd2015-09-09 16:36:06 -0700616 if images:
617 return images[0]
618
Scott Baker534dd2a2015-11-18 20:58:08 -0800619 raise XOSProgrammingError("No VPCE image (looked for %s)" % str(look_for_images))
Scott Bakere274f6b2015-11-02 20:54:28 -0800620
Scott Bakercfaf0d32015-11-16 22:51:02 -0800621 def save_instance(self, instance):
622 # Override this function to do custom pre-save or post-save processing,
623 # such as creating ports for containers.
624 instance.save()
Scott Bakere274f6b2015-11-02 20:54:28 -0800625
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600626 def pick_least_loaded_instance_in_slice(self, slices):
627 for slice in slices:
628 if slice.instances.all().count() > 0:
629 for instance in slice.instances.all():
630 #Pick the first instance that has lesser than 5 tenants
631 if self.count_of_tenants_of_an_instance(instance) < 5:
632 return instance
633 return None
634
635 #TODO: Ideally the tenant count for an instance should be maintained using a
636 #many-to-one relationship attribute, however this model being proxy, it does
637 #not permit any new attributes to be defined. Find if any better solutions
638 def count_of_tenants_of_an_instance(self, instance):
639 tenant_count = 0
640 for tenant in self.get_tenant_objects().all():
641 if tenant.get_attribute("instance_id", None) == instance.id:
642 tenant_count += 1
643 return tenant_count
644
Scott Bakercfaf0d32015-11-16 22:51:02 -0800645 def manage_container(self):
Tony Mack6a782f92015-09-13 22:50:39 +0000646 from core.models import Instance, Flavor
Scott Bakere458afd2015-09-09 16:36:06 -0700647
648 if self.deleted:
649 return
650
Tony Mack6a782f92015-09-13 22:50:39 +0000651 if (self.instance is not None) and (self.instance.image != self.image):
652 self.instance.delete()
653 self.instance = None
Scott Bakere458afd2015-09-09 16:36:06 -0700654
Tony Mack6a782f92015-09-13 22:50:39 +0000655 if self.instance is None:
Scott Bakere458afd2015-09-09 16:36:06 -0700656 if not self.provider_service.slices.count():
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600657 raise XOSConfigurationError("The service has no slices")
Scott Bakere458afd2015-09-09 16:36:06 -0700658
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600659 new_instance_created = False
660 instance = None
661 if self.get_attribute("use_same_instance_for_multiple_tenants", default=False):
662 #Find if any existing instances can be used for this tenant
663 slices = self.provider_service.slices.all()
664 instance = self.pick_least_loaded_instance_in_slice(slices)
Scott Bakere458afd2015-09-09 16:36:06 -0700665
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600666 if not instance:
Srikanth Vavilapalli71120a92015-11-17 13:41:38 -0600667 slice = self.provider_service.slices.all()[0]
Pingping Linfa30bae2016-03-03 09:52:24 -0800668 flavors = Flavor.objects.filter(name=slice.default_flavor)
669# flavors = Flavor.objects.filter(name="m1.small")
670 if not flavors:
671 raise XOSConfigurationError("No slice default flavor")
672# raise XOSConfigurationError("No m1.small flavor")
Scott Bakercfaf0d32015-11-16 22:51:02 -0800673
Pingping Linfa30bae2016-03-03 09:52:24 -0800674# slice = self.provider_service.slices.all()[0]
675 default_flavor = slice.default_flavor
Srikanth Vavilapalli71120a92015-11-17 13:41:38 -0600676 if slice.default_isolation == "container_vm":
Scott Baker534dd2a2015-11-18 20:58:08 -0800677 (node, parent) = ContainerVmScheduler(slice).pick()
Srikanth Vavilapalli71120a92015-11-17 13:41:38 -0600678 else:
Scott Baker534dd2a2015-11-18 20:58:08 -0800679 (node, parent) = LeastLoadedNodeScheduler(slice).pick()
Pingping Linfa30bae2016-03-03 09:52:24 -0800680# print "DEF_NODE", slice.default_node
681# self.image = slice.default_image
Srikanth Vavilapalli71120a92015-11-17 13:41:38 -0600682 instance = Instance(slice = slice,
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600683 node = node,
Pingping Linfa30bae2016-03-03 09:52:24 -0800684# image = slice.default_image,
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600685 image = self.image,
686 creator = self.creator,
687 deployment = node.site_deployment.deployment,
Srikanth Vavilapalli71120a92015-11-17 13:41:38 -0600688 flavor = flavors[0],
Pingping Linfa30bae2016-03-03 09:52:24 -0800689# flavor = slice.default_flavor,
Srikanth Vavilapalli71120a92015-11-17 13:41:38 -0600690 isolation = slice.default_isolation,
691 parent = parent)
Pingping Linfa30bae2016-03-03 09:52:24 -0800692 print "DEF_NODE", instance.node
Srikanth Vavilapalli71120a92015-11-17 13:41:38 -0600693 self.save_instance(instance)
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600694 new_instance_created = True
Scott Bakere458afd2015-09-09 16:36:06 -0700695
696 try:
Tony Mack6a782f92015-09-13 22:50:39 +0000697 self.instance = instance
Scott Bakere458afd2015-09-09 16:36:06 -0700698 super(TenantWithContainer, self).save()
699 except:
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600700 if new_instance_created:
701 instance.delete()
Scott Bakere458afd2015-09-09 16:36:06 -0700702 raise
703
704 def cleanup_container(self):
Tony Mack6a782f92015-09-13 22:50:39 +0000705 if self.instance:
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600706 if self.get_attribute("use_same_instance_for_multiple_tenants", default=False):
707 #Delete the instance only if this is last tenant in that instance
708 tenant_count = self.count_of_tenants_of_an_instance(self.instance)
709 if tenant_count == 0:
710 self.instance.delete()
711 else:
712 self.instance.delete()
Tony Mack6a782f92015-09-13 22:50:39 +0000713 self.instance = None
Scott Baker618a4892015-07-06 14:27:31 -0700714
Scott Bakeree83f2a2015-12-10 23:23:07 -0800715 def save(self, *args, **kwargs):
716 if (not self.creator) and (hasattr(self, "caller")) and (self.caller):
717 self.creator = self.caller
718 super(TenantWithContainer, self).save(*args, **kwargs)
719
Scott Baker925a8fa2015-04-26 20:30:40 -0700720class CoarseTenant(Tenant):
Scott Baker4587b822015-07-01 18:29:08 -0700721 """ TODO: rename "CoarseTenant" --> "StaticTenant" """
Scott Baker925a8fa2015-04-26 20:30:40 -0700722 class Meta:
723 proxy = True
Siobhan Tully00353f72013-10-08 21:53:27 -0400724
Scott Baker2461bec2015-08-14 09:10:11 -0700725 KIND = COARSE_KIND
Scott Baker925a8fa2015-04-26 20:30:40 -0700726
727 def save(self, *args, **kwargs):
728 if (not self.subscriber_service):
729 raise XOSValidationError("subscriber_service cannot be null")
730 if (self.subscriber_tenant or self.subscriber_user):
731 raise XOSValidationError("subscriber_tenant and subscriber_user must be null")
732
733 super(CoarseTenant,self).save()
Scott Baker4587b822015-07-01 18:29:08 -0700734
735class Subscriber(TenantRoot):
736 """ Intermediate class for TenantRoots that are to be Subscribers """
737
738 class Meta:
739 proxy = True
740
741 KIND = "Subscriber"
742
743class Provider(TenantRoot):
744 """ Intermediate class for TenantRoots that are to be Providers """
745
746 class Meta:
747 proxy = True
748
749 KIND = "Provider"
750
Scott Baker462a1d92015-10-15 15:59:19 -0700751class TenantAttribute(PlCoreBase):
Scott Baker907f9912015-10-20 17:12:36 -0700752 name = models.CharField(help_text="Attribute Name", max_length=128)
Scott Baker462a1d92015-10-15 15:59:19 -0700753 value = models.TextField(help_text="Attribute Value")
754 tenant = models.ForeignKey(Tenant, related_name='tenantattributes', help_text="The Tenant this attribute is associated with")
755
Scott Baker4587b822015-07-01 18:29:08 -0700756class TenantRootRole(PlCoreBase):
Scott Baker5116b302015-07-24 15:48:03 -0700757 ROLE_CHOICES = (('admin','Admin'), ('access','Access'))
Scott Baker4587b822015-07-01 18:29:08 -0700758
759 role = StrippedCharField(choices=ROLE_CHOICES, unique=True, max_length=30)
760
761 def __unicode__(self): return u'%s' % (self.role)
762
763class TenantRootPrivilege(PlCoreBase):
764 user = models.ForeignKey('User', related_name="tenant_root_privileges")
765 tenant_root = models.ForeignKey('TenantRoot', related_name="tenant_root_privileges")
766 role = models.ForeignKey('TenantRootRole', related_name="tenant_root_privileges")
767
768 class Meta:
769 unique_together = ('user', 'tenant_root', 'role')
770
Scott Baker618a4892015-07-06 14:27:31 -0700771 def __unicode__(self): return u'%s %s %s' % (self.tenant_root, self.user, self.role)
Scott Baker4587b822015-07-01 18:29:08 -0700772
773 def save(self, *args, **kwds):
774 if not self.user.is_active:
775 raise PermissionDenied, "Cannot modify role(s) of a disabled user"
Scott Baker335882a2015-07-24 10:15:31 -0700776 super(TenantRootPrivilege, self).save(*args, **kwds)
Scott Baker4587b822015-07-01 18:29:08 -0700777
778 def can_update(self, user):
Scott Baker335882a2015-07-24 10:15:31 -0700779 return user.can_update_tenant_root_privilege(self)
Scott Baker4587b822015-07-01 18:29:08 -0700780
Scott Baker16573d32015-07-24 15:36:02 -0700781 @classmethod
782 def select_by_user(cls, user):
Scott Baker4587b822015-07-01 18:29:08 -0700783 if user.is_admin:
Scott Baker5116b302015-07-24 15:48:03 -0700784 return cls.objects.all()
Scott Baker4587b822015-07-01 18:29:08 -0700785 else:
Scott Baker5116b302015-07-24 15:48:03 -0700786 # User can see his own privilege
787 trp_ids = [trp.id for trp in cls.objects.filter(user=user)]
788
789 # A slice admin can see the SlicePrivileges for his Slice
790 for priv in cls.objects.filter(user=user, role__role="admin"):
791 trp_ids.extend( [trp.id for trp in cls.objects.filter(tenant_root=priv.tenant_root)] )
792
793 return cls.objects.filter(id__in=trp_ids)
794
Scott Baker618a4892015-07-06 14:27:31 -0700795