Add more information on deploying; fix format.
[ganeti_webmgr.git] / ganeti_web / views / cluster.py
blob9b4d204f245ef081be04e02b2a00982305870cd7
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
34 from django.views.generic.list import ListView
36 from object_permissions import get_users_any
37 from object_permissions import signals as op_signals
38 from object_permissions.views.permissions import view_users, view_permissions
40 from object_log.models import LogItem
41 from object_log.views import list_for_object
43 log_action = LogItem.objects.log_action
45 from ganeti_web.util.client import GanetiApiError
46 from ganeti_web.middleware import Http403
47 from ganeti_web.models import (Cluster, ClusterUser, Profile, SSHKey,
48 VirtualMachine, Job)
49 from ganeti_web.views import render_404
50 from ganeti_web.forms.cluster import EditClusterForm, QuotaForm
51 from ganeti_web.views.generic import (NO_PRIVS, LoginRequiredMixin,
52 PagedListView)
55 class ClusterDetailView(LoginRequiredMixin, DetailView):
57 template_name = "ganeti/cluster/detail.html"
59 def get_object(self, queryset=None):
60 return get_object_or_404(Cluster, slug=self.kwargs["cluster_slug"])
62 def get_context_data(self, **kwargs):
63 cluster = kwargs["object"]
64 user = self.request.user
65 admin = user.is_superuser or user.has_perm("admin", cluster)
67 return {
68 "cluster": cluster,
69 "admin": admin,
70 "readonly": not admin,
74 class ClusterListView(LoginRequiredMixin, PagedListView):
76 template_name = "ganeti/cluster/list.html"
78 def get_queryset(self):
79 if self.request.user.is_superuser:
80 qs = Cluster.objects.all()
81 else:
82 perms = ['admin', 'migrate', 'export', 'replace_disks', 'tags']
83 qs = self.request.user.get_objects_any_perms(Cluster, perms)
85 self.queryset = qs
86 super(ClusterListView, self).get_queryset()
87 return qs.select_related()
89 def get_context_data(self, **kwargs):
90 user = self.request.user
91 context = super(ClusterListView, self).get_context_data(
92 object_list=kwargs["object_list"])
93 context["can_create"] = (user.is_superuser or
94 user.has_perm("admin", Cluster))
96 if "order_by" in self.request.GET:
97 context["order"] = self.request.GET["order_by"]
98 else:
99 context["order"] = "id"
101 return context
104 class ClusterVMListView(LoginRequiredMixin, PagedListView):
106 template_name = "ganeti/virtual_machine/table.html"
108 def get_queryset(self):
109 self.cluster = get_object_or_404(Cluster,
110 slug=self.kwargs["cluster_slug"])
111 user = self.request.user
112 admin = user.is_superuser or user.has_perm("admin", self.cluster)
113 if not admin:
114 raise Http403(NO_PRIVS)
116 return self.cluster.virtual_machines.select_related("cluster").all()
118 def get_context_data(self, **kwargs):
119 kwargs["cluster"] = self.cluster
120 return kwargs
123 class ClusterJobListView(LoginRequiredMixin, PagedListView):
125 template_name = "ganeti/cluster/jobs.html"
127 def get_queryset(self):
128 self.cluster = get_object_or_404(Cluster,
129 slug=self.kwargs["cluster_slug"])
131 user = self.request.user
132 admin = user.is_superuser or user.has_perm("admin", self.cluster) or \
133 user.has_perm("create_vm", self.cluster)
135 if not admin:
136 raise Http403(NO_PRIVS)
138 qs = Job.objects.filter(cluster=self.cluster.id)
139 if "order_by" in self.request.GET:
140 qs = qs.order_by(self.request.GET['order_by'])
142 self.queryset = qs
143 super(ClusterJobListView, self).get_queryset()
145 return qs.select_related()
147 def get_context_data(self, **kwargs):
148 context = super(ClusterJobListView, self).get_context_data(
149 object_list=kwargs["object_list"])
150 context["cluster"] = self.cluster
152 if "order_by" in self.request.GET:
153 context["order"] = self.request.GET["order_by"]
154 else:
155 context["order"] = "id"
157 return context
160 @login_required
161 def nodes(request, cluster_slug):
163 Display all nodes in a cluster
165 cluster = get_object_or_404(Cluster, slug=cluster_slug)
166 user = request.user
167 if not (user.is_superuser or user.has_perm('admin', cluster)):
168 raise Http403(NO_PRIVS)
170 # query allocated CPUS for all nodes in this list. Must be done here to
171 # avoid querying Node.allocated_cpus for each node in the list. Repackage
172 # list so it is easier to retrieve the values in the template
173 values = VirtualMachine.objects \
174 .filter(cluster=cluster, status='running') \
175 .exclude(virtual_cpus=-1) \
176 .order_by() \
177 .values('primary_node') \
178 .annotate(cpus=Sum('virtual_cpus'))
179 cpus = {}
180 nodes = cluster.nodes.all()
181 for d in values:
182 cpus[d['primary_node']] = d['cpus']
184 # Include nodes that do not have any virtual machines on them.
185 for node in nodes:
186 if node.pk not in cpus:
187 cpus[node.pk] = 0
189 return render_to_response("ganeti/node/table.html",
190 {'cluster': cluster,
191 'nodes': nodes,
192 'cpus': cpus,
194 context_instance=RequestContext(request),
198 @login_required
199 def edit(request, cluster_slug=None):
201 Edit a cluster
203 if cluster_slug:
204 cluster = get_object_or_404(Cluster, slug=cluster_slug)
205 else:
206 cluster = None
208 user = request.user
209 if not (user.is_superuser or (cluster and user.has_perm(
210 'admin', cluster))):
211 raise Http403(NO_PRIVS)
213 if request.method == 'POST':
214 form = EditClusterForm(request.POST, instance=cluster)
215 if form.is_valid():
216 cluster = form.save()
217 # TODO Create post signal to import
218 # virtual machines on edit of cluster
219 if cluster.info is None:
220 try:
221 cluster.sync_nodes()
222 cluster.sync_virtual_machines()
223 except GanetiApiError:
224 # ganeti errors here are silently discarded. It's
225 # valid to enter bad info. A user might be adding
226 # info for an offline cluster.
227 pass
229 log_action('EDIT' if cluster_slug else 'CREATE', user, cluster)
231 return HttpResponseRedirect(reverse('cluster-detail',
232 args=[cluster.slug]))
234 elif request.method == 'DELETE':
235 cluster.delete()
236 return HttpResponse('1', mimetype='application/json')
238 else:
239 form = EditClusterForm(instance=cluster)
241 return render_to_response("ganeti/cluster/edit.html", {
242 'form': form,
243 'cluster': cluster,
245 context_instance=RequestContext(request),
249 @login_required
250 def refresh(request, cluster_slug):
252 Display a notice to the user that we are refreshing
253 the cluster data, then redirect them back to the
254 cluster details page.
257 cluster = get_object_or_404(Cluster, slug=cluster_slug)
258 cluster.sync_nodes(remove=True)
259 cluster.sync_virtual_machines(remove=True)
261 url = reverse('cluster-detail', args=[cluster.slug])
262 return redirect(url)
265 @login_required
266 def users(request, cluster_slug):
268 Display all of the Users of a Cluster
270 cluster = get_object_or_404(Cluster, slug=cluster_slug)
272 user = request.user
273 if not (user.is_superuser or user.has_perm('admin', cluster)):
274 raise Http403(NO_PRIVS)
276 url = reverse('cluster-permissions', args=[cluster.slug])
277 return view_users(request, cluster, url,
278 template='ganeti/cluster/users.html')
281 @login_required
282 def permissions(request, cluster_slug, user_id=None, group_id=None):
284 Update a users permissions.
285 This wraps object_permissions.view_permissions()
286 with our custom permissions checks.
288 cluster = get_object_or_404(Cluster, slug=cluster_slug)
289 user = request.user
290 if not (user.is_superuser or user.has_perm('admin', cluster)):
291 raise Http403(NO_PRIVS)
293 url = reverse('cluster-permissions', args=[cluster.slug])
294 return view_permissions(request, cluster, url, user_id, group_id,
295 user_template='ganeti/cluster/user_row.html',
296 group_template='ganeti/cluster/group_row.html')
299 @require_POST
300 @login_required
301 def redistribute_config(request, cluster_slug):
303 Redistribute master-node config to all cluster's other nodes.
305 cluster = get_object_or_404(Cluster, slug=cluster_slug)
307 user = request.user
308 if not (user.is_superuser or user.has_perm('admin', cluster)):
309 raise Http403(NO_PRIVS)
311 try:
312 job = cluster.redistribute_config()
313 job.refresh()
314 msg = job.info
316 log_action('CLUSTER_REDISTRIBUTE', user, cluster, job)
317 except GanetiApiError, e:
318 msg = {'__all__': [str(e)]}
319 return HttpResponse(json.dumps(msg), mimetype='application/json')
322 def ssh_keys(request, cluster_slug, api_key):
324 Show all ssh keys which belong to users, who have any perms on the cluster
326 if settings.WEB_MGR_API_KEY != api_key:
327 return HttpResponseForbidden(_("You're not allowed to view keys."))
329 cluster = get_object_or_404(Cluster, slug=cluster_slug)
331 users = set(get_users_any(cluster).values_list("id", flat=True))
332 for vm in cluster.virtual_machines.all():
333 users = users.union(set(get_users_any(vm)
334 .values_list('id', flat=True)))
336 keys = SSHKey.objects \
337 .filter(Q(user__in=users) | Q(user__is_superuser=True)) \
338 .values_list('key', 'user__username') \
339 .order_by('user__username')
341 keys_list = list(keys)
342 return HttpResponse(json.dumps(keys_list), mimetype="application/json")
345 @login_required
346 def quota(request, cluster_slug, user_id):
348 Updates quota for a user
350 cluster = get_object_or_404(Cluster, slug=cluster_slug)
351 user = request.user
352 if not (user.is_superuser or user.has_perm('admin', cluster)):
353 raise Http403(NO_PRIVS)
355 if request.method == 'POST':
356 form = QuotaForm(request.POST)
357 if form.is_valid():
358 data = form.cleaned_data
359 cluster_user = data['user']
360 if data['delete']:
361 cluster.set_quota(cluster_user, None)
362 else:
363 cluster.set_quota(cluster_user, data)
365 # return updated html
366 cluster_user = cluster_user.cast()
367 url = reverse('cluster-permissions', args=[cluster.slug])
368 if isinstance(cluster_user, (Profile,)):
369 return render_to_response(
370 "ganeti/cluster/user_row.html",
371 {'object': cluster, 'user_detail': cluster_user.user,
372 'url': url},
373 context_instance=RequestContext(request))
374 else:
375 return render_to_response(
376 "ganeti/cluster/group_row.html",
377 {'object': cluster, 'group': cluster_user.group,
378 'url': url},
379 context_instance=RequestContext(request))
381 # error in form return ajax response
382 content = json.dumps(form.errors)
383 return HttpResponse(content, mimetype='application/json')
385 if user_id:
386 cluster_user = get_object_or_404(ClusterUser, id=user_id)
387 quota = cluster.get_quota(cluster_user)
388 data = {'user': user_id}
389 if quota:
390 data.update(quota)
391 else:
392 return render_404(request, _('User was not found'))
394 form = QuotaForm(data)
395 return render_to_response("ganeti/cluster/quota.html",
396 {'form': form, 'cluster': cluster,
397 'user_id': user_id},
398 context_instance=RequestContext(request))
401 @login_required
402 def job_status(request, id, rest=False):
404 Return a list of basic info for running jobs.
407 ct = ContentType.objects.get_for_model(Cluster)
408 jobs = Job.objects.filter(status__in=("error", "running", "waiting"),
409 content_type=ct,
410 object_id=id).order_by('job_id')
411 jobs = [j.info for j in jobs]
413 if rest:
414 return jobs
415 else:
416 return HttpResponse(json.dumps(jobs), mimetype='application/json')
419 @login_required
420 def object_log(request, cluster_slug):
421 """ displays object log for this cluster """
422 cluster = get_object_or_404(Cluster, slug=cluster_slug)
423 user = request.user
424 if not (user.is_superuser or user.has_perm('admin', cluster)):
425 raise Http403(NO_PRIVS)
426 return list_for_object(request, cluster)
429 def recv_user_add(sender, editor, user, obj, **kwargs):
431 receiver for object_permissions.signals.view_add_user, Logs action
433 log_action('ADD_USER', editor, obj, user)
436 def recv_user_remove(sender, editor, user, obj, **kwargs):
438 receiver for object_permissions.signals.view_remove_user, Logs action
440 log_action('REMOVE_USER', editor, obj, user)
442 # remove custom quota user may have had.
443 if isinstance(user, (User,)):
444 cluster_user = user.get_profile()
445 else:
446 cluster_user = user.organization
447 cluster_user.quotas.filter(cluster=obj).delete()
450 def recv_perm_edit(sender, editor, user, obj, **kwargs):
452 receiver for object_permissions.signals.view_edit_user, Logs action
454 log_action('MODIFY_PERMS', editor, obj, user)
457 op_signals.view_add_user.connect(recv_user_add, sender=Cluster)
458 op_signals.view_remove_user.connect(recv_user_remove, sender=Cluster)
459 op_signals.view_edit_user.connect(recv_perm_edit, sender=Cluster)