Statistics
| Branch: | Tag: | Revision:

root / logic / backend.py @ af90d919

History | View | Annotate | Download (13.3 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
from django.conf import settings
35
from django.db import transaction
36

    
37
from synnefo.db.models import (VirtualMachine, Network, NetworkInterface,
38
                                NetworkLink)
39
from synnefo.logic import utils
40
from synnefo.util.rapi import GanetiRapiClient
41

    
42

    
43
rapi = GanetiRapiClient(*settings.GANETI_CLUSTER_INFO)
44

    
45
_firewall_tags = {
46
    'ENABLED': settings.GANETI_FIREWALL_ENABLED_TAG,
47
    'DISABLED': settings.GANETI_FIREWALL_DISABLED_TAG,
48
    'PROTECTED': settings.GANETI_FIREWALL_PROTECTED_TAG}
49

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

    
52

    
53
@transaction.commit_on_success
54
def process_op_status(vm, jobid, opcode, status, logmsg):
55
    """Process a job progress notification from the backend
56

57
    Process an incoming message from the backend (currently Ganeti).
58
    Job notifications with a terminating status (sucess, error, or canceled),
59
    also update the operating state of the VM.
60

61
    """
62
    if (opcode not in [x[0] for x in VirtualMachine.BACKEND_OPCODES] or
63
       status not in [x[0] for x in VirtualMachine.BACKEND_STATUSES]):
64
        raise VirtualMachine.InvalidBackendMsgError(opcode, status)
65

    
66
    vm.backendjobid = jobid
67
    vm.backendjobstatus = status
68
    vm.backendopcode = opcode
69
    vm.backendlogmsg = logmsg
70

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

    
78
    # Special case: if OP_INSTANCE_CREATE fails --> ERROR
79
    if status in ('canceled', 'error') and opcode == 'OP_INSTANCE_CREATE':
80
        utils.update_state(vm, 'ERROR')
81

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

    
90
    # Any other notification of failure leaves the operating state unchanged
91

    
92
    vm.save()
93

    
94

    
95
@transaction.commit_on_success
96
def process_net_status(vm, nics):
97
    """Process a net status notification from the backend
98

99
    Process an incoming message from the Ganeti backend,
100
    detailing the NIC configuration of a VM instance.
101

102
    Update the state of the VM in the DB accordingly.
103
    """
104

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

    
137

    
138
@transaction.commit_on_success
139
def process_create_progress(vm, rprogress, wprogress):
140

    
141
    # XXX: This only uses the read progress for now.
142
    #      Explore whether it would make sense to use the value of wprogress
143
    #      somewhere.
144
    percentage = int(rprogress)
145

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

    
152
    last_update = vm.buildpercentage
153

    
154
    if last_update > percentage:
155
        raise ValueError("Build percentage should increase monotonically " \
156
                        "(old = %d, new = %d)" % (last_update, percentage))
157

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

    
168
    vm.buildpercentage = percentage
169
    vm.save()
170

    
171

    
172
def start_action(vm, action):
173
    """Update the state of a VM when a new action is initiated."""
174
    if not action in [x[0] for x in VirtualMachine.ACTIONS]:
175
        raise VirtualMachine.InvalidActionError(action)
176

    
177
    # No actions to deleted and no actions beside destroy to suspended VMs
178
    if vm.deleted:
179
        raise VirtualMachine.DeletedError
180

    
181
    # No actions to machines being built. They may be destroyed, however.
182
    if vm.operstate == 'BUILD' and action != 'DESTROY':
183
        raise VirtualMachine.BuildingError
184

    
185
    vm.action = action
186
    vm.backendjobid = None
187
    vm.backendopcode = None
188
    vm.backendjobstatus = None
189
    vm.backendlogmsg = None
190

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

    
207

    
208
def create_instance(vm, flavor, image, password):
209

    
210
    nic = {'ip': 'pool', 'mode': 'routed', 'link': settings.GANETI_PUBLIC_LINK}
211

    
212
    if settings.IGNORE_FLAVOR_DISK_SIZES:
213
        if image.backend_id.find("windows") >= 0:
214
            sz = 14000
215
        else:
216
            sz = 4000
217
    else:
218
        sz = flavor.disk * 1024
219

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

    
249
    return rapi.CreateInstance(**kw)
250

    
251

    
252
def delete_instance(vm):
253
    start_action(vm, 'DESTROY')
254
    rapi.DeleteInstance(vm.backend_id, dry_run=settings.TEST)
255
    vm.nics.all().delete()
256

    
257

    
258
def reboot_instance(vm, reboot_type):
259
    assert reboot_type in ('soft', 'hard')
260
    rapi.RebootInstance(vm.backend_id, reboot_type, dry_run=settings.TEST)
261

    
262

    
263
def startup_instance(vm):
264
    start_action(vm, 'START')
265
    rapi.StartupInstance(vm.backend_id, dry_run=settings.TEST)
266

    
267

    
268
def shutdown_instance(vm):
269
    start_action(vm, 'STOP')
270
    rapi.ShutdownInstance(vm.backend_id, dry_run=settings.TEST)
271

    
272

    
273
def get_instance_console(vm):
274
    # RAPI GetInstanceConsole() returns endpoints to the vnc_bind_address,
275
    # which is a cluster-wide setting, either 0.0.0.0 or 127.0.0.1, and pretty
276
    # useless (see #783).
277
    #
278
    # Until this is fixed on the Ganeti side, construct a console info reply
279
    # directly.
280
    # 
281
    # WARNING: This assumes that VNC runs on port network_port on
282
    #          the instance's primary node, and is probably
283
    #          hypervisor-specific.
284
    #
285
    console = {}
286
    console['kind'] = 'vnc'
287
    i = rapi.GetInstance(vm.backend_id)
288
    if i['hvparams']['serial_console']:
289
        raise Exception("hv parameter serial_console cannot be true")
290
    console['host'] = i['pnode']
291
    console['port'] = i['network_port']
292
    
293
    return console
294
    # return rapi.GetInstanceConsole(vm.backend_id)
295

    
296

    
297
def request_status_update(vm):
298
    return rapi.GetInstanceInfo(vm.backend_id)
299

    
300

    
301
def get_job_status(jobid):
302
    return rapi.GetJobStatus(jobid)
303

    
304

    
305
def update_status(vm, status):
306
    utils.update_state(vm, status)
307

    
308

    
309
def create_network_link():
310
    try:
311
        last = NetworkLink.objects.order_by('-index')[0]
312
        index = last.index + 1
313
    except IndexError:
314
        index = 1
315

    
316
    if index <= settings.GANETI_MAX_LINK_NUMBER:
317
        name = '%s%d' % (settings.GANETI_LINK_PREFIX, index)
318
        return NetworkLink.objects.create(index=index, name=name,
319
                                            available=True)
320
    return None     # All link slots are filled
321

    
322

    
323
@transaction.commit_on_success
324
def create_network(name, owner):
325
    try:
326
        link = NetworkLink.objects.filter(available=True)[0]
327
    except IndexError:
328
        link = create_network_link()
329
        if not link:
330
            return None
331

    
332
    network = Network.objects.create(
333
        name=name,
334
        owner=owner,
335
        state='ACTIVE',
336
        link=link)
337

    
338
    link.network = network
339
    link.available = False
340
    link.save()
341

    
342
    return network
343

    
344

    
345
@transaction.commit_on_success
346
def delete_network(net):
347
    link = net.link
348
    if link.name != settings.GANETI_NULL_LINK:
349
        link.available = True
350
        link.network = None
351
        link.save()
352

    
353
    for vm in net.machines.all():
354
        disconnect_from_network(vm, net)
355
        vm.save()
356
    net.state = 'DELETED'
357
    net.save()
358

    
359

    
360
def connect_to_network(vm, net):
361
    nic = {'mode': 'bridged', 'link': net.link.name}
362
    rapi.ModifyInstance(vm.backend_id,
363
        nics=[('add', nic)],
364
        dry_run=settings.TEST)
365

    
366

    
367
def disconnect_from_network(vm, net):
368
    nics = vm.nics.filter(network__public=False).order_by('index')
369
    new_nics = [nic for nic in nics if nic.network != net]
370
    if new_nics == nics:
371
        return      # Nothing to remove
372
    ops = [('remove', {})]
373
    for i, nic in enumerate(new_nics):
374
        ops.append((i + 1, {
375
            'mode': 'bridged',
376
            'link': nic.network.link.name}))
377
    rapi.ModifyInstance(vm.backend_id, nics=ops, dry_run=settings.TEST)
378

    
379

    
380
def set_firewall_profile(vm, profile):
381
    try:
382
        tag = _firewall_tags[profile]
383
    except KeyError:
384
        raise ValueError("Unsopported Firewall Profile: %s" % profile)
385

    
386
    # Delete all firewall tags
387
    for t in _firewall_tags.values():
388
        rapi.DeleteInstanceTags(vm.backend_id, [t], dry_run=settings.TEST)
389

    
390
    rapi.AddInstanceTags(vm.backend_id, [tag], dry_run=settings.TEST)
391
    
392
    # XXX NOP ModifyInstance call to force process_net_status to run
393
    # on the dispatcher
394
    rapi.ModifyInstance(vm.backend_id,
395
                        os_name=settings.GANETI_CREATEINSTANCE_KWARGS['os'])
396