Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / admin-interface / views.py @ c204fcff

History | View | Annotate | Download (12.4 kB)

1
# Copyright 2012, 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import re
35
import logging
36

    
37
from django.shortcuts import redirect
38
from django.views.generic.simple import direct_to_template
39
from django.conf import settings
40
from django.core.exceptions import PermissionDenied
41
from django.http import Http404, HttpResponseRedirect
42
from django.core.urlresolvers import reverse
43

    
44
from urllib import unquote
45

    
46
import astakosclient
47
from snf_django.lib import astakos
48

    
49
from synnefo.db.models import VirtualMachine, Network, IPAddressLog
50

    
51
# server actions specific imports
52
from synnefo.logic import servers as servers_backend
53
from synnefo.ui.views import UI_MEDIA_URL
54

    
55
logger = logging.getLogger(__name__)
56

    
57
ADMIN-INTERFACE_MEDIA_URL = getattr(settings, 'ADMIN_INTERFACE_MEDIA_URL',
58
                             settings.MEDIA_URL + 'admin-interface/')
59

    
60
IP_SEARCH_REGEX = re.compile('([0-9]+)(?:\.[0-9]+){3}')
61
UUID_SEARCH_REGEX = re.compile('([0-9a-z]{8}-([0-9a-z]{4}-){3}[0-9a-z]{12})')
62
VM_SEARCH_REGEX = re.compile('vm(-){0,}(?P<vmid>[0-9]+)')
63

    
64

    
65
def get_token_from_cookie(request, cookiename):
66
    """
67
    Extract token from the cookie name provided. Cookie should be in the same
68
    form as astakos service sets its cookie contents::
69

70
        <user_uniq>|<user_token>
71
    """
72
    try:
73
        cookie_content = unquote(request.COOKIES.get(cookiename, None))
74
        return cookie_content.split("|")[1]
75
    except AttributeError:
76
        pass
77

    
78
    return None
79

    
80

    
81
AUTH_COOKIE_NAME = getattr(settings, 'ADMIN-INTERFACE_AUTH_COOKIE_NAME',
82
                           getattr(settings, 'UI_AUTH_COOKIE_NAME',
83
                                   '_pithos2_a'))
84
PERMITTED_GROUPS = getattr(settings, 'ADMIN-INTERFACE_PERMITTED_GROUPS', ['admin-interface'])
85
SHOW_DELETED_VMS = getattr(settings, 'ADMIN-INTERFACE_SHOW_DELETED_VMS', False)
86

    
87

    
88
def token_check(func):
89
    """
90
    Mimic csrf security check using user auth token.
91
    """
92
    def wrapper(request, *args, **kwargs):
93
        if not hasattr(request, 'user'):
94
            raise PermissionDenied
95

    
96
        token = request.POST.get('token', None)
97
        if token:
98
            try:
99
                req_token = request.user["access"]["token"]["id"]
100
                if token == req_token:
101
                    return func(request, *args, **kwargs)
102
            except KeyError:
103
                pass
104

    
105
        raise PermissionDenied
106

    
107
    return wrapper
108

    
109

    
110
def admin-interface_user_required(func, permitted_groups=PERMITTED_GROUPS):
111
    """
112
    Django view wrapper that checks if identified request user has admin-interface
113
    permissions (exists in admin-interface group)
114
    """
115
    def wrapper(request, *args, **kwargs):
116
        ADMIN-INTERFACE_ENABLED = getattr(settings, 'ADMIN_INTERFACE_ENABLED', True)
117
        if not ADMIN-INTERFACE_ENABLED:
118
            raise Http404
119

    
120
        token = get_token_from_cookie(request, AUTH_COOKIE_NAME)
121
        astakos.get_user(request, settings.ASTAKOS_AUTH_URL,
122
                         fallback_token=token, logger=logger)
123
        if hasattr(request, 'user') and request.user:
124
            groups = request.user['access']['user']['roles']
125
            groups = [g["name"] for g in groups]
126

    
127
            if not groups:
128
                logger.info("Failed to access admin-interface view. User: %r",
129
                            request.user_uniq)
130
                raise PermissionDenied
131

    
132
            has_perm = False
133
            for g in groups:
134
                if g in permitted_groups:
135
                    has_perm = True
136

    
137
            if not has_perm:
138
                logger.info("Failed to access admin-interface view %r. No valid "
139
                            "admin-interface group (%r) matches user groups (%r)",
140
                            request.user_uniq, permitted_groups, groups)
141
                raise PermissionDenied
142
        else:
143
            logger.info("Failed to access admin-interface view %r. No authenticated "
144
                        "user found.", request.user_uniq)
145
            raise PermissionDenied
146

    
147
        logging.info("User %s accessed admin-interface view (%s)", request.user_uniq,
148
                     request.path)
149
        return func(request, *args, **kwargs)
150

    
151
    return wrapper
152

    
153

    
154
@admin-interface_user_required
155
def index(request):
156
    """
157
    Admin-Interface index view.
158
    """
159
    # if form submitted redirect to details
160
    account = request.GET.get('account', None)
161
    if account:
162
        return redirect('admin-interface-details',
163
                        search_query=account)
164

    
165
    # show index template
166
    return direct_to_template(request, "admin-interface/index.html",
167
                              extra_context={'ADMIN-INTERFACE_MEDIA_URL':
168
                                             ADMIN-INTERFACE_MEDIA_URL})
169

    
170

    
171
@admin-interface_user_required
172
def account(request, search_query):
173
    """
174
    Account details view.
175
    """
176

    
177
    logging.info("Admin-Interface search by %s: %s", request.user_uniq, search_query)
178
    show_deleted = bool(int(request.GET.get('deleted', SHOW_DELETED_VMS)))
179
    error = request.GET.get('error', None)
180

    
181
    account_exists = True
182
    # flag to indicate successfull astakos calls
183
    account_resolved = False
184
    vms = []
185
    networks = []
186
    is_ip = IP_SEARCH_REGEX.match(search_query)
187
    is_uuid = UUID_SEARCH_REGEX.match(search_query)
188
    is_vm = VM_SEARCH_REGEX.match(search_query)
189
    account_name = search_query
190
    auth_token = request.user['access']['token']['id']
191

    
192
    if is_ip:
193
        # Search the IPAddressLog for the full use history of this IP
194
        return search_by_ip(request, search_query)
195

    
196
    if is_vm:
197
        vmid = is_vm.groupdict().get('vmid')
198
        try:
199
            vm = VirtualMachine.objects.get(pk=int(vmid))
200
            search_query = vm.userid
201
            is_uuid = True
202
        except VirtualMachine.DoesNotExist:
203
            account_exists = False
204
            account = None
205
            search_query = vmid
206

    
207
    astakos_client = astakosclient.AstakosClient(
208
        auth_token, settings.ASTAKOS_AUTH_URL,
209
        retry=2, use_pool=True, logger=logger)
210

    
211
    account = None
212
    if is_uuid:
213
        account = search_query
214
        try:
215
            account_name = astakos_client.get_username(account)
216
        except:
217
            logger.info("Failed to resolve '%s' into account" % account)
218

    
219
    if account_exists and not is_uuid:
220
        account_name = search_query
221
        try:
222
            account = astakos_client.get_uuid(account_name)
223
        except:
224
            logger.info("Failed to resolve '%s' into account" % account_name)
225

    
226
    if not account:
227
        account_exists = False
228
    else:
229
        account_resolved = True
230

    
231
    filter_extra = {}
232
    if not show_deleted:
233
        filter_extra['deleted'] = False
234

    
235
    # all user vms
236
    vms = VirtualMachine.objects.filter(userid=account,
237
                                        **filter_extra).order_by('deleted')
238
    # return all user private and public networks
239
    public_networks = Network.objects.filter(public=True,
240
                                             nics__machine__userid=account,
241
                                             **filter_extra
242
                                             ).order_by('state').distinct()
243
    private_networks = Network.objects.filter(userid=account,
244
                                              **filter_extra).order_by('state')
245
    networks = list(public_networks) + list(private_networks)
246

    
247
    if vms.count() == 0 and private_networks.count() == 0 and not \
248
            account_resolved:
249
        account_exists = False
250

    
251
    user_context = {
252
        'account_exists': account_exists,
253
        'error': error,
254
        'is_ip': is_ip,
255
        'is_vm': is_vm,
256
        'is_uuid': is_uuid,
257
        'account': account,
258
        'search_query': search_query,
259
        'vms': vms,
260
        'show_deleted': show_deleted,
261
        'account_name': account_name,
262
        'token': request.user['access']['token']['id'],
263
        'networks': networks,
264
        'ADMIN-INTERFACE_MEDIA_URL': ADMIN_INTERFACE_MEDIA_URL,
265
        'UI_MEDIA_URL': UI_MEDIA_URL
266
    }
267

    
268
    return direct_to_template(request, "admin-interface/account.html",
269
                              extra_context=user_context)
270

    
271

    
272
def search_by_ip(request, search_query):
273
    """Search IP history for all uses of an IP address."""
274
    auth_token = request.user['access']['token']['id']
275
    astakos_client = astakosclient.AstakosClient(auth_token,
276
                                                 settings.ASTAKOS_AUTH_URL,
277
                                                 retry=2, use_pool=True,
278
                                                 logger=logger)
279

    
280
    ips = IPAddressLog.objects.filter(address=search_query)\
281
                              .order_by("allocated_at")
282

    
283
    for ip in ips:
284
        # Annotate IPs with the VM, Network and account attributes
285
        ip.vm = VirtualMachine.objects.get(id=ip.server_id)
286
        ip.network = Network.objects.get(id=ip.network_id)
287
        userid = ip.vm.userid
288

    
289
        try:
290
            ip.account = astakos_client.get_username(userid)
291
        except:
292
            ip.account = userid
293
            logger.info("Failed to resolve '%s' into account" % userid)
294

    
295
    user_context = {
296
        'ip_exists': bool(ips),
297
        'ips': ips,
298
        'search_query': search_query,
299
        'token': auth_token,
300
        'ADMIN-INTERFACE_MEDIA_URL': ADMIN_INTERFACE_MEDIA_URL,
301
        'UI_MEDIA_URL': UI_MEDIA_URL
302
    }
303

    
304
    return direct_to_template(request, "admin-interface/ip.html",
305
                              extra_context=user_context)
306

    
307

    
308
@admin-interface_user_required
309
@token_check
310
def vm_suspend(request, vm_id):
311
    vm = VirtualMachine.objects.get(pk=vm_id)
312
    vm.suspended = True
313
    vm.save()
314
    logging.info("VM %s suspended by %s", vm_id, request.user_uniq)
315
    account = vm.userid
316
    return HttpResponseRedirect(reverse('admin-interface-details', args=(account,)))
317

    
318

    
319
@admin-interface_user_required
320
@token_check
321
def vm_suspend_release(request, vm_id):
322
    vm = VirtualMachine.objects.get(pk=vm_id)
323
    vm.suspended = False
324
    vm.save()
325
    logging.info("VM %s unsuspended by %s", vm_id, request.user_uniq)
326
    account = vm.userid
327
    return HttpResponseRedirect(reverse('admin-interface-details', args=(account,)))
328

    
329

    
330
@admin-interface_user_required
331
@token_check
332
def vm_shutdown(request, vm_id):
333
    logging.info("VM %s shutdown by %s", vm_id, request.user_uniq)
334
    vm = VirtualMachine.objects.get(pk=vm_id)
335
    account = vm.userid
336
    error = None
337
    try:
338
        jobId = servers_backend.stop(vm)
339
    except Exception, e:
340
        error = e.message
341

    
342
    redirect = reverse('admin-interface-details', args=(account,))
343
    if error:
344
        redirect = "%s?error=%s" % (redirect, error)
345
    return HttpResponseRedirect(redirect)
346

    
347

    
348
@admin-interface_user_required
349
@token_check
350
def vm_start(request, vm_id):
351
    logging.info("VM %s start by %s", vm_id, request.user_uniq)
352
    vm = VirtualMachine.objects.get(pk=vm_id)
353
    account = vm.userid
354
    error = None
355
    try:
356
        jobId = servers_backend.start(vm)
357
    except Exception, e:
358
        error = e.message
359

    
360
    redirect = reverse('admin-interface-details', args=(account,))
361
    if error:
362
        redirect = "%s?error=%s" % (redirect, error)
363
    return HttpResponseRedirect(redirect)