Statistics
| Branch: | Tag: | Revision:

root / logic / backend.py @ 71099804

History | View | Annotate | Download (11 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
def process_op_status(vm, jobid, opcode, status, logmsg):
54
    """Process a job progress notification from the backend
55

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

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

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

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

    
77
    # Special case: if OP_INSTANCE_CREATE fails --> ERROR
78
    if status in ('canceled', 'error') and opcode == 'OP_INSTANCE_CREATE':
79
        utils.update_state(vm, 'ERROR')
80
    # Any other notification of failure leaves the operating state unchanged
81

    
82
    vm.save()
83

    
84

    
85
@transaction.commit_on_success
86
def process_net_status(vm, nics):
87
    """Process a net status notification from the backend
88

89
    Process an incoming message from the Ganeti backend,
90
    detailing the NIC configuration of a VM instance.
91

92
    Update the state of the VM in the DB accordingly.
93
    """
94

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

    
127

    
128
def start_action(vm, action):
129
    """Update the state of a VM when a new action is initiated."""
130
    if not action in [x[0] for x in VirtualMachine.ACTIONS]:
131
        raise VirtualMachine.InvalidActionError(action)
132

    
133
    # No actions to deleted and no actions beside destroy to suspended VMs
134
    if vm.deleted:
135
        raise VirtualMachine.DeletedError
136

    
137
    # No actions to machines being built. They may be destroyed, however.
138
    if vm.operstate == 'BUILD' and action != 'DESTROY':
139
        raise VirtualMachine.BuildingError
140

    
141
    vm.action = action
142
    vm.backendjobid = None
143
    vm.backendopcode = None
144
    vm.backendjobstatus = None
145
    vm.backendlogmsg = None
146

    
147
    # Update the relevant flags if the VM is being suspended or destroyed.
148
    # Do not set the deleted flag here, see ticket #721.
149
    #
150
    # The deleted flag is set asynchronously, when an OP_INSTANCE_REMOVE
151
    # completes successfully. Hence, a server may be visible for some time
152
    # after a DELETE /servers/id returns HTTP 204.
153
    #
154
    if action == "DESTROY":
155
        # vm.deleted = True
156
        pass
157
    elif action == "SUSPEND":
158
        vm.suspended = True
159
    elif action == "START":
160
        vm.suspended = False
161
    vm.save()
162

    
163

    
164
def create_instance(vm, flavor, image, password):
165

    
166
    nic = {'ip': 'pool', 'mode': 'routed', 'link': settings.GANETI_PUBLIC_LINK}
167

    
168
    if settings.IGNORE_FLAVOR_DISK_SIZES:
169
        if image.backend_id.find("windows") >= 0:
170
            sz = 14000
171
        else:
172
            sz = 4000
173
    else:
174
        sz = flavor.disk * 1024
175

    
176
    return rapi.CreateInstance(
177
        mode='create',
178
        name=vm.backend_id,
179
        disk_template='plain',
180
        disks=[{"size": sz}],     #FIXME: Always ask for a 4GB disk for now
181
        nics=[nic],
182
        os=settings.GANETI_OS_PROVIDER,
183
        ip_check=False,
184
        name_check=False,
185
        # Do not specific a node explicitly, have
186
        # Ganeti use an iallocator instead
187
        #
188
        # pnode=rapi.GetNodes()[0],
189
        dry_run=settings.TEST,
190
        beparams=dict(auto_balance=True, vcpus=flavor.cpu, memory=flavor.ram),
191
        osparams=dict(img_id=image.backend_id, img_passwd=password,
192
                      img_format=image.format))
193

    
194

    
195
def delete_instance(vm):
196
    start_action(vm, 'DESTROY')
197
    rapi.DeleteInstance(vm.backend_id, dry_run=settings.TEST)
198
    vm.nics.all().delete()
199

    
200

    
201
def reboot_instance(vm, reboot_type):
202
    assert reboot_type in ('soft', 'hard')
203
    rapi.RebootInstance(vm.backend_id, reboot_type, dry_run=settings.TEST)
204

    
205

    
206
def startup_instance(vm):
207
    start_action(vm, 'START')
208
    rapi.StartupInstance(vm.backend_id, dry_run=settings.TEST)
209

    
210

    
211
def shutdown_instance(vm):
212
    start_action(vm, 'STOP')
213
    rapi.ShutdownInstance(vm.backend_id, dry_run=settings.TEST)
214

    
215

    
216
def get_instance_console(vm):
217
    # RAPI GetInstanceConsole() returns endpoints to the vnc_bind_address,
218
    # which is a cluster-wide setting, either 0.0.0.0 or 127.0.0.1, and pretty
219
    # useless (see #783).
220
    #
221
    # Until this is fixed on the Ganeti side, construct a console info reply
222
    # directly.
223
    # 
224
    # WARNING: This assumes that VNC runs on port network_port on
225
    #          the instance's primary node, and is probably
226
    #          hypervisor-specific.
227
    #
228
    console = {}
229
    console['kind'] = 'vnc'
230
    i = rapi.GetInstance(vm.backend_id)
231
    if i['hvparams']['serial_console']:
232
        raise Exception("hv parameter serial_console cannot be true")
233
    console['host'] = i['pnode']
234
    console['port'] = i['network_port']
235
    
236
    return console
237
    # return rapi.GetInstanceConsole(vm.backend_id)
238

    
239
def request_status_update(vm):
240
    return rapi.GetInstanceInfo(vm.backend_id)
241

    
242

    
243
def get_job_status(jobid):
244
    return rapi.GetJobStatus(jobid)
245

    
246

    
247
def update_status(vm, status):
248
    utils.update_state(vm, status)
249

    
250
def create_network_link():
251
    try:
252
        last = NetworkLink.objects.order_by('-index')[0]
253
        index = last.index + 1
254
    except IndexError:
255
        index = 1
256

    
257
    if index <= settings.GANETI_MAX_LINK_NUMBER:
258
        name = '%s%d' % (settings.GANETI_LINK_PREFIX, index)
259
        return NetworkLink.objects.create(index=index, name=name,
260
                                            available=True)
261
    return None     # All link slots are filled
262

    
263
@transaction.commit_on_success
264
def create_network(name, owner):
265
    try:
266
        link = NetworkLink.objects.filter(available=True)[0]
267
    except IndexError:
268
        link = create_network_link()
269
        if not link:
270
            return None
271

    
272
    network = Network.objects.create(
273
        name=name,
274
        owner=owner,
275
        state='ACTIVE',
276
        link=link)
277

    
278
    link.network = network
279
    link.available = False
280
    link.save()
281

    
282
    return network
283

    
284
@transaction.commit_on_success
285
def delete_network(net):
286
    link = net.link
287
    if link.name != settings.GANETI_NULL_LINK:
288
        link.available = True
289
        link.network = None
290
        link.save()
291

    
292
    for vm in net.machines.all():
293
        disconnect_from_network(vm, net)
294
        vm.save()
295
    net.state = 'DELETED'
296
    net.save()
297

    
298
def connect_to_network(vm, net):
299
    nic = {'mode': 'bridged', 'link': net.link.name}
300
    rapi.ModifyInstance(vm.backend_id,
301
        nics=[('add', nic)],
302
        dry_run=settings.TEST)
303

    
304
def disconnect_from_network(vm, net):
305
    nics = vm.nics.filter(network__public=False).order_by('index')
306
    new_nics = [nic for nic in nics if nic.network != net]
307
    if new_nics == nics:
308
        return      # Nothing to remove
309
    ops = [('remove', {})]
310
    for i, nic in enumerate(new_nics):
311
        ops.append((i + 1, {
312
            'mode': 'bridged',
313
            'link': nic.network.link.name}))
314
    rapi.ModifyInstance(vm.backend_id, nics=ops, dry_run=settings.TEST)
315

    
316
def set_firewall_profile(vm, profile):
317
    try:
318
        tag = _firewall_tags[profile]
319
    except KeyError:
320
        raise ValueError("Unsopported Firewall Profile: %s" % profile)
321

    
322
    # Delete all firewall tags
323
    for t in _firewall_tags.values():
324
        rapi.DeleteInstanceTags(vm.backend_id, [t], dry_run=settings.TEST)
325

    
326
    rapi.AddInstanceTags(vm.backend_id, [tag], dry_run=settings.TEST)
327
    
328
    # XXX NOP ModifyInstance call to force process_net_status to run
329
    # on the dispatcher
330
    rapi.ModifyInstance(vm.backend_id, os_name=settings.GANETI_OS_PROVIDER)