Statistics
| Branch: | Tag: | Revision:

root / logic / backend.py @ 9068cd85

History | View | Annotate | Download (12.6 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
@transaction.commit_on_success
138
def process_progress_update(vm, pr_percentage):
139

    
140
    if not type(pr_percentage) == int:
141
        raise TypeError("Percentage not an integer")
142

    
143
    if pr_percentage < 0 or pr_percentage > 100:
144
        raise Exception("Percentage not in range (0, 100)")
145

    
146
    last_update = vm.buildpercentage
147

    
148
    if last_update > pr_percentage:
149
        raise Exception("Build percentage should increase monotonically" \
150
                        " (old=%d, new=%d)"%(last_update, pr_percentage))
151

    
152
    if not vm.operstate == 'BUILD':
153
        raise VirtualMachine.IllegalState("VM is not in building state")
154

    
155
    vm.buildpercentage = pr_percentage
156
    vm.save()
157

    
158
def start_action(vm, action):
159
    """Update the state of a VM when a new action is initiated."""
160
    if not action in [x[0] for x in VirtualMachine.ACTIONS]:
161
        raise VirtualMachine.InvalidActionError(action)
162

    
163
    # No actions to deleted and no actions beside destroy to suspended VMs
164
    if vm.deleted:
165
        raise VirtualMachine.DeletedError
166

    
167
    # No actions to machines being built. They may be destroyed, however.
168
    if vm.operstate == 'BUILD' and action != 'DESTROY':
169
        raise VirtualMachine.BuildingError
170

    
171
    vm.action = action
172
    vm.backendjobid = None
173
    vm.backendopcode = None
174
    vm.backendjobstatus = None
175
    vm.backendlogmsg = None
176

    
177
    # Update the relevant flags if the VM is being suspended or destroyed.
178
    # Do not set the deleted flag here, see ticket #721.
179
    #
180
    # The deleted flag is set asynchronously, when an OP_INSTANCE_REMOVE
181
    # completes successfully. Hence, a server may be visible for some time
182
    # after a DELETE /servers/id returns HTTP 204.
183
    #
184
    if action == "DESTROY":
185
        # vm.deleted = True
186
        pass
187
    elif action == "SUSPEND":
188
        vm.suspended = True
189
    elif action == "START":
190
        vm.suspended = False
191
    vm.save()
192

    
193

    
194
def create_instance(vm, flavor, image, password):
195

    
196
    nic = {'ip': 'pool', 'mode': 'routed', 'link': settings.GANETI_PUBLIC_LINK}
197

    
198
    if settings.IGNORE_FLAVOR_DISK_SIZES:
199
        if image.backend_id.find("windows") >= 0:
200
            sz = 14000
201
        else:
202
            sz = 4000
203
    else:
204
        sz = flavor.disk * 1024
205

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

    
235
    return rapi.CreateInstance(**kw)
236

    
237

    
238
def delete_instance(vm):
239
    start_action(vm, 'DESTROY')
240
    rapi.DeleteInstance(vm.backend_id, dry_run=settings.TEST)
241
    vm.nics.all().delete()
242

    
243

    
244
def reboot_instance(vm, reboot_type):
245
    assert reboot_type in ('soft', 'hard')
246
    rapi.RebootInstance(vm.backend_id, reboot_type, dry_run=settings.TEST)
247

    
248

    
249
def startup_instance(vm):
250
    start_action(vm, 'START')
251
    rapi.StartupInstance(vm.backend_id, dry_run=settings.TEST)
252

    
253

    
254
def shutdown_instance(vm):
255
    start_action(vm, 'STOP')
256
    rapi.ShutdownInstance(vm.backend_id, dry_run=settings.TEST)
257

    
258

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

    
282
def request_status_update(vm):
283
    return rapi.GetInstanceInfo(vm.backend_id)
284

    
285

    
286
def get_job_status(jobid):
287
    return rapi.GetJobStatus(jobid)
288

    
289

    
290
def update_status(vm, status):
291
    utils.update_state(vm, status)
292

    
293
def create_network_link():
294
    try:
295
        last = NetworkLink.objects.order_by('-index')[0]
296
        index = last.index + 1
297
    except IndexError:
298
        index = 1
299

    
300
    if index <= settings.GANETI_MAX_LINK_NUMBER:
301
        name = '%s%d' % (settings.GANETI_LINK_PREFIX, index)
302
        return NetworkLink.objects.create(index=index, name=name,
303
                                            available=True)
304
    return None     # All link slots are filled
305

    
306
@transaction.commit_on_success
307
def create_network(name, owner):
308
    try:
309
        link = NetworkLink.objects.filter(available=True)[0]
310
    except IndexError:
311
        link = create_network_link()
312
        if not link:
313
            return None
314

    
315
    network = Network.objects.create(
316
        name=name,
317
        owner=owner,
318
        state='ACTIVE',
319
        link=link)
320

    
321
    link.network = network
322
    link.available = False
323
    link.save()
324

    
325
    return network
326

    
327
@transaction.commit_on_success
328
def delete_network(net):
329
    link = net.link
330
    if link.name != settings.GANETI_NULL_LINK:
331
        link.available = True
332
        link.network = None
333
        link.save()
334

    
335
    for vm in net.machines.all():
336
        disconnect_from_network(vm, net)
337
        vm.save()
338
    net.state = 'DELETED'
339
    net.save()
340

    
341
def connect_to_network(vm, net):
342
    nic = {'mode': 'bridged', 'link': net.link.name}
343
    rapi.ModifyInstance(vm.backend_id,
344
        nics=[('add', nic)],
345
        dry_run=settings.TEST)
346

    
347
def disconnect_from_network(vm, net):
348
    nics = vm.nics.filter(network__public=False).order_by('index')
349
    new_nics = [nic for nic in nics if nic.network != net]
350
    if new_nics == nics:
351
        return      # Nothing to remove
352
    ops = [('remove', {})]
353
    for i, nic in enumerate(new_nics):
354
        ops.append((i + 1, {
355
            'mode': 'bridged',
356
            'link': nic.network.link.name}))
357
    rapi.ModifyInstance(vm.backend_id, nics=ops, dry_run=settings.TEST)
358

    
359
def set_firewall_profile(vm, profile):
360
    try:
361
        tag = _firewall_tags[profile]
362
    except KeyError:
363
        raise ValueError("Unsopported Firewall Profile: %s" % profile)
364

    
365
    # Delete all firewall tags
366
    for t in _firewall_tags.values():
367
        rapi.DeleteInstanceTags(vm.backend_id, [t], dry_run=settings.TEST)
368

    
369
    rapi.AddInstanceTags(vm.backend_id, [tag], dry_run=settings.TEST)
370
    
371
    # XXX NOP ModifyInstance call to force process_net_status to run
372
    # on the dispatcher
373
    rapi.ModifyInstance(vm.backend_id,
374
                        os_name=settings.GANETI_CREATEINSTANCE_KWARGS['os'])
375