Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (12.6 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
                              get_network_free_address)
47
from synnefo.db.models import NetworkInterface, Network
48
from synnefo.db.pools import EmptyPool
49
from synnefo.logic import backend
50
from synnefo.logic.utils import get_rsapi_state
51

    
52

    
53
server_actions = {}
54
network_actions = {}
55

    
56

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

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

    
67

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

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

    
78

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
196

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
272

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

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

    
292

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

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

    
310
    net = Network.objects.get(id=net.id)
311

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

    
315
    # Get a free IP from the address pool.
316
    try:
317
        address = get_network_free_address(net)
318
    except EmptyPool:
319
        raise ServiceUnavailable('Network is full')
320

    
321
    backend.connect_to_network(vm, net, address)
322
    return HttpResponse(status=202)
323

    
324

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

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

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

    
350
    if nic.dirty:
351
        raise BuildInProgress('Machine is busy.')
352
    else:
353
        vm.nics.all().update(dirty=True)
354

    
355
    backend.disconnect_from_network(vm, nic)
356
    return HttpResponse(status=202)