Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (12.7 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
from synnefo.api.util import random_password, get_vm, get_nic_from_index
46
from synnefo.db.models import NetworkInterface, Network
47
from synnefo.db.pools import IPPool
48
from synnefo.logic import backend
49
from synnefo.logic.utils import get_rsapi_state
50

    
51

    
52
server_actions = {}
53
network_actions = {}
54

    
55

    
56
def server_action(name):
57
    '''Decorator for functions implementing server actions.
58
    `name` is the key in the dict passed by the client.
59
    '''
60

    
61
    def decorator(func):
62
        server_actions[name] = func
63
        return func
64
    return decorator
65

    
66

    
67
def network_action(name):
68
    '''Decorator for functions implementing network actions.
69
    `name` is the key in the dict passed by the client.
70
    '''
71

    
72
    def decorator(func):
73
        network_actions[name] = func
74
        return func
75
    return decorator
76

    
77

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

    
90
    raise ServiceUnavailable('Changing password is not supported.')
91

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

    
104
    reboot_type = args.get('type', '')
105
    if reboot_type not in ('SOFT', 'HARD'):
106
        raise BadRequest('Malformed Request.')
107
    backend.reboot_instance(vm, reboot_type.lower())
108
    return HttpResponse(status=202)
109

    
110
@server_action('start')
111
def start(request, vm, args):
112
    # Normal Response Code: 202
113
    # Error Response Codes: serviceUnavailable (503),
114
    #                       itemNotFound (404)
115

    
116
    if args:
117
        raise BadRequest('Malformed Request.')
118
    backend.startup_instance(vm)
119
    return HttpResponse(status=202)
120

    
121
@server_action('shutdown')
122
def shutdown(request, vm, args):
123
    # Normal Response Code: 202
124
    # Error Response Codes: serviceUnavailable (503),
125
    #                       itemNotFound (404)
126

    
127
    if args:
128
        raise BadRequest('Malformed Request.')
129
    backend.shutdown_instance(vm)
130
    return HttpResponse(status=202)
131

    
132
@server_action('rebuild')
133
def rebuild(request, vm, args):
134
    # Normal Response Code: 202
135
    # Error Response Codes: computeFault (400, 500),
136
    #                       serviceUnavailable (503),
137
    #                       unauthorized (401),
138
    #                       badRequest (400),
139
    #                       badMediaType(415),
140
    #                       itemNotFound (404),
141
    #                       buildInProgress (409),
142
    #                       serverCapacityUnavailable (503),
143
    #                       overLimit (413)
144

    
145
    raise ServiceUnavailable('Rebuild not supported.')
146

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

    
161
    raise ServiceUnavailable('Resize not supported.')
162

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

    
177
    raise ServiceUnavailable('Resize not supported.')
178

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

    
193
    raise ServiceUnavailable('Resize not supported.')
194

    
195

    
196
@server_action('console')
197
def get_console(request, vm, args):
198
    """Arrange for an OOB console of the specified type
199

200
    This method arranges for an OOB console of the specified type.
201
    Only consoles of type "vnc" are supported for now.
202

203
    It uses a running instance of vncauthproxy to setup proper
204
    VNC forwarding with a random password, then returns the necessary
205
    VNC connection info to the caller.
206

207
    """
208
    # Normal Response Code: 200
209
    # Error Response Codes: computeFault (400, 500),
210
    #                       serviceUnavailable (503),
211
    #                       unauthorized (401),
212
    #                       badRequest (400),
213
    #                       badMediaType(415),
214
    #                       itemNotFound (404),
215
    #                       buildInProgress (409),
216
    #                       overLimit (413)
217

    
218
    console_type = args.get('type', '')
219
    if console_type != 'vnc':
220
        raise BadRequest('Type can only be "vnc".')
221

    
222
    # Use RAPI to get VNC console information for this instance
223
    if get_rsapi_state(vm) != 'ACTIVE':
224
        raise BadRequest('Server not in ACTIVE state.')
225

    
226
    if settings.TEST:
227
        console_data = {'kind': 'vnc', 'host': 'ganeti_node', 'port': 1000}
228
    else:
229
        console_data = backend.get_instance_console(vm)
230

    
231
    if console_data['kind'] != 'vnc':
232
        message = 'got console of kind %s, not "vnc"' % console_data['kind']
233
        raise ServiceUnavailable(message)
234

    
235
    # Let vncauthproxy decide on the source port.
236
    # The alternative: static allocation, e.g.
237
    # sport = console_data['port'] - 1000
238
    sport = 0
239
    daddr = console_data['host']
240
    dport = console_data['port']
241
    password = random_password()
242

    
243
    if settings.TEST:
244
        fwd = {'source_port': 1234, 'status': 'OK'}
245
    else:
246
        fwd = request_vnc_forwarding(sport, daddr, dport, password)
247

    
248
    if fwd['status'] != "OK":
249
        raise ServiceUnavailable('vncauthproxy returned error status')
250

    
251
    # Verify that the VNC server settings haven't changed
252
    if not settings.TEST:
253
        if console_data != backend.get_instance_console(vm):
254
            raise ServiceUnavailable('VNC Server settings changed.')
255

    
256
    console = {
257
        'type': 'vnc',
258
        'host': getfqdn(),
259
        'port': fwd['source_port'],
260
        'password': password}
261

    
262
    if request.serialization == 'xml':
263
        mimetype = 'application/xml'
264
        data = render_to_string('console.xml', {'console': console})
265
    else:
266
        mimetype = 'application/json'
267
        data = json.dumps({'console': console})
268

    
269
    return HttpResponse(data, mimetype=mimetype, status=200)
270

    
271

    
272
@server_action('firewallProfile')
273
@transaction.commit_on_success
274
def set_firewall_profile(request, vm, args):
275
    # Normal Response Code: 200
276
    # Error Response Codes: computeFault (400, 500),
277
    #                       serviceUnavailable (503),
278
    #                       unauthorized (401),
279
    #                       badRequest (400),
280
    #                       badMediaType(415),
281
    #                       itemNotFound (404),
282
    #                       buildInProgress (409),
283
    #                       overLimit (413)
284

    
285
    profile = args.get('profile', '')
286
    if profile not in [x[0] for x in NetworkInterface.FIREWALL_PROFILES]:
287
        raise BadRequest("Unsupported firewall profile")
288
    backend.set_firewall_profile(vm, profile)
289
    return HttpResponse(status=202)
290

    
291

    
292
@network_action('add')
293
@transaction.commit_on_success
294
def add(request, net, args):
295
    # Normal Response Code: 202
296
    # Error Response Codes: computeFault (400, 500),
297
    #                       serviceUnavailable (503),
298
    #                       unauthorized (401),
299
    #                       badRequest (400),
300
    #                       badMediaType(415),
301
    #                       itemNotFound (404),
302
    #                       overLimit (413)
303

    
304
    server_id = args.get('serverRef', None)
305
    if not server_id:
306
        raise BadRequest('Malformed Request.')
307
    vm = get_vm(server_id, request.user_uniq)
308

    
309
    # Get the Network object in exclusive mode in order to
310
    # guarantee consistency of the pool
311
    net = Network.objects.select_for_update().get(id=net.id)
312

    
313
    if net.state != 'ACTIVE':
314
        raise ServiceUnavailable('Network not active yet')
315

    
316
    # Get a free IP from the address pool.
317
    pool = IPPool(net)
318
    try:
319
        address = pool.get_free_address()
320
    except IPPool.IPPoolExhausted:
321
        raise ServiceUnavailable('Network is full')
322
    pool.save()
323

    
324
    backend.connect_to_network(vm, net, address)
325
    return HttpResponse(status=202)
326

    
327

    
328
@network_action('remove')
329
@transaction.commit_on_success
330
def remove(request, net, args):
331
    # Normal Response Code: 202
332
    # Error Response Codes: computeFault (400, 500),
333
    #                       serviceUnavailable (503),
334
    #                       unauthorized (401),
335
    #                       badRequest (400),
336
    #                       badMediaType(415),
337
    #                       itemNotFound (404),
338
    #                       overLimit (413)
339

    
340
    try:  # attachment string: nic-<vm-id>-<nic-index>
341
        server_id = args.get('attachment', None).split('-')[1]
342
        nic_index = args.get('attachment', None).split('-')[2]
343
    except AttributeError:
344
        raise BadRequest("Malformed Request")
345
    except IndexError:
346
        raise BadRequest('Malformed Network Interface Id')
347

    
348
    if not server_id or not nic_index:
349
        raise BadRequest('Malformed Request.')
350
    vm = get_vm(server_id, request.user_uniq)
351
    nic = get_nic_from_index(vm, nic_index)
352

    
353
    if nic.dirty:
354
        raise BuildInProgress('Machine is busy.')
355
    else:
356
        vm.nics.all().update(dirty=True)
357

    
358
    backend.disconnect_from_network(vm, nic)
359
    return HttpResponse(status=202)