1 # Copyright (C) 2010 Oregon State University et al.
2 # Copyright (C) 2010 Greek Research and Technology Network
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
22 from django
import forms
23 from django
.conf
import settings
24 from django
.contrib
.auth
.decorators
import login_required
25 from django
.contrib
.auth
.models
import User
26 from django
.core
.urlresolvers
import reverse
27 from django
.db
.models
.query_utils
import Q
28 from django
.http
import HttpResponse
, HttpResponseRedirect
, HttpResponseForbidden
, HttpResponseNotAllowed
29 from django
.shortcuts
import get_object_or_404
, render_to_response
30 from django
.template
import RequestContext
31 from django
.template
.defaultfilters
import slugify
33 from object_permissions
import get_users_any
34 from object_permissions
.views
.permissions
import view_users
, view_permissions
35 from object_permissions
import signals
as op_signals
37 from object_log
.models
import LogItem
38 from object_log
.views
import list_for_object
39 from util
.client
import GanetiApiError
41 log_action
= LogItem
.objects
.log_action
43 from ganeti
.models
import Cluster
, ClusterUser
, Profile
, SSHKey
44 from ganeti
.views
import render_403
, render_404
45 from ganeti
.views
.virtual_machine
import render_vms
46 from ganeti
.fields
import DataVolumeField
47 from django
.utils
.translation
import ugettext
as _
49 # Regex for a resolvable hostname
50 FQDN_RE
= r
'^[\w]+(\.[\w]+)*$'
54 def detail(request
, cluster_slug
):
56 Display details of a cluster
58 cluster
= get_object_or_404(Cluster
, slug
=cluster_slug
)
60 admin
= True if user
.is_superuser
else user
.has_perm('admin', cluster
)
62 return render_403(request
, _("You do not have sufficient privileges"))
64 return render_to_response("cluster/detail.html", {
68 context_instance
=RequestContext(request
),
73 def nodes(request
, cluster_slug
):
75 Display all nodes in a cluster
77 cluster
= get_object_or_404(Cluster
, slug
=cluster_slug
)
79 if not (user
.is_superuser
or user
.has_perm('admin', cluster
)):
80 return render_403(request
, _("You do not have sufficient privileges"))
82 return render_to_response("node/table.html", \
83 {'cluster': cluster
, 'nodes':cluster
.nodes
.all()}, \
84 context_instance
=RequestContext(request
),
89 def virtual_machines(request
, cluster_slug
):
91 Display all virtual machines in a cluster. Filtered by access the user
94 cluster
= get_object_or_404(Cluster
, slug
=cluster_slug
)
96 admin
= True if user
.is_superuser
else user
.has_perm('admin', cluster
)
98 return render_403(request
, _("You do not have sufficient privileges"))
100 vms
= cluster
.virtual_machines
.select_related('cluster').all()
101 vms
= render_vms(request
, vms
)
103 return render_to_response("virtual_machine/table.html", \
104 {'cluster': cluster
, 'vms':vms
}, \
105 context_instance
=RequestContext(request
))
109 def edit(request
, cluster_slug
=None):
114 cluster
= get_object_or_404(Cluster
, slug
=cluster_slug
)
119 if not (user
.is_superuser
or (cluster
and user
.has_perm('admin', cluster
))):
120 return render_403(request
, _("You do not have sufficient privileges"))
122 if request
.method
== 'POST':
123 form
= EditClusterForm(request
.POST
, instance
=cluster
)
125 cluster
= form
.save()
126 # TODO Create post signal to import
127 # virtual machines on edit of cluster
128 if cluster
.info
is None:
131 cluster
.sync_virtual_machines()
132 except GanetiApiError
:
133 # ganeti errors here are silently discarded. It's
134 # valid to enter bad info. A user might be adding
135 # info for an offline cluster.
138 log_action('EDIT', user
, cluster
)
140 return HttpResponseRedirect(reverse('cluster-detail', \
141 args
=[cluster
.slug
]))
143 elif request
.method
== 'DELETE':
145 return HttpResponse('1', mimetype
='application/json')
148 form
= EditClusterForm(instance
=cluster
)
150 return render_to_response("cluster/edit.html", {
154 context_instance
=RequestContext(request
),
164 if user
.is_superuser
:
165 cluster_list
= Cluster
.objects
.all()
167 cluster_list
= user
.get_objects_all_perms(Cluster
, ['admin',])
168 return render_to_response("cluster/list.html", {
169 'cluster_list': cluster_list
,
170 'user': request
.user
,
172 context_instance
=RequestContext(request
),
177 def users(request
, cluster_slug
):
179 Display all of the Users of a Cluster
181 cluster
= get_object_or_404(Cluster
, slug
=cluster_slug
)
184 if not (user
.is_superuser
or user
.has_perm('admin', cluster
)):
185 return render_403(request
, _("You do not have sufficient privileges"))
187 url
= reverse('cluster-permissions', args
=[cluster
.slug
])
188 return view_users(request
, cluster
, url
, template
='cluster/users.html')
192 def permissions(request
, cluster_slug
, user_id
=None, group_id
=None):
194 Update a users permissions. This wraps object_permissions.view_permissions()
195 with our custom permissions checks.
197 cluster
= get_object_or_404(Cluster
, slug
=cluster_slug
)
199 if not (user
.is_superuser
or user
.has_perm('admin', cluster
)):
200 return render_403(request
, "You do not have sufficient privileges")
202 url
= reverse('cluster-permissions', args
=[cluster
.slug
])
203 return view_permissions(request
, cluster
, url
, user_id
, group_id
,
204 user_template
='cluster/user_row.html',
205 group_template
='cluster/group_row.html')
209 def redistribute_config(request
, cluster_slug
):
211 Redistribute master-node config to all cluster's other nodes.
213 cluster
= get_object_or_404(Cluster
, slug
=cluster_slug
)
216 if not (user
.is_superuser
or user
.has_perm('admin', cluster
)):
217 return render_403(request
, "You do not have sufficient privileges")
219 if request
.method
== 'POST':
221 job
= cluster
.redistribute_config()
225 log_action('CLUSTER_REDISTRIBUTE', user
, cluster
, job
)
226 except GanetiApiError
, e
:
227 msg
= {'__all__':[str(e
)]}
228 return HttpResponse(json
.dumps(msg
), mimetype
='application/json')
229 return HttpResponseNotAllowed(['POST'])
232 def ssh_keys(request
, cluster_slug
, api_key
):
234 Show all ssh keys which belong to users, who have any perms on the cluster
236 if settings
.WEB_MGR_API_KEY
!= api_key
:
237 return HttpResponseForbidden(_("You're not allowed to view keys."))
239 cluster
= get_object_or_404(Cluster
, slug
=cluster_slug
)
241 users
= set(get_users_any(cluster
).values_list("id", flat
=True))
242 for vm
in cluster
.virtual_machines
.all():
243 users
= users
.union(set(get_users_any(vm
).values_list('id', flat
=True)))
245 keys
= SSHKey
.objects \
246 .filter(Q(user__in
=users
) |
Q(user__is_superuser
=True)) \
247 .values_list('key','user__username') \
248 .order_by('user__username')
250 keys_list
= list(keys
)
251 return HttpResponse(json
.dumps(keys_list
), mimetype
="application/json")
254 class QuotaForm(forms
.Form
):
256 Form for editing user quota on a cluster
258 input = forms
.TextInput(attrs
={'size':5})
260 user
= forms
.ModelChoiceField(queryset
=ClusterUser
.objects
.all(), \
261 widget
=forms
.HiddenInput
)
262 ram
= DataVolumeField(label
='Memory', required
=False, min_value
=0)
263 virtual_cpus
= forms
.IntegerField(label
='Virtual CPUs', required
=False, \
264 min_value
=0, widget
=input)
265 disk
= DataVolumeField(label
='Disk Space', required
=False, min_value
=0)
266 delete
= forms
.BooleanField(required
=False, widget
=forms
.HiddenInput
)
269 def quota(request
, cluster_slug
, user_id
):
271 Updates quota for a user
273 cluster
= get_object_or_404(Cluster
, slug
=cluster_slug
)
275 if not (user
.is_superuser
or user
.has_perm('admin', cluster
)):
276 return render_403(request
, _("You do not have sufficient privileges"))
278 if request
.method
== 'POST':
279 form
= QuotaForm(request
.POST
)
281 data
= form
.cleaned_data
282 cluster_user
= data
['user']
284 cluster
.set_quota(cluster_user
)
286 quota
= cluster
.get_quota()
287 same
= data
['virtual_cpus'] == quota
['virtual_cpus'] \
288 and data
['disk']==quota
['disk'] \
289 and data
['ram']==quota
['ram']
291 # same as default, set quota to default.
292 cluster
.set_quota(cluster_user
)
294 cluster
.set_quota(cluster_user
, data
)
296 # return updated html
297 cluster_user
= cluster_user
.cast()
298 url
= reverse('cluster-permissions', args
=[cluster
.slug
])
299 if isinstance(cluster_user
, (Profile
,)):
300 return render_to_response("cluster/user_row.html",
301 {'object':cluster
, 'user':cluster_user
.user
, 'url':url
})
303 return render_to_response("cluster/group_row.html",
304 {'object':cluster
, 'group':cluster_user
.group
, \
307 # error in form return ajax response
308 content
= json
.dumps(form
.errors
)
309 return HttpResponse(content
, mimetype
='application/json')
312 cluster_user
= get_object_or_404(ClusterUser
, id=user_id
)
313 quota
= cluster
.get_quota(cluster_user
)
314 data
= {'user':user_id
}
318 return render_404(request
, _('User was not found'))
320 form
= QuotaForm(data
)
321 return render_to_response("cluster/quota.html", \
322 {'form':form
, 'cluster':cluster
, 'user_id':user_id
}, \
323 context_instance
=RequestContext(request
))
326 class EditClusterForm(forms
.ModelForm
):
330 'password' : forms
.PasswordInput(),
333 ram
= DataVolumeField(label
=_('Memory'), required
=False, min_value
=0)
334 disk
= DataVolumeField(label
=_('Disk Space'), required
=False, min_value
=0)
337 self
.cleaned_data
= super(EditClusterForm
, self
).clean()
338 data
= self
.cleaned_data
339 host
= data
.get('hostname', None)
340 user
= data
.get('username', None)
341 new
= data
.get('password', None)
343 # Automatically set the slug on cluster creation
345 msg
= _('Enter a hostname')
346 self
._errors
['hostname'] = self
.error_class([msg
])
350 if 'password' in data
: del data
['password']
351 msg
= _('Enter a password')
352 self
._errors
['password'] = self
.error_class([msg
])
355 msg
= _('Enter a username')
356 self
._errors
['username'] = self
.error_class([msg
])
359 if 'password' in data
: del data
['password']
360 msg
= _('Enter a password')
361 self
._errors
['password'] = self
.error_class([msg
])
363 if 'hostname' in data
and data
['hostname'] and 'slug' not in data
:
364 data
['slug'] = slugify(data
['hostname'].split('.')[0])
365 del self
._errors
['slug']
371 def object_log(request
, cluster_slug
):
372 """ displays object log for this cluster """
373 cluster
= get_object_or_404(Cluster
, slug
=cluster_slug
)
375 if not (user
.is_superuser
or user
.has_perm('admin', cluster
)):
376 return render_403(request
, _("You do not have sufficient privileges"))
377 return list_for_object(request
, cluster
)
380 def recv_user_add(sender
, editor
, user
, obj
, **kwargs
):
382 receiver for object_permissions.signals.view_add_user, Logs action
384 log_action('ADD_USER', editor
, obj
, user
)
387 def recv_user_remove(sender
, editor
, user
, obj
, **kwargs
):
389 receiver for object_permissions.signals.view_remove_user, Logs action
391 log_action('REMOVE_USER', editor
, obj
, user
)
393 # remove custom quota user may have had.
394 if isinstance(user
, (User
,)):
395 cluster_user
= user
.get_profile()
397 cluster_user
= user
.organization
398 cluster_user
.quotas
.filter(cluster
=obj
).delete()
401 def recv_perm_edit(sender
, editor
, user
, obj
, **kwargs
):
403 receiver for object_permissions.signals.view_edit_user, Logs action
405 log_action('MODIFY_PERMS', editor
, obj
, user
)
408 op_signals
.view_add_user
.connect(recv_user_add
, sender
=Cluster
)
409 op_signals
.view_remove_user
.connect(recv_user_remove
, sender
=Cluster
)
410 op_signals
.view_edit_user
.connect(recv_perm_edit
, sender
=Cluster
)