blob: b90d76533f3a22258e17ec029566db9148a2d9f1 [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
8class Service(PlCoreBase):
Scott Baker008a9962015-04-15 20:58:20 -07009 # when subclassing a service, redefine KIND to describe the new service
10 KIND = "generic"
11
Siobhan Tully00353f72013-10-08 21:53:27 -040012 description = models.TextField(max_length=254,null=True, blank=True,help_text="Description of Service")
13 enabled = models.BooleanField(default=True)
Scott Baker008a9962015-04-15 20:58:20 -070014 kind = StrippedCharField(max_length=30, help_text="Kind of service", default=KIND)
Tony Mackd84b1ff2015-03-09 13:03:56 -040015 name = StrippedCharField(max_length=30, help_text="Service Name")
16 versionNumber = StrippedCharField(max_length=30, help_text="Version of Service Definition")
Siobhan Tullycf04fb62014-01-11 11:25:57 -050017 published = models.BooleanField(default=True)
Tony Mackd84b1ff2015-03-09 13:03:56 -040018 view_url = StrippedCharField(blank=True, null=True, max_length=1024)
19 icon_url = StrippedCharField(blank=True, null=True, max_length=1024)
Scott Baker68944742015-04-30 14:30:56 -070020 public_key = models.TextField(null=True, blank=True, max_length=1024, help_text="Public key string")
Siobhan Tully00353f72013-10-08 21:53:27 -040021
Scott Baker2f0828e2015-07-13 12:33:28 -070022 # Service_specific_attribute and service_specific_id are opaque to XOS
23 service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
24 service_specific_attribute = models.TextField(blank=True, null=True)
25
Scott Baker008a9962015-04-15 20:58:20 -070026 def __init__(self, *args, **kwargs):
27 # for subclasses, set the default kind appropriately
28 self._meta.get_field("kind").default = self.KIND
29 super(Service, self).__init__(*args, **kwargs)
30
31 @classmethod
32 def get_service_objects(cls):
33 return cls.objects.filter(kind = cls.KIND)
34
Siobhan Tully00353f72013-10-08 21:53:27 -040035 def __unicode__(self): return u'%s' % (self.name)
36
Tony Mack9d2ea092015-04-29 12:23:10 -040037 def can_update(self, user):
38 return user.can_update_service(self, allow=['admin'])
Scott Baker618a4892015-07-06 14:27:31 -070039
Scott Baker98436732015-05-11 16:36:41 -070040 def get_scalable_nodes(self, slice, max_per_node=None, exclusive_slices=[]):
41 """
42 Get a list of nodes that can be used to scale up a slice.
43
44 slice - slice to scale up
45 max_per_node - maximum numbers of slivers that 'slice' can have on a single node
46 exclusive_slices - list of slices that must have no nodes in common with 'slice'.
47 """
48
49 from core.models import Node, Sliver # late import to get around order-of-imports constraint in __init__.py
50
51 nodes = list(Node.objects.all())
52
53 conflicting_slivers = Sliver.objects.filter(slice__in = exclusive_slices)
54 conflicting_nodes = Node.objects.filter(slivers__in = conflicting_slivers)
55
56 nodes = [x for x in nodes if x not in conflicting_nodes]
57
58 # If max_per_node is set, then limit the number of slivers this slice
59 # can have on a single node.
60 if max_per_node:
61 acceptable_nodes = []
62 for node in nodes:
63 existing_count = node.slivers.filter(slice=slice).count()
64 if existing_count < max_per_node:
65 acceptable_nodes.append(node)
66 nodes = acceptable_nodes
67
68 return nodes
69
70 def pick_node(self, slice, max_per_node=None, exclusive_slices=[]):
71 # Pick the best node to scale up a slice.
72
73 nodes = self.get_scalable_nodes(slice, max_per_node, exclusive_slices)
74 nodes = sorted(nodes, key=lambda node: node.slivers.all().count())
75 if not nodes:
76 return None
77 return nodes[0]
78
79 def adjust_scale(self, slice_hint, scale, max_per_node=None, exclusive_slices=[]):
80 from core.models import Sliver # late import to get around order-of-imports constraint in __init__.py
81
82 slices = [x for x in self.slices.all() if slice_hint in x.name]
83 for slice in slices:
84 while slice.slivers.all().count() > scale:
85 s = slice.slivers.all()[0]
86 # print "drop sliver", s
87 s.delete()
88
89 while slice.slivers.all().count() < scale:
90 node = self.pick_node(slice, max_per_node, exclusive_slices)
91 if not node:
92 # no more available nodes
93 break
94
95 image = slice.default_image
96 if not image:
97 raise XOSConfigurationError("No default_image for slice %s" % slice.name)
98
99 flavor = slice.default_flavor
100 if not flavor:
101 raise XOSConfigurationError("No default_flavor for slice %s" % slice.name)
102
103 s = Sliver(slice=slice,
104 node=node,
105 creator=slice.creator,
106 image=image,
107 flavor=flavor,
108 deployment=node.site_deployment.deployment)
109 s.save()
110
111 # print "add sliver", s
Tony Mack9d2ea092015-04-29 12:23:10 -0400112
Siobhan Tully00353f72013-10-08 21:53:27 -0400113class ServiceAttribute(PlCoreBase):
114 name = models.SlugField(help_text="Attribute Name", max_length=128)
Tony Mackd84b1ff2015-03-09 13:03:56 -0400115 value = StrippedCharField(help_text="Attribute Value", max_length=1024)
Siobhan Tully00353f72013-10-08 21:53:27 -0400116 service = models.ForeignKey(Service, related_name='serviceattributes', help_text="The Service this attribute is associated with")
117
Tony Mack9d2ea092015-04-29 12:23:10 -0400118class ServiceRole(PlCoreBase):
119 ROLE_CHOICES = (('admin','Admin'),)
120 role = StrippedCharField(choices=ROLE_CHOICES, unique=True, max_length=30)
121
122 def __unicode__(self): return u'%s' % (self.role)
123
124class ServicePrivilege(PlCoreBase):
125 user = models.ForeignKey('User', related_name='serviceprivileges')
126 service = models.ForeignKey('Service', related_name='serviceprivileges')
127 role = models.ForeignKey('ServiceRole',related_name='serviceprivileges')
128
129 class Meta:
Tony Mack9ec754e2015-05-13 12:21:28 -0400130 unique_together = ('user', 'service', 'role')
Tony Mack9d2ea092015-04-29 12:23:10 -0400131
132 def __unicode__(self): return u'%s %s %s' % (self.service, self.user, self.role)
133
134 def can_update(self, user):
135 if not self.service.enabled:
136 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
137 return self.service.can_update(user)
138
139 def save(self, *args, **kwds):
140 if not self.service.enabled:
141 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
142 super(ServicePrivilege, self).save(*args, **kwds)
143
144 def delete(self, *args, **kwds):
145 if not self.service.enabled:
146 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
Scott Baker4587b822015-07-01 18:29:08 -0700147 super(ServicePrivilege, self).delete(*args, **kwds)
148
Tony Mack9d2ea092015-04-29 12:23:10 -0400149 @staticmethod
150 def select_by_user(user):
151 if user.is_admin:
152 qs = ServicePrivilege.objects.all()
153 else:
154 qs = SitePrivilege.objects.filter(user=user)
Scott Baker4587b822015-07-01 18:29:08 -0700155 return qs
156
157class TenantRoot(PlCoreBase):
158 """ A tenantRoot is one of the things that can sit at the root of a chain
159 of tenancy. This object represents a node.
160 """
161
162 KIND= "generic"
163 kind = StrippedCharField(max_length=30, default=KIND)
Scott Baker618a4892015-07-06 14:27:31 -0700164 name = StrippedCharField(max_length=255, help_text="name", blank=True, null=True)
Scott Baker4587b822015-07-01 18:29:08 -0700165
Scott Bakerefcec632015-07-07 12:12:42 -0700166 service_specific_attribute = models.TextField(blank=True, null=True)
167 service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
Scott Baker4587b822015-07-01 18:29:08 -0700168
Scott Bakerdb66fd32015-07-07 17:59:44 -0700169 def __init__(self, *args, **kwargs):
170 # for subclasses, set the default kind appropriately
171 self._meta.get_field("kind").default = self.KIND
172 super(TenantRoot, self).__init__(*args, **kwargs)
173
174
Scott Baker4587b822015-07-01 18:29:08 -0700175 # helper for extracting things from a json-encoded attribute
176 def get_attribute(self, name, default=None):
177 if self.service_specific_attribute:
Scott Bakerefcec632015-07-07 12:12:42 -0700178 attributes = json.loads(self.service_specific_attribute)
Scott Baker4587b822015-07-01 18:29:08 -0700179 else:
180 attributes = {}
181 return attributes.get(name, default)
182
183 def set_attribute(self, name, value):
184 if self.service_specific_attribute:
Scott Bakerefcec632015-07-07 12:12:42 -0700185 attributes = json.loads(self.service_specific_attribute)
Scott Baker4587b822015-07-01 18:29:08 -0700186 else:
187 attributes = {}
188 attributes[name]=value
Scott Bakerefcec632015-07-07 12:12:42 -0700189 self.service_specific_attribute = json.dumps(attributes)
Tony Mack9d2ea092015-04-29 12:23:10 -0400190
Scott Baker618a4892015-07-06 14:27:31 -0700191 def __unicode__(self):
192 if not self.name:
193 return u"%s-tenant_root-#%s" % (str(self.kind), str(self.id))
194 else:
195 return self.name
196
197 def can_update(self, user):
198 return user.can_update_tenant_root(self, allow=['admin'])
199
Scott Bakerefcec632015-07-07 12:12:42 -0700200 def get_subscribed_tenants(self, tenant_class):
201 ids = self.subscribed_tenants.filter(kind=tenant_class.KIND)
202 return tenant_class.objects.filter(id__in = ids)
203
204 def get_newest_subscribed_tenant(self, kind):
205 st = list(self.get_subscribed_tenants(kind))
206 if not st:
207 return None
208 return sorted(st, key=attrgetter('id'))[0]
209
210 @classmethod
211 def get_tenant_objects(cls):
212 return cls.objects.filter(kind = cls.KIND)
213
Scott Baker8103d0f2015-04-10 16:42:26 -0700214class Tenant(PlCoreBase):
215 """ A tenant is a relationship between two entities, a subscriber and a
Scott Baker4587b822015-07-01 18:29:08 -0700216 provider. This object represents an edge.
Scott Baker8103d0f2015-04-10 16:42:26 -0700217
218 The subscriber can be a User, a Service, or a Tenant.
219
220 The provider is always a Service.
Scott Baker4587b822015-07-01 18:29:08 -0700221
222 TODO: rename "Tenant" to "Tenancy"
Scott Baker8103d0f2015-04-10 16:42:26 -0700223 """
Scott Baker008a9962015-04-15 20:58:20 -0700224
Scott Baker925a8fa2015-04-26 20:30:40 -0700225 CONNECTIVITY_CHOICES = (('public', 'Public'), ('private', 'Private'), ('na', 'Not Applicable'))
226
Scott Baker008a9962015-04-15 20:58:20 -0700227 # when subclassing a service, redefine KIND to describe the new service
228 KIND = "generic"
229
230 kind = StrippedCharField(max_length=30, default=KIND)
Scott Baker4587b822015-07-01 18:29:08 -0700231 provider_service = models.ForeignKey(Service, related_name='provided_tenants')
232
233 # The next four things are the various type of objects that can be subscribers of this Tenancy
234 # relationship. One and only one can be used at a time.
235 subscriber_service = models.ForeignKey(Service, related_name='subscribed_tenants', blank=True, null=True)
236 subscriber_tenant = models.ForeignKey("Tenant", related_name='subscribed_tenants', blank=True, null=True)
237 subscriber_user = models.ForeignKey("User", related_name='subscribed_tenants', blank=True, null=True)
238 subscriber_root = models.ForeignKey("TenantRoot", related_name="subscribed_tenants", blank=True, null=True)
239
240 # Service_specific_attribute and service_specific_id are opaque to XOS
Scott Baker76934d82015-05-06 19:49:31 -0700241 service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
242 service_specific_attribute = models.TextField(blank=True, null=True)
Scott Baker4587b822015-07-01 18:29:08 -0700243
244 # Connect_method is only used by Coarse tenants
Scott Baker925a8fa2015-04-26 20:30:40 -0700245 connect_method = models.CharField(null=False, blank=False, max_length=30, choices=CONNECTIVITY_CHOICES, default="na")
Scott Baker8103d0f2015-04-10 16:42:26 -0700246
Scott Baker008a9962015-04-15 20:58:20 -0700247 def __init__(self, *args, **kwargs):
248 # for subclasses, set the default kind appropriately
249 self._meta.get_field("kind").default = self.KIND
250 super(Tenant, self).__init__(*args, **kwargs)
251
Scott Baker8103d0f2015-04-10 16:42:26 -0700252 def __unicode__(self):
Scott Bakerf996b762015-05-20 20:42:04 -0700253 return u"%s-tenant-%s" % (str(self.kind), str(self.id))
Scott Baker8103d0f2015-04-10 16:42:26 -0700254
255 # helper for extracting things from a json-encoded service_specific_attribute
256 def get_attribute(self, name, default=None):
257 if self.service_specific_attribute:
258 attributes = json.loads(self.service_specific_attribute)
259 else:
260 attributes = {}
261 return attributes.get(name, default)
262
263 def set_attribute(self, name, value):
Scott Bakerf57e5592015-04-14 17:18:51 -0700264 if self.service_specific_attribute:
Scott Baker8103d0f2015-04-10 16:42:26 -0700265 attributes = json.loads(self.service_specific_attribute)
266 else:
267 attributes = {}
268 attributes[name]=value
269 self.service_specific_attribute = json.dumps(attributes)
270
Scott Baker5e76f802015-06-24 12:54:24 -0700271 def get_initial_attribute(self, name, default=None):
272 if self._initial["service_specific_attribute"]:
273 attributes = json.loads(self._initial["service_specific_attribute"])
274 else:
275 attributes = {}
276 return attributes.get(name, default)
277
Scott Baker008a9962015-04-15 20:58:20 -0700278 @classmethod
279 def get_tenant_objects(cls):
280 return cls.objects.filter(kind = cls.KIND)
281
Scott Bakere7fc9f52015-05-05 17:52:03 -0700282 @classmethod
283 def get_deleted_tenant_objects(cls):
284 return cls.deleted_objects.filter(kind = cls.KIND)
285
Scott Bakerd921e1c2015-04-20 14:24:29 -0700286 # helper function to be used in subclasses that want to ensure service_specific_id is unique
287 def validate_unique_service_specific_id(self):
288 if self.pk is None:
289 if self.service_specific_id is None:
290 raise XOSMissingField("subscriber_specific_id is None, and it's a required field", fields={"service_specific_id": "cannot be none"})
291
292 conflicts = self.get_tenant_objects().filter(service_specific_id=self.service_specific_id)
293 if conflicts:
294 raise XOSDuplicateKey("service_specific_id %s already exists" % self.service_specific_id, fields={"service_specific_id": "duplicate key"})
295
Scott Baker618a4892015-07-06 14:27:31 -0700296 def save(self, *args, **kwargs):
297 subCount = sum( [1 for e in [self.subscriber_service, self.subscriber_tenant, self.subscriber_user, self.subscriber_root] if e is not None])
298 if (subCount > 1):
299 raise XOSConflictingField("Only one of subscriber_service, subscriber_tenant, subscriber_user, subscriber_root should be set")
300
301 super(Tenant, self).save(*args, **kwargs)
302
303 def get_subscribed_tenants(self, tenant_class):
304 ids = self.subscribed_tenants.filter(kind=tenant_class.KIND)
305 return tenant_class.objects.filter(id__in = ids)
306
307 def get_newest_subscribed_tenant(self, kind):
308 st = list(self.get_subscribed_tenants(kind))
309 if not st:
310 return None
311 return sorted(st, key=attrgetter('id'))[0]
312
313
Scott Baker925a8fa2015-04-26 20:30:40 -0700314class CoarseTenant(Tenant):
Scott Baker4587b822015-07-01 18:29:08 -0700315 """ TODO: rename "CoarseTenant" --> "StaticTenant" """
Scott Baker925a8fa2015-04-26 20:30:40 -0700316 class Meta:
317 proxy = True
Siobhan Tully00353f72013-10-08 21:53:27 -0400318
Scott Baker925a8fa2015-04-26 20:30:40 -0700319 KIND = "coarse"
320
321 def save(self, *args, **kwargs):
322 if (not self.subscriber_service):
323 raise XOSValidationError("subscriber_service cannot be null")
324 if (self.subscriber_tenant or self.subscriber_user):
325 raise XOSValidationError("subscriber_tenant and subscriber_user must be null")
326
327 super(CoarseTenant,self).save()
Scott Baker4587b822015-07-01 18:29:08 -0700328
329class Subscriber(TenantRoot):
330 """ Intermediate class for TenantRoots that are to be Subscribers """
331
332 class Meta:
333 proxy = True
334
335 KIND = "Subscriber"
336
337class Provider(TenantRoot):
338 """ Intermediate class for TenantRoots that are to be Providers """
339
340 class Meta:
341 proxy = True
342
343 KIND = "Provider"
344
345class TenantRootRole(PlCoreBase):
346 ROLE_CHOICES = (('admin','Admin'),)
347
348 role = StrippedCharField(choices=ROLE_CHOICES, unique=True, max_length=30)
349
350 def __unicode__(self): return u'%s' % (self.role)
351
352class TenantRootPrivilege(PlCoreBase):
353 user = models.ForeignKey('User', related_name="tenant_root_privileges")
354 tenant_root = models.ForeignKey('TenantRoot', related_name="tenant_root_privileges")
355 role = models.ForeignKey('TenantRootRole', related_name="tenant_root_privileges")
356
357 class Meta:
358 unique_together = ('user', 'tenant_root', 'role')
359
Scott Baker618a4892015-07-06 14:27:31 -0700360 def __unicode__(self): return u'%s %s %s' % (self.tenant_root, self.user, self.role)
Scott Baker4587b822015-07-01 18:29:08 -0700361
362 def save(self, *args, **kwds):
363 if not self.user.is_active:
364 raise PermissionDenied, "Cannot modify role(s) of a disabled user"
365 super(SlicePrivilege, self).save(*args, **kwds)
366
367 def can_update(self, user):
Scott Baker618a4892015-07-06 14:27:31 -0700368 return user.tenant_root.can_update(user)
Scott Baker4587b822015-07-01 18:29:08 -0700369
370 @staticmethod
371 def select_by_user(user):
372 if user.is_admin:
Scott Baker618a4892015-07-06 14:27:31 -0700373 qs = TenantRootPrivilege.objects.all()
Scott Baker4587b822015-07-01 18:29:08 -0700374 else:
Scott Baker618a4892015-07-06 14:27:31 -0700375 qs = TenantRootPrivilege.objects.filter(user=user)
Scott Baker4587b822015-07-01 18:29:08 -0700376 return qs
Scott Baker618a4892015-07-06 14:27:31 -0700377