Statistics
| Branch: | Tag: | Revision:

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

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, NetworkLink)
42
from synnefo.logic import utils
43
from synnefo.util.rapi import GanetiRapiClient
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, etime, 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
            vm.nics.all().delete()
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
        vm.nics.all().delete()
98

    
99
    vm.backendtime = etime
100
    # Any other notification of failure leaves the operating state unchanged
101

    
102
    vm.save()
103

    
104

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

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

112
    Update the state of the VM in the DB accordingly.
113
    """
114

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

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

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

    
146
        # network nics modified, update network object
147
        net.save()
148

    
149
    vm.backendtime = etime
150
    vm.save()
151

    
152

    
153
@transaction.commit_on_success
154
def process_create_progress(vm, etime, rprogress, wprogress):
155

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

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

    
167
    last_update = vm.buildpercentage
168

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

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

    
184
    vm.buildpercentage = percentage
185
    vm.backendtime = etime
186
    vm.save()
187

    
188

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

    
194
    # No actions to deleted and no actions beside destroy to suspended VMs
195
    if vm.deleted:
196
        raise VirtualMachine.DeletedError
197

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

    
202
    vm.action = action
203
    vm.backendjobid = None
204
    vm.backendopcode = None
205
    vm.backendjobstatus = None
206
    vm.backendlogmsg = None
207

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

    
224

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

229
        metadata value should be a dictionary.
230
    """
231
    nic = {'ip': 'pool', 'network': settings.GANETI_PUBLIC_NETWORK}
232

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

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

    
264
    kw['beparams'] = {
265
        'auto_balance': True,
266
        'vcpus': flavor.cpu,
267
        'memory': flavor.ram}
268

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

    
276
    kw['osparams']['img_properties'] = json.dumps(image['metadata'])
277

    
278
    # Defined in settings.GANETI_CREATEINSTANCE_KWARGS
279
    # kw['hvparams'] = dict(serial_console=False)
280

    
281
    return rapi.CreateInstance(**kw)
282

    
283

    
284
def delete_instance(vm):
285
    start_action(vm, 'DESTROY')
286
    rapi.DeleteInstance(vm.backend_id, dry_run=settings.TEST)
287

    
288

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

    
294

    
295
def startup_instance(vm):
296
    start_action(vm, 'START')
297
    rapi.StartupInstance(vm.backend_id, dry_run=settings.TEST)
298

    
299

    
300
def shutdown_instance(vm):
301
    start_action(vm, 'STOP')
302
    rapi.ShutdownInstance(vm.backend_id, dry_run=settings.TEST)
303

    
304

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

    
325
    return console
326
    # return rapi.GetInstanceConsole(vm.backend_id)
327

    
328

    
329
def request_status_update(vm):
330
    return rapi.GetInstanceInfo(vm.backend_id)
331

    
332

    
333
def get_job_status(jobid):
334
    return rapi.GetJobStatus(jobid)
335

    
336

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

    
340

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

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

    
354

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

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

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

    
374
    return network
375

    
376

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

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

    
391

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

    
397

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

    
406

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

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

    
417
    rapi.AddInstanceTags(vm.backend_id, [tag], dry_run=settings.TEST)
418

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

    
424

    
425
def get_ganeti_instances():
426
    return rapi.GetInstances()
427

    
428

    
429
def get_ganeti_nodes():
430
    return rapi.GetNodes()
431

    
432

    
433
def get_ganeti_jobs():
434
    return rapi.GetJobs()