Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (25.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 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
        VirtualMachineMetadata.objects.create(
129
            meta_key=key,
130
            meta_value=val,
131
            vm=vm)
132

    
133
    # Create the server in Ganeti.
134
    vm = create_server(vm, ports, server_volumes, flavor, image, personality,
135
                       password)
136

    
137
    return vm
138

    
139

    
140
@transaction.commit_on_success
141
def allocate_new_server(userid, flavor):
142
    """Allocate a new server to a Ganeti backend.
143

144
    Allocation is performed based on the owner of the server and the specified
145
    flavor. Also, backends that do not have a public IPv4 address are excluded
146
    from server allocation.
147

148
    This function runs inside a transaction, because after allocating the
149
    instance a commit must be performed in order to release all locks.
150

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

    
159

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

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

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

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

    
198
    return jobID
199

    
200

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

    
214

    
215
@commands.server_command("START")
216
def start(vm):
217
    log.info("Starting VM %s", vm)
218
    return backend.startup_instance(vm)
219

    
220

    
221
@commands.server_command("STOP")
222
def stop(vm, shutdown_timeout=None):
223
    log.info("Stopping VM %s", vm)
224
    return backend.shutdown_instance(vm, shutdown_timeout=shutdown_timeout)
225

    
226

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

    
234
    return backend.reboot_instance(vm, reboot_type.lower(),
235
                                   shutdown_timeout=shutdown_timeout)
236

    
237

    
238
def resize(vm, flavor):
239
    action_fields = {"beparams": {"vcpus": flavor.cpu,
240
                                  "maxmem": flavor.ram}}
241
    comm = commands.server_command("RESIZE", action_fields=action_fields)
242
    return comm(_resize)(vm, flavor)
243

    
244

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

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

    
260

    
261
@commands.server_command("SET_FIREWALL_PROFILE")
262
def set_firewall_profile(vm, profile, nic):
263
    log.info("Setting VM %s, NIC %s, firewall %s", vm, nic, profile)
264

    
265
    if profile not in [x[0] for x in NetworkInterface.FIREWALL_PROFILES]:
266
        raise faults.BadRequest("Unsupported firewall profile")
267
    backend.set_firewall_profile(vm, profile=profile, nic=nic)
268
    return None
269

    
270

    
271
@commands.server_command("CONNECT")
272
def connect(vm, network, port=None):
273
    if port is None:
274
        port = _create_port(vm.userid, network)
275
    associate_port_with_machine(port, vm)
276

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

    
279
    return backend.connect_to_network(vm, port)
280

    
281

    
282
@commands.server_command("DISCONNECT")
283
def disconnect(vm, nic):
284
    log.info("Removing NIC %s from VM %s", nic, vm)
285
    return backend.disconnect_from_network(vm, nic)
286

    
287

    
288
def console(vm, console_type):
289
    """Arrange for an OOB console of the specified type
290

291
    This method arranges for an OOB console of the specified type.
292
    Only consoles of type "vnc" are supported for now.
293

294
    It uses a running instance of vncauthproxy to setup proper
295
    VNC forwarding with a random password, then returns the necessary
296
    VNC connection info to the caller.
297

298
    """
299
    log.info("Get console  VM %s, type %s", vm, console_type)
300

    
301
    # Use RAPI to get VNC console information for this instance
302
    if vm.operstate != "STARTED":
303
        raise faults.BadRequest('Server not in ACTIVE state.')
304

    
305
    if settings.TEST:
306
        console_data = {'kind': 'vnc', 'host': 'ganeti_node', 'port': 1000}
307
    else:
308
        console_data = backend.get_instance_console(vm)
309

    
310
    if console_data['kind'] != 'vnc':
311
        message = 'got console of kind %s, not "vnc"' % console_data['kind']
312
        raise faults.ServiceUnavailable(message)
313

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

    
322
    if settings.TEST:
323
        fwd = {'source_port': 1234, 'status': 'OK'}
324
    else:
325
        vnc_extra_opts = settings.CYCLADES_VNCAUTHPROXY_OPTS
326
        fwd = request_vnc_forwarding(sport, daddr, dport, password,
327
                                     **vnc_extra_opts)
328

    
329
    if fwd['status'] != "OK":
330
        raise faults.ServiceUnavailable('vncauthproxy returned error status')
331

    
332
    # Verify that the VNC server settings haven't changed
333
    if not settings.TEST:
334
        if console_data != backend.get_instance_console(vm):
335
            raise faults.ServiceUnavailable('VNC Server settings changed.')
336

    
337
    console = {
338
        'type': 'vnc',
339
        'host': getfqdn(),
340
        'port': fwd['source_port'],
341
        'password': password}
342

    
343
    return console
344

    
345

    
346
def rename(server, new_name):
347
    """Rename a VirtualMachine."""
348
    old_name = server.name
349
    server.name = new_name
350
    server.save()
351
    log.info("Renamed server '%s' from '%s' to '%s'", server, old_name,
352
             new_name)
353
    return server
354

    
355

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

    
366

    
367
def _create_port(userid, network, machine=None, use_ipaddress=None,
368
                 address=None, name="", security_groups=None,
369
                 device_owner=None):
370
    """Create a new port on the specified network.
371

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

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

    
389
    utils.check_name_length(name, NetworkInterface.NETWORK_IFACE_NAME_LENGTH,
390
                            "Port name is too long")
391

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

    
409
    if ipaddress is not None and ipaddress.nic is not None:
410
        raise faults.Conflict("IP address '%s' is already in use" %
411
                              ipaddress.address)
412

    
413
    port = NetworkInterface.objects.create(network=network,
414
                                           state="DOWN",
415
                                           userid=userid,
416
                                           device_owner=None,
417
                                           name=name)
418

    
419
    # add the security groups if any
420
    if security_groups:
421
        port.security_groups.add(*security_groups)
422

    
423
    if ipaddress is not None:
424
        # Associate IPAddress with the Port
425
        ipaddress.nic = port
426
        ipaddress.save()
427

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

    
438
    return port
439

    
440

    
441
def associate_port_with_machine(port, machine):
442
    """Associate a Port with a VirtualMachine.
443

444
    Associate the port with the VirtualMachine and add an entry to the
445
    IPAddressLog if the port has a public IPv4 address from a public network.
446

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

    
464

    
465
@transaction.commit_on_success
466
def delete_port(port):
467
    """Delete a port by removing the NIC card from the instance.
468

469
    Send a Job to remove the NIC card from the instance. The port
470
    will be deleted and the associated IPv4 addressess will be released
471
    when the job completes successfully.
472

473
    """
474

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

    
484
    return port
485

    
486

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

    
502

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

    
513
    ports = []
514
    for network_ids in network_setting:
515
        # Treat even simple network IDs as group of networks with one network
516
        if type(network_ids) not in (list, tuple):
517
            network_ids = [network_ids]
518

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

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

    
546
    return ports
547

    
548

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

    
576

    
577
def create_public_ipv4_port(user_id, network=None, address=None,
578
                            category="user"):
579
    """Create a port in a public IPv4 network.
580

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

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

    
604

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

    
617

    
618
def create_ports_for_request(user_id, networks):
619
    """Create the server ports requested by the user.
620

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

630
    """
631
    if not isinstance(networks, list):
632
        raise faults.BadRequest("Malformed request. Invalid 'networks' field")
633
    return [_port_for_request(user_id, network) for network in networks]
634

    
635

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