Merge tag '0.10.2'
[ganeti_webmgr.git] / ganeti_web / views / cluster.py
blob6e7e31c197a38dbd8585b76c798cb9d4cd347137
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,
17 # USA.
20 from django.conf import settings
21 from django.contrib.auth.decorators import login_required
22 from django.contrib.auth.models import User
23 from django.contrib.contenttypes.models import ContentType
24 from django.core.exceptions import PermissionDenied
25 from django.core.urlresolvers import reverse
26 from django.db.models import Q, Sum
27 from django.http import (HttpResponse, HttpResponseRedirect,
28 HttpResponseForbidden)
29 from django.shortcuts import get_object_or_404, render_to_response, redirect
30 from django.template import RequestContext
31 from django.utils import simplejson as json
32 from django.utils.translation import ugettext as _
33 from django.views.decorators.http import require_POST
34 from django.views.generic.detail import DetailView
36 from django_tables2 import SingleTableView
38 from object_permissions import get_users_any
39 from object_permissions import signals as op_signals
40 from object_permissions.views.permissions import view_users, view_permissions
42 from object_log.models import LogItem
43 from object_log.views import list_for_object
45 log_action = LogItem.objects.log_action
47 from ganeti_web.backend.queries import vm_qs_for_users, cluster_qs_for_user
48 from ganeti_web.forms.cluster import EditClusterForm, QuotaForm
49 from ganeti_web.models import (Cluster, ClusterUser, Profile, SSHKey,
50 VirtualMachine, Job)
51 from ganeti_web.views import render_404
52 from ganeti_web.views.generic import (NO_PRIVS, LoginRequiredMixin,
53 PaginationMixin, GWMBaseView)
54 from ganeti_web.views.tables import (ClusterTable, ClusterVMTable,
55 ClusterJobTable)
56 from ganeti_web.views.virtual_machine import BaseVMListView
57 from ganeti_web.util.client import GanetiApiError
60 class ClusterDetailView(LoginRequiredMixin, DetailView):
62 template_name = "ganeti/cluster/detail.html"
64 def get_object(self, queryset=None):
65 return get_object_or_404(Cluster, slug=self.kwargs["cluster_slug"])
67 def get_context_data(self, **kwargs):
68 cluster = kwargs["object"]
69 user = self.request.user
70 admin = user.is_superuser or user.has_perm("admin", cluster)
72 # If we're not admin we might still have admin on a VM so this
73 # is to determine if we should show the VM tab to the user.
74 show_vms = admin or user.get_objects_any_perms(
75 VirtualMachine, perms=['admin']).filter(cluster=cluster)
77 return {
78 "cluster": cluster,
79 "admin": admin,
80 "readonly": not admin,
81 "show_vms": show_vms
85 class ClusterListView(LoginRequiredMixin, PaginationMixin, GWMBaseView,
86 SingleTableView):
88 template_name = "ganeti/cluster/list.html"
89 model = Cluster
90 table_class = ClusterTable
92 def get_queryset(self):
93 self.queryset = cluster_qs_for_user(self.request.user)
94 qs = super(ClusterListView, self).get_queryset()
95 qs = qs.select_related("nodes", "virtual_machines")
96 return qs
98 def get_context_data(self, **kwargs):
99 user = self.request.user
100 context = super(ClusterListView, self).get_context_data(**kwargs)
101 context["create_vm"] = (user.is_superuser or
102 user.has_perm("admin", Cluster))
103 return context
106 class ClusterVMListView(BaseVMListView):
107 table_class = ClusterVMTable
109 def get_queryset(self):
110 self.get_kwargs()
111 # Store most of these variables on the object, because we'll be using
112 # them in context data too
113 self.cluster = get_object_or_404(Cluster, slug=self.cluster_slug)
114 self.admin = self.can_create(self.cluster)
116 # Do we have admin on any VMs for this cluster?
117 vm_perms = self.request.user.get_objects_any_perms(
118 VirtualMachine, perms=['admin']
119 ).filter(cluster=self.cluster).exists()
121 if not self.admin and not vm_perms:
122 raise PermissionDenied(NO_PRIVS)
123 self.queryset = vm_qs_for_users(self.request.user)
124 # Calling super automatically filters by cluster
125 return super(ClusterVMListView, self).get_queryset()
127 def get_context_data(self, **kwargs):
128 context = super(ClusterVMListView, self).get_context_data(**kwargs)
129 if self.cluster_slug:
130 context["cluster"] = self.cluster
131 context["create_vm"] = self.admin
132 # Required since we cant use a relative link.
133 context["ajax_url"] = reverse(
134 "cluster-vm-list",
135 kwargs={'cluster_slug': self.cluster_slug}
137 return context
140 class ClusterJobListView(LoginRequiredMixin, PaginationMixin, GWMBaseView,
141 SingleTableView):
143 template_name = "ganeti/cluster/jobs.html"
144 model = Job
145 table_class = ClusterJobTable
147 def get_template_names(self):
148 if self.request.is_ajax():
149 template = ['table.html'] # all we need is the table
150 else:
151 template = [self.template_name]
152 return template
154 def get_queryset(self):
155 self.get_kwargs()
156 self.cluster = get_object_or_404(Cluster, slug=self.cluster_slug)
157 perms = self.can_create(self.cluster)
158 self.queryset = self.cluster.jobs.all()
159 if not perms:
160 raise PermissionDenied(NO_PRIVS)
162 return super(ClusterJobListView, self).get_queryset()
164 def get_context_data(self, **kwargs):
165 context = super(ClusterJobListView, self).get_context_data(**kwargs)
166 context['ajax_url'] = reverse(
167 'cluster-job-list',
168 kwargs={'cluster_slug': self.cluster_slug}
170 return context
173 @login_required
174 def nodes(request, cluster_slug):
176 Display all nodes in a cluster
178 cluster = get_object_or_404(Cluster, slug=cluster_slug)
179 user = request.user
180 if not (user.is_superuser or user.has_perm('admin', cluster)):
181 raise PermissionDenied(NO_PRIVS)
183 # query allocated CPUS for all nodes in this list. Must be done here to
184 # avoid querying Node.allocated_cpus for each node in the list. Repackage
185 # list so it is easier to retrieve the values in the template
186 values = VirtualMachine.objects \
187 .filter(cluster=cluster, status='running') \
188 .exclude(virtual_cpus=-1) \
189 .order_by() \
190 .values('primary_node') \
191 .annotate(cpus=Sum('virtual_cpus'))
192 cpus = {}
193 nodes = cluster.nodes.all()
194 for d in values:
195 cpus[d['primary_node']] = d['cpus']
197 # Include nodes that do not have any virtual machines on them.
198 for node in nodes:
199 if node.pk not in cpus:
200 cpus[node.pk] = 0
202 return render_to_response("ganeti/node/table.html",
203 {'cluster': cluster,
204 'nodes': nodes,
205 'cpus': cpus,
207 context_instance=RequestContext(request),
211 @login_required
212 def edit(request, cluster_slug=None):
214 Edit a cluster
216 if cluster_slug:
217 cluster = get_object_or_404(Cluster, slug=cluster_slug)
218 else:
219 cluster = None
221 user = request.user
222 if not (user.is_superuser or (cluster and user.has_perm(
223 'admin', cluster))):
224 raise PermissionDenied(NO_PRIVS)
226 if request.method == 'POST':
227 form = EditClusterForm(request.POST, instance=cluster)
228 if form.is_valid():
229 cluster = form.save()
230 # TODO Create post signal to import
231 # virtual machines on edit of cluster
232 if cluster.info is None:
233 try:
234 cluster.sync_nodes()
235 cluster.sync_virtual_machines()
236 except GanetiApiError:
237 # ganeti errors here are silently discarded. It's
238 # valid to enter bad info. A user might be adding
239 # info for an offline cluster.
240 pass
242 log_action('EDIT' if cluster_slug else 'CREATE', user, cluster)
244 return HttpResponseRedirect(reverse('cluster-detail',
245 args=[cluster.slug]))
247 elif request.method == 'DELETE':
248 cluster.delete()
249 return HttpResponse('1', mimetype='application/json')
251 else:
252 form = EditClusterForm(instance=cluster)
254 return render_to_response("ganeti/cluster/edit.html", {
255 'form': form,
256 'cluster': cluster,
258 context_instance=RequestContext(request),
262 @login_required
263 def refresh(request, cluster_slug):
265 Display a notice to the user that we are refreshing
266 the cluster data, then redirect them back to the
267 cluster details page.
270 cluster = get_object_or_404(Cluster, slug=cluster_slug)
271 cluster.refresh()
272 cluster.sync_nodes(remove=True)
273 cluster.sync_virtual_machines(remove=True)
275 url = reverse('cluster-detail', args=[cluster.slug])
276 return redirect(url)
279 @login_required
280 def users(request, cluster_slug):
282 Display all of the Users of a Cluster
284 cluster = get_object_or_404(Cluster, slug=cluster_slug)
286 user = request.user
287 if not (user.is_superuser or user.has_perm('admin', cluster)):
288 raise PermissionDenied(NO_PRIVS)
290 url = reverse('cluster-permissions', args=[cluster.slug])
291 return view_users(request, cluster, url,
292 template='ganeti/cluster/users.html')
295 @login_required
296 def permissions(request, cluster_slug, user_id=None, group_id=None):
298 Update a users permissions.
299 This wraps object_permissions.view_permissions()
300 with our custom permissions checks.
302 cluster = get_object_or_404(Cluster, slug=cluster_slug)
303 user = request.user
304 if not (user.is_superuser or user.has_perm('admin', cluster)):
305 raise PermissionDenied(NO_PRIVS)
307 url = reverse('cluster-permissions', args=[cluster.slug])
308 return view_permissions(request, cluster, url, user_id, group_id,
309 user_template='ganeti/cluster/user_row.html',
310 group_template='ganeti/cluster/group_row.html')
313 @require_POST
314 @login_required
315 def redistribute_config(request, cluster_slug):
317 Redistribute master-node config to all cluster's other nodes.
319 cluster = get_object_or_404(Cluster, slug=cluster_slug)
321 user = request.user
322 if not (user.is_superuser or user.has_perm('admin', cluster)):
323 raise PermissionDenied(NO_PRIVS)
325 try:
326 job = cluster.redistribute_config()
327 job.refresh()
328 msg = job.info
330 log_action('CLUSTER_REDISTRIBUTE', user, cluster, job)
331 except GanetiApiError, e:
332 msg = {'__all__': [str(e)]}
333 return HttpResponse(json.dumps(msg), mimetype='application/json')
336 def ssh_keys(request, cluster_slug, api_key):
338 Show all ssh keys which belong to users, who have any perms on the cluster
340 if settings.WEB_MGR_API_KEY != api_key:
341 return HttpResponseForbidden(_("You're not allowed to view keys."))
343 cluster = get_object_or_404(Cluster, slug=cluster_slug)
345 users = set(get_users_any(cluster).values_list("id", flat=True))
346 for vm in cluster.virtual_machines.all():
347 users = users.union(set(get_users_any(vm)
348 .values_list('id', flat=True)))
350 keys = SSHKey.objects \
351 .filter(Q(user__in=users) | Q(user__is_superuser=True)) \
352 .values_list('key', 'user__username') \
353 .order_by('user__username')
355 keys_list = list(keys)
356 return HttpResponse(json.dumps(keys_list), mimetype="application/json")
359 @login_required
360 def quota(request, cluster_slug, user_id):
362 Updates quota for a user
364 cluster = get_object_or_404(Cluster, slug=cluster_slug)
365 user = request.user
366 if not (user.is_superuser or user.has_perm('admin', cluster)):
367 raise PermissionDenied(NO_PRIVS)
369 if request.method == 'POST':
370 form = QuotaForm(request.POST)
371 if form.is_valid():
372 data = form.cleaned_data
373 cluster_user = data['user']
374 if data['delete']:
375 cluster.set_quota(cluster_user, None)
376 else:
377 cluster.set_quota(cluster_user, data)
379 # return updated html
380 cluster_user = cluster_user.cast()
381 url = reverse('cluster-permissions', args=[cluster.slug])
382 if isinstance(cluster_user, (Profile,)):
383 return render_to_response(
384 "ganeti/cluster/user_row.html",
385 {'object': cluster, 'user_detail': cluster_user.user,
386 'url': url},
387 context_instance=RequestContext(request))
388 else:
389 return render_to_response(
390 "ganeti/cluster/group_row.html",
391 {'object': cluster, 'group': cluster_user.group,
392 'url': url},
393 context_instance=RequestContext(request))
395 # error in form return ajax response
396 content = json.dumps(form.errors)
397 return HttpResponse(content, mimetype='application/json')
399 if user_id:
400 cluster_user = get_object_or_404(ClusterUser, id=user_id)
401 quota = cluster.get_quota(cluster_user)
402 data = {'user': user_id}
403 if quota:
404 data.update(quota)
405 else:
406 return render_404(request, _('User was not found'))
408 form = QuotaForm(data)
409 return render_to_response("ganeti/cluster/quota.html",
410 {'form': form, 'cluster': cluster,
411 'user_id': user_id},
412 context_instance=RequestContext(request))
415 @login_required
416 def job_status(request, id, rest=False):
418 Return a list of basic info for running jobs.
421 ct = ContentType.objects.get_for_model(Cluster)
422 jobs = Job.objects.filter(status__in=("error", "running", "waiting"),
423 content_type=ct,
424 object_id=id).order_by('job_id')
425 jobs = [j.info for j in jobs]
427 if rest:
428 return jobs
429 else:
430 return HttpResponse(json.dumps(jobs), mimetype='application/json')
433 @login_required
434 def object_log(request, cluster_slug):
435 """ displays object log for this cluster """
436 cluster = get_object_or_404(Cluster, slug=cluster_slug)
437 user = request.user
438 if not (user.is_superuser or user.has_perm('admin', cluster)):
439 raise PermissionDenied(NO_PRIVS)
440 return list_for_object(request, cluster)
443 def recv_user_add(sender, editor, user, obj, **kwargs):
445 receiver for object_permissions.signals.view_add_user, Logs action
447 log_action('ADD_USER', editor, obj, user)
450 def recv_user_remove(sender, editor, user, obj, **kwargs):
452 receiver for object_permissions.signals.view_remove_user, Logs action
454 log_action('REMOVE_USER', editor, obj, user)
456 # remove custom quota user may have had.
457 if isinstance(user, (User,)):
458 cluster_user = user.get_profile()
459 else:
460 cluster_user = user.organization
461 cluster_user.quotas.filter(cluster=obj).delete()
464 def recv_perm_edit(sender, editor, user, obj, **kwargs):
466 receiver for object_permissions.signals.view_edit_user, Logs action
468 log_action('MODIFY_PERMS', editor, obj, user)
471 op_signals.view_add_user.connect(recv_user_add, sender=Cluster)
472 op_signals.view_remove_user.connect(recv_user_remove, sender=Cluster)
473 op_signals.view_edit_user.connect(recv_perm_edit, sender=Cluster)