Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / logic / backend.py @ 2035039b

History | View | Annotate | Download (14.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
import json
35

    
36
from logging import getLogger
37

    
38
from django.conf import settings
39
from django.db import transaction
40

    
41
from synnefo.db.models import (VirtualMachine, Network, NetworkInterface,
42
                                NetworkLink)
43
from synnefo.logic import utils
44
from synnefo.util.rapi import GanetiRapiClient
45

    
46

    
47
log = getLogger('synnefo.logic')
48

    
49
rapi = GanetiRapiClient(*settings.GANETI_CLUSTER_INFO)
50

    
51
_firewall_tags = {
52
    'ENABLED': settings.GANETI_FIREWALL_ENABLED_TAG,
53
    'DISABLED': settings.GANETI_FIREWALL_DISABLED_TAG,
54
    'PROTECTED': settings.GANETI_FIREWALL_PROTECTED_TAG}
55

    
56
_reverse_tags = dict((v.split(':')[3], k) for k, v in _firewall_tags.items())
57

    
58

    
59
@transaction.commit_on_success
60
def process_op_status(vm, jobid, opcode, status, logmsg):
61
    """Process a job progress notification from the backend
62

63
    Process an incoming message from the backend (currently Ganeti).
64
    Job notifications with a terminating status (sucess, error, or canceled),
65
    also update the operating state of the VM.
66

67
    """
68
    # See #1492, #1031, #1111 why this line has been removed
69
    #if (opcode not in [x[0] for x in VirtualMachine.BACKEND_OPCODES] or
70
    if status not in [x[0] for x in VirtualMachine.BACKEND_STATUSES]:
71
        raise VirtualMachine.InvalidBackendMsgError(opcode, status)
72

    
73
    vm.backendjobid = jobid
74
    vm.backendjobstatus = status
75
    vm.backendopcode = opcode
76
    vm.backendlogmsg = logmsg
77

    
78
    # Notifications of success change the operating state
79
    state_for_success = VirtualMachine.OPER_STATE_FROM_OPCODE.get(opcode, None)
80
    if status == 'success' and state_for_success is not None:
81
        utils.update_state(vm, state_for_success)
82
        # Set the deleted flag explicitly, cater for admin-initiated removals
83
        if opcode == 'OP_INSTANCE_REMOVE':
84
            vm.deleted = True
85

    
86
    # Special case: if OP_INSTANCE_CREATE fails --> ERROR
87
    if status in ('canceled', 'error') and opcode == 'OP_INSTANCE_CREATE':
88
        utils.update_state(vm, 'ERROR')
89

    
90
    # Special case: OP_INSTANCE_REMOVE fails for machines in ERROR,
91
    # when no instance exists at the Ganeti backend.
92
    # See ticket #799 for all the details.
93
    #
94
    if (status == 'error' and opcode == 'OP_INSTANCE_REMOVE' and
95
        vm.operstate == 'ERROR'):
96
        vm.deleted = True
97

    
98
    # Any other notification of failure leaves the operating state unchanged
99

    
100
    vm.save()
101

    
102

    
103
@transaction.commit_on_success
104
def process_net_status(vm, nics):
105
    """Process a net status notification from the backend
106

107
    Process an incoming message from the Ganeti backend,
108
    detailing the NIC configuration of a VM instance.
109

110
    Update the state of the VM in the DB accordingly.
111
    """
112

    
113
    vm.nics.all().delete()
114
    for i, nic in enumerate(nics):
115
        if i == 0:
116
            net = Network.objects.get(public=True)
117
        else:
118
            try:
119
                link = NetworkLink.objects.get(name=nic['link'])
120
            except NetworkLink.DoesNotExist:
121
                # Cannot find an instance of NetworkLink for
122
                # the link attribute specified in the notification
123
                raise NetworkLink.DoesNotExist("Cannot find a NetworkLink "
124
                    "object for link='%s'" % nic['link'])
125
            net = link.network
126
            if net is None:
127
                raise Network.DoesNotExist("NetworkLink for link='%s' not "
128
                    "associated with an existing Network instance." %
129
                    nic['link'])
130

    
131
        firewall = nic.get('firewall', '')
132
        firewall_profile = _reverse_tags.get(firewall, '')
133
        if not firewall_profile and net.public:
134
            firewall_profile = settings.DEFAULT_FIREWALL_PROFILE
135

    
136
        vm.nics.create(
137
            network=net,
138
            index=i,
139
            mac=nic.get('mac', ''),
140
            ipv4=nic.get('ip', ''),
141
            ipv6=nic.get('ipv6', ''),
142
            firewall_profile=firewall_profile)
143

    
144
        # network nics modified, update network object
145
        net.save()
146

    
147
    vm.save()
148

    
149

    
150
@transaction.commit_on_success
151
def process_create_progress(vm, rprogress, wprogress):
152

    
153
    # XXX: This only uses the read progress for now.
154
    #      Explore whether it would make sense to use the value of wprogress
155
    #      somewhere.
156
    percentage = int(rprogress)
157

    
158
    # The percentage may exceed 100%, due to the way
159
    # snf-progress-monitor tracks bytes read by image handling processes
160
    percentage = 100 if percentage > 100 else percentage
161
    if percentage < 0:
162
        raise ValueError("Percentage cannot be negative")
163

    
164
    last_update = vm.buildpercentage
165

    
166
    # FIXME: log a warning here, see #1033
167
#   if last_update > percentage:
168
#       raise ValueError("Build percentage should increase monotonically " \
169
#                        "(old = %d, new = %d)" % (last_update, percentage))
170

    
171
    # This assumes that no message of type 'ganeti-create-progress' is going to
172
    # arrive once OP_INSTANCE_CREATE has succeeded for a Ganeti instance and
173
    # the instance is STARTED.  What if the two messages are processed by two
174
    # separate dispatcher threads, and the 'ganeti-op-status' message for
175
    # successful creation gets processed before the 'ganeti-create-progress'
176
    # message? [vkoukis]
177
    #
178
    #if not vm.operstate == 'BUILD':
179
    #    raise VirtualMachine.IllegalState("VM is not in building state")
180

    
181
    vm.buildpercentage = percentage
182
    vm.save()
183

    
184

    
185
def start_action(vm, action):
186
    """Update the state of a VM when a new action is initiated."""
187
    if not action in [x[0] for x in VirtualMachine.ACTIONS]:
188
        raise VirtualMachine.InvalidActionError(action)
189

    
190
    # No actions to deleted and no actions beside destroy to suspended VMs
191
    if vm.deleted:
192
        raise VirtualMachine.DeletedError
193

    
194
    # No actions to machines being built. They may be destroyed, however.
195
    if vm.operstate == 'BUILD' and action != 'DESTROY':
196
        raise VirtualMachine.BuildingError
197

    
198
    vm.action = action
199
    vm.backendjobid = None
200
    vm.backendopcode = None
201
    vm.backendjobstatus = None
202
    vm.backendlogmsg = None
203

    
204
    # Update the relevant flags if the VM is being suspended or destroyed.
205
    # Do not set the deleted flag here, see ticket #721.
206
    #
207
    # The deleted flag is set asynchronously, when an OP_INSTANCE_REMOVE
208
    # completes successfully. Hence, a server may be visible for some time
209
    # after a DELETE /servers/id returns HTTP 204.
210
    #
211
    if action == "DESTROY":
212
        # vm.deleted = True
213
        pass
214
    elif action == "SUSPEND":
215
        vm.suspended = True
216
    elif action == "START":
217
        vm.suspended = False
218
    vm.save()
219

    
220

    
221
def create_instance(vm, flavor, image, password, personality):
222
    """`image` is a dictionary which should contain the keys:
223
            'backend_id', 'format' and 'metadata'
224
        
225
        metadata value should be a dictionary.
226
    """
227
    nic = {'ip': 'pool', 'mode': 'routed', 'link': settings.GANETI_PUBLIC_LINK}
228

    
229
    if settings.IGNORE_FLAVOR_DISK_SIZES:
230
        if image['backend_id'].find("windows") >= 0:
231
            sz = 14000
232
        else:
233
            sz = 4000
234
    else:
235
        sz = flavor.disk * 1024
236

    
237
    # Handle arguments to CreateInstance() as a dictionary,
238
    # initialize it based on a deployment-specific value.
239
    # This enables the administrator to override deployment-specific
240
    # arguments, such as the disk template to use, name of os provider
241
    # and hypervisor-specific parameters at will (see Synnefo #785, #835).
242
    #
243
    kw = settings.GANETI_CREATEINSTANCE_KWARGS
244
    kw['mode'] = 'create'
245
    kw['name'] = vm.backend_id
246
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
247
    kw['disk_template'] = flavor.disk_template
248
    kw['disks'] = [{"size": sz}]
249
    kw['nics'] = [nic]
250
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
251
    # kw['os'] = settings.GANETI_OS_PROVIDER
252
    kw['ip_check'] = False
253
    kw['name_check'] = False
254
    # Do not specific a node explicitly, have
255
    # Ganeti use an iallocator instead
256
    #
257
    # kw['pnode']=rapi.GetNodes()[0]
258
    kw['dry_run'] = settings.TEST
259

    
260
    kw['beparams'] = {
261
        'auto_balance': True,
262
        'vcpus': flavor.cpu,
263
        'memory': flavor.ram}
264

    
265
    kw['osparams'] = {
266
        'img_id': image['backend_id'],
267
        'img_passwd': password,
268
        'img_format': image['format']}
269
    if personality:
270
        kw['osparams']['img_personality'] = json.dumps(personality)
271
    
272
    kw['osparams']['img_properties'] = json.dumps(image['metadata'])
273
    
274
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
275
    # kw['hvparams'] = dict(serial_console=False)
276

    
277
    return rapi.CreateInstance(**kw)
278

    
279

    
280
def delete_instance(vm):
281
    start_action(vm, 'DESTROY')
282
    rapi.DeleteInstance(vm.backend_id, dry_run=settings.TEST)
283
    vm.nics.all().delete()
284

    
285

    
286
def reboot_instance(vm, reboot_type):
287
    assert reboot_type in ('soft', 'hard')
288
    rapi.RebootInstance(vm.backend_id, reboot_type, dry_run=settings.TEST)
289
    log.info('Rebooting instance %s', vm.backend_id)
290

    
291

    
292
def startup_instance(vm):
293
    start_action(vm, 'START')
294
    rapi.StartupInstance(vm.backend_id, dry_run=settings.TEST)
295

    
296

    
297
def shutdown_instance(vm):
298
    start_action(vm, 'STOP')
299
    rapi.ShutdownInstance(vm.backend_id, dry_run=settings.TEST)
300

    
301

    
302
def get_instance_console(vm):
303
    # RAPI GetInstanceConsole() returns endpoints to the vnc_bind_address,
304
    # which is a cluster-wide setting, either 0.0.0.0 or 127.0.0.1, and pretty
305
    # useless (see #783).
306
    #
307
    # Until this is fixed on the Ganeti side, construct a console info reply
308
    # directly.
309
    #
310
    # WARNING: This assumes that VNC runs on port network_port on
311
    #          the instance's primary node, and is probably
312
    #          hypervisor-specific.
313
    #
314
    console = {}
315
    console['kind'] = 'vnc'
316
    i = rapi.GetInstance(vm.backend_id)
317
    if i['hvparams']['serial_console']:
318
        raise Exception("hv parameter serial_console cannot be true")
319
    console['host'] = i['pnode']
320
    console['port'] = i['network_port']
321

    
322
    return console
323
    # return rapi.GetInstanceConsole(vm.backend_id)
324

    
325

    
326
def request_status_update(vm):
327
    return rapi.GetInstanceInfo(vm.backend_id)
328

    
329

    
330
def get_job_status(jobid):
331
    return rapi.GetJobStatus(jobid)
332

    
333

    
334
def update_status(vm, status):
335
    utils.update_state(vm, status)
336

    
337

    
338
def create_network_link():
339
    try:
340
        last = NetworkLink.objects.order_by('-index')[0]
341
        index = last.index + 1
342
    except IndexError:
343
        index = 1
344

    
345
    if index <= settings.GANETI_MAX_LINK_NUMBER:
346
        name = '%s%d' % (settings.GANETI_LINK_PREFIX, index)
347
        return NetworkLink.objects.create(index=index, name=name,
348
                                            available=True)
349
    return None     # All link slots are filled
350

    
351

    
352
@transaction.commit_on_success
353
def create_network(name, user_id):
354
    try:
355
        link = NetworkLink.objects.filter(available=True)[0]
356
    except IndexError:
357
        link = create_network_link()
358
        if not link:
359
            return None
360

    
361
    network = Network.objects.create(
362
        name=name,
363
        userid=user_id,
364
        state='ACTIVE',
365
        link=link)
366

    
367
    link.network = network
368
    link.available = False
369
    link.save()
370

    
371
    return network
372

    
373

    
374
@transaction.commit_on_success
375
def delete_network(net):
376
    link = net.link
377
    if link.name != settings.GANETI_NULL_LINK:
378
        link.available = True
379
        link.network = None
380
        link.save()
381

    
382
    for vm in net.machines.all():
383
        disconnect_from_network(vm, net)
384
        vm.save()
385
    net.state = 'DELETED'
386
    net.save()
387

    
388

    
389
def connect_to_network(vm, net):
390
    nic = {'mode': 'bridged', 'link': net.link.name}
391
    rapi.ModifyInstance(vm.backend_id,
392
        nics=[('add', nic)],
393
        dry_run=settings.TEST)
394

    
395

    
396
def disconnect_from_network(vm, net):
397
    nics = vm.nics.filter(network__public=False).order_by('index')
398
    new_nics = [nic for nic in nics if nic.network != net]
399
    if new_nics == nics:
400
        return      # Nothing to remove
401
    ops = [('remove', {})]
402
    for i, nic in enumerate(new_nics):
403
        ops.append((i + 1, {
404
            'mode': 'bridged',
405
            'link': nic.network.link.name}))
406
    rapi.ModifyInstance(vm.backend_id, nics=ops, dry_run=settings.TEST)
407

    
408

    
409
def set_firewall_profile(vm, profile):
410
    try:
411
        tag = _firewall_tags[profile]
412
    except KeyError:
413
        raise ValueError("Unsopported Firewall Profile: %s" % profile)
414

    
415
    # Delete all firewall tags
416
    for t in _firewall_tags.values():
417
        rapi.DeleteInstanceTags(vm.backend_id, [t], dry_run=settings.TEST)
418

    
419
    rapi.AddInstanceTags(vm.backend_id, [tag], dry_run=settings.TEST)
420

    
421
    # XXX NOP ModifyInstance call to force process_net_status to run
422
    # on the dispatcher
423
    rapi.ModifyInstance(vm.backend_id,
424
                        os_name=settings.GANETI_CREATEINSTANCE_KWARGS['os'])
425

    
426

    
427
def get_ganeti_instances():
428
    return rapi.GetInstances()
429

    
430

    
431
def get_ganeti_nodes():
432
    return rapi.GetNodes()
433

    
434

    
435
def get_ganeti_jobs():
436
    return rapi.GetJobs()