Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / logic / servers.py @ ff863a80

History | View | Annotate | Download (18.6 kB)

1
import logging
2

    
3
from socket import getfqdn
4
from functools import wraps
5
from django import dispatch
6
from django.db import transaction
7
from django.utils import simplejson as json
8

    
9
from snf_django.lib.api import faults
10
from django.conf import settings
11
from synnefo import quotas
12
from synnefo.api import util
13
from synnefo.logic import backend
14
from synnefo.logic.backend_allocator import BackendAllocator
15
from synnefo.db.models import (NetworkInterface, VirtualMachine,
16
                               VirtualMachineMetadata, IPAddress,
17
                               IPAddressLog)
18
from synnefo.db import query as db_query, pools
19

    
20
from vncauthproxy.client import request_forwarding as request_vnc_forwarding
21

    
22
log = logging.getLogger(__name__)
23

    
24
# server creation signal
25
server_created = dispatch.Signal(providing_args=["created_vm_params"])
26

    
27

    
28
def validate_server_action(vm, action):
29
    if vm.deleted:
30
        raise faults.BadRequest("Server '%s' has been deleted." % vm.id)
31

    
32
    # Destroyin a server should always be permitted
33
    if action == "DESTROY":
34
        return
35

    
36
    # Check that there is no pending action
37
    pending_action = vm.task
38
    if pending_action:
39
        if pending_action == "BUILD":
40
            raise faults.BuildInProgress("Server '%s' is being build." % vm.id)
41
        raise faults.BadRequest("Can not perform '%s' action while there is a"
42
                                " pending '%s'." % (action, pending_action))
43

    
44
    # Check if action can be performed to VM's operstate
45
    operstate = vm.operstate
46
    if operstate == "BUILD" and action != "BUILD":
47
        raise faults.BuildInProgress("Server '%s' is being build." % vm.id)
48
    elif (action == "START" and operstate != "STOPPED") or\
49
         (action == "STOP" and operstate != "STARTED") or\
50
         (action == "RESIZE" and operstate != "STOPPED") or\
51
         (action in ["CONNECT", "DISCONNECT"] and operstate != "STOPPED"
52
          and not settings.GANETI_USE_HOTPLUG):
53
        raise faults.BadRequest("Can not perform '%s' action while server is"
54
                                " in '%s' state." % (action, operstate))
55
    return
56

    
57

    
58
def server_command(action):
59
    """Handle execution of a server action.
60

61
    Helper function to validate and execute a server action, handle quota
62
    commission and update the 'task' of the VM in the DB.
63

64
    1) Check if action can be performed. If it can, then there must be no
65
       pending task (with the exception of DESTROY).
66
    2) Handle previous commission if unresolved:
67
       * If it is not pending and it to accept, then accept
68
       * If it is not pending and to reject or is pending then reject it. Since
69
       the action can be performed only if there is no pending task, then there
70
       can be no pending commission. The exception is DESTROY, but in this case
71
       the commission can safely be rejected, and the dispatcher will generate
72
       the correct ones!
73
    3) Issue new commission and associate it with the VM. Also clear the task.
74
    4) Send job to ganeti
75
    5) Update task and commit
76
    """
77
    def decorator(func):
78
        @wraps(func)
79
        @transaction.commit_on_success
80
        def wrapper(vm, *args, **kwargs):
81
            user_id = vm.userid
82
            validate_server_action(vm, action)
83
            vm.action = action
84

    
85
            commission_name = "client: api, resource: %s" % vm
86
            quotas.handle_resource_commission(vm, action=action,
87
                                              commission_name=commission_name)
88
            vm.save()
89

    
90
            # XXX: Special case for server creation!
91
            if action == "BUILD":
92
                # Perform a commit, because the VirtualMachine must be saved to
93
                # DB before the OP_INSTANCE_CREATE job in enqueued in Ganeti.
94
                # Otherwise, messages will arrive from snf-dispatcher about
95
                # this instance, before the VM is stored in DB.
96
                transaction.commit()
97
                # After committing the locks are released. Refetch the instance
98
                # to guarantee x-lock.
99
                vm = VirtualMachine.objects.select_for_update().get(id=vm.id)
100

    
101
            # Send the job to Ganeti and get the associated jobID
102
            try:
103
                job_id = func(vm, *args, **kwargs)
104
            except Exception as e:
105
                if vm.serial is not None:
106
                    # Since the job never reached Ganeti, reject the commission
107
                    log.debug("Rejecting commission: '%s', could not perform"
108
                              " action '%s': %s" % (vm.serial,  action, e))
109
                    transaction.rollback()
110
                    quotas.reject_serial(vm.serial)
111
                    transaction.commit()
112
                raise
113

    
114
            if action == "BUILD" and vm.serial is not None:
115
                # XXX: Special case for server creation: we must accept the
116
                # commission because the VM has been stored in DB. Also, if
117
                # communication with Ganeti fails, the job will never reach
118
                # Ganeti, and the commission will never be resolved.
119
                quotas.accept_serial(vm.serial)
120

    
121
            log.info("user: %s, vm: %s, action: %s, job_id: %s, serial: %s",
122
                     user_id, vm.id, action, job_id, vm.serial)
123

    
124
            # store the new task in the VM
125
            if job_id is not None:
126
                vm.task = action
127
                vm.task_job_id = job_id
128
            vm.save()
129

    
130
            return vm
131
        return wrapper
132
    return decorator
133

    
134

    
135
@transaction.commit_on_success
136
def create(userid, name, password, flavor, image, metadata={},
137
           personality=[], private_networks=None, floating_ips=None,
138
           use_backend=None):
139
    if use_backend is None:
140
        # Allocate server to a Ganeti backend
141
        use_backend = allocate_new_server(userid, flavor)
142

    
143
    if private_networks is None:
144
        private_networks = []
145
    if floating_ips is None:
146
        floating_ips = []
147

    
148
    # Fix flavor for archipelago
149
    disk_template, provider = util.get_flavor_provider(flavor)
150
    if provider:
151
        flavor.disk_template = disk_template
152
        flavor.disk_provider = provider
153
        flavor.disk_origin = None
154
        if provider == 'vlmc':
155
            flavor.disk_origin = image['checksum']
156
            image['backend_id'] = 'null'
157
    else:
158
        flavor.disk_provider = None
159

    
160
    # We must save the VM instance now, so that it gets a valid
161
    # vm.backend_vm_id.
162
    vm = VirtualMachine.objects.create(name=name,
163
                                       backend=use_backend,
164
                                       userid=userid,
165
                                       imageid=image["id"],
166
                                       flavor=flavor,
167
                                       operstate="BUILD")
168
    log.info("Created entry in DB for VM '%s'", vm)
169

    
170
    nics = create_instance_nics(vm, userid, private_networks, floating_ips)
171

    
172
    for key, val in metadata.items():
173
        VirtualMachineMetadata.objects.create(
174
            meta_key=key,
175
            meta_value=val,
176
            vm=vm)
177

    
178
    # Create the server in Ganeti.
179
    vm = create_server(vm, nics, flavor, image, personality, password)
180

    
181
    return vm
182

    
183

    
184
@transaction.commit_on_success
185
def allocate_new_server(userid, flavor):
186
    """Allocate a new server to a Ganeti backend.
187

188
    Allocation is performed based on the owner of the server and the specified
189
    flavor. Also, backends that do not have a public IPv4 address are excluded
190
    from server allocation.
191

192
    This function runs inside a transaction, because after allocating the
193
    instance a commit must be performed in order to release all locks.
194

195
    """
196
    backend_allocator = BackendAllocator()
197
    use_backend = backend_allocator.allocate(userid, flavor)
198
    if use_backend is None:
199
        log.error("No available backend for VM with flavor %s", flavor)
200
        raise faults.ServiceUnavailable("No available backends")
201
    return use_backend
202

    
203

    
204
@server_command("BUILD")
205
def create_server(vm, nics, flavor, image, personality, password):
206
    # dispatch server created signal needed to trigger the 'vmapi', which
207
    # enriches the vm object with the 'config_url' attribute which must be
208
    # passed to the Ganeti job.
209
    server_created.send(sender=vm, created_vm_params={
210
        'img_id': image['backend_id'],
211
        'img_passwd': password,
212
        'img_format': str(image['format']),
213
        'img_personality': json.dumps(personality),
214
        'img_properties': json.dumps(image['metadata']),
215
    })
216
    # send job to Ganeti
217
    try:
218
        jobID = backend.create_instance(vm, nics, flavor, image)
219
    except:
220
        log.exception("Failed create instance '%s'", vm)
221
        jobID = None
222
        vm.operstate = "ERROR"
223
        vm.backendlogmsg = "Failed to send job to Ganeti."
224
        vm.save()
225
        vm.nics.all().update(state="ERROR")
226

    
227
    # At this point the job is enqueued in the Ganeti backend
228
    vm.backendjobid = jobID
229
    vm.save()
230
    log.info("User %s created VM %s, NICs %s, Backend %s, JobID %s",
231
             vm.userid, vm, nics, backend, str(jobID))
232

    
233
    return jobID
234

    
235

    
236
def create_instance_nics(vm, userid, private_networks=[], floating_ips=[]):
237
    """Create NICs for VirtualMachine.
238

239
    Helper function for allocating IP addresses and creating NICs in the DB
240
    for a VirtualMachine. Created NICs are the combination of the default
241
    network policy (defined by administration settings) and the private
242
    networks defined by the user.
243

244
    """
245
    nics = []
246
    for network_id in settings.DEFAULT_INSTANCE_NETWORKS:
247
        if network_id == "SNF:ANY_PUBLIC":
248
            ipaddress = util.allocate_public_ip(userid=userid,
249
                                                backend=vm.backend)
250
            nic, ipaddress = create_nic(vm, ipaddress=ipaddress)
251
        else:
252
            try:
253
                network = util.get_network(network_id, userid,
254
                                           non_deleted=True)
255
            except faults.ItemNotFound:
256
                msg = "Invalid configuration. Setting"\
257
                      " 'DEFAULT_INSTANCE_NETWORKS' contains invalid"\
258
                      " network '%s'" % network_id
259
                log.error(msg)
260
                raise faults.InternalServerError(msg)
261
            nic, ipaddress = create_nic(vm, network=network)
262
        nics.append(nic)
263
    for address in floating_ips:
264
        floating_ip = util.get_floating_ip_by_address(vm.userid, address,
265
                                                      for_update=True)
266
        nic, ipaddress = create_nic(vm, ipaddress=floating_ip)
267
        nics.append(nic)
268
    for network_id in private_networks:
269
        network = util.get_network(network_id, userid, non_deleted=True)
270
        if network.public:
271
            raise faults.Forbidden("Can not connect to public network")
272
        nic, ipaddress = create_nic(vm, network=network)
273
        nics.append(nic)
274
    for index, nic in enumerate(nics):
275
        nic.index = index
276
        nic.save()
277
    return nics
278

    
279

    
280
def create_nic(vm, network=None, ipaddress=None, address=None, name=None):
281
    """Helper functions for create NIC objects.
282

283
    Create a NetworkInterface connecting a VirtualMachine to a network with the
284
    IPAddress specified. If no 'ipaddress' is passed and the network has an
285
    IPv4 subnet, then an IPv4 address will be automatically be allocated.
286

287
    """
288
    userid = vm.userid
289

    
290
    if ipaddress is None:
291
        if network.subnets.filter(ipversion=4).exists():
292
            try:
293
                ipaddress = util.allocate_ip(network, userid=userid,
294
                                             address=address)
295
            except pools.ValueNotAvailable:
296
                raise faults.Conflict("Address '%s' is not available." %
297
                                      address)
298

    
299
    if ipaddress is not None and ipaddress.nic is not None:
300
        raise faults.Conflict("IP address '%s' already in use" %
301
                              ipaddress.address)
302

    
303
    if network is None:
304
        network = ipaddress.network
305
    elif network.state != 'ACTIVE':
306
        # TODO: What if is in settings ?
307
        raise faults.BuildInProgress('Network not active yet')
308

    
309
    #device_owner = "router" if vm.router else "vm"
310
    device_owner = "vm"
311
    nic = NetworkInterface.objects.create(machine=vm, network=network,
312
                                          state="BUILD",
313
                                          userid=vm.userid,
314
                                          device_owner=device_owner,
315
                                          name=name)
316
    log.debug("Created NIC %s with IP %s", nic, ipaddress)
317
    if ipaddress is not None:
318
        ipaddress.nic = nic
319
        ipaddress.save()
320

    
321
        if ipaddress.network.public:
322
            ip_log = IPAddressLog.objects.create(
323
                server_id=vm.id,
324
                network_id=ipaddress.network_id,
325
                address=ipaddress.address,
326
                active=True)
327
            log.debug("Created IP log entry %s", ip_log)
328

    
329
    return nic, ipaddress
330

    
331

    
332
@server_command("DESTROY")
333
def destroy(vm):
334
    log.info("Deleting VM %s", vm)
335
    return backend.delete_instance(vm)
336

    
337

    
338
@server_command("START")
339
def start(vm):
340
    log.info("Starting VM %s", vm)
341
    return backend.startup_instance(vm)
342

    
343

    
344
@server_command("STOP")
345
def stop(vm):
346
    log.info("Stopping VM %s", vm)
347
    return backend.shutdown_instance(vm)
348

    
349

    
350
@server_command("REBOOT")
351
def reboot(vm, reboot_type):
352
    if reboot_type not in ("SOFT", "HARD"):
353
        raise faults.BadRequest("Malformed request. Invalid reboot"
354
                                " type %s" % reboot_type)
355
    log.info("Rebooting VM %s. Type %s", vm, reboot_type)
356

    
357
    return backend.reboot_instance(vm, reboot_type.lower())
358

    
359

    
360
@server_command("RESIZE")
361
def resize(vm, flavor):
362
    old_flavor = vm.flavor
363
    # User requested the same flavor
364
    if old_flavor.id == flavor.id:
365
        raise faults.BadRequest("Server '%s' flavor is already '%s'."
366
                                % (vm, flavor))
367
        return None
368
    # Check that resize can be performed
369
    if old_flavor.disk != flavor.disk:
370
        raise faults.BadRequest("Can not resize instance disk.")
371
    if old_flavor.disk_template != flavor.disk_template:
372
        raise faults.BadRequest("Can not change instance disk template.")
373

    
374
    log.info("Resizing VM from flavor '%s' to '%s", old_flavor, flavor)
375
    commission_info = {"cyclades.cpu": flavor.cpu - old_flavor.cpu,
376
                       "cyclades.ram": 1048576 * (flavor.ram - old_flavor.ram)}
377
    # Save serial to VM, since it is needed by server_command decorator
378
    vm.serial = quotas.issue_commission(user=vm.userid,
379
                                        source=quotas.DEFAULT_SOURCE,
380
                                        provisions=commission_info,
381
                                        name="resource: %s. resize" % vm)
382
    return backend.resize_instance(vm, vcpus=flavor.cpu, memory=flavor.ram)
383

    
384

    
385
@server_command("SET_FIREWALL_PROFILE")
386
def set_firewall_profile(vm, profile, nic):
387
    log.info("Setting VM %s, NIC %s, firewall %s", vm, nic, profile)
388

    
389
    if profile not in [x[0] for x in NetworkInterface.FIREWALL_PROFILES]:
390
        raise faults.BadRequest("Unsupported firewall profile")
391
    backend.set_firewall_profile(vm, profile=profile, nic=nic)
392
    return None
393

    
394

    
395
@server_command("CONNECT")
396
def connect(vm, network, port=None):
397
    if port is None:
398
        nic, ipaddress = create_nic(vm, network)
399
    else:
400
        nic = port
401
        ipaddress = port.ips.all()[0]
402

    
403
    log.info("Creating NIC %s with IPAddress %s", nic, ipaddress)
404

    
405
    return backend.connect_to_network(vm, nic)
406

    
407

    
408
@server_command("DISCONNECT")
409
def disconnect(vm, nic):
410
    log.info("Removing NIC %s from VM %s", nic, vm)
411
    return backend.disconnect_from_network(vm, nic)
412

    
413

    
414
def console(vm, console_type):
415
    """Arrange for an OOB console of the specified type
416

417
    This method arranges for an OOB console of the specified type.
418
    Only consoles of type "vnc" are supported for now.
419

420
    It uses a running instance of vncauthproxy to setup proper
421
    VNC forwarding with a random password, then returns the necessary
422
    VNC connection info to the caller.
423

424
    """
425
    log.info("Get console  VM %s, type %s", vm, console_type)
426

    
427
    # Use RAPI to get VNC console information for this instance
428
    if vm.operstate != "STARTED":
429
        raise faults.BadRequest('Server not in ACTIVE state.')
430

    
431
    if settings.TEST:
432
        console_data = {'kind': 'vnc', 'host': 'ganeti_node', 'port': 1000}
433
    else:
434
        console_data = backend.get_instance_console(vm)
435

    
436
    if console_data['kind'] != 'vnc':
437
        message = 'got console of kind %s, not "vnc"' % console_data['kind']
438
        raise faults.ServiceUnavailable(message)
439

    
440
    # Let vncauthproxy decide on the source port.
441
    # The alternative: static allocation, e.g.
442
    # sport = console_data['port'] - 1000
443
    sport = 0
444
    daddr = console_data['host']
445
    dport = console_data['port']
446
    password = util.random_password()
447

    
448
    if settings.TEST:
449
        fwd = {'source_port': 1234, 'status': 'OK'}
450
    else:
451
        fwd = request_vnc_forwarding(sport, daddr, dport, password)
452

    
453
    if fwd['status'] != "OK":
454
        raise faults.ServiceUnavailable('vncauthproxy returned error status')
455

    
456
    # Verify that the VNC server settings haven't changed
457
    if not settings.TEST:
458
        if console_data != backend.get_instance_console(vm):
459
            raise faults.ServiceUnavailable('VNC Server settings changed.')
460

    
461
    console = {
462
        'type': 'vnc',
463
        'host': getfqdn(),
464
        'port': fwd['source_port'],
465
        'password': password}
466

    
467
    return console
468

    
469

    
470
@server_command("CONNECT")
471
def add_floating_ip(vm, address):
472
    # Use for_update, to guarantee that floating IP will only by assigned once
473
    # and that it can not be released will it is being attached!
474
    floating_ip = util.get_floating_ip_by_address(vm.userid, address,
475
                                                  for_update=True)
476
    nic, floating_ip = create_nic(vm, ipaddress=floating_ip)
477
    log.info("Created NIC %s with floating IP %s", nic, floating_ip)
478
    return backend.connect_to_network(vm, nic)
479

    
480

    
481
@server_command("DISCONNECT")
482
def remove_floating_ip(vm, address):
483
    try:
484
        floating_ip = db_query.get_server_floating_ip(server=vm,
485
                                                      address=address,
486
                                                      for_update=True)
487
    except IPAddress.DoesNotExist:
488
        raise faults.BadRequest("Server '%s' has no floating ip with"
489
                                " address '%s'" % (vm, address))
490

    
491
    nic = floating_ip.nic
492
    log.info("Removing NIC %s from VM %s. Floating IP '%s'", str(nic.index),
493
             vm, floating_ip)
494

    
495
    return backend.disconnect_from_network(vm, nic)
496

    
497

    
498
def rename(server, new_name):
499
    """Rename a VirtualMachine."""
500
    old_name = server.name
501
    server.name = new_name
502
    server.save()
503
    log.info("Renamed server '%s' from '%s' to '%s'", server, old_name,
504
             new_name)
505
    return server