Statistics
| Branch: | Tag: | Revision:

root / logic / backend.py @ 5eedb0e4

History | View | Annotate | Download (12 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
def start_action(vm, action):
139
    """Update the state of a VM when a new action is initiated."""
140
    if not action in [x[0] for x in VirtualMachine.ACTIONS]:
141
        raise VirtualMachine.InvalidActionError(action)
142

    
143
    # No actions to deleted and no actions beside destroy to suspended VMs
144
    if vm.deleted:
145
        raise VirtualMachine.DeletedError
146

    
147
    # No actions to machines being built. They may be destroyed, however.
148
    if vm.operstate == 'BUILD' and action != 'DESTROY':
149
        raise VirtualMachine.BuildingError
150

    
151
    vm.action = action
152
    vm.backendjobid = None
153
    vm.backendopcode = None
154
    vm.backendjobstatus = None
155
    vm.backendlogmsg = None
156

    
157
    # Update the relevant flags if the VM is being suspended or destroyed.
158
    # Do not set the deleted flag here, see ticket #721.
159
    #
160
    # The deleted flag is set asynchronously, when an OP_INSTANCE_REMOVE
161
    # completes successfully. Hence, a server may be visible for some time
162
    # after a DELETE /servers/id returns HTTP 204.
163
    #
164
    if action == "DESTROY":
165
        # vm.deleted = True
166
        pass
167
    elif action == "SUSPEND":
168
        vm.suspended = True
169
    elif action == "START":
170
        vm.suspended = False
171
    vm.save()
172

    
173

    
174
def create_instance(vm, flavor, image, password):
175

    
176
    nic = {'ip': 'pool', 'mode': 'routed', 'link': settings.GANETI_PUBLIC_LINK}
177

    
178
    if settings.IGNORE_FLAVOR_DISK_SIZES:
179
        if image.backend_id.find("windows") >= 0:
180
            sz = 14000
181
        else:
182
            sz = 4000
183
    else:
184
        sz = flavor.disk * 1024
185

    
186
    # Handle arguments to CreateInstance() as a dictionary,
187
    # initialize it based on a deployment-specific value.
188
    # This enables the administrator to override deployment-specific
189
    # arguments, such as the disk templatei to use, name of os provider
190
    # and hypervisor-specific parameters at will (see Synnefo #785, #835).
191
    #
192
    kw = settings.GANETI_CREATEINSTANCE_KWARGS
193
    kw['mode'] = 'create'
194
    kw['name'] = vm.backend_id
195
    # Defined in settings.GANETI_CREATE_INSTANCE_KWARGS
196
    # kw['disk_template'] = settings.GANETI_DISK_TEMPLATE
197
    kw['disks'] = [{"size": sz}]
198
    kw['nics'] = [nic]
199
    # Defined in settings.GANETI_CREATE_INSTANCE_KWARGS
200
    # kw['os'] = settings.GANETI_OS_PROVIDER
201
    kw['ip_check'] = False
202
    kw['name_check'] = False
203
    # Do not specific a node explicitly, have
204
    # Ganeti use an iallocator instead
205
    #
206
    # kw['pnode']=rapi.GetNodes()[0]
207
    kw['dry_run'] = settings.TEST
208
    kw['beparams'] = dict(auto_balance=True, vcpus=flavor.cpu,
209
                          memory=flavor.ram)
210
    kw['osparams'] = dict(img_id=image.backend_id, img_passwd=password,
211
                         img_format=image.format)
212
    # Defined in settings.GANETI_CREATE_INSTANCE_KWARGS
213
    # kw['hvparams'] = dict(serial_console=False)
214

    
215
    return rapi.CreateInstance(**kw)
216

    
217

    
218
def delete_instance(vm):
219
    start_action(vm, 'DESTROY')
220
    rapi.DeleteInstance(vm.backend_id, dry_run=settings.TEST)
221
    vm.nics.all().delete()
222

    
223

    
224
def reboot_instance(vm, reboot_type):
225
    assert reboot_type in ('soft', 'hard')
226
    rapi.RebootInstance(vm.backend_id, reboot_type, dry_run=settings.TEST)
227

    
228

    
229
def startup_instance(vm):
230
    start_action(vm, 'START')
231
    rapi.StartupInstance(vm.backend_id, dry_run=settings.TEST)
232

    
233

    
234
def shutdown_instance(vm):
235
    start_action(vm, 'STOP')
236
    rapi.ShutdownInstance(vm.backend_id, dry_run=settings.TEST)
237

    
238

    
239
def get_instance_console(vm):
240
    # RAPI GetInstanceConsole() returns endpoints to the vnc_bind_address,
241
    # which is a cluster-wide setting, either 0.0.0.0 or 127.0.0.1, and pretty
242
    # useless (see #783).
243
    #
244
    # Until this is fixed on the Ganeti side, construct a console info reply
245
    # directly.
246
    # 
247
    # WARNING: This assumes that VNC runs on port network_port on
248
    #          the instance's primary node, and is probably
249
    #          hypervisor-specific.
250
    #
251
    console = {}
252
    console['kind'] = 'vnc'
253
    i = rapi.GetInstance(vm.backend_id)
254
    if i['hvparams']['serial_console']:
255
        raise Exception("hv parameter serial_console cannot be true")
256
    console['host'] = i['pnode']
257
    console['port'] = i['network_port']
258
    
259
    return console
260
    # return rapi.GetInstanceConsole(vm.backend_id)
261

    
262
def request_status_update(vm):
263
    return rapi.GetInstanceInfo(vm.backend_id)
264

    
265

    
266
def get_job_status(jobid):
267
    return rapi.GetJobStatus(jobid)
268

    
269

    
270
def update_status(vm, status):
271
    utils.update_state(vm, status)
272

    
273
def create_network_link():
274
    try:
275
        last = NetworkLink.objects.order_by('-index')[0]
276
        index = last.index + 1
277
    except IndexError:
278
        index = 1
279

    
280
    if index <= settings.GANETI_MAX_LINK_NUMBER:
281
        name = '%s%d' % (settings.GANETI_LINK_PREFIX, index)
282
        return NetworkLink.objects.create(index=index, name=name,
283
                                            available=True)
284
    return None     # All link slots are filled
285

    
286
@transaction.commit_on_success
287
def create_network(name, owner):
288
    try:
289
        link = NetworkLink.objects.filter(available=True)[0]
290
    except IndexError:
291
        link = create_network_link()
292
        if not link:
293
            return None
294

    
295
    network = Network.objects.create(
296
        name=name,
297
        owner=owner,
298
        state='ACTIVE',
299
        link=link)
300

    
301
    link.network = network
302
    link.available = False
303
    link.save()
304

    
305
    return network
306

    
307
@transaction.commit_on_success
308
def delete_network(net):
309
    link = net.link
310
    if link.name != settings.GANETI_NULL_LINK:
311
        link.available = True
312
        link.network = None
313
        link.save()
314

    
315
    for vm in net.machines.all():
316
        disconnect_from_network(vm, net)
317
        vm.save()
318
    net.state = 'DELETED'
319
    net.save()
320

    
321
def connect_to_network(vm, net):
322
    nic = {'mode': 'bridged', 'link': net.link.name}
323
    rapi.ModifyInstance(vm.backend_id,
324
        nics=[('add', nic)],
325
        dry_run=settings.TEST)
326

    
327
def disconnect_from_network(vm, net):
328
    nics = vm.nics.filter(network__public=False).order_by('index')
329
    new_nics = [nic for nic in nics if nic.network != net]
330
    if new_nics == nics:
331
        return      # Nothing to remove
332
    ops = [('remove', {})]
333
    for i, nic in enumerate(new_nics):
334
        ops.append((i + 1, {
335
            'mode': 'bridged',
336
            'link': nic.network.link.name}))
337
    rapi.ModifyInstance(vm.backend_id, nics=ops, dry_run=settings.TEST)
338

    
339
def set_firewall_profile(vm, profile):
340
    try:
341
        tag = _firewall_tags[profile]
342
    except KeyError:
343
        raise ValueError("Unsopported Firewall Profile: %s" % profile)
344

    
345
    # Delete all firewall tags
346
    for t in _firewall_tags.values():
347
        rapi.DeleteInstanceTags(vm.backend_id, [t], dry_run=settings.TEST)
348

    
349
    rapi.AddInstanceTags(vm.backend_id, [tag], dry_run=settings.TEST)
350
    
351
    # XXX NOP ModifyInstance call to force process_net_status to run
352
    # on the dispatcher
353
    rapi.ModifyInstance(vm.backend_id,
354
                        os_name=settings.GANETI_CREATEINSTANCE_KWARGS['os'])
355