Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (28.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 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
from vncauthproxy.client import request_forwarding as request_vnc_forwarding
47
from synnefo.logic import rapi
48

    
49
log = logging.getLogger(__name__)
50

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

    
54

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

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

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

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

    
87

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

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

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

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

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

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

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

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

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

    
161
            return vm
162
        return wrapper
163
    return decorator
164

    
165

    
166
@transaction.commit_on_success
167
def create(userid, name, password, flavor, image, metadata={},
168
           personality=[], networks=None, use_backend=None):
169
    if use_backend is None:
170
        # Allocate server to a Ganeti backend
171
        use_backend = allocate_new_server(userid, flavor)
172

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

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

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

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

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

    
207
    for key, val in metadata.items():
208
        VirtualMachineMetadata.objects.create(
209
            meta_key=key,
210
            meta_value=val,
211
            vm=vm)
212

    
213
    # Create the server in Ganeti.
214
    vm = create_server(vm, ports, flavor, image, personality, password)
215

    
216
    return vm
217

    
218

    
219
@transaction.commit_on_success
220
def allocate_new_server(userid, flavor):
221
    """Allocate a new server to a Ganeti backend.
222

223
    Allocation is performed based on the owner of the server and the specified
224
    flavor. Also, backends that do not have a public IPv4 address are excluded
225
    from server allocation.
226

227
    This function runs inside a transaction, because after allocating the
228
    instance a commit must be performed in order to release all locks.
229

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

    
238

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

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

    
269
    return jobID
270

    
271

    
272
@server_command("DESTROY")
273
def destroy(vm, shutdown_timeout=None):
274
    # XXX: Workaround for race where OP_INSTANCE_REMOVE starts executing on
275
    # Ganeti before OP_INSTANCE_CREATE. This will be fixed when
276
    # OP_INSTANCE_REMOVE supports the 'depends' request attribute.
277
    if (vm.backendopcode == "OP_INSTANCE_CREATE" and
278
       vm.backendjobstatus not in rapi.JOB_STATUS_FINALIZED and
279
       backend.job_is_still_running(vm) and
280
       not backend.vm_exists_in_backend(vm)):
281
            raise faults.BuildInProgress("Server is being build")
282
    log.info("Deleting VM %s", vm)
283
    return backend.delete_instance(vm, shutdown_timeout=shutdown_timeout)
284

    
285

    
286
@server_command("START")
287
def start(vm):
288
    log.info("Starting VM %s", vm)
289
    return backend.startup_instance(vm)
290

    
291

    
292
@server_command("STOP")
293
def stop(vm, shutdown_timeout=None):
294
    log.info("Stopping VM %s", vm)
295
    return backend.shutdown_instance(vm, shutdown_timeout=shutdown_timeout)
296

    
297

    
298
@server_command("REBOOT")
299
def reboot(vm, reboot_type, shutdown_timeout=None):
300
    if reboot_type not in ("SOFT", "HARD"):
301
        raise faults.BadRequest("Malformed request. Invalid reboot"
302
                                " type %s" % reboot_type)
303
    log.info("Rebooting VM %s. Type %s", vm, reboot_type)
304

    
305
    return backend.reboot_instance(vm, reboot_type.lower(),
306
                                   shutdown_timeout=shutdown_timeout)
307

    
308

    
309
def resize(vm, flavor):
310
    action_fields = {"beparams": {"vcpus": flavor.cpu,
311
                                  "maxmem": flavor.ram}}
312
    comm = server_command("RESIZE", action_fields=action_fields)
313
    return comm(_resize)(vm, flavor)
314

    
315

    
316
def _resize(vm, flavor):
317
    old_flavor = vm.flavor
318
    # User requested the same flavor
319
    if old_flavor.id == flavor.id:
320
        raise faults.BadRequest("Server '%s' flavor is already '%s'."
321
                                % (vm, flavor))
322
    # Check that resize can be performed
323
    if old_flavor.disk != flavor.disk:
324
        raise faults.BadRequest("Cannot resize instance disk.")
325
    if old_flavor.disk_template != flavor.disk_template:
326
        raise faults.BadRequest("Cannot change instance disk template.")
327

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

    
331

    
332
@server_command("SET_FIREWALL_PROFILE")
333
def set_firewall_profile(vm, profile, nic):
334
    log.info("Setting VM %s, NIC %s, firewall %s", vm, nic, profile)
335

    
336
    if profile not in [x[0] for x in NetworkInterface.FIREWALL_PROFILES]:
337
        raise faults.BadRequest("Unsupported firewall profile")
338
    backend.set_firewall_profile(vm, profile=profile, nic=nic)
339
    return None
340

    
341

    
342
@server_command("CONNECT")
343
def connect(vm, network, port=None):
344
    if port is None:
345
        port = _create_port(vm.userid, network)
346
    associate_port_with_machine(port, vm)
347

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

    
350
    return backend.connect_to_network(vm, port)
351

    
352

    
353
@server_command("DISCONNECT")
354
def disconnect(vm, nic):
355
    log.info("Removing NIC %s from VM %s", nic, vm)
356
    return backend.disconnect_from_network(vm, nic)
357

    
358

    
359
def console(vm, console_type):
360
    """Arrange for an OOB console of the specified type
361

362
    This method arranges for an OOB console of the specified type.
363
    Only consoles of type "vnc" are supported for now.
364

365
    It uses a running instance of vncauthproxy to setup proper
366
    VNC forwarding with a random password, then returns the necessary
367
    VNC connection info to the caller.
368

369
    """
370
    log.info("Get console  VM %s, type %s", vm, console_type)
371

    
372
    # Use RAPI to get VNC console information for this instance
373
    if vm.operstate != "STARTED":
374
        raise faults.BadRequest('Server not in ACTIVE state.')
375

    
376
    if settings.TEST:
377
        console_data = {'kind': 'vnc', 'host': 'ganeti_node', 'port': 1000}
378
    else:
379
        console_data = backend.get_instance_console(vm)
380

    
381
    if console_data['kind'] != 'vnc':
382
        message = 'got console of kind %s, not "vnc"' % console_data['kind']
383
        raise faults.ServiceUnavailable(message)
384

    
385
    # Let vncauthproxy decide on the source port.
386
    # The alternative: static allocation, e.g.
387
    # sport = console_data['port'] - 1000
388
    sport = 0
389
    daddr = console_data['host']
390
    dport = console_data['port']
391
    password = util.random_password()
392

    
393
    if settings.TEST:
394
        fwd = {'source_port': 1234, 'status': 'OK'}
395
    else:
396
        vnc_extra_opts = settings.CYCLADES_VNCAUTHPROXY_OPTS
397
        fwd = request_vnc_forwarding(sport, daddr, dport, password,
398
                                     **vnc_extra_opts)
399

    
400
    if fwd['status'] != "OK":
401
        raise faults.ServiceUnavailable('vncauthproxy returned error status')
402

    
403
    # Verify that the VNC server settings haven't changed
404
    if not settings.TEST:
405
        if console_data != backend.get_instance_console(vm):
406
            raise faults.ServiceUnavailable('VNC Server settings changed.')
407

    
408
    console = {
409
        'type': 'vnc',
410
        'host': getfqdn(),
411
        'port': fwd['source_port'],
412
        'password': password}
413

    
414
    return console
415

    
416

    
417
def rename(server, new_name):
418
    """Rename a VirtualMachine."""
419
    utils.check_name_length(new_name,
420
                            VirtualMachine.VIRTUAL_MACHINE_NAME_LENGTH,
421
                            "Server name is too long")
422
    old_name = server.name
423
    server.name = new_name
424
    server.save()
425
    log.info("Renamed server '%s' from '%s' to '%s'", server, old_name,
426
             new_name)
427
    return server
428

    
429

    
430
@transaction.commit_on_success
431
def create_port(*args, **kwargs):
432
    vm = kwargs.get("machine", None)
433
    if vm is None and len(args) >= 3:
434
        vm = args[2]
435
    if vm is not None:
436
        if vm.nics.count() == settings.GANETI_MAX_NICS_PER_INSTANCE:
437
            raise faults.BadRequest("Maximum ports per server limit reached")
438
    return _create_port(*args, **kwargs)
439

    
440

    
441
def _create_port(userid, network, machine=None, use_ipaddress=None,
442
                 address=None, name="", security_groups=None,
443
                 device_owner=None):
444
    """Create a new port on the specified network.
445

446
    Create a new Port(NetworkInterface model) on the specified Network. If
447
    'machine' is specified, the machine will be connected to the network using
448
    this port. If 'use_ipaddress' argument is specified, the port will be
449
    assigned this IPAddress. Otherwise, an IPv4 address from the IPv4 subnet
450
    will be allocated.
451

452
    """
453
    if network.state != "ACTIVE":
454
        raise faults.Conflict("Cannot create port while network '%s' is in"
455
                              " '%s' status" % (network.id, network.state))
456
    elif network.action == "DESTROY":
457
        msg = "Cannot create port. Network %s is being deleted."
458
        raise faults.Conflict(msg % network.id)
459
    elif network.drained:
460
        raise faults.Conflict("Cannot create port while network %s is in"
461
                              " 'SNF:DRAINED' status" % network.id)
462

    
463
    utils.check_name_length(name, NetworkInterface.NETWORK_IFACE_NAME_LENGTH,
464
                            "Port name is too long")
465

    
466
    ipaddress = None
467
    if use_ipaddress is not None:
468
        # Use an existing IPAddress object.
469
        ipaddress = use_ipaddress
470
        if ipaddress and (ipaddress.network_id != network.id):
471
            msg = "IP Address %s does not belong to network %s"
472
            raise faults.Conflict(msg % (ipaddress.address, network.id))
473
    else:
474
        # If network has IPv4 subnets, try to allocate the address that the
475
        # the user specified or a random one.
476
        if network.subnets.filter(ipversion=4).exists():
477
            ipaddress = ips.allocate_ip(network, userid=userid,
478
                                        address=address)
479
        elif address is not None:
480
            raise faults.BadRequest("Address %s is not a valid IP for the"
481
                                    " defined network subnets" % address)
482

    
483
    if ipaddress is not None and ipaddress.nic is not None:
484
        raise faults.Conflict("IP address '%s' is already in use" %
485
                              ipaddress.address)
486

    
487
    port = NetworkInterface.objects.create(network=network,
488
                                           state="DOWN",
489
                                           userid=userid,
490
                                           device_owner=None,
491
                                           name=name)
492

    
493
    # add the security groups if any
494
    if security_groups:
495
        port.security_groups.add(*security_groups)
496

    
497
    if ipaddress is not None:
498
        # Associate IPAddress with the Port
499
        ipaddress.nic = port
500
        ipaddress.save()
501

    
502
    if machine is not None:
503
        # Connect port to the instance.
504
        machine = connect(machine, network, port)
505
        jobID = machine.task_job_id
506
        log.info("Created Port %s with IP %s. Ganeti Job: %s",
507
                 port, ipaddress, jobID)
508
    else:
509
        log.info("Created Port %s with IP %s not attached to any instance",
510
                 port, ipaddress)
511

    
512
    return port
513

    
514

    
515
def associate_port_with_machine(port, machine):
516
    """Associate a Port with a VirtualMachine.
517

518
    Associate the port with the VirtualMachine and add an entry to the
519
    IPAddressLog if the port has a public IPv4 address from a public network.
520

521
    """
522
    if port.machine is not None:
523
        raise faults.Conflict("Port %s is already in use." % port.id)
524
    if port.network.public:
525
        ipv4_address = port.ipv4_address
526
        if ipv4_address is not None:
527
            ip_log = IPAddressLog.objects.create(server_id=machine.id,
528
                                                 network_id=port.network_id,
529
                                                 address=ipv4_address,
530
                                                 active=True)
531
            log.debug("Created IP log entry %s", ip_log)
532
    port.machine = machine
533
    port.state = "BUILD"
534
    port.device_owner = "vm"
535
    port.save()
536
    return port
537

    
538

    
539
@transaction.commit_on_success
540
def delete_port(port):
541
    """Delete a port by removing the NIC card from the instance.
542

543
    Send a Job to remove the NIC card from the instance. The port
544
    will be deleted and the associated IPv4 addressess will be released
545
    when the job completes successfully.
546

547
    """
548

    
549
    vm = port.machine
550
    if vm is not None and not vm.deleted:
551
        vm = disconnect(port.machine, port)
552
        log.info("Removing port %s, Job: %s", port, vm.task_job_id)
553
    else:
554
        backend.remove_nic_ips(port)
555
        port.delete()
556
        log.info("Removed port %s", port)
557

    
558
    return port
559

    
560

    
561
def create_instance_ports(user_id, networks=None):
562
    # First connect the instance to the networks defined by the admin
563
    forced_ports = create_ports_for_setting(user_id, category="admin")
564
    if networks is None:
565
        # If the user did not asked for any networks, connect instance to
566
        # default networks as defined by the admin
567
        ports = create_ports_for_setting(user_id, category="default")
568
    else:
569
        # Else just connect to the networks that the user defined
570
        ports = create_ports_for_request(user_id, networks)
571
    total_ports = forced_ports + ports
572
    if len(total_ports) > settings.GANETI_MAX_NICS_PER_INSTANCE:
573
        raise faults.BadRequest("Maximum ports per server limit reached")
574
    return total_ports
575

    
576

    
577
def create_ports_for_setting(user_id, category):
578
    if category == "admin":
579
        network_setting = settings.CYCLADES_FORCED_SERVER_NETWORKS
580
        exception = faults.ServiceUnavailable
581
    elif category == "default":
582
        network_setting = settings.CYCLADES_DEFAULT_SERVER_NETWORKS
583
        exception = faults.Conflict
584
    else:
585
        raise ValueError("Unknown category: %s" % category)
586

    
587
    ports = []
588
    for network_ids in network_setting:
589
        # Treat even simple network IDs as group of networks with one network
590
        if type(network_ids) not in (list, tuple):
591
            network_ids = [network_ids]
592

    
593
        error_msgs = []
594
        for network_id in network_ids:
595
            success = False
596
            try:
597
                ports.append(_port_from_setting(user_id, network_id, category))
598
                # Port successfully created in one of the networks. Skip the
599
                # the rest.
600
                success = True
601
                break
602
            except faults.Conflict as e:
603
                if len(network_ids) == 1:
604
                    raise exception(e.message)
605
                else:
606
                    error_msgs.append(e.message)
607

    
608
        if not success:
609
            if category == "admin":
610
                log.error("Cannot connect server to forced networks '%s': %s",
611
                          network_ids, error_msgs)
612
                raise exception("Cannot connect server to forced server"
613
                                " networks.")
614
            else:
615
                log.debug("Cannot connect server to default networks '%s': %s",
616
                          network_ids, error_msgs)
617
                raise exception("Cannot connect server to default server"
618
                                " networks.")
619

    
620
    return ports
621

    
622

    
623
def _port_from_setting(user_id, network_id, category):
624
    # TODO: Fix this..you need only IPv4 and only IPv6 network
625
    if network_id == "SNF:ANY_PUBLIC_IPV4":
626
        return create_public_ipv4_port(user_id, category=category)
627
    elif network_id == "SNF:ANY_PUBLIC_IPV6":
628
        return create_public_ipv6_port(user_id, category=category)
629
    elif network_id == "SNF:ANY_PUBLIC":
630
        try:
631
            return create_public_ipv4_port(user_id, category=category)
632
        except faults.Conflict as e1:
633
            try:
634
                return create_public_ipv6_port(user_id, category=category)
635
            except faults.Conflict as e2:
636
                log.error("Failed to connect server to a public IPv4 or IPv6"
637
                          " network. IPv4: %s, IPv6: %s", e1, e2)
638
                msg = ("Cannot connect server to a public IPv4 or IPv6"
639
                       " network.")
640
                raise faults.Conflict(msg)
641
    else:  # Case of network ID
642
        if category in ["user", "default"]:
643
            return _port_for_request(user_id, {"uuid": network_id})
644
        elif category == "admin":
645
            network = util.get_network(network_id, user_id, non_deleted=True)
646
            return _create_port(user_id, network)
647
        else:
648
            raise ValueError("Unknown category: %s" % category)
649

    
650

    
651
def create_public_ipv4_port(user_id, network=None, address=None,
652
                            category="user"):
653
    """Create a port in a public IPv4 network.
654

655
    Create a port in a public IPv4 network (that may also have an IPv6
656
    subnet). If the category is 'user' or 'default' this will try to use
657
    one of the users floating IPs. If the category is 'admin' will
658
    create a port to the public network (without floating IPs or quotas).
659

660
    """
661
    if category in ["user", "default"]:
662
        if address is None:
663
            ipaddress = ips.get_free_floating_ip(user_id, network)
664
        else:
665
            ipaddress = util.get_floating_ip_by_address(user_id, address,
666
                                                        for_update=True)
667
    elif category == "admin":
668
        if network is None:
669
            ipaddress = ips.allocate_public_ip(user_id)
670
        else:
671
            ipaddress = ips.allocate_ip(network, user_id)
672
    else:
673
        raise ValueError("Unknown category: %s" % category)
674
    if network is None:
675
        network = ipaddress.network
676
    return _create_port(user_id, network, use_ipaddress=ipaddress)
677

    
678

    
679
def create_public_ipv6_port(user_id, category=None):
680
    """Create a port in a public IPv6 only network."""
681
    networks = Network.objects.filter(public=True, deleted=False,
682
                                      drained=False, subnets__ipversion=6)\
683
                              .exclude(subnets__ipversion=4)
684
    if networks:
685
        return _create_port(user_id, networks[0])
686
    else:
687
        msg = "No available IPv6 only network!"
688
        log.error(msg)
689
        raise faults.Conflict(msg)
690

    
691

    
692
def create_ports_for_request(user_id, networks):
693
    """Create the server ports requested by the user.
694

695
    Create the ports for the new servers as requested in the 'networks'
696
    attribute. The networks attribute contains either a list of network IDs
697
    ('uuid') or a list of ports IDs ('port'). In case of network IDs, the user
698
    can also specify an IPv4 address ('fixed_ip'). In order to connect to a
699
    public network, the 'fixed_ip' attribute must contain the IPv4 address of a
700
    floating IP. If the network is public but the 'fixed_ip' attribute is not
701
    specified, the system will automatically reserve one of the users floating
702
    IPs.
703

704
    """
705
    if not isinstance(networks, list):
706
        raise faults.BadRequest("Malformed request. Invalid 'networks' field")
707
    return [_port_for_request(user_id, network) for network in networks]
708

    
709

    
710
def _port_for_request(user_id, network_dict):
711
    if not isinstance(network_dict, dict):
712
        raise faults.BadRequest("Malformed request. Invalid 'networks' field")
713
    port_id = network_dict.get("port")
714
    network_id = network_dict.get("uuid")
715
    if port_id is not None:
716
        return util.get_port(port_id, user_id, for_update=True)
717
    elif network_id is not None:
718
        address = network_dict.get("fixed_ip")
719
        network = util.get_network(network_id, user_id, non_deleted=True)
720
        if network.public:
721
            if network.subnet4 is not None:
722
                if not "fixed_ip" in network_dict:
723
                    return create_public_ipv4_port(user_id, network)
724
                elif address is None:
725
                    msg = "Cannot connect to public network"
726
                    raise faults.BadRequest(msg % network.id)
727
                else:
728
                    return create_public_ipv4_port(user_id, network, address)
729
            else:
730
                raise faults.Forbidden("Cannot connect to IPv6 only public"
731
                                       " network %" % network.id)
732
        else:
733
            return _create_port(user_id, network, address=address)
734
    else:
735
        raise faults.BadRequest("Network 'uuid' or 'port' attribute"
736
                                " is required.")