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
.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
,
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
)
75 "readonly": not admin
,
79 class ClusterListView(LoginRequiredMixin
, PaginationMixin
, GWMBaseView
,
82 template_name
= "ganeti/cluster/list.html"
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")
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
))
100 class ClusterVMListView(BaseVMListView
):
101 table_class
= ClusterVMTable
103 def get_queryset(self
):
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
)
109 self
.admin
= self
.can_create(self
.cluster
)
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(
124 kwargs
={'cluster_slug': self
.cluster_slug
}
129 class ClusterJobListView(LoginRequiredMixin
, PaginationMixin
, GWMBaseView
,
132 template_name
= "ganeti/cluster/jobs.html"
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
140 template
= [self
.template_name
]
143 def get_queryset(self
):
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()
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(
157 kwargs
={'cluster_slug': self
.cluster_slug
}
163 def nodes(request
, cluster_slug
):
165 Display all nodes in a cluster
167 cluster
= get_object_or_404(Cluster
, slug
=cluster_slug
)
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) \
179 .values('primary_node') \
180 .annotate(cpus
=Sum('virtual_cpus'))
182 nodes
= cluster
.nodes
.all()
184 cpus
[d
['primary_node']] = d
['cpus']
186 # Include nodes that do not have any virtual machines on them.
188 if node
.pk
not in cpus
:
191 return render_to_response("ganeti/node/table.html",
196 context_instance
=RequestContext(request
),
201 def edit(request
, cluster_slug
=None):
206 cluster
= get_object_or_404(Cluster
, slug
=cluster_slug
)
211 if not (user
.is_superuser
or (cluster
and user
.has_perm(
213 raise Http403(NO_PRIVS
)
215 if request
.method
== 'POST':
216 form
= EditClusterForm(request
.POST
, instance
=cluster
)
218 cluster
= form
.save()
219 # TODO Create post signal to import
220 # virtual machines on edit of cluster
221 if cluster
.info
is None:
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.
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':
238 return HttpResponse('1', mimetype
='application/json')
241 form
= EditClusterForm(instance
=cluster
)
243 return render_to_response("ganeti/cluster/edit.html", {
247 context_instance
=RequestContext(request
),
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
])
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
)
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')
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
)
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')
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
)
310 if not (user
.is_superuser
or user
.has_perm('admin', cluster
)):
311 raise Http403(NO_PRIVS
)
314 job
= cluster
.redistribute_config()
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")
348 def quota(request
, cluster_slug
, user_id
):
350 Updates quota for a user
352 cluster
= get_object_or_404(Cluster
, slug
=cluster_slug
)
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
)
360 data
= form
.cleaned_data
361 cluster_user
= data
['user']
363 cluster
.set_quota(cluster_user
, None)
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
,
375 context_instance
=RequestContext(request
))
377 return render_to_response(
378 "ganeti/cluster/group_row.html",
379 {'object': cluster
, 'group': cluster_user
.group
,
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')
388 cluster_user
= get_object_or_404(ClusterUser
, id=user_id
)
389 quota
= cluster
.get_quota(cluster_user
)
390 data
= {'user': user_id
}
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
,
400 context_instance
=RequestContext(request
))
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"),
412 object_id
=id).order_by('job_id')
413 jobs
= [j
.info
for j
in jobs
]
418 return HttpResponse(json
.dumps(jobs
), mimetype
='application/json')
422 def object_log(request
, cluster_slug
):
423 """ displays object log for this cluster """
424 cluster
= get_object_or_404(Cluster
, slug
=cluster_slug
)
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()
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
)