Statistics
| Branch: | Tag: | Revision:

root / snf-app / synnefo / logic / backend.py @ 483c9197

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

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

    
45

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

    
48
rapi = GanetiRapiClient(*settings.GANETI_CLUSTER_INFO)
49

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

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

    
57

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

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

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

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

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

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

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

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

    
99
    vm.save()
100

    
101

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

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

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

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

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

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

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

    
146
    vm.save()
147

    
148

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

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

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

    
163
    last_update = vm.buildpercentage
164

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

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

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

    
183

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

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

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

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

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

    
219

    
220
def create_instance(vm, flavor, image, password, personality):
221

    
222
    nic = {'ip': 'pool', 'mode': 'routed', 'link': settings.GANETI_PUBLIC_LINK}
223

    
224
    if settings.IGNORE_FLAVOR_DISK_SIZES:
225
        if image.backend_id.find("windows") >= 0:
226
            sz = 14000
227
        else:
228
            sz = 4000
229
    else:
230
        sz = flavor.disk * 1024
231

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

    
255
    kw['beparams'] = {
256
        'auto_balance': True,
257
        'vcpus': flavor.cpu,
258
        'memory': flavor.ram}
259

    
260
    kw['osparams'] = {
261
        'img_id': image.backend_id,
262
        'img_passwd': password,
263
        'img_format': image.format}
264
    if personality:
265
        kw['osparams']['img_personality'] = json.dumps(personality)
266
    
267
    image_meta = dict((m.meta_key, m.meta_value) for m in image.metadata.all())
268
    kw['osparams']['img_properties'] = json.dumps(image_meta)
269
    
270
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
271
    # kw['hvparams'] = dict(serial_console=False)
272

    
273
    return rapi.CreateInstance(**kw)
274

    
275

    
276
def delete_instance(vm):
277
    start_action(vm, 'DESTROY')
278
    rapi.DeleteInstance(vm.backend_id, dry_run=settings.TEST)
279
    vm.nics.all().delete()
280

    
281

    
282
def reboot_instance(vm, reboot_type):
283
    assert reboot_type in ('soft', 'hard')
284
    rapi.RebootInstance(vm.backend_id, reboot_type, dry_run=settings.TEST)
285
    log.info('Rebooting instance %s', vm.backend_id)
286

    
287

    
288
def startup_instance(vm):
289
    start_action(vm, 'START')
290
    rapi.StartupInstance(vm.backend_id, dry_run=settings.TEST)
291

    
292

    
293
def shutdown_instance(vm):
294
    start_action(vm, 'STOP')
295
    rapi.ShutdownInstance(vm.backend_id, dry_run=settings.TEST)
296

    
297

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

    
318
    return console
319
    # return rapi.GetInstanceConsole(vm.backend_id)
320

    
321

    
322
def request_status_update(vm):
323
    return rapi.GetInstanceInfo(vm.backend_id)
324

    
325

    
326
def get_job_status(jobid):
327
    return rapi.GetJobStatus(jobid)
328

    
329

    
330
def update_status(vm, status):
331
    utils.update_state(vm, status)
332

    
333

    
334
def create_network_link():
335
    try:
336
        last = NetworkLink.objects.order_by('-index')[0]
337
        index = last.index + 1
338
    except IndexError:
339
        index = 1
340

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

    
347

    
348
@transaction.commit_on_success
349
def create_network(name, owner):
350
    try:
351
        link = NetworkLink.objects.filter(available=True)[0]
352
    except IndexError:
353
        link = create_network_link()
354
        if not link:
355
            return None
356

    
357
    network = Network.objects.create(
358
        name=name,
359
        owner=owner,
360
        state='ACTIVE',
361
        link=link)
362

    
363
    link.network = network
364
    link.available = False
365
    link.save()
366

    
367
    return network
368

    
369

    
370
@transaction.commit_on_success
371
def delete_network(net):
372
    link = net.link
373
    if link.name != settings.GANETI_NULL_LINK:
374
        link.available = True
375
        link.network = None
376
        link.save()
377

    
378
    for vm in net.machines.all():
379
        disconnect_from_network(vm, net)
380
        vm.save()
381
    net.state = 'DELETED'
382
    net.save()
383

    
384

    
385
def connect_to_network(vm, net):
386
    nic = {'mode': 'bridged', 'link': net.link.name}
387
    rapi.ModifyInstance(vm.backend_id,
388
        nics=[('add', nic)],
389
        dry_run=settings.TEST)
390

    
391

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

    
404

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

    
411
    # Delete all firewall tags
412
    for t in _firewall_tags.values():
413
        rapi.DeleteInstanceTags(vm.backend_id, [t], dry_run=settings.TEST)
414

    
415
    rapi.AddInstanceTags(vm.backend_id, [tag], dry_run=settings.TEST)
416

    
417
    # XXX NOP ModifyInstance call to force process_net_status to run
418
    # on the dispatcher
419
    rapi.ModifyInstance(vm.backend_id,
420
                        os_name=settings.GANETI_CREATEINSTANCE_KWARGS['os'])
421

    
422

    
423
def get_ganeti_instances():
424
    return rapi.GetInstances()
425

    
426

    
427
def get_ganeti_nodes():
428
    return rapi.GetNodes()
429

    
430

    
431
def get_ganeti_jobs():
432
    return rapi.GetJobs()