blob: 54160aacfa2db303824659e20ecefe68a176b011 [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 Baker82498c52015-07-13 13:07:27 -07008class AttributeMixin(object):
9 # helper for extracting things from a json-encoded service_specific_attribute
10 def get_attribute(self, name, default=None):
11 if self.service_specific_attribute:
12 attributes = json.loads(self.service_specific_attribute)
13 else:
14 attributes = {}
15 return attributes.get(name, default)
16
17 def set_attribute(self, name, value):
18 if self.service_specific_attribute:
19 attributes = json.loads(self.service_specific_attribute)
20 else:
21 attributes = {}
22 attributes[name]=value
23 self.service_specific_attribute = json.dumps(attributes)
24
25 def get_initial_attribute(self, name, default=None):
26 if self._initial["service_specific_attribute"]:
27 attributes = json.loads(self._initial["service_specific_attribute"])
28 else:
29 attributes = {}
30 return attributes.get(name, default)
31
Scott Baker74404fe2015-07-13 13:54:06 -070032 @classmethod
33 def setup_simple_attributes(cls):
34 for (attrname, default) in cls.simple_attributes:
Scott Baker096dce82015-07-13 14:27:51 -070035 setattr(cls, attrname, property(lambda self, attrname=attrname, default=default: self.get_attribute(attrname, default),
36 lambda self, value, attrname=attrname: self.set_attribute(attrname, value),
37 None,
38 attrname))
Scott Baker74404fe2015-07-13 13:54:06 -070039
Scott Baker82498c52015-07-13 13:07:27 -070040class Service(PlCoreBase, AttributeMixin):
Scott Baker008a9962015-04-15 20:58:20 -070041 # when subclassing a service, redefine KIND to describe the new service
42 KIND = "generic"
43
Siobhan Tully00353f72013-10-08 21:53:27 -040044 description = models.TextField(max_length=254,null=True, blank=True,help_text="Description of Service")
45 enabled = models.BooleanField(default=True)
Scott Baker008a9962015-04-15 20:58:20 -070046 kind = StrippedCharField(max_length=30, help_text="Kind of service", default=KIND)
Tony Mackd84b1ff2015-03-09 13:03:56 -040047 name = StrippedCharField(max_length=30, help_text="Service Name")
48 versionNumber = StrippedCharField(max_length=30, help_text="Version of Service Definition")
Siobhan Tullycf04fb62014-01-11 11:25:57 -050049 published = models.BooleanField(default=True)
Tony Mackd84b1ff2015-03-09 13:03:56 -040050 view_url = StrippedCharField(blank=True, null=True, max_length=1024)
51 icon_url = StrippedCharField(blank=True, null=True, max_length=1024)
Scott Baker68944742015-04-30 14:30:56 -070052 public_key = models.TextField(null=True, blank=True, max_length=1024, help_text="Public key string")
Siobhan Tully00353f72013-10-08 21:53:27 -040053
Scott Baker2f0828e2015-07-13 12:33:28 -070054 # Service_specific_attribute and service_specific_id are opaque to XOS
55 service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
56 service_specific_attribute = models.TextField(blank=True, null=True)
57
Scott Baker008a9962015-04-15 20:58:20 -070058 def __init__(self, *args, **kwargs):
59 # for subclasses, set the default kind appropriately
60 self._meta.get_field("kind").default = self.KIND
61 super(Service, self).__init__(*args, **kwargs)
62
63 @classmethod
64 def get_service_objects(cls):
65 return cls.objects.filter(kind = cls.KIND)
66
Scott Baker16573d32015-07-24 15:36:02 -070067 @classmethod
68 def get_service_objects_by_user(cls, user):
69 return cls.select_by_user(user).filter(kind = cls.KIND)
70
71 @classmethod
72 def select_by_user(cls, user):
73 if user.is_admin:
74 return cls.objects.all()
75 else:
76 service_ids = [sp.slice.id for sp in ServicePrivilege.objects.filter(user=user)]
77 return cls.objects.filter(id__in=service_ids)
78
Siobhan Tully00353f72013-10-08 21:53:27 -040079 def __unicode__(self): return u'%s' % (self.name)
80
Tony Mack9d2ea092015-04-29 12:23:10 -040081 def can_update(self, user):
82 return user.can_update_service(self, allow=['admin'])
Scott Baker618a4892015-07-06 14:27:31 -070083
Scott Baker98436732015-05-11 16:36:41 -070084 def get_scalable_nodes(self, slice, max_per_node=None, exclusive_slices=[]):
85 """
86 Get a list of nodes that can be used to scale up a slice.
87
88 slice - slice to scale up
89 max_per_node - maximum numbers of slivers that 'slice' can have on a single node
90 exclusive_slices - list of slices that must have no nodes in common with 'slice'.
91 """
92
93 from core.models import Node, Sliver # late import to get around order-of-imports constraint in __init__.py
94
95 nodes = list(Node.objects.all())
96
97 conflicting_slivers = Sliver.objects.filter(slice__in = exclusive_slices)
98 conflicting_nodes = Node.objects.filter(slivers__in = conflicting_slivers)
99
100 nodes = [x for x in nodes if x not in conflicting_nodes]
101
102 # If max_per_node is set, then limit the number of slivers this slice
103 # can have on a single node.
104 if max_per_node:
105 acceptable_nodes = []
106 for node in nodes:
107 existing_count = node.slivers.filter(slice=slice).count()
108 if existing_count < max_per_node:
109 acceptable_nodes.append(node)
110 nodes = acceptable_nodes
111
112 return nodes
113
114 def pick_node(self, slice, max_per_node=None, exclusive_slices=[]):
115 # Pick the best node to scale up a slice.
116
117 nodes = self.get_scalable_nodes(slice, max_per_node, exclusive_slices)
118 nodes = sorted(nodes, key=lambda node: node.slivers.all().count())
119 if not nodes:
120 return None
121 return nodes[0]
122
123 def adjust_scale(self, slice_hint, scale, max_per_node=None, exclusive_slices=[]):
124 from core.models import Sliver # late import to get around order-of-imports constraint in __init__.py
125
126 slices = [x for x in self.slices.all() if slice_hint in x.name]
127 for slice in slices:
128 while slice.slivers.all().count() > scale:
129 s = slice.slivers.all()[0]
130 # print "drop sliver", s
131 s.delete()
132
133 while slice.slivers.all().count() < scale:
134 node = self.pick_node(slice, max_per_node, exclusive_slices)
135 if not node:
136 # no more available nodes
137 break
138
139 image = slice.default_image
140 if not image:
141 raise XOSConfigurationError("No default_image for slice %s" % slice.name)
142
143 flavor = slice.default_flavor
144 if not flavor:
145 raise XOSConfigurationError("No default_flavor for slice %s" % slice.name)
146
147 s = Sliver(slice=slice,
148 node=node,
149 creator=slice.creator,
150 image=image,
151 flavor=flavor,
152 deployment=node.site_deployment.deployment)
153 s.save()
154
155 # print "add sliver", s
Tony Mack9d2ea092015-04-29 12:23:10 -0400156
Siobhan Tully00353f72013-10-08 21:53:27 -0400157class ServiceAttribute(PlCoreBase):
158 name = models.SlugField(help_text="Attribute Name", max_length=128)
Tony Mackd84b1ff2015-03-09 13:03:56 -0400159 value = StrippedCharField(help_text="Attribute Value", max_length=1024)
Siobhan Tully00353f72013-10-08 21:53:27 -0400160 service = models.ForeignKey(Service, related_name='serviceattributes', help_text="The Service this attribute is associated with")
161
Tony Mack9d2ea092015-04-29 12:23:10 -0400162class ServiceRole(PlCoreBase):
163 ROLE_CHOICES = (('admin','Admin'),)
164 role = StrippedCharField(choices=ROLE_CHOICES, unique=True, max_length=30)
165
166 def __unicode__(self): return u'%s' % (self.role)
167
168class ServicePrivilege(PlCoreBase):
169 user = models.ForeignKey('User', related_name='serviceprivileges')
170 service = models.ForeignKey('Service', related_name='serviceprivileges')
171 role = models.ForeignKey('ServiceRole',related_name='serviceprivileges')
172
173 class Meta:
Tony Mack9ec754e2015-05-13 12:21:28 -0400174 unique_together = ('user', 'service', 'role')
Tony Mack9d2ea092015-04-29 12:23:10 -0400175
176 def __unicode__(self): return u'%s %s %s' % (self.service, self.user, self.role)
177
178 def can_update(self, user):
179 if not self.service.enabled:
180 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
181 return self.service.can_update(user)
182
183 def save(self, *args, **kwds):
184 if not self.service.enabled:
185 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
186 super(ServicePrivilege, self).save(*args, **kwds)
187
188 def delete(self, *args, **kwds):
189 if not self.service.enabled:
190 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
Scott Baker4587b822015-07-01 18:29:08 -0700191 super(ServicePrivilege, self).delete(*args, **kwds)
192
Scott Baker16573d32015-07-24 15:36:02 -0700193 @classmethod
194 def select_by_user(cls, user):
Tony Mack9d2ea092015-04-29 12:23:10 -0400195 if user.is_admin:
Scott Baker16573d32015-07-24 15:36:02 -0700196 qs = cls.objects.all()
Tony Mack9d2ea092015-04-29 12:23:10 -0400197 else:
Scott Baker16573d32015-07-24 15:36:02 -0700198 qs = cls.objects.filter(user=user)
Scott Baker4587b822015-07-01 18:29:08 -0700199 return qs
200
Scott Baker82498c52015-07-13 13:07:27 -0700201class TenantRoot(PlCoreBase, AttributeMixin):
Scott Baker4587b822015-07-01 18:29:08 -0700202 """ A tenantRoot is one of the things that can sit at the root of a chain
203 of tenancy. This object represents a node.
204 """
205
206 KIND= "generic"
207 kind = StrippedCharField(max_length=30, default=KIND)
Scott Baker618a4892015-07-06 14:27:31 -0700208 name = StrippedCharField(max_length=255, help_text="name", blank=True, null=True)
Scott Baker4587b822015-07-01 18:29:08 -0700209
Scott Bakerefcec632015-07-07 12:12:42 -0700210 service_specific_attribute = models.TextField(blank=True, null=True)
211 service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
Scott Baker4587b822015-07-01 18:29:08 -0700212
Scott Bakerdb66fd32015-07-07 17:59:44 -0700213 def __init__(self, *args, **kwargs):
214 # for subclasses, set the default kind appropriately
215 self._meta.get_field("kind").default = self.KIND
216 super(TenantRoot, self).__init__(*args, **kwargs)
217
Scott Baker618a4892015-07-06 14:27:31 -0700218 def __unicode__(self):
219 if not self.name:
220 return u"%s-tenant_root-#%s" % (str(self.kind), str(self.id))
221 else:
222 return self.name
223
224 def can_update(self, user):
225 return user.can_update_tenant_root(self, allow=['admin'])
226
Scott Bakerefcec632015-07-07 12:12:42 -0700227 def get_subscribed_tenants(self, tenant_class):
228 ids = self.subscribed_tenants.filter(kind=tenant_class.KIND)
229 return tenant_class.objects.filter(id__in = ids)
230
231 def get_newest_subscribed_tenant(self, kind):
232 st = list(self.get_subscribed_tenants(kind))
233 if not st:
234 return None
235 return sorted(st, key=attrgetter('id'))[0]
236
237 @classmethod
238 def get_tenant_objects(cls):
239 return cls.objects.filter(kind = cls.KIND)
240
Scott Baker16573d32015-07-24 15:36:02 -0700241 @classmethod
242 def get_tenant_objects_by_user(cls, user):
243 return cls.select_by_user(user).filter(kind = cls.KIND)
244
245 @classmethod
246 def select_by_user(cls, user):
247 if user.is_admin:
248 return cls.objects.all()
249 else:
250 tr_ids = [trp.tenant_root.id for trp in TenantRootPrivilege.objects.filter(user=user)]
251 return cls.objects.filter(id__in=tr_ids)
252
Scott Baker82498c52015-07-13 13:07:27 -0700253class Tenant(PlCoreBase, AttributeMixin):
Scott Baker8103d0f2015-04-10 16:42:26 -0700254 """ A tenant is a relationship between two entities, a subscriber and a
Scott Baker4587b822015-07-01 18:29:08 -0700255 provider. This object represents an edge.
Scott Baker8103d0f2015-04-10 16:42:26 -0700256
257 The subscriber can be a User, a Service, or a Tenant.
258
259 The provider is always a Service.
Scott Baker4587b822015-07-01 18:29:08 -0700260
261 TODO: rename "Tenant" to "Tenancy"
Scott Baker8103d0f2015-04-10 16:42:26 -0700262 """
Scott Baker008a9962015-04-15 20:58:20 -0700263
Scott Baker925a8fa2015-04-26 20:30:40 -0700264 CONNECTIVITY_CHOICES = (('public', 'Public'), ('private', 'Private'), ('na', 'Not Applicable'))
265
Scott Baker008a9962015-04-15 20:58:20 -0700266 # when subclassing a service, redefine KIND to describe the new service
267 KIND = "generic"
268
269 kind = StrippedCharField(max_length=30, default=KIND)
Scott Baker4587b822015-07-01 18:29:08 -0700270 provider_service = models.ForeignKey(Service, related_name='provided_tenants')
271
272 # The next four things are the various type of objects that can be subscribers of this Tenancy
273 # relationship. One and only one can be used at a time.
274 subscriber_service = models.ForeignKey(Service, related_name='subscribed_tenants', blank=True, null=True)
275 subscriber_tenant = models.ForeignKey("Tenant", related_name='subscribed_tenants', blank=True, null=True)
276 subscriber_user = models.ForeignKey("User", related_name='subscribed_tenants', blank=True, null=True)
277 subscriber_root = models.ForeignKey("TenantRoot", related_name="subscribed_tenants", blank=True, null=True)
278
279 # Service_specific_attribute and service_specific_id are opaque to XOS
Scott Baker76934d82015-05-06 19:49:31 -0700280 service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
281 service_specific_attribute = models.TextField(blank=True, null=True)
Scott Baker4587b822015-07-01 18:29:08 -0700282
283 # Connect_method is only used by Coarse tenants
Scott Baker925a8fa2015-04-26 20:30:40 -0700284 connect_method = models.CharField(null=False, blank=False, max_length=30, choices=CONNECTIVITY_CHOICES, default="na")
Scott Baker8103d0f2015-04-10 16:42:26 -0700285
Scott Baker008a9962015-04-15 20:58:20 -0700286 def __init__(self, *args, **kwargs):
287 # for subclasses, set the default kind appropriately
288 self._meta.get_field("kind").default = self.KIND
289 super(Tenant, self).__init__(*args, **kwargs)
290
Scott Baker8103d0f2015-04-10 16:42:26 -0700291 def __unicode__(self):
Scott Bakerf996b762015-05-20 20:42:04 -0700292 return u"%s-tenant-%s" % (str(self.kind), str(self.id))
Scott Baker8103d0f2015-04-10 16:42:26 -0700293
Scott Baker008a9962015-04-15 20:58:20 -0700294 @classmethod
295 def get_tenant_objects(cls):
296 return cls.objects.filter(kind = cls.KIND)
297
Scott Bakere7fc9f52015-05-05 17:52:03 -0700298 @classmethod
Scott Baker16573d32015-07-24 15:36:02 -0700299 def get_tenant_objects_by_user(cls, user):
300 return cls.select_by_user(user).filter(kind = cls.KIND)
301
302 @classmethod
Scott Bakere7fc9f52015-05-05 17:52:03 -0700303 def get_deleted_tenant_objects(cls):
304 return cls.deleted_objects.filter(kind = cls.KIND)
305
Scott Bakerd921e1c2015-04-20 14:24:29 -0700306 # helper function to be used in subclasses that want to ensure service_specific_id is unique
307 def validate_unique_service_specific_id(self):
308 if self.pk is None:
309 if self.service_specific_id is None:
310 raise XOSMissingField("subscriber_specific_id is None, and it's a required field", fields={"service_specific_id": "cannot be none"})
311
312 conflicts = self.get_tenant_objects().filter(service_specific_id=self.service_specific_id)
313 if conflicts:
314 raise XOSDuplicateKey("service_specific_id %s already exists" % self.service_specific_id, fields={"service_specific_id": "duplicate key"})
315
Scott Baker618a4892015-07-06 14:27:31 -0700316 def save(self, *args, **kwargs):
317 subCount = sum( [1 for e in [self.subscriber_service, self.subscriber_tenant, self.subscriber_user, self.subscriber_root] if e is not None])
318 if (subCount > 1):
319 raise XOSConflictingField("Only one of subscriber_service, subscriber_tenant, subscriber_user, subscriber_root should be set")
320
321 super(Tenant, self).save(*args, **kwargs)
322
323 def get_subscribed_tenants(self, tenant_class):
324 ids = self.subscribed_tenants.filter(kind=tenant_class.KIND)
325 return tenant_class.objects.filter(id__in = ids)
326
327 def get_newest_subscribed_tenant(self, kind):
328 st = list(self.get_subscribed_tenants(kind))
329 if not st:
330 return None
331 return sorted(st, key=attrgetter('id'))[0]
332
333
Scott Baker925a8fa2015-04-26 20:30:40 -0700334class CoarseTenant(Tenant):
Scott Baker4587b822015-07-01 18:29:08 -0700335 """ TODO: rename "CoarseTenant" --> "StaticTenant" """
Scott Baker925a8fa2015-04-26 20:30:40 -0700336 class Meta:
337 proxy = True
Siobhan Tully00353f72013-10-08 21:53:27 -0400338
Scott Baker925a8fa2015-04-26 20:30:40 -0700339 KIND = "coarse"
340
341 def save(self, *args, **kwargs):
342 if (not self.subscriber_service):
343 raise XOSValidationError("subscriber_service cannot be null")
344 if (self.subscriber_tenant or self.subscriber_user):
345 raise XOSValidationError("subscriber_tenant and subscriber_user must be null")
346
347 super(CoarseTenant,self).save()
Scott Baker4587b822015-07-01 18:29:08 -0700348
349class Subscriber(TenantRoot):
350 """ Intermediate class for TenantRoots that are to be Subscribers """
351
352 class Meta:
353 proxy = True
354
355 KIND = "Subscriber"
356
357class Provider(TenantRoot):
358 """ Intermediate class for TenantRoots that are to be Providers """
359
360 class Meta:
361 proxy = True
362
363 KIND = "Provider"
364
365class TenantRootRole(PlCoreBase):
366 ROLE_CHOICES = (('admin','Admin'),)
367
368 role = StrippedCharField(choices=ROLE_CHOICES, unique=True, max_length=30)
369
370 def __unicode__(self): return u'%s' % (self.role)
371
372class TenantRootPrivilege(PlCoreBase):
373 user = models.ForeignKey('User', related_name="tenant_root_privileges")
374 tenant_root = models.ForeignKey('TenantRoot', related_name="tenant_root_privileges")
375 role = models.ForeignKey('TenantRootRole', related_name="tenant_root_privileges")
376
377 class Meta:
378 unique_together = ('user', 'tenant_root', 'role')
379
Scott Baker618a4892015-07-06 14:27:31 -0700380 def __unicode__(self): return u'%s %s %s' % (self.tenant_root, self.user, self.role)
Scott Baker4587b822015-07-01 18:29:08 -0700381
382 def save(self, *args, **kwds):
383 if not self.user.is_active:
384 raise PermissionDenied, "Cannot modify role(s) of a disabled user"
Scott Baker335882a2015-07-24 10:15:31 -0700385 super(TenantRootPrivilege, self).save(*args, **kwds)
Scott Baker4587b822015-07-01 18:29:08 -0700386
387 def can_update(self, user):
Scott Baker335882a2015-07-24 10:15:31 -0700388 return user.can_update_tenant_root_privilege(self)
Scott Baker4587b822015-07-01 18:29:08 -0700389
Scott Baker16573d32015-07-24 15:36:02 -0700390 @classmethod
391 def select_by_user(cls, user):
Scott Baker4587b822015-07-01 18:29:08 -0700392 if user.is_admin:
Scott Baker16573d32015-07-24 15:36:02 -0700393 qs = cls.objects.all()
Scott Baker4587b822015-07-01 18:29:08 -0700394 else:
Scott Baker16573d32015-07-24 15:36:02 -0700395 qs = cls.objects.filter(user=user)
Scott Baker4587b822015-07-01 18:29:08 -0700396 return qs
Scott Baker618a4892015-07-06 14:27:31 -0700397