Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (13.1 kB)

1
# Copyright 2011 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
                                ItemNotFound, BuildInProgress,
45
                                OverLimit)
46
from synnefo.api.util import (random_password, get_vm, get_nic_from_index,
47
                              get_network_free_address)
48
from synnefo.db.models import NetworkInterface, Network
49
from synnefo.db.pools import EmptyPool
50
from synnefo.logic import backend
51
from synnefo.logic.utils import get_rsapi_state
52

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

    
56

    
57
server_actions = {}
58
network_actions = {}
59

    
60

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

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

    
71

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

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

    
82

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

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

    
97

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

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

    
117

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

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

    
130

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

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

    
143

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

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

    
159

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

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

    
176

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

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

    
193

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

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

    
210

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
288

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

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

    
308

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

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

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

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

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

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

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

    
344

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

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

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

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

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

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

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