Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (25.6 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 django import dispatch
34
from django.db import transaction
35
from django.utils import simplejson as json
36

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

    
50
log = logging.getLogger(__name__)
51

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

    
55

    
56
@transaction.commit_on_success
57
def create(userid, name, password, flavor, image_id, metadata={},
58
           personality=[], networks=None, volumes=None,
59
           use_backend=None):
60

    
61
    # Get image information
62
    # TODO: Image is not mandatory if disks are specified
63
    image = util.get_image_dict(image_id, userid)
64

    
65
    # Check that image fits into the disk
66
    if int(image["size"]) > (flavor.disk << 30):
67
        msg = ("Flavor's disk size '%s' is smaller than the image's"
68
               "size '%s'" % (flavor.disk << 30, image["size"]))
69
        raise faults.BadRequest(msg)
70

    
71
    if use_backend is None:
72
        # Allocate server to a Ganeti backend
73
        use_backend = allocate_new_server(userid, flavor)
74

    
75
    utils.check_name_length(name, VirtualMachine.VIRTUAL_MACHINE_NAME_LENGTH,
76
                            "Server name is too long")
77

    
78
    # Create the ports for the server
79
    ports = create_instance_ports(userid, networks)
80

    
81
    # We must save the VM instance now, so that it gets a valid
82
    # vm.backend_vm_id.
83
    vm = VirtualMachine.objects.create(name=name,
84
                                       backend=use_backend,
85
                                       userid=userid,
86
                                       imageid=image["id"],
87
                                       flavor=flavor,
88
                                       operstate="BUILD")
89
    log.info("Created entry in DB for VM '%s'", vm)
90

    
91
    # Associate the ports with the server
92
    for index, port in enumerate(ports):
93
        associate_port_with_machine(port, vm)
94
        port.index = index
95
        port.save()
96

    
97
    # If no volumes are specified, we automatically create a volume with the
98
    # size of the flavor and filled with the specified image.
99
    if not volumes:
100
        volumes = [{"source_type": "image",
101
                    "source_uuid": image["id"],
102
                    "size": flavor.disk,
103
                    "delete_on_termination": True}]
104

    
105
    assert(len(volumes) > 0), "Cannot create server without volumes"
106

    
107
    if volumes[0]["source_type"] == "blank":
108
        raise faults.BadRequest("Root volume cannot be blank")
109

    
110
    server_volumes = []
111
    for index, vol_info in enumerate(volumes):
112
        if vol_info["source_type"] == "volume":
113
            uuid = vol_info["source_uuid"]
114
            v = get_volume(userid, uuid, for_update=True,
115
                           exception=faults.BadRequest)
116
            if v.status != "AVAILABLE":
117
                raise faults.BadRequest("Cannot use volume while it is in %s"
118
                                        " status" % v.status)
119
            v.delete_on_termination = vol_info["delete_on_termination"]
120
            v.index = index
121
            v.save()
122
        else:
123
            v = _create_volume(server=vm, user_id=userid,
124
                               index=index, **vol_info)
125
        server_volumes.append(v)
126

    
127
    for key, val in metadata.items():
128
        utils.check_name_length(key, VirtualMachineMetadata.KEY_LENGTH,
129
                                "Metadata key is too long")
130
        utils.check_name_length(val, VirtualMachineMetadata.VALUE_LENGTH,
131
                                "Metadata value is too long")
132
        VirtualMachineMetadata.objects.create(
133
            meta_key=key,
134
            meta_value=val,
135
            vm=vm)
136

    
137
    # Create the server in Ganeti.
138
    vm = create_server(vm, ports, server_volumes, flavor, image, personality,
139
                       password)
140

    
141
    return vm
142

    
143

    
144
@transaction.commit_on_success
145
def allocate_new_server(userid, flavor):
146
    """Allocate a new server to a Ganeti backend.
147

148
    Allocation is performed based on the owner of the server and the specified
149
    flavor. Also, backends that do not have a public IPv4 address are excluded
150
    from server allocation.
151

152
    This function runs inside a transaction, because after allocating the
153
    instance a commit must be performed in order to release all locks.
154

155
    """
156
    backend_allocator = BackendAllocator()
157
    use_backend = backend_allocator.allocate(userid, flavor)
158
    if use_backend is None:
159
        log.error("No available backend for VM with flavor %s", flavor)
160
        raise faults.ServiceUnavailable("No available backends")
161
    return use_backend
162

    
163

    
164
@commands.server_command("BUILD")
165
def create_server(vm, nics, volumes, flavor, image, personality, password):
166
    # dispatch server created signal needed to trigger the 'vmapi', which
167
    # enriches the vm object with the 'config_url' attribute which must be
168
    # passed to the Ganeti job.
169

    
170
    # If the root volume has a provider, then inform snf-image to not fill
171
    # the volume with data
172
    image_id = image["backend_id"]
173
    root_volume = volumes[0]
174
    if root_volume.provider is not None:
175
        image_id = "null"
176

    
177
    server_created.send(sender=vm, created_vm_params={
178
        'img_id': image_id,
179
        'img_passwd': password,
180
        'img_format': str(image['format']),
181
        'img_personality': json.dumps(personality),
182
        'img_properties': json.dumps(image['metadata']),
183
    })
184
    # send job to Ganeti
185
    try:
186
        jobID = backend.create_instance(vm, nics, volumes, flavor, image)
187
    except:
188
        log.exception("Failed create instance '%s'", vm)
189
        jobID = None
190
        vm.operstate = "ERROR"
191
        vm.backendlogmsg = "Failed to send job to Ganeti."
192
        vm.save()
193
        vm.nics.all().update(state="ERROR")
194

    
195
    # At this point the job is enqueued in the Ganeti backend
196
    vm.backendopcode = "OP_INSTANCE_CREATE"
197
    vm.backendjobid = jobID
198
    vm.save()
199
    log.info("User %s created VM %s, NICs %s, Backend %s, JobID %s",
200
             vm.userid, vm, nics, vm.backend, str(jobID))
201

    
202
    return jobID
203

    
204

    
205
@commands.server_command("DESTROY")
206
def destroy(vm, shutdown_timeout=None):
207
    # XXX: Workaround for race where OP_INSTANCE_REMOVE starts executing on
208
    # Ganeti before OP_INSTANCE_CREATE. This will be fixed when
209
    # OP_INSTANCE_REMOVE supports the 'depends' request attribute.
210
    if (vm.backendopcode == "OP_INSTANCE_CREATE" and
211
       vm.backendjobstatus not in rapi.JOB_STATUS_FINALIZED and
212
       backend.job_is_still_running(vm) and
213
       not backend.vm_exists_in_backend(vm)):
214
            raise faults.BuildInProgress("Server is being build")
215
    log.info("Deleting VM %s", vm)
216
    return backend.delete_instance(vm, shutdown_timeout=shutdown_timeout)
217

    
218

    
219
@commands.server_command("START")
220
def start(vm):
221
    log.info("Starting VM %s", vm)
222
    return backend.startup_instance(vm)
223

    
224

    
225
@commands.server_command("STOP")
226
def stop(vm, shutdown_timeout=None):
227
    log.info("Stopping VM %s", vm)
228
    return backend.shutdown_instance(vm, shutdown_timeout=shutdown_timeout)
229

    
230

    
231
@commands.server_command("REBOOT")
232
def reboot(vm, reboot_type, shutdown_timeout=None):
233
    if reboot_type not in ("SOFT", "HARD"):
234
        raise faults.BadRequest("Malformed request. Invalid reboot"
235
                                " type %s" % reboot_type)
236
    log.info("Rebooting VM %s. Type %s", vm, reboot_type)
237

    
238
    return backend.reboot_instance(vm, reboot_type.lower(),
239
                                   shutdown_timeout=shutdown_timeout)
240

    
241

    
242
def resize(vm, flavor):
243
    action_fields = {"beparams": {"vcpus": flavor.cpu,
244
                                  "maxmem": flavor.ram}}
245
    comm = commands.server_command("RESIZE", action_fields=action_fields)
246
    return comm(_resize)(vm, flavor)
247

    
248

    
249
def _resize(vm, flavor):
250
    old_flavor = vm.flavor
251
    # User requested the same flavor
252
    if old_flavor.id == flavor.id:
253
        raise faults.BadRequest("Server '%s' flavor is already '%s'."
254
                                % (vm, flavor))
255
    # Check that resize can be performed
256
    if old_flavor.disk != flavor.disk:
257
        raise faults.BadRequest("Cannot resize instance disk.")
258
    if old_flavor.disk_template != flavor.disk_template:
259
        raise faults.BadRequest("Cannot change instance disk template.")
260

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

    
264

    
265
@commands.server_command("SET_FIREWALL_PROFILE")
266
def set_firewall_profile(vm, profile, nic):
267
    log.info("Setting VM %s, NIC %s, firewall %s", vm, nic, profile)
268

    
269
    if profile not in [x[0] for x in NetworkInterface.FIREWALL_PROFILES]:
270
        raise faults.BadRequest("Unsupported firewall profile")
271
    backend.set_firewall_profile(vm, profile=profile, nic=nic)
272
    return None
273

    
274

    
275
@commands.server_command("CONNECT")
276
def connect(vm, network, port=None):
277
    if port is None:
278
        port = _create_port(vm.userid, network)
279
    associate_port_with_machine(port, vm)
280

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

    
283
    return backend.connect_to_network(vm, port)
284

    
285

    
286
@commands.server_command("DISCONNECT")
287
def disconnect(vm, nic):
288
    log.info("Removing NIC %s from VM %s", nic, vm)
289
    return backend.disconnect_from_network(vm, nic)
290

    
291

    
292
def console(vm, console_type):
293
    """Arrange for an OOB console of the specified type
294

295
    This method arranges for an OOB console of the specified type.
296
    Only consoles of type "vnc" are supported for now.
297

298
    It uses a running instance of vncauthproxy to setup proper
299
    VNC forwarding with a random password, then returns the necessary
300
    VNC connection info to the caller.
301

302
    """
303
    log.info("Get console  VM %s, type %s", vm, console_type)
304

    
305
    # Use RAPI to get VNC console information for this instance
306
    if vm.operstate != "STARTED":
307
        raise faults.BadRequest('Server not in ACTIVE state.')
308

    
309
    if settings.TEST:
310
        console_data = {'kind': 'vnc', 'host': 'ganeti_node', 'port': 1000}
311
    else:
312
        console_data = backend.get_instance_console(vm)
313

    
314
    if console_data['kind'] != 'vnc':
315
        message = 'got console of kind %s, not "vnc"' % console_data['kind']
316
        raise faults.ServiceUnavailable(message)
317

    
318
    # Let vncauthproxy decide on the source port.
319
    # The alternative: static allocation, e.g.
320
    # sport = console_data['port'] - 1000
321
    sport = 0
322
    daddr = console_data['host']
323
    dport = console_data['port']
324
    password = util.random_password()
325

    
326
    if settings.TEST:
327
        fwd = {'source_port': 1234, 'status': 'OK'}
328
    else:
329
        vnc_extra_opts = settings.CYCLADES_VNCAUTHPROXY_OPTS
330
        fwd = request_vnc_forwarding(sport, daddr, dport, password,
331
                                     **vnc_extra_opts)
332

    
333
    if fwd['status'] != "OK":
334
        raise faults.ServiceUnavailable('vncauthproxy returned error status')
335

    
336
    # Verify that the VNC server settings haven't changed
337
    if not settings.TEST:
338
        if console_data != backend.get_instance_console(vm):
339
            raise faults.ServiceUnavailable('VNC Server settings changed.')
340

    
341
    console = {
342
        'type': 'vnc',
343
        'host': getfqdn(),
344
        'port': fwd['source_port'],
345
        'password': password}
346

    
347
    return console
348

    
349

    
350
def rename(server, new_name):
351
    """Rename a VirtualMachine."""
352
    old_name = server.name
353
    server.name = new_name
354
    server.save()
355
    log.info("Renamed server '%s' from '%s' to '%s'", server, old_name,
356
             new_name)
357
    return server
358

    
359

    
360
@transaction.commit_on_success
361
def create_port(*args, **kwargs):
362
    vm = kwargs.get("machine", None)
363
    if vm is None and len(args) >= 3:
364
        vm = args[2]
365
    if vm is not None:
366
        if vm.nics.count() == settings.GANETI_MAX_NICS_PER_INSTANCE:
367
            raise faults.BadRequest("Maximum ports per server limit reached")
368
    return _create_port(*args, **kwargs)
369

    
370

    
371
def _create_port(userid, network, machine=None, use_ipaddress=None,
372
                 address=None, name="", security_groups=None,
373
                 device_owner=None):
374
    """Create a new port on the specified network.
375

376
    Create a new Port(NetworkInterface model) on the specified Network. If
377
    'machine' is specified, the machine will be connected to the network using
378
    this port. If 'use_ipaddress' argument is specified, the port will be
379
    assigned this IPAddress. Otherwise, an IPv4 address from the IPv4 subnet
380
    will be allocated.
381

382
    """
383
    if network.state != "ACTIVE":
384
        raise faults.Conflict("Cannot create port while network '%s' is in"
385
                              " '%s' status" % (network.id, network.state))
386
    elif network.action == "DESTROY":
387
        msg = "Cannot create port. Network %s is being deleted."
388
        raise faults.Conflict(msg % network.id)
389
    elif network.drained:
390
        raise faults.Conflict("Cannot create port while network %s is in"
391
                              " 'SNF:DRAINED' status" % network.id)
392

    
393
    utils.check_name_length(name, NetworkInterface.NETWORK_IFACE_NAME_LENGTH,
394
                            "Port name is too long")
395

    
396
    ipaddress = None
397
    if use_ipaddress is not None:
398
        # Use an existing IPAddress object.
399
        ipaddress = use_ipaddress
400
        if ipaddress and (ipaddress.network_id != network.id):
401
            msg = "IP Address %s does not belong to network %s"
402
            raise faults.Conflict(msg % (ipaddress.address, network.id))
403
    else:
404
        # If network has IPv4 subnets, try to allocate the address that the
405
        # the user specified or a random one.
406
        if network.subnets.filter(ipversion=4).exists():
407
            ipaddress = ips.allocate_ip(network, userid=userid,
408
                                        address=address)
409
        elif address is not None:
410
            raise faults.BadRequest("Address %s is not a valid IP for the"
411
                                    " defined network subnets" % address)
412

    
413
    if ipaddress is not None and ipaddress.nic is not None:
414
        raise faults.Conflict("IP address '%s' is already in use" %
415
                              ipaddress.address)
416

    
417
    port = NetworkInterface.objects.create(network=network,
418
                                           state="DOWN",
419
                                           userid=userid,
420
                                           device_owner=None,
421
                                           name=name)
422

    
423
    # add the security groups if any
424
    if security_groups:
425
        port.security_groups.add(*security_groups)
426

    
427
    if ipaddress is not None:
428
        # Associate IPAddress with the Port
429
        ipaddress.nic = port
430
        ipaddress.save()
431

    
432
    if machine is not None:
433
        # Connect port to the instance.
434
        machine = connect(machine, network, port)
435
        jobID = machine.task_job_id
436
        log.info("Created Port %s with IP %s. Ganeti Job: %s",
437
                 port, ipaddress, jobID)
438
    else:
439
        log.info("Created Port %s with IP %s not attached to any instance",
440
                 port, ipaddress)
441

    
442
    return port
443

    
444

    
445
def associate_port_with_machine(port, machine):
446
    """Associate a Port with a VirtualMachine.
447

448
    Associate the port with the VirtualMachine and add an entry to the
449
    IPAddressLog if the port has a public IPv4 address from a public network.
450

451
    """
452
    if port.machine is not None:
453
        raise faults.Conflict("Port %s is already in use." % port.id)
454
    if port.network.public:
455
        ipv4_address = port.ipv4_address
456
        if ipv4_address is not None:
457
            ip_log = IPAddressLog.objects.create(server_id=machine.id,
458
                                                 network_id=port.network_id,
459
                                                 address=ipv4_address,
460
                                                 active=True)
461
            log.debug("Created IP log entry %s", ip_log)
462
    port.machine = machine
463
    port.state = "BUILD"
464
    port.device_owner = "vm"
465
    port.save()
466
    return port
467

    
468

    
469
@transaction.commit_on_success
470
def delete_port(port):
471
    """Delete a port by removing the NIC card from the instance.
472

473
    Send a Job to remove the NIC card from the instance. The port
474
    will be deleted and the associated IPv4 addressess will be released
475
    when the job completes successfully.
476

477
    """
478

    
479
    vm = port.machine
480
    if vm is not None and not vm.deleted:
481
        vm = disconnect(port.machine, port)
482
        log.info("Removing port %s, Job: %s", port, vm.task_job_id)
483
    else:
484
        backend.remove_nic_ips(port)
485
        port.delete()
486
        log.info("Removed port %s", port)
487

    
488
    return port
489

    
490

    
491
def create_instance_ports(user_id, networks=None):
492
    # First connect the instance to the networks defined by the admin
493
    forced_ports = create_ports_for_setting(user_id, category="admin")
494
    if networks is None:
495
        # If the user did not asked for any networks, connect instance to
496
        # default networks as defined by the admin
497
        ports = create_ports_for_setting(user_id, category="default")
498
    else:
499
        # Else just connect to the networks that the user defined
500
        ports = create_ports_for_request(user_id, networks)
501
    total_ports = forced_ports + ports
502
    if len(total_ports) > settings.GANETI_MAX_NICS_PER_INSTANCE:
503
        raise faults.BadRequest("Maximum ports per server limit reached")
504
    return total_ports
505

    
506

    
507
def create_ports_for_setting(user_id, category):
508
    if category == "admin":
509
        network_setting = settings.CYCLADES_FORCED_SERVER_NETWORKS
510
        exception = faults.ServiceUnavailable
511
    elif category == "default":
512
        network_setting = settings.CYCLADES_DEFAULT_SERVER_NETWORKS
513
        exception = faults.Conflict
514
    else:
515
        raise ValueError("Unknown category: %s" % category)
516

    
517
    ports = []
518
    for network_ids in network_setting:
519
        # Treat even simple network IDs as group of networks with one network
520
        if type(network_ids) not in (list, tuple):
521
            network_ids = [network_ids]
522

    
523
        error_msgs = []
524
        for network_id in network_ids:
525
            success = False
526
            try:
527
                ports.append(_port_from_setting(user_id, network_id, category))
528
                # Port successfully created in one of the networks. Skip the
529
                # the rest.
530
                success = True
531
                break
532
            except faults.Conflict as e:
533
                if len(network_ids) == 1:
534
                    raise exception(e.message)
535
                else:
536
                    error_msgs.append(e.message)
537

    
538
        if not success:
539
            if category == "admin":
540
                log.error("Cannot connect server to forced networks '%s': %s",
541
                          network_ids, error_msgs)
542
                raise exception("Cannot connect server to forced server"
543
                                " networks.")
544
            else:
545
                log.debug("Cannot connect server to default networks '%s': %s",
546
                          network_ids, error_msgs)
547
                raise exception("Cannot connect server to default server"
548
                                " networks.")
549

    
550
    return ports
551

    
552

    
553
def _port_from_setting(user_id, network_id, category):
554
    # TODO: Fix this..you need only IPv4 and only IPv6 network
555
    if network_id == "SNF:ANY_PUBLIC_IPV4":
556
        return create_public_ipv4_port(user_id, category=category)
557
    elif network_id == "SNF:ANY_PUBLIC_IPV6":
558
        return create_public_ipv6_port(user_id, category=category)
559
    elif network_id == "SNF:ANY_PUBLIC":
560
        try:
561
            return create_public_ipv4_port(user_id, category=category)
562
        except faults.Conflict as e1:
563
            try:
564
                return create_public_ipv6_port(user_id, category=category)
565
            except faults.Conflict as e2:
566
                log.error("Failed to connect server to a public IPv4 or IPv6"
567
                          " network. IPv4: %s, IPv6: %s", e1, e2)
568
                msg = ("Cannot connect server to a public IPv4 or IPv6"
569
                       " network.")
570
                raise faults.Conflict(msg)
571
    else:  # Case of network ID
572
        if category in ["user", "default"]:
573
            return _port_for_request(user_id, {"uuid": network_id})
574
        elif category == "admin":
575
            network = util.get_network(network_id, user_id, non_deleted=True)
576
            return _create_port(user_id, network)
577
        else:
578
            raise ValueError("Unknown category: %s" % category)
579

    
580

    
581
def create_public_ipv4_port(user_id, network=None, address=None,
582
                            category="user"):
583
    """Create a port in a public IPv4 network.
584

585
    Create a port in a public IPv4 network (that may also have an IPv6
586
    subnet). If the category is 'user' or 'default' this will try to use
587
    one of the users floating IPs. If the category is 'admin' will
588
    create a port to the public network (without floating IPs or quotas).
589

590
    """
591
    if category in ["user", "default"]:
592
        if address is None:
593
            ipaddress = ips.get_free_floating_ip(user_id, network)
594
        else:
595
            ipaddress = util.get_floating_ip_by_address(user_id, address,
596
                                                        for_update=True)
597
    elif category == "admin":
598
        if network is None:
599
            ipaddress = ips.allocate_public_ip(user_id)
600
        else:
601
            ipaddress = ips.allocate_ip(network, user_id)
602
    else:
603
        raise ValueError("Unknown category: %s" % category)
604
    if network is None:
605
        network = ipaddress.network
606
    return _create_port(user_id, network, use_ipaddress=ipaddress)
607

    
608

    
609
def create_public_ipv6_port(user_id, category=None):
610
    """Create a port in a public IPv6 only network."""
611
    networks = Network.objects.filter(public=True, deleted=False,
612
                                      drained=False, subnets__ipversion=6)\
613
                              .exclude(subnets__ipversion=4)
614
    if networks:
615
        return _create_port(user_id, networks[0])
616
    else:
617
        msg = "No available IPv6 only network!"
618
        log.error(msg)
619
        raise faults.Conflict(msg)
620

    
621

    
622
def create_ports_for_request(user_id, networks):
623
    """Create the server ports requested by the user.
624

625
    Create the ports for the new servers as requested in the 'networks'
626
    attribute. The networks attribute contains either a list of network IDs
627
    ('uuid') or a list of ports IDs ('port'). In case of network IDs, the user
628
    can also specify an IPv4 address ('fixed_ip'). In order to connect to a
629
    public network, the 'fixed_ip' attribute must contain the IPv4 address of a
630
    floating IP. If the network is public but the 'fixed_ip' attribute is not
631
    specified, the system will automatically reserve one of the users floating
632
    IPs.
633

634
    """
635
    if not isinstance(networks, list):
636
        raise faults.BadRequest("Malformed request. Invalid 'networks' field")
637
    return [_port_for_request(user_id, network) for network in networks]
638

    
639

    
640
def _port_for_request(user_id, network_dict):
641
    if not isinstance(network_dict, dict):
642
        raise faults.BadRequest("Malformed request. Invalid 'networks' field")
643
    port_id = network_dict.get("port")
644
    network_id = network_dict.get("uuid")
645
    if port_id is not None:
646
        return util.get_port(port_id, user_id, for_update=True)
647
    elif network_id is not None:
648
        address = network_dict.get("fixed_ip")
649
        network = util.get_network(network_id, user_id, non_deleted=True)
650
        if network.public:
651
            if network.subnet4 is not None:
652
                if not "fixed_ip" in network_dict:
653
                    return create_public_ipv4_port(user_id, network)
654
                elif address is None:
655
                    msg = "Cannot connect to public network"
656
                    raise faults.BadRequest(msg % network.id)
657
                else:
658
                    return create_public_ipv4_port(user_id, network, address)
659
            else:
660
                raise faults.Forbidden("Cannot connect to IPv6 only public"
661
                                       " network %" % network.id)
662
        else:
663
            return _create_port(user_id, network, address=address)
664
    else:
665
        raise faults.BadRequest("Network 'uuid' or 'port' attribute"
666
                                " is required.")