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

34
import re
35
import logging
36

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

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
44
from urllib import unquote
45

46
47
48
import astakosclient
from snf_django.lib import astakos

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
49
from synnefo.db.models import VirtualMachine, NetworkInterface, Network
50

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

56
57
logger = logging.getLogger(__name__)

58
59
60
HELPDESK_MEDIA_URL = getattr(settings, 'HELPDESK_MEDIA_URL',
                             settings.MEDIA_URL + 'helpdesk/')

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


Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def get_token_from_cookie(request, cookiename):
    """
    Extract token from the cookie name provided. Cookie should be in the same
    form as astakos service sets its cookie contents::

        <user_uniq>|<user_token>
    """
    try:
        cookie_content = unquote(request.COOKIES.get(cookiename, None))
        return cookie_content.split("|")[1]
    except AttributeError:
        pass

    return None

81

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
82
83
84
85
AUTH_COOKIE_NAME = getattr(settings, 'HELPDESK_AUTH_COOKIE_NAME',
                           getattr(settings, 'UI_AUTH_COOKIE_NAME',
                                   '_pithos2_a'))
PERMITTED_GROUPS = getattr(settings, 'HELPDESK_PERMITTED_GROUPS', ['helpdesk'])
86
87
88
89
90
91
92
93
94
95
96
97
SHOW_DELETED_VMS = getattr(settings, 'HELPDESK_SHOW_DELETED_VMS', False)


def token_check(func):
    """
    Mimic csrf security check using user auth token.
    """
    def wrapper(request, *args, **kwargs):
        if not hasattr(request, 'user'):
            raise PermissionDenied

        token = request.POST.get('token', None)
98
        if token and token == request.user.get('auth_token', None):
99
100
101
102
103
            return func(request, *args, **kwargs)

        raise PermissionDenied

    return wrapper
104

105

106
def helpdesk_user_required(func, permitted_groups=PERMITTED_GROUPS):
107
108
109
110
111
    """
    Django view wrapper that checks if identified request user has helpdesk
    permissions (exists in helpdesk group)
    """
    def wrapper(request, *args, **kwargs):
112
113
114
115
        HELPDESK_ENABLED = getattr(settings, 'HELPDESK_ENABLED', True)
        if not HELPDESK_ENABLED:
            raise Http404

116
        token = get_token_from_cookie(request, AUTH_COOKIE_NAME)
117
        astakos.get_user(request, settings.ASTAKOS_BASE_URL,
118
                         fallback_token=token, logger=logger)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
119
        if hasattr(request, 'user') and request.user:
120
            groups = request.user.get('groups', [])
121
122

            if not groups:
123
                logger.error("Failed to access helpdesk view. User: %r",
124
                             request.user_uniq)
125
126
                raise PermissionDenied

127
            has_perm = False
128
            for g in groups:
129
130
131
132
                if g in permitted_groups:
                    has_perm = True

            if not has_perm:
133
                logger.error("Failed to access helpdesk view %r. No valid "
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
134
135
                             "helpdesk group (%r) matches user groups (%r)",
                             request.user_uniq, permitted_groups, groups)
136
                raise PermissionDenied
137
        else:
138
            logger.error("Failed to access helpdesk view %r. No authenticated "
139
                         "user found.", request.user_uniq)
140
141
            raise PermissionDenied

142
143
        logging.info("User %s accessed helpdesk view (%s)", request.user_uniq,
                     request.path)
144
145
146
147
148
149
        return func(request, *args, **kwargs)

    return wrapper


@helpdesk_user_required
150
151
152
153
154
155
156
def index(request):
    """
    Helpdesk index view.
    """
    # if form submitted redirect to details
    account = request.GET.get('account', None)
    if account:
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
157
        return redirect('synnefo.helpdesk.views.account',
158
                        search_query=account)
159
160

    # show index template
161
162
163
    return direct_to_template(request, "helpdesk/index.html",
                              extra_context={'HELPDESK_MEDIA_URL':
                                             HELPDESK_MEDIA_URL})
164
165


166
@helpdesk_user_required
167
def account(request, search_query):
168
    """
169
    Account details view.
170
    """
171

172
    logging.info("Helpdesk search by %s: %s", request.user_uniq, search_query)
173
174
    show_deleted = bool(int(request.GET.get('deleted', SHOW_DELETED_VMS)))

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
175
    account_exists = True
176
177
    # flag to indicate successfull astakos calls
    account_resolved = False
178
179
    vms = []
    networks = []
180
181
182
183
    is_ip = IP_SEARCH_REGEX.match(search_query)
    is_uuid = UUID_SEARCH_REGEX.match(search_query)
    is_vm = VM_SEARCH_REGEX.match(search_query)
    account_name = search_query
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
184
    auth_token = request.user.get('auth_token')
185
186
187

    if is_ip:
        try:
188
189
            nic = NetworkInterface.objects.filter(ipv4=search_query).exclude(
                machine__deleted=True).get()
190
            search_query = nic.machine.userid
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
191
            is_uuid = True
192
193
        except NetworkInterface.DoesNotExist:
            account_exists = False
194
195
196
197
198
199
200
201
202
203
204
205
            account = None

    if is_vm:
        vmid = is_vm.groupdict().get('vmid')
        try:
            vm = VirtualMachine.objects.get(pk=int(vmid))
            search_query = vm.userid
            is_uuid = True
        except VirtualMachine.DoesNotExist:
            account_exists = False
            account = None
            search_query = vmid
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
206

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

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

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

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

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
231
232
    filter_extra = {}
    if not show_deleted:
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
233
        filter_extra['deleted'] = False
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
234
235
236
237
238
239
240
241
242
243
244
245
246

    # all user vms
    vms = VirtualMachine.objects.filter(userid=account,
                                        **filter_extra).order_by('deleted')
    # return all user private and public networks
    public_networks = Network.objects.filter(public=True,
                                             nics__machine__userid=account,
                                             **filter_extra
                                             ).order_by('state').distinct()
    private_networks = Network.objects.filter(userid=account,
                                              **filter_extra).order_by('state')
    networks = list(public_networks) + list(private_networks)

247
248
    if vms.count() == 0 and private_networks.count() == 0 and not \
            account_resolved:
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
249
        account_exists = False
250

251
    user_context = {
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
252
        'account_exists': account_exists,
253
        'is_ip': is_ip,
254
255
        'is_vm': is_vm,
        'is_uuid': is_uuid,
256
        'account': account,
257
        'search_query': search_query,
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
258
        'vms': vms,
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
259
260
        'show_deleted': show_deleted,
        'account_name': account_name,
261
        'token': request.user['auth_token'],
262
        'networks': networks,
263
264
        'HELPDESK_MEDIA_URL': HELPDESK_MEDIA_URL,
        'UI_MEDIA_URL': UI_MEDIA_URL
265
    }
266

267
    return direct_to_template(request, "helpdesk/account.html",
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
268
                              extra_context=user_context)
269

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
270

271
272
@helpdesk_user_required
@token_check
273
def vm_suspend(request, vm_id):
274
275
276
    vm = VirtualMachine.objects.get(pk=vm_id)
    vm.suspended = True
    vm.save()
277
    logging.info("VM %s suspended by %s", vm_id, request.user_uniq)
278
279
280
281
282
283
    account = vm.userid
    return HttpResponseRedirect(reverse('helpdesk-details', args=(account,)))


@helpdesk_user_required
@token_check
284
def vm_suspend_release(request, vm_id):
285
286
287
    vm = VirtualMachine.objects.get(pk=vm_id)
    vm.suspended = False
    vm.save()
288
    logging.info("VM %s unsuspended by %s", vm_id, request.user_uniq)
289
290
    account = vm.userid
    return HttpResponseRedirect(reverse('helpdesk-details', args=(account,)))
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312


@helpdesk_user_required
@token_check
def vm_shutdown(request, vm_id):
    logging.info("VM %s shutdown by %s", vm_id, request.user_uniq)
    vm = VirtualMachine.objects.get(pk=vm_id)
    servers.start_action(vm, 'STOP')
    servers_backend.shutdown_instance(vm)
    account = vm.userid
    return HttpResponseRedirect(reverse('helpdesk-details', args=(account,)))


@helpdesk_user_required
@token_check
def vm_start(request, vm_id):
    logging.info("VM %s start by %s", vm_id, request.user_uniq)
    vm = VirtualMachine.objects.get(pk=vm_id)
    servers.start_action(vm, 'START')
    servers_backend.startup_instance(vm)
    account = vm.userid
    return HttpResponseRedirect(reverse('helpdesk-details', args=(account,)))