Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (29.1 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
    if use_backend is None:
171
        # Allocate server to a Ganeti backend
172
        use_backend = allocate_new_server(userid, flavor)
173

    
174
    utils.check_name_length(name, VirtualMachine.VIRTUAL_MACHINE_NAME_LENGTH,
175
                            "Server name is too long")
176

    
177
    # Create the ports for the server
178
    ports = create_instance_ports(userid, networks)
179

    
180
    # Fix flavor for archipelago
181
    disk_template, provider = util.get_flavor_provider(flavor)
182
    if provider:
183
        flavor.disk_template = disk_template
184
        flavor.disk_provider = provider
185
        flavor.disk_origin = None
186
        if provider in settings.GANETI_CLONE_PROVIDERS:
187
            flavor.disk_origin = image['checksum']
188
            image['backend_id'] = 'null'
189
    else:
190
        flavor.disk_provider = None
191

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

    
202
    # Associate the ports with the server
203
    for index, port in enumerate(ports):
204
        associate_port_with_machine(port, vm)
205
        port.index = index
206
        port.save()
207

    
208
    volumes = create_instance_volumes(vm, flavor, image)
209

    
210
    for key, val in metadata.items():
211
        VirtualMachineMetadata.objects.create(
212
            meta_key=key,
213
            meta_value=val,
214
            vm=vm)
215

    
216
    # Create the server in Ganeti.
217
    vm = create_server(vm, ports, volumes, flavor, image, personality,
218
                       password)
219

    
220
    return vm
221

    
222

    
223
@transaction.commit_on_success
224
def allocate_new_server(userid, flavor):
225
    """Allocate a new server to a Ganeti backend.
226

227
    Allocation is performed based on the owner of the server and the specified
228
    flavor. Also, backends that do not have a public IPv4 address are excluded
229
    from server allocation.
230

231
    This function runs inside a transaction, because after allocating the
232
    instance a commit must be performed in order to release all locks.
233

234
    """
235
    backend_allocator = BackendAllocator()
236
    use_backend = backend_allocator.allocate(userid, flavor)
237
    if use_backend is None:
238
        log.error("No available backend for VM with flavor %s", flavor)
239
        raise faults.ServiceUnavailable("No available backends")
240
    return use_backend
241

    
242

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

    
266
    # At this point the job is enqueued in the Ganeti backend
267
    vm.backendopcode = "OP_INSTANCE_CREATE"
268
    vm.backendjobid = jobID
269
    vm.save()
270
    log.info("User %s created VM %s, NICs %s, Backend %s, JobID %s",
271
             vm.userid, vm, nics, vm.backend, str(jobID))
272

    
273
    return jobID
274

    
275

    
276
def create_instance_volumes(vm, flavor, image):
277
    name = "Root volume of server: %s" % vm.id
278
    volume = Volume.objects.create(userid=vm.userid,
279
                                   machine=vm,
280
                                   name=name,
281
                                   size=flavor.disk,
282
                                   source_image_id=image["id"],
283
                                   status="CREATING")
284

    
285
    volume.source_image = image
286
    volume.save()
287

    
288
    return [volume]
289

    
290

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

    
304

    
305
@server_command("START")
306
def start(vm):
307
    log.info("Starting VM %s", vm)
308
    return backend.startup_instance(vm)
309

    
310

    
311
@server_command("STOP")
312
def stop(vm, shutdown_timeout=None):
313
    log.info("Stopping VM %s", vm)
314
    return backend.shutdown_instance(vm, shutdown_timeout=shutdown_timeout)
315

    
316

    
317
@server_command("REBOOT")
318
def reboot(vm, reboot_type, shutdown_timeout=None):
319
    if reboot_type not in ("SOFT", "HARD"):
320
        raise faults.BadRequest("Malformed request. Invalid reboot"
321
                                " type %s" % reboot_type)
322
    log.info("Rebooting VM %s. Type %s", vm, reboot_type)
323

    
324
    return backend.reboot_instance(vm, reboot_type.lower(),
325
                                   shutdown_timeout=shutdown_timeout)
326

    
327

    
328
def resize(vm, flavor):
329
    action_fields = {"beparams": {"vcpus": flavor.cpu,
330
                                  "maxmem": flavor.ram}}
331
    comm = server_command("RESIZE", action_fields=action_fields)
332
    return comm(_resize)(vm, flavor)
333

    
334

    
335
def _resize(vm, flavor):
336
    old_flavor = vm.flavor
337
    # User requested the same flavor
338
    if old_flavor.id == flavor.id:
339
        raise faults.BadRequest("Server '%s' flavor is already '%s'."
340
                                % (vm, flavor))
341
    # Check that resize can be performed
342
    if old_flavor.disk != flavor.disk:
343
        raise faults.BadRequest("Cannot resize instance disk.")
344
    if old_flavor.disk_template != flavor.disk_template:
345
        raise faults.BadRequest("Cannot change instance disk template.")
346

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

    
350

    
351
@server_command("SET_FIREWALL_PROFILE")
352
def set_firewall_profile(vm, profile, nic):
353
    log.info("Setting VM %s, NIC %s, firewall %s", vm, nic, profile)
354

    
355
    if profile not in [x[0] for x in NetworkInterface.FIREWALL_PROFILES]:
356
        raise faults.BadRequest("Unsupported firewall profile")
357
    backend.set_firewall_profile(vm, profile=profile, nic=nic)
358
    return None
359

    
360

    
361
@server_command("CONNECT")
362
def connect(vm, network, port=None):
363
    if port is None:
364
        port = _create_port(vm.userid, network)
365
    associate_port_with_machine(port, vm)
366

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

    
369
    return backend.connect_to_network(vm, port)
370

    
371

    
372
@server_command("DISCONNECT")
373
def disconnect(vm, nic):
374
    log.info("Removing NIC %s from VM %s", nic, vm)
375
    return backend.disconnect_from_network(vm, nic)
376

    
377

    
378
def console(vm, console_type):
379
    """Arrange for an OOB console of the specified type
380

381
    This method arranges for an OOB console of the specified type.
382
    Only consoles of type "vnc" are supported for now.
383

384
    It uses a running instance of vncauthproxy to setup proper
385
    VNC forwarding with a random password, then returns the necessary
386
    VNC connection info to the caller.
387

388
    """
389
    log.info("Get console  VM %s, type %s", vm, console_type)
390

    
391
    # Use RAPI to get VNC console information for this instance
392
    if vm.operstate != "STARTED":
393
        raise faults.BadRequest('Server not in ACTIVE state.')
394

    
395
    if settings.TEST:
396
        console_data = {'kind': 'vnc', 'host': 'ganeti_node', 'port': 1000}
397
    else:
398
        console_data = backend.get_instance_console(vm)
399

    
400
    if console_data['kind'] != 'vnc':
401
        message = 'got console of kind %s, not "vnc"' % console_data['kind']
402
        raise faults.ServiceUnavailable(message)
403

    
404
    # Let vncauthproxy decide on the source port.
405
    # The alternative: static allocation, e.g.
406
    # sport = console_data['port'] - 1000
407
    sport = 0
408
    daddr = console_data['host']
409
    dport = console_data['port']
410
    password = util.random_password()
411

    
412
    if settings.TEST:
413
        fwd = {'source_port': 1234, 'status': 'OK'}
414
    else:
415
        vnc_extra_opts = settings.CYCLADES_VNCAUTHPROXY_OPTS
416
        fwd = request_vnc_forwarding(sport, daddr, dport, password,
417
                                     **vnc_extra_opts)
418

    
419
    if fwd['status'] != "OK":
420
        raise faults.ServiceUnavailable('vncauthproxy returned error status')
421

    
422
    # Verify that the VNC server settings haven't changed
423
    if not settings.TEST:
424
        if console_data != backend.get_instance_console(vm):
425
            raise faults.ServiceUnavailable('VNC Server settings changed.')
426

    
427
    console = {
428
        'type': 'vnc',
429
        'host': getfqdn(),
430
        'port': fwd['source_port'],
431
        'password': password}
432

    
433
    return console
434

    
435

    
436
def rename(server, new_name):
437
    """Rename a VirtualMachine."""
438
    old_name = server.name
439
    server.name = new_name
440
    server.save()
441
    log.info("Renamed server '%s' from '%s' to '%s'", server, old_name,
442
             new_name)
443
    return server
444

    
445

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

    
456

    
457
def _create_port(userid, network, machine=None, use_ipaddress=None,
458
                 address=None, name="", security_groups=None,
459
                 device_owner=None):
460
    """Create a new port on the specified network.
461

462
    Create a new Port(NetworkInterface model) on the specified Network. If
463
    'machine' is specified, the machine will be connected to the network using
464
    this port. If 'use_ipaddress' argument is specified, the port will be
465
    assigned this IPAddress. Otherwise, an IPv4 address from the IPv4 subnet
466
    will be allocated.
467

468
    """
469
    if network.state != "ACTIVE":
470
        raise faults.Conflict("Cannot create port while network '%s' is in"
471
                              " '%s' status" % (network.id, network.state))
472
    elif network.action == "DESTROY":
473
        msg = "Cannot create port. Network %s is being deleted."
474
        raise faults.Conflict(msg % network.id)
475
    elif network.drained:
476
        raise faults.Conflict("Cannot create port while network %s is in"
477
                              " 'SNF:DRAINED' status" % network.id)
478

    
479
    utils.check_name_length(name, NetworkInterface.NETWORK_IFACE_NAME_LENGTH,
480
                            "Port name is too long")
481

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

    
499
    if ipaddress is not None and ipaddress.nic is not None:
500
        raise faults.Conflict("IP address '%s' is already in use" %
501
                              ipaddress.address)
502

    
503
    port = NetworkInterface.objects.create(network=network,
504
                                           state="DOWN",
505
                                           userid=userid,
506
                                           device_owner=None,
507
                                           name=name)
508

    
509
    # add the security groups if any
510
    if security_groups:
511
        port.security_groups.add(*security_groups)
512

    
513
    if ipaddress is not None:
514
        # Associate IPAddress with the Port
515
        ipaddress.nic = port
516
        ipaddress.save()
517

    
518
    if machine is not None:
519
        # Connect port to the instance.
520
        machine = connect(machine, network, port)
521
        jobID = machine.task_job_id
522
        log.info("Created Port %s with IP %s. Ganeti Job: %s",
523
                 port, ipaddress, jobID)
524
    else:
525
        log.info("Created Port %s with IP %s not attached to any instance",
526
                 port, ipaddress)
527

    
528
    return port
529

    
530

    
531
def associate_port_with_machine(port, machine):
532
    """Associate a Port with a VirtualMachine.
533

534
    Associate the port with the VirtualMachine and add an entry to the
535
    IPAddressLog if the port has a public IPv4 address from a public network.
536

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

    
554

    
555
@transaction.commit_on_success
556
def delete_port(port):
557
    """Delete a port by removing the NIC card from the instance.
558

559
    Send a Job to remove the NIC card from the instance. The port
560
    will be deleted and the associated IPv4 addressess will be released
561
    when the job completes successfully.
562

563
    """
564

    
565
    vm = port.machine
566
    if vm is not None and not vm.deleted:
567
        vm = disconnect(port.machine, port)
568
        log.info("Removing port %s, Job: %s", port, vm.task_job_id)
569
    else:
570
        backend.remove_nic_ips(port)
571
        port.delete()
572
        log.info("Removed port %s", port)
573

    
574
    return port
575

    
576

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

    
592

    
593
def create_ports_for_setting(user_id, category):
594
    if category == "admin":
595
        network_setting = settings.CYCLADES_FORCED_SERVER_NETWORKS
596
        exception = faults.ServiceUnavailable
597
    elif category == "default":
598
        network_setting = settings.CYCLADES_DEFAULT_SERVER_NETWORKS
599
        exception = faults.Conflict
600
    else:
601
        raise ValueError("Unknown category: %s" % category)
602

    
603
    ports = []
604
    for network_ids in network_setting:
605
        # Treat even simple network IDs as group of networks with one network
606
        if type(network_ids) not in (list, tuple):
607
            network_ids = [network_ids]
608

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

    
624
        if not success:
625
            if category == "admin":
626
                log.error("Cannot connect server to forced networks '%s': %s",
627
                          network_ids, error_msgs)
628
                raise exception("Cannot connect server to forced server"
629
                                " networks.")
630
            else:
631
                log.debug("Cannot connect server to default networks '%s': %s",
632
                          network_ids, error_msgs)
633
                raise exception("Cannot connect server to default server"
634
                                " networks.")
635

    
636
    return ports
637

    
638

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

    
666

    
667
def create_public_ipv4_port(user_id, network=None, address=None,
668
                            category="user"):
669
    """Create a port in a public IPv4 network.
670

671
    Create a port in a public IPv4 network (that may also have an IPv6
672
    subnet). If the category is 'user' or 'default' this will try to use
673
    one of the users floating IPs. If the category is 'admin' will
674
    create a port to the public network (without floating IPs or quotas).
675

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

    
694

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

    
707

    
708
def create_ports_for_request(user_id, networks):
709
    """Create the server ports requested by the user.
710

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

720
    """
721
    if not isinstance(networks, list):
722
        raise faults.BadRequest("Malformed request. Invalid 'networks' field")
723
    return [_port_for_request(user_id, network) for network in networks]
724

    
725

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