Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (14.6 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 import util
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 faults.NotImplemented('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 faults.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 faults.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 faults.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 faults.NotImplemented('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
    log.debug("resize %s: %s", vm, args)
173
    flavorRef = args.get("flavorRef", None)
174
    if flavorRef is None:
175
        raise faults.BadRequest("Missing 'flavorRef' attribute")
176

    
177
    new_flavor = util.get_flavor(flavor_id=flavorRef, include_deleted=False)
178
    old_flavor = vm.flavor
179
    # User requested the same flavor
180
    if old_flavor.id == new_flavor.id:
181
        return HttpResponse(status=200)
182
    # Check that resize can be performed
183
    if old_flavor.disk != new_flavor.disk:
184
        raise faults.BadRequest("Can not resize instance disk")
185
    if old_flavor.disk_template != new_flavor.disk_template:
186
        raise faults.BadRequest("Can not change instance disk template")
187

    
188
    if not vm_is_stopped(vm):
189
        raise faults.BadRequest("Can not resize running instance")
190

    
191
    vcpus, memory = new_flavor.cpu, new_flavor.ram
192
    jobId = backend.resize_instance(vm, vcpus=vcpus, memory=memory)
193

    
194
    log.info("User '%s' resized VM from flavor '%s' to '%s', job %s",
195
              request.user_uniq, old_flavor, new_flavor, jobId)
196

    
197
    # Save operstate now, since you don't want any other action when an
198
    # an instance is resizing
199
    vm.operstate = "RESIZE"
200
    util.start_action(vm, "RESIZE", jobId)
201

    
202
    vm.save()
203
    return HttpResponse(status=202)
204

    
205

    
206
def vm_is_stopped(vm):
207
    """Check if a VirtualMachine is currently stopped.
208

209
    A server is stopped if it's operstate is 'STOPPED'. Also, you must check
210
    that no other job is currently running, because this job may start the
211
    instance
212
    """
213
    return vm.operstate == "STOPPED" and vm.backendjobstatus == "success"
214

    
215

    
216
@server_action('confirmResize')
217
def confirm_resize(request, vm, args):
218
    # Normal Response Code: 204
219
    # Error Response Codes: computeFault (400, 500),
220
    #                       serviceUnavailable (503),
221
    #                       unauthorized (401),
222
    #                       badRequest (400),
223
    #                       badMediaType(415),
224
    #                       itemNotFound (404),
225
    #                       buildInProgress (409),
226
    #                       serverCapacityUnavailable (503),
227
    #                       overLimit (413),
228
    #                       resizeNotAllowed (403)
229

    
230
    raise faults.NotImplemented('Resize not supported.')
231

    
232

    
233
@server_action('revertResize')
234
def revert_resize(request, vm, args):
235
    # Normal Response Code: 202
236
    # Error Response Codes: computeFault (400, 500),
237
    #                       serviceUnavailable (503),
238
    #                       unauthorized (401),
239
    #                       badRequest (400),
240
    #                       badMediaType(415),
241
    #                       itemNotFound (404),
242
    #                       buildInProgress (409),
243
    #                       serverCapacityUnavailable (503),
244
    #                       overLimit (413),
245
    #                       resizeNotAllowed (403)
246

    
247
    raise faults.NotImplemented('Resize not supported.')
248

    
249

    
250
@server_action('console')
251
def get_console(request, vm, args):
252
    """Arrange for an OOB console of the specified type
253

254
    This method arranges for an OOB console of the specified type.
255
    Only consoles of type "vnc" are supported for now.
256

257
    It uses a running instance of vncauthproxy to setup proper
258
    VNC forwarding with a random password, then returns the necessary
259
    VNC connection info to the caller.
260

261
    """
262
    # Normal Response Code: 200
263
    # Error Response Codes: computeFault (400, 500),
264
    #                       serviceUnavailable (503),
265
    #                       unauthorized (401),
266
    #                       badRequest (400),
267
    #                       badMediaType(415),
268
    #                       itemNotFound (404),
269
    #                       buildInProgress (409),
270
    #                       overLimit (413)
271

    
272
    log.info("Get console  VM %s", vm)
273

    
274
    console_type = args.get('type', '')
275
    if console_type != 'vnc':
276
        raise faults.BadRequest('Type can only be "vnc".')
277

    
278
    # Use RAPI to get VNC console information for this instance
279
    if get_rsapi_state(vm) != 'ACTIVE':
280
        raise faults.BadRequest('Server not in ACTIVE state.')
281

    
282
    if settings.TEST:
283
        console_data = {'kind': 'vnc', 'host': 'ganeti_node', 'port': 1000}
284
    else:
285
        console_data = backend.get_instance_console(vm)
286

    
287
    if console_data['kind'] != 'vnc':
288
        message = 'got console of kind %s, not "vnc"' % console_data['kind']
289
        raise faults.ServiceUnavailable(message)
290

    
291
    # Let vncauthproxy decide on the source port.
292
    # The alternative: static allocation, e.g.
293
    # sport = console_data['port'] - 1000
294
    sport = 0
295
    daddr = console_data['host']
296
    dport = console_data['port']
297
    password = random_password()
298

    
299
    if settings.TEST:
300
        fwd = {'source_port': 1234, 'status': 'OK'}
301
    else:
302
        fwd = request_vnc_forwarding(sport, daddr, dport, password)
303

    
304
    if fwd['status'] != "OK":
305
        raise faults.ServiceUnavailable('vncauthproxy returned error status')
306

    
307
    # Verify that the VNC server settings haven't changed
308
    if not settings.TEST:
309
        if console_data != backend.get_instance_console(vm):
310
            raise faults.ServiceUnavailable('VNC Server settings changed.')
311

    
312
    console = {
313
        'type': 'vnc',
314
        'host': getfqdn(),
315
        'port': fwd['source_port'],
316
        'password': password}
317

    
318
    if request.serialization == 'xml':
319
        mimetype = 'application/xml'
320
        data = render_to_string('console.xml', {'console': console})
321
    else:
322
        mimetype = 'application/json'
323
        data = json.dumps({'console': console})
324

    
325
    return HttpResponse(data, mimetype=mimetype, status=200)
326

    
327

    
328
@server_action('firewallProfile')
329
def set_firewall_profile(request, vm, args):
330
    # Normal Response Code: 200
331
    # Error Response Codes: computeFault (400, 500),
332
    #                       serviceUnavailable (503),
333
    #                       unauthorized (401),
334
    #                       badRequest (400),
335
    #                       badMediaType(415),
336
    #                       itemNotFound (404),
337
    #                       buildInProgress (409),
338
    #                       overLimit (413)
339

    
340
    profile = args.get('profile', '')
341
    log.info("Set VM %s firewall %s", vm, profile)
342
    if profile not in [x[0] for x in NetworkInterface.FIREWALL_PROFILES]:
343
        raise faults.BadRequest("Unsupported firewall profile")
344
    backend.set_firewall_profile(vm, profile)
345
    return HttpResponse(status=202)
346

    
347

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

    
361
    if net.state != 'ACTIVE':
362
        raise faults.BuildInProgress('Network not active yet')
363

    
364
    server_id = args.get('serverRef', None)
365
    if not server_id:
366
        raise faults.BadRequest('Malformed Request.')
367

    
368
    vm = get_vm(server_id, request.user_uniq, non_suspended=True)
369

    
370
    address = None
371
    if net.dhcp:
372
        # Get a free IP from the address pool.
373
        try:
374
            address = get_network_free_address(net)
375
        except EmptyPool:
376
            raise faults.OverLimit('Network is full')
377

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

    
380
    backend.connect_to_network(vm, net, address)
381
    return HttpResponse(status=202)
382

    
383

    
384
@network_action('remove')
385
@transaction.commit_on_success
386
def remove(request, net, args):
387
    # Normal Response Code: 202
388
    # Error Response Codes: computeFault (400, 500),
389
    #                       serviceUnavailable (503),
390
    #                       unauthorized (401),
391
    #                       badRequest (400),
392
    #                       badMediaType(415),
393
    #                       itemNotFound (404),
394
    #                       overLimit (413)
395

    
396
    try:  # attachment string: nic-<vm-id>-<nic-index>
397
        server_id = args.get('attachment', None).split('-')[1]
398
        nic_index = args.get('attachment', None).split('-')[2]
399
    except AttributeError:
400
        raise faults.BadRequest("Malformed Request")
401
    except IndexError:
402
        raise faults.BadRequest('Malformed Network Interface Id')
403

    
404
    if not server_id or not nic_index:
405
        raise faults.BadRequest('Malformed Request.')
406

    
407
    vm = get_vm(server_id, request.user_uniq, non_suspended=True)
408
    nic = get_nic_from_index(vm, nic_index)
409

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

    
412
    if nic.dirty:
413
        raise faults.BuildInProgress('Machine is busy.')
414
    else:
415
        vm.nics.all().update(dirty=True)
416

    
417
    backend.disconnect_from_network(vm, nic)
418
    return HttpResponse(status=202)