73f3a4de2d2c5f8fb00d78d13075860dd9d26bbc
[plstackapi.git] / planetstack / core / admin.py
1 from core.models import Site
2 from core.models import *
3 from openstack.manager import OpenStackManager
4
5 from django.contrib import admin
6 from django.contrib.auth.models import Group
7 from django import forms
8 from django.utils.safestring import mark_safe
9 from django.contrib.auth.admin import UserAdmin
10 from django.contrib.admin.widgets import FilteredSelectMultiple, AdminTextareaWidget
11 from django.contrib.auth.forms import ReadOnlyPasswordHashField, AdminPasswordChangeForm
12 from django.contrib.auth.signals import user_logged_in
13 from django.utils import timezone
14 from django.contrib.contenttypes import generic
15 from suit.widgets import LinkedSelect
16 from django.core.exceptions import PermissionDenied
17 from django.core.urlresolvers import reverse, NoReverseMatch
18 from django.utils.encoding import force_text, python_2_unicode_compatible
19 from django.utils.html import conditional_escape, format_html
20 from django.forms.utils import flatatt, to_current_timezone
21 from cgi import escape as html_escape
22
23 import django_evolution
24 import threading
25
26 # thread locals necessary to work around a django-suit issue
27 _thread_locals = threading.local()
28
29 def backend_icon(obj): # backend_status, enacted, updated):
30     #return "%s %s %s" % (str(obj.updated), str(obj.enacted), str(obj.backend_status))
31     if (obj.enacted is not None) and obj.enacted >= obj.updated or obj.backend_status.startswith("1 -"):
32         return '<span style="min-width:16px;"><img src="/static/admin/img/icon_success.gif"></span>'
33     else:
34         if ((obj.backend_status is not None) and obj.backend_status.startswith("0 -")) or obj.backend_status == "Provisioning in progress" or obj.backend_status=="":
35             return '<span style="min-width:16px;" title="%s"><img src="/static/admin/img/icon_clock.gif"></span>' % obj.backend_status
36         else:
37             return '<span style="min-width:16px;" title="%s"><img src="/static/admin/img/icon_error.gif"></span>' % html_escape(obj.backend_status, quote=True)
38
39 def backend_text(obj):
40     icon = backend_icon(obj)
41     if (obj.enacted is not None) and obj.enacted >= obj.updated:
42         return "%s %s" % (icon, "successfully enacted")
43     else:
44         return "%s %s" % (icon, html_escape(obj.backend_status, quote=True))
45
46 class UploadTextareaWidget(AdminTextareaWidget):
47     def render(self, name, value, attrs=None):
48         if value is None:
49             value = ''\r
50         final_attrs = self.build_attrs(attrs, name=name)\r
51         return format_html('<input type="file" style="width: 0; height: 0" id="btn_upload_%s" onChange="uploadTextarea(event,\'%s\');">' \\r
52                            '<button onClick="$(\'#btn_upload_%s\').click(); return false;">Upload</button>' \\r
53                            '<br><textarea{0}>\r\n{1}</textarea>' % (attrs["id"], attrs["id"], attrs["id"]),\r
54                            flatatt(final_attrs),\r
55                            force_text(value))
56
57 class PlainTextWidget(forms.HiddenInput):
58     input_type = 'hidden'
59
60     def render(self, name, value, attrs=None):
61         if value is None:
62             value = ''
63         return mark_safe(str(value) + super(PlainTextWidget, self).render(name, value, attrs))
64
65 class PermissionCheckingAdminMixin(object):
66     # call save_by_user and delete_by_user instead of save and delete
67
68     def has_add_permission(self, request, obj=None):
69         return (not self.__user_is_readonly(request))
70
71     def has_delete_permission(self, request, obj=None):
72         return (not self.__user_is_readonly(request))
73
74     def save_model(self, request, obj, form, change):
75         if self.__user_is_readonly(request):
76             # this 'if' might be redundant if save_by_user is implemented right
77             raise PermissionDenied
78
79         obj.caller = request.user
80         # update openstack connection to use this site/tenant
81         obj.save_by_user(request.user)
82
83     def delete_model(self, request, obj):
84         obj.delete_by_user(request.user)
85
86     def save_formset(self, request, form, formset, change):
87         instances = formset.save(commit=False)
88         for instance in instances:
89             instance.save_by_user(request.user)
90
91         # BUG in django 1.7? Objects are not deleted by formset.save if
92         # commit is False. So let's delete them ourselves.
93         #
94         # code from forms/models.py save_existing_objects()
95         try:
96             forms_to_delete = formset.deleted_forms\r
97         except AttributeError:\r
98             forms_to_delete = []
99         if formset.initial_forms:
100             for form in formset.initial_forms:
101                 obj = form.instance
102                 if form in forms_to_delete:
103                     if obj.pk is None:
104                         continue
105                     formset.deleted_objects.append(obj)
106                     obj.delete()
107
108         formset.save_m2m()
109
110     def get_actions(self,request):
111         actions = super(PermissionCheckingAdminMixin,self).get_actions(request)
112
113         if self.__user_is_readonly(request):
114             if 'delete_selected' in actions:
115                 del actions['delete_selected']
116
117         return actions
118
119     def change_view(self,request,object_id, extra_context=None):
120         if self.__user_is_readonly(request):
121             if not hasattr(self, "readonly_save"):\r
122                 # save the original readonly fields\r
123                 self.readonly_save = self.readonly_fields\r
124                 self.inlines_save = self.inlines\r
125             if hasattr(self, "user_readonly_fields"):\r
126                 self.readonly_fields=self.user_readonly_fields\r
127             if hasattr(self, "user_readonly_inlines"):\r
128                 self.inlines = self.user_readonly_inlines\r
129         else:\r
130             if hasattr(self, "readonly_save"):\r
131                 # restore the original readonly fields\r
132                 self.readonly_fields = self.readonly_save\r
133             if hasattr(self, "inlines_save"):\r
134                 self.inlines = self.inlines_save
135
136         try:
137             return super(PermissionCheckingAdminMixin, self).change_view(request, object_id, extra_context=extra_context)
138         except PermissionDenied:
139             pass
140         if request.method == 'POST':
141             raise PermissionDenied
142         request.readonly = True
143         return super(PermissionCheckingAdminMixin, self).change_view(request, object_id, extra_context=extra_context)
144
145     def __user_is_readonly(self, request):
146         return request.user.isReadOnlyUser()
147
148     def backend_status_text(self, obj):
149         return mark_safe(backend_text(obj))
150
151     def backend_status_icon(self, obj):
152         return mark_safe(backend_icon(obj))
153     backend_status_icon.short_description = ""
154
155     def get_form(self, request, obj=None, **kwargs):
156         # Save obj and request in thread-local storage, so suit_form_tabs can
157         # use it to determine whether we're in edit or add mode, and can
158         # determine whether the user is an admin.
159         _thread_locals.request = request
160         _thread_locals.obj = obj
161         return super(PermissionCheckingAdminMixin, self).get_form(request, obj, **kwargs)
162
163     def get_inline_instances(self, request, obj=None):
164         inlines = super(PermissionCheckingAdminMixin, self).get_inline_instances(request, obj)
165
166         # inlines that should only be shown to an admin user
167         if request.user.is_admin:
168             for inline_class in getattr(self, "admin_inlines", []):
169                 inlines.append(inline_class(self.model, self.admin_site))
170
171         return inlines
172
173 class ReadOnlyAwareAdmin(PermissionCheckingAdminMixin, admin.ModelAdmin):
174     # Note: Make sure PermissionCheckingAdminMixin is listed before
175     # admin.ModelAdmin in the class declaration.
176
177     pass
178
179 class PlanetStackBaseAdmin(ReadOnlyAwareAdmin):
180     save_on_top = False
181
182 class SingletonAdmin (ReadOnlyAwareAdmin):
183     def has_add_permission(self, request):
184         if not super(SingletonAdmin, self).has_add_permission(request):
185             return False
186
187         num_objects = self.model.objects.count()
188         if num_objects >= 1:
189             return False
190         else:
191             return True
192
193 class PlStackTabularInline(admin.TabularInline):
194     def __init__(self, *args, **kwargs):
195         super(PlStackTabularInline, self).__init__(*args, **kwargs)
196
197         # InlineModelAdmin as no get_fields() method, so in order to add
198         # the selflink field, we override __init__ to modify self.fields and
199         # self.readonly_fields.
200
201         self.setup_selflink()
202
203     def get_change_url(self, model, id):
204         """ Get the URL to a change form in the admin for this model """
205         reverse_path = "admin:%s_change" % (model._meta.db_table)
206         try:
207             url = reverse(reverse_path, args=(id,))
208         except NoReverseMatch:
209             return None
210
211         return url
212
213     def setup_selflink(self):
214         if hasattr(self, "selflink_fieldname"):
215             """ self.selflink_model can be defined to punch through a relation
216                 to its target object. For example, in SliceNetworkInline, set
217                 selflink_model = "network", and the URL will lead to the Network
218                 object instead of trying to bring up a change view of the
219                 SliceNetwork object.
220             """
221             self.selflink_model = getattr(self.model,self.selflink_fieldname).field.rel.to
222         else:
223             self.selflink_model = self.model
224
225         url = self.get_change_url(self.selflink_model, 0)
226
227         # We don't have an admin for this object, so don't create the
228         # selflink.
229         if (url == None):
230             return
231
232         # Since we need to add "selflink" to the field list, we need to create
233         # self.fields if it is None.
234         if (self.fields is None):
235             self.fields = []
236             for f in self.model._meta.fields:
237                 if f.editable and f.name != "id":
238                     self.fields.append(f.name)
239
240         self.fields = tuple(self.fields) + ("selflink", )
241
242         if self.readonly_fields is None:
243             self.readonly_fields = ()
244
245         self.readonly_fields = tuple(self.readonly_fields) + ("selflink", )
246
247     def selflink(self, obj):
248         if hasattr(self, "selflink_fieldname"):
249             obj = getattr(obj, self.selflink_fieldname)
250
251         if obj.id:
252             url = self.get_change_url(self.selflink_model, obj.id)
253             return "<a href='%s'>Details</a>" % str(url)
254         else:\r
255             return "Not present"\r
256
257     selflink.allow_tags = True
258     selflink.short_description = "Details"
259
260     def has_add_permission(self, request):
261         return not request.user.isReadOnlyUser()
262
263     def get_readonly_fields(self, request, obj=None):
264         readonly_fields = list(self.readonly_fields)[:]
265         if request.user.isReadOnlyUser():
266             for field in self.fields:
267                 if not field in readonly_fields:
268                     readonly_fields.append(field)
269         return readonly_fields
270
271     def backend_status_icon(self, obj):
272         return mark_safe(backend_icon(obj))
273     backend_status_icon.short_description = ""
274
275 class PlStackGenericTabularInline(generic.GenericTabularInline):
276     def has_add_permission(self, request):
277         return not request.user.isReadOnlyUser()
278
279     def get_readonly_fields(self, request, obj=None):
280         readonly_fields = list(self.readonly_fields)[:]
281         if request.user.isReadOnlyUser():
282             for field in self.fields:
283                 if not field in readonly_fields:
284                     readonly_fields.append(field)
285         return readonly_fields
286
287     def backend_status_icon(self, obj):
288         return mark_safe(backend_icon(obj))
289     backend_status_icon.short_description = ""
290
291 class ReservationInline(PlStackTabularInline):
292     model = Reservation
293     extra = 0
294     suit_classes = 'suit-tab suit-tab-reservations'
295
296     def queryset(self, request):
297         return Reservation.select_by_user(request.user)
298
299 class TagInline(PlStackGenericTabularInline):
300     model = Tag
301     extra = 0
302     suit_classes = 'suit-tab suit-tab-tags'
303     fields = ['service', 'name', 'value']
304
305     def queryset(self, request):
306         return Tag.select_by_user(request.user)
307
308 class NetworkLookerUpper:
309     """ This is a callable that looks up a network name in a sliver and returns
310         the ip address for that network.
311     """
312
313     byNetworkName = {}    # class variable
314
315     def __init__(self, name):
316         self.short_description = name
317         self.__name__ = name
318         self.network_name = name
319
320     def __call__(self, obj):
321         if obj is not None:
322             for nbs in obj.networksliver_set.all():
323                 if (nbs.network.name == self.network_name):
324                     return nbs.ip
325         return ""
326
327     def __str__(self):
328         return self.network_name
329
330     @staticmethod
331     def get(network_name):
332         """ We want to make sure we alwars return the same NetworkLookerUpper
333             because sometimes django will cause them to be instantiated multiple
334             times (and we don't want different ones in form.fields vs
335             SliverInline.readonly_fields).
336         """
337         if network_name not in NetworkLookerUpper.byNetworkName:
338             NetworkLookerUpper.byNetworkName[network_name] = NetworkLookerUpper(network_name)
339         return NetworkLookerUpper.byNetworkName[network_name]
340
341 class SliverInline(PlStackTabularInline):
342     model = Sliver
343     fields = ['backend_status_icon', 'all_ips_string', 'instance_id', 'instance_name', 'slice', 'deployment', 'flavor', 'image', 'node']
344     extra = 0
345     readonly_fields = ['backend_status_icon', 'all_ips_string', 'instance_id', 'instance_name']
346     suit_classes = 'suit-tab suit-tab-slivers'
347
348     def queryset(self, request):
349         return Sliver.select_by_user(request.user)
350
351     def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
352         if db_field.name == 'deployment':
353            kwargs['queryset'] = Deployment.select_by_acl(request.user)
354            kwargs['widget'] = forms.Select(attrs={'onChange': "sliver_deployment_changed(this);"})
355         if db_field.name == 'flavor':
356            kwargs['widget'] = forms.Select(attrs={'onChange': "sliver_flavor_changed(this);"})
357
358         field = super(SliverInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
359
360         return field
361
362 class SiteInline(PlStackTabularInline):
363     model = Site
364     extra = 0
365     suit_classes = 'suit-tab suit-tab-sites'
366
367     def queryset(self, request):
368         return Site.select_by_user(request.user)
369
370 class UserInline(PlStackTabularInline):
371     model = User
372     fields = ['backend_status_icon', 'email', 'firstname', 'lastname']
373     readonly_fields = ('backend_status_icon', )
374     extra = 0
375     suit_classes = 'suit-tab suit-tab-users'
376
377     def queryset(self, request):
378         return User.select_by_user(request.user)
379
380 class SliceInline(PlStackTabularInline):
381     model = Slice
382     fields = ['backend_status_icon', 'name', 'site', 'serviceClass', 'service']
383     readonly_fields = ('backend_status_icon', )
384     extra = 0
385     suit_classes = 'suit-tab suit-tab-slices'
386
387     def queryset(self, request):
388         return Slice.select_by_user(request.user)
389
390 class NodeInline(PlStackTabularInline):
391     model = Node
392     extra = 0
393     suit_classes = 'suit-tab suit-tab-nodes'
394     fields = ['backend_status_icon', 'name', 'site_deployment']
395     readonly_fields = ('backend_status_icon', )
396
397 class DeploymentPrivilegeInline(PlStackTabularInline):
398     model = DeploymentPrivilege
399     extra = 0
400     suit_classes = 'suit-tab suit-tab-admin-only'
401     fields = ['backend_status_icon', 'user','role','deployment']
402     readonly_fields = ('backend_status_icon', )
403
404     def queryset(self, request):
405         return DeploymentPrivilege.select_by_user(request.user)
406
407 class ControllerSiteInline(PlStackTabularInline):
408     model = ControllerSite
409     extra = 0
410     suit_classes = 'suit-tab suit-tab-admin-only'
411     fields = ['controller', 'site', 'tenant_id']
412
413
414 class SitePrivilegeInline(PlStackTabularInline):
415     model = SitePrivilege
416     extra = 0
417     suit_classes = 'suit-tab suit-tab-siteprivileges'
418     fields = ['backend_status_icon', 'user','site', 'role']
419     readonly_fields = ('backend_status_icon', )
420
421     def formfield_for_foreignkey(self, db_field, request, **kwargs):
422         if db_field.name == 'site':
423             kwargs['queryset'] = Site.select_by_user(request.user)
424
425         if db_field.name == 'user':
426             kwargs['queryset'] = User.select_by_user(request.user)
427         return super(SitePrivilegeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
428
429     def queryset(self, request):
430         return SitePrivilege.select_by_user(request.user)
431
432 class SiteDeploymentInline(PlStackTabularInline):
433     model = SiteDeployment
434     extra = 0
435     suit_classes = 'suit-tab suit-tab-deployments'
436     fields = ['backend_status_icon', 'deployment','site', 'controller']
437     readonly_fields = ('backend_status_icon', )
438
439     def formfield_for_foreignkey(self, db_field, request, **kwargs):
440         if db_field.name == 'site':
441             kwargs['queryset'] = Site.select_by_user(request.user)
442
443         if db_field.name == 'deployment':
444             kwargs['queryset'] = Deployment.select_by_user(request.user)
445
446         if db_field.name == 'controller':
447             kwargs['queryset'] = Controller.select_by_user(request.user)
448
449         return super(SiteDeploymentInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
450
451     def queryset(self, request):
452         return SiteDeployment.select_by_user(request.user)
453
454
455 class SlicePrivilegeInline(PlStackTabularInline):
456     model = SlicePrivilege
457     suit_classes = 'suit-tab suit-tab-sliceprivileges'
458     extra = 0
459     fields = ('backend_status_icon', 'user', 'slice', 'role')
460     readonly_fields = ('backend_status_icon', )
461
462     def formfield_for_foreignkey(self, db_field, request, **kwargs):
463         if db_field.name == 'slice':
464            kwargs['queryset'] = Slice.select_by_user(request.user)
465         if db_field.name == 'user':
466            kwargs['queryset'] = User.select_by_user(request.user)
467
468         return super(SlicePrivilegeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
469
470     def queryset(self, request):
471         return SlicePrivilege.select_by_user(request.user)
472
473 class SliceNetworkInline(PlStackTabularInline):
474     model = Network.slices.through
475     selflink_fieldname = "network"
476     extra = 0
477     verbose_name = "Network Connection"
478     verbose_name_plural = "Network Connections"
479     suit_classes = 'suit-tab suit-tab-slicenetworks'
480     fields = ['backend_status_icon', 'network']
481     readonly_fields = ('backend_status_icon', )
482
483 class ImageDeploymentsInline(PlStackTabularInline):
484     model = ImageDeployments
485     extra = 0
486     verbose_name = "Image Deployments"
487     verbose_name_plural = "Image Deployments"
488     suit_classes = 'suit-tab suit-tab-imagedeployments'
489     fields = ['backend_status_icon', 'image', 'deployment']
490     readonly_fields = ['backend_status_icon']
491
492 class ControllerImagesInline(PlStackTabularInline):
493     model = ControllerImages
494     extra = 0
495     verbose_name = "Controller Images"
496     verbose_name_plural = "Controller Images"
497     suit_classes = 'suit-tab suit-tab-admin-only'
498     fields = ['backend_status_icon', 'image', 'controller', 'glance_image_id']
499     readonly_fields = ['backend_status_icon', 'glance_image_id']
500
501 class SliceRoleAdmin(PlanetStackBaseAdmin):
502     model = SliceRole
503     pass
504
505 class SiteRoleAdmin(PlanetStackBaseAdmin):
506     model = SiteRole
507     pass
508
509 class DeploymentAdminForm(forms.ModelForm):
510     sites = forms.ModelMultipleChoiceField(
511         queryset=Site.objects.all(),
512         required=False,
513         help_text="Select which sites are allowed to host nodes in this deployment",
514         widget=FilteredSelectMultiple(
515             verbose_name=('Sites'), is_stacked=False
516         )
517     )
518     images = forms.ModelMultipleChoiceField(
519         queryset=Image.objects.all(),
520         required=False,
521         help_text="Select which images should be deployed on this deployment",
522         widget=FilteredSelectMultiple(
523             verbose_name=('Images'), is_stacked=False
524         )
525     )
526     flavors = forms.ModelMultipleChoiceField(
527         queryset=Flavor.objects.all(),
528         required=False,
529         help_text="Select which flavors should be usable on this deployment",
530         widget=FilteredSelectMultiple(
531             verbose_name=('Flavors'), is_stacked=False
532         )
533     )
534     class Meta:
535         model = Deployment
536         many_to_many = ["flavors",]
537
538     def __init__(self, *args, **kwargs):
539       request = kwargs.pop('request', None)
540       super(DeploymentAdminForm, self).__init__(*args, **kwargs)
541
542       self.fields['accessControl'].initial = "allow site " + request.user.site.name
543
544       if self.instance and self.instance.pk:
545         self.fields['sites'].initial = [x.site for x in self.instance.sitedeployments.all()]
546         self.fields['images'].initial = [x.image for x in self.instance.imagedeployments.all()]
547         self.fields['flavors'].initial = self.instance.flavors.all()
548
549     def manipulate_m2m_objs(self, this_obj, selected_objs, all_relations, relation_class, local_attrname, foreign_attrname):
550         """ helper function for handling m2m relations from the MultipleChoiceField
551
552             this_obj: the source object we want to link from
553
554             selected_objs: a list of destination objects we want to link to
555
556             all_relations: the full set of relations involving this_obj, including ones we don't want
557
558             relation_class: the class that implements the relation from source to dest
559
560             local_attrname: field name representing this_obj in relation_class
561
562             foreign_attrname: field name representing selected_objs in relation_class
563
564             This function will remove all newobjclass relations from this_obj
565             that are not contained in selected_objs, and add any relations that
566             are in selected_objs but don't exist in the data model yet.
567         """
568
569         existing_dest_objs = []
570         for relation in list(all_relations):
571             if getattr(relation, foreign_attrname) not in selected_objs:
572                 #print "deleting site", sdp.site
573                 relation.delete()
574             else:
575                 existing_dest_objs.append(getattr(relation, foreign_attrname))
576
577         for dest_obj in selected_objs:
578             if dest_obj not in existing_dest_objs:
579                 #print "adding site", site
580                 kwargs = {foreign_attrname: dest_obj, local_attrname: this_obj}
581                 relation = relation_class(**kwargs)
582                 relation.save()
583
584     def save(self, commit=True):
585       deployment = super(DeploymentAdminForm, self).save(commit=False)
586
587       if commit:
588         deployment.save()
589         # this has to be done after save() if/when a deployment is first created
590         deployment.flavors = self.cleaned_data['flavors']
591
592       if deployment.pk:
593         # save_m2m() doesn't seem to work with 'through' relations. So we
594         #    create/destroy the through models ourselves. There has to be
595         #    a better way...
596
597         self.manipulate_m2m_objs(deployment, self.cleaned_data['sites'], deployment.sitedeployments.all(), SiteDeployment, "deployment", "site")
598         self.manipulate_m2m_objs(deployment, self.cleaned_data['images'], deployment.imagedeployments.all(), ImageDeployments, "deployment", "image")
599         # manipulate_m2m_objs doesn't work for Flavor/Deployment relationship
600         # so well handle that manually here
601         for flavor in deployment.flavors.all():
602             if getattr(flavor, 'name') not in self.cleaned_data['flavors']:
603                 deployment.flavors.remove(flavor)
604         for flavor in self.cleaned_data['flavors']:
605             if flavor not in deployment.flavors.all():
606                 flavor.deployments.add(deployment)
607
608       self.save_m2m()
609
610       return deployment
611
612 class DeploymentAdminROForm(DeploymentAdminForm):
613     def save(self, commit=True):
614         raise PermissionDenied
615
616 class SiteAssocInline(PlStackTabularInline):
617     model = Site.deployments.through
618     extra = 0
619     suit_classes = 'suit-tab suit-tab-sites'
620
621 class DeploymentAdmin(PlanetStackBaseAdmin):
622     model = Deployment
623     fieldList = ['backend_status_text', 'name', 'sites', 'images', 'flavors', 'accessControl']
624     fieldsets = [(None, {'fields': fieldList, 'classes':['suit-tab suit-tab-sites']})]
625     # node no longer directly connected to deployment
626     #inlines = [DeploymentPrivilegeInline,NodeInline,TagInline,ImageDeploymentsInline]
627     inlines = [DeploymentPrivilegeInline,TagInline,ImageDeploymentsInline]
628     list_display = ['backend_status_icon', 'name']
629     list_display_links = ('backend_status_icon', 'name', )
630     readonly_fields = ('backend_status_text', )
631
632     user_readonly_fields = ['name']
633
634     # nodes no longer direclty connected to deployments
635     #suit_form_tabs =(('sites','Deployment Details'),('nodes','Nodes'),('deploymentprivileges','Privileges'),('tags','Tags'),('imagedeployments','Images'))
636     suit_form_tabs =(('sites','Deployment Details'),('deploymentprivileges','Privileges'))
637
638     def get_form(self, request, obj=None, **kwargs):
639         if request.user.isReadOnlyUser():
640             kwargs["form"] = DeploymentAdminROForm
641         else:
642             kwargs["form"] = DeploymentAdminForm
643         adminForm = super(DeploymentAdmin,self).get_form(request, obj, **kwargs)
644
645         # from stackexchange: pass the request object into the form
646
647         class AdminFormMetaClass(adminForm):
648            def __new__(cls, *args, **kwargs):
649                kwargs['request'] = request
650                return adminForm(*args, **kwargs)
651
652         return AdminFormMetaClass
653
654 class ControllerAdminForm(forms.ModelForm):
655     sites = forms.ModelMultipleChoiceField(
656         queryset=Site.objects.all(),
657         required=False,
658         help_text="Select which sites are managed by this controller",
659         widget=FilteredSelectMultiple(
660             verbose_name=('Sites'), is_stacked=False
661         )
662     )
663
664     class Meta: 
665         model = Controller
666         
667     def __init__(self, *args, **kwargs):
668         request = kwargs.pop('request', None)
669         super(ControllerAdminForm, self).__init__(*args, **kwargs)  
670
671         if self.instance and self.instance.pk:
672             self.fields['sites'].initial = [x.site for x in self.instance.controllersite.all()]
673
674     def manipulate_m2m_objs(self, this_obj, selected_objs, all_relations, relation_class, local_attrname, foreign_attrname):
675         """ helper function for handling m2m relations from the MultipleChoiceField
676             this_obj: the source object we want to link from
677             selected_objs: a list of destination objects we want to link to
678             all_relations: the full set of relations involving this_obj, including ones we don't want
679             relation_class: the class that implements the relation from source to dest
680             local_attrname: field name representing this_obj in relation_class
681             foreign_attrname: field name representing selected_objs in relation_class
682             This function will remove all newobjclass relations from this_obj
683             that are not contained in selected_objs, and add any relations that
684             are in selected_objs but don't exist in the data model yet.
685         """
686         existing_dest_objs = []
687         for relation in list(all_relations):
688             if getattr(relation, foreign_attrname) not in selected_objs:
689                 #print "deleting site", sdp.site
690                 relation.delete()
691             else:
692                 existing_dest_objs.append(getattr(relation, foreign_attrname))
693
694         for dest_obj in selected_objs:
695             if dest_obj not in existing_dest_objs:
696                 #print "adding site", site
697                 kwargs = {foreign_attrname: dest_obj, local_attrname: this_obj}
698                 relation = relation_class(**kwargs)
699                 relation.save()
700
701     def save(self, commit=True):
702         controller = super(ControllerAdminForm, self).save(commit=False)
703         if commit:
704             controller.save()
705
706         if controller.pk:
707             # save_m2m() doesn't seem to work with 'through' relations. So we
708             #    create/destroy the through models ourselves. There has to be
709             #    a better way...
710             self.manipulate_m2m_objs(controller, self.cleaned_data['sites'], controller.controllersite.all(), ControllerSite, "controller", "site")
711             pass
712         
713         self.save_m2m()
714
715         return controller 
716       
717 class ControllerAdmin(PlanetStackBaseAdmin):
718     model = Controller 
719     fieldList = ['name', 'backend_type', 'version', 'auth_url', 'admin_user', 'admin_tenant','admin_password']
720     #fieldsets = [(None, {'fields': fieldList, 'classes':['suit-tab suit-tab-general']})]
721     inlines = [ControllerSiteInline] # ,ControllerImagesInline]
722     list_display = ['backend_status_icon', 'name', 'version', 'backend_type']
723     list_display_links = ('backend_status_icon', 'name', )
724     readonly_fields = ('backend_status_text',)
725
726     user_readonly_fields = []
727
728     def get_form(self, request, obj=None, **kwargs):
729         print self.fieldsets
730         if request.user.isReadOnlyUser():
731             kwargs["form"] = ControllerAdminROForm
732         else:
733             kwargs["form"] = ControllerAdminForm
734         adminForm = super(ControllerAdmin,self).get_form(request, obj, **kwargs)
735
736         # from stackexchange: pass the request object into the form
737
738         class AdminFormMetaClass(adminForm):
739            def __new__(cls, *args, **kwargs):
740                kwargs['request'] = request
741                return adminForm(*args, **kwargs)
742
743         return AdminFormMetaClass
744
745     def save_model(self, request, obj, form, change):
746         # update openstack connection to use this site/tenant
747         obj.save_by_user(request.user)
748                     
749     def delete_model(self, request, obj):
750         obj.delete_by_user(request.user)    
751
752 class ServiceAttrAsTabInline(PlStackTabularInline):
753     model = ServiceAttribute
754     fields = ['name','value']
755     extra = 0
756     suit_classes = 'suit-tab suit-tab-serviceattrs'
757
758 class ServiceAdmin(PlanetStackBaseAdmin):
759     list_display = ("backend_status_icon","name","description","versionNumber","enabled","published")
760     list_display_links = ('backend_status_icon', 'name', )
761     fieldList = ["backend_status_text","name","description","versionNumber","enabled","published"]
762     fieldsets = [(None, {'fields': fieldList, 'classes':['suit-tab suit-tab-general']})]
763     inlines = [ServiceAttrAsTabInline,SliceInline]
764     readonly_fields = ('backend_status_text', )
765
766     user_readonly_fields = fieldList
767
768     suit_form_tabs =(('general', 'Service Details'),
769         ('slices','Slices'),
770         ('serviceattrs','Additional Attributes'),
771     )
772
773 class SiteNodeInline(PlStackTabularInline):
774     model = Node
775     fields = ['name', 'site_deployment']
776     extra = 0
777     suit_classes = 'suit-tab suit-tab-nodes'
778
779 class SiteAdmin(PlanetStackBaseAdmin):
780     #fieldList = ['backend_status_text', 'name', 'site_url', 'enabled', 'is_public', 'login_base', 'accountLink','location']
781     fieldList = ['backend_status_text', 'name', 'site_url', 'enabled', 'is_public', 'login_base', 'location']
782     fieldsets = [
783         (None, {'fields': fieldList, 'classes':['suit-tab suit-tab-general']}),
784         #('Deployment Networks', {'fields': ['deployments'], 'classes':['suit-tab suit-tab-deployments']}),
785     ]
786     #readonly_fields = ['backend_status_text', 'accountLink']
787     readonly_fields = ['backend_status_text']
788
789     #user_readonly_fields = ['name', 'deployments','site_url', 'enabled', 'is_public', 'login_base', 'accountLink']
790     user_readonly_fields = ['name', 'deployments','site_url', 'enabled', 'is_public', 'login_base']
791
792     list_display = ('backend_status_icon', 'name', 'login_base','site_url', 'enabled')
793     list_display_links = ('backend_status_icon', 'name', )
794     filter_horizontal = ('deployments',)
795     inlines = [SliceInline,UserInline,TagInline, SitePrivilegeInline, SiteDeploymentInline, SiteNodeInline]
796     admin_inlines = [ControllerSiteInline]
797     search_fields = ['name']
798
799     @property
800     def suit_form_tabs(self):
801         tabs = [('general', 'Site Details'),
802             ('users','Users'),
803             ('siteprivileges','Privileges'),
804             ('deployments','Deployments'),
805             ('slices','Slices'),
806             ('nodes','Nodes'),
807         ]
808
809         request=getattr(_thread_locals, "request", None)
810         if request and request.user.is_admin:
811             tabs.append( ('admin-only', 'Admin-Only') )
812
813         return tabs
814
815     def queryset(self, request):
816         return Site.select_by_user(request.user)
817
818     def get_formsets(self, request, obj=None):
819         for inline in self.get_inline_instances(request, obj):
820             # hide MyInline in the add view
821             if obj is None:
822                 continue
823             if isinstance(inline, SliverInline):
824                 inline.model.caller = request.user
825             yield inline.get_formset(request, obj)
826
827     def accountLink(self, obj):
828         link_obj = obj.accounts.all()
829         if link_obj:
830             reverse_path = "admin:core_account_change"
831             url = reverse(reverse_path, args =(link_obj[0].id,))
832             return "<a href='%s'>%s</a>" % (url, "view billing details")
833         else:
834             return "no billing data for this site"
835     accountLink.allow_tags = True
836     accountLink.short_description = "Billing"
837
838     def save_model(self, request, obj, form, change):
839         # update openstack connection to use this site/tenant
840         obj.save_by_user(request.user) 
841
842     def delete_model(self, request, obj):
843         obj.delete_by_user(request.user)
844         
845
846 class SitePrivilegeAdmin(PlanetStackBaseAdmin):
847     fieldList = ['backend_status_text', 'user', 'site', 'role']
848     fieldsets = [
849         (None, {'fields': fieldList, 'classes':['collapse']})
850     ]
851     readonly_fields = ('backend_status_text', )
852     list_display = ('backend_status_icon', 'user', 'site', 'role')
853     list_display_links = list_display
854     user_readonly_fields = fieldList
855     user_readonly_inlines = []
856
857     def formfield_for_foreignkey(self, db_field, request, **kwargs):
858         if db_field.name == 'site':
859             if not request.user.is_admin:
860                 # only show sites where user is an admin or pi
861                 sites = set()
862                 for site_privilege in SitePrivilege.objects.filer(user=request.user):
863                     if site_privilege.role.role_type in ['admin', 'pi']:
864                         sites.add(site_privilege.site)
865                 kwargs['queryset'] = Site.objects.filter(site__in=list(sites))
866
867         if db_field.name == 'user':
868             if not request.user.is_admin:
869                 # only show users from sites where caller has admin or pi role
870                 roles = Role.objects.filter(role_type__in=['admin', 'pi'])
871                 site_privileges = SitePrivilege.objects.filter(user=request.user).filter(role__in=roles)
872                 sites = [site_privilege.site for site_privilege in site_privileges]
873                 site_privileges = SitePrivilege.objects.filter(site__in=sites)
874                 emails = [site_privilege.user.email for site_privilege in site_privileges]
875                 users = User.objects.filter(email__in=emails)
876                 kwargs['queryset'] = users
877
878         return super(SitePrivilegeAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
879
880     def queryset(self, request):
881         # admins can see all privileges. Users can only see privileges at sites
882         # where they have the admin role or pi role.
883         qs = super(SitePrivilegeAdmin, self).queryset(request)
884         #if not request.user.is_admin:
885         #    roles = Role.objects.filter(role_type__in=['admin', 'pi'])
886         #    site_privileges = SitePrivilege.objects.filter(user=request.user).filter(role__in=roles)
887         #    login_bases = [site_privilege.site.login_base for site_privilege in site_privileges]
888         #    sites = Site.objects.filter(login_base__in=login_bases)
889         #    qs = qs.filter(site__in=sites)
890         return qs
891
892 class SliceForm(forms.ModelForm):
893     class Meta:
894         model = Slice
895         widgets = {
896             'service': LinkedSelect
897         }
898
899     def clean(self):
900         cleaned_data = super(SliceForm, self).clean()
901         name = cleaned_data.get('name')
902         site = cleaned_data.get('site')
903         slice_id = self.instance.id
904         if not site and slice_id:
905             site = Slice.objects.get(id=slice_id).site
906         if (not isinstance(site,Site)):
907             # previous code indicates 'site' could be a site_id and not a site?
908             site = Slice.objects.get(id=site.id)
909         if not name.startswith(site.login_base):
910             raise forms.ValidationError('slice name must begin with %s' % site.login_base)
911         return cleaned_data
912
913 class ControllerSliceInline(PlStackTabularInline):
914     model = ControllerSlice
915     extra = 0
916     verbose_name = "Controller Slices"
917     verbose_name_plural = "Controller Slices"
918     suit_classes = 'suit-tab suit-tab-admin-only'
919     fields = ['backend_status_icon', 'controller', 'tenant_id']
920     readonly_fields = ('backend_status_icon', 'controller' )
921
922 class SliceAdmin(PlanetStackBaseAdmin):
923     form = SliceForm
924     fieldList = ['backend_status_text', 'site', 'name', 'serviceClass', 'enabled','description', 'service', 'slice_url', 'max_slivers']
925     fieldsets = [('Slice Details', {'fields': fieldList, 'classes':['suit-tab suit-tab-general']}),]
926     readonly_fields = ('backend_status_text', )
927     list_display = ('backend_status_icon', 'name', 'site','serviceClass', 'slice_url', 'max_slivers')
928     list_display_links = ('backend_status_icon', 'name', )
929     inlines = [SlicePrivilegeInline,SliverInline, TagInline, ReservationInline,SliceNetworkInline]
930     admin_inlines = [ControllerSliceInline]
931
932     user_readonly_fields = fieldList
933
934     @property
935     def suit_form_tabs(self):
936         tabs =[('general', 'Slice Details'),
937           ('slicenetworks','Networks'),
938           ('sliceprivileges','Privileges'),
939           ('slivers','Slivers'),
940           #('reservations','Reservations'), 
941           ('tags','Tags'),
942           ]
943
944         request=getattr(_thread_locals, "request", None)
945         if request and request.user.is_admin:
946             tabs.append( ('admin-only', 'Admin-Only') )
947
948         return tabs
949     
950     def add_view(self, request, form_url='', extra_context=None):
951         # revert to default read-only fields
952         self.readonly_fields = ('backend_status_text',)
953         return super(SliceAdmin, self).add_view(request, form_url, extra_context=extra_context)
954
955     def change_view(self, request, object_id, form_url='', extra_context=None):
956         # cannot change the site of an existing slice so make the site field read only
957         if object_id:
958             self.readonly_fields = ('backend_status_text','site')
959         return super(SliceAdmin, self).change_view(request, object_id, form_url)
960
961     def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
962         deployment_nodes = []
963         for node in Node.objects.all():
964             deployment_nodes.append( (node.site_deployment.deployment.id, node.id, node.name) )
965
966         deployment_flavors = []
967         for flavor in Flavor.objects.all():
968             for deployment in flavor.deployments.all():
969                 deployment_flavors.append( (deployment.id, flavor.id, flavor.name) )
970
971         deployment_images = []
972         for image in Image.objects.all():
973             for deployment_image in image.imagedeployments.all():
974                 deployment_images.append( (deployment_image.deployment.id, image.id, image.name) )
975
976         site_login_bases = []
977         for site in Site.objects.all():
978             site_login_bases.append((site.id, site.login_base))
979
980         context["deployment_nodes"] = deployment_nodes
981         context["deployment_flavors"] = deployment_flavors
982         context["deployment_images"] = deployment_images
983         context["site_login_bases"] = site_login_bases
984         return super(SliceAdmin, self).render_change_form(request, context, add, change, form_url, obj)
985
986     def formfield_for_foreignkey(self, db_field, request, **kwargs):
987         if db_field.name == 'site':
988             kwargs['queryset'] = Site.select_by_user(request.user)
989             kwargs['widget'] = forms.Select(attrs={'onChange': "update_slice_prefix(this, $($(this).closest('fieldset')[0]).find('.field-name input')[0].id)"})
990
991         return super(SliceAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
992
993     def queryset(self, request):
994         # admins can see all keys. Users can only see slices they belong to.
995         return Slice.select_by_user(request.user)
996
997     def get_formsets(self, request, obj=None):
998         for inline in self.get_inline_instances(request, obj):
999             # hide MyInline in the add view
1000             if obj is None:
1001                 continue
1002             if isinstance(inline, SliverInline):
1003                 inline.model.caller = request.user
1004             yield inline.get_formset(request, obj)
1005
1006 class SlicePrivilegeAdmin(PlanetStackBaseAdmin):
1007     fieldsets = [
1008         (None, {'fields': ['backend_status_text', 'user', 'slice', 'role']})
1009     ]
1010     readonly_fields = ('backend_status_text', )
1011     list_display = ('backend_status_icon', 'user', 'slice', 'role')
1012     list_display_links = list_display
1013
1014     user_readonly_fields = ['user', 'slice', 'role']
1015     user_readonly_inlines = []
1016
1017     def formfield_for_foreignkey(self, db_field, request, **kwargs):
1018         if db_field.name == 'slice':
1019             kwargs['queryset'] = Slice.select_by_user(request.user)
1020         
1021         if db_field.name == 'user':
1022             kwargs['queryset'] = User.select_by_user(request.user)
1023
1024         return super(SlicePrivilegeAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
1025
1026     def queryset(self, request):
1027         # admins can see all memberships. Users can only see memberships of
1028         # slices where they have the admin role.
1029         return SlicePrivilege.select_by_user(request.user)
1030
1031     def save_model(self, request, obj, form, change):
1032         # update openstack connection to use this site/tenant
1033         auth = request.session.get('auth', {})
1034         auth['tenant'] = obj.slice.slicename
1035         obj.os_manager = OpenStackManager(auth=auth, caller=request.user)
1036         obj.save()
1037
1038     def delete_model(self, request, obj):
1039         # update openstack connection to use this site/tenant
1040         auth = request.session.get('auth', {})
1041         auth['tenant'] = obj.slice.slicename
1042         obj.os_manager = OpenStackManager(auth=auth, caller=request.user)
1043         obj.delete()
1044
1045
1046 class ImageAdmin(PlanetStackBaseAdmin):
1047
1048     fieldsets = [('Image Details',
1049                    {'fields': ['backend_status_text', 'name', 'disk_format', 'container_format'],
1050                     'classes': ['suit-tab suit-tab-general']})
1051                ]
1052     readonly_fields = ('backend_status_text', )
1053
1054     suit_form_tabs =(('general','Image Details'),('slivers','Slivers'),('imagedeployments','Deployments'), ('controllerimages', 'Controllers'))
1055
1056     inlines = [SliverInline, ControllerImagesInline]
1057
1058     user_readonly_fields = ['name', 'disk_format', 'container_format']
1059
1060     list_display = ['backend_status_icon', 'name']
1061     list_display_links = ('backend_status_icon', 'name', )
1062
1063 class NodeForm(forms.ModelForm):
1064     class Meta:
1065         widgets = {
1066             'site': LinkedSelect,
1067             'deployment': LinkedSelect
1068         }
1069
1070 class NodeAdmin(PlanetStackBaseAdmin):
1071     form = NodeForm
1072     list_display = ('backend_status_icon', 'name', 'site_deployment')
1073     list_display_links = ('backend_status_icon', 'name', )
1074     list_filter = ('site_deployment',)
1075
1076     inlines = [TagInline,SliverInline]
1077     fieldsets = [('Node Details', {'fields': ['backend_status_text', 'name','site_deployment'], 'classes':['suit-tab suit-tab-details']})]
1078     readonly_fields = ('backend_status_text', )
1079
1080     user_readonly_fields = ['name','site_deployment']
1081     user_readonly_inlines = [TagInline,SliverInline]
1082
1083     suit_form_tabs =(('details','Node Details'),('slivers','Slivers'))
1084
1085
1086 class SliverForm(forms.ModelForm):
1087     class Meta:
1088         model = Sliver
1089         ip = forms.CharField(widget=PlainTextWidget)
1090         instance_name = forms.CharField(widget=PlainTextWidget)
1091         widgets = {
1092             'ip': PlainTextWidget(),
1093             'instance_name': PlainTextWidget(),
1094             'instance_id': PlainTextWidget(),
1095             'slice': LinkedSelect,
1096             'deployment': LinkedSelect,
1097             'node': LinkedSelect,
1098             'image': LinkedSelect
1099         }
1100
1101 class TagAdmin(PlanetStackBaseAdmin):
1102     list_display = ['backend_status_icon', 'service', 'name', 'value', 'content_type', 'content_object',]
1103     list_display_links = list_display
1104     user_readonly_fields = ['service', 'name', 'value', 'content_type', 'content_object',]
1105     user_readonly_inlines = []
1106
1107 class SliverAdmin(PlanetStackBaseAdmin):
1108     form = SliverForm
1109     fieldsets = [
1110         ('Sliver Details', {'fields': ['backend_status_text', 'slice', 'deployment', 'node', 'ip', 'instance_id', 'instance_name', 'flavor', 'image', 'ssh_command'], 'classes': ['suit-tab suit-tab-general'], })
1111     ]
1112     readonly_fields = ('backend_status_text', 'ssh_command', )
1113     list_display = ['backend_status_icon', 'ip', 'instance_id', 'instance_name', 'slice', 'flavor', 'image', 'node', 'deployment']
1114     list_display_links = ('backend_status_icon', 'ip',)
1115
1116     suit_form_tabs =(('general', 'Sliver Details'),)
1117
1118     inlines = [TagInline]
1119
1120     user_readonly_fields = ['slice', 'deployment', 'node', 'ip', 'instance_name', 'flavor', 'image']
1121
1122     def ssh_command(self, obj):
1123         ssh_command = obj.get_ssh_command()
1124         if ssh_command:
1125             return ssh_command
1126         else:
1127             return "(not available)"
1128
1129     def formfield_for_foreignkey(self, db_field, request, **kwargs):
1130         if db_field.name == 'slice':
1131             kwargs['queryset'] = Slice.select_by_user(request.user)
1132
1133         return super(SliverAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
1134
1135     def queryset(self, request):
1136         # admins can see all slivers. Users can only see slivers of
1137         # the slices they belong to.
1138         return Sliver.select_by_user(request.user)
1139
1140
1141     def get_formsets(self, request, obj=None):
1142         # make some fields read only if we are updating an existing record
1143         if obj == None:
1144             self.readonly_fields = ('backend_status_text', 'ssh_command', )
1145         else:
1146             self.readonly_fields = ('backend_status_text', 'ssh_command',)
1147
1148         for inline in self.get_inline_instances(request, obj):
1149             # hide MyInline in the add view
1150             if obj is None:
1151                 continue
1152             if isinstance(inline, SliverInline):
1153                 inline.model.caller = request.user
1154             yield inline.get_formset(request, obj)
1155
1156     #def save_model(self, request, obj, form, change):
1157     #    # update openstack connection to use this site/tenant
1158     #    auth = request.session.get('auth', {})
1159     #    auth['tenant'] = obj.slice.name
1160     #    obj.os_manager = OpenStackManager(auth=auth, caller=request.user)
1161     #    obj.creator = request.user
1162     #    obj.save()
1163
1164     #def delete_model(self, request, obj):
1165     #    # update openstack connection to use this site/tenant
1166     #    auth = request.session.get('auth', {})
1167     #    auth['tenant'] = obj.slice.name
1168     #    obj.os_manager = OpenStackManager(auth=auth, caller=request.user)
1169     #    obj.delete()
1170
1171 class UserCreationForm(forms.ModelForm):
1172     """A form for creating new users. Includes all the required
1173     fields, plus a repeated password."""
1174     password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
1175     password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput)
1176
1177     class Meta:
1178         model = User
1179         fields = ('email', 'firstname', 'lastname', 'phone', 'public_key')
1180
1181     def clean_password2(self):
1182         # Check that the two password entries match
1183         password1 = self.cleaned_data.get("password1")
1184         password2 = self.cleaned_data.get("password2")
1185         if password1 and password2 and password1 != password2:
1186             raise forms.ValidationError("Passwords don't match")
1187         return password2
1188
1189     def save(self, commit=True):
1190         # Save the provided password in hashed format
1191         user = super(UserCreationForm, self).save(commit=False)
1192         user.password = self.cleaned_data["password1"]
1193         #user.set_password(self.cleaned_data["password1"])
1194         if commit:
1195             user.save()
1196         return user
1197
1198
1199 class UserChangeForm(forms.ModelForm):
1200     """A form for updating users. Includes all the fields on
1201     the user, but replaces the password field with admin's
1202     password hash display field.
1203     """
1204     password = ReadOnlyPasswordHashField(label='Password',
1205                    help_text= '<a href=\"password/\">Change Password</a>.')
1206
1207     class Meta:
1208         model = User
1209         widgets = { 'public_key': UploadTextareaWidget, }
1210
1211     def clean_password(self):
1212         # Regardless of what the user provides, return the initial value.
1213         # This is done here, rather than on the field, because the
1214         # field does not have access to the initial value
1215         return self.initial["password"]
1216
1217 class UserDashboardViewInline(PlStackTabularInline):
1218     model = UserDashboardView
1219     extra = 0
1220     suit_classes = 'suit-tab suit-tab-dashboards'
1221     fields = ['user', 'dashboardView', 'order']
1222
1223 class ControllerUserInline(PlStackTabularInline):
1224     model = ControllerUser
1225     extra = 0
1226     suit_classes = 'suit-tab suit-tab-admin-only'
1227     fields = ['controller', 'user', 'kuser_id']
1228     readonly_fields=['controller']
1229
1230
1231 class UserAdmin(PermissionCheckingAdminMixin, UserAdmin):
1232     # Note: Make sure PermissionCheckingAdminMixin is listed before
1233     # admin.ModelAdmin in the class declaration.
1234
1235     class Meta:
1236         app_label = "core"
1237
1238     # The forms to add and change user instances
1239     form = UserChangeForm
1240     add_form = UserCreationForm
1241
1242     # The fields to be used in displaying the User model.
1243     # These override the definitions on the base UserAdmin
1244     # that reference specific fields on auth.User.
1245     list_display = ('backend_status_icon', 'email', 'firstname', 'lastname', 'site', 'last_login')
1246     list_display_links = ("email",)
1247     list_filter = ('site',)
1248     inlines = [SlicePrivilegeInline,SitePrivilegeInline,UserDashboardViewInline]
1249     admin_inlines = [ControllerUserInline]
1250     fieldListLoginDetails = ['backend_status_text', 'email','site','password','is_active','is_readonly','is_admin','public_key']
1251     fieldListContactInfo = ['firstname','lastname','phone','timezone']
1252
1253     fieldsets = (
1254         ('Login Details', {'fields': ['backend_status_text', 'email', 'site','password', 'is_active', 'is_readonly', 'is_admin', 'public_key'], 'classes':['suit-tab suit-tab-general']}),
1255         ('Contact Information', {'fields': ('firstname','lastname','phone', 'timezone'), 'classes':['suit-tab suit-tab-contact']}),
1256         #('Dashboard Views', {'fields': ('dashboards',), 'classes':['suit-tab suit-tab-dashboards']}),
1257         #('Important dates', {'fields': ('last_login',)}),
1258     )
1259     add_fieldsets = (
1260         (None, {
1261             'classes': ('wide',),
1262             'fields': ('site', 'email', 'firstname', 'lastname', 'is_admin', 'is_readonly', 'phone', 'public_key','password1', 'password2')},
1263         ),
1264     )
1265     readonly_fields = ('backend_status_text', )
1266     search_fields = ('email',)
1267     ordering = ('email',)
1268     filter_horizontal = ()
1269
1270     user_readonly_fields = fieldListLoginDetails + fieldListContactInfo
1271
1272     @property
1273     def suit_form_tabs(self):
1274         if getattr(_thread_locals, "obj", None) is None:
1275             return []
1276         else:
1277             tabs = [('general','Login Details'),
1278                          ('contact','Contact Information'),
1279                          ('sliceprivileges','Slice Privileges'),
1280                          ('siteprivileges','Site Privileges'),
1281                          ('dashboards','Dashboard Views')]
1282
1283             request=getattr(_thread_locals, "request", None)
1284             if request and request.user.is_admin:
1285                 tabs.append( ('admin-only', 'Admin-Only') )
1286
1287             return tabs
1288
1289     def formfield_for_foreignkey(self, db_field, request, **kwargs):
1290         if db_field.name == 'site':
1291             kwargs['queryset'] = Site.select_by_user(request.user)
1292
1293         return super(UserAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
1294
1295     def queryset(self, request):
1296         return User.select_by_user(request.user)
1297
1298     def get_form(self, request, obj=None, **kwargs):
1299         # copy login details list
1300         login_details_fields = list(self.fieldListLoginDetails)
1301         if not request.user.is_admin:
1302             # only admins can see 'is_admin' and 'is_readonly' fields 
1303             if 'is_admin' in login_details_fields:
1304                 login_details_fields.remove('is_admin')
1305             if 'is_readonly' in login_details_fields:
1306                 login_details_fields.remove('is_readonly') 
1307             #if len(request.user.siteprivileges.filter(role__role = 'pi')) > 0:
1308                 # only admins and pis can change a user's site  
1309             #    self.readonly_fields = ('backend_status_text', 'site') 
1310         self.fieldsets = (
1311             ('Login Details', {'fields': login_details_fields, 'classes':['suit-tab suit-tab-general']}),
1312             ('Contact Information', {'fields': self.fieldListContactInfo, 'classes':['suit-tab suit-tab-contact']}),
1313         )
1314         return super(UserAdmin, self).get_form(request, obj, **kwargs)     
1315
1316 class ControllerDashboardViewInline(PlStackTabularInline):
1317     model = ControllerDashboardView
1318     extra = 0
1319     fields = ["controller", "url"]
1320     suit_classes = 'suit-tab suit-tab-controllers'
1321
1322 class DashboardViewAdmin(PlanetStackBaseAdmin):
1323     fieldsets = [('Dashboard View Details',
1324                    {'fields': ['backend_status_text', 'name', 'url', 'enabled'],
1325                     'classes': ['suit-tab suit-tab-general']})
1326                ]
1327     list_display = ["name", "enabled", "url"]
1328     readonly_fields = ('backend_status_text', )
1329     inlines = [ControllerDashboardViewInline]
1330
1331     suit_form_tabs =(('general','Dashboard View Details'),
1332                      ('controllers', 'Per-controller Dashboard Details'))
1333
1334 class ServiceResourceInline(PlStackTabularInline):
1335     model = ServiceResource
1336     extra = 0
1337
1338 class ServiceClassAdmin(PlanetStackBaseAdmin):
1339     list_display = ('backend_status_icon', 'name', 'commitment', 'membershipFee')
1340     list_display_links = ('backend_status_icon', 'name', )
1341     inlines = [ServiceResourceInline]
1342
1343     user_readonly_fields = ['name', 'commitment', 'membershipFee']
1344     user_readonly_inlines = []
1345
1346 class ReservedResourceInline(PlStackTabularInline):
1347     model = ReservedResource
1348     extra = 0
1349     suit_classes = 'suit-tab suit-tab-reservedresources'
1350
1351     def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
1352         field = super(ReservedResourceInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
1353
1354         if db_field.name == 'resource':
1355             # restrict resources to those that the slice's service class allows
1356             if request._slice is not None:
1357                 field.queryset = field.queryset.filter(serviceClass = request._slice.serviceClass, calendarReservable=True)
1358                 if len(field.queryset) > 0:
1359                     field.initial = field.queryset.all()[0]
1360             else:\r
1361                 field.queryset = field.queryset.none()\r
1362         elif db_field.name == 'sliver':\r
1363             # restrict slivers to those that belong to the slice\r
1364             if request._slice is not None:\r
1365                 field.queryset = field.queryset.filter(slice = request._slice)
1366             else:
1367                 field.queryset = field.queryset.none()\r
1368 \r
1369         return field
1370
1371     def queryset(self, request):
1372         return ReservedResource.select_by_user(request.user)
1373
1374 class ReservationChangeForm(forms.ModelForm):
1375     class Meta:
1376         model = Reservation
1377         widgets = {
1378             'slice' : LinkedSelect
1379         }
1380
1381 class ReservationAddForm(forms.ModelForm):
1382     slice = forms.ModelChoiceField(queryset=Slice.objects.all(), widget=forms.Select(attrs={"onChange":"document.getElementById('id_refresh').value=1; submit()"}))
1383     refresh = forms.CharField(widget=forms.HiddenInput())
1384
1385     class Media:
1386        css = {'all': ('planetstack.css',)}   # .field-refresh { display: none; }
1387
1388     def clean_slice(self):
1389         slice = self.cleaned_data.get("slice")
1390         x = ServiceResource.objects.filter(serviceClass = slice.serviceClass, calendarReservable=True)
1391         if len(x) == 0:
1392             raise forms.ValidationError("The slice you selected does not have a service class that allows reservations")
1393         return slice
1394
1395     class Meta:
1396         model = Reservation
1397         widgets = {
1398             'slice' : LinkedSelect
1399         }
1400
1401
1402 class ReservationAddRefreshForm(ReservationAddForm):
1403     """ This form is displayed when the Reservation Form receives an update
1404         from the Slice dropdown onChange handler. It doesn't validate the
1405         data and doesn't save the data. This will cause the form to be
1406         redrawn.
1407     """
1408
1409     """ don't validate anything other than slice """
1410     dont_validate_fields = ("startTime", "duration")
1411
1412     def full_clean(self):
1413         result = super(ReservationAddForm, self).full_clean()
1414
1415         for fieldname in self.dont_validate_fields:
1416             if fieldname in self._errors:
1417                 del self._errors[fieldname]
1418
1419         return result
1420
1421     """ don't save anything """
1422     def is_valid(self):
1423         return False
1424
1425 class ReservationAdmin(PlanetStackBaseAdmin):
1426     fieldList = ['backend_status_text', 'slice', 'startTime', 'duration']
1427     fieldsets = [('Reservation Details', {'fields': fieldList, 'classes': ['suit-tab suit-tab-general']})]
1428     readonly_fields = ('backend_status_text', )
1429     list_display = ('startTime', 'duration')
1430     form = ReservationAddForm
1431
1432     suit_form_tabs = (('general','Reservation Details'), ('reservedresources','Reserved Resources'))
1433
1434     inlines = [ReservedResourceInline]
1435     user_readonly_fields = fieldList
1436
1437     def add_view(self, request, form_url='', extra_context=None):
1438         timezone.activate(request.user.timezone)
1439         request._refresh = False
1440         request._slice = None
1441         if request.method == 'POST':
1442             # "refresh" will be set to "1" if the form was submitted due to
1443             # a change in the Slice dropdown.
1444             if request.POST.get("refresh","1") == "1":
1445                 request._refresh = True
1446                 request.POST["refresh"] = "0"
1447
1448             # Keep track of the slice that was selected, so the
1449             # reservedResource inline can filter items for the slice.
1450             request._slice = request.POST.get("slice",None)
1451             if (request._slice is not None):
1452                 request._slice = Slice.objects.get(id=request._slice)
1453
1454         result =  super(ReservationAdmin, self).add_view(request, form_url, extra_context)
1455         return result
1456
1457     def changelist_view(self, request, extra_context = None):
1458         timezone.activate(request.user.timezone)
1459         return super(ReservationAdmin, self).changelist_view(request, extra_context)
1460
1461     def get_form(self, request, obj=None, **kwargs):
1462         request._obj_ = obj
1463         if obj is not None:
1464             # For changes, set request._slice to the slice already set in the
1465             # object.
1466             request._slice = obj.slice
1467             self.form = ReservationChangeForm
1468         else:
1469             if getattr(request, "_refresh", False):
1470                 self.form = ReservationAddRefreshForm
1471             else:
1472                 self.form = ReservationAddForm
1473         return super(ReservationAdmin, self).get_form(request, obj, **kwargs)
1474
1475     def get_readonly_fields(self, request, obj=None):
1476         if (obj is not None):
1477             # Prevent slice from being changed after the reservation has been
1478             # created.
1479             return ['slice']
1480         else:
1481             return []
1482
1483     def queryset(self, request):
1484         return Reservation.select_by_user(request.user)
1485
1486 class NetworkParameterTypeAdmin(PlanetStackBaseAdmin):
1487     list_display = ("backend_status_icon", "name", )
1488     list_display_links = ('backend_status_icon', 'name', )
1489     user_readonly_fields = ['name']
1490     user_readonly_inlines = []
1491
1492 class RouterAdmin(PlanetStackBaseAdmin):
1493     list_display = ("backend_status_icon", "name", )
1494     list_display_links = ('backend_status_icon', 'name', )
1495     user_readonly_fields = ['name']
1496     user_readonly_inlines = []
1497
1498 class RouterInline(PlStackTabularInline):
1499     model = Router.networks.through
1500     extra = 0
1501     verbose_name_plural = "Routers"
1502     verbose_name = "Router"
1503     suit_classes = 'suit-tab suit-tab-routers'
1504
1505 class NetworkParameterInline(PlStackGenericTabularInline):
1506     model = NetworkParameter
1507     extra = 0
1508     verbose_name_plural = "Parameters"
1509     verbose_name = "Parameter"
1510     suit_classes = 'suit-tab suit-tab-netparams'
1511     fields = ['backend_status_icon', 'parameter', 'value']
1512     readonly_fields = ('backend_status_icon', )
1513
1514 class NetworkSliversInline(PlStackTabularInline):
1515     fields = ['backend_status_icon', 'network','sliver','ip']
1516     readonly_fields = ("backend_status_icon", "ip", )
1517     model = NetworkSliver
1518     selflink_fieldname = "sliver"
1519     extra = 0
1520     verbose_name_plural = "Slivers"
1521     verbose_name = "Sliver"
1522     suit_classes = 'suit-tab suit-tab-networkslivers'
1523
1524 class NetworkSlicesInline(PlStackTabularInline):
1525     model = NetworkSlice
1526     selflink_fieldname = "slice"
1527     extra = 0
1528     verbose_name_plural = "Slices"
1529     verbose_name = "Slice"
1530     suit_classes = 'suit-tab suit-tab-networkslices'
1531     fields = ['backend_status_icon', 'network','slice']
1532     readonly_fields = ('backend_status_icon', )
1533
1534 class ControllerNetworkInline(PlStackTabularInline):
1535     model = ControllerNetwork
1536     extra = 0
1537     verbose_name_plural = "Controller Networks"
1538     verbose_name = "Controller Network"
1539     suit_classes = 'suit-tab suit-tab-admin-only'
1540     fields = ['backend_status_icon', 'controller','net_id','subnet_id']
1541     readonly_fields = ('backend_status_icon', )
1542
1543 class NetworkForm(forms.ModelForm):
1544     class Meta:
1545         model = Network
1546         widgets = {
1547             'topologyParameters': UploadTextareaWidget,
1548             'controllerParameters': UploadTextareaWidget,
1549         }
1550
1551 class NetworkAdmin(PlanetStackBaseAdmin):
1552     list_display = ("backend_status_icon", "name", "subnet", "ports", "labels")
1553     list_display_links = ('backend_status_icon', 'name', )
1554     readonly_fields = ("subnet", )
1555
1556     inlines = [NetworkParameterInline, NetworkSliversInline, NetworkSlicesInline, RouterInline]
1557     admin_inlines = [ControllerNetworkInline]
1558
1559     form=NetworkForm
1560
1561     fieldsets = [
1562         (None, {'fields': ['backend_status_text', 'name','template','ports','labels','owner','guaranteed_bandwidth', 'permit_all_slices','permitted_slices','network_id','router_id','subnet_id','subnet'],
1563                 'classes':['suit-tab suit-tab-general']}),
1564         (None, {'fields': ['topology_parameters', 'controller_url', 'controller_parameters'],
1565                 'classes':['suit-tab suit-tab-sdn']}),
1566                 ]
1567
1568     readonly_fields = ('backend_status_text', )
1569     user_readonly_fields = ['name','template','ports','labels','owner','guaranteed_bandwidth', 'permit_all_slices','permitted_slices','network_id','router_id','subnet_id','subnet']
1570
1571     @property
1572     def suit_form_tabs(self):
1573         tabs=[('general','Network Details'),
1574             ('sdn', 'SDN Configuration'),
1575             ('netparams', 'Parameters'),
1576             ('networkslivers','Slivers'),
1577             ('networkslices','Slices'),
1578             ('routers','Routers'),
1579         ]
1580
1581         request=getattr(_thread_locals, "request", None)
1582         if request and request.user.is_admin:
1583             tabs.append( ('admin-only', 'Admin-Only') )
1584
1585         return tabs
1586
1587
1588 class NetworkTemplateAdmin(PlanetStackBaseAdmin):
1589     list_display = ("backend_status_icon", "name", "guaranteed_bandwidth", "visibility")
1590     list_display_links = ('backend_status_icon', 'name', )
1591     user_readonly_fields = ["name", "guaranteed_bandwidth", "visibility"]
1592     user_readonly_inlines = []
1593     fieldsets = [
1594         (None, {'fields': ['name', 'description', 'guaranteed_bandwidth', 'visibility', 'translation', 'shared_network_name', 'shared_network_id', 'topology_kind', 'controller_kind'],
1595                 'classes':['suit-tab suit-tab-general']}),]
1596     suit_form_tabs = (('general','Network Template Details'), )
1597
1598 class FlavorAdmin(PlanetStackBaseAdmin):
1599     list_display = ("backend_status_icon", "name", "flavor", "order", "default")
1600     list_display_links = ("backend_status_icon", "name")
1601     user_readonly_fields = ("name", "flavor")
1602     fields = ("name", "description", "flavor", "order", "default")
1603
1604 # register a signal that caches the user's credentials when they log in
1605 def cache_credentials(sender, user, request, **kwds):
1606     auth = {'username': request.POST['username'],
1607             'password': request.POST['password']}
1608     request.session['auth'] = auth
1609 user_logged_in.connect(cache_credentials)
1610
1611 def dollar_field(fieldName, short_description):
1612     def newFunc(self, obj):
1613         try:
1614             x= "$ %0.2f" % float(getattr(obj, fieldName, 0.0))
1615         except:
1616             x=getattr(obj, fieldName, 0.0)
1617         return x
1618     newFunc.short_description = short_description
1619     return newFunc
1620
1621 def right_dollar_field(fieldName, short_description):
1622     def newFunc(self, obj):
1623         try:
1624             #x= '<div align=right style="width:6em">$ %0.2f</div>' % float(getattr(obj, fieldName, 0.0))
1625             x= '<div align=right>$ %0.2f</div>' % float(getattr(obj, fieldName, 0.0))
1626         except:
1627             x=getattr(obj, fieldName, 0.0)
1628         return x
1629     newFunc.short_description = short_description
1630     newFunc.allow_tags = True
1631     return newFunc
1632
1633 class InvoiceChargeInline(PlStackTabularInline):
1634     model = Charge
1635     extra = 0
1636     verbose_name_plural = "Charges"
1637     verbose_name = "Charge"
1638     exclude = ['account']
1639     fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1640     readonly_fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1641     can_delete = False
1642     max_num = 0
1643
1644     dollar_amount = right_dollar_field("amount", "Amount")
1645
1646 class InvoiceAdmin(admin.ModelAdmin):
1647     list_display = ("date", "account")
1648
1649     inlines = [InvoiceChargeInline]
1650
1651     fields = ["date", "account", "dollar_amount"]
1652     readonly_fields = ["date", "account", "dollar_amount"]
1653
1654     dollar_amount = dollar_field("amount", "Amount")
1655
1656 class InvoiceInline(PlStackTabularInline):
1657     model = Invoice
1658     extra = 0
1659     verbose_name_plural = "Invoices"
1660     verbose_name = "Invoice"
1661     fields = ["date", "dollar_amount"]
1662     readonly_fields = ["date", "dollar_amount"]
1663     suit_classes = 'suit-tab suit-tab-accountinvoice'
1664     can_delete=False
1665     max_num=0
1666
1667     dollar_amount = right_dollar_field("amount", "Amount")
1668
1669 class PendingChargeInline(PlStackTabularInline):
1670     model = Charge
1671     extra = 0
1672     verbose_name_plural = "Charges"
1673     verbose_name = "Charge"
1674     exclude = ["invoice"]
1675     fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1676     readonly_fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1677     suit_classes = 'suit-tab suit-tab-accountpendingcharges'
1678     can_delete=False
1679     max_num=0
1680
1681     def queryset(self, request):
1682         qs = super(PendingChargeInline, self).queryset(request)
1683         qs = qs.filter(state="pending")
1684         return qs
1685
1686     dollar_amount = right_dollar_field("amount", "Amount")
1687
1688 class PaymentInline(PlStackTabularInline):
1689     model=Payment
1690     extra = 1
1691     verbose_name_plural = "Payments"
1692     verbose_name = "Payment"
1693     fields = ["date", "dollar_amount"]
1694     readonly_fields = ["date", "dollar_amount"]
1695     suit_classes = 'suit-tab suit-tab-accountpayments'
1696     can_delete=False
1697     max_num=0
1698
1699     dollar_amount = right_dollar_field("amount", "Amount")
1700
1701 class AccountAdmin(admin.ModelAdmin):
1702     list_display = ("site", "balance_due")
1703
1704     inlines = [InvoiceInline, PaymentInline, PendingChargeInline]
1705
1706     fieldsets = [
1707         (None, {'fields': ['site', 'dollar_balance_due', 'dollar_total_invoices', 'dollar_total_payments'],'classes':['suit-tab suit-tab-general']}),]
1708
1709     readonly_fields = ['site', 'dollar_balance_due', 'dollar_total_invoices', 'dollar_total_payments']
1710
1711     suit_form_tabs =(
1712         ('general','Account Details'),
1713         ('accountinvoice', 'Invoices'),
1714         ('accountpayments', 'Payments'),
1715         ('accountpendingcharges','Pending Charges'),
1716     )
1717
1718     dollar_balance_due = dollar_field("balance_due", "Balance Due")
1719     dollar_total_invoices = dollar_field("total_invoices", "Total Invoices")
1720     dollar_total_payments = dollar_field("total_payments", "Total Payments")
1721
1722 # Now register the new UserAdmin...
1723 admin.site.register(User, UserAdmin)
1724 # ... and, since we're not using Django's builtin permissions,
1725 # unregister the Group model from admin.
1726 #admin.site.unregister(Group)
1727
1728 #Do not show django evolution in the admin interface
1729 from django_evolution.models import Version, Evolution
1730 #admin.site.unregister(Version)
1731 #admin.site.unregister(Evolution)
1732
1733
1734 # When debugging it is often easier to see all the classes, but for regular use 
1735 # only the top-levels should be displayed
1736 showAll = False
1737
1738 admin.site.register(Deployment, DeploymentAdmin)
1739 admin.site.register(Controller, ControllerAdmin)
1740 admin.site.register(Site, SiteAdmin)
1741 admin.site.register(Slice, SliceAdmin)
1742 admin.site.register(Service, ServiceAdmin)
1743 #admin.site.register(Reservation, ReservationAdmin)
1744 admin.site.register(Network, NetworkAdmin)
1745 admin.site.register(Router, RouterAdmin)
1746 admin.site.register(NetworkTemplate, NetworkTemplateAdmin)
1747 #admin.site.register(Account, AccountAdmin)
1748 #admin.site.register(Invoice, InvoiceAdmin)
1749
1750 if True:
1751     admin.site.register(NetworkParameterType, NetworkParameterTypeAdmin)
1752     admin.site.register(ServiceClass, ServiceClassAdmin)
1753     #admin.site.register(PlanetStack)
1754     admin.site.register(Tag, TagAdmin)
1755     admin.site.register(ControllerRole)
1756     admin.site.register(SiteRole)
1757     admin.site.register(SliceRole)
1758     admin.site.register(PlanetStackRole)
1759     admin.site.register(Node, NodeAdmin)
1760     #admin.site.register(SlicePrivilege, SlicePrivilegeAdmin)
1761     #admin.site.register(SitePrivilege, SitePrivilegeAdmin)
1762     admin.site.register(Sliver, SliverAdmin)
1763     admin.site.register(Image, ImageAdmin)
1764     admin.site.register(DashboardView, DashboardViewAdmin)
1765     admin.site.register(Flavor, FlavorAdmin)
1766