Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / api / actions.py @ be1dca04

History | View | Annotate | Download (13 kB)

1
# Copyright 2011, 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
from socket import getfqdn
35
from vncauthproxy.client import request_forwarding as request_vnc_forwarding
36

    
37
from django.db import transaction
38
from django.conf import settings
39
from django.http import HttpResponse
40
from django.template.loader import render_to_string
41
from django.utils import simplejson as json
42

    
43
from synnefo.api.faults import (BadRequest, ServiceUnavailable,
44
                                BuildInProgress, OverLimit)
45
from synnefo.api.util import (random_password, get_vm, get_nic_from_index,
46
                              get_network_free_address)
47
from synnefo.db.models import NetworkInterface
48
from synnefo.db.pools import EmptyPool
49
from synnefo.logic import backend
50
from synnefo.logic.utils import get_rsapi_state
51

    
52
from logging import getLogger
53
log = getLogger(__name__)
54

    
55

    
56
server_actions = {}
57
network_actions = {}
58

    
59

    
60
def server_action(name):
61
    '''Decorator for functions implementing server actions.
62
    `name` is the key in the dict passed by the client.
63
    '''
64

    
65
    def decorator(func):
66
        server_actions[name] = func
67
        return func
68
    return decorator
69

    
70

    
71
def network_action(name):
72
    '''Decorator for functions implementing network actions.
73
    `name` is the key in the dict passed by the client.
74
    '''
75

    
76
    def decorator(func):
77
        network_actions[name] = func
78
        return func
79
    return decorator
80

    
81

    
82
@server_action('changePassword')
83
def change_password(request, vm, args):
84
    # Normal Response Code: 202
85
    # Error Response Codes: computeFault (400, 500),
86
    #                       serviceUnavailable (503),
87
    #                       unauthorized (401),
88
    #                       badRequest (400),
89
    #                       badMediaType(415),
90
    #                       itemNotFound (404),
91
    #                       buildInProgress (409),
92
    #                       overLimit (413)
93

    
94
    raise ServiceUnavailable('Changing password is not supported.')
95

    
96

    
97
@server_action('reboot')
98
def reboot(request, vm, args):
99
    # Normal Response Code: 202
100
    # Error Response Codes: computeFault (400, 500),
101
    #                       serviceUnavailable (503),
102
    #                       unauthorized (401),
103
    #                       badRequest (400),
104
    #                       badMediaType(415),
105
    #                       itemNotFound (404),
106
    #                       buildInProgress (409),
107
    #                       overLimit (413)
108

    
109
    log.info("Reboot VM %s", vm)
110
    reboot_type = args.get('type', '')
111
    if reboot_type not in ('SOFT', 'HARD'):
112
        raise BadRequest('Malformed Request.')
113
    backend.reboot_instance(vm, reboot_type.lower())
114
    return HttpResponse(status=202)
115

    
116

    
117
@server_action('start')
118
def start(request, vm, args):
119
    # Normal Response Code: 202
120
    # Error Response Codes: serviceUnavailable (503),
121
    #                       itemNotFound (404)
122

    
123
    log.info("Start VM %s", vm)
124
    if args:
125
        raise BadRequest('Malformed Request.')
126
    backend.startup_instance(vm)
127
    return HttpResponse(status=202)
128

    
129

    
130
@server_action('shutdown')
131
def shutdown(request, vm, args):
132
    # Normal Response Code: 202
133
    # Error Response Codes: serviceUnavailable (503),
134
    #                       itemNotFound (404)
135

    
136
    log.info("Shutdown VM %s", vm)
137
    if args:
138
        raise BadRequest('Malformed Request.')
139
    backend.shutdown_instance(vm)
140
    return HttpResponse(status=202)
141

    
142

    
143
@server_action('rebuild')
144
def rebuild(request, vm, args):
145
    # Normal Response Code: 202
146
    # Error Response Codes: computeFault (400, 500),
147
    #                       serviceUnavailable (503),
148
    #                       unauthorized (401),
149
    #                       badRequest (400),
150
    #                       badMediaType(415),
151
    #                       itemNotFound (404),
152
    #                       buildInProgress (409),
153
    #                       serverCapacityUnavailable (503),
154
    #                       overLimit (413)
155

    
156
    raise ServiceUnavailable('Rebuild not supported.')
157

    
158

    
159
@server_action('resize')
160
def resize(request, vm, args):
161
    # Normal Response Code: 202
162
    # Error Response Codes: computeFault (400, 500),
163
    #                       serviceUnavailable (503),
164
    #                       unauthorized (401),
165
    #                       badRequest (400),
166
    #                       badMediaType(415),
167
    #                       itemNotFound (404),
168
    #                       buildInProgress (409),
169
    #                       serverCapacityUnavailable (503),
170
    #                       overLimit (413),
171
    #                       resizeNotAllowed (403)
172

    
173
    raise ServiceUnavailable('Resize not supported.')
174

    
175

    
176
@server_action('confirmResize')
177
def confirm_resize(request, vm, args):
178
    # Normal Response Code: 204
179
    # Error Response Codes: computeFault (400, 500),
180
    #                       serviceUnavailable (503),
181
    #                       unauthorized (401),
182
    #                       badRequest (400),
183
    #                       badMediaType(415),
184
    #                       itemNotFound (404),
185
    #                       buildInProgress (409),
186
    #                       serverCapacityUnavailable (503),
187
    #                       overLimit (413),
188
    #                       resizeNotAllowed (403)
189

    
190
    raise ServiceUnavailable('Resize not supported.')
191

    
192

    
193
@server_action('revertResize')
194
def revert_resize(request, vm, args):
195
    # Normal Response Code: 202
196
    # Error Response Codes: computeFault (400, 500),
197
    #                       serviceUnavailable (503),
198
    #                       unauthorized (401),
199
    #                       badRequest (400),
200
    #                       badMediaType(415),
201
    #                       itemNotFound (404),
202
    #                       buildInProgress (409),
203
    #                       serverCapacityUnavailable (503),
204
    #                       overLimit (413),
205
    #                       resizeNotAllowed (403)
206

    
207
    raise ServiceUnavailable('Resize not supported.')
208

    
209

    
210
@server_action('console')
211
def get_console(request, vm, args):
212
    """Arrange for an OOB console of the specified type
213

214
    This method arranges for an OOB console of the specified type.
215
    Only consoles of type "vnc" are supported for now.
216

217
    It uses a running instance of vncauthproxy to setup proper
218
    VNC forwarding with a random password, then returns the necessary
219
    VNC connection info to the caller.
220

221
    """
222
    # Normal Response Code: 200
223
    # Error Response Codes: computeFault (400, 500),
224
    #                       serviceUnavailable (503),
225
    #                       unauthorized (401),
226
    #                       badRequest (400),
227
    #                       badMediaType(415),
228
    #                       itemNotFound (404),
229
    #                       buildInProgress (409),
230
    #                       overLimit (413)
231

    
232
    log.info("Get console  VM %s", vm)
233

    
234
    console_type = args.get('type', '')
235
    if console_type != 'vnc':
236
        raise BadRequest('Type can only be "vnc".')
237

    
238
    # Use RAPI to get VNC console information for this instance
239
    if get_rsapi_state(vm) != 'ACTIVE':
240
        raise BadRequest('Server not in ACTIVE state.')
241

    
242
    if settings.TEST:
243
        console_data = {'kind': 'vnc', 'host': 'ganeti_node', 'port': 1000}
244
    else:
245
        console_data = backend.get_instance_console(vm)
246

    
247
    if console_data['kind'] != 'vnc':
248
        message = 'got console of kind %s, not "vnc"' % console_data['kind']
249
        raise ServiceUnavailable(message)
250

    
251
    # Let vncauthproxy decide on the source port.
252
    # The alternative: static allocation, e.g.
253
    # sport = console_data['port'] - 1000
254
    sport = 0
255
    daddr = console_data['host']
256
    dport = console_data['port']
257
    password = random_password()
258

    
259
    if settings.TEST:
260
        fwd = {'source_port': 1234, 'status': 'OK'}
261
    else:
262
        fwd = request_vnc_forwarding(sport, daddr, dport, password)
263

    
264
    if fwd['status'] != "OK":
265
        raise ServiceUnavailable('vncauthproxy returned error status')
266

    
267
    # Verify that the VNC server settings haven't changed
268
    if not settings.TEST:
269
        if console_data != backend.get_instance_console(vm):
270
            raise ServiceUnavailable('VNC Server settings changed.')
271

    
272
    console = {
273
        'type': 'vnc',
274
        'host': getfqdn(),
275
        'port': fwd['source_port'],
276
        'password': password}
277

    
278
    if request.serialization == 'xml':
279
        mimetype = 'application/xml'
280
        data = render_to_string('console.xml', {'console': console})
281
    else:
282
        mimetype = 'application/json'
283
        data = json.dumps({'console': console})
284

    
285
    return HttpResponse(data, mimetype=mimetype, status=200)
286

    
287

    
288
@server_action('firewallProfile')
289
def set_firewall_profile(request, vm, args):
290
    # Normal Response Code: 200
291
    # Error Response Codes: computeFault (400, 500),
292
    #                       serviceUnavailable (503),
293
    #                       unauthorized (401),
294
    #                       badRequest (400),
295
    #                       badMediaType(415),
296
    #                       itemNotFound (404),
297
    #                       buildInProgress (409),
298
    #                       overLimit (413)
299

    
300
    profile = args.get('profile', '')
301
    log.info("Set VM %s firewall %s", vm, profile)
302
    if profile not in [x[0] for x in NetworkInterface.FIREWALL_PROFILES]:
303
        raise BadRequest("Unsupported firewall profile")
304
    backend.set_firewall_profile(vm, profile)
305
    return HttpResponse(status=202)
306

    
307

    
308
@network_action('add')
309
@transaction.commit_on_success
310
def add(request, net, args):
311
    # Normal Response Code: 202
312
    # Error Response Codes: computeFault (400, 500),
313
    #                       serviceUnavailable (503),
314
    #                       unauthorized (401),
315
    #                       badRequest (400),
316
    #                       buildInProgress (409),
317
    #                       badMediaType(415),
318
    #                       itemNotFound (404),
319
    #                       overLimit (413)
320

    
321
    if net.state != 'ACTIVE':
322
        raise BuildInProgress('Network not active yet')
323

    
324
    server_id = args.get('serverRef', None)
325
    if not server_id:
326
        raise BadRequest('Malformed Request.')
327

    
328
    vm = get_vm(server_id, request.user_uniq, non_suspended=True)
329

    
330
    address = None
331
    if net.dhcp:
332
        # Get a free IP from the address pool.
333
        try:
334
            address = get_network_free_address(net)
335
        except EmptyPool:
336
            raise OverLimit('Network is full')
337

    
338
    log.info("Connecting VM %s to Network %s(%s)", vm, net, address)
339

    
340
    backend.connect_to_network(vm, net, address)
341
    return HttpResponse(status=202)
342

    
343

    
344
@network_action('remove')
345
@transaction.commit_on_success
346
def remove(request, net, args):
347
    # Normal Response Code: 202
348
    # Error Response Codes: computeFault (400, 500),
349
    #                       serviceUnavailable (503),
350
    #                       unauthorized (401),
351
    #                       badRequest (400),
352
    #                       badMediaType(415),
353
    #                       itemNotFound (404),
354
    #                       overLimit (413)
355

    
356
    try:  # attachment string: nic-<vm-id>-<nic-index>
357
        server_id = args.get('attachment', None).split('-')[1]
358
        nic_index = args.get('attachment', None).split('-')[2]
359
    except AttributeError:
360
        raise BadRequest("Malformed Request")
361
    except IndexError:
362
        raise BadRequest('Malformed Network Interface Id')
363

    
364
    if not server_id or not nic_index:
365
        raise BadRequest('Malformed Request.')
366

    
367
    vm = get_vm(server_id, request.user_uniq, non_suspended=True)
368
    nic = get_nic_from_index(vm, nic_index)
369

    
370
    log.info("Removing NIC %s from VM %s", str(nic.index), vm)
371

    
372
    if nic.dirty:
373
        raise BuildInProgress('Machine is busy.')
374
    else:
375
        vm.nics.all().update(dirty=True)
376

    
377
    backend.disconnect_from_network(vm, nic)
378
    return HttpResponse(status=202)