replaced sliver.controllerNetwork with sliver.deployment
[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:
32         return '<span style="min-width:16px;"><img src="/static/admin/img/icon_success.gif"></span>'
33     else:
34         if 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_name', 'slice', 'deployment', 'flavor', 'image', 'node']
344     extra = 0
345     readonly_fields = ['backend_status_icon', 'all_ips_string', '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 ControllerPrivilegeInline(PlStackTabularInline):
408     model = ControllerPrivilege
409     extra = 0
410     suit_classes = 'suit-tab suit-tab-admin-only'
411     fields = ['backend_status_icon', 'user','role','controller']
412     readonly_fields = ('backend_status_icon', )
413
414     def queryset(self, request):
415         return ControllerPrivilege.select_by_user(request.user)
416
417 class ControllerSiteDeploymentsInline(PlStackTabularInline):
418     model = ControllerSiteDeployments
419     extra = 0
420     suit_classes = 'suit-tab suit-tab-admin-only'
421     fields = ['controller', 'site_deployment', 'tenant_id']
422
423 class SitePrivilegeInline(PlStackTabularInline):
424     model = SitePrivilege
425     extra = 0
426     suit_classes = 'suit-tab suit-tab-siteprivileges'
427     fields = ['backend_status_icon', 'user','site', 'role']
428     readonly_fields = ('backend_status_icon', )
429
430     def formfield_for_foreignkey(self, db_field, request, **kwargs):
431         if db_field.name == 'site':
432             kwargs['queryset'] = Site.select_by_user(request.user)
433
434         if db_field.name == 'user':
435             kwargs['queryset'] = User.select_by_user(request.user)
436         return super(SitePrivilegeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
437
438     def queryset(self, request):
439         return SitePrivilege.select_by_user(request.user)
440
441 class SiteDeploymentsInline(PlStackTabularInline):
442     model = SiteDeployments
443     extra = 0
444     suit_classes = 'suit-tab suit-tab-deployments'
445     fields = ['backend_status_icon', 'deployment','site', 'controller']
446     readonly_fields = ('backend_status_icon', )
447
448     def formfield_for_foreignkey(self, db_field, request, **kwargs):
449         if db_field.name == 'site':
450             kwargs['queryset'] = Site.select_by_user(request.user)
451
452         if db_field.name == 'deployment':
453             kwargs['queryset'] = Deployment.select_by_user(request.user)
454
455         if db_field.name == 'controller':
456             kwargs['queryset'] = Controller.select_by_user(request.user)
457
458         return super(SiteDeploymentsInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
459
460     def queryset(self, request):
461         return SiteDeployments.select_by_user(request.user)
462
463
464 class SlicePrivilegeInline(PlStackTabularInline):
465     model = SlicePrivilege
466     suit_classes = 'suit-tab suit-tab-sliceprivileges'
467     extra = 0
468     fields = ('backend_status_icon', 'user', 'slice', 'role')
469     readonly_fields = ('backend_status_icon', )
470
471     def formfield_for_foreignkey(self, db_field, request, **kwargs):
472         if db_field.name == 'slice':
473            kwargs['queryset'] = Slice.select_by_user(request.user)
474         if db_field.name == 'user':
475            kwargs['queryset'] = User.select_by_user(request.user)
476
477         return super(SlicePrivilegeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
478
479     def queryset(self, request):
480         return SlicePrivilege.select_by_user(request.user)
481
482 class SliceNetworkInline(PlStackTabularInline):
483     model = Network.slices.through
484     selflink_fieldname = "network"
485     extra = 0
486     verbose_name = "Network Connection"
487     verbose_name_plural = "Network Connections"
488     suit_classes = 'suit-tab suit-tab-slicenetworks'
489     fields = ['backend_status_icon', 'network']
490     readonly_fields = ('backend_status_icon', )
491
492 class ImageDeploymentsInline(PlStackTabularInline):
493     model = ImageDeployments
494     extra = 0
495     verbose_name = "Image Deployments"
496     verbose_name_plural = "Image Deployments"
497     suit_classes = 'suit-tab suit-tab-imagedeployments'
498     fields = ['backend_status_icon', 'image', 'deployment']
499     readonly_fields = ['backend_status_icon']
500
501 class ControllerImagesInline(PlStackTabularInline):
502     model = ControllerImages
503     extra = 0
504     verbose_name = "Controller Images"
505     verbose_name_plural = "Controller Images"
506     suit_classes = 'suit-tab suit-tab-admin-only'
507     fields = ['backend_status_icon', 'image', 'controller', 'glance_image_id']
508     readonly_fields = ['backend_status_icon', 'glance_image_id']
509
510 class SliceRoleAdmin(PlanetStackBaseAdmin):
511     model = SliceRole
512     pass
513
514 class SiteRoleAdmin(PlanetStackBaseAdmin):
515     model = SiteRole
516     pass
517
518 class DeploymentAdminForm(forms.ModelForm):
519     sites = forms.ModelMultipleChoiceField(
520         queryset=Site.objects.all(),
521         required=False,
522         help_text="Select which sites are allowed to host nodes in this deployment",
523         widget=FilteredSelectMultiple(
524             verbose_name=('Sites'), is_stacked=False
525         )
526     )
527     images = forms.ModelMultipleChoiceField(
528         queryset=Image.objects.all(),
529         required=False,
530         help_text="Select which images should be deployed on this deployment",
531         widget=FilteredSelectMultiple(
532             verbose_name=('Images'), is_stacked=False
533         )
534     )
535     flavors = forms.ModelMultipleChoiceField(
536         queryset=Flavor.objects.all(),
537         required=False,
538         help_text="Select which flavors should be usable on this deployment",
539         widget=FilteredSelectMultiple(
540             verbose_name=('Flavors'), is_stacked=False
541         )
542     )
543     class Meta:
544         model = Deployment
545         many_to_many = ["flavors",]
546
547     def __init__(self, *args, **kwargs):
548       request = kwargs.pop('request', None)
549       super(DeploymentAdminForm, self).__init__(*args, **kwargs)
550
551       self.fields['accessControl'].initial = "allow site " + request.user.site.name
552
553       if self.instance and self.instance.pk:
554         self.fields['sites'].initial = [x.site for x in self.instance.sitedeployments.all()]
555         self.fields['images'].initial = [x.image for x in self.instance.imagedeployments.all()]
556         self.fields['flavors'].initial = self.instance.flavors.all()
557
558     def manipulate_m2m_objs(self, this_obj, selected_objs, all_relations, relation_class, local_attrname, foreign_attrname):
559         """ helper function for handling m2m relations from the MultipleChoiceField
560
561             this_obj: the source object we want to link from
562
563             selected_objs: a list of destination objects we want to link to
564
565             all_relations: the full set of relations involving this_obj, including ones we don't want
566
567             relation_class: the class that implements the relation from source to dest
568
569             local_attrname: field name representing this_obj in relation_class
570
571             foreign_attrname: field name representing selected_objs in relation_class
572
573             This function will remove all newobjclass relations from this_obj
574             that are not contained in selected_objs, and add any relations that
575             are in selected_objs but don't exist in the data model yet.
576         """
577
578         existing_dest_objs = []
579         for relation in list(all_relations):
580             if getattr(relation, foreign_attrname) not in selected_objs:
581                 #print "deleting site", sdp.site
582                 relation.delete()
583             else:
584                 existing_dest_objs.append(getattr(relation, foreign_attrname))
585
586         for dest_obj in selected_objs:
587             if dest_obj not in existing_dest_objs:
588                 #print "adding site", site
589                 kwargs = {foreign_attrname: dest_obj, local_attrname: this_obj}
590                 relation = relation_class(**kwargs)
591                 relation.save()
592
593     def save(self, commit=True):
594       deployment = super(DeploymentAdminForm, self).save(commit=False)
595
596       if commit:
597         deployment.save()
598         # this has to be done after save() if/when a deployment is first created
599         deployment.flavors = self.cleaned_data['flavors']
600
601       if deployment.pk:
602         # save_m2m() doesn't seem to work with 'through' relations. So we
603         #    create/destroy the through models ourselves. There has to be
604         #    a better way...
605
606         self.manipulate_m2m_objs(deployment, self.cleaned_data['sites'], deployment.sitedeployments.all(), SiteDeployments, "deployment", "site")
607         self.manipulate_m2m_objs(deployment, self.cleaned_data['images'], deployment.imagedeployments.all(), ImageDeployments, "deployment", "image")
608         # manipulate_m2m_objs doesn't work for Flavor/Deployment relationship
609         # so well handle that manually here
610         for flavor in deployment.flavors.all():
611             if getattr(flavor, 'name') not in self.cleaned_data['flavors']:
612                 deployment.flavors.remove(flavor)
613         for flavor in self.cleaned_data['flavors']:
614             if flavor not in deployment.flavors.all():
615                 flavor.deployments.add(deployment)
616
617       self.save_m2m()
618
619       return deployment
620
621 class DeploymentAdminROForm(DeploymentAdminForm):
622     def save(self, commit=True):
623         raise PermissionDenied
624
625 class SiteAssocInline(PlStackTabularInline):
626     model = Site.deployments.through
627     extra = 0
628     suit_classes = 'suit-tab suit-tab-sites'
629
630 class DeploymentAdmin(PlanetStackBaseAdmin):
631     model = Deployment
632     fieldList = ['backend_status_text', 'name', 'sites', 'images', 'flavors', 'accessControl']
633     fieldsets = [(None, {'fields': fieldList, 'classes':['suit-tab suit-tab-sites']})]
634     # node no longer directly connected to deployment
635     #inlines = [DeploymentPrivilegeInline,NodeInline,TagInline,ImageDeploymentsInline]
636     inlines = [DeploymentPrivilegeInline,TagInline,ImageDeploymentsInline]
637     list_display = ['backend_status_icon', 'name']
638     list_display_links = ('backend_status_icon', 'name', )
639     readonly_fields = ('backend_status_text', )
640
641     user_readonly_fields = ['name']
642
643     # nodes no longer direclty connected to deployments
644     #suit_form_tabs =(('sites','Deployment Details'),('nodes','Nodes'),('deploymentprivileges','Privileges'),('tags','Tags'),('imagedeployments','Images'))
645     suit_form_tabs =(('sites','Deployment Details'),('deploymentprivileges','Privileges'),('tags','Tags'),('imagedeployments','Images'))
646
647     def get_form(self, request, obj=None, **kwargs):
648         if request.user.isReadOnlyUser():
649             kwargs["form"] = DeploymentAdminROForm
650         else:
651             kwargs["form"] = DeploymentAdminForm
652         adminForm = super(DeploymentAdmin,self).get_form(request, obj, **kwargs)
653
654         # from stackexchange: pass the request object into the form
655
656         class AdminFormMetaClass(adminForm):
657            def __new__(cls, *args, **kwargs):
658                kwargs['request'] = request
659                return adminForm(*args, **kwargs)
660
661         return AdminFormMetaClass
662
663 class ControllerAdminForm(forms.ModelForm):
664     site_deployments = forms.ModelMultipleChoiceField(
665         queryset=SiteDeployments.objects.all(),
666         required=False,
667         help_text="Select which sites deployments are managed by this controller",
668         widget=FilteredSelectMultiple(
669             verbose_name=('Site Deployments'), is_stacked=False
670         )
671     )
672
673     class Meta: 
674         model = Controller
675         
676     def __init__(self, *args, **kwargs):
677         request = kwargs.pop('request', None)
678         super(ControllerAdminForm, self).__init__(*args, **kwargs)  
679
680         if self.instance and self.instance.pk:
681             self.fields['site_deployments'].initial = [x.site_deployment for x in self.instance.controllersitedeployments.all()]
682
683     def manipulate_m2m_objs(self, this_obj, selected_objs, all_relations, relation_class, local_attrname, foreign_attrname):
684         """ helper function for handling m2m relations from the MultipleChoiceField
685             this_obj: the source object we want to link from
686             selected_objs: a list of destination objects we want to link to
687             all_relations: the full set of relations involving this_obj, including ones we don't want
688             relation_class: the class that implements the relation from source to dest
689             local_attrname: field name representing this_obj in relation_class
690             foreign_attrname: field name representing selected_objs in relation_class
691             This function will remove all newobjclass relations from this_obj
692             that are not contained in selected_objs, and add any relations that
693             are in selected_objs but don't exist in the data model yet.
694         """
695         existing_dest_objs = []
696         for relation in list(all_relations):
697             if getattr(relation, foreign_attrname) not in selected_objs:
698                 #print "deleting site", sdp.site
699                 relation.delete()
700             else:
701                 existing_dest_objs.append(getattr(relation, foreign_attrname))
702
703         for dest_obj in selected_objs:
704             if dest_obj not in existing_dest_objs:
705                 #print "adding site", site
706                 kwargs = {foreign_attrname: dest_obj, local_attrname: this_obj}
707                 relation = relation_class(**kwargs)
708                 relation.save()
709
710     def save(self, commit=True):
711         controller = super(ControllerAdminForm, self).save(commit=False)
712         if commit:
713             controller.save()
714
715         if controller.pk:
716             # save_m2m() doesn't seem to work with 'through' relations. So we
717             #    create/destroy the through models ourselves. There has to be
718             #    a better way...
719             self.manipulate_m2m_objs(controller, self.cleaned_data['site_deployments'], controller.controllersitedeployments.all(), ControllerSiteDeployments, "controller", "site_deployment")
720
721         self.save_m2m()
722
723         return controller 
724       
725 class ControllerAdmin(PlanetStackBaseAdmin):
726     model = Controller 
727     fieldList = ['name', 'version', 'backend_type', 'auth_url', 'admin_user', 'admin_tenant','admin_password']
728     #fieldsets = [(None, {'fields': fieldList, 'classes':['suit-tab suit-tab-general']})]
729     inlines = [ControllerSiteDeploymentsInline] # ,ControllerImagesInline]
730     list_display = ['backend_status_icon', 'name', 'version', 'backend_type']
731     list_display_links = ('backend_status_icon', 'name', )
732     readonly_fields = ('backend_status_text',)
733
734     user_readonly_fields = []
735
736     def get_form(self, request, obj=None, **kwargs):
737         print self.fieldsets
738         if request.user.isReadOnlyUser():
739             kwargs["form"] = ControllerAdminROForm
740         else:
741             kwargs["form"] = ControllerAdminForm
742         adminForm = super(ControllerAdmin,self).get_form(request, obj, **kwargs)
743
744         # from stackexchange: pass the request object into the form
745
746         class AdminFormMetaClass(adminForm):
747            def __new__(cls, *args, **kwargs):
748                kwargs['request'] = request
749                return adminForm(*args, **kwargs)
750
751         return AdminFormMetaClass
752
753 class ServiceAttrAsTabInline(PlStackTabularInline):
754     model = ServiceAttribute
755     fields = ['name','value']
756     extra = 0
757     suit_classes = 'suit-tab suit-tab-serviceattrs'
758
759 class ServiceAdmin(PlanetStackBaseAdmin):
760     list_display = ("backend_status_icon","name","description","versionNumber","enabled","published")
761     list_display_links = ('backend_status_icon', 'name', )
762     fieldList = ["backend_status_text","name","description","versionNumber","enabled","published"]
763     fieldsets = [(None, {'fields': fieldList, 'classes':['suit-tab suit-tab-general']})]
764     inlines = [ServiceAttrAsTabInline,SliceInline]
765     readonly_fields = ('backend_status_text', )
766
767     user_readonly_fields = fieldList
768
769     suit_form_tabs =(('general', 'Service Details'),
770         ('slices','Slices'),
771         ('serviceattrs','Additional Attributes'),
772     )
773
774 class SiteAdmin(PlanetStackBaseAdmin):
775     fieldList = ['backend_status_text', 'name', 'site_url', 'enabled', 'is_public', 'login_base', 'accountLink','location']
776     fieldsets = [
777         (None, {'fields': fieldList, 'classes':['suit-tab suit-tab-general']}),
778         #('Deployment Networks', {'fields': ['deployments'], 'classes':['suit-tab suit-tab-deployments']}),
779     ]
780     suit_form_tabs =(('general', 'Site Details'),
781         ('users','Users'),
782         ('siteprivileges','Privileges'),
783         ('deployments','Deployments'),
784         ('slices','Slices'),
785         #('nodes','Nodes'),
786         ('tags','Tags'),
787     )
788     readonly_fields = ['backend_status_text', 'accountLink']
789
790     user_readonly_fields = ['name', 'deployments','site_url', 'enabled', 'is_public', 'login_base', 'accountLink']
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, SiteDeploymentsInline]
796     search_fields = ['name']
797
798     def queryset(self, request):
799         return Site.select_by_user(request.user)
800
801     def get_formsets(self, request, obj=None):
802         for inline in self.get_inline_instances(request, obj):
803             # hide MyInline in the add view
804             if obj is None:
805                 continue
806             if isinstance(inline, SliverInline):
807                 inline.model.caller = request.user
808             yield inline.get_formset(request, obj)
809
810     def accountLink(self, obj):
811         link_obj = obj.accounts.all()
812         if link_obj:
813             reverse_path = "admin:core_account_change"
814             url = reverse(reverse_path, args =(link_obj[0].id,))
815             return "<a href='%s'>%s</a>" % (url, "view billing details")
816         else:
817             return "no billing data for this site"
818     accountLink.allow_tags = True
819     accountLink.short_description = "Billing"
820
821     def save_model(self, request, obj, form, change):
822         # update openstack connection to use this site/tenant
823         obj.save_by_user(request.user) 
824
825     def delete_model(self, request, obj):
826         obj.delete_by_user(request.user)
827         
828
829 class SitePrivilegeAdmin(PlanetStackBaseAdmin):
830     fieldList = ['backend_status_text', 'user', 'site', 'role']
831     fieldsets = [
832         (None, {'fields': fieldList, 'classes':['collapse']})
833     ]
834     readonly_fields = ('backend_status_text', )
835     list_display = ('backend_status_icon', 'user', 'site', 'role')
836     list_display_links = list_display
837     user_readonly_fields = fieldList
838     user_readonly_inlines = []
839
840     def formfield_for_foreignkey(self, db_field, request, **kwargs):
841         if db_field.name == 'site':
842             if not request.user.is_admin:
843                 # only show sites where user is an admin or pi
844                 sites = set()
845                 for site_privilege in SitePrivilege.objects.filer(user=request.user):
846                     if site_privilege.role.role_type in ['admin', 'pi']:
847                         sites.add(site_privilege.site)
848                 kwargs['queryset'] = Site.objects.filter(site__in=list(sites))
849
850         if db_field.name == 'user':
851             if not request.user.is_admin:
852                 # only show users from sites where caller has admin or pi role
853                 roles = Role.objects.filter(role_type__in=['admin', 'pi'])
854                 site_privileges = SitePrivilege.objects.filter(user=request.user).filter(role__in=roles)
855                 sites = [site_privilege.site for site_privilege in site_privileges]
856                 site_privileges = SitePrivilege.objects.filter(site__in=sites)
857                 emails = [site_privilege.user.email for site_privilege in site_privileges]
858                 users = User.objects.filter(email__in=emails)
859                 kwargs['queryset'] = users
860
861         return super(SitePrivilegeAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
862
863     def queryset(self, request):
864         # admins can see all privileges. Users can only see privileges at sites
865         # where they have the admin role or pi role.
866         qs = super(SitePrivilegeAdmin, self).queryset(request)
867         #if not request.user.is_admin:
868         #    roles = Role.objects.filter(role_type__in=['admin', 'pi'])
869         #    site_privileges = SitePrivilege.objects.filter(user=request.user).filter(role__in=roles)
870         #    login_bases = [site_privilege.site.login_base for site_privilege in site_privileges]
871         #    sites = Site.objects.filter(login_base__in=login_bases)
872         #    qs = qs.filter(site__in=sites)
873         return qs
874
875 class SliceForm(forms.ModelForm):
876     class Meta:
877         model = Slice
878         widgets = {
879             'service': LinkedSelect
880         }
881
882     def clean(self):
883         cleaned_data = super(SliceForm, self).clean()
884         name = cleaned_data.get('name')
885         site = cleaned_data.get('site')
886         slice_id = self.instance.id
887         if not site and slice_id:
888             site = Slice.objects.get(id=slice_id).site
889         if (not isinstance(site,Site)):
890             # previous code indicates 'site' could be a site_id and not a site?
891             site = Slice.objects.get(id=site.id)
892         if not name.startswith(site.login_base):
893             raise forms.ValidationError('slice name must begin with %s' % site.login_base)
894         return cleaned_data
895
896 class ControllerSlicesInline(PlStackTabularInline):
897     model = ControllerSlices
898     extra = 0
899     verbose_name = "Controller Slices"
900     verbose_name_plural = "Controller Slices"
901     suit_classes = 'suit-tab suit-tab-admin-only'
902     fields = ['backend_status_icon', 'controller', 'tenant_id']
903     readonly_fields = ('backend_status_icon', )
904
905 class SliceAdmin(PlanetStackBaseAdmin):
906     form = SliceForm
907     fieldList = ['backend_status_text', 'site', 'name', 'serviceClass', 'enabled','description', 'service', 'slice_url', 'max_slivers']
908     fieldsets = [('Slice Details', {'fields': fieldList, 'classes':['suit-tab suit-tab-general']}),]
909     readonly_fields = ('backend_status_text', )
910     list_display = ('backend_status_icon', 'name', 'site','serviceClass', 'slice_url', 'max_slivers')
911     list_display_links = ('backend_status_icon', 'name', )
912     inlines = [SlicePrivilegeInline,SliverInline, TagInline, ReservationInline,SliceNetworkInline]
913     admin_inlines = [ControllerSlicesInline]
914
915     user_readonly_fields = fieldList
916
917     @property
918     def suit_form_tabs(self):
919         tabs =[('general', 'Slice Details'),
920           ('slicenetworks','Networks'),
921           ('sliceprivileges','Privileges'),
922           ('slivers','Slivers'),
923           ('tags','Tags'),
924           ('reservations','Reservations'),
925           ]
926
927         request=getattr(_thread_locals, "request", None)
928         if request and request.user.is_admin:
929             tabs.append( ('admin-only', 'Admin-Only') )
930
931         return tabs
932     
933     def add_view(self, request, form_url='', extra_context=None):
934         # revert to default read-only fields
935         self.readonly_fields = ('backend_status_text',)
936         return super(SliceAdmin, self).add_view(request, form_url, extra_context=extra_context)
937
938     def change_view(self, request, object_id, form_url='', extra_context=None):
939         # cannot change the site of an existing slice so make the site field read only
940         if object_id:
941             self.readonly_fields = ('backend_status_text','site')
942         return super(SliceAdmin, self).change_view(request, object_id, form_url)
943
944     def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
945         deployment_nodes = []
946         for node in Node.objects.all():
947             deployment_nodes.append( (node.site_deployment.id, node.id, node.name) )
948
949         deployment_flavors = []
950         for flavor in Flavor.objects.all():
951             for deployment in flavor.deployments.all():
952                 deployment_flavors.append( (deployment.id, flavor.id, flavor.name) )
953
954         deployment_images = []
955         for image in Image.objects.all():
956             for deployment_image in image.imagedeployments.all():
957                 deployment_images.append( (deployment_image.deployment.id, image.id, image.name) )
958
959         site_login_bases = []
960         for site in Site.objects.all():
961             site_login_bases.append((site.id, site.login_base))
962
963         context["deployment_nodes"] = deployment_nodes
964         context["deployment_flavors"] = deployment_flavors
965         context["deployment_images"] = deployment_images
966         context["site_login_bases"] = site_login_bases
967         return super(SliceAdmin, self).render_change_form(request, context, add, change, form_url, obj)
968
969     def formfield_for_foreignkey(self, db_field, request, **kwargs):
970         if db_field.name == 'site':
971             kwargs['queryset'] = Site.select_by_user(request.user)
972             kwargs['widget'] = forms.Select(attrs={'onChange': "update_slice_prefix(this, $($(this).closest('fieldset')[0]).find('.field-name input')[0].id)"})
973
974         return super(SliceAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
975
976     def queryset(self, request):
977         # admins can see all keys. Users can only see slices they belong to.
978         return Slice.select_by_user(request.user)
979
980     def get_formsets(self, request, obj=None):
981         for inline in self.get_inline_instances(request, obj):
982             # hide MyInline in the add view
983             if obj is None:
984                 continue
985             if isinstance(inline, SliverInline):
986                 inline.model.caller = request.user
987             yield inline.get_formset(request, obj)
988
989 class SlicePrivilegeAdmin(PlanetStackBaseAdmin):
990     fieldsets = [
991         (None, {'fields': ['backend_status_text', 'user', 'slice', 'role']})
992     ]
993     readonly_fields = ('backend_status_text', )
994     list_display = ('backend_status_icon', 'user', 'slice', 'role')
995     list_display_links = list_display
996
997     user_readonly_fields = ['user', 'slice', 'role']
998     user_readonly_inlines = []
999
1000     def formfield_for_foreignkey(self, db_field, request, **kwargs):
1001         if db_field.name == 'slice':
1002             kwargs['queryset'] = Slice.select_by_user(request.user)
1003         
1004         if db_field.name == 'user':
1005             kwargs['queryset'] = User.select_by_user(request.user)
1006
1007         return super(SlicePrivilegeAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
1008
1009     def queryset(self, request):
1010         # admins can see all memberships. Users can only see memberships of
1011         # slices where they have the admin role.
1012         return SlicePrivilege.select_by_user(request.user)
1013
1014     def save_model(self, request, obj, form, change):
1015         # update openstack connection to use this site/tenant
1016         auth = request.session.get('auth', {})
1017         auth['tenant'] = obj.slice.slicename
1018         obj.os_manager = OpenStackManager(auth=auth, caller=request.user)
1019         obj.save()
1020
1021     def delete_model(self, request, obj):
1022         # update openstack connection to use this site/tenant
1023         auth = request.session.get('auth', {})
1024         auth['tenant'] = obj.slice.slicename
1025         obj.os_manager = OpenStackManager(auth=auth, caller=request.user)
1026         obj.delete()
1027
1028
1029 class ImageAdmin(PlanetStackBaseAdmin):
1030
1031     fieldsets = [('Image Details',
1032                    {'fields': ['backend_status_text', 'name', 'disk_format', 'container_format'],
1033                     'classes': ['suit-tab suit-tab-general']})
1034                ]
1035     readonly_fields = ('backend_status_text', )
1036
1037     suit_form_tabs =(('general','Image Details'),('slivers','Slivers'),('imagedeployments','Deployments'), ('controllerimages', 'Controllers'))
1038
1039     inlines = [SliverInline, ControllerImagesInline]
1040
1041     user_readonly_fields = ['name', 'disk_format', 'container_format']
1042
1043     list_display = ['backend_status_icon', 'name']
1044     list_display_links = ('backend_status_icon', 'name', )
1045
1046 class NodeForm(forms.ModelForm):
1047     class Meta:
1048         widgets = {
1049             'site': LinkedSelect,
1050             'deployment': LinkedSelect
1051         }
1052
1053 class NodeAdmin(PlanetStackBaseAdmin):
1054     form = NodeForm
1055     list_display = ('backend_status_icon', 'name', 'site_deployment')
1056     list_display_links = ('backend_status_icon', 'name', )
1057     list_filter = ('site_deployment',)
1058
1059     inlines = [TagInline,SliverInline]
1060     fieldsets = [('Node Details', {'fields': ['backend_status_text', 'name','site_deployment'], 'classes':['suit-tab suit-tab-details']})]
1061     readonly_fields = ('backend_status_text', )
1062
1063     user_readonly_fields = ['name','site_deployment']
1064     user_readonly_inlines = [TagInline,SliverInline]
1065
1066     suit_form_tabs =(('details','Node Details'),('slivers','Slivers'),('tags','Tags'))
1067
1068
1069 class SliverForm(forms.ModelForm):
1070     class Meta:
1071         model = Sliver
1072         ip = forms.CharField(widget=PlainTextWidget)
1073         instance_name = forms.CharField(widget=PlainTextWidget)
1074         widgets = {
1075             'ip': PlainTextWidget(),
1076             'instance_name': PlainTextWidget(),
1077             'slice': LinkedSelect,
1078             'deployment': LinkedSelect,
1079             'node': LinkedSelect,
1080             'image': LinkedSelect
1081         }
1082
1083 class TagAdmin(PlanetStackBaseAdmin):
1084     list_display = ['backend_status_icon', 'service', 'name', 'value', 'content_type', 'content_object',]
1085     list_display_links = list_display
1086     user_readonly_fields = ['service', 'name', 'value', 'content_type', 'content_object',]
1087     user_readonly_inlines = []
1088
1089 class SliverAdmin(PlanetStackBaseAdmin):
1090     form = SliverForm
1091     fieldsets = [
1092         ('Sliver Details', {'fields': ['backend_status_text', 'slice', 'deployment', 'node', 'ip', 'instance_name', 'flavor', 'image', ], 'classes': ['suit-tab suit-tab-general'], })
1093     ]
1094     readonly_fields = ('backend_status_text', )
1095     list_display = ['backend_status_icon', 'ip', 'instance_name', 'slice', 'flavor', 'image', 'node', 'deployment']
1096     list_display_links = ('backend_status_icon', 'ip',)
1097
1098     suit_form_tabs =(('general', 'Sliver Details'),
1099         ('tags','Tags'),
1100     )
1101
1102     inlines = [TagInline]
1103
1104     user_readonly_fields = ['slice', 'deployment', 'node', 'ip', 'instance_name', 'flavor', 'image']
1105
1106     def formfield_for_foreignkey(self, db_field, request, **kwargs):
1107         if db_field.name == 'slice':
1108             kwargs['queryset'] = Slice.select_by_user(request.user)
1109
1110         return super(SliverAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
1111
1112     def queryset(self, request):
1113         # admins can see all slivers. Users can only see slivers of
1114         # the slices they belong to.
1115         return Sliver.select_by_user(request.user)
1116
1117
1118     def get_formsets(self, request, obj=None):
1119         # make some fields read only if we are updating an existing record
1120         if obj == None:
1121             #self.readonly_fields = ('ip', 'instance_name')
1122             self.readonly_fields = ('backend_status_text',)
1123         else:
1124             self.readonly_fields = ('backend_status_text',)
1125             #self.readonly_fields = ('ip', 'instance_name', 'slice', 'image', 'key')
1126
1127         for inline in self.get_inline_instances(request, obj):
1128             # hide MyInline in the add view
1129             if obj is None:
1130                 continue
1131             if isinstance(inline, SliverInline):
1132                 inline.model.caller = request.user
1133             yield inline.get_formset(request, obj)
1134
1135     #def save_model(self, request, obj, form, change):
1136     #    # update openstack connection to use this site/tenant
1137     #    auth = request.session.get('auth', {})
1138     #    auth['tenant'] = obj.slice.name
1139     #    obj.os_manager = OpenStackManager(auth=auth, caller=request.user)
1140     #    obj.creator = request.user
1141     #    obj.save()
1142
1143     #def delete_model(self, request, obj):
1144     #    # update openstack connection to use this site/tenant
1145     #    auth = request.session.get('auth', {})
1146     #    auth['tenant'] = obj.slice.name
1147     #    obj.os_manager = OpenStackManager(auth=auth, caller=request.user)
1148     #    obj.delete()
1149
1150 class UserCreationForm(forms.ModelForm):
1151     """A form for creating new users. Includes all the required
1152     fields, plus a repeated password."""
1153     password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
1154     password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput)
1155
1156     class Meta:
1157         model = User
1158         fields = ('email', 'firstname', 'lastname', 'phone', 'public_key')
1159
1160     def clean_password2(self):
1161         # Check that the two password entries match
1162         password1 = self.cleaned_data.get("password1")
1163         password2 = self.cleaned_data.get("password2")
1164         if password1 and password2 and password1 != password2:
1165             raise forms.ValidationError("Passwords don't match")
1166         return password2
1167
1168     def save(self, commit=True):
1169         # Save the provided password in hashed format
1170         user = super(UserCreationForm, self).save(commit=False)
1171         user.password = self.cleaned_data["password1"]
1172         #user.set_password(self.cleaned_data["password1"])
1173         if commit:
1174             user.save()
1175         return user
1176
1177
1178 class UserChangeForm(forms.ModelForm):
1179     """A form for updating users. Includes all the fields on
1180     the user, but replaces the password field with admin's
1181     password hash display field.
1182     """
1183     password = ReadOnlyPasswordHashField(label='Password',
1184                    help_text= '<a href=\"password/\">Change Password</a>.')
1185
1186     class Meta:
1187         model = User
1188         widgets = { 'public_key': UploadTextareaWidget, }
1189
1190     def clean_password(self):
1191         # Regardless of what the user provides, return the initial value.
1192         # This is done here, rather than on the field, because the
1193         # field does not have access to the initial value
1194         return self.initial["password"]
1195
1196 class UserDashboardViewInline(PlStackTabularInline):
1197     model = UserDashboardView
1198     extra = 0
1199     suit_classes = 'suit-tab suit-tab-dashboards'
1200     fields = ['user', 'dashboardView', 'order']
1201
1202 class UserAdmin(PermissionCheckingAdminMixin, UserAdmin):
1203     # Note: Make sure PermissionCheckingAdminMixin is listed before
1204     # admin.ModelAdmin in the class declaration.
1205
1206     class Meta:
1207         app_label = "core"
1208
1209     # The forms to add and change user instances
1210     form = UserChangeForm
1211     add_form = UserCreationForm
1212
1213     # The fields to be used in displaying the User model.
1214     # These override the definitions on the base UserAdmin
1215     # that reference specific fields on auth.User.
1216     list_display = ('email', 'firstname', 'lastname', 'site', 'last_login')
1217     list_filter = ('site',)
1218     inlines = [SlicePrivilegeInline,SitePrivilegeInline,ControllerPrivilegeInline,UserDashboardViewInline]
1219
1220     fieldListLoginDetails = ['backend_status_text', 'email','site','password','is_active','is_readonly','is_admin','public_key']
1221     fieldListContactInfo = ['firstname','lastname','phone','timezone']
1222
1223     fieldsets = (
1224         ('Login Details', {'fields': ['backend_status_text', 'email', 'site','password', 'is_active', 'is_readonly', 'is_admin', 'public_key'], 'classes':['suit-tab suit-tab-general']}),
1225         ('Contact Information', {'fields': ('firstname','lastname','phone', 'timezone'), 'classes':['suit-tab suit-tab-contact']}),
1226         #('Dashboard Views', {'fields': ('dashboards',), 'classes':['suit-tab suit-tab-dashboards']}),
1227         #('Important dates', {'fields': ('last_login',)}),
1228     )
1229     add_fieldsets = (
1230         (None, {
1231             'classes': ('wide',),
1232             'fields': ('email', 'firstname', 'lastname', 'is_readonly', 'phone', 'public_key','password1', 'password2')},
1233         ),
1234     )
1235     readonly_fields = ('backend_status_text', )
1236     search_fields = ('email',)
1237     ordering = ('email',)
1238     filter_horizontal = ()
1239
1240     user_readonly_fields = fieldListLoginDetails + fieldListContactInfo
1241
1242     @property
1243     def suit_form_tabs(self):
1244         if getattr(_thread_locals, "obj", None) is None:
1245             return []
1246         else:
1247             return (('general','Login Details'),
1248                          ('contact','Contact Information'),
1249                          ('sliceprivileges','Slice Privileges'),
1250                          ('siteprivileges','Site Privileges'),
1251                          ('controllerprivileges','Controller Privileges'),
1252                          ('dashboards','Dashboard Views'))
1253
1254     def formfield_for_foreignkey(self, db_field, request, **kwargs):
1255         if db_field.name == 'site':
1256             kwargs['queryset'] = Site.select_by_user(request.user)
1257
1258         return super(UserAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
1259
1260     def queryset(self, request):
1261         return User.select_by_user(request.user)
1262
1263 class ControllerDashboardViewInline(PlStackTabularInline):
1264     model = ControllerDashboardView
1265     extra = 0
1266     fields = ["controller", "url"]
1267     suit_classes = 'suit-tab suit-tab-controllers'
1268
1269 class DashboardViewAdmin(PlanetStackBaseAdmin):
1270     fieldsets = [('Dashboard View Details',
1271                    {'fields': ['backend_status_text', 'name', 'url'],
1272                     'classes': ['suit-tab suit-tab-general']})
1273                ]
1274     readonly_fields = ('backend_status_text', )
1275     inlines = [ControllerDashboardViewInline]
1276
1277     suit_form_tabs =(('general','Dashboard View Details'),
1278                      ('controllers', 'Per-controller Dashboard Details'))
1279
1280 class ServiceResourceInline(PlStackTabularInline):
1281     model = ServiceResource
1282     extra = 0
1283
1284 class ServiceClassAdmin(PlanetStackBaseAdmin):
1285     list_display = ('backend_status_icon', 'name', 'commitment', 'membershipFee')
1286     list_display_links = ('backend_status_icon', 'name', )
1287     inlines = [ServiceResourceInline]
1288
1289     user_readonly_fields = ['name', 'commitment', 'membershipFee']
1290     user_readonly_inlines = []
1291
1292 class ReservedResourceInline(PlStackTabularInline):
1293     model = ReservedResource
1294     extra = 0
1295     suit_classes = 'suit-tab suit-tab-reservedresources'
1296
1297     def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
1298         field = super(ReservedResourceInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
1299
1300         if db_field.name == 'resource':
1301             # restrict resources to those that the slice's service class allows
1302             if request._slice is not None:
1303                 field.queryset = field.queryset.filter(serviceClass = request._slice.serviceClass, calendarReservable=True)
1304                 if len(field.queryset) > 0:
1305                     field.initial = field.queryset.all()[0]
1306             else:\r
1307                 field.queryset = field.queryset.none()\r
1308         elif db_field.name == 'sliver':\r
1309             # restrict slivers to those that belong to the slice\r
1310             if request._slice is not None:\r
1311                 field.queryset = field.queryset.filter(slice = request._slice)
1312             else:
1313                 field.queryset = field.queryset.none()\r
1314 \r
1315         return field
1316
1317     def queryset(self, request):
1318         return ReservedResource.select_by_user(request.user)
1319
1320 class ReservationChangeForm(forms.ModelForm):
1321     class Meta:
1322         model = Reservation
1323         widgets = {
1324             'slice' : LinkedSelect
1325         }
1326
1327 class ReservationAddForm(forms.ModelForm):
1328     slice = forms.ModelChoiceField(queryset=Slice.objects.all(), widget=forms.Select(attrs={"onChange":"document.getElementById('id_refresh').value=1; submit()"}))
1329     refresh = forms.CharField(widget=forms.HiddenInput())
1330
1331     class Media:
1332        css = {'all': ('planetstack.css',)}   # .field-refresh { display: none; }
1333
1334     def clean_slice(self):
1335         slice = self.cleaned_data.get("slice")
1336         x = ServiceResource.objects.filter(serviceClass = slice.serviceClass, calendarReservable=True)
1337         if len(x) == 0:
1338             raise forms.ValidationError("The slice you selected does not have a service class that allows reservations")
1339         return slice
1340
1341     class Meta:
1342         model = Reservation
1343         widgets = {
1344             'slice' : LinkedSelect
1345         }
1346
1347
1348 class ReservationAddRefreshForm(ReservationAddForm):
1349     """ This form is displayed when the Reservation Form receives an update
1350         from the Slice dropdown onChange handler. It doesn't validate the
1351         data and doesn't save the data. This will cause the form to be
1352         redrawn.
1353     """
1354
1355     """ don't validate anything other than slice """
1356     dont_validate_fields = ("startTime", "duration")
1357
1358     def full_clean(self):
1359         result = super(ReservationAddForm, self).full_clean()
1360
1361         for fieldname in self.dont_validate_fields:
1362             if fieldname in self._errors:
1363                 del self._errors[fieldname]
1364
1365         return result
1366
1367     """ don't save anything """
1368     def is_valid(self):
1369         return False
1370
1371 class ReservationAdmin(PlanetStackBaseAdmin):
1372     fieldList = ['backend_status_text', 'slice', 'startTime', 'duration']
1373     fieldsets = [('Reservation Details', {'fields': fieldList, 'classes': ['suit-tab suit-tab-general']})]
1374     readonly_fields = ('backend_status_text', )
1375     list_display = ('startTime', 'duration')
1376     form = ReservationAddForm
1377
1378     suit_form_tabs = (('general','Reservation Details'), ('reservedresources','Reserved Resources'))
1379
1380     inlines = [ReservedResourceInline]
1381     user_readonly_fields = fieldList
1382
1383     def add_view(self, request, form_url='', extra_context=None):
1384         timezone.activate(request.user.timezone)
1385         request._refresh = False
1386         request._slice = None
1387         if request.method == 'POST':
1388             # "refresh" will be set to "1" if the form was submitted due to
1389             # a change in the Slice dropdown.
1390             if request.POST.get("refresh","1") == "1":
1391                 request._refresh = True
1392                 request.POST["refresh"] = "0"
1393
1394             # Keep track of the slice that was selected, so the
1395             # reservedResource inline can filter items for the slice.
1396             request._slice = request.POST.get("slice",None)
1397             if (request._slice is not None):
1398                 request._slice = Slice.objects.get(id=request._slice)
1399
1400         result =  super(ReservationAdmin, self).add_view(request, form_url, extra_context)
1401         return result
1402
1403     def changelist_view(self, request, extra_context = None):
1404         timezone.activate(request.user.timezone)
1405         return super(ReservationAdmin, self).changelist_view(request, extra_context)
1406
1407     def get_form(self, request, obj=None, **kwargs):
1408         request._obj_ = obj
1409         if obj is not None:
1410             # For changes, set request._slice to the slice already set in the
1411             # object.
1412             request._slice = obj.slice
1413             self.form = ReservationChangeForm
1414         else:
1415             if getattr(request, "_refresh", False):
1416                 self.form = ReservationAddRefreshForm
1417             else:
1418                 self.form = ReservationAddForm
1419         return super(ReservationAdmin, self).get_form(request, obj, **kwargs)
1420
1421     def get_readonly_fields(self, request, obj=None):
1422         if (obj is not None):
1423             # Prevent slice from being changed after the reservation has been
1424             # created.
1425             return ['slice']
1426         else:
1427             return []
1428
1429     def queryset(self, request):
1430         return Reservation.select_by_user(request.user)
1431
1432 class NetworkParameterTypeAdmin(PlanetStackBaseAdmin):
1433     list_display = ("backend_status_icon", "name", )
1434     list_display_links = ('backend_status_icon', 'name', )
1435     user_readonly_fields = ['name']
1436     user_readonly_inlines = []
1437
1438 class RouterAdmin(PlanetStackBaseAdmin):
1439     list_display = ("backend_status_icon", "name", )
1440     list_display_links = ('backend_status_icon', 'name', )
1441     user_readonly_fields = ['name']
1442     user_readonly_inlines = []
1443
1444 class RouterInline(PlStackTabularInline):
1445     model = Router.networks.through
1446     extra = 0
1447     verbose_name_plural = "Routers"
1448     verbose_name = "Router"
1449     suit_classes = 'suit-tab suit-tab-routers'
1450
1451 class NetworkParameterInline(PlStackGenericTabularInline):
1452     model = NetworkParameter
1453     extra = 0
1454     verbose_name_plural = "Parameters"
1455     verbose_name = "Parameter"
1456     suit_classes = 'suit-tab suit-tab-netparams'
1457     fields = ['backend_status_icon', 'parameter', 'value']
1458     readonly_fields = ('backend_status_icon', )
1459
1460 class NetworkSliversInline(PlStackTabularInline):
1461     fields = ['backend_status_icon', 'network','sliver','ip']
1462     readonly_fields = ("backend_status_icon", "ip", )
1463     model = NetworkSliver
1464     selflink_fieldname = "sliver"
1465     extra = 0
1466     verbose_name_plural = "Slivers"
1467     verbose_name = "Sliver"
1468     suit_classes = 'suit-tab suit-tab-networkslivers'
1469
1470 class NetworkSlicesInline(PlStackTabularInline):
1471     model = NetworkSlice
1472     selflink_fieldname = "slice"
1473     extra = 0
1474     verbose_name_plural = "Slices"
1475     verbose_name = "Slice"
1476     suit_classes = 'suit-tab suit-tab-networkslices'
1477     fields = ['backend_status_icon', 'network','slice']
1478     readonly_fields = ('backend_status_icon', )
1479
1480 class ControllerNetworksInline(PlStackTabularInline):
1481     model = ControllerNetworks
1482     extra = 0
1483     verbose_name_plural = "Controller Networks"
1484     verbose_name = "Controller Network"
1485     suit_classes = 'suit-tab suit-tab-admin-only'
1486     fields = ['backend_status_icon', 'controller','net_id','subnet_id']
1487     readonly_fields = ('backend_status_icon', )
1488
1489 class NetworkForm(forms.ModelForm):
1490     class Meta:
1491         model = Network
1492         widgets = {
1493             'topologyParameters': UploadTextareaWidget,
1494             'controllerParameters': UploadTextareaWidget,
1495         }
1496
1497 class NetworkAdmin(PlanetStackBaseAdmin):
1498     list_display = ("backend_status_icon", "name", "subnet", "ports", "labels")
1499     list_display_links = ('backend_status_icon', 'name', )
1500     readonly_fields = ("subnet", )
1501
1502     inlines = [NetworkParameterInline, NetworkSliversInline, NetworkSlicesInline, RouterInline]
1503     admin_inlines = [ControllerNetworksInline]
1504
1505     form=NetworkForm
1506
1507     fieldsets = [
1508         (None, {'fields': ['backend_status_text', 'name','template','ports','labels','owner','guaranteedBandwidth', 'permitAllSlices','permittedSlices','network_id','router_id','subnet_id','subnet'],
1509                 'classes':['suit-tab suit-tab-general']}),
1510         (None, {'fields': ['topologyParameters', 'controllerUrl', 'controllerParameters'],
1511                 'classes':['suit-tab suit-tab-sdn']}),
1512                 ]
1513
1514     readonly_fields = ('backend_status_text', )
1515     user_readonly_fields = ['name','template','ports','labels','owner','guaranteedBandwidth', 'permitAllSlices','permittedSlices','network_id','router_id','subnet_id','subnet']
1516
1517     @property
1518     def suit_form_tabs(self):
1519         tabs=[('general','Network Details'),
1520             ('sdn', 'SDN Configuration'),
1521             ('netparams', 'Parameters'),
1522             ('networkslivers','Slivers'),
1523             ('networkslices','Slices'),
1524             ('routers','Routers'),
1525         ]
1526
1527         request=getattr(_thread_locals, "request", None)
1528         if request and request.user.is_admin:
1529             tabs.append( ('admin-only', 'Admin-Only') )
1530
1531         return tabs
1532
1533
1534 class NetworkTemplateAdmin(PlanetStackBaseAdmin):
1535     list_display = ("backend_status_icon", "name", "guaranteedBandwidth", "visibility")
1536     list_display_links = ('backend_status_icon', 'name', )
1537     user_readonly_fields = ["name", "guaranteedBandwidth", "visibility"]
1538     user_readonly_inlines = []
1539     fieldsets = [
1540         (None, {'fields': ['name', 'description', 'guaranteedBandwidth', 'visibility', 'translation', 'sharedNetworkName', 'sharedNetworkId', 'topologyKind', 'controllerKind'],
1541                 'classes':['suit-tab suit-tab-general']}),]
1542     suit_form_tabs = (('general','Network Template Details'), )
1543
1544 class FlavorAdmin(PlanetStackBaseAdmin):
1545     list_display = ("backend_status_icon", "name", "flavor", "order", "default")
1546     list_display_links = ("backend_status_icon", "name")
1547     user_readonly_fields = ("name", "flavor")
1548     fields = ("name", "description", "flavor", "order", "default")
1549
1550 # register a signal that caches the user's credentials when they log in
1551 def cache_credentials(sender, user, request, **kwds):
1552     auth = {'username': request.POST['username'],
1553             'password': request.POST['password']}
1554     request.session['auth'] = auth
1555 user_logged_in.connect(cache_credentials)
1556
1557 def dollar_field(fieldName, short_description):
1558     def newFunc(self, obj):
1559         try:
1560             x= "$ %0.2f" % float(getattr(obj, fieldName, 0.0))
1561         except:
1562             x=getattr(obj, fieldName, 0.0)
1563         return x
1564     newFunc.short_description = short_description
1565     return newFunc
1566
1567 def right_dollar_field(fieldName, short_description):
1568     def newFunc(self, obj):
1569         try:
1570             #x= '<div align=right style="width:6em">$ %0.2f</div>' % float(getattr(obj, fieldName, 0.0))
1571             x= '<div align=right>$ %0.2f</div>' % float(getattr(obj, fieldName, 0.0))
1572         except:
1573             x=getattr(obj, fieldName, 0.0)
1574         return x
1575     newFunc.short_description = short_description
1576     newFunc.allow_tags = True
1577     return newFunc
1578
1579 class InvoiceChargeInline(PlStackTabularInline):
1580     model = Charge
1581     extra = 0
1582     verbose_name_plural = "Charges"
1583     verbose_name = "Charge"
1584     exclude = ['account']
1585     fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1586     readonly_fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1587     can_delete = False
1588     max_num = 0
1589
1590     dollar_amount = right_dollar_field("amount", "Amount")
1591
1592 class InvoiceAdmin(admin.ModelAdmin):
1593     list_display = ("date", "account")
1594
1595     inlines = [InvoiceChargeInline]
1596
1597     fields = ["date", "account", "dollar_amount"]
1598     readonly_fields = ["date", "account", "dollar_amount"]
1599
1600     dollar_amount = dollar_field("amount", "Amount")
1601
1602 class InvoiceInline(PlStackTabularInline):
1603     model = Invoice
1604     extra = 0
1605     verbose_name_plural = "Invoices"
1606     verbose_name = "Invoice"
1607     fields = ["date", "dollar_amount"]
1608     readonly_fields = ["date", "dollar_amount"]
1609     suit_classes = 'suit-tab suit-tab-accountinvoice'
1610     can_delete=False
1611     max_num=0
1612
1613     dollar_amount = right_dollar_field("amount", "Amount")
1614
1615 class PendingChargeInline(PlStackTabularInline):
1616     model = Charge
1617     extra = 0
1618     verbose_name_plural = "Charges"
1619     verbose_name = "Charge"
1620     exclude = ["invoice"]
1621     fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1622     readonly_fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1623     suit_classes = 'suit-tab suit-tab-accountpendingcharges'
1624     can_delete=False
1625     max_num=0
1626
1627     def queryset(self, request):
1628         qs = super(PendingChargeInline, self).queryset(request)
1629         qs = qs.filter(state="pending")
1630         return qs
1631
1632     dollar_amount = right_dollar_field("amount", "Amount")
1633
1634 class PaymentInline(PlStackTabularInline):
1635     model=Payment
1636     extra = 1
1637     verbose_name_plural = "Payments"
1638     verbose_name = "Payment"
1639     fields = ["date", "dollar_amount"]
1640     readonly_fields = ["date", "dollar_amount"]
1641     suit_classes = 'suit-tab suit-tab-accountpayments'
1642     can_delete=False
1643     max_num=0
1644
1645     dollar_amount = right_dollar_field("amount", "Amount")
1646
1647 class AccountAdmin(admin.ModelAdmin):
1648     list_display = ("site", "balance_due")
1649
1650     inlines = [InvoiceInline, PaymentInline, PendingChargeInline]
1651
1652     fieldsets = [
1653         (None, {'fields': ['site', 'dollar_balance_due', 'dollar_total_invoices', 'dollar_total_payments'],'classes':['suit-tab suit-tab-general']}),]
1654
1655     readonly_fields = ['site', 'dollar_balance_due', 'dollar_total_invoices', 'dollar_total_payments']
1656
1657     suit_form_tabs =(
1658         ('general','Account Details'),
1659         ('accountinvoice', 'Invoices'),
1660         ('accountpayments', 'Payments'),
1661         ('accountpendingcharges','Pending Charges'),
1662     )
1663
1664     dollar_balance_due = dollar_field("balance_due", "Balance Due")
1665     dollar_total_invoices = dollar_field("total_invoices", "Total Invoices")
1666     dollar_total_payments = dollar_field("total_payments", "Total Payments")
1667
1668 # Now register the new UserAdmin...
1669 admin.site.register(User, UserAdmin)
1670 # ... and, since we're not using Django's builtin permissions,
1671 # unregister the Group model from admin.
1672 #admin.site.unregister(Group)
1673
1674 #Do not show django evolution in the admin interface
1675 from django_evolution.models import Version, Evolution
1676 #admin.site.unregister(Version)
1677 #admin.site.unregister(Evolution)
1678
1679
1680 # When debugging it is often easier to see all the classes, but for regular use 
1681 # only the top-levels should be displayed
1682 showAll = False
1683
1684 admin.site.register(Deployment, DeploymentAdmin)
1685 admin.site.register(Controller, ControllerAdmin)
1686 admin.site.register(Site, SiteAdmin)
1687 admin.site.register(Slice, SliceAdmin)
1688 admin.site.register(Service, ServiceAdmin)
1689 admin.site.register(Reservation, ReservationAdmin)
1690 admin.site.register(Network, NetworkAdmin)
1691 admin.site.register(Router, RouterAdmin)
1692 admin.site.register(NetworkTemplate, NetworkTemplateAdmin)
1693 admin.site.register(Account, AccountAdmin)
1694 admin.site.register(Invoice, InvoiceAdmin)
1695
1696 if True:
1697     admin.site.register(NetworkParameterType, NetworkParameterTypeAdmin)
1698     admin.site.register(ServiceClass, ServiceClassAdmin)
1699     #admin.site.register(PlanetStack)
1700     admin.site.register(Tag, TagAdmin)
1701     admin.site.register(ControllerRole)
1702     admin.site.register(SiteRole)
1703     admin.site.register(SliceRole)
1704     admin.site.register(PlanetStackRole)
1705     admin.site.register(Node, NodeAdmin)
1706     #admin.site.register(SlicePrivilege, SlicePrivilegeAdmin)
1707     #admin.site.register(SitePrivilege, SitePrivilegeAdmin)
1708     admin.site.register(Sliver, SliverAdmin)
1709     admin.site.register(Image, ImageAdmin)
1710     admin.site.register(DashboardView, DashboardViewAdmin)
1711     admin.site.register(Flavor, FlavorAdmin)
1712