Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (28.5 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
    old_name = server.name
420
    server.name = new_name
421
    server.save()
422
    log.info("Renamed server '%s' from '%s' to '%s'", server, old_name,
423
             new_name)
424
    return server
425

    
426

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

    
437

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

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

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

    
460
    utils.check_name_length(name, NetworkInterface.NETWORK_IFACE_NAME_LENGTH,
461
                            "Port name is too long")
462

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

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

    
484
    port = NetworkInterface.objects.create(network=network,
485
                                           state="DOWN",
486
                                           userid=userid,
487
                                           device_owner=None,
488
                                           name=name)
489

    
490
    # add the security groups if any
491
    if security_groups:
492
        port.security_groups.add(*security_groups)
493

    
494
    if ipaddress is not None:
495
        # Associate IPAddress with the Port
496
        ipaddress.nic = port
497
        ipaddress.save()
498

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

    
509
    return port
510

    
511

    
512
def associate_port_with_machine(port, machine):
513
    """Associate a Port with a VirtualMachine.
514

515
    Associate the port with the VirtualMachine and add an entry to the
516
    IPAddressLog if the port has a public IPv4 address from a public network.
517

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

    
535

    
536
@transaction.commit_on_success
537
def delete_port(port):
538
    """Delete a port by removing the NIC card from the instance.
539

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

544
    """
545

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

    
555
    return port
556

    
557

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

    
573

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

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

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

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

    
617
    return ports
618

    
619

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

    
647

    
648
def create_public_ipv4_port(user_id, network=None, address=None,
649
                            category="user"):
650
    """Create a port in a public IPv4 network.
651

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

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

    
675

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

    
688

    
689
def create_ports_for_request(user_id, networks):
690
    """Create the server ports requested by the user.
691

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

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

    
706

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