Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (27.7 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
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_serial(vm.serial)
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_serial(vm.serial)
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
    # Create the ports for the server
174
    ports = create_instance_ports(userid, networks)
175

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

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

    
198
    # Associate the ports with the server
199
    for index, port in enumerate(ports):
200
        associate_port_with_machine(port, vm)
201
        port.index = index
202
        port.save()
203

    
204
    for key, val in metadata.items():
205
        VirtualMachineMetadata.objects.create(
206
            meta_key=key,
207
            meta_value=val,
208
            vm=vm)
209

    
210
    # Create the server in Ganeti.
211
    vm = create_server(vm, ports, flavor, image, personality, password)
212

    
213
    return vm
214

    
215

    
216
@transaction.commit_on_success
217
def allocate_new_server(userid, flavor):
218
    """Allocate a new server to a Ganeti backend.
219

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

224
    This function runs inside a transaction, because after allocating the
225
    instance a commit must be performed in order to release all locks.
226

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

    
235

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

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

    
266
    return jobID
267

    
268

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

    
282

    
283
@server_command("START")
284
def start(vm):
285
    log.info("Starting VM %s", vm)
286
    return backend.startup_instance(vm)
287

    
288

    
289
@server_command("STOP")
290
def stop(vm):
291
    log.info("Stopping VM %s", vm)
292
    return backend.shutdown_instance(vm)
293

    
294

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

    
302
    return backend.reboot_instance(vm, reboot_type.lower())
303

    
304

    
305
def resize(vm, flavor):
306
    action_fields = {"beparams": {"vcpus": flavor.cpu,
307
                                  "maxmem": flavor.ram}}
308
    comm = server_command("RESIZE", action_fields=action_fields)
309
    return comm(_resize)(vm, flavor)
310

    
311

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

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

    
327

    
328
@server_command("SET_FIREWALL_PROFILE")
329
def set_firewall_profile(vm, profile, nic):
330
    log.info("Setting VM %s, NIC %s, firewall %s", vm, nic, profile)
331

    
332
    if profile not in [x[0] for x in NetworkInterface.FIREWALL_PROFILES]:
333
        raise faults.BadRequest("Unsupported firewall profile")
334
    backend.set_firewall_profile(vm, profile=profile, nic=nic)
335
    return None
336

    
337

    
338
@server_command("CONNECT")
339
def connect(vm, network, port=None):
340
    if port is None:
341
        port = _create_port(vm.userid, network)
342
    associate_port_with_machine(port, vm)
343

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

    
346
    return backend.connect_to_network(vm, port)
347

    
348

    
349
@server_command("DISCONNECT")
350
def disconnect(vm, nic):
351
    log.info("Removing NIC %s from VM %s", nic, vm)
352
    return backend.disconnect_from_network(vm, nic)
353

    
354

    
355
def console(vm, console_type):
356
    """Arrange for an OOB console of the specified type
357

358
    This method arranges for an OOB console of the specified type.
359
    Only consoles of type "vnc" are supported for now.
360

361
    It uses a running instance of vncauthproxy to setup proper
362
    VNC forwarding with a random password, then returns the necessary
363
    VNC connection info to the caller.
364

365
    """
366
    log.info("Get console  VM %s, type %s", vm, console_type)
367

    
368
    # Use RAPI to get VNC console information for this instance
369
    if vm.operstate != "STARTED":
370
        raise faults.BadRequest('Server not in ACTIVE state.')
371

    
372
    if settings.TEST:
373
        console_data = {'kind': 'vnc', 'host': 'ganeti_node', 'port': 1000}
374
    else:
375
        console_data = backend.get_instance_console(vm)
376

    
377
    if console_data['kind'] != 'vnc':
378
        message = 'got console of kind %s, not "vnc"' % console_data['kind']
379
        raise faults.ServiceUnavailable(message)
380

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

    
389
    if settings.TEST:
390
        fwd = {'source_port': 1234, 'status': 'OK'}
391
    else:
392
        vnc_extra_opts = settings.CYCLADES_VNCAUTHPROXY_OPTS
393
        fwd = request_vnc_forwarding(sport, daddr, dport, password,
394
                                     **vnc_extra_opts)
395

    
396
    if fwd['status'] != "OK":
397
        raise faults.ServiceUnavailable('vncauthproxy returned error status')
398

    
399
    # Verify that the VNC server settings haven't changed
400
    if not settings.TEST:
401
        if console_data != backend.get_instance_console(vm):
402
            raise faults.ServiceUnavailable('VNC Server settings changed.')
403

    
404
    console = {
405
        'type': 'vnc',
406
        'host': getfqdn(),
407
        'port': fwd['source_port'],
408
        'password': password}
409

    
410
    return console
411

    
412

    
413
def rename(server, new_name):
414
    """Rename a VirtualMachine."""
415
    old_name = server.name
416
    server.name = new_name
417
    server.save()
418
    log.info("Renamed server '%s' from '%s' to '%s'", server, old_name,
419
             new_name)
420
    return server
421

    
422

    
423
@transaction.commit_on_success
424
def create_port(*args, **kwargs):
425
    vm = kwargs.get("machine", None)
426
    if vm is None and len(args) >= 3:
427
        vm = args[2]
428
    if vm is not None:
429
        if vm.nics.count() == settings.GANETI_MAX_NICS_PER_INSTANCE:
430
            raise faults.BadRequest("Maximum ports per server limit reached")
431
    return _create_port(*args, **kwargs)
432

    
433

    
434
def _create_port(userid, network, machine=None, use_ipaddress=None,
435
                 address=None, name="", security_groups=None,
436
                 device_owner=None):
437
    """Create a new port on the specified network.
438

439
    Create a new Port(NetworkInterface model) on the specified Network. If
440
    'machine' is specified, the machine will be connected to the network using
441
    this port. If 'use_ipaddress' argument is specified, the port will be
442
    assigned this IPAddress. Otherwise, an IPv4 address from the IPv4 subnet
443
    will be allocated.
444

445
    """
446
    if network.state != "ACTIVE":
447
        raise faults.Conflict("Cannot create port while network '%s' is in"
448
                              " '%s' status" % (network.id, network.state))
449
    elif network.action == "DESTROY":
450
        msg = "Cannot create port. Network %s is being deleted."
451
        raise faults.Conflict(msg % network.id)
452
    elif network.drained:
453
        raise faults.Conflict("Cannot create port while network %s is in"
454
                              " 'SNF:DRAINED' status" % network.id)
455

    
456
    ipaddress = None
457
    if use_ipaddress is not None:
458
        # Use an existing IPAddress object.
459
        ipaddress = use_ipaddress
460
        if ipaddress and (ipaddress.network_id != network.id):
461
            msg = "IP Address %s does not belong to network %s"
462
            raise faults.Conflict(msg % (ipaddress.address, network.id))
463
    else:
464
        # If network has IPv4 subnets, try to allocate the address that the
465
        # the user specified or a random one.
466
        if network.subnets.filter(ipversion=4).exists():
467
            ipaddress = ips.allocate_ip(network, userid=userid,
468
                                        address=address)
469
        elif address is not None:
470
            raise faults.BadRequest("Address %s is not a valid IP for the"
471
                                    " defined network subnets" % address)
472

    
473
    if ipaddress is not None and ipaddress.nic is not None:
474
        raise faults.Conflict("IP address '%s' is already in use" %
475
                              ipaddress.address)
476

    
477
    port = NetworkInterface.objects.create(network=network,
478
                                           state="DOWN",
479
                                           userid=userid,
480
                                           device_owner=None,
481
                                           name=name)
482

    
483
    # add the security groups if any
484
    if security_groups:
485
        port.security_groups.add(*security_groups)
486

    
487
    if ipaddress is not None:
488
        # Associate IPAddress with the Port
489
        ipaddress.nic = port
490
        ipaddress.save()
491

    
492
    if machine is not None:
493
        # Connect port to the instance.
494
        machine = connect(machine, network, port)
495
        jobID = machine.task_job_id
496
        log.info("Created Port %s with IP %s. Ganeti Job: %s",
497
                 port, ipaddress, jobID)
498
    else:
499
        log.info("Created Port %s with IP %s not attached to any instance",
500
                 port, ipaddress)
501

    
502
    return port
503

    
504

    
505
def associate_port_with_machine(port, machine):
506
    """Associate a Port with a VirtualMachine.
507

508
    Associate the port with the VirtualMachine and add an entry to the
509
    IPAddressLog if the port has a public IPv4 address from a public network.
510

511
    """
512
    if port.machine is not None:
513
        raise faults.Conflict("Port %s is already in use." % port.id)
514
    if port.network.public:
515
        ipv4_address = port.ipv4_address
516
        if ipv4_address is not None:
517
            ip_log = IPAddressLog.objects.create(server_id=machine.id,
518
                                                 network_id=port.network_id,
519
                                                 address=ipv4_address,
520
                                                 active=True)
521
            log.debug("Created IP log entry %s", ip_log)
522
    port.machine = machine
523
    port.state = "BUILD"
524
    port.device_owner = "vm"
525
    port.save()
526
    return port
527

    
528

    
529
@transaction.commit_on_success
530
def delete_port(port):
531
    """Delete a port by removing the NIC card from the instance.
532

533
    Send a Job to remove the NIC card from the instance. The port
534
    will be deleted and the associated IPv4 addressess will be released
535
    when the job completes successfully.
536

537
    """
538

    
539
    if port.machine is not None:
540
        vm = disconnect(port.machine, port)
541
        log.info("Removing port %s, Job: %s", port, vm.task_job_id)
542
    else:
543
        backend.remove_nic_ips(port)
544
        port.delete()
545
        log.info("Removed port %s", port)
546

    
547
    return port
548

    
549

    
550
def create_instance_ports(user_id, networks=None):
551
    # First connect the instance to the networks defined by the admin
552
    forced_ports = create_ports_for_setting(user_id, category="admin")
553
    if networks is None:
554
        # If the user did not asked for any networks, connect instance to
555
        # default networks as defined by the admin
556
        ports = create_ports_for_setting(user_id, category="default")
557
    else:
558
        # Else just connect to the networks that the user defined
559
        ports = create_ports_for_request(user_id, networks)
560
    total_ports = forced_ports + ports
561
    if len(total_ports) > settings.GANETI_MAX_NICS_PER_INSTANCE:
562
        raise faults.BadRequest("Maximum ports per server limit reached")
563
    return total_ports
564

    
565

    
566
def create_ports_for_setting(user_id, category):
567
    if category == "admin":
568
        network_setting = settings.CYCLADES_FORCED_SERVER_NETWORKS
569
        exception = faults.ServiceUnavailable
570
    elif category == "default":
571
        network_setting = settings.CYCLADES_DEFAULT_SERVER_NETWORKS
572
        exception = faults.Conflict
573
    else:
574
        raise ValueError("Unknown category: %s" % category)
575

    
576
    ports = []
577
    for network_ids in network_setting:
578
        # Treat even simple network IDs as group of networks with one network
579
        if type(network_ids) not in (list, tuple):
580
            network_ids = [network_ids]
581

    
582
        error_msgs = []
583
        for network_id in network_ids:
584
            success = False
585
            try:
586
                ports.append(_port_from_setting(user_id, network_id, category))
587
                # Port successfully created in one of the networks. Skip the
588
                # the rest.
589
                success = True
590
                break
591
            except faults.Conflict as e:
592
                if len(network_ids) == 1:
593
                    raise exception(e.message)
594
                else:
595
                    error_msgs.append(e.message)
596

    
597
        if not success:
598
            if category == "admin":
599
                log.error("Cannot connect server to forced networks '%s': %s",
600
                          network_ids, error_msgs)
601
                raise exception("Cannot connect server to forced server"
602
                                " networks.")
603
            else:
604
                log.debug("Cannot connect server to default networks '%s': %s",
605
                          network_ids, error_msgs)
606
                raise exception("Cannot connect server to default server"
607
                                " networks.")
608

    
609
    return ports
610

    
611

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

    
639

    
640
def create_public_ipv4_port(user_id, network=None, address=None,
641
                            category="user"):
642
    """Create a port in a public IPv4 network.
643

644
    Create a port in a public IPv4 network (that may also have an IPv6
645
    subnet). If the category is 'user' or 'default' this will try to use
646
    one of the users floating IPs. If the category is 'admin' will
647
    create a port to the public network (without floating IPs or quotas).
648

649
    """
650
    if category in ["user", "default"]:
651
        if address is None:
652
            ipaddress = ips.get_free_floating_ip(user_id, network)
653
        else:
654
            ipaddress = util.get_floating_ip_by_address(user_id, address,
655
                                                        for_update=True)
656
    elif category == "admin":
657
        if network is None:
658
            ipaddress = ips.allocate_public_ip(user_id)
659
        else:
660
            ipaddress = ips.allocate_ip(network, user_id)
661
    else:
662
        raise ValueError("Unknown category: %s" % category)
663
    if network is None:
664
        network = ipaddress.network
665
    return _create_port(user_id, network, use_ipaddress=ipaddress)
666

    
667

    
668
def create_public_ipv6_port(user_id, category=None):
669
    """Create a port in a public IPv6 only network."""
670
    networks = Network.objects.filter(public=True, deleted=False,
671
                                      drained=False, subnets__ipversion=6)\
672
                              .exclude(subnets__ipversion=4)
673
    if networks:
674
        return _create_port(user_id, networks[0])
675
    else:
676
        msg = "No available IPv6 only network!"
677
        log.error(msg)
678
        raise faults.Conflict(msg)
679

    
680

    
681
def create_ports_for_request(user_id, networks):
682
    """Create the server ports requested by the user.
683

684
    Create the ports for the new servers as requested in the 'networks'
685
    attribute. The networks attribute contains either a list of network IDs
686
    ('uuid') or a list of ports IDs ('port'). In case of network IDs, the user
687
    can also specify an IPv4 address ('fixed_ip'). In order to connect to a
688
    public network, the 'fixed_ip' attribute must contain the IPv4 address of a
689
    floating IP. If the network is public but the 'fixed_ip' attribute is not
690
    specified, the system will automatically reserve one of the users floating
691
    IPs.
692

693
    """
694
    return [_port_for_request(user_id, network) for network in networks]
695

    
696

    
697
def _port_for_request(user_id, network_dict):
698
    port_id = network_dict.get("port")
699
    network_id = network_dict.get("uuid")
700
    if port_id is not None:
701
        return util.get_port(port_id, user_id, for_update=True)
702
    elif network_id is not None:
703
        address = network_dict.get("fixed_ip")
704
        network = util.get_network(network_id, user_id, non_deleted=True)
705
        if network.public:
706
            if network.subnet4 is not None:
707
                if not "fixed_ip" in network_dict:
708
                    return create_public_ipv4_port(user_id, network)
709
                elif address is None:
710
                    msg = "Cannot connect to public network"
711
                    raise faults.BadRequest(msg % network.id)
712
                else:
713
                    return create_public_ipv4_port(user_id, network, address)
714
            else:
715
                raise faults.Forbidden("Cannot connect to IPv6 only public"
716
                                       " network %" % network.id)
717
        else:
718
            return _create_port(user_id, network, address=address)
719
    else:
720
        raise faults.BadRequest("Network 'uuid' or 'port' attribute"
721
                                " is required.")