Move default template for tables out of template into table base class
[ganeti_webmgr.git] / ganeti_web / views / cluster.py
blobd06750290ce7b5ab33899adadedcef3c64e9dbbb
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.urlresolvers import reverse
25 from django.db.models import Q, Sum
26 from django.http import (HttpResponse, HttpResponseRedirect,
27 HttpResponseForbidden)
28 from django.shortcuts import get_object_or_404, render_to_response, redirect
29 from django.template import RequestContext
30 from django.utils import simplejson as json
31 from django.utils.translation import ugettext as _
32 from django.views.decorators.http import require_POST
33 from django.views.generic.detail import DetailView
35 from django_tables2 import SingleTableView
37 from object_permissions import get_users_any
38 from object_permissions import signals as op_signals
39 from object_permissions.views.permissions import view_users, view_permissions
41 from object_log.models import LogItem
42 from object_log.views import list_for_object
44 log_action = LogItem.objects.log_action
46 from ganeti_web.backend.queries import vm_qs_for_users, cluster_qs_for_user
47 from ganeti_web.forms.cluster import EditClusterForm, QuotaForm
48 from ganeti_web.middleware import Http403
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 return {
73 "cluster": cluster,
74 "admin": admin,
75 "readonly": not admin,
79 class ClusterListView(LoginRequiredMixin, PaginationMixin, GWMBaseView,
80 SingleTableView):
82 template_name = "ganeti/cluster/list.html"
83 model = Cluster
84 table_class = ClusterTable
86 def get_queryset(self):
87 self.queryset = cluster_qs_for_user(self.request.user)
88 qs = super(ClusterListView, self).get_queryset()
89 qs = qs.select_related("nodes", "virtual_machines")
90 return qs
92 def get_context_data(self, **kwargs):
93 user = self.request.user
94 context = super(ClusterListView, self).get_context_data(**kwargs)
95 context["create_vm"] = (user.is_superuser or
96 user.has_perm("admin", Cluster))
97 return context
100 class ClusterVMListView(BaseVMListView):
101 table_class = ClusterVMTable
103 def get_queryset(self):
104 self.get_kwargs()
105 # Store most of these variables on the object, because we'll be using
106 # them in context data too
107 self.cluster = get_object_or_404(Cluster, slug=self.cluster_slug)
108 # check privs
109 self.admin = self.can_create(self.cluster)
110 if not self.admin:
111 raise Http403(NO_PRIVS)
112 self.queryset = vm_qs_for_users(self.request.user, clusters=False)
113 # Calling super automatically filters by cluster
114 return super(ClusterVMListView, self).get_queryset()
116 def get_context_data(self, **kwargs):
117 context = super(ClusterVMListView, self).get_context_data(**kwargs)
118 if self.cluster_slug:
119 context["cluster"] = self.cluster
120 context["create_vm"] = self.admin
121 # Required since we cant use a relative link.
122 context["ajax_url"] = reverse(
123 "cluster-vm-list",
124 kwargs={'cluster_slug': self.cluster_slug}
126 return context
129 class ClusterJobListView(LoginRequiredMixin, PaginationMixin, GWMBaseView,
130 SingleTableView):
132 template_name = "ganeti/cluster/jobs.html"
133 model = Job
134 table_class = ClusterJobTable
136 def get_template_names(self):
137 if self.request.is_ajax():
138 template = ['table.html'] # all we need is the table
139 else:
140 template = [self.template_name]
141 return template
143 def get_queryset(self):
144 self.get_kwargs()
145 self.cluster = get_object_or_404(Cluster, slug=self.cluster_slug)
146 perms = self.can_create(self.cluster)
147 self.queryset = self.cluster.jobs.all()
148 if not perms:
149 return Http403(NO_PRIVS)
151 return super(ClusterJobListView, self).get_queryset()
153 def get_context_data(self, **kwargs):
154 context = super(ClusterJobListView, self).get_context_data(**kwargs)
155 context['ajax_url'] = reverse(
156 'cluster-job-list',
157 kwargs={'cluster_slug': self.cluster_slug}
159 return context
162 @login_required
163 def nodes(request, cluster_slug):
165 Display all nodes in a cluster
167 cluster = get_object_or_404(Cluster, slug=cluster_slug)
168 user = request.user
169 if not (user.is_superuser or user.has_perm('admin', cluster)):
170 raise Http403(NO_PRIVS)
172 # query allocated CPUS for all nodes in this list. Must be done here to
173 # avoid querying Node.allocated_cpus for each node in the list. Repackage
174 # list so it is easier to retrieve the values in the template
175 values = VirtualMachine.objects \
176 .filter(cluster=cluster, status='running') \
177 .exclude(virtual_cpus=-1) \
178 .order_by() \
179 .values('primary_node') \
180 .annotate(cpus=Sum('virtual_cpus'))
181 cpus = {}
182 nodes = cluster.nodes.all()
183 for d in values:
184 cpus[d['primary_node']] = d['cpus']
186 # Include nodes that do not have any virtual machines on them.
187 for node in nodes:
188 if node.pk not in cpus:
189 cpus[node.pk] = 0
191 return render_to_response("ganeti/node/table.html",
192 {'cluster': cluster,
193 'nodes': nodes,
194 'cpus': cpus,
196 context_instance=RequestContext(request),
200 @login_required
201 def edit(request, cluster_slug=None):
203 Edit a cluster
205 if cluster_slug:
206 cluster = get_object_or_404(Cluster, slug=cluster_slug)
207 else:
208 cluster = None
210 user = request.user
211 if not (user.is_superuser or (cluster and user.has_perm(
212 'admin', cluster))):
213 raise Http403(NO_PRIVS)
215 if request.method == 'POST':
216 form = EditClusterForm(request.POST, instance=cluster)
217 if form.is_valid():
218 cluster = form.save()
219 # TODO Create post signal to import
220 # virtual machines on edit of cluster
221 if cluster.info is None:
222 try:
223 cluster.sync_nodes()
224 cluster.sync_virtual_machines()
225 except GanetiApiError:
226 # ganeti errors here are silently discarded. It's
227 # valid to enter bad info. A user might be adding
228 # info for an offline cluster.
229 pass
231 log_action('EDIT' if cluster_slug else 'CREATE', user, cluster)
233 return HttpResponseRedirect(reverse('cluster-detail',
234 args=[cluster.slug]))
236 elif request.method == 'DELETE':
237 cluster.delete()
238 return HttpResponse('1', mimetype='application/json')
240 else:
241 form = EditClusterForm(instance=cluster)
243 return render_to_response("ganeti/cluster/edit.html", {
244 'form': form,
245 'cluster': cluster,
247 context_instance=RequestContext(request),
251 @login_required
252 def refresh(request, cluster_slug):
254 Display a notice to the user that we are refreshing
255 the cluster data, then redirect them back to the
256 cluster details page.
259 cluster = get_object_or_404(Cluster, slug=cluster_slug)
260 cluster.sync_nodes(remove=True)
261 cluster.sync_virtual_machines(remove=True)
263 url = reverse('cluster-detail', args=[cluster.slug])
264 return redirect(url)
267 @login_required
268 def users(request, cluster_slug):
270 Display all of the Users of a Cluster
272 cluster = get_object_or_404(Cluster, slug=cluster_slug)
274 user = request.user
275 if not (user.is_superuser or user.has_perm('admin', cluster)):
276 raise Http403(NO_PRIVS)
278 url = reverse('cluster-permissions', args=[cluster.slug])
279 return view_users(request, cluster, url,
280 template='ganeti/cluster/users.html')
283 @login_required
284 def permissions(request, cluster_slug, user_id=None, group_id=None):
286 Update a users permissions.
287 This wraps object_permissions.view_permissions()
288 with our custom permissions checks.
290 cluster = get_object_or_404(Cluster, slug=cluster_slug)
291 user = request.user
292 if not (user.is_superuser or user.has_perm('admin', cluster)):
293 raise Http403(NO_PRIVS)
295 url = reverse('cluster-permissions', args=[cluster.slug])
296 return view_permissions(request, cluster, url, user_id, group_id,
297 user_template='ganeti/cluster/user_row.html',
298 group_template='ganeti/cluster/group_row.html')
301 @require_POST
302 @login_required
303 def redistribute_config(request, cluster_slug):
305 Redistribute master-node config to all cluster's other nodes.
307 cluster = get_object_or_404(Cluster, slug=cluster_slug)
309 user = request.user
310 if not (user.is_superuser or user.has_perm('admin', cluster)):
311 raise Http403(NO_PRIVS)
313 try:
314 job = cluster.redistribute_config()
315 job.refresh()
316 msg = job.info
318 log_action('CLUSTER_REDISTRIBUTE', user, cluster, job)
319 except GanetiApiError, e:
320 msg = {'__all__': [str(e)]}
321 return HttpResponse(json.dumps(msg), mimetype='application/json')
324 def ssh_keys(request, cluster_slug, api_key):
326 Show all ssh keys which belong to users, who have any perms on the cluster
328 if settings.WEB_MGR_API_KEY != api_key:
329 return HttpResponseForbidden(_("You're not allowed to view keys."))
331 cluster = get_object_or_404(Cluster, slug=cluster_slug)
333 users = set(get_users_any(cluster).values_list("id", flat=True))
334 for vm in cluster.virtual_machines.all():
335 users = users.union(set(get_users_any(vm)
336 .values_list('id', flat=True)))
338 keys = SSHKey.objects \
339 .filter(Q(user__in=users) | Q(user__is_superuser=True)) \
340 .values_list('key', 'user__username') \
341 .order_by('user__username')
343 keys_list = list(keys)
344 return HttpResponse(json.dumps(keys_list), mimetype="application/json")
347 @login_required
348 def quota(request, cluster_slug, user_id):
350 Updates quota for a user
352 cluster = get_object_or_404(Cluster, slug=cluster_slug)
353 user = request.user
354 if not (user.is_superuser or user.has_perm('admin', cluster)):
355 raise Http403(NO_PRIVS)
357 if request.method == 'POST':
358 form = QuotaForm(request.POST)
359 if form.is_valid():
360 data = form.cleaned_data
361 cluster_user = data['user']
362 if data['delete']:
363 cluster.set_quota(cluster_user, None)
364 else:
365 cluster.set_quota(cluster_user, data)
367 # return updated html
368 cluster_user = cluster_user.cast()
369 url = reverse('cluster-permissions', args=[cluster.slug])
370 if isinstance(cluster_user, (Profile,)):
371 return render_to_response(
372 "ganeti/cluster/user_row.html",
373 {'object': cluster, 'user_detail': cluster_user.user,
374 'url': url},
375 context_instance=RequestContext(request))
376 else:
377 return render_to_response(
378 "ganeti/cluster/group_row.html",
379 {'object': cluster, 'group': cluster_user.group,
380 'url': url},
381 context_instance=RequestContext(request))
383 # error in form return ajax response
384 content = json.dumps(form.errors)
385 return HttpResponse(content, mimetype='application/json')
387 if user_id:
388 cluster_user = get_object_or_404(ClusterUser, id=user_id)
389 quota = cluster.get_quota(cluster_user)
390 data = {'user': user_id}
391 if quota:
392 data.update(quota)
393 else:
394 return render_404(request, _('User was not found'))
396 form = QuotaForm(data)
397 return render_to_response("ganeti/cluster/quota.html",
398 {'form': form, 'cluster': cluster,
399 'user_id': user_id},
400 context_instance=RequestContext(request))
403 @login_required
404 def job_status(request, id, rest=False):
406 Return a list of basic info for running jobs.
409 ct = ContentType.objects.get_for_model(Cluster)
410 jobs = Job.objects.filter(status__in=("error", "running", "waiting"),
411 content_type=ct,
412 object_id=id).order_by('job_id')
413 jobs = [j.info for j in jobs]
415 if rest:
416 return jobs
417 else:
418 return HttpResponse(json.dumps(jobs), mimetype='application/json')
421 @login_required
422 def object_log(request, cluster_slug):
423 """ displays object log for this cluster """
424 cluster = get_object_or_404(Cluster, slug=cluster_slug)
425 user = request.user
426 if not (user.is_superuser or user.has_perm('admin', cluster)):
427 raise Http403(NO_PRIVS)
428 return list_for_object(request, cluster)
431 def recv_user_add(sender, editor, user, obj, **kwargs):
433 receiver for object_permissions.signals.view_add_user, Logs action
435 log_action('ADD_USER', editor, obj, user)
438 def recv_user_remove(sender, editor, user, obj, **kwargs):
440 receiver for object_permissions.signals.view_remove_user, Logs action
442 log_action('REMOVE_USER', editor, obj, user)
444 # remove custom quota user may have had.
445 if isinstance(user, (User,)):
446 cluster_user = user.get_profile()
447 else:
448 cluster_user = user.organization
449 cluster_user.quotas.filter(cluster=obj).delete()
452 def recv_perm_edit(sender, editor, user, obj, **kwargs):
454 receiver for object_permissions.signals.view_edit_user, Logs action
456 log_action('MODIFY_PERMS', editor, obj, user)
459 op_signals.view_add_user.connect(recv_user_add, sender=Cluster)
460 op_signals.view_remove_user.connect(recv_user_remove, sender=Cluster)
461 op_signals.view_edit_user.connect(recv_perm_edit, sender=Cluster)