Statistics
| Branch: | Tag: | Revision:

root / logic / backend.py @ 9e98ba3c

History | View | Annotate | Download (13.7 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
    if (opcode not in [x[0] for x in VirtualMachine.BACKEND_OPCODES] or
68
       status not in [x[0] for x in VirtualMachine.BACKEND_STATUSES]):
69
        raise VirtualMachine.InvalidBackendMsgError(opcode, status)
70

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

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

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

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

    
95
    # Any other notification of failure leaves the operating state unchanged
96

    
97
    vm.save()
98

    
99

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

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

107
    Update the state of the VM in the DB accordingly.
108
    """
109

    
110
    vm.nics.all().delete()
111
    for i, nic in enumerate(nics):
112
        if i == 0:
113
            net = Network.objects.get(public=True)
114
        else:
115
            try:
116
                link = NetworkLink.objects.get(name=nic['link'])
117
            except NetworkLink.DoesNotExist:
118
                # Cannot find an instance of NetworkLink for
119
                # the link attribute specified in the notification
120
                raise NetworkLink.DoesNotExist("Cannot find a NetworkLink "
121
                    "object for link='%s'" % nic['link'])
122
            net = link.network
123
            if net is None:
124
                raise Network.DoesNotExist("NetworkLink for link='%s' not "
125
                    "associated with an existing Network instance." %
126
                    nic['link'])
127
    
128
        firewall = nic.get('firewall', '')
129
        firewall_profile = _reverse_tags.get(firewall, '')
130
        if not firewall_profile and net.public:
131
            firewall_profile = settings.DEFAULT_FIREWALL_PROFILE
132
    
133
        vm.nics.create(
134
            network=net,
135
            index=i,
136
            mac=nic.get('mac', ''),
137
            ipv4=nic.get('ip', ''),
138
            ipv6=nic.get('ipv6',''),
139
            firewall_profile=firewall_profile)
140
    vm.save()
141

    
142

    
143
@transaction.commit_on_success
144
def process_create_progress(vm, rprogress, wprogress):
145

    
146
    # XXX: This only uses the read progress for now.
147
    #      Explore whether it would make sense to use the value of wprogress
148
    #      somewhere.
149
    percentage = int(rprogress)
150

    
151
    # The percentage may exceed 100%, due to the way
152
    # snf-progress-monitor tracks bytes read by image handling processes
153
    percentage = 100 if percentage > 100 else percentage
154
    if percentage < 0:
155
        raise ValueError("Percentage cannot be negative")
156

    
157
    last_update = vm.buildpercentage
158

    
159
    # FIXME: log a warning here, see #1033
160
#   if last_update > percentage:
161
#       raise ValueError("Build percentage should increase monotonically " \
162
#                        "(old = %d, new = %d)" % (last_update, percentage))
163

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

    
174
    vm.buildpercentage = percentage
175
    vm.save()
176

    
177

    
178
def start_action(vm, action):
179
    """Update the state of a VM when a new action is initiated."""
180
    if not action in [x[0] for x in VirtualMachine.ACTIONS]:
181
        raise VirtualMachine.InvalidActionError(action)
182

    
183
    # No actions to deleted and no actions beside destroy to suspended VMs
184
    if vm.deleted:
185
        raise VirtualMachine.DeletedError
186

    
187
    # No actions to machines being built. They may be destroyed, however.
188
    if vm.operstate == 'BUILD' and action != 'DESTROY':
189
        raise VirtualMachine.BuildingError
190

    
191
    vm.action = action
192
    vm.backendjobid = None
193
    vm.backendopcode = None
194
    vm.backendjobstatus = None
195
    vm.backendlogmsg = None
196

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

    
213

    
214
def create_instance(vm, flavor, image, password, personality):
215

    
216
    nic = {'ip': 'pool', 'mode': 'routed', 'link': settings.GANETI_PUBLIC_LINK}
217

    
218
    if settings.IGNORE_FLAVOR_DISK_SIZES:
219
        if image.backend_id.find("windows") >= 0:
220
            sz = 14000
221
        else:
222
            sz = 4000
223
    else:
224
        sz = flavor.disk * 1024
225

    
226
    # Handle arguments to CreateInstance() as a dictionary,
227
    # initialize it based on a deployment-specific value.
228
    # This enables the administrator to override deployment-specific
229
    # arguments, such as the disk template to use, name of os provider
230
    # and hypervisor-specific parameters at will (see Synnefo #785, #835).
231
    #
232
    kw = settings.GANETI_CREATEINSTANCE_KWARGS
233
    kw['mode'] = 'create'
234
    kw['name'] = vm.backend_id
235
    # Defined in settings.GANETI_CREATE_INSTANCE_KWARGS
236
    # kw['disk_template'] = settings.GANETI_DISK_TEMPLATE
237
    kw['disks'] = [{"size": sz}]
238
    kw['nics'] = [nic]
239
    # Defined in settings.GANETI_CREATE_INSTANCE_KWARGS
240
    # kw['os'] = settings.GANETI_OS_PROVIDER
241
    kw['ip_check'] = False
242
    kw['name_check'] = False
243
    # Do not specific a node explicitly, have
244
    # Ganeti use an iallocator instead
245
    #
246
    # kw['pnode']=rapi.GetNodes()[0]
247
    kw['dry_run'] = settings.TEST
248
    
249
    kw['beparams'] = {
250
        'auto_balance': True,
251
        'vcpus': flavor.cpu,
252
        'memory': flavor.ram}
253
    
254
    kw['osparams'] = {
255
        'img_id': image.backend_id,
256
        'img_passwd': password,
257
        'img_format': image.format}
258
    if personality:
259
        kw['osparams']['img_personality'] = json.dumps(personality)
260
    
261
    # Defined in settings.GANETI_CREATE_INSTANCE_KWARGS
262
    # kw['hvparams'] = dict(serial_console=False)
263

    
264
    return rapi.CreateInstance(**kw)
265

    
266

    
267
def delete_instance(vm):
268
    start_action(vm, 'DESTROY')
269
    rapi.DeleteInstance(vm.backend_id, dry_run=settings.TEST)
270
    vm.nics.all().delete()
271

    
272

    
273
def reboot_instance(vm, reboot_type):
274
    assert reboot_type in ('soft', 'hard')
275
    rapi.RebootInstance(vm.backend_id, reboot_type, dry_run=settings.TEST)
276
    log.info('Rebooting instance %s', vm.backend_id)
277

    
278

    
279
def startup_instance(vm):
280
    start_action(vm, 'START')
281
    rapi.StartupInstance(vm.backend_id, dry_run=settings.TEST)
282

    
283

    
284
def shutdown_instance(vm):
285
    start_action(vm, 'STOP')
286
    rapi.ShutdownInstance(vm.backend_id, dry_run=settings.TEST)
287

    
288

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

    
312

    
313
def request_status_update(vm):
314
    return rapi.GetInstanceInfo(vm.backend_id)
315

    
316

    
317
def get_job_status(jobid):
318
    return rapi.GetJobStatus(jobid)
319

    
320

    
321
def update_status(vm, status):
322
    utils.update_state(vm, status)
323

    
324

    
325
def create_network_link():
326
    try:
327
        last = NetworkLink.objects.order_by('-index')[0]
328
        index = last.index + 1
329
    except IndexError:
330
        index = 1
331

    
332
    if index <= settings.GANETI_MAX_LINK_NUMBER:
333
        name = '%s%d' % (settings.GANETI_LINK_PREFIX, index)
334
        return NetworkLink.objects.create(index=index, name=name,
335
                                            available=True)
336
    return None     # All link slots are filled
337

    
338

    
339
@transaction.commit_on_success
340
def create_network(name, owner):
341
    try:
342
        link = NetworkLink.objects.filter(available=True)[0]
343
    except IndexError:
344
        link = create_network_link()
345
        if not link:
346
            return None
347

    
348
    network = Network.objects.create(
349
        name=name,
350
        owner=owner,
351
        state='ACTIVE',
352
        link=link)
353

    
354
    link.network = network
355
    link.available = False
356
    link.save()
357

    
358
    return network
359

    
360

    
361
@transaction.commit_on_success
362
def delete_network(net):
363
    link = net.link
364
    if link.name != settings.GANETI_NULL_LINK:
365
        link.available = True
366
        link.network = None
367
        link.save()
368

    
369
    for vm in net.machines.all():
370
        disconnect_from_network(vm, net)
371
        vm.save()
372
    net.state = 'DELETED'
373
    net.save()
374

    
375

    
376
def connect_to_network(vm, net):
377
    nic = {'mode': 'bridged', 'link': net.link.name}
378
    rapi.ModifyInstance(vm.backend_id,
379
        nics=[('add', nic)],
380
        dry_run=settings.TEST)
381

    
382

    
383
def disconnect_from_network(vm, net):
384
    nics = vm.nics.filter(network__public=False).order_by('index')
385
    new_nics = [nic for nic in nics if nic.network != net]
386
    if new_nics == nics:
387
        return      # Nothing to remove
388
    ops = [('remove', {})]
389
    for i, nic in enumerate(new_nics):
390
        ops.append((i + 1, {
391
            'mode': 'bridged',
392
            'link': nic.network.link.name}))
393
    rapi.ModifyInstance(vm.backend_id, nics=ops, dry_run=settings.TEST)
394

    
395

    
396
def set_firewall_profile(vm, profile):
397
    try:
398
        tag = _firewall_tags[profile]
399
    except KeyError:
400
        raise ValueError("Unsopported Firewall Profile: %s" % profile)
401

    
402
    # Delete all firewall tags
403
    for t in _firewall_tags.values():
404
        rapi.DeleteInstanceTags(vm.backend_id, [t], dry_run=settings.TEST)
405

    
406
    rapi.AddInstanceTags(vm.backend_id, [tag], dry_run=settings.TEST)
407
    
408
    # XXX NOP ModifyInstance call to force process_net_status to run
409
    # on the dispatcher
410
    rapi.ModifyInstance(vm.backend_id,
411
                        os_name=settings.GANETI_CREATEINSTANCE_KWARGS['os'])
412

    
413
def get_ganeti_instances():
414
    return rapi.GetInstances()
415

    
416
def get_ganeti_nodes():
417
    return rapi.GetNodes()
418

    
419
def get_ganeti_jobs():
420
    return rapi.GetJobs()