blob: 015564acd4a66b1ce5f3b7657986f80b4a680b76 [file] [log] [blame]
Scott Baker7211f5b2015-04-14 17:18:51 -07001import json
Jeremy Mowerya74c31d2016-04-04 22:30:44 -07002from operator import attrgetter
3
4from core.models import PlCoreBase, PlCoreBaseManager, SingletonModel
5from core.models.plcorebase import StrippedCharField
6from django.db import models
7from xos.exceptions import *
Siobhan Tully00353f72013-10-08 21:53:27 -04008
Scott Bakerc24f86d2015-08-14 09:10:11 -07009COARSE_KIND="coarse"
10
Scott Baker9d1c6d92015-07-13 13:07:27 -070011class AttributeMixin(object):
12 # helper for extracting things from a json-encoded service_specific_attribute
13 def get_attribute(self, name, default=None):
14 if self.service_specific_attribute:
15 attributes = json.loads(self.service_specific_attribute)
16 else:
17 attributes = {}
18 return attributes.get(name, default)
19
20 def set_attribute(self, name, value):
21 if self.service_specific_attribute:
22 attributes = json.loads(self.service_specific_attribute)
23 else:
24 attributes = {}
25 attributes[name]=value
26 self.service_specific_attribute = json.dumps(attributes)
27
28 def get_initial_attribute(self, name, default=None):
29 if self._initial["service_specific_attribute"]:
30 attributes = json.loads(self._initial["service_specific_attribute"])
31 else:
32 attributes = {}
33 return attributes.get(name, default)
34
Scott Bakereb098e62015-07-13 13:54:06 -070035 @classmethod
Scott Baker440d1152016-03-01 20:10:24 -080036 def get_default_attribute(cls, name):
37 for (attrname, default) in cls.simple_attributes:
38 if attrname==name:
39 return default
Scott Baker39e1e912016-03-01 20:12:05 -080040 if hasattr(cls,"default_attributes"):
41 if attrname in cls.default_attributes:
42 return cls.default_attributes[attrname]
Scott Baker440d1152016-03-01 20:10:24 -080043 else:
44 return None
45
46 @classmethod
Scott Bakereb098e62015-07-13 13:54:06 -070047 def setup_simple_attributes(cls):
48 for (attrname, default) in cls.simple_attributes:
Scott Bakere2879d32015-07-13 14:27:51 -070049 setattr(cls, attrname, property(lambda self, attrname=attrname, default=default: self.get_attribute(attrname, default),
50 lambda self, value, attrname=attrname: self.set_attribute(attrname, value),
51 None,
52 attrname))
Scott Bakereb098e62015-07-13 13:54:06 -070053
Scott Baker9d1c6d92015-07-13 13:07:27 -070054class Service(PlCoreBase, AttributeMixin):
Scott Baker0d306722015-04-15 20:58:20 -070055 # when subclassing a service, redefine KIND to describe the new service
56 KIND = "generic"
57
Siobhan Tully00353f72013-10-08 21:53:27 -040058 description = models.TextField(max_length=254,null=True, blank=True,help_text="Description of Service")
59 enabled = models.BooleanField(default=True)
Scott Baker0d306722015-04-15 20:58:20 -070060 kind = StrippedCharField(max_length=30, help_text="Kind of service", default=KIND)
Tony Mack50e12212015-03-09 13:03:56 -040061 name = StrippedCharField(max_length=30, help_text="Service Name")
62 versionNumber = StrippedCharField(max_length=30, help_text="Version of Service Definition")
Siobhan Tullycf04fb62014-01-11 11:25:57 -050063 published = models.BooleanField(default=True)
Tony Mack50e12212015-03-09 13:03:56 -040064 view_url = StrippedCharField(blank=True, null=True, max_length=1024)
65 icon_url = StrippedCharField(blank=True, null=True, max_length=1024)
Scott Baker5b044612015-04-30 14:30:56 -070066 public_key = models.TextField(null=True, blank=True, max_length=1024, help_text="Public key string")
Scott Bakerdc63fb32015-11-12 16:22:52 -080067 private_key_fn = StrippedCharField(blank=True, null=True, max_length=1024)
Siobhan Tully00353f72013-10-08 21:53:27 -040068
Scott Bakerb9040e92015-07-13 12:33:28 -070069 # Service_specific_attribute and service_specific_id are opaque to XOS
70 service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
71 service_specific_attribute = models.TextField(blank=True, null=True)
72
Scott Baker0d306722015-04-15 20:58:20 -070073 def __init__(self, *args, **kwargs):
74 # for subclasses, set the default kind appropriately
75 self._meta.get_field("kind").default = self.KIND
76 super(Service, self).__init__(*args, **kwargs)
77
78 @classmethod
79 def get_service_objects(cls):
80 return cls.objects.filter(kind = cls.KIND)
81
Scott Baker27de6012015-07-24 15:36:02 -070082 @classmethod
Scott Bakercd32ad02015-10-19 21:18:53 -070083 def get_deleted_service_objects(cls):
84 return cls.deleted_objects.filter(kind = cls.KIND)
85
86 @classmethod
Scott Baker27de6012015-07-24 15:36:02 -070087 def get_service_objects_by_user(cls, user):
88 return cls.select_by_user(user).filter(kind = cls.KIND)
89
90 @classmethod
91 def select_by_user(cls, user):
92 if user.is_admin:
93 return cls.objects.all()
94 else:
95 service_ids = [sp.slice.id for sp in ServicePrivilege.objects.filter(user=user)]
96 return cls.objects.filter(id__in=service_ids)
97
Scott Bakerbcea8cf2015-12-07 22:20:40 -080098 @property
99 def serviceattribute_dict(self):
100 attrs = {}
101 for attr in self.serviceattributes.all():
102 attrs[attr.name] = attr.value
103 return attrs
104
Siobhan Tully00353f72013-10-08 21:53:27 -0400105 def __unicode__(self): return u'%s' % (self.name)
106
Tony Mack950b4492015-04-29 12:23:10 -0400107 def can_update(self, user):
108 return user.can_update_service(self, allow=['admin'])
Scott Bakerb2385622015-07-06 14:27:31 -0700109
Scott Baker25757222015-05-11 16:36:41 -0700110 def get_scalable_nodes(self, slice, max_per_node=None, exclusive_slices=[]):
111 """
112 Get a list of nodes that can be used to scale up a slice.
113
114 slice - slice to scale up
Tony Mackd8515472015-08-19 11:58:18 -0400115 max_per_node - maximum numbers of instances that 'slice' can have on a single node
Scott Baker25757222015-05-11 16:36:41 -0700116 exclusive_slices - list of slices that must have no nodes in common with 'slice'.
117 """
118
Tony Mackd8515472015-08-19 11:58:18 -0400119 from core.models import Node, Instance # late import to get around order-of-imports constraint in __init__.py
Scott Baker25757222015-05-11 16:36:41 -0700120
121 nodes = list(Node.objects.all())
122
Tony Mackd8515472015-08-19 11:58:18 -0400123 conflicting_instances = Instance.objects.filter(slice__in = exclusive_slices)
124 conflicting_nodes = Node.objects.filter(instances__in = conflicting_instances)
Scott Baker25757222015-05-11 16:36:41 -0700125
126 nodes = [x for x in nodes if x not in conflicting_nodes]
127
Tony Mackd8515472015-08-19 11:58:18 -0400128 # If max_per_node is set, then limit the number of instances this slice
Scott Baker25757222015-05-11 16:36:41 -0700129 # can have on a single node.
130 if max_per_node:
131 acceptable_nodes = []
132 for node in nodes:
Tony Mackd8515472015-08-19 11:58:18 -0400133 existing_count = node.instances.filter(slice=slice).count()
Scott Baker25757222015-05-11 16:36:41 -0700134 if existing_count < max_per_node:
135 acceptable_nodes.append(node)
136 nodes = acceptable_nodes
137
138 return nodes
139
140 def pick_node(self, slice, max_per_node=None, exclusive_slices=[]):
141 # Pick the best node to scale up a slice.
142
143 nodes = self.get_scalable_nodes(slice, max_per_node, exclusive_slices)
Tony Mackd8515472015-08-19 11:58:18 -0400144 nodes = sorted(nodes, key=lambda node: node.instances.all().count())
Scott Baker25757222015-05-11 16:36:41 -0700145 if not nodes:
146 return None
147 return nodes[0]
148
149 def adjust_scale(self, slice_hint, scale, max_per_node=None, exclusive_slices=[]):
Tony Mackd8515472015-08-19 11:58:18 -0400150 from core.models import Instance # late import to get around order-of-imports constraint in __init__.py
Scott Baker25757222015-05-11 16:36:41 -0700151
152 slices = [x for x in self.slices.all() if slice_hint in x.name]
153 for slice in slices:
Tony Mackd8515472015-08-19 11:58:18 -0400154 while slice.instances.all().count() > scale:
155 s = slice.instances.all()[0]
156 # print "drop instance", s
Scott Baker25757222015-05-11 16:36:41 -0700157 s.delete()
158
Tony Mackd8515472015-08-19 11:58:18 -0400159 while slice.instances.all().count() < scale:
Scott Baker25757222015-05-11 16:36:41 -0700160 node = self.pick_node(slice, max_per_node, exclusive_slices)
161 if not node:
162 # no more available nodes
163 break
164
165 image = slice.default_image
166 if not image:
167 raise XOSConfigurationError("No default_image for slice %s" % slice.name)
168
169 flavor = slice.default_flavor
170 if not flavor:
171 raise XOSConfigurationError("No default_flavor for slice %s" % slice.name)
172
Tony Mackd8515472015-08-19 11:58:18 -0400173 s = Instance(slice=slice,
Scott Baker25757222015-05-11 16:36:41 -0700174 node=node,
175 creator=slice.creator,
176 image=image,
177 flavor=flavor,
178 deployment=node.site_deployment.deployment)
179 s.save()
180
Tony Mackd8515472015-08-19 11:58:18 -0400181 # print "add instance", s
Tony Mack950b4492015-04-29 12:23:10 -0400182
Scott Bakercbf4c782015-12-09 22:54:52 -0800183 def get_vtn_src_nets(self):
184 nets=[]
185 for slice in self.slices.all():
186 for ns in slice.networkslices.all():
187 if not ns.network:
188 continue
Scott Bakereb3bad92016-01-12 19:59:12 -0800189# if ns.network.template.access in ["direct", "indirect"]:
190# # skip access networks; we want to use the private network
191# continue
Scott Bakercbf4c782015-12-09 22:54:52 -0800192 if ns.network.name in ["wan_network", "lan_network"]:
193 # we don't want to attach to the vCPE's lan or wan network
194 # we only want to attach to its private network
195 # TODO: fix hard-coding of network name
196 continue
197 for cn in ns.network.controllernetworks.all():
198 if cn.net_id:
199 net = {"name": ns.network.name, "net_id": cn.net_id}
200 nets.append(net)
201 return nets
202
Scott Baker1b7d98b2015-12-08 21:31:18 -0800203 def get_vtn_nets(self):
204 nets=[]
205 for slice in self.slices.all():
206 for ns in slice.networkslices.all():
207 if not ns.network:
208 continue
Scott Bakercbf4c782015-12-09 22:54:52 -0800209 if ns.network.template.access not in ["direct", "indirect"]:
210 # skip anything that's not an access network
211 continue
Scott Baker1b7d98b2015-12-08 21:31:18 -0800212 for cn in ns.network.controllernetworks.all():
213 if cn.net_id:
214 net = {"name": ns.network.name, "net_id": cn.net_id}
215 nets.append(net)
216 return nets
217
218 def get_vtn_dependencies_nets(self):
219 provider_nets = []
Scott Baker012c54b2015-12-08 19:27:50 -0800220 for tenant in self.subscribed_tenants.all():
221 if tenant.provider_service:
Scott Baker1b7d98b2015-12-08 21:31:18 -0800222 for net in tenant.provider_service.get_vtn_nets():
223 if not net in provider_nets:
224 provider_nets.append(net)
225 return provider_nets
226
227 def get_vtn_dependencies_ids(self):
228 return [x["net_id"] for x in self.get_vtn_dependencies_nets()]
229
230 def get_vtn_dependencies_names(self):
231 return [x["name"]+"_"+x["net_id"] for x in self.get_vtn_dependencies_nets()]
232
Scott Bakercbf4c782015-12-09 22:54:52 -0800233 def get_vtn_src_ids(self):
234 return [x["net_id"] for x in self.get_vtn_src_nets()]
Scott Baker1b7d98b2015-12-08 21:31:18 -0800235
Scott Bakercbf4c782015-12-09 22:54:52 -0800236 def get_vtn_src_names(self):
237 return [x["name"]+"_"+x["net_id"] for x in self.get_vtn_src_nets()]
Scott Baker012c54b2015-12-08 19:27:50 -0800238
239
Siobhan Tully00353f72013-10-08 21:53:27 -0400240class ServiceAttribute(PlCoreBase):
Scott Bakerbcea8cf2015-12-07 22:20:40 -0800241 name = models.CharField(help_text="Attribute Name", max_length=128)
Tony Mack50e12212015-03-09 13:03:56 -0400242 value = StrippedCharField(help_text="Attribute Value", max_length=1024)
Siobhan Tully00353f72013-10-08 21:53:27 -0400243 service = models.ForeignKey(Service, related_name='serviceattributes', help_text="The Service this attribute is associated with")
244
Tony Mack950b4492015-04-29 12:23:10 -0400245class ServiceRole(PlCoreBase):
246 ROLE_CHOICES = (('admin','Admin'),)
247 role = StrippedCharField(choices=ROLE_CHOICES, unique=True, max_length=30)
248
249 def __unicode__(self): return u'%s' % (self.role)
250
251class ServicePrivilege(PlCoreBase):
252 user = models.ForeignKey('User', related_name='serviceprivileges')
253 service = models.ForeignKey('Service', related_name='serviceprivileges')
254 role = models.ForeignKey('ServiceRole',related_name='serviceprivileges')
255
256 class Meta:
Tony Mack02683de2015-05-13 12:21:28 -0400257 unique_together = ('user', 'service', 'role')
Tony Mack950b4492015-04-29 12:23:10 -0400258
259 def __unicode__(self): return u'%s %s %s' % (self.service, self.user, self.role)
260
261 def can_update(self, user):
262 if not self.service.enabled:
263 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
264 return self.service.can_update(user)
265
266 def save(self, *args, **kwds):
267 if not self.service.enabled:
268 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
269 super(ServicePrivilege, self).save(*args, **kwds)
270
271 def delete(self, *args, **kwds):
272 if not self.service.enabled:
273 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
Scott Bakera86489f2015-07-01 18:29:08 -0700274 super(ServicePrivilege, self).delete(*args, **kwds)
275
Scott Baker27de6012015-07-24 15:36:02 -0700276 @classmethod
277 def select_by_user(cls, user):
Tony Mack950b4492015-04-29 12:23:10 -0400278 if user.is_admin:
Scott Baker27de6012015-07-24 15:36:02 -0700279 qs = cls.objects.all()
Tony Mack950b4492015-04-29 12:23:10 -0400280 else:
Scott Baker27de6012015-07-24 15:36:02 -0700281 qs = cls.objects.filter(user=user)
Scott Bakera86489f2015-07-01 18:29:08 -0700282 return qs
283
Scott Baker9d1c6d92015-07-13 13:07:27 -0700284class TenantRoot(PlCoreBase, AttributeMixin):
Scott Bakera86489f2015-07-01 18:29:08 -0700285 """ A tenantRoot is one of the things that can sit at the root of a chain
286 of tenancy. This object represents a node.
287 """
288
289 KIND= "generic"
290 kind = StrippedCharField(max_length=30, default=KIND)
Scott Bakerb2385622015-07-06 14:27:31 -0700291 name = StrippedCharField(max_length=255, help_text="name", blank=True, null=True)
Scott Bakera86489f2015-07-01 18:29:08 -0700292
Scott Baker29415a82015-07-07 12:12:42 -0700293 service_specific_attribute = models.TextField(blank=True, null=True)
294 service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
Scott Bakera86489f2015-07-01 18:29:08 -0700295
Scott Baker126ad472015-07-07 17:59:44 -0700296 def __init__(self, *args, **kwargs):
297 # for subclasses, set the default kind appropriately
298 self._meta.get_field("kind").default = self.KIND
299 super(TenantRoot, self).__init__(*args, **kwargs)
300
Scott Bakerb2385622015-07-06 14:27:31 -0700301 def __unicode__(self):
302 if not self.name:
303 return u"%s-tenant_root-#%s" % (str(self.kind), str(self.id))
304 else:
305 return self.name
306
307 def can_update(self, user):
308 return user.can_update_tenant_root(self, allow=['admin'])
309
Scott Baker29415a82015-07-07 12:12:42 -0700310 def get_subscribed_tenants(self, tenant_class):
311 ids = self.subscribed_tenants.filter(kind=tenant_class.KIND)
312 return tenant_class.objects.filter(id__in = ids)
313
314 def get_newest_subscribed_tenant(self, kind):
315 st = list(self.get_subscribed_tenants(kind))
316 if not st:
317 return None
318 return sorted(st, key=attrgetter('id'))[0]
319
320 @classmethod
321 def get_tenant_objects(cls):
322 return cls.objects.filter(kind = cls.KIND)
323
Scott Baker27de6012015-07-24 15:36:02 -0700324 @classmethod
325 def get_tenant_objects_by_user(cls, user):
326 return cls.select_by_user(user).filter(kind = cls.KIND)
327
328 @classmethod
329 def select_by_user(cls, user):
330 if user.is_admin:
331 return cls.objects.all()
332 else:
333 tr_ids = [trp.tenant_root.id for trp in TenantRootPrivilege.objects.filter(user=user)]
334 return cls.objects.filter(id__in=tr_ids)
335
Scott Baker9d1c6d92015-07-13 13:07:27 -0700336class Tenant(PlCoreBase, AttributeMixin):
Scott Baker91e85882015-04-10 16:42:26 -0700337 """ A tenant is a relationship between two entities, a subscriber and a
Scott Bakera86489f2015-07-01 18:29:08 -0700338 provider. This object represents an edge.
Scott Baker91e85882015-04-10 16:42:26 -0700339
340 The subscriber can be a User, a Service, or a Tenant.
341
342 The provider is always a Service.
Scott Bakera86489f2015-07-01 18:29:08 -0700343
344 TODO: rename "Tenant" to "Tenancy"
Scott Baker91e85882015-04-10 16:42:26 -0700345 """
Scott Baker0d306722015-04-15 20:58:20 -0700346
Scott Bakeref58a842015-04-26 20:30:40 -0700347 CONNECTIVITY_CHOICES = (('public', 'Public'), ('private', 'Private'), ('na', 'Not Applicable'))
348
Scott Baker0d306722015-04-15 20:58:20 -0700349 # when subclassing a service, redefine KIND to describe the new service
350 KIND = "generic"
351
352 kind = StrippedCharField(max_length=30, default=KIND)
Scott Bakera86489f2015-07-01 18:29:08 -0700353 provider_service = models.ForeignKey(Service, related_name='provided_tenants')
354
355 # The next four things are the various type of objects that can be subscribers of this Tenancy
356 # relationship. One and only one can be used at a time.
357 subscriber_service = models.ForeignKey(Service, related_name='subscribed_tenants', blank=True, null=True)
358 subscriber_tenant = models.ForeignKey("Tenant", related_name='subscribed_tenants', blank=True, null=True)
359 subscriber_user = models.ForeignKey("User", related_name='subscribed_tenants', blank=True, null=True)
360 subscriber_root = models.ForeignKey("TenantRoot", related_name="subscribed_tenants", blank=True, null=True)
361
362 # Service_specific_attribute and service_specific_id are opaque to XOS
Scott Baker1b7c6f12015-05-06 19:49:31 -0700363 service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
364 service_specific_attribute = models.TextField(blank=True, null=True)
Scott Bakera86489f2015-07-01 18:29:08 -0700365
366 # Connect_method is only used by Coarse tenants
Scott Bakeref58a842015-04-26 20:30:40 -0700367 connect_method = models.CharField(null=False, blank=False, max_length=30, choices=CONNECTIVITY_CHOICES, default="na")
Scott Baker91e85882015-04-10 16:42:26 -0700368
Scott Baker0d306722015-04-15 20:58:20 -0700369 def __init__(self, *args, **kwargs):
370 # for subclasses, set the default kind appropriately
371 self._meta.get_field("kind").default = self.KIND
372 super(Tenant, self).__init__(*args, **kwargs)
373
Scott Baker91e85882015-04-10 16:42:26 -0700374 def __unicode__(self):
Scott Bakerfe91f622015-05-20 20:42:04 -0700375 return u"%s-tenant-%s" % (str(self.kind), str(self.id))
Scott Baker91e85882015-04-10 16:42:26 -0700376
Scott Baker0d306722015-04-15 20:58:20 -0700377 @classmethod
378 def get_tenant_objects(cls):
379 return cls.objects.filter(kind = cls.KIND)
380
Scott Bakereb50ee32015-05-05 17:52:03 -0700381 @classmethod
Scott Baker27de6012015-07-24 15:36:02 -0700382 def get_tenant_objects_by_user(cls, user):
383 return cls.select_by_user(user).filter(kind = cls.KIND)
384
385 @classmethod
Scott Bakereb50ee32015-05-05 17:52:03 -0700386 def get_deleted_tenant_objects(cls):
387 return cls.deleted_objects.filter(kind = cls.KIND)
388
Scott Bakerbcea8cf2015-12-07 22:20:40 -0800389 @property
390 def tenantattribute_dict(self):
391 attrs = {}
392 for attr in self.tenantattributes.all():
393 attrs[attr.name] = attr.value
394 return attrs
395
Scott Baker7f8ef8f2015-04-20 14:24:29 -0700396 # helper function to be used in subclasses that want to ensure service_specific_id is unique
397 def validate_unique_service_specific_id(self):
398 if self.pk is None:
399 if self.service_specific_id is None:
400 raise XOSMissingField("subscriber_specific_id is None, and it's a required field", fields={"service_specific_id": "cannot be none"})
401
402 conflicts = self.get_tenant_objects().filter(service_specific_id=self.service_specific_id)
403 if conflicts:
404 raise XOSDuplicateKey("service_specific_id %s already exists" % self.service_specific_id, fields={"service_specific_id": "duplicate key"})
405
Scott Bakerb2385622015-07-06 14:27:31 -0700406 def save(self, *args, **kwargs):
407 subCount = sum( [1 for e in [self.subscriber_service, self.subscriber_tenant, self.subscriber_user, self.subscriber_root] if e is not None])
408 if (subCount > 1):
409 raise XOSConflictingField("Only one of subscriber_service, subscriber_tenant, subscriber_user, subscriber_root should be set")
410
411 super(Tenant, self).save(*args, **kwargs)
412
413 def get_subscribed_tenants(self, tenant_class):
414 ids = self.subscribed_tenants.filter(kind=tenant_class.KIND)
415 return tenant_class.objects.filter(id__in = ids)
416
417 def get_newest_subscribed_tenant(self, kind):
418 st = list(self.get_subscribed_tenants(kind))
419 if not st:
420 return None
421 return sorted(st, key=attrgetter('id'))[0]
422
Scott Bakerc8914bf2015-11-18 20:58:08 -0800423class Scheduler(object):
424 # XOS Scheduler Abstract Base Class
425 # Used to implement schedulers that pick which node to put instances on
426
427 def __init__(self, slice):
428 self.slice = slice
429
430 def pick(self):
431 # this method should return a tuple (node, parent)
432 # node is the node to instantiate on
433 # parent is for container_vm instances only, and is the VM that will
434 # hold the container
435
436 raise Exception("Abstract Base")
437
438class LeastLoadedNodeScheduler(Scheduler):
439 # This scheduler always return the node with the fewest number of instances.
440
Scott Baker67074ab2016-03-04 11:29:02 -0800441 def __init__(self, slice, label=None):
Scott Bakerc8914bf2015-11-18 20:58:08 -0800442 super(LeastLoadedNodeScheduler, self).__init__(slice)
Scott Baker67074ab2016-03-04 11:29:02 -0800443 self.label = label
Scott Bakerc8914bf2015-11-18 20:58:08 -0800444
445 def pick(self):
446 from core.models import Node
Scott Baker67074ab2016-03-04 11:29:02 -0800447 nodes = Node.objects.all()
448
449 if self.label:
450 nodes = nodes.filter(labels__name=self.label)
451
452 nodes = list(nodes)
453
454 if not nodes:
455 raise Exception("LeastLoadedNodeScheduler: No suitable nodes to pick from")
Scott Bakerbcea8cf2015-12-07 22:20:40 -0800456
Scott Bakerc8914bf2015-11-18 20:58:08 -0800457 # TODO: logic to filter nodes by which nodes are up, and which
458 # nodes the slice can instantiate on.
459 nodes = sorted(nodes, key=lambda node: node.instances.all().count())
460 return [nodes[0], None]
461
462class ContainerVmScheduler(Scheduler):
463 # This scheduler picks a VM in the slice with the fewest containers inside
464 # of it. If no VMs are suitable, then it creates a VM.
465
466 # this is a hack and should be replaced by something smarter...
467 LOOK_FOR_IMAGES=["ubuntu-vcpe4", # ONOS demo machine -- preferred vcpe image
468 "Ubuntu 14.04 LTS", # portal
469 "Ubuntu-14.04-LTS", # ONOS demo machine
470 "trusty-server-multi-nic", # CloudLab
471 ]
472
473 MAX_VM_PER_CONTAINER = 10
474
475 def __init__(self, slice):
476 super(ContainerVmScheduler, self).__init__(slice)
477
478 @property
479 def image(self):
480 from core.models import Image
481
482 look_for_images = self.LOOK_FOR_IMAGES
483 for image_name in look_for_images:
484 images = Image.objects.filter(name = image_name)
485 if images:
486 return images[0]
487
488 raise XOSProgrammingError("No ContainerVM image (looked for %s)" % str(look_for_images))
489
490 def make_new_instance(self):
491 from core.models import Instance, Flavor
492
493 flavors = Flavor.objects.filter(name="m1.small")
494 if not flavors:
495 raise XOSConfigurationError("No m1.small flavor")
496
497 (node,parent) = LeastLoadedNodeScheduler(self.slice).pick()
498
499 instance = Instance(slice = self.slice,
500 node = node,
501 image = self.image,
502 creator = self.slice.creator,
503 deployment = node.site_deployment.deployment,
504 flavor = flavors[0],
505 isolation = "vm",
506 parent = parent)
507 instance.save()
508 # We rely on a special naming convention to identify the VMs that will
509 # hole containers.
510 instance.name = "%s-outer-%s" % (instance.slice.name, instance.id)
511 instance.save()
512 return instance
513
514 def pick(self):
515 from core.models import Instance, Flavor
516
517 for vm in self.slice.instances.filter(isolation="vm"):
518 avail_vms = []
519 if (vm.name.startswith("%s-outer-" % self.slice.name)):
520 container_count = Instance.objects.filter(parent=vm).count()
521 if (container_count < self.MAX_VM_PER_CONTAINER):
522 avail_vms.append( (vm, container_count) )
523 # sort by least containers-per-vm
524 avail_vms = sorted(avail_vms, key = lambda x: x[1])
525 print "XXX", avail_vms
526 if avail_vms:
527 instance = avail_vms[0][0]
528 return (instance.node, instance)
529
530 instance = self.make_new_instance()
531 return (instance.node, instance)
532
Scott Bakerc1584b82015-09-09 16:36:06 -0700533class TenantWithContainer(Tenant):
534 """ A tenant that manages a container """
535
536 # this is a hack and should be replaced by something smarter...
Scott Bakerf05c4972015-09-09 16:43:39 -0700537 LOOK_FOR_IMAGES=["ubuntu-vcpe4", # ONOS demo machine -- preferred vcpe image
Scott Bakerc1584b82015-09-09 16:36:06 -0700538 "Ubuntu 14.04 LTS", # portal
539 "Ubuntu-14.04-LTS", # ONOS demo machine
Scott Bakerf05c4972015-09-09 16:43:39 -0700540 "trusty-server-multi-nic", # CloudLab
Scott Bakerc1584b82015-09-09 16:36:06 -0700541 ]
542
Scott Baker5e505a52015-12-14 10:21:53 -0800543 LOOK_FOR_CONTAINER_IMAGES=["docker-vcpe"]
Scott Bakera759fe32015-11-16 22:51:02 -0800544
Scott Bakerc1584b82015-09-09 16:36:06 -0700545 class Meta:
546 proxy = True
547
548 def __init__(self, *args, **kwargs):
549 super(TenantWithContainer, self).__init__(*args, **kwargs)
Tony Mack32010062015-09-13 22:50:39 +0000550 self.cached_instance=None
551 self.orig_instance_id = self.get_initial_attribute("instance_id")
Scott Baker5c125e42015-11-02 20:54:28 -0800552
Scott Bakerc1584b82015-09-09 16:36:06 -0700553 @property
Tony Mack32010062015-09-13 22:50:39 +0000554 def instance(self):
555 from core.models import Instance
556 if getattr(self, "cached_instance", None):
557 return self.cached_instance
558 instance_id=self.get_attribute("instance_id")
Srikanth Vavilapalli17b5a3c2015-11-17 12:21:02 -0600559 if not instance_id:
560 return None
561 instances=Instance.objects.filter(id=instance_id)
562 if not instances:
563 return None
564 instance=instances[0]
Tony Mack32010062015-09-13 22:50:39 +0000565 instance.caller = self.creator
566 self.cached_instance = instance
567 return instance
Scott Bakerc1584b82015-09-09 16:36:06 -0700568
Tony Mack32010062015-09-13 22:50:39 +0000569 @instance.setter
570 def instance(self, value):
Scott Bakerc1584b82015-09-09 16:36:06 -0700571 if value:
572 value = value.id
Tony Mack32010062015-09-13 22:50:39 +0000573 if (value != self.get_attribute("instance_id", None)):
574 self.cached_instance=None
575 self.set_attribute("instance_id", value)
Scott Bakerc1584b82015-09-09 16:36:06 -0700576
Scott Baker5c125e42015-11-02 20:54:28 -0800577 @property
Scott Baker268e2aa2016-02-10 12:23:53 -0800578 def external_hostname(self):
579 return self.get_attribute("external_hostname", "")
580
581 @external_hostname.setter
582 def external_hostname(self, value):
583 self.set_attribute("external_hostname", value)
584
585 @property
586 def external_container(self):
587 return self.get_attribute("external_container", "")
588
589 @external_container.setter
590 def external_container(self, value):
591 self.set_attribute("external_container", value)
592
593 @property
Scott Bakerc1584b82015-09-09 16:36:06 -0700594 def creator(self):
595 from core.models import User
596 if getattr(self, "cached_creator", None):
597 return self.cached_creator
598 creator_id=self.get_attribute("creator_id")
599 if not creator_id:
600 return None
601 users=User.objects.filter(id=creator_id)
602 if not users:
603 return None
604 user=users[0]
605 self.cached_creator = users[0]
606 return user
607
608 @creator.setter
609 def creator(self, value):
610 if value:
611 value = value.id
612 if (value != self.get_attribute("creator_id", None)):
613 self.cached_creator=None
614 self.set_attribute("creator_id", value)
615
616 @property
617 def image(self):
618 from core.models import Image
619 # Implement the logic here to pick the image that should be used when
620 # instantiating the VM that will hold the container.
Scott Bakera759fe32015-11-16 22:51:02 -0800621
622 slice = self.provider_service.slices.all()
623 if not slice:
624 raise XOSProgrammingError("provider service has no slice")
625 slice = slice[0]
626
627 if slice.default_isolation in ["container", "container_vm"]:
628 look_for_images = self.LOOK_FOR_CONTAINER_IMAGES
629 else:
630 look_for_images = self.LOOK_FOR_IMAGES
631
632 for image_name in look_for_images:
Scott Bakerc1584b82015-09-09 16:36:06 -0700633 images = Image.objects.filter(name = image_name)
634 if images:
635 return images[0]
636
Scott Bakerc8914bf2015-11-18 20:58:08 -0800637 raise XOSProgrammingError("No VPCE image (looked for %s)" % str(look_for_images))
Scott Baker5c125e42015-11-02 20:54:28 -0800638
Scott Bakera759fe32015-11-16 22:51:02 -0800639 def save_instance(self, instance):
640 # Override this function to do custom pre-save or post-save processing,
641 # such as creating ports for containers.
642 instance.save()
Scott Baker5c125e42015-11-02 20:54:28 -0800643
Srikanth Vavilapalli17b5a3c2015-11-17 12:21:02 -0600644 def pick_least_loaded_instance_in_slice(self, slices):
645 for slice in slices:
646 if slice.instances.all().count() > 0:
647 for instance in slice.instances.all():
Jeremy Mowery752dc502016-03-14 23:59:11 -0700648 #Pick the first instance that has lesser than 5 tenants
Srikanth Vavilapalli17b5a3c2015-11-17 12:21:02 -0600649 if self.count_of_tenants_of_an_instance(instance) < 5:
650 return instance
651 return None
652
Jeremy Mowery752dc502016-03-14 23:59:11 -0700653 #TODO: Ideally the tenant count for an instance should be maintained using a
654 #many-to-one relationship attribute, however this model being proxy, it does
655 #not permit any new attributes to be defined. Find if any better solutions
Srikanth Vavilapalli17b5a3c2015-11-17 12:21:02 -0600656 def count_of_tenants_of_an_instance(self, instance):
657 tenant_count = 0
658 for tenant in self.get_tenant_objects().all():
659 if tenant.get_attribute("instance_id", None) == instance.id:
660 tenant_count += 1
661 return tenant_count
662
Scott Bakera759fe32015-11-16 22:51:02 -0800663 def manage_container(self):
Tony Mack32010062015-09-13 22:50:39 +0000664 from core.models import Instance, Flavor
Scott Bakerc1584b82015-09-09 16:36:06 -0700665
666 if self.deleted:
667 return
668
Tony Mack32010062015-09-13 22:50:39 +0000669 if (self.instance is not None) and (self.instance.image != self.image):
670 self.instance.delete()
671 self.instance = None
Scott Bakerc1584b82015-09-09 16:36:06 -0700672
Tony Mack32010062015-09-13 22:50:39 +0000673 if self.instance is None:
Scott Bakerc1584b82015-09-09 16:36:06 -0700674 if not self.provider_service.slices.count():
Srikanth Vavilapalli17b5a3c2015-11-17 12:21:02 -0600675 raise XOSConfigurationError("The service has no slices")
Scott Bakerc1584b82015-09-09 16:36:06 -0700676
Srikanth Vavilapalli17b5a3c2015-11-17 12:21:02 -0600677 new_instance_created = False
678 instance = None
679 if self.get_attribute("use_same_instance_for_multiple_tenants", default=False):
680 #Find if any existing instances can be used for this tenant
681 slices = self.provider_service.slices.all()
682 instance = self.pick_least_loaded_instance_in_slice(slices)
Scott Bakerc1584b82015-09-09 16:36:06 -0700683
Srikanth Vavilapalli17b5a3c2015-11-17 12:21:02 -0600684 if not instance:
Srikanth Vavilapalli3406fbb2015-11-17 13:41:38 -0600685 slice = self.provider_service.slices.all()[0]
Scott Bakera759fe32015-11-16 22:51:02 -0800686
Srikanth Vavilapalli204a5c42016-02-15 01:18:09 -0500687 flavor = slice.default_flavor
688 if not flavor:
689 flavors = Flavor.objects.filter(name="m1.small")
690 if not flavors:
691 raise XOSConfigurationError("No m1.small flavor")
692 flavor = flavors[0]
693
Srikanth Vavilapalli3406fbb2015-11-17 13:41:38 -0600694 if slice.default_isolation == "container_vm":
Scott Bakerc8914bf2015-11-18 20:58:08 -0800695 (node, parent) = ContainerVmScheduler(slice).pick()
Srikanth Vavilapalli3406fbb2015-11-17 13:41:38 -0600696 else:
Scott Bakerc8914bf2015-11-18 20:58:08 -0800697 (node, parent) = LeastLoadedNodeScheduler(slice).pick()
Scott Bakera759fe32015-11-16 22:51:02 -0800698
Srikanth Vavilapalli3406fbb2015-11-17 13:41:38 -0600699 instance = Instance(slice = slice,
Srikanth Vavilapalli17b5a3c2015-11-17 12:21:02 -0600700 node = node,
701 image = self.image,
702 creator = self.creator,
703 deployment = node.site_deployment.deployment,
Srikanth Vavilapalli204a5c42016-02-15 01:18:09 -0500704 flavor = flavor,
Srikanth Vavilapalli3406fbb2015-11-17 13:41:38 -0600705 isolation = slice.default_isolation,
706 parent = parent)
707 self.save_instance(instance)
Srikanth Vavilapalli17b5a3c2015-11-17 12:21:02 -0600708 new_instance_created = True
Scott Bakerc1584b82015-09-09 16:36:06 -0700709
710 try:
Tony Mack32010062015-09-13 22:50:39 +0000711 self.instance = instance
Scott Bakerc1584b82015-09-09 16:36:06 -0700712 super(TenantWithContainer, self).save()
713 except:
Srikanth Vavilapalli17b5a3c2015-11-17 12:21:02 -0600714 if new_instance_created:
715 instance.delete()
Scott Bakerc1584b82015-09-09 16:36:06 -0700716 raise
717
718 def cleanup_container(self):
Tony Mack32010062015-09-13 22:50:39 +0000719 if self.instance:
Srikanth Vavilapalli17b5a3c2015-11-17 12:21:02 -0600720 if self.get_attribute("use_same_instance_for_multiple_tenants", default=False):
721 #Delete the instance only if this is last tenant in that instance
722 tenant_count = self.count_of_tenants_of_an_instance(self.instance)
723 if tenant_count == 0:
724 self.instance.delete()
725 else:
726 self.instance.delete()
Tony Mack32010062015-09-13 22:50:39 +0000727 self.instance = None
Scott Bakerb2385622015-07-06 14:27:31 -0700728
Scott Baker88fa6732015-12-10 23:23:07 -0800729 def save(self, *args, **kwargs):
730 if (not self.creator) and (hasattr(self, "caller")) and (self.caller):
731 self.creator = self.caller
732 super(TenantWithContainer, self).save(*args, **kwargs)
733
Scott Bakeref58a842015-04-26 20:30:40 -0700734class CoarseTenant(Tenant):
Scott Bakera86489f2015-07-01 18:29:08 -0700735 """ TODO: rename "CoarseTenant" --> "StaticTenant" """
Scott Bakeref58a842015-04-26 20:30:40 -0700736 class Meta:
737 proxy = True
Siobhan Tully00353f72013-10-08 21:53:27 -0400738
Scott Bakerc24f86d2015-08-14 09:10:11 -0700739 KIND = COARSE_KIND
Scott Bakeref58a842015-04-26 20:30:40 -0700740
741 def save(self, *args, **kwargs):
742 if (not self.subscriber_service):
743 raise XOSValidationError("subscriber_service cannot be null")
744 if (self.subscriber_tenant or self.subscriber_user):
745 raise XOSValidationError("subscriber_tenant and subscriber_user must be null")
746
747 super(CoarseTenant,self).save()
Scott Bakera86489f2015-07-01 18:29:08 -0700748
749class Subscriber(TenantRoot):
750 """ Intermediate class for TenantRoots that are to be Subscribers """
751
752 class Meta:
753 proxy = True
754
755 KIND = "Subscriber"
756
757class Provider(TenantRoot):
758 """ Intermediate class for TenantRoots that are to be Providers """
759
760 class Meta:
761 proxy = True
762
763 KIND = "Provider"
764
Scott Baker1e7e3482015-10-15 15:59:19 -0700765class TenantAttribute(PlCoreBase):
Scott Baker3ab4db82015-10-20 17:12:36 -0700766 name = models.CharField(help_text="Attribute Name", max_length=128)
Scott Baker1e7e3482015-10-15 15:59:19 -0700767 value = models.TextField(help_text="Attribute Value")
768 tenant = models.ForeignKey(Tenant, related_name='tenantattributes', help_text="The Tenant this attribute is associated with")
769
Scott Bakera86489f2015-07-01 18:29:08 -0700770class TenantRootRole(PlCoreBase):
Scott Baker1729e342015-07-24 15:48:03 -0700771 ROLE_CHOICES = (('admin','Admin'), ('access','Access'))
Scott Bakera86489f2015-07-01 18:29:08 -0700772
773 role = StrippedCharField(choices=ROLE_CHOICES, unique=True, max_length=30)
774
775 def __unicode__(self): return u'%s' % (self.role)
776
777class TenantRootPrivilege(PlCoreBase):
778 user = models.ForeignKey('User', related_name="tenant_root_privileges")
779 tenant_root = models.ForeignKey('TenantRoot', related_name="tenant_root_privileges")
780 role = models.ForeignKey('TenantRootRole', related_name="tenant_root_privileges")
781
782 class Meta:
783 unique_together = ('user', 'tenant_root', 'role')
784
Scott Bakerb2385622015-07-06 14:27:31 -0700785 def __unicode__(self): return u'%s %s %s' % (self.tenant_root, self.user, self.role)
Scott Bakera86489f2015-07-01 18:29:08 -0700786
787 def save(self, *args, **kwds):
788 if not self.user.is_active:
789 raise PermissionDenied, "Cannot modify role(s) of a disabled user"
Scott Bakerc8e947a2015-07-24 10:15:31 -0700790 super(TenantRootPrivilege, self).save(*args, **kwds)
Scott Bakera86489f2015-07-01 18:29:08 -0700791
792 def can_update(self, user):
Scott Bakerc8e947a2015-07-24 10:15:31 -0700793 return user.can_update_tenant_root_privilege(self)
Scott Bakera86489f2015-07-01 18:29:08 -0700794
Scott Baker27de6012015-07-24 15:36:02 -0700795 @classmethod
796 def select_by_user(cls, user):
Scott Bakera86489f2015-07-01 18:29:08 -0700797 if user.is_admin:
Scott Baker1729e342015-07-24 15:48:03 -0700798 return cls.objects.all()
Scott Bakera86489f2015-07-01 18:29:08 -0700799 else:
Scott Baker1729e342015-07-24 15:48:03 -0700800 # User can see his own privilege
801 trp_ids = [trp.id for trp in cls.objects.filter(user=user)]
802
803 # A slice admin can see the SlicePrivileges for his Slice
804 for priv in cls.objects.filter(user=user, role__role="admin"):
805 trp_ids.extend( [trp.id for trp in cls.objects.filter(tenant_root=priv.tenant_root)] )
806
807 return cls.objects.filter(id__in=trp_ids)
808
Jeremy Mowery752dc502016-03-14 23:59:11 -0700809class TenantRole(PlCoreBase):
810 ROLE_CHOICES = (('admin','Admin'), ('access','Access'))
Scott Bakerb2385622015-07-06 14:27:31 -0700811
Jeremy Mowery752dc502016-03-14 23:59:11 -0700812 role = StrippedCharField(choices=ROLE_CHOICES, unique=True, max_length=30)
813
814 def __unicode__(self): return u'%s' % (self.role)
815
816class TenantPrivilege(PlCoreBase):
Jeremy Mowerya74c31d2016-04-04 22:30:44 -0700817 user = models.ForeignKey('User', related_name="tenantprivileges")
818 tenant = models.ForeignKey('Tenant', related_name="tenantprivileges")
819 role = models.ForeignKey('TenantRole', related_name="tenantprivileges")
Jeremy Mowery752dc502016-03-14 23:59:11 -0700820
821 class Meta:
822 unique_together = ('user', 'tenant', 'role')
823
824 def __unicode__(self): return u'%s %s %s' % (self.tenant, self.user, self.role)
825
826 def save(self, *args, **kwds):
827 if not self.user.is_active:
828 raise PermissionDenied, "Cannot modify role(s) of a disabled user"
829 super(TenantPrivilege, self).save(*args, **kwds)
830
831 def can_update(self, user):
832 return user.can_update_tenant_privilege(self)
833
834 @classmethod
835 def select_by_user(cls, user):
836 if user.is_admin:
837 return cls.objects.all()
838 else:
839 # User can see his own privilege
840 trp_ids = [trp.id for trp in cls.objects.filter(user=user)]
841
842 # A tenant admin can see the TenantPrivileges for their Tenants
843 for priv in cls.objects.filter(user=user, role__role="admin"):
844 trp_ids.extend( [trp.id for trp in cls.objects.filter(tenant=priv.tenant)] )
845
846 return cls.objects.filter(id__in=trp_ids)