Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (30.1 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 datetime import datetime
33
from socket import getfqdn
34
from functools import wraps
35
from django import dispatch
36
from django.db import transaction
37
from django.utils import simplejson as json
38

    
39
from snf_django.lib.api import faults
40
from django.conf import settings
41
from synnefo import quotas
42
from synnefo.api import util
43
from synnefo.logic import backend, ips, utils
44
from synnefo.logic.backend_allocator import BackendAllocator
45
from synnefo.db.models import (NetworkInterface, VirtualMachine,
46
                               VirtualMachineMetadata, IPAddressLog, Network,
47
                               pooled_rapi_client)
48
from vncauthproxy.client import request_forwarding as request_vnc_forwarding
49
from synnefo.logic import rapi
50

    
51
log = logging.getLogger(__name__)
52

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

    
56

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

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

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

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

    
89

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

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

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

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

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

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

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

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

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

    
163
            return vm
164
        return wrapper
165
    return decorator
166

    
167

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

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

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

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

    
193
    if project is None:
194
        project = userid
195

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

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

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

    
219
    # Create the server in Ganeti.
220
    vm = create_server(vm, ports, flavor, image, personality, password)
221

    
222
    return vm
223

    
224

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

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

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

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

    
244

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

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

    
275
    return jobID
276

    
277

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

    
291

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

    
297

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

    
303

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

    
311
    return backend.reboot_instance(vm, reboot_type.lower(),
312
                                   shutdown_timeout=shutdown_timeout)
313

    
314

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

    
321

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

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

    
337

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

    
348

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

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

    
358

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

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

    
367
    return backend.connect_to_network(vm, port)
368

    
369

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

    
375

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

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

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

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

    
389
    if vm.operstate != "STARTED":
390
        raise faults.BadRequest('Server not in ACTIVE state.')
391

    
392
    # Use RAPI to get VNC console information for this instance
393
    # RAPI GetInstanceConsole() returns endpoints to the vnc_bind_address,
394
    # which is a cluster-wide setting, either 0.0.0.0 or 127.0.0.1, and pretty
395
    # useless (see #783).
396
    #
397
    # Until this is fixed on the Ganeti side, construct a console info reply
398
    # directly.
399
    #
400
    # WARNING: This assumes that VNC runs on port network_port on
401
    #          the instance's primary node, and is probably
402
    #          hypervisor-specific.
403
    def get_console_data(i):
404
        return {"kind": "vnc",
405
                "host": i["pnode"],
406
                "port": i["network_port"]}
407
    with pooled_rapi_client(vm) as c:
408
        i = c.GetInstance(vm.backend_vm_id)
409
    console_data = get_console_data(i)
410

    
411
    if vm.backend.hypervisor == "kvm" and i['hvparams']['serial_console']:
412
        raise Exception("hv parameter serial_console cannot be true")
413

    
414
    # Check that the instance is really running
415
    if not i["oper_state"]:
416
        log.warning("VM '%s' is marked as '%s' in DB while DOWN in Ganeti",
417
                    vm.id, vm.operstate)
418
        # Instance is not running. Mock a shutdown job to sync DB
419
        backend.process_op_status(vm, etime=datetime.now(), jobid=0,
420
                                  opcode="OP_INSTANCE_SHUTDOWN",
421
                                  status="success",
422
                                  logmsg="Reconciliation simulated event")
423
        raise faults.BadRequest('Server not in ACTIVE state.')
424

    
425
    # Let vncauthproxy decide on the source port.
426
    # The alternative: static allocation, e.g.
427
    # sport = console_data['port'] - 1000
428
    sport = 0
429
    daddr = console_data['host']
430
    dport = console_data['port']
431
    password = util.random_password()
432

    
433
    vnc_extra_opts = settings.CYCLADES_VNCAUTHPROXY_OPTS
434
    fwd = request_vnc_forwarding(sport, daddr, dport, password,
435
                                 **vnc_extra_opts)
436

    
437
    if fwd['status'] != "OK":
438
        raise faults.ServiceUnavailable('vncauthproxy returned error status')
439

    
440
    # Verify that the VNC server settings haven't changed
441
    with pooled_rapi_client(vm) as c:
442
        i = c.GetInstance(vm.backend_vm_id)
443
    if get_console_data(i) != console_data:
444
        raise faults.ServiceUnavailable('VNC Server settings changed.')
445

    
446
    console = {
447
        'type': 'vnc',
448
        'host': getfqdn(),
449
        'port': fwd['source_port'],
450
        'password': password}
451

    
452
    return console
453

    
454

    
455
def rename(server, new_name):
456
    """Rename a VirtualMachine."""
457
    old_name = server.name
458
    server.name = new_name
459
    server.save()
460
    log.info("Renamed server '%s' from '%s' to '%s'", server, old_name,
461
             new_name)
462
    return server
463

    
464

    
465
@transaction.commit_on_success
466
def create_port(*args, **kwargs):
467
    vm = kwargs.get("machine", None)
468
    if vm is None and len(args) >= 3:
469
        vm = args[2]
470
    if vm is not None:
471
        if vm.nics.count() == settings.GANETI_MAX_NICS_PER_INSTANCE:
472
            raise faults.BadRequest("Maximum ports per server limit reached")
473
    return _create_port(*args, **kwargs)
474

    
475

    
476
def _create_port(userid, network, machine=None, use_ipaddress=None,
477
                 address=None, name="", security_groups=None,
478
                 device_owner=None):
479
    """Create a new port on the specified network.
480

481
    Create a new Port(NetworkInterface model) on the specified Network. If
482
    'machine' is specified, the machine will be connected to the network using
483
    this port. If 'use_ipaddress' argument is specified, the port will be
484
    assigned this IPAddress. Otherwise, an IPv4 address from the IPv4 subnet
485
    will be allocated.
486

487
    """
488
    if network.state != "ACTIVE":
489
        raise faults.Conflict("Cannot create port while network '%s' is in"
490
                              " '%s' status" % (network.id, network.state))
491
    elif network.action == "DESTROY":
492
        msg = "Cannot create port. Network %s is being deleted."
493
        raise faults.Conflict(msg % network.id)
494
    elif network.drained:
495
        raise faults.Conflict("Cannot create port while network %s is in"
496
                              " 'SNF:DRAINED' status" % network.id)
497

    
498
    utils.check_name_length(name, NetworkInterface.NETWORK_IFACE_NAME_LENGTH,
499
                            "Port name is too long")
500

    
501
    ipaddress = None
502
    if use_ipaddress is not None:
503
        # Use an existing IPAddress object.
504
        ipaddress = use_ipaddress
505
        if ipaddress and (ipaddress.network_id != network.id):
506
            msg = "IP Address %s does not belong to network %s"
507
            raise faults.Conflict(msg % (ipaddress.address, network.id))
508
    else:
509
        # If network has IPv4 subnets, try to allocate the address that the
510
        # the user specified or a random one.
511
        if network.subnets.filter(ipversion=4).exists():
512
            ipaddress = ips.allocate_ip(network, userid=userid,
513
                                        address=address)
514
        elif address is not None:
515
            raise faults.BadRequest("Address %s is not a valid IP for the"
516
                                    " defined network subnets" % address)
517

    
518
    if ipaddress is not None and ipaddress.nic is not None:
519
        raise faults.Conflict("IP address '%s' is already in use" %
520
                              ipaddress.address)
521

    
522
    port = NetworkInterface.objects.create(network=network,
523
                                           state="DOWN",
524
                                           userid=userid,
525
                                           device_owner=None,
526
                                           public=network.public,
527
                                           name=name)
528

    
529
    # add the security groups if any
530
    if security_groups:
531
        port.security_groups.add(*security_groups)
532

    
533
    if ipaddress is not None:
534
        # Associate IPAddress with the Port
535
        ipaddress.nic = port
536
        ipaddress.save()
537

    
538
    if machine is not None:
539
        # Connect port to the instance.
540
        machine = connect(machine, network, port)
541
        jobID = machine.task_job_id
542
        log.info("Created Port %s with IP %s. Ganeti Job: %s",
543
                 port, ipaddress, jobID)
544
    else:
545
        log.info("Created Port %s with IP %s not attached to any instance",
546
                 port, ipaddress)
547

    
548
    return port
549

    
550

    
551
def associate_port_with_machine(port, machine):
552
    """Associate a Port with a VirtualMachine.
553

554
    Associate the port with the VirtualMachine and add an entry to the
555
    IPAddressLog if the port has a public IPv4 address from a public network.
556

557
    """
558
    if port.machine is not None:
559
        raise faults.Conflict("Port %s is already in use." % port.id)
560
    if port.network.public:
561
        ipv4_address = port.ipv4_address
562
        if ipv4_address is not None:
563
            ip_log = IPAddressLog.objects.create(server_id=machine.id,
564
                                                 network_id=port.network_id,
565
                                                 address=ipv4_address,
566
                                                 active=True)
567
            log.debug("Created IP log entry %s", ip_log)
568
    port.machine = machine
569
    port.state = "BUILD"
570
    port.device_owner = "vm"
571
    port.save()
572
    return port
573

    
574

    
575
@transaction.commit_on_success
576
def delete_port(port):
577
    """Delete a port by removing the NIC card from the instance.
578

579
    Send a Job to remove the NIC card from the instance. The port
580
    will be deleted and the associated IPv4 addressess will be released
581
    when the job completes successfully.
582

583
    """
584

    
585
    vm = port.machine
586
    if vm is not None and not vm.deleted:
587
        vm = disconnect(port.machine, port)
588
        log.info("Removing port %s, Job: %s", port, vm.task_job_id)
589
    else:
590
        backend.remove_nic_ips(port)
591
        port.delete()
592
        log.info("Removed port %s", port)
593

    
594
    return port
595

    
596

    
597
def create_instance_ports(user_id, networks=None):
598
    # First connect the instance to the networks defined by the admin
599
    forced_ports = create_ports_for_setting(user_id, category="admin")
600
    if networks is None:
601
        # If the user did not asked for any networks, connect instance to
602
        # default networks as defined by the admin
603
        ports = create_ports_for_setting(user_id, category="default")
604
    else:
605
        # Else just connect to the networks that the user defined
606
        ports = create_ports_for_request(user_id, networks)
607
    total_ports = forced_ports + ports
608
    if len(total_ports) > settings.GANETI_MAX_NICS_PER_INSTANCE:
609
        raise faults.BadRequest("Maximum ports per server limit reached")
610
    return total_ports
611

    
612

    
613
def create_ports_for_setting(user_id, category):
614
    if category == "admin":
615
        network_setting = settings.CYCLADES_FORCED_SERVER_NETWORKS
616
        exception = faults.ServiceUnavailable
617
    elif category == "default":
618
        network_setting = settings.CYCLADES_DEFAULT_SERVER_NETWORKS
619
        exception = faults.Conflict
620
    else:
621
        raise ValueError("Unknown category: %s" % category)
622

    
623
    ports = []
624
    for network_ids in network_setting:
625
        # Treat even simple network IDs as group of networks with one network
626
        if type(network_ids) not in (list, tuple):
627
            network_ids = [network_ids]
628

    
629
        error_msgs = []
630
        for network_id in network_ids:
631
            success = False
632
            try:
633
                ports.append(_port_from_setting(user_id, network_id, category))
634
                # Port successfully created in one of the networks. Skip the
635
                # the rest.
636
                success = True
637
                break
638
            except faults.Conflict as e:
639
                if len(network_ids) == 1:
640
                    raise exception(e.message)
641
                else:
642
                    error_msgs.append(e.message)
643

    
644
        if not success:
645
            if category == "admin":
646
                log.error("Cannot connect server to forced networks '%s': %s",
647
                          network_ids, error_msgs)
648
                raise exception("Cannot connect server to forced server"
649
                                " networks.")
650
            else:
651
                log.debug("Cannot connect server to default networks '%s': %s",
652
                          network_ids, error_msgs)
653
                raise exception("Cannot connect server to default server"
654
                                " networks.")
655

    
656
    return ports
657

    
658

    
659
def _port_from_setting(user_id, network_id, category):
660
    # TODO: Fix this..you need only IPv4 and only IPv6 network
661
    if network_id == "SNF:ANY_PUBLIC_IPV4":
662
        return create_public_ipv4_port(user_id, category=category)
663
    elif network_id == "SNF:ANY_PUBLIC_IPV6":
664
        return create_public_ipv6_port(user_id, category=category)
665
    elif network_id == "SNF:ANY_PUBLIC":
666
        try:
667
            return create_public_ipv4_port(user_id, category=category)
668
        except faults.Conflict as e1:
669
            try:
670
                return create_public_ipv6_port(user_id, category=category)
671
            except faults.Conflict as e2:
672
                log.error("Failed to connect server to a public IPv4 or IPv6"
673
                          " network. IPv4: %s, IPv6: %s", e1, e2)
674
                msg = ("Cannot connect server to a public IPv4 or IPv6"
675
                       " network.")
676
                raise faults.Conflict(msg)
677
    else:  # Case of network ID
678
        if category in ["user", "default"]:
679
            return _port_for_request(user_id, {"uuid": network_id})
680
        elif category == "admin":
681
            network = util.get_network(network_id, user_id, non_deleted=True)
682
            return _create_port(user_id, network)
683
        else:
684
            raise ValueError("Unknown category: %s" % category)
685

    
686

    
687
def create_public_ipv4_port(user_id, network=None, address=None,
688
                            category="user"):
689
    """Create a port in a public IPv4 network.
690

691
    Create a port in a public IPv4 network (that may also have an IPv6
692
    subnet). If the category is 'user' or 'default' this will try to use
693
    one of the users floating IPs. If the category is 'admin' will
694
    create a port to the public network (without floating IPs or quotas).
695

696
    """
697
    if category in ["user", "default"]:
698
        if address is None:
699
            ipaddress = ips.get_free_floating_ip(user_id, network)
700
        else:
701
            ipaddress = util.get_floating_ip_by_address(user_id, address,
702
                                                        for_update=True)
703
    elif category == "admin":
704
        if network is None:
705
            ipaddress = ips.allocate_public_ip(user_id)
706
        else:
707
            ipaddress = ips.allocate_ip(network, user_id)
708
    else:
709
        raise ValueError("Unknown category: %s" % category)
710
    if network is None:
711
        network = ipaddress.network
712
    return _create_port(user_id, network, use_ipaddress=ipaddress)
713

    
714

    
715
def create_public_ipv6_port(user_id, category=None):
716
    """Create a port in a public IPv6 only network."""
717
    networks = Network.objects.filter(public=True, deleted=False,
718
                                      drained=False, subnets__ipversion=6)\
719
                              .exclude(subnets__ipversion=4)
720
    if networks:
721
        return _create_port(user_id, networks[0])
722
    else:
723
        msg = "No available IPv6 only network!"
724
        log.error(msg)
725
        raise faults.Conflict(msg)
726

    
727

    
728
def create_ports_for_request(user_id, networks):
729
    """Create the server ports requested by the user.
730

731
    Create the ports for the new servers as requested in the 'networks'
732
    attribute. The networks attribute contains either a list of network IDs
733
    ('uuid') or a list of ports IDs ('port'). In case of network IDs, the user
734
    can also specify an IPv4 address ('fixed_ip'). In order to connect to a
735
    public network, the 'fixed_ip' attribute must contain the IPv4 address of a
736
    floating IP. If the network is public but the 'fixed_ip' attribute is not
737
    specified, the system will automatically reserve one of the users floating
738
    IPs.
739

740
    """
741
    if not isinstance(networks, list):
742
        raise faults.BadRequest("Malformed request. Invalid 'networks' field")
743
    return [_port_for_request(user_id, network) for network in networks]
744

    
745

    
746
def _port_for_request(user_id, network_dict):
747
    if not isinstance(network_dict, dict):
748
        raise faults.BadRequest("Malformed request. Invalid 'networks' field")
749
    port_id = network_dict.get("port")
750
    network_id = network_dict.get("uuid")
751
    if port_id is not None:
752
        return util.get_port(port_id, user_id, for_update=True)
753
    elif network_id is not None:
754
        address = network_dict.get("fixed_ip")
755
        network = util.get_network(network_id, user_id, non_deleted=True)
756
        if network.public:
757
            if network.subnet4 is not None:
758
                if not "fixed_ip" in network_dict:
759
                    return create_public_ipv4_port(user_id, network)
760
                elif address is None:
761
                    msg = "Cannot connect to public network"
762
                    raise faults.BadRequest(msg % network.id)
763
                else:
764
                    return create_public_ipv4_port(user_id, network, address)
765
            else:
766
                raise faults.Forbidden("Cannot connect to IPv6 only public"
767
                                       " network %" % network.id)
768
        else:
769
            return _create_port(user_id, network, address=address)
770
    else:
771
        raise faults.BadRequest("Network 'uuid' or 'port' attribute"
772
                                " is required.")