Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (13.1 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 snf_django.lib.api import faults
44
from synnefo.api.util import (random_password, get_vm, get_nic_from_index,
45
                              get_network_free_address)
46
from synnefo.db.models import NetworkInterface
47
from synnefo.db.pools import EmptyPool
48
from synnefo.logic import backend
49
from synnefo.logic.utils import get_rsapi_state
50

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

    
54

    
55
server_actions = {}
56
network_actions = {}
57

    
58

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

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

    
69

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

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

    
80

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

    
93
    raise faults.NotImplemented('Changing password is not supported.')
94

    
95

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

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

    
115

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

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

    
128

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

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

    
141

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

    
155
    raise faults.NotImplemented('Rebuild not supported.')
156

    
157

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

    
172
    raise faults.NotImplemented('Resize not supported.')
173

    
174

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

    
189
    raise faults.NotImplemented('Resize not supported.')
190

    
191

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

    
206
    raise faults.NotImplemented('Resize not supported.')
207

    
208

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
286

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

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

    
306

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

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

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

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

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

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

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

    
342

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

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

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

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

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

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

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