Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (32.2 kB)

1
# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or without
4
# modification, are permitted provided that the following conditions
5
# are met:
6
#
7
#   1. Redistributions of source code must retain the above copyright
8
#      notice, this list of conditions and the following disclaimer.
9
#
10
#  2. Redistributions in binary form must reproduce the above copyright
11
#     notice, this list of conditions and the following disclaimer in the
12
#     documentation and/or other materials provided with the distribution.
13
#
14
# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
15
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17
# ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
18
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24
# SUCH DAMAGE.
25
#
26
# The views and conclusions contained in the software and documentation are
27
# those of the authors and should not be interpreted as representing official
28
# policies, either expressed or implied, of GRNET S.A.
29

    
30
import logging
31

    
32
from socket import getfqdn
33
from functools import wraps
34
from django import dispatch
35
from django.db import transaction
36
from django.utils import simplejson as json
37

    
38
from snf_django.lib.api import faults
39
from django.conf import settings
40
from synnefo import quotas
41
from synnefo.api import util
42
from synnefo.logic import backend, ips, utils
43
from synnefo.logic.backend_allocator import BackendAllocator
44
from synnefo.db.models import (NetworkInterface, VirtualMachine,
45
                               VirtualMachineMetadata, IPAddressLog, Network,
46
                               Volume)
47
from vncauthproxy.client import request_forwarding as request_vnc_forwarding
48
from synnefo.logic import rapi
49

    
50
log = logging.getLogger(__name__)
51

    
52
# server creation signal
53
server_created = dispatch.Signal(providing_args=["created_vm_params"])
54

    
55

    
56
def validate_server_action(vm, action):
57
    if vm.deleted:
58
        raise faults.BadRequest("Server '%s' has been deleted." % vm.id)
59

    
60
    # Destroyin a server should always be permitted
61
    if action == "DESTROY":
62
        return
63

    
64
    # Check that there is no pending action
65
    pending_action = vm.task
66
    if pending_action:
67
        if pending_action == "BUILD":
68
            raise faults.BuildInProgress("Server '%s' is being build." % vm.id)
69
        raise faults.BadRequest("Cannot perform '%s' action while there is a"
70
                                " pending '%s'." % (action, pending_action))
71

    
72
    # Check if action can be performed to VM's operstate
73
    operstate = vm.operstate
74
    if operstate == "ERROR":
75
        raise faults.BadRequest("Cannot perform '%s' action while server is"
76
                                " in 'ERROR' state." % action)
77
    elif operstate == "BUILD" and action != "BUILD":
78
        raise faults.BuildInProgress("Server '%s' is being build." % vm.id)
79
    elif (action == "START" and operstate != "STOPPED") or\
80
         (action == "STOP" and operstate != "STARTED") or\
81
         (action == "RESIZE" and operstate != "STOPPED") or\
82
         (action in ["CONNECT", "DISCONNECT"]
83
          and operstate != "STOPPED"
84
          and not settings.GANETI_USE_HOTPLUG) or \
85
         (action in ["ATTACH_VOLUME", "DETACH_VOLUME"]
86
          and operstate != "STOPPED"
87
          and not settings.GANETI_USE_HOTPLUG):
88
        raise faults.BadRequest("Cannot perform '%s' action while server is"
89
                                " in '%s' state." % (action, operstate))
90
    return
91

    
92

    
93
def server_command(action, action_fields=None):
94
    """Handle execution of a server action.
95

96
    Helper function to validate and execute a server action, handle quota
97
    commission and update the 'task' of the VM in the DB.
98

99
    1) Check if action can be performed. If it can, then there must be no
100
       pending task (with the exception of DESTROY).
101
    2) Handle previous commission if unresolved:
102
       * If it is not pending and it to accept, then accept
103
       * If it is not pending and to reject or is pending then reject it. Since
104
       the action can be performed only if there is no pending task, then there
105
       can be no pending commission. The exception is DESTROY, but in this case
106
       the commission can safely be rejected, and the dispatcher will generate
107
       the correct ones!
108
    3) Issue new commission and associate it with the VM. Also clear the task.
109
    4) Send job to ganeti
110
    5) Update task and commit
111
    """
112
    def decorator(func):
113
        @wraps(func)
114
        @transaction.commit_on_success
115
        def wrapper(vm, *args, **kwargs):
116
            user_id = vm.userid
117
            validate_server_action(vm, action)
118
            vm.action = action
119

    
120
            commission_name = "client: api, resource: %s" % vm
121
            quotas.handle_resource_commission(vm, action=action,
122
                                              action_fields=action_fields,
123
                                              commission_name=commission_name)
124
            vm.save()
125

    
126
            # XXX: Special case for server creation!
127
            if action == "BUILD":
128
                # Perform a commit, because the VirtualMachine must be saved to
129
                # DB before the OP_INSTANCE_CREATE job in enqueued in Ganeti.
130
                # Otherwise, messages will arrive from snf-dispatcher about
131
                # this instance, before the VM is stored in DB.
132
                transaction.commit()
133
                # After committing the locks are released. Refetch the instance
134
                # to guarantee x-lock.
135
                vm = VirtualMachine.objects.select_for_update().get(id=vm.id)
136

    
137
            # Send the job to Ganeti and get the associated jobID
138
            try:
139
                job_id = func(vm, *args, **kwargs)
140
            except Exception as e:
141
                if vm.serial is not None:
142
                    # Since the job never reached Ganeti, reject the commission
143
                    log.debug("Rejecting commission: '%s', could not perform"
144
                              " action '%s': %s" % (vm.serial,  action, e))
145
                    transaction.rollback()
146
                    quotas.reject_resource_serial(vm)
147
                    transaction.commit()
148
                raise
149

    
150
            if action == "BUILD" and vm.serial is not None:
151
                # XXX: Special case for server creation: we must accept the
152
                # commission because the VM has been stored in DB. Also, if
153
                # communication with Ganeti fails, the job will never reach
154
                # Ganeti, and the commission will never be resolved.
155
                quotas.accept_resource_serial(vm)
156

    
157
            log.info("user: %s, vm: %s, action: %s, job_id: %s, serial: %s",
158
                     user_id, vm.id, action, job_id, vm.serial)
159

    
160
            # store the new task in the VM
161
            if job_id is not None:
162
                vm.task = action
163
                vm.task_job_id = job_id
164
            vm.save()
165

    
166
            return vm
167
        return wrapper
168
    return decorator
169

    
170

    
171
@transaction.commit_on_success
172
def create(userid, name, password, flavor, image, metadata={},
173
           personality=[], networks=None, use_backend=None):
174

    
175
    # Check that image fits into the disk
176
    if image["size"] > (flavor.disk << 30):
177
        msg = "Flavor's disk size '%s' is smaller than the image's size '%s'"
178
        raise faults.BadRequest(msg % (flavor.disk << 30, image["size"]))
179

    
180
    if use_backend is None:
181
        # Allocate server to a Ganeti backend
182
        use_backend = allocate_new_server(userid, flavor)
183

    
184
    utils.check_name_length(name, VirtualMachine.VIRTUAL_MACHINE_NAME_LENGTH,
185
                            "Server name is too long")
186

    
187
    # Create the ports for the server
188
    ports = create_instance_ports(userid, networks)
189

    
190
    # Fix flavor for archipelago
191
    disk_template, provider = util.get_flavor_provider(flavor)
192
    if provider:
193
        flavor.disk_template = disk_template
194
        flavor.disk_provider = provider
195
        flavor.disk_origin = None
196
        if provider in settings.GANETI_CLONE_PROVIDERS:
197
            flavor.disk_origin = image['checksum']
198
            image['backend_id'] = 'null'
199
    else:
200
        flavor.disk_provider = None
201

    
202
    # We must save the VM instance now, so that it gets a valid
203
    # vm.backend_vm_id.
204
    vm = VirtualMachine.objects.create(name=name,
205
                                       backend=use_backend,
206
                                       userid=userid,
207
                                       imageid=image["id"],
208
                                       flavor=flavor,
209
                                       operstate="BUILD")
210
    log.info("Created entry in DB for VM '%s'", vm)
211

    
212
    # Associate the ports with the server
213
    for index, port in enumerate(ports):
214
        associate_port_with_machine(port, vm)
215
        port.index = index
216
        port.save()
217

    
218
    volumes = create_instance_volumes(vm, flavor, image)
219

    
220
    for key, val in metadata.items():
221
        VirtualMachineMetadata.objects.create(
222
            meta_key=key,
223
            meta_value=val,
224
            vm=vm)
225

    
226
    # Create the server in Ganeti.
227
    vm = create_server(vm, ports, volumes, flavor, image, personality,
228
                       password)
229

    
230
    return vm
231

    
232

    
233
@transaction.commit_on_success
234
def allocate_new_server(userid, flavor):
235
    """Allocate a new server to a Ganeti backend.
236

237
    Allocation is performed based on the owner of the server and the specified
238
    flavor. Also, backends that do not have a public IPv4 address are excluded
239
    from server allocation.
240

241
    This function runs inside a transaction, because after allocating the
242
    instance a commit must be performed in order to release all locks.
243

244
    """
245
    backend_allocator = BackendAllocator()
246
    use_backend = backend_allocator.allocate(userid, flavor)
247
    if use_backend is None:
248
        log.error("No available backend for VM with flavor %s", flavor)
249
        raise faults.ServiceUnavailable("No available backends")
250
    return use_backend
251

    
252

    
253
@server_command("BUILD")
254
def create_server(vm, nics, volumes, flavor, image, personality, password):
255
    # dispatch server created signal needed to trigger the 'vmapi', which
256
    # enriches the vm object with the 'config_url' attribute which must be
257
    # passed to the Ganeti job.
258
    server_created.send(sender=vm, created_vm_params={
259
        'img_id': image['backend_id'],
260
        'img_passwd': password,
261
        'img_format': str(image['format']),
262
        'img_personality': json.dumps(personality),
263
        'img_properties': json.dumps(image['metadata']),
264
    })
265
    # send job to Ganeti
266
    try:
267
        jobID = backend.create_instance(vm, nics, volumes, flavor, image)
268
    except:
269
        log.exception("Failed create instance '%s'", vm)
270
        jobID = None
271
        vm.operstate = "ERROR"
272
        vm.backendlogmsg = "Failed to send job to Ganeti."
273
        vm.save()
274
        vm.nics.all().update(state="ERROR")
275

    
276
    # At this point the job is enqueued in the Ganeti backend
277
    vm.backendopcode = "OP_INSTANCE_CREATE"
278
    vm.backendjobid = jobID
279
    vm.save()
280
    log.info("User %s created VM %s, NICs %s, Backend %s, JobID %s",
281
             vm.userid, vm, nics, vm.backend, str(jobID))
282

    
283
    return jobID
284

    
285

    
286
def create_instance_volumes(vm, flavor, image):
287
    name = "Root volume of server: %s" % vm.id
288
    volume = Volume.objects.create(userid=vm.userid,
289
                                   machine=vm,
290
                                   name=name,
291
                                   size=flavor.disk,
292
                                   source=Volume.SOURCE_IMAGE_PREFIX+image["id"],
293
                                   origin=image["checksum"],
294
                                   status="CREATING")
295

    
296
    volume.save()
297

    
298
    return [volume]
299

    
300

    
301
@server_command("DESTROY")
302
def destroy(vm, shutdown_timeout=None):
303
    # XXX: Workaround for race where OP_INSTANCE_REMOVE starts executing on
304
    # Ganeti before OP_INSTANCE_CREATE. This will be fixed when
305
    # OP_INSTANCE_REMOVE supports the 'depends' request attribute.
306
    if (vm.backendopcode == "OP_INSTANCE_CREATE" and
307
       vm.backendjobstatus not in rapi.JOB_STATUS_FINALIZED and
308
       backend.job_is_still_running(vm) and
309
       not backend.vm_exists_in_backend(vm)):
310
            raise faults.BuildInProgress("Server is being build")
311
    log.info("Deleting VM %s", vm)
312
    return backend.delete_instance(vm, shutdown_timeout=shutdown_timeout)
313

    
314

    
315
@server_command("START")
316
def start(vm):
317
    log.info("Starting VM %s", vm)
318
    return backend.startup_instance(vm)
319

    
320

    
321
@server_command("STOP")
322
def stop(vm, shutdown_timeout=None):
323
    log.info("Stopping VM %s", vm)
324
    return backend.shutdown_instance(vm, shutdown_timeout=shutdown_timeout)
325

    
326

    
327
@server_command("REBOOT")
328
def reboot(vm, reboot_type, shutdown_timeout=None):
329
    if reboot_type not in ("SOFT", "HARD"):
330
        raise faults.BadRequest("Malformed request. Invalid reboot"
331
                                " type %s" % reboot_type)
332
    log.info("Rebooting VM %s. Type %s", vm, reboot_type)
333

    
334
    return backend.reboot_instance(vm, reboot_type.lower(),
335
                                   shutdown_timeout=shutdown_timeout)
336

    
337

    
338
def resize(vm, flavor):
339
    action_fields = {"beparams": {"vcpus": flavor.cpu,
340
                                  "maxmem": flavor.ram}}
341
    comm = server_command("RESIZE", action_fields=action_fields)
342
    return comm(_resize)(vm, flavor)
343

    
344

    
345
def _resize(vm, flavor):
346
    old_flavor = vm.flavor
347
    # User requested the same flavor
348
    if old_flavor.id == flavor.id:
349
        raise faults.BadRequest("Server '%s' flavor is already '%s'."
350
                                % (vm, flavor))
351
    # Check that resize can be performed
352
    if old_flavor.disk != flavor.disk:
353
        raise faults.BadRequest("Cannot resize instance disk.")
354
    if old_flavor.disk_template != flavor.disk_template:
355
        raise faults.BadRequest("Cannot change instance disk template.")
356

    
357
    log.info("Resizing VM from flavor '%s' to '%s", old_flavor, flavor)
358
    return backend.resize_instance(vm, vcpus=flavor.cpu, memory=flavor.ram)
359

    
360

    
361
@server_command("SET_FIREWALL_PROFILE")
362
def set_firewall_profile(vm, profile, nic):
363
    log.info("Setting VM %s, NIC %s, firewall %s", vm, nic, profile)
364

    
365
    if profile not in [x[0] for x in NetworkInterface.FIREWALL_PROFILES]:
366
        raise faults.BadRequest("Unsupported firewall profile")
367
    backend.set_firewall_profile(vm, profile=profile, nic=nic)
368
    return None
369

    
370

    
371
@server_command("CONNECT")
372
def connect(vm, network, port=None):
373
    if port is None:
374
        port = _create_port(vm.userid, network)
375
    associate_port_with_machine(port, vm)
376

    
377
    log.info("Creating NIC %s with IPv4 Address %s", port, port.ipv4_address)
378

    
379
    return backend.connect_to_network(vm, port)
380

    
381

    
382
@server_command("DISCONNECT")
383
def disconnect(vm, nic):
384
    log.info("Removing NIC %s from VM %s", nic, vm)
385
    return backend.disconnect_from_network(vm, nic)
386

    
387

    
388
def console(vm, console_type):
389
    """Arrange for an OOB console of the specified type
390

391
    This method arranges for an OOB console of the specified type.
392
    Only consoles of type "vnc" are supported for now.
393

394
    It uses a running instance of vncauthproxy to setup proper
395
    VNC forwarding with a random password, then returns the necessary
396
    VNC connection info to the caller.
397

398
    """
399
    log.info("Get console  VM %s, type %s", vm, console_type)
400

    
401
    # Use RAPI to get VNC console information for this instance
402
    if vm.operstate != "STARTED":
403
        raise faults.BadRequest('Server not in ACTIVE state.')
404

    
405
    if settings.TEST:
406
        console_data = {'kind': 'vnc', 'host': 'ganeti_node', 'port': 1000}
407
    else:
408
        console_data = backend.get_instance_console(vm)
409

    
410
    if console_data['kind'] != 'vnc':
411
        message = 'got console of kind %s, not "vnc"' % console_data['kind']
412
        raise faults.ServiceUnavailable(message)
413

    
414
    # Let vncauthproxy decide on the source port.
415
    # The alternative: static allocation, e.g.
416
    # sport = console_data['port'] - 1000
417
    sport = 0
418
    daddr = console_data['host']
419
    dport = console_data['port']
420
    password = util.random_password()
421

    
422
    if settings.TEST:
423
        fwd = {'source_port': 1234, 'status': 'OK'}
424
    else:
425
        vnc_extra_opts = settings.CYCLADES_VNCAUTHPROXY_OPTS
426
        fwd = request_vnc_forwarding(sport, daddr, dport, password,
427
                                     **vnc_extra_opts)
428

    
429
    if fwd['status'] != "OK":
430
        raise faults.ServiceUnavailable('vncauthproxy returned error status')
431

    
432
    # Verify that the VNC server settings haven't changed
433
    if not settings.TEST:
434
        if console_data != backend.get_instance_console(vm):
435
            raise faults.ServiceUnavailable('VNC Server settings changed.')
436

    
437
    console = {
438
        'type': 'vnc',
439
        'host': getfqdn(),
440
        'port': fwd['source_port'],
441
        'password': password}
442

    
443
    return console
444

    
445

    
446
def rename(server, new_name):
447
    """Rename a VirtualMachine."""
448
    old_name = server.name
449
    server.name = new_name
450
    server.save()
451
    log.info("Renamed server '%s' from '%s' to '%s'", server, old_name,
452
             new_name)
453
    return server
454

    
455

    
456
@transaction.commit_on_success
457
def create_port(*args, **kwargs):
458
    vm = kwargs.get("machine", None)
459
    if vm is None and len(args) >= 3:
460
        vm = args[2]
461
    if vm is not None:
462
        if vm.nics.count() == settings.GANETI_MAX_NICS_PER_INSTANCE:
463
            raise faults.BadRequest("Maximum ports per server limit reached")
464
    return _create_port(*args, **kwargs)
465

    
466

    
467
def _create_port(userid, network, machine=None, use_ipaddress=None,
468
                 address=None, name="", security_groups=None,
469
                 device_owner=None):
470
    """Create a new port on the specified network.
471

472
    Create a new Port(NetworkInterface model) on the specified Network. If
473
    'machine' is specified, the machine will be connected to the network using
474
    this port. If 'use_ipaddress' argument is specified, the port will be
475
    assigned this IPAddress. Otherwise, an IPv4 address from the IPv4 subnet
476
    will be allocated.
477

478
    """
479
    if network.state != "ACTIVE":
480
        raise faults.Conflict("Cannot create port while network '%s' is in"
481
                              " '%s' status" % (network.id, network.state))
482
    elif network.action == "DESTROY":
483
        msg = "Cannot create port. Network %s is being deleted."
484
        raise faults.Conflict(msg % network.id)
485
    elif network.drained:
486
        raise faults.Conflict("Cannot create port while network %s is in"
487
                              " 'SNF:DRAINED' status" % network.id)
488

    
489
    utils.check_name_length(name, NetworkInterface.NETWORK_IFACE_NAME_LENGTH,
490
                            "Port name is too long")
491

    
492
    ipaddress = None
493
    if use_ipaddress is not None:
494
        # Use an existing IPAddress object.
495
        ipaddress = use_ipaddress
496
        if ipaddress and (ipaddress.network_id != network.id):
497
            msg = "IP Address %s does not belong to network %s"
498
            raise faults.Conflict(msg % (ipaddress.address, network.id))
499
    else:
500
        # If network has IPv4 subnets, try to allocate the address that the
501
        # the user specified or a random one.
502
        if network.subnets.filter(ipversion=4).exists():
503
            ipaddress = ips.allocate_ip(network, userid=userid,
504
                                        address=address)
505
        elif address is not None:
506
            raise faults.BadRequest("Address %s is not a valid IP for the"
507
                                    " defined network subnets" % address)
508

    
509
    if ipaddress is not None and ipaddress.nic is not None:
510
        raise faults.Conflict("IP address '%s' is already in use" %
511
                              ipaddress.address)
512

    
513
    port = NetworkInterface.objects.create(network=network,
514
                                           state="DOWN",
515
                                           userid=userid,
516
                                           device_owner=None,
517
                                           name=name)
518

    
519
    # add the security groups if any
520
    if security_groups:
521
        port.security_groups.add(*security_groups)
522

    
523
    if ipaddress is not None:
524
        # Associate IPAddress with the Port
525
        ipaddress.nic = port
526
        ipaddress.save()
527

    
528
    if machine is not None:
529
        # Connect port to the instance.
530
        machine = connect(machine, network, port)
531
        jobID = machine.task_job_id
532
        log.info("Created Port %s with IP %s. Ganeti Job: %s",
533
                 port, ipaddress, jobID)
534
    else:
535
        log.info("Created Port %s with IP %s not attached to any instance",
536
                 port, ipaddress)
537

    
538
    return port
539

    
540

    
541
def associate_port_with_machine(port, machine):
542
    """Associate a Port with a VirtualMachine.
543

544
    Associate the port with the VirtualMachine and add an entry to the
545
    IPAddressLog if the port has a public IPv4 address from a public network.
546

547
    """
548
    if port.machine is not None:
549
        raise faults.Conflict("Port %s is already in use." % port.id)
550
    if port.network.public:
551
        ipv4_address = port.ipv4_address
552
        if ipv4_address is not None:
553
            ip_log = IPAddressLog.objects.create(server_id=machine.id,
554
                                                 network_id=port.network_id,
555
                                                 address=ipv4_address,
556
                                                 active=True)
557
            log.debug("Created IP log entry %s", ip_log)
558
    port.machine = machine
559
    port.state = "BUILD"
560
    port.device_owner = "vm"
561
    port.save()
562
    return port
563

    
564

    
565
@transaction.commit_on_success
566
def delete_port(port):
567
    """Delete a port by removing the NIC card from the instance.
568

569
    Send a Job to remove the NIC card from the instance. The port
570
    will be deleted and the associated IPv4 addressess will be released
571
    when the job completes successfully.
572

573
    """
574

    
575
    vm = port.machine
576
    if vm is not None and not vm.deleted:
577
        vm = disconnect(port.machine, port)
578
        log.info("Removing port %s, Job: %s", port, vm.task_job_id)
579
    else:
580
        backend.remove_nic_ips(port)
581
        port.delete()
582
        log.info("Removed port %s", port)
583

    
584
    return port
585

    
586

    
587
def create_instance_ports(user_id, networks=None):
588
    # First connect the instance to the networks defined by the admin
589
    forced_ports = create_ports_for_setting(user_id, category="admin")
590
    if networks is None:
591
        # If the user did not asked for any networks, connect instance to
592
        # default networks as defined by the admin
593
        ports = create_ports_for_setting(user_id, category="default")
594
    else:
595
        # Else just connect to the networks that the user defined
596
        ports = create_ports_for_request(user_id, networks)
597
    total_ports = forced_ports + ports
598
    if len(total_ports) > settings.GANETI_MAX_NICS_PER_INSTANCE:
599
        raise faults.BadRequest("Maximum ports per server limit reached")
600
    return total_ports
601

    
602

    
603
def create_ports_for_setting(user_id, category):
604
    if category == "admin":
605
        network_setting = settings.CYCLADES_FORCED_SERVER_NETWORKS
606
        exception = faults.ServiceUnavailable
607
    elif category == "default":
608
        network_setting = settings.CYCLADES_DEFAULT_SERVER_NETWORKS
609
        exception = faults.Conflict
610
    else:
611
        raise ValueError("Unknown category: %s" % category)
612

    
613
    ports = []
614
    for network_ids in network_setting:
615
        # Treat even simple network IDs as group of networks with one network
616
        if type(network_ids) not in (list, tuple):
617
            network_ids = [network_ids]
618

    
619
        error_msgs = []
620
        for network_id in network_ids:
621
            success = False
622
            try:
623
                ports.append(_port_from_setting(user_id, network_id, category))
624
                # Port successfully created in one of the networks. Skip the
625
                # the rest.
626
                success = True
627
                break
628
            except faults.Conflict as e:
629
                if len(network_ids) == 1:
630
                    raise exception(e.message)
631
                else:
632
                    error_msgs.append(e.message)
633

    
634
        if not success:
635
            if category == "admin":
636
                log.error("Cannot connect server to forced networks '%s': %s",
637
                          network_ids, error_msgs)
638
                raise exception("Cannot connect server to forced server"
639
                                " networks.")
640
            else:
641
                log.debug("Cannot connect server to default networks '%s': %s",
642
                          network_ids, error_msgs)
643
                raise exception("Cannot connect server to default server"
644
                                " networks.")
645

    
646
    return ports
647

    
648

    
649
def _port_from_setting(user_id, network_id, category):
650
    # TODO: Fix this..you need only IPv4 and only IPv6 network
651
    if network_id == "SNF:ANY_PUBLIC_IPV4":
652
        return create_public_ipv4_port(user_id, category=category)
653
    elif network_id == "SNF:ANY_PUBLIC_IPV6":
654
        return create_public_ipv6_port(user_id, category=category)
655
    elif network_id == "SNF:ANY_PUBLIC":
656
        try:
657
            return create_public_ipv4_port(user_id, category=category)
658
        except faults.Conflict as e1:
659
            try:
660
                return create_public_ipv6_port(user_id, category=category)
661
            except faults.Conflict as e2:
662
                log.error("Failed to connect server to a public IPv4 or IPv6"
663
                          " network. IPv4: %s, IPv6: %s", e1, e2)
664
                msg = ("Cannot connect server to a public IPv4 or IPv6"
665
                       " network.")
666
                raise faults.Conflict(msg)
667
    else:  # Case of network ID
668
        if category in ["user", "default"]:
669
            return _port_for_request(user_id, {"uuid": network_id})
670
        elif category == "admin":
671
            network = util.get_network(network_id, user_id, non_deleted=True)
672
            return _create_port(user_id, network)
673
        else:
674
            raise ValueError("Unknown category: %s" % category)
675

    
676

    
677
def create_public_ipv4_port(user_id, network=None, address=None,
678
                            category="user"):
679
    """Create a port in a public IPv4 network.
680

681
    Create a port in a public IPv4 network (that may also have an IPv6
682
    subnet). If the category is 'user' or 'default' this will try to use
683
    one of the users floating IPs. If the category is 'admin' will
684
    create a port to the public network (without floating IPs or quotas).
685

686
    """
687
    if category in ["user", "default"]:
688
        if address is None:
689
            ipaddress = ips.get_free_floating_ip(user_id, network)
690
        else:
691
            ipaddress = util.get_floating_ip_by_address(user_id, address,
692
                                                        for_update=True)
693
    elif category == "admin":
694
        if network is None:
695
            ipaddress = ips.allocate_public_ip(user_id)
696
        else:
697
            ipaddress = ips.allocate_ip(network, user_id)
698
    else:
699
        raise ValueError("Unknown category: %s" % category)
700
    if network is None:
701
        network = ipaddress.network
702
    return _create_port(user_id, network, use_ipaddress=ipaddress)
703

    
704

    
705
def create_public_ipv6_port(user_id, category=None):
706
    """Create a port in a public IPv6 only network."""
707
    networks = Network.objects.filter(public=True, deleted=False,
708
                                      drained=False, subnets__ipversion=6)\
709
                              .exclude(subnets__ipversion=4)
710
    if networks:
711
        return _create_port(user_id, networks[0])
712
    else:
713
        msg = "No available IPv6 only network!"
714
        log.error(msg)
715
        raise faults.Conflict(msg)
716

    
717

    
718
def create_ports_for_request(user_id, networks):
719
    """Create the server ports requested by the user.
720

721
    Create the ports for the new servers as requested in the 'networks'
722
    attribute. The networks attribute contains either a list of network IDs
723
    ('uuid') or a list of ports IDs ('port'). In case of network IDs, the user
724
    can also specify an IPv4 address ('fixed_ip'). In order to connect to a
725
    public network, the 'fixed_ip' attribute must contain the IPv4 address of a
726
    floating IP. If the network is public but the 'fixed_ip' attribute is not
727
    specified, the system will automatically reserve one of the users floating
728
    IPs.
729

730
    """
731
    if not isinstance(networks, list):
732
        raise faults.BadRequest("Malformed request. Invalid 'networks' field")
733
    return [_port_for_request(user_id, network) for network in networks]
734

    
735

    
736
def _port_for_request(user_id, network_dict):
737
    if not isinstance(network_dict, dict):
738
        raise faults.BadRequest("Malformed request. Invalid 'networks' field")
739
    port_id = network_dict.get("port")
740
    network_id = network_dict.get("uuid")
741
    if port_id is not None:
742
        return util.get_port(port_id, user_id, for_update=True)
743
    elif network_id is not None:
744
        address = network_dict.get("fixed_ip")
745
        network = util.get_network(network_id, user_id, non_deleted=True)
746
        if network.public:
747
            if network.subnet4 is not None:
748
                if not "fixed_ip" in network_dict:
749
                    return create_public_ipv4_port(user_id, network)
750
                elif address is None:
751
                    msg = "Cannot connect to public network"
752
                    raise faults.BadRequest(msg % network.id)
753
                else:
754
                    return create_public_ipv4_port(user_id, network, address)
755
            else:
756
                raise faults.Forbidden("Cannot connect to IPv6 only public"
757
                                       " network %" % network.id)
758
        else:
759
            return _create_port(user_id, network, address=address)
760
    else:
761
        raise faults.BadRequest("Network 'uuid' or 'port' attribute"
762
                                " is required.")
763

    
764

    
765
@server_command("ATTACH_VOLUME")
766
def attach_volume(vm, volume):
767
    """Attach a volume to a server.
768

769
    The volume must be in 'AVAILABLE' status in order to be attached. Also,
770
    number of the volumes that are attached to the server must remain less
771
    than 'GANETI_MAX_DISKS_PER_INSTANCE' setting. This function will send
772
    the corresponding job to Ganeti backend and update the status of the
773
    volume to 'ATTACHING'.
774

775
    """
776
    # Check volume state
777
    if volume.status not in ["AVAILABLE", "CREATING"]:
778
        raise faults.BadRequest("Cannot attach volume while volume is in"
779
                                " '%s' status." % volume.status)
780

    
781
    # Check that disk templates are the same
782
    if volume.disk_template != vm.flavor.disk_template:
783
        msg = ("Volume and server must have the same disk template. Volume has"
784
               " disk template '%s' while server has '%s'"
785
               % (volume.disk_template, vm.flavor.disk_template))
786
        raise faults.BadRequest(msg)
787

    
788
    # Check maximum disk per instance hard limit
789
    if vm.volumes.count() == settings.GANETI_MAX_DISKS_PER_INSTANCE:
790
        raise faults.BadRequest("Maximum volumes per server limit reached")
791

    
792
    jobid = backend.attach_volume(vm, volume)
793

    
794
    log.info("Attached volume '%s' to server '%s'. JobID: '%s'", volume.id,
795
             volume.machine_id, jobid)
796

    
797
    volume.backendjobid = jobid
798
    volume.machine = vm
799
    volume.status = "ATTACHING"
800
    volume.save()
801
    return jobid
802

    
803

    
804
@server_command("DETACH_VOLUME")
805
def detach_volume(vm, volume):
806
    """Detach a volume to a server.
807

808
    The volume must be in 'IN_USE' status in order to be detached. Also,
809
    the root volume of the instance (index=0) can not be detached. This
810
    function will send the corresponding job to Ganeti backend and update the
811
    status of the volume to 'DETACHING'.
812

813
    """
814

    
815
    _check_attachment(vm, volume)
816
    if volume.status != "IN_USE":
817
        #TODO: Maybe allow other statuses as well ?
818
        raise faults.BadRequest("Cannot detach volume while volume is in"
819
                                " '%s' status." % volume.status)
820
    if volume.index == 0:
821
        raise faults.BadRequest("Cannot detach the root volume of a server")
822
    jobid = backend.detach_volume(vm, volume)
823
    log.info("Detached volume '%s' from server '%s'. JobID: '%s'", volume.id,
824
             volume.machine_id, jobid)
825
    volume.backendjobid = jobid
826
    volume.status = "DETACHING"
827
    volume.save()
828
    return jobid
829

    
830

    
831
def _check_attachment(vm, volume):
832
    """Check that volume is attached to vm."""
833
    if volume.machine_id != vm.id:
834
        raise faults.BadRequest("Volume '%s' is not attached to server '%s'"
835
                                % volume.id, vm.id)