Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (12.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.conf import settings
38
from django.http import HttpResponse
39
from django.template.loader import render_to_string
40
from django.utils import simplejson as json
41

    
42
from synnefo.api.faults import (BadRequest, ServiceUnavailable,
43
                                ItemNotFound, BuildInProgress)
44
from synnefo.api.util import random_password, get_vm, get_nic_from_index
45
from synnefo.db.models import NetworkInterface
46
from synnefo.logic import backend
47
from synnefo.logic.utils import get_rsapi_state
48

    
49

    
50
server_actions = {}
51
network_actions = {}
52

    
53

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

    
59
    def decorator(func):
60
        server_actions[name] = func
61
        return func
62
    return decorator
63

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

    
69
    def decorator(func):
70
        network_actions[name] = func
71
        return func
72
    return decorator
73

    
74

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

    
87
    try:
88
        password = args['adminPass']
89
    except KeyError:
90
        raise BadRequest('Malformed request.')
91

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
195
    raise ServiceUnavailable('Resize not supported.')
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
@server_action('firewallProfile')
273
def set_firewall_profile(request, vm, args):
274
    # Normal Response Code: 200
275
    # Error Response Codes: computeFault (400, 500),
276
    #                       serviceUnavailable (503),
277
    #                       unauthorized (401),
278
    #                       badRequest (400),
279
    #                       badMediaType(415),
280
    #                       itemNotFound (404),
281
    #                       buildInProgress (409),
282
    #                       overLimit (413)
283
    
284
    profile = args.get('profile', '')
285
    if profile not in [x[0] for x in NetworkInterface.FIREWALL_PROFILES]:
286
        raise BadRequest("Unsupported firewall profile")
287
    backend.set_firewall_profile(vm, profile)
288
    return HttpResponse(status=202)
289

    
290

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

    
302
    server_id = args.get('serverRef', None)
303
    if not server_id:
304
        raise BadRequest('Malformed Request.')
305
    vm = get_vm(server_id, request.user_uniq)
306
    backend.connect_to_network(vm, net)
307
    return HttpResponse(status=202)
308

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

    
320
    try: #attachment string: nic-<vm-id>-<nic-index>
321
        server_id = args.get('attachment', None).split('-')[1]
322
        nic_index = args.get('attachment', None).split('-')[2]
323
    except IndexError:
324
        raise BadRequest('Malformed Network Interface Id')
325

    
326
    if not server_id or not nic_index:
327
        raise BadRequest('Malformed Request.')
328
    vm = get_vm(server_id, request.user_uniq)
329
    nic = get_nic_from_index(vm, nic_index)
330

    
331
    if nic.dirty:
332
        raise BuildInProgress('Machine is busy.')
333
    else:
334
        vm.nics.all().update(dirty=True)
335

    
336
    backend.disconnect_from_network(vm, nic)
337
    return HttpResponse(status=202)