blob: ba3a9916bf4206ff49fc2796bb44d49e64305ed8 [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 Bakerf57e5592015-04-14 17:18:51 -07005import json
Siobhan Tully00353f72013-10-08 21:53:27 -04006
7class Service(PlCoreBase):
Scott Baker008a9962015-04-15 20:58:20 -07008 # when subclassing a service, redefine KIND to describe the new service
9 KIND = "generic"
10
Siobhan Tully00353f72013-10-08 21:53:27 -040011 description = models.TextField(max_length=254,null=True, blank=True,help_text="Description of Service")
12 enabled = models.BooleanField(default=True)
Scott Baker008a9962015-04-15 20:58:20 -070013 kind = StrippedCharField(max_length=30, help_text="Kind of service", default=KIND)
Tony Mackd84b1ff2015-03-09 13:03:56 -040014 name = StrippedCharField(max_length=30, help_text="Service Name")
15 versionNumber = StrippedCharField(max_length=30, help_text="Version of Service Definition")
Siobhan Tullycf04fb62014-01-11 11:25:57 -050016 published = models.BooleanField(default=True)
Tony Mackd84b1ff2015-03-09 13:03:56 -040017 view_url = StrippedCharField(blank=True, null=True, max_length=1024)
18 icon_url = StrippedCharField(blank=True, null=True, max_length=1024)
Scott Baker68944742015-04-30 14:30:56 -070019 public_key = models.TextField(null=True, blank=True, max_length=1024, help_text="Public key string")
Siobhan Tully00353f72013-10-08 21:53:27 -040020
Scott Baker008a9962015-04-15 20:58:20 -070021 def __init__(self, *args, **kwargs):
22 # for subclasses, set the default kind appropriately
23 self._meta.get_field("kind").default = self.KIND
24 super(Service, self).__init__(*args, **kwargs)
25
26 @classmethod
27 def get_service_objects(cls):
28 return cls.objects.filter(kind = cls.KIND)
29
Siobhan Tully00353f72013-10-08 21:53:27 -040030 def __unicode__(self): return u'%s' % (self.name)
31
Tony Mack9d2ea092015-04-29 12:23:10 -040032 def can_update(self, user):
33 return user.can_update_service(self, allow=['admin'])
34
Scott Baker98436732015-05-11 16:36:41 -070035 def get_scalable_nodes(self, slice, max_per_node=None, exclusive_slices=[]):
36 """
37 Get a list of nodes that can be used to scale up a slice.
38
39 slice - slice to scale up
40 max_per_node - maximum numbers of slivers that 'slice' can have on a single node
41 exclusive_slices - list of slices that must have no nodes in common with 'slice'.
42 """
43
44 from core.models import Node, Sliver # late import to get around order-of-imports constraint in __init__.py
45
46 nodes = list(Node.objects.all())
47
48 conflicting_slivers = Sliver.objects.filter(slice__in = exclusive_slices)
49 conflicting_nodes = Node.objects.filter(slivers__in = conflicting_slivers)
50
51 nodes = [x for x in nodes if x not in conflicting_nodes]
52
53 # If max_per_node is set, then limit the number of slivers this slice
54 # can have on a single node.
55 if max_per_node:
56 acceptable_nodes = []
57 for node in nodes:
58 existing_count = node.slivers.filter(slice=slice).count()
59 if existing_count < max_per_node:
60 acceptable_nodes.append(node)
61 nodes = acceptable_nodes
62
63 return nodes
64
65 def pick_node(self, slice, max_per_node=None, exclusive_slices=[]):
66 # Pick the best node to scale up a slice.
67
68 nodes = self.get_scalable_nodes(slice, max_per_node, exclusive_slices)
69 nodes = sorted(nodes, key=lambda node: node.slivers.all().count())
70 if not nodes:
71 return None
72 return nodes[0]
73
74 def adjust_scale(self, slice_hint, scale, max_per_node=None, exclusive_slices=[]):
75 from core.models import Sliver # late import to get around order-of-imports constraint in __init__.py
76
77 slices = [x for x in self.slices.all() if slice_hint in x.name]
78 for slice in slices:
79 while slice.slivers.all().count() > scale:
80 s = slice.slivers.all()[0]
81 # print "drop sliver", s
82 s.delete()
83
84 while slice.slivers.all().count() < scale:
85 node = self.pick_node(slice, max_per_node, exclusive_slices)
86 if not node:
87 # no more available nodes
88 break
89
90 image = slice.default_image
91 if not image:
92 raise XOSConfigurationError("No default_image for slice %s" % slice.name)
93
94 flavor = slice.default_flavor
95 if not flavor:
96 raise XOSConfigurationError("No default_flavor for slice %s" % slice.name)
97
98 s = Sliver(slice=slice,
99 node=node,
100 creator=slice.creator,
101 image=image,
102 flavor=flavor,
103 deployment=node.site_deployment.deployment)
104 s.save()
105
106 # print "add sliver", s
Tony Mack9d2ea092015-04-29 12:23:10 -0400107
Siobhan Tully00353f72013-10-08 21:53:27 -0400108class ServiceAttribute(PlCoreBase):
109 name = models.SlugField(help_text="Attribute Name", max_length=128)
Tony Mackd84b1ff2015-03-09 13:03:56 -0400110 value = StrippedCharField(help_text="Attribute Value", max_length=1024)
Siobhan Tully00353f72013-10-08 21:53:27 -0400111 service = models.ForeignKey(Service, related_name='serviceattributes', help_text="The Service this attribute is associated with")
112
Tony Mack9d2ea092015-04-29 12:23:10 -0400113class ServiceRole(PlCoreBase):
114 ROLE_CHOICES = (('admin','Admin'),)
115 role = StrippedCharField(choices=ROLE_CHOICES, unique=True, max_length=30)
116
117 def __unicode__(self): return u'%s' % (self.role)
118
119class ServicePrivilege(PlCoreBase):
120 user = models.ForeignKey('User', related_name='serviceprivileges')
121 service = models.ForeignKey('Service', related_name='serviceprivileges')
122 role = models.ForeignKey('ServiceRole',related_name='serviceprivileges')
123
124 class Meta:
Tony Mack9ec754e2015-05-13 12:21:28 -0400125 unique_together = ('user', 'service', 'role')
Tony Mack9d2ea092015-04-29 12:23:10 -0400126
127 def __unicode__(self): return u'%s %s %s' % (self.service, self.user, self.role)
128
129 def can_update(self, user):
130 if not self.service.enabled:
131 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
132 return self.service.can_update(user)
133
134 def save(self, *args, **kwds):
135 if not self.service.enabled:
136 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
137 super(ServicePrivilege, self).save(*args, **kwds)
138
139 def delete(self, *args, **kwds):
140 if not self.service.enabled:
141 raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
Scott Baker4587b822015-07-01 18:29:08 -0700142 super(ServicePrivilege, self).delete(*args, **kwds)
143
Tony Mack9d2ea092015-04-29 12:23:10 -0400144 @staticmethod
145 def select_by_user(user):
146 if user.is_admin:
147 qs = ServicePrivilege.objects.all()
148 else:
149 qs = SitePrivilege.objects.filter(user=user)
Scott Baker4587b822015-07-01 18:29:08 -0700150 return qs
151
152class TenantRoot(PlCoreBase):
153 """ A tenantRoot is one of the things that can sit at the root of a chain
154 of tenancy. This object represents a node.
155 """
156
157 KIND= "generic"
158 kind = StrippedCharField(max_length=30, default=KIND)
159
160 attribute = models.TextField(blank=True, null=True)
161
162 # helper for extracting things from a json-encoded attribute
163 def get_attribute(self, name, default=None):
164 if self.service_specific_attribute:
165 attributes = json.loads(self.attribute)
166 else:
167 attributes = {}
168 return attributes.get(name, default)
169
170 def set_attribute(self, name, value):
171 if self.service_specific_attribute:
172 attributes = json.loads(self.attribute)
173 else:
174 attributes = {}
175 attributes[name]=value
176 self.attribute = json.dumps(attributes)
Tony Mack9d2ea092015-04-29 12:23:10 -0400177
Scott Baker8103d0f2015-04-10 16:42:26 -0700178class Tenant(PlCoreBase):
179 """ A tenant is a relationship between two entities, a subscriber and a
Scott Baker4587b822015-07-01 18:29:08 -0700180 provider. This object represents an edge.
Scott Baker8103d0f2015-04-10 16:42:26 -0700181
182 The subscriber can be a User, a Service, or a Tenant.
183
184 The provider is always a Service.
Scott Baker4587b822015-07-01 18:29:08 -0700185
186 TODO: rename "Tenant" to "Tenancy"
Scott Baker8103d0f2015-04-10 16:42:26 -0700187 """
Scott Baker008a9962015-04-15 20:58:20 -0700188
Scott Baker925a8fa2015-04-26 20:30:40 -0700189 CONNECTIVITY_CHOICES = (('public', 'Public'), ('private', 'Private'), ('na', 'Not Applicable'))
190
Scott Baker008a9962015-04-15 20:58:20 -0700191 # when subclassing a service, redefine KIND to describe the new service
192 KIND = "generic"
193
194 kind = StrippedCharField(max_length=30, default=KIND)
Scott Baker4587b822015-07-01 18:29:08 -0700195 provider_service = models.ForeignKey(Service, related_name='provided_tenants')
196
197 # The next four things are the various type of objects that can be subscribers of this Tenancy
198 # relationship. One and only one can be used at a time.
199 subscriber_service = models.ForeignKey(Service, related_name='subscribed_tenants', blank=True, null=True)
200 subscriber_tenant = models.ForeignKey("Tenant", related_name='subscribed_tenants', blank=True, null=True)
201 subscriber_user = models.ForeignKey("User", related_name='subscribed_tenants', blank=True, null=True)
202 subscriber_root = models.ForeignKey("TenantRoot", related_name="subscribed_tenants", blank=True, null=True)
203
204 # Service_specific_attribute and service_specific_id are opaque to XOS
Scott Baker76934d82015-05-06 19:49:31 -0700205 service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
206 service_specific_attribute = models.TextField(blank=True, null=True)
Scott Baker4587b822015-07-01 18:29:08 -0700207
208 # Connect_method is only used by Coarse tenants
Scott Baker925a8fa2015-04-26 20:30:40 -0700209 connect_method = models.CharField(null=False, blank=False, max_length=30, choices=CONNECTIVITY_CHOICES, default="na")
Scott Baker8103d0f2015-04-10 16:42:26 -0700210
Scott Baker008a9962015-04-15 20:58:20 -0700211 def __init__(self, *args, **kwargs):
212 # for subclasses, set the default kind appropriately
213 self._meta.get_field("kind").default = self.KIND
214 super(Tenant, self).__init__(*args, **kwargs)
215
Scott Baker8103d0f2015-04-10 16:42:26 -0700216 def __unicode__(self):
Scott Bakerf996b762015-05-20 20:42:04 -0700217 return u"%s-tenant-%s" % (str(self.kind), str(self.id))
Scott Baker8103d0f2015-04-10 16:42:26 -0700218
219 # helper for extracting things from a json-encoded service_specific_attribute
220 def get_attribute(self, name, default=None):
221 if self.service_specific_attribute:
222 attributes = json.loads(self.service_specific_attribute)
223 else:
224 attributes = {}
225 return attributes.get(name, default)
226
227 def set_attribute(self, name, value):
Scott Bakerf57e5592015-04-14 17:18:51 -0700228 if self.service_specific_attribute:
Scott Baker8103d0f2015-04-10 16:42:26 -0700229 attributes = json.loads(self.service_specific_attribute)
230 else:
231 attributes = {}
232 attributes[name]=value
233 self.service_specific_attribute = json.dumps(attributes)
234
Scott Baker5e76f802015-06-24 12:54:24 -0700235 def get_initial_attribute(self, name, default=None):
236 if self._initial["service_specific_attribute"]:
237 attributes = json.loads(self._initial["service_specific_attribute"])
238 else:
239 attributes = {}
240 return attributes.get(name, default)
241
Scott Baker008a9962015-04-15 20:58:20 -0700242 @classmethod
243 def get_tenant_objects(cls):
244 return cls.objects.filter(kind = cls.KIND)
245
Scott Bakere7fc9f52015-05-05 17:52:03 -0700246 @classmethod
247 def get_deleted_tenant_objects(cls):
248 return cls.deleted_objects.filter(kind = cls.KIND)
249
Scott Bakerd921e1c2015-04-20 14:24:29 -0700250 # helper function to be used in subclasses that want to ensure service_specific_id is unique
251 def validate_unique_service_specific_id(self):
252 if self.pk is None:
253 if self.service_specific_id is None:
254 raise XOSMissingField("subscriber_specific_id is None, and it's a required field", fields={"service_specific_id": "cannot be none"})
255
256 conflicts = self.get_tenant_objects().filter(service_specific_id=self.service_specific_id)
257 if conflicts:
258 raise XOSDuplicateKey("service_specific_id %s already exists" % self.service_specific_id, fields={"service_specific_id": "duplicate key"})
259
Scott Baker925a8fa2015-04-26 20:30:40 -0700260class CoarseTenant(Tenant):
Scott Baker4587b822015-07-01 18:29:08 -0700261 """ TODO: rename "CoarseTenant" --> "StaticTenant" """
Scott Baker925a8fa2015-04-26 20:30:40 -0700262 class Meta:
263 proxy = True
Siobhan Tully00353f72013-10-08 21:53:27 -0400264
Scott Baker925a8fa2015-04-26 20:30:40 -0700265 KIND = "coarse"
266
267 def save(self, *args, **kwargs):
268 if (not self.subscriber_service):
269 raise XOSValidationError("subscriber_service cannot be null")
270 if (self.subscriber_tenant or self.subscriber_user):
271 raise XOSValidationError("subscriber_tenant and subscriber_user must be null")
272
273 super(CoarseTenant,self).save()
Scott Baker4587b822015-07-01 18:29:08 -0700274
275class Subscriber(TenantRoot):
276 """ Intermediate class for TenantRoots that are to be Subscribers """
277
278 class Meta:
279 proxy = True
280
281 KIND = "Subscriber"
282
283class Provider(TenantRoot):
284 """ Intermediate class for TenantRoots that are to be Providers """
285
286 class Meta:
287 proxy = True
288
289 KIND = "Provider"
290
291class TenantRootRole(PlCoreBase):
292 ROLE_CHOICES = (('admin','Admin'),)
293
294 role = StrippedCharField(choices=ROLE_CHOICES, unique=True, max_length=30)
295
296 def __unicode__(self): return u'%s' % (self.role)
297
298class TenantRootPrivilege(PlCoreBase):
299 user = models.ForeignKey('User', related_name="tenant_root_privileges")
300 tenant_root = models.ForeignKey('TenantRoot', related_name="tenant_root_privileges")
301 role = models.ForeignKey('TenantRootRole', related_name="tenant_root_privileges")
302
303 class Meta:
304 unique_together = ('user', 'tenant_root', 'role')
305
306 def __unicode__(self): return u'%s %s %s' % (self.slice, self.user, self.role)
307
308 def save(self, *args, **kwds):
309 if not self.user.is_active:
310 raise PermissionDenied, "Cannot modify role(s) of a disabled user"
311 super(SlicePrivilege, self).save(*args, **kwds)
312
313 def can_update(self, user):
314 return user.can_update_tenant_root(self.tenant_root)
315
316 @staticmethod
317 def select_by_user(user):
318 if user.is_admin:
319 qs = TenantRoot.objects.all()
320 else:
321 sp_ids = [sp.id for sp in TenantRoot.objects.filter(user=user)]
322 qs = TenantRoot.objects.filter(id__in=sp_ids)
323 return qs