Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (29 kB)

1
# Copyright 2011-2014 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, project=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
    if project is None:
192
        project = userid
193

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

    
205
    # Associate the ports with the server
206
    for index, port in enumerate(ports):
207
        associate_port_with_machine(port, vm)
208
        port.index = index
209
        port.save()
210

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

    
217
    # Create the server in Ganeti.
218
    vm = create_server(vm, ports, flavor, image, personality, 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, 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, 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
@server_command("DESTROY")
277
def destroy(vm, shutdown_timeout=None):
278
    # XXX: Workaround for race where OP_INSTANCE_REMOVE starts executing on
279
    # Ganeti before OP_INSTANCE_CREATE. This will be fixed when
280
    # OP_INSTANCE_REMOVE supports the 'depends' request attribute.
281
    if (vm.backendopcode == "OP_INSTANCE_CREATE" and
282
       vm.backendjobstatus not in rapi.JOB_STATUS_FINALIZED and
283
       backend.job_is_still_running(vm) and
284
       not backend.vm_exists_in_backend(vm)):
285
            raise faults.BuildInProgress("Server is being build")
286
    log.info("Deleting VM %s", vm)
287
    return backend.delete_instance(vm, shutdown_timeout=shutdown_timeout)
288

    
289

    
290
@server_command("START")
291
def start(vm):
292
    log.info("Starting VM %s", vm)
293
    return backend.startup_instance(vm)
294

    
295

    
296
@server_command("STOP")
297
def stop(vm, shutdown_timeout=None):
298
    log.info("Stopping VM %s", vm)
299
    return backend.shutdown_instance(vm, shutdown_timeout=shutdown_timeout)
300

    
301

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

    
309
    return backend.reboot_instance(vm, reboot_type.lower(),
310
                                   shutdown_timeout=shutdown_timeout)
311

    
312

    
313
def resize(vm, flavor):
314
    action_fields = {"beparams": {"vcpus": flavor.cpu,
315
                                  "maxmem": flavor.ram}}
316
    comm = server_command("RESIZE", action_fields=action_fields)
317
    return comm(_resize)(vm, flavor)
318

    
319

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

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

    
335

    
336
@transaction.commit_on_success
337
def reassign(vm, project):
338
    action_fields = {"to_project": project, "from_project": vm.project}
339
    log.info("Reassigning VM %s from project %s to %s",
340
             vm, vm.project, project)
341
    vm.project = project
342
    vm.save()
343
    quotas.issue_and_accept_commission(vm, action="REASSIGN",
344
                                       action_fields=action_fields)
345

    
346

    
347
@server_command("SET_FIREWALL_PROFILE")
348
def set_firewall_profile(vm, profile, nic):
349
    log.info("Setting VM %s, NIC %s, firewall %s", vm, nic, profile)
350

    
351
    if profile not in [x[0] for x in NetworkInterface.FIREWALL_PROFILES]:
352
        raise faults.BadRequest("Unsupported firewall profile")
353
    backend.set_firewall_profile(vm, profile=profile, nic=nic)
354
    return None
355

    
356

    
357
@server_command("CONNECT")
358
def connect(vm, network, port=None):
359
    if port is None:
360
        port = _create_port(vm.userid, network)
361
    associate_port_with_machine(port, vm)
362

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

    
365
    return backend.connect_to_network(vm, port)
366

    
367

    
368
@server_command("DISCONNECT")
369
def disconnect(vm, nic):
370
    log.info("Removing NIC %s from VM %s", nic, vm)
371
    return backend.disconnect_from_network(vm, nic)
372

    
373

    
374
def console(vm, console_type):
375
    """Arrange for an OOB console of the specified type
376

377
    This method arranges for an OOB console of the specified type.
378
    Only consoles of type "vnc" are supported for now.
379

380
    It uses a running instance of vncauthproxy to setup proper
381
    VNC forwarding with a random password, then returns the necessary
382
    VNC connection info to the caller.
383

384
    """
385
    log.info("Get console  VM %s, type %s", vm, console_type)
386

    
387
    # Use RAPI to get VNC console information for this instance
388
    if vm.operstate != "STARTED":
389
        raise faults.BadRequest('Server not in ACTIVE state.')
390

    
391
    if settings.TEST:
392
        console_data = {'kind': 'vnc', 'host': 'ganeti_node', 'port': 1000}
393
    else:
394
        console_data = backend.get_instance_console(vm)
395

    
396
    if console_data['kind'] != 'vnc':
397
        message = 'got console of kind %s, not "vnc"' % console_data['kind']
398
        raise faults.ServiceUnavailable(message)
399

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

    
408
    if settings.TEST:
409
        fwd = {'source_port': 1234, 'status': 'OK'}
410
    else:
411
        vnc_extra_opts = settings.CYCLADES_VNCAUTHPROXY_OPTS
412
        fwd = request_vnc_forwarding(sport, daddr, dport, password,
413
                                     **vnc_extra_opts)
414

    
415
    if fwd['status'] != "OK":
416
        raise faults.ServiceUnavailable('vncauthproxy returned error status')
417

    
418
    # Verify that the VNC server settings haven't changed
419
    if not settings.TEST:
420
        if console_data != backend.get_instance_console(vm):
421
            raise faults.ServiceUnavailable('VNC Server settings changed.')
422

    
423
    console = {
424
        'type': 'vnc',
425
        'host': getfqdn(),
426
        'port': fwd['source_port'],
427
        'password': password}
428

    
429
    return console
430

    
431

    
432
def rename(server, new_name):
433
    """Rename a VirtualMachine."""
434
    old_name = server.name
435
    server.name = new_name
436
    server.save()
437
    log.info("Renamed server '%s' from '%s' to '%s'", server, old_name,
438
             new_name)
439
    return server
440

    
441

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

    
452

    
453
def _create_port(userid, network, machine=None, use_ipaddress=None,
454
                 address=None, name="", security_groups=None,
455
                 device_owner=None):
456
    """Create a new port on the specified network.
457

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

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

    
475
    utils.check_name_length(name, NetworkInterface.NETWORK_IFACE_NAME_LENGTH,
476
                            "Port name is too long")
477

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

    
495
    if ipaddress is not None and ipaddress.nic is not None:
496
        raise faults.Conflict("IP address '%s' is already in use" %
497
                              ipaddress.address)
498

    
499
    port = NetworkInterface.objects.create(network=network,
500
                                           state="DOWN",
501
                                           userid=userid,
502
                                           device_owner=None,
503
                                           public=network.public,
504
                                           name=name)
505

    
506
    # add the security groups if any
507
    if security_groups:
508
        port.security_groups.add(*security_groups)
509

    
510
    if ipaddress is not None:
511
        # Associate IPAddress with the Port
512
        ipaddress.nic = port
513
        ipaddress.save()
514

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

    
525
    return port
526

    
527

    
528
def associate_port_with_machine(port, machine):
529
    """Associate a Port with a VirtualMachine.
530

531
    Associate the port with the VirtualMachine and add an entry to the
532
    IPAddressLog if the port has a public IPv4 address from a public network.
533

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

    
551

    
552
@transaction.commit_on_success
553
def delete_port(port):
554
    """Delete a port by removing the NIC card from the instance.
555

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

560
    """
561

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

    
571
    return port
572

    
573

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

    
589

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

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

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

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

    
633
    return ports
634

    
635

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

    
663

    
664
def create_public_ipv4_port(user_id, network=None, address=None,
665
                            category="user"):
666
    """Create a port in a public IPv4 network.
667

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

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

    
691

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

    
704

    
705
def create_ports_for_request(user_id, networks):
706
    """Create the server ports requested by the user.
707

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

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

    
722

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