Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (14.8 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
from django.conf import settings
38
from django.db import transaction
39

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

    
44

    
45

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

    
48

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

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

    
56

    
57
def create_client(hostname, port=5080, username=None, password=None):
58
    return GanetiRapiClient(hostname=hostname,
59
                            port=port,
60
                            username=username,
61
                            password=password)
62

    
63
@transaction.commit_on_success
64
def process_op_status(vm, etime, jobid, opcode, status, logmsg):
65
    """Process a job progress notification from the backend
66

67
    Process an incoming message from the backend (currently Ganeti).
68
    Job notifications with a terminating status (sucess, error, or canceled),
69
    also update the operating state of the VM.
70

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

    
77
    vm.backendjobid = jobid
78
    vm.backendjobstatus = status
79
    vm.backendopcode = opcode
80
    vm.backendlogmsg = logmsg
81

    
82
    # Notifications of success change the operating state
83
    state_for_success = VirtualMachine.OPER_STATE_FROM_OPCODE.get(opcode, None)
84
    if status == 'success' and state_for_success is not None:
85
        utils.update_state(vm, state_for_success)
86
        # Set the deleted flag explicitly, cater for admin-initiated removals
87
        if opcode == 'OP_INSTANCE_REMOVE':
88
            vm.deleted = True
89
            vm.nics.all().delete()
90

    
91
    # Special case: if OP_INSTANCE_CREATE fails --> ERROR
92
    if status in ('canceled', 'error') and opcode == 'OP_INSTANCE_CREATE':
93
        utils.update_state(vm, 'ERROR')
94

    
95
    # Special case: OP_INSTANCE_REMOVE fails for machines in ERROR,
96
    # when no instance exists at the Ganeti backend.
97
    # See ticket #799 for all the details.
98
    #
99
    if (status == 'error' and opcode == 'OP_INSTANCE_REMOVE' and
100
        vm.operstate == 'ERROR'):
101
        vm.deleted = True
102
        vm.nics.all().delete()
103

    
104
    vm.backendtime = etime
105
    # Any other notification of failure leaves the operating state unchanged
106

    
107
    vm.save()
108

    
109

    
110
@transaction.commit_on_success
111
def process_net_status(vm, etime, nics):
112
    """Process a net status notification from the backend
113

114
    Process an incoming message from the Ganeti backend,
115
    detailing the NIC configuration of a VM instance.
116

117
    Update the state of the VM in the DB accordingly.
118
    """
119

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

    
138
        firewall = nic.get('firewall', '')
139
        firewall_profile = _reverse_tags.get(firewall, '')
140
        if not firewall_profile and net.public:
141
            firewall_profile = settings.DEFAULT_FIREWALL_PROFILE
142

    
143
        vm.nics.create(
144
            network=net,
145
            index=i,
146
            mac=nic.get('mac', ''),
147
            ipv4=nic.get('ip', ''),
148
            ipv6=nic.get('ipv6', ''),
149
            firewall_profile=firewall_profile)
150

    
151
        # network nics modified, update network object
152
        net.save()
153

    
154
    vm.backendtime = etime
155
    vm.save()
156

    
157

    
158
@transaction.commit_on_success
159
def process_create_progress(vm, etime, rprogress, wprogress):
160

    
161
    # XXX: This only uses the read progress for now.
162
    #      Explore whether it would make sense to use the value of wprogress
163
    #      somewhere.
164
    percentage = int(rprogress)
165

    
166
    # The percentage may exceed 100%, due to the way
167
    # snf-progress-monitor tracks bytes read by image handling processes
168
    percentage = 100 if percentage > 100 else percentage
169
    if percentage < 0:
170
        raise ValueError("Percentage cannot be negative")
171

    
172
    # FIXME: log a warning here, see #1033
173
#   if last_update > percentage:
174
#       raise ValueError("Build percentage should increase monotonically " \
175
#                        "(old = %d, new = %d)" % (last_update, percentage))
176

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

    
187
    vm.buildpercentage = percentage
188
    vm.backendtime = etime
189
    vm.save()
190

    
191

    
192
def start_action(vm, action):
193
    """Update the state of a VM when a new action is initiated."""
194
    if not action in [x[0] for x in VirtualMachine.ACTIONS]:
195
        raise VirtualMachine.InvalidActionError(action)
196

    
197
    # No actions to deleted and no actions beside destroy to suspended VMs
198
    if vm.deleted:
199
        raise VirtualMachine.DeletedError
200

    
201
    # No actions to machines being built. They may be destroyed, however.
202
    if vm.operstate == 'BUILD' and action != 'DESTROY':
203
        raise VirtualMachine.BuildingError
204

    
205
    vm.action = action
206
    vm.backendjobid = None
207
    vm.backendopcode = None
208
    vm.backendjobstatus = None
209
    vm.backendlogmsg = None
210

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

    
227

    
228
def create_instance(vm, flavor, image, password, personality):
229
    """`image` is a dictionary which should contain the keys:
230
            'backend_id', 'format' and 'metadata'
231

232
        metadata value should be a dictionary.
233
    """
234
    nic = {'ip': 'pool', 'network': settings.GANETI_PUBLIC_NETWORK}
235

    
236
    if settings.IGNORE_FLAVOR_DISK_SIZES:
237
        if image['backend_id'].find("windows") >= 0:
238
            sz = 14000
239
        else:
240
            sz = 4000
241
    else:
242
        sz = flavor.disk * 1024
243

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

    
267
    kw['beparams'] = {
268
        'auto_balance': True,
269
        'vcpus': flavor.cpu,
270
        'memory': flavor.ram}
271

    
272
    kw['osparams'] = {
273
        'img_id': image['backend_id'],
274
        'img_passwd': password,
275
        'img_format': image['format']}
276
    if personality:
277
        kw['osparams']['img_personality'] = json.dumps(personality)
278

    
279
    kw['osparams']['img_properties'] = json.dumps(image['metadata'])
280

    
281
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
282
    # kw['hvparams'] = dict(serial_console=False)
283

    
284
    return vm.client.CreateInstance(**kw)
285

    
286

    
287
def delete_instance(vm):
288
    start_action(vm, 'DESTROY')
289
    vm.client.DeleteInstance(vm.backend_vm_id, dry_run=settings.TEST)
290

    
291

    
292
def reboot_instance(vm, reboot_type):
293
    assert reboot_type in ('soft', 'hard')
294
    vm.client.RebootInstance(vm.backend_vm_id, reboot_type, dry_run=settings.TEST)
295
    log.info('Rebooting instance %s', vm.backend_vm_id)
296

    
297

    
298
def startup_instance(vm):
299
    start_action(vm, 'START')
300
    vm.client.StartupInstance(vm.backend_vm_id, dry_run=settings.TEST)
301

    
302

    
303
def shutdown_instance(vm):
304
    start_action(vm, 'STOP')
305
    vm.client.ShutdownInstance(vm.backend_vm_id, dry_run=settings.TEST)
306

    
307

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

    
328
    return console
329
    # return rapi.GetInstanceConsole(vm.backend_vm_id)
330

    
331

    
332
def request_status_update(vm):
333
    return vm.client.GetInstanceInfo(vm.backend_vm_id)
334

    
335

    
336
def update_status(vm, status):
337
    utils.update_state(vm, status)
338

    
339

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

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

    
353

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

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

    
369
    link.network = network
370
    link.available = False
371
    link.save()
372

    
373
    return network
374

    
375

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

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

    
390

    
391
def connect_to_network(vm, net):
392
    nic = {'mode': 'bridged', 'link': net.link.name}
393
    vm.client.ModifyInstance(vm.backend_vm_id, nics=[('add', -1, nic)],
394
                        hotplug=True, dry_run=settings.TEST)
395

    
396

    
397
def disconnect_from_network(vm, net):
398
    nics = vm.nics.filter(network__public=False).order_by('index')
399
    ops = [('remove', nic.index, {}) for nic in nics if nic.network == net]
400
    if not ops:  # Vm not connected to network
401
        return
402
    vm.client.ModifyInstance(vm.backend_vm_id, nics=ops[::-1],
403
                        hotplug=True, dry_run=settings.TEST)
404

    
405

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

    
412
    client = vm.client
413
    # Delete all firewall tags
414
    for t in _firewall_tags.values():
415
        client.DeleteInstanceTags(vm.backend_vm_id, [t], dry_run=settings.TEST)
416

    
417
    client.AddInstanceTags(vm.backend_vm_id, [tag], dry_run=settings.TEST)
418

    
419
    # XXX NOP ModifyInstance call to force process_net_status to run
420
    # on the dispatcher
421
    vm.client.ModifyInstance(vm.backend_vm_id,
422
                        os_name=settings.GANETI_CREATEINSTANCE_KWARGS['os'])
423

    
424

    
425
def get_ganeti_instances(backend=None, bulk=False):
426
    Instances = [c.client.GetInstances(bulk=bulk) for c in get_backends(backend)]
427
    return reduce(list.__add__, Instances, [])
428

    
429

    
430
def get_ganeti_nodes(backend=None, bulk=False):
431
    Nodes = [c.client.GetNodes(bulk=bulk) for c in get_backends(backend)]
432
    return reduce(list.__add__, Nodes, [])
433

    
434

    
435
def get_ganeti_jobs(backend=None, bulk=False):
436
    Jobs = [c.client.GetJobs(bulk=bulk) for c in get_backends(backend)]
437
    return reduce(list.__add__, Jobs, [])
438

    
439
##
440
##
441
##
442
def get_backends(backend=None):
443
    if backend:
444
        return [backend]
445
    return Backend.objects.all()
446

    
447

    
448

    
449

    
450