blob: 61b0795f7e1109822b6d8bda6521274dca5cc2a4 [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
Scott Baker6dfde212016-03-01 20:10:24 -080035 def get_default_attribute(cls, name):
36 for (attrname, default) in cls.simple_attributes:
37 if attrname==name:
38 return default
39 if hasattr(cls,"defaults"):
40 if attrname in cls.defaults:
41 return cls.defaults[attrname]
42 else:
43 return None
44
45 @classmethod
Scott Baker74404fe2015-07-13 13:54:06 -070046 def setup_simple_attributes(cls):
47 for (attrname, default) in cls.simple_attributes:
Scott Baker096dce82015-07-13 14:27:51 -070048 setattr(cls, attrname, property(lambda self, attrname=attrname, default=default: self.get_attribute(attrname, default),
49 lambda self, value, attrname=attrname: self.set_attribute(attrname, value),
50 None,
51 attrname))
Scott Baker74404fe2015-07-13 13:54:06 -070052
Scott Baker82498c52015-07-13 13:07:27 -070053class Service(PlCoreBase, AttributeMixin):
Scott Baker008a9962015-04-15 20:58:20 -070054 # when subclassing a service, redefine KIND to describe the new service
55 KIND = "generic"
56
Siobhan Tully00353f72013-10-08 21:53:27 -040057 description = models.TextField(max_length=254,null=True, blank=True,help_text="Description of Service")
58 enabled = models.BooleanField(default=True)
Scott Baker008a9962015-04-15 20:58:20 -070059 kind = StrippedCharField(max_length=30, help_text="Kind of service", default=KIND)
Tony Mackd84b1ff2015-03-09 13:03:56 -040060 name = StrippedCharField(max_length=30, help_text="Service Name")
61 versionNumber = StrippedCharField(max_length=30, help_text="Version of Service Definition")
Siobhan Tullycf04fb62014-01-11 11:25:57 -050062 published = models.BooleanField(default=True)
Tony Mackd84b1ff2015-03-09 13:03:56 -040063 view_url = StrippedCharField(blank=True, null=True, max_length=1024)
64 icon_url = StrippedCharField(blank=True, null=True, max_length=1024)
Scott Baker68944742015-04-30 14:30:56 -070065 public_key = models.TextField(null=True, blank=True, max_length=1024, help_text="Public key string")
Scott Bakerf60c0102015-11-12 16:22:52 -080066 private_key_fn = StrippedCharField(blank=True, null=True, max_length=1024)
Siobhan Tully00353f72013-10-08 21:53:27 -040067
Scott Baker2f0828e2015-07-13 12:33:28 -070068 # Service_specific_attribute and service_specific_id are opaque to XOS
69 service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
70 service_specific_attribute = models.TextField(blank=True, null=True)
71
Scott Baker008a9962015-04-15 20:58:20 -070072 def __init__(self, *args, **kwargs):
73 # for subclasses, set the default kind appropriately
74 self._meta.get_field("kind").default = self.KIND
75 super(Service, self).__init__(*args, **kwargs)
76
77 @classmethod
78 def get_service_objects(cls):
79 return cls.objects.filter(kind = cls.KIND)
80
Scott Baker16573d32015-07-24 15:36:02 -070081 @classmethod
Scott Baker542cd6f2015-10-19 21:18:53 -070082 def get_deleted_service_objects(cls):
83 return cls.deleted_objects.filter(kind = cls.KIND)
84
85 @classmethod
Scott Baker16573d32015-07-24 15:36:02 -070086 def get_service_objects_by_user(cls, user):
87 return cls.select_by_user(user).filter(kind = cls.KIND)
88
89 @classmethod
90 def select_by_user(cls, user):
91 if user.is_admin:
92 return cls.objects.all()
93 else:
94 service_ids = [sp.slice.id for sp in ServicePrivilege.objects.filter(user=user)]
95 return cls.objects.filter(id__in=service_ids)
96
Scott Bakercce158d2015-12-07 22:20:40 -080097 @property
98 def serviceattribute_dict(self):
99 attrs = {}
100 for attr in self.serviceattributes.all():
101 attrs[attr.name] = attr.value
102 return attrs
103
Siobhan Tully00353f72013-10-08 21:53:27 -0400104 def __unicode__(self): return u'%s' % (self.name)
105
Tony Mack9d2ea092015-04-29 12:23:10 -0400106 def can_update(self, user):
107 return user.can_update_service(self, allow=['admin'])
Scott Baker618a4892015-07-06 14:27:31 -0700108
Scott Baker98436732015-05-11 16:36:41 -0700109 def get_scalable_nodes(self, slice, max_per_node=None, exclusive_slices=[]):
110 """
111 Get a list of nodes that can be used to scale up a slice.
112
113 slice - slice to scale up
Tony Mack3de59e32015-08-19 11:58:18 -0400114 max_per_node - maximum numbers of instances that 'slice' can have on a single node
Scott Baker98436732015-05-11 16:36:41 -0700115 exclusive_slices - list of slices that must have no nodes in common with 'slice'.
116 """
117
Tony Mack3de59e32015-08-19 11:58:18 -0400118 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 -0700119
120 nodes = list(Node.objects.all())
121
Tony Mack3de59e32015-08-19 11:58:18 -0400122 conflicting_instances = Instance.objects.filter(slice__in = exclusive_slices)
123 conflicting_nodes = Node.objects.filter(instances__in = conflicting_instances)
Scott Baker98436732015-05-11 16:36:41 -0700124
125 nodes = [x for x in nodes if x not in conflicting_nodes]
126
Tony Mack3de59e32015-08-19 11:58:18 -0400127 # If max_per_node is set, then limit the number of instances this slice
Scott Baker98436732015-05-11 16:36:41 -0700128 # can have on a single node.
129 if max_per_node:
130 acceptable_nodes = []
131 for node in nodes:
Tony Mack3de59e32015-08-19 11:58:18 -0400132 existing_count = node.instances.filter(slice=slice).count()
Scott Baker98436732015-05-11 16:36:41 -0700133 if existing_count < max_per_node:
134 acceptable_nodes.append(node)
135 nodes = acceptable_nodes
136
137 return nodes
138
139 def pick_node(self, slice, max_per_node=None, exclusive_slices=[]):
140 # Pick the best node to scale up a slice.
141
142 nodes = self.get_scalable_nodes(slice, max_per_node, exclusive_slices)
Tony Mack3de59e32015-08-19 11:58:18 -0400143 nodes = sorted(nodes, key=lambda node: node.instances.all().count())
Scott Baker98436732015-05-11 16:36:41 -0700144 if not nodes:
145 return None
146 return nodes[0]
147
148 def adjust_scale(self, slice_hint, scale, max_per_node=None, exclusive_slices=[]):
Tony Mack3de59e32015-08-19 11:58:18 -0400149 from core.models import Instance # late import to get around order-of-imports constraint in __init__.py
Scott Baker98436732015-05-11 16:36:41 -0700150
151 slices = [x for x in self.slices.all() if slice_hint in x.name]
152 for slice in slices:
Tony Mack3de59e32015-08-19 11:58:18 -0400153 while slice.instances.all().count() > scale:
154 s = slice.instances.all()[0]
155 # print "drop instance", s
Scott Baker98436732015-05-11 16:36:41 -0700156 s.delete()
157
Tony Mack3de59e32015-08-19 11:58:18 -0400158 while slice.instances.all().count() < scale:
Scott Baker98436732015-05-11 16:36:41 -0700159 node = self.pick_node(slice, max_per_node, exclusive_slices)
160 if not node:
161 # no more available nodes
162 break
163
164 image = slice.default_image
165 if not image:
166 raise XOSConfigurationError("No default_image for slice %s" % slice.name)
167
168 flavor = slice.default_flavor
169 if not flavor:
170 raise XOSConfigurationError("No default_flavor for slice %s" % slice.name)
171
Tony Mack3de59e32015-08-19 11:58:18 -0400172 s = Instance(slice=slice,
Scott Baker98436732015-05-11 16:36:41 -0700173 node=node,
174 creator=slice.creator,
175 image=image,
176 flavor=flavor,
177 deployment=node.site_deployment.deployment)
178 s.save()
179
Tony Mack3de59e32015-08-19 11:58:18 -0400180 # print "add instance", s
Tony Mack9d2ea092015-04-29 12:23:10 -0400181
Scott Baker8a67ae72015-12-09 22:54:52 -0800182 def get_vtn_src_nets(self):
183 nets=[]
184 for slice in self.slices.all():
185 for ns in slice.networkslices.all():
186 if not ns.network:
187 continue
Scott Bakerf63cf1c2016-01-12 19:59:12 -0800188# if ns.network.template.access in ["direct", "indirect"]:
189# # skip access networks; we want to use the private network
190# continue
Scott Baker8a67ae72015-12-09 22:54:52 -0800191 if ns.network.name in ["wan_network", "lan_network"]:
192 # we don't want to attach to the vCPE's lan or wan network
193 # we only want to attach to its private network
194 # TODO: fix hard-coding of network name
195 continue
196 for cn in ns.network.controllernetworks.all():
197 if cn.net_id:
198 net = {"name": ns.network.name, "net_id": cn.net_id}
199 nets.append(net)
200 return nets
201
Scott Bakercbd718e2015-12-08 21:31:18 -0800202 def get_vtn_nets(self):
203 nets=[]
204 for slice in self.slices.all():
205 for ns in slice.networkslices.all():
206 if not ns.network:
207 continue
Scott Baker8a67ae72015-12-09 22:54:52 -0800208 if ns.network.template.access not in ["direct", "indirect"]:
209 # skip anything that's not an access network
210 continue
Scott Bakercbd718e2015-12-08 21:31:18 -0800211 for cn in ns.network.controllernetworks.all():
212 if cn.net_id:
213 net = {"name": ns.network.name, "net_id": cn.net_id}
214 nets.append(net)
215 return nets
216
217 def get_vtn_dependencies_nets(self):
218 provider_nets = []
Scott Baker5380a442015-12-08 19:27:50 -0800219 for tenant in self.subscribed_tenants.all():
220 if tenant.provider_service:
Scott Bakercbd718e2015-12-08 21:31:18 -0800221 for net in tenant.provider_service.get_vtn_nets():
222 if not net in provider_nets:
223 provider_nets.append(net)
224 return provider_nets
225
226 def get_vtn_dependencies_ids(self):
227 return [x["net_id"] for x in self.get_vtn_dependencies_nets()]
228
229 def get_vtn_dependencies_names(self):
230 return [x["name"]+"_"+x["net_id"] for x in self.get_vtn_dependencies_nets()]
231
Scott Baker8a67ae72015-12-09 22:54:52 -0800232 def get_vtn_src_ids(self):
233 return [x["net_id"] for x in self.get_vtn_src_nets()]
Scott Bakercbd718e2015-12-08 21:31:18 -0800234
Scott Baker8a67ae72015-12-09 22:54:52 -0800235 def get_vtn_src_names(self):
236 return [x["name"]+"_"+x["net_id"] for x in self.get_vtn_src_nets()]
Scott Baker5380a442015-12-08 19:27:50 -0800237
238
Siobhan Tully00353f72013-10-08 21:53:27 -0400239class ServiceAttribute(PlCoreBase):
Scott Bakercce158d2015-12-07 22:20:40 -0800240 name = models.CharField(help_text="Attribute Name", max_length=128)
Tony Mackd84b1ff2015-03-09 13:03:56 -0400241 value = StrippedCharField(help_text="Attribute Value", max_length=1024)
Siobhan Tully00353f72013-10-08 21:53:27 -0400242 service = models.ForeignKey(Service, related_name='serviceattributes', help_text="The Service this attribute is associated with")
243
Tony Mack9d2ea092015-04-29 12:23:10 -0400244class ServiceRole(PlCoreBase):
245 ROLE_CHOICES = (('admin','Admin'),)
246 role = StrippedCharField(choices=ROLE_CHOICES, unique=True, max_length=30)
247
248 def __unicode__(self): return u'%s' % (self.role)
249
250class ServicePrivilege(PlCoreBase):
251 user = models.ForeignKey('User', related_name='serviceprivileges')
252 service = models.ForeignKey('Service', related_name='serviceprivileges')
253 role = models.ForeignKey('ServiceRole',related_name='serviceprivileges')
254
255 class Meta:
Tony Mack9ec754e2015-05-13 12:21:28 -0400256 unique_together = ('user', 'service', 'role')
Tony Mack9d2ea092015-04-29 12:23:10 -0400257
258 def __unicode__(self): return u'%s %s %s' % (self.service, self.user, self.role)
259
260 def can_update(self, user):
261 if not self.service.enabled:
262 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
263 return self.service.can_update(user)
264
265 def save(self, *args, **kwds):
266 if not self.service.enabled:
267 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
268 super(ServicePrivilege, self).save(*args, **kwds)
269
270 def delete(self, *args, **kwds):
271 if not self.service.enabled:
272 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
Scott Baker4587b822015-07-01 18:29:08 -0700273 super(ServicePrivilege, self).delete(*args, **kwds)
274
Scott Baker16573d32015-07-24 15:36:02 -0700275 @classmethod
276 def select_by_user(cls, user):
Tony Mack9d2ea092015-04-29 12:23:10 -0400277 if user.is_admin:
Scott Baker16573d32015-07-24 15:36:02 -0700278 qs = cls.objects.all()
Tony Mack9d2ea092015-04-29 12:23:10 -0400279 else:
Scott Baker16573d32015-07-24 15:36:02 -0700280 qs = cls.objects.filter(user=user)
Scott Baker4587b822015-07-01 18:29:08 -0700281 return qs
282
Scott Baker82498c52015-07-13 13:07:27 -0700283class TenantRoot(PlCoreBase, AttributeMixin):
Scott Baker4587b822015-07-01 18:29:08 -0700284 """ A tenantRoot is one of the things that can sit at the root of a chain
285 of tenancy. This object represents a node.
286 """
287
288 KIND= "generic"
289 kind = StrippedCharField(max_length=30, default=KIND)
Scott Baker618a4892015-07-06 14:27:31 -0700290 name = StrippedCharField(max_length=255, help_text="name", blank=True, null=True)
Scott Baker4587b822015-07-01 18:29:08 -0700291
Scott Bakerefcec632015-07-07 12:12:42 -0700292 service_specific_attribute = models.TextField(blank=True, null=True)
293 service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
Scott Baker4587b822015-07-01 18:29:08 -0700294
Scott Bakerdb66fd32015-07-07 17:59:44 -0700295 def __init__(self, *args, **kwargs):
296 # for subclasses, set the default kind appropriately
297 self._meta.get_field("kind").default = self.KIND
298 super(TenantRoot, self).__init__(*args, **kwargs)
299
Scott Baker618a4892015-07-06 14:27:31 -0700300 def __unicode__(self):
301 if not self.name:
302 return u"%s-tenant_root-#%s" % (str(self.kind), str(self.id))
303 else:
304 return self.name
305
306 def can_update(self, user):
307 return user.can_update_tenant_root(self, allow=['admin'])
308
Scott Bakerefcec632015-07-07 12:12:42 -0700309 def get_subscribed_tenants(self, tenant_class):
310 ids = self.subscribed_tenants.filter(kind=tenant_class.KIND)
311 return tenant_class.objects.filter(id__in = ids)
312
313 def get_newest_subscribed_tenant(self, kind):
314 st = list(self.get_subscribed_tenants(kind))
315 if not st:
316 return None
317 return sorted(st, key=attrgetter('id'))[0]
318
319 @classmethod
320 def get_tenant_objects(cls):
321 return cls.objects.filter(kind = cls.KIND)
322
Scott Baker16573d32015-07-24 15:36:02 -0700323 @classmethod
324 def get_tenant_objects_by_user(cls, user):
325 return cls.select_by_user(user).filter(kind = cls.KIND)
326
327 @classmethod
328 def select_by_user(cls, user):
329 if user.is_admin:
330 return cls.objects.all()
331 else:
332 tr_ids = [trp.tenant_root.id for trp in TenantRootPrivilege.objects.filter(user=user)]
333 return cls.objects.filter(id__in=tr_ids)
334
Scott Baker82498c52015-07-13 13:07:27 -0700335class Tenant(PlCoreBase, AttributeMixin):
Scott Baker8103d0f2015-04-10 16:42:26 -0700336 """ A tenant is a relationship between two entities, a subscriber and a
Scott Baker4587b822015-07-01 18:29:08 -0700337 provider. This object represents an edge.
Scott Baker8103d0f2015-04-10 16:42:26 -0700338
339 The subscriber can be a User, a Service, or a Tenant.
340
341 The provider is always a Service.
Scott Baker4587b822015-07-01 18:29:08 -0700342
343 TODO: rename "Tenant" to "Tenancy"
Scott Baker8103d0f2015-04-10 16:42:26 -0700344 """
Scott Baker008a9962015-04-15 20:58:20 -0700345
Scott Baker925a8fa2015-04-26 20:30:40 -0700346 CONNECTIVITY_CHOICES = (('public', 'Public'), ('private', 'Private'), ('na', 'Not Applicable'))
347
Scott Baker008a9962015-04-15 20:58:20 -0700348 # when subclassing a service, redefine KIND to describe the new service
349 KIND = "generic"
350
351 kind = StrippedCharField(max_length=30, default=KIND)
Scott Baker4587b822015-07-01 18:29:08 -0700352 provider_service = models.ForeignKey(Service, related_name='provided_tenants')
353
354 # The next four things are the various type of objects that can be subscribers of this Tenancy
355 # relationship. One and only one can be used at a time.
356 subscriber_service = models.ForeignKey(Service, related_name='subscribed_tenants', blank=True, null=True)
357 subscriber_tenant = models.ForeignKey("Tenant", related_name='subscribed_tenants', blank=True, null=True)
358 subscriber_user = models.ForeignKey("User", related_name='subscribed_tenants', blank=True, null=True)
359 subscriber_root = models.ForeignKey("TenantRoot", related_name="subscribed_tenants", blank=True, null=True)
360
361 # Service_specific_attribute and service_specific_id are opaque to XOS
Scott Baker76934d82015-05-06 19:49:31 -0700362 service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
363 service_specific_attribute = models.TextField(blank=True, null=True)
Scott Baker4587b822015-07-01 18:29:08 -0700364
365 # Connect_method is only used by Coarse tenants
Scott Baker925a8fa2015-04-26 20:30:40 -0700366 connect_method = models.CharField(null=False, blank=False, max_length=30, choices=CONNECTIVITY_CHOICES, default="na")
Scott Baker8103d0f2015-04-10 16:42:26 -0700367
Scott Baker008a9962015-04-15 20:58:20 -0700368 def __init__(self, *args, **kwargs):
369 # for subclasses, set the default kind appropriately
370 self._meta.get_field("kind").default = self.KIND
371 super(Tenant, self).__init__(*args, **kwargs)
372
Scott Baker8103d0f2015-04-10 16:42:26 -0700373 def __unicode__(self):
Scott Bakerf996b762015-05-20 20:42:04 -0700374 return u"%s-tenant-%s" % (str(self.kind), str(self.id))
Scott Baker8103d0f2015-04-10 16:42:26 -0700375
Scott Baker008a9962015-04-15 20:58:20 -0700376 @classmethod
377 def get_tenant_objects(cls):
378 return cls.objects.filter(kind = cls.KIND)
379
Scott Bakere7fc9f52015-05-05 17:52:03 -0700380 @classmethod
Scott Baker16573d32015-07-24 15:36:02 -0700381 def get_tenant_objects_by_user(cls, user):
382 return cls.select_by_user(user).filter(kind = cls.KIND)
383
384 @classmethod
Scott Bakere7fc9f52015-05-05 17:52:03 -0700385 def get_deleted_tenant_objects(cls):
386 return cls.deleted_objects.filter(kind = cls.KIND)
387
Scott Bakercce158d2015-12-07 22:20:40 -0800388 @property
389 def tenantattribute_dict(self):
390 attrs = {}
391 for attr in self.tenantattributes.all():
392 attrs[attr.name] = attr.value
393 return attrs
394
Scott Bakerd921e1c2015-04-20 14:24:29 -0700395 # helper function to be used in subclasses that want to ensure service_specific_id is unique
396 def validate_unique_service_specific_id(self):
397 if self.pk is None:
398 if self.service_specific_id is None:
399 raise XOSMissingField("subscriber_specific_id is None, and it's a required field", fields={"service_specific_id": "cannot be none"})
400
401 conflicts = self.get_tenant_objects().filter(service_specific_id=self.service_specific_id)
402 if conflicts:
403 raise XOSDuplicateKey("service_specific_id %s already exists" % self.service_specific_id, fields={"service_specific_id": "duplicate key"})
404
Scott Baker618a4892015-07-06 14:27:31 -0700405 def save(self, *args, **kwargs):
406 subCount = sum( [1 for e in [self.subscriber_service, self.subscriber_tenant, self.subscriber_user, self.subscriber_root] if e is not None])
407 if (subCount > 1):
408 raise XOSConflictingField("Only one of subscriber_service, subscriber_tenant, subscriber_user, subscriber_root should be set")
409
410 super(Tenant, self).save(*args, **kwargs)
411
412 def get_subscribed_tenants(self, tenant_class):
413 ids = self.subscribed_tenants.filter(kind=tenant_class.KIND)
414 return tenant_class.objects.filter(id__in = ids)
415
416 def get_newest_subscribed_tenant(self, kind):
417 st = list(self.get_subscribed_tenants(kind))
418 if not st:
419 return None
420 return sorted(st, key=attrgetter('id'))[0]
421
Scott Baker534dd2a2015-11-18 20:58:08 -0800422class Scheduler(object):
423 # XOS Scheduler Abstract Base Class
424 # Used to implement schedulers that pick which node to put instances on
425
426 def __init__(self, slice):
427 self.slice = slice
428
429 def pick(self):
430 # this method should return a tuple (node, parent)
431 # node is the node to instantiate on
432 # parent is for container_vm instances only, and is the VM that will
433 # hold the container
434
435 raise Exception("Abstract Base")
436
437class LeastLoadedNodeScheduler(Scheduler):
438 # This scheduler always return the node with the fewest number of instances.
439
440 def __init__(self, slice):
441 super(LeastLoadedNodeScheduler, self).__init__(slice)
442
443 def pick(self):
444 from core.models import Node
445 nodes = list(Node.objects.all())
Scott Bakercce158d2015-12-07 22:20:40 -0800446
Scott Baker534dd2a2015-11-18 20:58:08 -0800447 # TODO: logic to filter nodes by which nodes are up, and which
448 # nodes the slice can instantiate on.
449 nodes = sorted(nodes, key=lambda node: node.instances.all().count())
450 return [nodes[0], None]
451
452class ContainerVmScheduler(Scheduler):
453 # This scheduler picks a VM in the slice with the fewest containers inside
454 # of it. If no VMs are suitable, then it creates a VM.
455
456 # this is a hack and should be replaced by something smarter...
457 LOOK_FOR_IMAGES=["ubuntu-vcpe4", # ONOS demo machine -- preferred vcpe image
458 "Ubuntu 14.04 LTS", # portal
459 "Ubuntu-14.04-LTS", # ONOS demo machine
460 "trusty-server-multi-nic", # CloudLab
461 ]
462
463 MAX_VM_PER_CONTAINER = 10
464
465 def __init__(self, slice):
466 super(ContainerVmScheduler, self).__init__(slice)
467
468 @property
469 def image(self):
470 from core.models import Image
471
472 look_for_images = self.LOOK_FOR_IMAGES
473 for image_name in look_for_images:
474 images = Image.objects.filter(name = image_name)
475 if images:
476 return images[0]
477
478 raise XOSProgrammingError("No ContainerVM image (looked for %s)" % str(look_for_images))
479
480 def make_new_instance(self):
481 from core.models import Instance, Flavor
482
483 flavors = Flavor.objects.filter(name="m1.small")
484 if not flavors:
485 raise XOSConfigurationError("No m1.small flavor")
486
487 (node,parent) = LeastLoadedNodeScheduler(self.slice).pick()
488
489 instance = Instance(slice = self.slice,
490 node = node,
491 image = self.image,
492 creator = self.slice.creator,
493 deployment = node.site_deployment.deployment,
494 flavor = flavors[0],
495 isolation = "vm",
496 parent = parent)
497 instance.save()
498 # We rely on a special naming convention to identify the VMs that will
499 # hole containers.
500 instance.name = "%s-outer-%s" % (instance.slice.name, instance.id)
501 instance.save()
502 return instance
503
504 def pick(self):
505 from core.models import Instance, Flavor
506
507 for vm in self.slice.instances.filter(isolation="vm"):
508 avail_vms = []
509 if (vm.name.startswith("%s-outer-" % self.slice.name)):
510 container_count = Instance.objects.filter(parent=vm).count()
511 if (container_count < self.MAX_VM_PER_CONTAINER):
512 avail_vms.append( (vm, container_count) )
513 # sort by least containers-per-vm
514 avail_vms = sorted(avail_vms, key = lambda x: x[1])
515 print "XXX", avail_vms
516 if avail_vms:
517 instance = avail_vms[0][0]
518 return (instance.node, instance)
519
520 instance = self.make_new_instance()
521 return (instance.node, instance)
522
Scott Bakere458afd2015-09-09 16:36:06 -0700523class TenantWithContainer(Tenant):
524 """ A tenant that manages a container """
525
526 # this is a hack and should be replaced by something smarter...
Scott Baker7288c522015-09-09 16:43:39 -0700527 LOOK_FOR_IMAGES=["ubuntu-vcpe4", # ONOS demo machine -- preferred vcpe image
Scott Bakere458afd2015-09-09 16:36:06 -0700528 "Ubuntu 14.04 LTS", # portal
529 "Ubuntu-14.04-LTS", # ONOS demo machine
Scott Baker7288c522015-09-09 16:43:39 -0700530 "trusty-server-multi-nic", # CloudLab
Scott Bakere458afd2015-09-09 16:36:06 -0700531 ]
532
Scott Baker0f908e02015-12-14 10:21:53 -0800533 LOOK_FOR_CONTAINER_IMAGES=["docker-vcpe"]
Scott Bakercfaf0d32015-11-16 22:51:02 -0800534
Scott Bakere458afd2015-09-09 16:36:06 -0700535 class Meta:
536 proxy = True
537
538 def __init__(self, *args, **kwargs):
539 super(TenantWithContainer, self).__init__(*args, **kwargs)
Tony Mack6a782f92015-09-13 22:50:39 +0000540 self.cached_instance=None
541 self.orig_instance_id = self.get_initial_attribute("instance_id")
Scott Bakere274f6b2015-11-02 20:54:28 -0800542
Scott Bakere458afd2015-09-09 16:36:06 -0700543 @property
Tony Mack6a782f92015-09-13 22:50:39 +0000544 def instance(self):
545 from core.models import Instance
546 if getattr(self, "cached_instance", None):
547 return self.cached_instance
548 instance_id=self.get_attribute("instance_id")
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600549 if not instance_id:
550 return None
551 instances=Instance.objects.filter(id=instance_id)
552 if not instances:
553 return None
554 instance=instances[0]
Tony Mack6a782f92015-09-13 22:50:39 +0000555 instance.caller = self.creator
556 self.cached_instance = instance
557 return instance
Scott Bakere458afd2015-09-09 16:36:06 -0700558
Tony Mack6a782f92015-09-13 22:50:39 +0000559 @instance.setter
560 def instance(self, value):
Scott Bakere458afd2015-09-09 16:36:06 -0700561 if value:
562 value = value.id
Tony Mack6a782f92015-09-13 22:50:39 +0000563 if (value != self.get_attribute("instance_id", None)):
564 self.cached_instance=None
565 self.set_attribute("instance_id", value)
Scott Bakere458afd2015-09-09 16:36:06 -0700566
Scott Bakere274f6b2015-11-02 20:54:28 -0800567 @property
Scott Bakerbdda0be2016-02-10 12:23:53 -0800568 def external_hostname(self):
569 return self.get_attribute("external_hostname", "")
570
571 @external_hostname.setter
572 def external_hostname(self, value):
573 self.set_attribute("external_hostname", value)
574
575 @property
576 def external_container(self):
577 return self.get_attribute("external_container", "")
578
579 @external_container.setter
580 def external_container(self, value):
581 self.set_attribute("external_container", value)
582
583 @property
Scott Bakere458afd2015-09-09 16:36:06 -0700584 def creator(self):
585 from core.models import User
586 if getattr(self, "cached_creator", None):
587 return self.cached_creator
588 creator_id=self.get_attribute("creator_id")
589 if not creator_id:
590 return None
591 users=User.objects.filter(id=creator_id)
592 if not users:
593 return None
594 user=users[0]
595 self.cached_creator = users[0]
596 return user
597
598 @creator.setter
599 def creator(self, value):
600 if value:
601 value = value.id
602 if (value != self.get_attribute("creator_id", None)):
603 self.cached_creator=None
604 self.set_attribute("creator_id", value)
605
606 @property
607 def image(self):
608 from core.models import Image
609 # Implement the logic here to pick the image that should be used when
610 # instantiating the VM that will hold the container.
Scott Bakercfaf0d32015-11-16 22:51:02 -0800611
612 slice = self.provider_service.slices.all()
613 if not slice:
614 raise XOSProgrammingError("provider service has no slice")
615 slice = slice[0]
616
617 if slice.default_isolation in ["container", "container_vm"]:
618 look_for_images = self.LOOK_FOR_CONTAINER_IMAGES
619 else:
620 look_for_images = self.LOOK_FOR_IMAGES
621
622 for image_name in look_for_images:
Scott Bakere458afd2015-09-09 16:36:06 -0700623 images = Image.objects.filter(name = image_name)
624 if images:
625 return images[0]
626
Scott Baker534dd2a2015-11-18 20:58:08 -0800627 raise XOSProgrammingError("No VPCE image (looked for %s)" % str(look_for_images))
Scott Bakere274f6b2015-11-02 20:54:28 -0800628
Scott Bakercfaf0d32015-11-16 22:51:02 -0800629 def save_instance(self, instance):
630 # Override this function to do custom pre-save or post-save processing,
631 # such as creating ports for containers.
632 instance.save()
Scott Bakere274f6b2015-11-02 20:54:28 -0800633
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600634 def pick_least_loaded_instance_in_slice(self, slices):
635 for slice in slices:
636 if slice.instances.all().count() > 0:
637 for instance in slice.instances.all():
638 #Pick the first instance that has lesser than 5 tenants
639 if self.count_of_tenants_of_an_instance(instance) < 5:
640 return instance
641 return None
642
643 #TODO: Ideally the tenant count for an instance should be maintained using a
644 #many-to-one relationship attribute, however this model being proxy, it does
645 #not permit any new attributes to be defined. Find if any better solutions
646 def count_of_tenants_of_an_instance(self, instance):
647 tenant_count = 0
648 for tenant in self.get_tenant_objects().all():
649 if tenant.get_attribute("instance_id", None) == instance.id:
650 tenant_count += 1
651 return tenant_count
652
Scott Bakercfaf0d32015-11-16 22:51:02 -0800653 def manage_container(self):
Tony Mack6a782f92015-09-13 22:50:39 +0000654 from core.models import Instance, Flavor
Scott Bakere458afd2015-09-09 16:36:06 -0700655
656 if self.deleted:
657 return
658
Tony Mack6a782f92015-09-13 22:50:39 +0000659 if (self.instance is not None) and (self.instance.image != self.image):
660 self.instance.delete()
661 self.instance = None
Scott Bakere458afd2015-09-09 16:36:06 -0700662
Tony Mack6a782f92015-09-13 22:50:39 +0000663 if self.instance is None:
Scott Bakere458afd2015-09-09 16:36:06 -0700664 if not self.provider_service.slices.count():
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600665 raise XOSConfigurationError("The service has no slices")
Scott Bakere458afd2015-09-09 16:36:06 -0700666
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600667 new_instance_created = False
668 instance = None
669 if self.get_attribute("use_same_instance_for_multiple_tenants", default=False):
670 #Find if any existing instances can be used for this tenant
671 slices = self.provider_service.slices.all()
672 instance = self.pick_least_loaded_instance_in_slice(slices)
Scott Bakere458afd2015-09-09 16:36:06 -0700673
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600674 if not instance:
Srikanth Vavilapalli71120a92015-11-17 13:41:38 -0600675 slice = self.provider_service.slices.all()[0]
Scott Bakercfaf0d32015-11-16 22:51:02 -0800676
Srikanth Vavilapalli2ab2d072016-02-15 01:18:09 -0500677 flavor = slice.default_flavor
678 if not flavor:
679 flavors = Flavor.objects.filter(name="m1.small")
680 if not flavors:
681 raise XOSConfigurationError("No m1.small flavor")
682 flavor = flavors[0]
683
Srikanth Vavilapalli71120a92015-11-17 13:41:38 -0600684 if slice.default_isolation == "container_vm":
Scott Baker534dd2a2015-11-18 20:58:08 -0800685 (node, parent) = ContainerVmScheduler(slice).pick()
Srikanth Vavilapalli71120a92015-11-17 13:41:38 -0600686 else:
Scott Baker534dd2a2015-11-18 20:58:08 -0800687 (node, parent) = LeastLoadedNodeScheduler(slice).pick()
Scott Bakercfaf0d32015-11-16 22:51:02 -0800688
Srikanth Vavilapalli71120a92015-11-17 13:41:38 -0600689 instance = Instance(slice = slice,
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600690 node = node,
691 image = self.image,
692 creator = self.creator,
693 deployment = node.site_deployment.deployment,
Srikanth Vavilapalli2ab2d072016-02-15 01:18:09 -0500694 flavor = flavor,
Srikanth Vavilapalli71120a92015-11-17 13:41:38 -0600695 isolation = slice.default_isolation,
696 parent = parent)
697 self.save_instance(instance)
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600698 new_instance_created = True
Scott Bakere458afd2015-09-09 16:36:06 -0700699
700 try:
Tony Mack6a782f92015-09-13 22:50:39 +0000701 self.instance = instance
Scott Bakere458afd2015-09-09 16:36:06 -0700702 super(TenantWithContainer, self).save()
703 except:
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600704 if new_instance_created:
705 instance.delete()
Scott Bakere458afd2015-09-09 16:36:06 -0700706 raise
707
708 def cleanup_container(self):
Tony Mack6a782f92015-09-13 22:50:39 +0000709 if self.instance:
Srikanth Vavilapalli1d14ff02015-11-17 12:21:02 -0600710 if self.get_attribute("use_same_instance_for_multiple_tenants", default=False):
711 #Delete the instance only if this is last tenant in that instance
712 tenant_count = self.count_of_tenants_of_an_instance(self.instance)
713 if tenant_count == 0:
714 self.instance.delete()
715 else:
716 self.instance.delete()
Tony Mack6a782f92015-09-13 22:50:39 +0000717 self.instance = None
Scott Baker618a4892015-07-06 14:27:31 -0700718
Scott Bakeree83f2a2015-12-10 23:23:07 -0800719 def save(self, *args, **kwargs):
720 if (not self.creator) and (hasattr(self, "caller")) and (self.caller):
721 self.creator = self.caller
722 super(TenantWithContainer, self).save(*args, **kwargs)
723
Scott Baker925a8fa2015-04-26 20:30:40 -0700724class CoarseTenant(Tenant):
Scott Baker4587b822015-07-01 18:29:08 -0700725 """ TODO: rename "CoarseTenant" --> "StaticTenant" """
Scott Baker925a8fa2015-04-26 20:30:40 -0700726 class Meta:
727 proxy = True
Siobhan Tully00353f72013-10-08 21:53:27 -0400728
Scott Baker2461bec2015-08-14 09:10:11 -0700729 KIND = COARSE_KIND
Scott Baker925a8fa2015-04-26 20:30:40 -0700730
731 def save(self, *args, **kwargs):
732 if (not self.subscriber_service):
733 raise XOSValidationError("subscriber_service cannot be null")
734 if (self.subscriber_tenant or self.subscriber_user):
735 raise XOSValidationError("subscriber_tenant and subscriber_user must be null")
736
737 super(CoarseTenant,self).save()
Scott Baker4587b822015-07-01 18:29:08 -0700738
739class Subscriber(TenantRoot):
740 """ Intermediate class for TenantRoots that are to be Subscribers """
741
742 class Meta:
743 proxy = True
744
745 KIND = "Subscriber"
746
747class Provider(TenantRoot):
748 """ Intermediate class for TenantRoots that are to be Providers """
749
750 class Meta:
751 proxy = True
752
753 KIND = "Provider"
754
Scott Baker462a1d92015-10-15 15:59:19 -0700755class TenantAttribute(PlCoreBase):
Scott Baker907f9912015-10-20 17:12:36 -0700756 name = models.CharField(help_text="Attribute Name", max_length=128)
Scott Baker462a1d92015-10-15 15:59:19 -0700757 value = models.TextField(help_text="Attribute Value")
758 tenant = models.ForeignKey(Tenant, related_name='tenantattributes', help_text="The Tenant this attribute is associated with")
759
Scott Baker4587b822015-07-01 18:29:08 -0700760class TenantRootRole(PlCoreBase):
Scott Baker5116b302015-07-24 15:48:03 -0700761 ROLE_CHOICES = (('admin','Admin'), ('access','Access'))
Scott Baker4587b822015-07-01 18:29:08 -0700762
763 role = StrippedCharField(choices=ROLE_CHOICES, unique=True, max_length=30)
764
765 def __unicode__(self): return u'%s' % (self.role)
766
767class TenantRootPrivilege(PlCoreBase):
768 user = models.ForeignKey('User', related_name="tenant_root_privileges")
769 tenant_root = models.ForeignKey('TenantRoot', related_name="tenant_root_privileges")
770 role = models.ForeignKey('TenantRootRole', related_name="tenant_root_privileges")
771
772 class Meta:
773 unique_together = ('user', 'tenant_root', 'role')
774
Scott Baker618a4892015-07-06 14:27:31 -0700775 def __unicode__(self): return u'%s %s %s' % (self.tenant_root, self.user, self.role)
Scott Baker4587b822015-07-01 18:29:08 -0700776
777 def save(self, *args, **kwds):
778 if not self.user.is_active:
779 raise PermissionDenied, "Cannot modify role(s) of a disabled user"
Scott Baker335882a2015-07-24 10:15:31 -0700780 super(TenantRootPrivilege, self).save(*args, **kwds)
Scott Baker4587b822015-07-01 18:29:08 -0700781
782 def can_update(self, user):
Scott Baker335882a2015-07-24 10:15:31 -0700783 return user.can_update_tenant_root_privilege(self)
Scott Baker4587b822015-07-01 18:29:08 -0700784
Scott Baker16573d32015-07-24 15:36:02 -0700785 @classmethod
786 def select_by_user(cls, user):
Scott Baker4587b822015-07-01 18:29:08 -0700787 if user.is_admin:
Scott Baker5116b302015-07-24 15:48:03 -0700788 return cls.objects.all()
Scott Baker4587b822015-07-01 18:29:08 -0700789 else:
Scott Baker5116b302015-07-24 15:48:03 -0700790 # User can see his own privilege
791 trp_ids = [trp.id for trp in cls.objects.filter(user=user)]
792
793 # A slice admin can see the SlicePrivileges for his Slice
794 for priv in cls.objects.filter(user=user, role__role="admin"):
795 trp_ids.extend( [trp.id for trp in cls.objects.filter(tenant_root=priv.tenant_root)] )
796
797 return cls.objects.filter(id__in=trp_ids)
798
Scott Baker618a4892015-07-06 14:27:31 -0700799