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,
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
,
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
,
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
)
80 "readonly": not admin
,
85 class ClusterListView(LoginRequiredMixin
, PaginationMixin
, GWMBaseView
,
88 template_name
= "ganeti/cluster/list.html"
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")
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
))
106 class ClusterVMListView(BaseVMListView
):
107 table_class
= ClusterVMTable
109 def get_queryset(self
):
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(
135 kwargs
={'cluster_slug': self
.cluster_slug
}
140 class ClusterJobListView(LoginRequiredMixin
, PaginationMixin
, GWMBaseView
,
143 template_name
= "ganeti/cluster/jobs.html"
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
151 template
= [self
.template_name
]
154 def get_queryset(self
):
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()
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(
168 kwargs
={'cluster_slug': self
.cluster_slug
}
174 def nodes(request
, cluster_slug
):
176 Display all nodes in a cluster
178 cluster
= get_object_or_404(Cluster
, slug
=cluster_slug
)
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) \
190 .values('primary_node') \
191 .annotate(cpus
=Sum('virtual_cpus'))
193 nodes
= cluster
.nodes
.all()
195 cpus
[d
['primary_node']] = d
['cpus']
197 # Include nodes that do not have any virtual machines on them.
199 if node
.pk
not in cpus
:
202 return render_to_response("ganeti/node/table.html",
207 context_instance
=RequestContext(request
),
212 def edit(request
, cluster_slug
=None):
217 cluster
= get_object_or_404(Cluster
, slug
=cluster_slug
)
222 if not (user
.is_superuser
or (cluster
and user
.has_perm(
224 raise PermissionDenied(NO_PRIVS
)
226 if request
.method
== 'POST':
227 form
= EditClusterForm(request
.POST
, instance
=cluster
)
229 cluster
= form
.save()
230 # TODO Create post signal to import
231 # virtual machines on edit of cluster
232 if cluster
.info
is None:
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.
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':
249 return HttpResponse('1', mimetype
='application/json')
252 form
= EditClusterForm(instance
=cluster
)
254 return render_to_response("ganeti/cluster/edit.html", {
258 context_instance
=RequestContext(request
),
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
)
272 cluster
.sync_nodes(remove
=True)
273 cluster
.sync_virtual_machines(remove
=True)
275 url
= reverse('cluster-detail', args
=[cluster
.slug
])
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
)
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')
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
)
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')
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
)
322 if not (user
.is_superuser
or user
.has_perm('admin', cluster
)):
323 raise PermissionDenied(NO_PRIVS
)
326 job
= cluster
.redistribute_config()
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")
360 def quota(request
, cluster_slug
, user_id
):
362 Updates quota for a user
364 cluster
= get_object_or_404(Cluster
, slug
=cluster_slug
)
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
)
372 data
= form
.cleaned_data
373 cluster_user
= data
['user']
375 cluster
.set_quota(cluster_user
, None)
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
,
387 context_instance
=RequestContext(request
))
389 return render_to_response(
390 "ganeti/cluster/group_row.html",
391 {'object': cluster
, 'group': cluster_user
.group
,
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')
400 cluster_user
= get_object_or_404(ClusterUser
, id=user_id
)
401 quota
= cluster
.get_quota(cluster_user
)
402 data
= {'user': user_id
}
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
,
412 context_instance
=RequestContext(request
))
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"),
424 object_id
=id).order_by('job_id')
425 jobs
= [j
.info
for j
in jobs
]
430 return HttpResponse(json
.dumps(jobs
), mimetype
='application/json')
434 def object_log(request
, cluster_slug
):
435 """ displays object log for this cluster """
436 cluster
= get_object_or_404(Cluster
, slug
=cluster_slug
)
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()
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
)