Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (29.3 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"] and operstate != "STOPPED"
83
          and not settings.GANETI_USE_HOTPLUG):
84
        raise faults.BadRequest("Cannot perform '%s' action while server is"
85
                                " in '%s' state." % (action, operstate))
86
    return
87

    
88

    
89
def server_command(action, action_fields=None):
90
    """Handle execution of a server action.
91

92
    Helper function to validate and execute a server action, handle quota
93
    commission and update the 'task' of the VM in the DB.
94

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

    
116
            commission_name = "client: api, resource: %s" % vm
117
            quotas.handle_resource_commission(vm, action=action,
118
                                              action_fields=action_fields,
119
                                              commission_name=commission_name)
120
            vm.save()
121

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

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

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

    
153
            log.info("user: %s, vm: %s, action: %s, job_id: %s, serial: %s",
154
                     user_id, vm.id, action, job_id, vm.serial)
155

    
156
            # store the new task in the VM
157
            if job_id is not None:
158
                vm.task = action
159
                vm.task_job_id = job_id
160
            vm.save()
161

    
162
            return vm
163
        return wrapper
164
    return decorator
165

    
166

    
167
@transaction.commit_on_success
168
def create(userid, name, password, flavor, image, metadata={},
169
           personality=[], networks=None, use_backend=None):
170

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

    
176
    if use_backend is None:
177
        # Allocate server to a Ganeti backend
178
        use_backend = allocate_new_server(userid, flavor)
179

    
180
    utils.check_name_length(name, VirtualMachine.VIRTUAL_MACHINE_NAME_LENGTH,
181
                            "Server name is too long")
182

    
183
    # Create the ports for the server
184
    ports = create_instance_ports(userid, networks)
185

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

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

    
208
    # Associate the ports with the server
209
    for index, port in enumerate(ports):
210
        associate_port_with_machine(port, vm)
211
        port.index = index
212
        port.save()
213

    
214
    volumes = create_instance_volumes(vm, flavor, image)
215

    
216
    for key, val in metadata.items():
217
        VirtualMachineMetadata.objects.create(
218
            meta_key=key,
219
            meta_value=val,
220
            vm=vm)
221

    
222
    # Create the server in Ganeti.
223
    vm = create_server(vm, ports, volumes, flavor, image, personality,
224
                       password)
225

    
226
    return vm
227

    
228

    
229
@transaction.commit_on_success
230
def allocate_new_server(userid, flavor):
231
    """Allocate a new server to a Ganeti backend.
232

233
    Allocation is performed based on the owner of the server and the specified
234
    flavor. Also, backends that do not have a public IPv4 address are excluded
235
    from server allocation.
236

237
    This function runs inside a transaction, because after allocating the
238
    instance a commit must be performed in order to release all locks.
239

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

    
248

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

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

    
279
    return jobID
280

    
281

    
282
def create_instance_volumes(vm, flavor, image):
283
    name = "Root volume of server: %s" % vm.id
284
    volume = Volume.objects.create(userid=vm.userid,
285
                                   machine=vm,
286
                                   name=name,
287
                                   size=flavor.disk,
288
                                   source_image_id=image["id"],
289
                                   status="CREATING")
290

    
291
    volume.source_image = image
292
    volume.save()
293

    
294
    return [volume]
295

    
296

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

    
310

    
311
@server_command("START")
312
def start(vm):
313
    log.info("Starting VM %s", vm)
314
    return backend.startup_instance(vm)
315

    
316

    
317
@server_command("STOP")
318
def stop(vm, shutdown_timeout=None):
319
    log.info("Stopping VM %s", vm)
320
    return backend.shutdown_instance(vm, shutdown_timeout=shutdown_timeout)
321

    
322

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

    
330
    return backend.reboot_instance(vm, reboot_type.lower(),
331
                                   shutdown_timeout=shutdown_timeout)
332

    
333

    
334
def resize(vm, flavor):
335
    action_fields = {"beparams": {"vcpus": flavor.cpu,
336
                                  "maxmem": flavor.ram}}
337
    comm = server_command("RESIZE", action_fields=action_fields)
338
    return comm(_resize)(vm, flavor)
339

    
340

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

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

    
356

    
357
@server_command("SET_FIREWALL_PROFILE")
358
def set_firewall_profile(vm, profile, nic):
359
    log.info("Setting VM %s, NIC %s, firewall %s", vm, nic, profile)
360

    
361
    if profile not in [x[0] for x in NetworkInterface.FIREWALL_PROFILES]:
362
        raise faults.BadRequest("Unsupported firewall profile")
363
    backend.set_firewall_profile(vm, profile=profile, nic=nic)
364
    return None
365

    
366

    
367
@server_command("CONNECT")
368
def connect(vm, network, port=None):
369
    if port is None:
370
        port = _create_port(vm.userid, network)
371
    associate_port_with_machine(port, vm)
372

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

    
375
    return backend.connect_to_network(vm, port)
376

    
377

    
378
@server_command("DISCONNECT")
379
def disconnect(vm, nic):
380
    log.info("Removing NIC %s from VM %s", nic, vm)
381
    return backend.disconnect_from_network(vm, nic)
382

    
383

    
384
def console(vm, console_type):
385
    """Arrange for an OOB console of the specified type
386

387
    This method arranges for an OOB console of the specified type.
388
    Only consoles of type "vnc" are supported for now.
389

390
    It uses a running instance of vncauthproxy to setup proper
391
    VNC forwarding with a random password, then returns the necessary
392
    VNC connection info to the caller.
393

394
    """
395
    log.info("Get console  VM %s, type %s", vm, console_type)
396

    
397
    # Use RAPI to get VNC console information for this instance
398
    if vm.operstate != "STARTED":
399
        raise faults.BadRequest('Server not in ACTIVE state.')
400

    
401
    if settings.TEST:
402
        console_data = {'kind': 'vnc', 'host': 'ganeti_node', 'port': 1000}
403
    else:
404
        console_data = backend.get_instance_console(vm)
405

    
406
    if console_data['kind'] != 'vnc':
407
        message = 'got console of kind %s, not "vnc"' % console_data['kind']
408
        raise faults.ServiceUnavailable(message)
409

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

    
418
    if settings.TEST:
419
        fwd = {'source_port': 1234, 'status': 'OK'}
420
    else:
421
        vnc_extra_opts = settings.CYCLADES_VNCAUTHPROXY_OPTS
422
        fwd = request_vnc_forwarding(sport, daddr, dport, password,
423
                                     **vnc_extra_opts)
424

    
425
    if fwd['status'] != "OK":
426
        raise faults.ServiceUnavailable('vncauthproxy returned error status')
427

    
428
    # Verify that the VNC server settings haven't changed
429
    if not settings.TEST:
430
        if console_data != backend.get_instance_console(vm):
431
            raise faults.ServiceUnavailable('VNC Server settings changed.')
432

    
433
    console = {
434
        'type': 'vnc',
435
        'host': getfqdn(),
436
        'port': fwd['source_port'],
437
        'password': password}
438

    
439
    return console
440

    
441

    
442
def rename(server, new_name):
443
    """Rename a VirtualMachine."""
444
    old_name = server.name
445
    server.name = new_name
446
    server.save()
447
    log.info("Renamed server '%s' from '%s' to '%s'", server, old_name,
448
             new_name)
449
    return server
450

    
451

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

    
462

    
463
def _create_port(userid, network, machine=None, use_ipaddress=None,
464
                 address=None, name="", security_groups=None,
465
                 device_owner=None):
466
    """Create a new port on the specified network.
467

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

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

    
485
    utils.check_name_length(name, NetworkInterface.NETWORK_IFACE_NAME_LENGTH,
486
                            "Port name is too long")
487

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

    
505
    if ipaddress is not None and ipaddress.nic is not None:
506
        raise faults.Conflict("IP address '%s' is already in use" %
507
                              ipaddress.address)
508

    
509
    port = NetworkInterface.objects.create(network=network,
510
                                           state="DOWN",
511
                                           userid=userid,
512
                                           device_owner=None,
513
                                           name=name)
514

    
515
    # add the security groups if any
516
    if security_groups:
517
        port.security_groups.add(*security_groups)
518

    
519
    if ipaddress is not None:
520
        # Associate IPAddress with the Port
521
        ipaddress.nic = port
522
        ipaddress.save()
523

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

    
534
    return port
535

    
536

    
537
def associate_port_with_machine(port, machine):
538
    """Associate a Port with a VirtualMachine.
539

540
    Associate the port with the VirtualMachine and add an entry to the
541
    IPAddressLog if the port has a public IPv4 address from a public network.
542

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

    
560

    
561
@transaction.commit_on_success
562
def delete_port(port):
563
    """Delete a port by removing the NIC card from the instance.
564

565
    Send a Job to remove the NIC card from the instance. The port
566
    will be deleted and the associated IPv4 addressess will be released
567
    when the job completes successfully.
568

569
    """
570

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

    
580
    return port
581

    
582

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

    
598

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

    
609
    ports = []
610
    for network_ids in network_setting:
611
        # Treat even simple network IDs as group of networks with one network
612
        if type(network_ids) not in (list, tuple):
613
            network_ids = [network_ids]
614

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

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

    
642
    return ports
643

    
644

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

    
672

    
673
def create_public_ipv4_port(user_id, network=None, address=None,
674
                            category="user"):
675
    """Create a port in a public IPv4 network.
676

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

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

    
700

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

    
713

    
714
def create_ports_for_request(user_id, networks):
715
    """Create the server ports requested by the user.
716

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

726
    """
727
    if not isinstance(networks, list):
728
        raise faults.BadRequest("Malformed request. Invalid 'networks' field")
729
    return [_port_for_request(user_id, network) for network in networks]
730

    
731

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