models: Remove "cleared" and "processed" fields from Jobs.
[ganeti_webmgr.git] / ganeti_web / views / general.py
blob5c2856a4cbe3ff044277795506b9202e8ee14a91
1 # Copyright (C) 2010 Oregon State University et al.
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
16 # USA.
18 from itertools import chain, izip, repeat
20 from django.conf import settings
21 from django.contrib.auth.decorators import login_required
22 from django.contrib.contenttypes.models import ContentType
23 from django.core.cache import cache
24 from django.db.models import Q, Count
25 from django.http import HttpResponse, HttpResponseForbidden
26 from django.shortcuts import render_to_response, get_object_or_404
27 from django.template import RequestContext
28 from django.utils import simplejson as json
29 from django.views.generic.base import TemplateView
31 from object_permissions import get_users_any
33 from ganeti_web.middleware import Http403
34 from ganeti_web.models import Cluster, VirtualMachine, Job, GanetiError, \
35 ClusterUser, Profile, Organization, SSHKey
36 from ganeti_web.views import render_404
37 from ganeti_web.views.generic import NO_PRIVS
38 from django.utils.translation import ugettext as _
39 from ganeti_web.constants import VERSION
41 class AboutView(TemplateView):
43 template_name = "ganeti/about.html"
45 def render_to_response(self, context, **kwargs):
46 context["version"] = VERSION
47 return super(AboutView, self).render_to_response(context, **kwargs)
49 def merge_errors(errors, jobs):
50 """
51 Merge iterables of errors and jobs together.
53 The resulting list contains tuples of (bool, object) where the first
54 member indicates whether the object is a ``GanetiError`` or ``Job``.
55 """
57 def keyfunc(x):
58 """
59 Either the "finished" or "timestamp" attribute.
60 """
62 return getattr(x[1], "finished", getattr(x[1], "timestamp", 0))
64 i = chain(izip(repeat(True), errors), izip(repeat(False), jobs))
65 return list(sorted(i, key=keyfunc))
68 USED_NOTHING = dict(disk=0, ram=0, virtual_cpus=0)
71 def get_used_resources(cluster_user):
72 """ help function for querying resources used for a given cluster_user """
73 resources = {}
74 owned_vms = cluster_user.virtual_machines.all()
75 used = cluster_user.used_resources()
76 clusters = cluster_user.permissable.get_objects_any_perms(Cluster)
77 quotas = Cluster.get_quotas(clusters, cluster_user)
79 for cluster, quota in quotas.items():
80 resources[cluster] = {
81 "used": used.pop(cluster.id) if cluster.id in used else USED_NOTHING,
82 "set": quota
84 resources[cluster]["total"] = owned_vms.filter(cluster=cluster).count()
85 resources[cluster]["running"] = owned_vms.filter(cluster=cluster, \
86 status="running").count()
88 # add any clusters that have used resources but no perms (and thus no quota)
89 # since we know they don't have a custom quota just add the default quota
90 if used:
91 for cluster in Cluster.objects.filter(pk__in=used):
92 resources[cluster] = {"used":used[cluster.id],
93 "set":cluster.get_default_quota()}
94 resources[cluster]["total"] = owned_vms.filter(cluster=cluster).count()
95 resources[cluster]["running"] = owned_vms.filter(cluster=cluster, \
96 status="running").count()
98 return resources
101 def update_vm_counts(key, data):
103 Updates the cache for numbers of orphaned / ready to import / missing VMs.
105 If the cluster's data is not in cache it is ignored. This is only for
106 updating the cache with information we already have.
108 @param key - admin data key that is being updated: orphaned, ready_to_import,
109 or missing
110 @param data - dict of data stored by cluster.pk
112 format_key = 'cluster_admin_%d'
113 keys = [format_key % k for k in data.keys()]
114 cached = cache.get_many(keys)
116 for k, v in data.items():
117 try:
118 values = cached[format_key % k]
119 values[key] += v
120 except KeyError:
121 pass
123 cache.set_many(cached, 600)
126 def get_vm_counts(clusters, timeout=600):
128 Helper for getting the list of orphaned/ready to import/missing VMs.
129 Caches by the way.
131 This caches data under the keys: cluster_admin_<cluster_id>
133 @param clusters the list of clusters, for which numbers of VM are counted.
134 May be None, if update is set.
135 @param timeout specified timeout for cache, in seconds.
137 format_key = 'cluster_admin_%d'
138 orphaned = import_ready = missing = 0
139 cached = cache.get_many((format_key % cluster.pk for cluster in clusters))
140 exclude = [int(key[14:]) for key in cached.keys()]
141 keys = [k for k in clusters.values_list('pk', flat=True) if k not in exclude]
142 cluster_list = Cluster.objects.filter(pk__in=keys)
144 # total the cached values first
145 for k in cached.values():
146 orphaned += k["orphaned"]
147 import_ready += k["import_ready"]
148 missing += k["missing"]
150 # update the values that were not cached
151 if cluster_list.count():
152 base = VirtualMachine.objects.filter(cluster__in=cluster_list,
153 owner=None).order_by()
154 annotated = base.values("cluster__pk").annotate(orphaned=Count("id"))
156 result = {}
157 for i in annotated:
158 result[format_key % i["cluster__pk"]] = {"orphaned": i["orphaned"]}
159 orphaned += i["orphaned"]
160 for cluster in cluster_list:
161 key = format_key % cluster.pk
163 if key not in result:
164 result[key] = {"orphaned": 0}
166 result[key]["import_ready"] = len(cluster.missing_in_db)
167 result[key]["missing"] = len(cluster.missing_in_ganeti)
169 import_ready += result[key]["import_ready"]
170 missing += result[key]["missing"]
172 # add all results into cache
173 cache.set_many(result, timeout)
175 return orphaned, import_ready, missing
178 @login_required
179 def overview(request, rest=False):
181 Status page
183 user = request.user
185 if user.is_superuser:
186 clusters = Cluster.objects.all()
187 else:
188 clusters = user.get_objects_all_perms(Cluster, ['admin',])
189 admin = user.is_superuser or clusters
191 #orphaned, ready to import, missing
192 if admin:
193 # build list of admin tasks for this user's clusters
194 orphaned, import_ready, missing = get_vm_counts(clusters)
195 else:
196 orphaned = import_ready = missing = 0
198 if user.is_superuser:
199 vms = VirtualMachine.objects.all()
200 else:
201 # Get query containing any virtual machines the user has permissions for
202 vms = user.get_objects_any_perms(VirtualMachine, groups=True, cluster=['admin']).values('pk')
204 # build list of job errors. Include jobs from any vm the user has access to
205 # If the user has admin on any cluster then those clusters and it's objects
206 # must be included too.
208 # XXX all jobs have the cluster listed, filtering by cluster includes jobs
209 # for both the cluster itself and any of its VMs or Nodes
210 error_clause = Q(status='error')
211 vm_type = ContentType.objects.get_for_model(VirtualMachine)
212 select_clause = Q(content_type=vm_type, object_id__in=vms)
213 if admin:
214 select_clause |= Q(cluster__in=clusters)
215 job_errors = Job.objects.filter(error_clause & select_clause) \
216 .order_by("-finished")[:5]
218 # Build the list of job errors. Include jobs from any VMs for which the
219 # user has access.
220 qs = GanetiError.objects.filter(cleared=False)
221 ganeti_errors = qs.get_errors(obj=vms)
222 # If the user is an admin on any cluster, then include administrated
223 # clusters and related objects.
224 if admin:
225 ganeti_errors |= qs.get_errors(obj=clusters)
227 # merge error lists
228 errors = merge_errors(ganeti_errors, job_errors)
230 # get vm summary - running and totals need to be done as separate queries
231 # and then merged into a single list
232 vms_running = vms.filter(status='running') \
233 .order_by() \
234 .values('cluster__hostname','cluster__slug') \
235 .annotate(running=Count('pk'))
236 vms_total = vms.order_by()\
237 .values('cluster__hostname','cluster__slug') \
238 .annotate(total=Count('pk'))
239 vm_summary = {}
240 for cluster in vms_total:
241 vm_summary[cluster.pop('cluster__hostname')] = cluster
242 for cluster in vms_running:
243 vm_summary[cluster['cluster__hostname']]['running'] = cluster['running']
245 # get list of personas for the user: All groups, plus the user.
246 # include the user only if it owns a vm or has perms on at least one cluster
247 profile = user.get_profile()
248 personas = list(Organization.objects.filter(group__user=user))
249 if profile.virtual_machines.count() \
250 or user.has_any_perms(Cluster, ['admin', 'create_vm'], groups=False) \
251 or not personas:
252 personas.insert(0, profile)
254 # get resources used per cluster from the first persona in the list
255 resources = get_used_resources(personas[0])
257 if rest:
258 return clusters
259 else:
260 return render_to_response("ganeti/overview.html", {
261 'admin':admin,
262 'cluster_list': clusters,
263 'user': request.user,
264 'errors': errors,
265 'orphaned': orphaned,
266 'import_ready': import_ready,
267 'missing': missing,
268 'resources': resources,
269 'vm_summary': vm_summary,
270 'personas': personas,
272 context_instance=RequestContext(request),
276 @login_required
277 def used_resources(request, rest=False):
278 """ view for returning used resources for a given cluster user """
279 try:
280 cluster_user_id = request.GET['id']
281 except KeyError:
282 return render_404(request, 'requested user was not found')
283 cu = get_object_or_404(ClusterUser, pk=cluster_user_id)
285 # must be a super user, the user in question, or a member of the group
286 user = request.user
287 if not user.is_superuser:
288 user_type = ContentType.objects.get_for_model(Profile)
289 if cu.real_type_id == user_type.pk:
290 if not Profile.objects.filter(clusteruser_ptr=cu.pk, user=user)\
291 .exists():
292 raise Http403(_('You are not authorized to view this page'))
293 else:
294 if not Organization.objects.filter(clusteruser_ptr=cu.pk, \
295 group__user=user).exists():
296 raise Http403(_('You are not authorized to view this page'))
298 resources = get_used_resources(cu.cast())
299 if rest:
300 return resources
301 else:
302 return render_to_response("ganeti/overview/used_resources.html", {
303 'resources':resources
304 }, context_instance=RequestContext(request))
308 @login_required
309 def clear_ganeti_error(request, pk):
311 Clear a single error message
313 user = request.user
314 error = get_object_or_404(GanetiError, pk=pk)
315 obj = error.obj
317 # if not a superuser, check permissions on the object itself
318 if not user.is_superuser:
319 if isinstance(obj, (Cluster,)) and not user.has_perm('admin', obj):
320 raise Http403(NO_PRIVS)
321 elif isinstance(obj, (VirtualMachine,)):
322 # object is a virtual machine, check perms on VM and on Cluster
323 if not (obj.owner_id == user.get_profile().pk or \
324 user.has_perm('admin', obj.cluster)):
325 raise Http403(NO_PRIVS)
327 # clear the error
328 GanetiError.objects.filter(pk=error.pk).update(cleared=True)
330 return HttpResponse('1', mimetype='application/json')
333 def ssh_keys(request, api_key):
334 """ Lists all keys for all clusters managed by GWM """
336 Show all ssh keys which belong to users, who have any perms on the cluster
338 if settings.WEB_MGR_API_KEY != api_key:
339 return HttpResponseForbidden(_("You're not allowed to view keys."))
341 users = set()
342 for cluster in Cluster.objects.all():
343 users = users.union(set(get_users_any(cluster).values_list("id", flat=True)))
344 for vm in VirtualMachine.objects.all():
345 users = users.union(set(get_users_any(vm).values_list('id', flat=True)))
347 keys = SSHKey.objects \
348 .filter(Q(user__in=users) | Q(user__is_superuser=True)) \
349 .values_list('key','user__username')\
350 .order_by('user__username')
352 keys_list = list(keys)
353 return HttpResponse(json.dumps(keys_list), mimetype="application/json")