Statistics
| Branch: | Tag: | Revision:

root / logic / backend.py @ c25cc9ec

History | View | Annotate | Download (13.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
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
    if percentage < 0 or percentage > 100:
147
        raise ValueError("Percentage not in range [0, 100]")
148

    
149
    last_update = vm.buildpercentage
150

    
151
    if last_update > percentage:
152
        raise ValueError("Build percentage should increase monotonically " \
153
                        "(old = %d, new = %d)" % (last_update, percentage))
154

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

    
165
    vm.buildpercentage = percentage
166
    vm.save()
167

    
168

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

    
174
    # No actions to deleted and no actions beside destroy to suspended VMs
175
    if vm.deleted:
176
        raise VirtualMachine.DeletedError
177

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

    
182
    vm.action = action
183
    vm.backendjobid = None
184
    vm.backendopcode = None
185
    vm.backendjobstatus = None
186
    vm.backendlogmsg = None
187

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

    
204

    
205
def create_instance(vm, flavor, image, password):
206

    
207
    nic = {'ip': 'pool', 'mode': 'routed', 'link': settings.GANETI_PUBLIC_LINK}
208

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

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

    
246
    return rapi.CreateInstance(**kw)
247

    
248

    
249
def delete_instance(vm):
250
    start_action(vm, 'DESTROY')
251
    rapi.DeleteInstance(vm.backend_id, dry_run=settings.TEST)
252
    vm.nics.all().delete()
253

    
254

    
255
def reboot_instance(vm, reboot_type):
256
    assert reboot_type in ('soft', 'hard')
257
    rapi.RebootInstance(vm.backend_id, reboot_type, dry_run=settings.TEST)
258

    
259

    
260
def startup_instance(vm):
261
    start_action(vm, 'START')
262
    rapi.StartupInstance(vm.backend_id, dry_run=settings.TEST)
263

    
264

    
265
def shutdown_instance(vm):
266
    start_action(vm, 'STOP')
267
    rapi.ShutdownInstance(vm.backend_id, dry_run=settings.TEST)
268

    
269

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

    
293

    
294
def request_status_update(vm):
295
    return rapi.GetInstanceInfo(vm.backend_id)
296

    
297

    
298
def get_job_status(jobid):
299
    return rapi.GetJobStatus(jobid)
300

    
301

    
302
def update_status(vm, status):
303
    utils.update_state(vm, status)
304

    
305

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

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

    
319

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

    
329
    network = Network.objects.create(
330
        name=name,
331
        owner=owner,
332
        state='ACTIVE',
333
        link=link)
334

    
335
    link.network = network
336
    link.available = False
337
    link.save()
338

    
339
    return network
340

    
341

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

    
350
    for vm in net.machines.all():
351
        disconnect_from_network(vm, net)
352
        vm.save()
353
    net.state = 'DELETED'
354
    net.save()
355

    
356

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

    
363

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

    
376

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

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

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