Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (24.8 kB)

1
import logging
2

    
3
from socket import getfqdn
4
from functools import wraps
5
from django import dispatch
6
from django.db import transaction
7
from django.utils import simplejson as json
8

    
9
from snf_django.lib.api import faults
10
from django.conf import settings
11
from synnefo import quotas
12
from synnefo.api import util
13
from synnefo.logic import backend, ips
14
from synnefo.logic.backend_allocator import BackendAllocator
15
from synnefo.db.models import (NetworkInterface, VirtualMachine,
16
                               VirtualMachineMetadata, IPAddressLog, Network)
17
from vncauthproxy.client import request_forwarding as request_vnc_forwarding
18

    
19
log = logging.getLogger(__name__)
20

    
21
# server creation signal
22
server_created = dispatch.Signal(providing_args=["created_vm_params"])
23

    
24

    
25
def validate_server_action(vm, action):
26
    if vm.deleted:
27
        raise faults.BadRequest("Server '%s' has been deleted." % vm.id)
28

    
29
    # Destroyin a server should always be permitted
30
    if action == "DESTROY":
31
        return
32

    
33
    # Check that there is no pending action
34
    pending_action = vm.task
35
    if pending_action:
36
        if pending_action == "BUILD":
37
            raise faults.BuildInProgress("Server '%s' is being build." % vm.id)
38
        raise faults.BadRequest("Cannot perform '%s' action while there is a"
39
                                " pending '%s'." % (action, pending_action))
40

    
41
    # Check if action can be performed to VM's operstate
42
    operstate = vm.operstate
43
    if operstate == "ERROR":
44
        raise faults.BadRequest("Cannot perform '%s' action while server is"
45
                                " in 'ERROR' state." % action)
46
    elif operstate == "BUILD" and action != "BUILD":
47
        raise faults.BuildInProgress("Server '%s' is being build." % vm.id)
48
    elif (action == "START" and operstate != "STOPPED") or\
49
         (action == "STOP" and operstate != "STARTED") or\
50
         (action == "RESIZE" and operstate != "STOPPED") or\
51
         (action in ["CONNECT", "DISCONNECT"] and operstate != "STOPPED"
52
          and not settings.GANETI_USE_HOTPLUG):
53
        raise faults.BadRequest("Cannot perform '%s' action while server is"
54
                                " in '%s' state." % (action, operstate))
55
    return
56

    
57

    
58
def server_command(action):
59
    """Handle execution of a server action.
60

61
    Helper function to validate and execute a server action, handle quota
62
    commission and update the 'task' of the VM in the DB.
63

64
    1) Check if action can be performed. If it can, then there must be no
65
       pending task (with the exception of DESTROY).
66
    2) Handle previous commission if unresolved:
67
       * If it is not pending and it to accept, then accept
68
       * If it is not pending and to reject or is pending then reject it. Since
69
       the action can be performed only if there is no pending task, then there
70
       can be no pending commission. The exception is DESTROY, but in this case
71
       the commission can safely be rejected, and the dispatcher will generate
72
       the correct ones!
73
    3) Issue new commission and associate it with the VM. Also clear the task.
74
    4) Send job to ganeti
75
    5) Update task and commit
76
    """
77
    def decorator(func):
78
        @wraps(func)
79
        @transaction.commit_on_success
80
        def wrapper(vm, *args, **kwargs):
81
            user_id = vm.userid
82
            validate_server_action(vm, action)
83
            vm.action = action
84

    
85
            commission_name = "client: api, resource: %s" % vm
86
            quotas.handle_resource_commission(vm, action=action,
87
                                              commission_name=commission_name)
88
            vm.save()
89

    
90
            # XXX: Special case for server creation!
91
            if action == "BUILD":
92
                # Perform a commit, because the VirtualMachine must be saved to
93
                # DB before the OP_INSTANCE_CREATE job in enqueued in Ganeti.
94
                # Otherwise, messages will arrive from snf-dispatcher about
95
                # this instance, before the VM is stored in DB.
96
                transaction.commit()
97
                # After committing the locks are released. Refetch the instance
98
                # to guarantee x-lock.
99
                vm = VirtualMachine.objects.select_for_update().get(id=vm.id)
100

    
101
            # Send the job to Ganeti and get the associated jobID
102
            try:
103
                job_id = func(vm, *args, **kwargs)
104
            except Exception as e:
105
                if vm.serial is not None:
106
                    # Since the job never reached Ganeti, reject the commission
107
                    log.debug("Rejecting commission: '%s', could not perform"
108
                              " action '%s': %s" % (vm.serial,  action, e))
109
                    transaction.rollback()
110
                    quotas.reject_serial(vm.serial)
111
                    transaction.commit()
112
                raise
113

    
114
            if action == "BUILD" and vm.serial is not None:
115
                # XXX: Special case for server creation: we must accept the
116
                # commission because the VM has been stored in DB. Also, if
117
                # communication with Ganeti fails, the job will never reach
118
                # Ganeti, and the commission will never be resolved.
119
                quotas.accept_serial(vm.serial)
120

    
121
            log.info("user: %s, vm: %s, action: %s, job_id: %s, serial: %s",
122
                     user_id, vm.id, action, job_id, vm.serial)
123

    
124
            # store the new task in the VM
125
            if job_id is not None:
126
                vm.task = action
127
                vm.task_job_id = job_id
128
            vm.save()
129

    
130
            return vm
131
        return wrapper
132
    return decorator
133

    
134

    
135
@transaction.commit_on_success
136
def create(userid, name, password, flavor, image, metadata={},
137
           personality=[], networks=None, use_backend=None):
138
    if use_backend is None:
139
        # Allocate server to a Ganeti backend
140
        use_backend = allocate_new_server(userid, flavor)
141

    
142
    # Create the ports for the server
143
    try:
144
        ports = create_instance_ports(userid, networks)
145
    except Exception as e:
146
        raise e
147

    
148
    # Fix flavor for archipelago
149
    disk_template, provider = util.get_flavor_provider(flavor)
150
    if provider:
151
        flavor.disk_template = disk_template
152
        flavor.disk_provider = provider
153
        flavor.disk_origin = None
154
        if provider == 'vlmc':
155
            flavor.disk_origin = image['checksum']
156
            image['backend_id'] = 'null'
157
    else:
158
        flavor.disk_provider = None
159

    
160
    # We must save the VM instance now, so that it gets a valid
161
    # vm.backend_vm_id.
162
    vm = VirtualMachine.objects.create(name=name,
163
                                       backend=use_backend,
164
                                       userid=userid,
165
                                       imageid=image["id"],
166
                                       flavor=flavor,
167
                                       operstate="BUILD")
168
    log.info("Created entry in DB for VM '%s'", vm)
169

    
170
    # Associate the ports with the server
171
    for index, port in enumerate(ports):
172
        associate_port_with_machine(port, vm)
173
        port.index = index
174
        port.save()
175

    
176
    for key, val in metadata.items():
177
        VirtualMachineMetadata.objects.create(
178
            meta_key=key,
179
            meta_value=val,
180
            vm=vm)
181

    
182
    # Create the server in Ganeti.
183
    vm = create_server(vm, ports, flavor, image, personality, password)
184

    
185
    return vm
186

    
187

    
188
@transaction.commit_on_success
189
def allocate_new_server(userid, flavor):
190
    """Allocate a new server to a Ganeti backend.
191

192
    Allocation is performed based on the owner of the server and the specified
193
    flavor. Also, backends that do not have a public IPv4 address are excluded
194
    from server allocation.
195

196
    This function runs inside a transaction, because after allocating the
197
    instance a commit must be performed in order to release all locks.
198

199
    """
200
    backend_allocator = BackendAllocator()
201
    use_backend = backend_allocator.allocate(userid, flavor)
202
    if use_backend is None:
203
        log.error("No available backend for VM with flavor %s", flavor)
204
        raise faults.ServiceUnavailable("No available backends")
205
    return use_backend
206

    
207

    
208
@server_command("BUILD")
209
def create_server(vm, nics, flavor, image, personality, password):
210
    # dispatch server created signal needed to trigger the 'vmapi', which
211
    # enriches the vm object with the 'config_url' attribute which must be
212
    # passed to the Ganeti job.
213
    server_created.send(sender=vm, created_vm_params={
214
        'img_id': image['backend_id'],
215
        'img_passwd': password,
216
        'img_format': str(image['format']),
217
        'img_personality': json.dumps(personality),
218
        'img_properties': json.dumps(image['metadata']),
219
    })
220
    # send job to Ganeti
221
    try:
222
        jobID = backend.create_instance(vm, nics, flavor, image)
223
    except:
224
        log.exception("Failed create instance '%s'", vm)
225
        jobID = None
226
        vm.operstate = "ERROR"
227
        vm.backendlogmsg = "Failed to send job to Ganeti."
228
        vm.save()
229
        vm.nics.all().update(state="ERROR")
230

    
231
    # At this point the job is enqueued in the Ganeti backend
232
    vm.backendjobid = jobID
233
    vm.save()
234
    log.info("User %s created VM %s, NICs %s, Backend %s, JobID %s",
235
             vm.userid, vm, nics, backend, str(jobID))
236

    
237
    return jobID
238

    
239

    
240
@server_command("DESTROY")
241
def destroy(vm):
242
    log.info("Deleting VM %s", vm)
243
    return backend.delete_instance(vm)
244

    
245

    
246
@server_command("START")
247
def start(vm):
248
    log.info("Starting VM %s", vm)
249
    return backend.startup_instance(vm)
250

    
251

    
252
@server_command("STOP")
253
def stop(vm):
254
    log.info("Stopping VM %s", vm)
255
    return backend.shutdown_instance(vm)
256

    
257

    
258
@server_command("REBOOT")
259
def reboot(vm, reboot_type):
260
    if reboot_type not in ("SOFT", "HARD"):
261
        raise faults.BadRequest("Malformed request. Invalid reboot"
262
                                " type %s" % reboot_type)
263
    log.info("Rebooting VM %s. Type %s", vm, reboot_type)
264

    
265
    return backend.reboot_instance(vm, reboot_type.lower())
266

    
267

    
268
@server_command("RESIZE")
269
def resize(vm, flavor):
270
    old_flavor = vm.flavor
271
    # User requested the same flavor
272
    if old_flavor.id == flavor.id:
273
        raise faults.BadRequest("Server '%s' flavor is already '%s'."
274
                                % (vm, flavor))
275
        return None
276
    # Check that resize can be performed
277
    if old_flavor.disk != flavor.disk:
278
        raise faults.BadRequest("Cannot resize instance disk.")
279
    if old_flavor.disk_template != flavor.disk_template:
280
        raise faults.BadRequest("Cannot change instance disk template.")
281

    
282
    log.info("Resizing VM from flavor '%s' to '%s", old_flavor, flavor)
283
    commission_info = {"cyclades.cpu": flavor.cpu - old_flavor.cpu,
284
                       "cyclades.ram": 1048576 * (flavor.ram - old_flavor.ram)}
285
    # Save serial to VM, since it is needed by server_command decorator
286
    vm.serial = quotas.issue_commission(user=vm.userid,
287
                                        source=quotas.DEFAULT_SOURCE,
288
                                        provisions=commission_info,
289
                                        name="resource: %s. resize" % vm)
290
    return backend.resize_instance(vm, vcpus=flavor.cpu, memory=flavor.ram)
291

    
292

    
293
@server_command("SET_FIREWALL_PROFILE")
294
def set_firewall_profile(vm, profile, nic):
295
    log.info("Setting VM %s, NIC %s, firewall %s", vm, nic, profile)
296

    
297
    if profile not in [x[0] for x in NetworkInterface.FIREWALL_PROFILES]:
298
        raise faults.BadRequest("Unsupported firewall profile")
299
    backend.set_firewall_profile(vm, profile=profile, nic=nic)
300
    return None
301

    
302

    
303
@server_command("CONNECT")
304
def connect(vm, network, port=None):
305
    if port is None:
306
        port = _create_port(vm.userid, network)
307
    associate_port_with_machine(port, vm)
308

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

    
311
    return backend.connect_to_network(vm, port)
312

    
313

    
314
@server_command("DISCONNECT")
315
def disconnect(vm, nic):
316
    log.info("Removing NIC %s from VM %s", nic, vm)
317
    return backend.disconnect_from_network(vm, nic)
318

    
319

    
320
def console(vm, console_type):
321
    """Arrange for an OOB console of the specified type
322

323
    This method arranges for an OOB console of the specified type.
324
    Only consoles of type "vnc" are supported for now.
325

326
    It uses a running instance of vncauthproxy to setup proper
327
    VNC forwarding with a random password, then returns the necessary
328
    VNC connection info to the caller.
329

330
    """
331
    log.info("Get console  VM %s, type %s", vm, console_type)
332

    
333
    # Use RAPI to get VNC console information for this instance
334
    if vm.operstate != "STARTED":
335
        raise faults.BadRequest('Server not in ACTIVE state.')
336

    
337
    if settings.TEST:
338
        console_data = {'kind': 'vnc', 'host': 'ganeti_node', 'port': 1000}
339
    else:
340
        console_data = backend.get_instance_console(vm)
341

    
342
    if console_data['kind'] != 'vnc':
343
        message = 'got console of kind %s, not "vnc"' % console_data['kind']
344
        raise faults.ServiceUnavailable(message)
345

    
346
    # Let vncauthproxy decide on the source port.
347
    # The alternative: static allocation, e.g.
348
    # sport = console_data['port'] - 1000
349
    sport = 0
350
    daddr = console_data['host']
351
    dport = console_data['port']
352
    password = util.random_password()
353

    
354
    if settings.TEST:
355
        fwd = {'source_port': 1234, 'status': 'OK'}
356
    else:
357
        fwd = request_vnc_forwarding(sport, daddr, dport, password)
358

    
359
    if fwd['status'] != "OK":
360
        raise faults.ServiceUnavailable('vncauthproxy returned error status')
361

    
362
    # Verify that the VNC server settings haven't changed
363
    if not settings.TEST:
364
        if console_data != backend.get_instance_console(vm):
365
            raise faults.ServiceUnavailable('VNC Server settings changed.')
366

    
367
    console = {
368
        'type': 'vnc',
369
        'host': getfqdn(),
370
        'port': fwd['source_port'],
371
        'password': password}
372

    
373
    return console
374

    
375

    
376
def rename(server, new_name):
377
    """Rename a VirtualMachine."""
378
    old_name = server.name
379
    server.name = new_name
380
    server.save()
381
    log.info("Renamed server '%s' from '%s' to '%s'", server, old_name,
382
             new_name)
383
    return server
384

    
385

    
386
@transaction.commit_on_success
387
def create_port(*args, **kwargs):
388
    return _create_port(*args, **kwargs)
389

    
390

    
391
def _create_port(userid, network, machine=None, use_ipaddress=None,
392
                 address=None, name="", security_groups=None,
393
                 device_owner=None):
394
    """Create a new port on the specified network.
395

396
    Create a new Port(NetworkInterface model) on the specified Network. If
397
    'machine' is specified, the machine will be connected to the network using
398
    this port. If 'use_ipaddress' argument is specified, the port will be
399
    assigned this IPAddress. Otherwise, an IPv4 address from the IPv4 subnet
400
    will be allocated.
401

402
    """
403
    if network.state != "ACTIVE":
404
        raise faults.Conflict("Cannot create port while network '%s' is in"
405
                              " '%s' status" % (network.id, network.state))
406
    elif network.drained:
407
        raise faults.Conflict("Cannot create port while network %s is in"
408
                              " 'SNF:DRAINED' status" % network.id)
409
    elif network.action == "DESTROY":
410
        msg = "Cannot create port. Network %s is being deleted."
411
        raise faults.Conflict(msg % network.id)
412

    
413
    ipaddress = None
414
    if use_ipaddress is not None:
415
        # Use an existing IPAddress object.
416
        ipaddress = use_ipaddress
417
        if ipaddress and (ipaddress.network_id != network.id):
418
            msg = "IP Address %s does not belong to network %s"
419
            raise faults.Conflict(msg % (ipaddress.address, network.id))
420
    else:
421
        # If network has IPv4 subnets, try to allocate the address that the
422
        # the user specified or a random one.
423
        if network.subnets.filter(ipversion=4).exists():
424
            ipaddress = ips.allocate_ip(network, userid=userid,
425
                                        address=address)
426
        elif address is not None:
427
            raise faults.BadRequest("Address %s is not a valid IP for the"
428
                                    " defined network subnets" % address)
429

    
430
    if ipaddress is not None and ipaddress.nic is not None:
431
        raise faults.Conflict("IP address '%s' is already in use" %
432
                              ipaddress.address)
433

    
434
    port = NetworkInterface.objects.create(network=network,
435
                                           state="DOWN",
436
                                           userid=userid,
437
                                           device_owner=None,
438
                                           name=name)
439

    
440
    # add the security groups if any
441
    if security_groups:
442
        port.security_groups.add(*security_groups)
443

    
444
    if ipaddress is not None:
445
        # Associate IPAddress with the Port
446
        ipaddress.nic = port
447
        ipaddress.save()
448

    
449
    if machine is not None:
450
        # Connect port to the instance.
451
        machine = connect(machine, network, port)
452
        jobID = machine.task_job_id
453
        log.info("Created Port %s with IP %s. Ganeti Job: %s",
454
                 port, ipaddress, jobID)
455
    else:
456
        log.info("Created Port %s with IP %s not attached to any instance",
457
                 port, ipaddress)
458

    
459
    return port
460

    
461

    
462
def associate_port_with_machine(port, machine):
463
    """Associate a Port with a VirtualMachine.
464

465
    Associate the port with the VirtualMachine and add an entry to the
466
    IPAddressLog if the port has a public IPv4 address from a public network.
467

468
    """
469
    if port.machine is not None:
470
        raise faults.Conflict("Port %s is already in use." % port.id)
471
    if port.network.public:
472
        ipv4_address = port.ipv4_address
473
        if ipv4_address is not None:
474
            ip_log = IPAddressLog.objects.create(server_id=machine.id,
475
                                                 network_id=port.network_id,
476
                                                 address=ipv4_address,
477
                                                 active=True)
478
            log.debug("Created IP log entry %s", ip_log)
479
    port.machine = machine
480
    port.state = "BUILD"
481
    port.device_owner = "vm"
482
    port.save()
483
    return port
484

    
485

    
486
@transaction.commit_on_success
487
def delete_port(port):
488
    """Delete a port by removing the NIC card from the instance.
489

490
    Send a Job to remove the NIC card from the instance. The port
491
    will be deleted and the associated IPv4 addressess will be released
492
    when the job completes successfully. Deleting port that is connected to
493
    a public network is allowed only if the port has an associated floating IP
494
    address.
495

496
    """
497

    
498
    if port.network.public and not port.ips.filter(floating_ip=True,
499
                                                   deleted=False).exists():
500
        raise faults.Forbidden("Cannot disconnect from public network.")
501

    
502
    if port.machine is not None:
503
        vm = disconnect(port.machine, port)
504
        log.info("Removing port %s, Job: %s", port, vm.task_job_id)
505
    else:
506
        backend.remove_nic_ips(port)
507
        port.delete()
508
        log.info("Removed port %s", port)
509

    
510
    return port
511

    
512

    
513
def create_instance_ports(user_id, networks=None):
514
    # First connect the instance to the networks defined by the admin
515
    forced_ports = create_ports_for_setting(user_id, category="admin")
516
    if networks is None:
517
        # If the user did not asked for any networks, connect instance to
518
        # default networks as defined by the admin
519
        ports = create_ports_for_setting(user_id, category="default")
520
    else:
521
        # Else just connect to the networks that the user defined
522
        ports = create_ports_for_request(user_id, networks)
523
    return forced_ports + ports
524

    
525

    
526
def create_ports_for_setting(user_id, category):
527
    if category == "admin":
528
        network_setting = settings.CYCLADES_FORCED_SERVER_NETWORKS
529
    elif category == "default":
530
        network_setting = settings.CYCLADES_DEFAULT_SERVER_NETWORKS
531
    else:
532
        raise ValueError("Unknown category: %s" % category)
533

    
534
    ports = []
535
    for network_ids in network_setting:
536
        # Treat even simple network IDs as group of networks with one network
537
        if type(network_ids) not in (list, tuple):
538
            network_ids = [network_ids]
539

    
540
        for network_id in network_ids:
541
            try:
542
                ports.append(_port_from_setting(user_id, network_id, category))
543
                break
544
            except faults.Conflict:
545
                # Try all network IDs in the network group
546
                pass
547

    
548
            # Diffrent exception for each category!
549
            if category == "admin":
550
                exception = faults.ServiceUnavailable
551
            else:
552
                exception = faults.Conflict
553
            raise exception("Cannot connect instance to any of the following"
554
                            " networks %s" % network_ids)
555
    return ports
556

    
557

    
558
def _port_from_setting(user_id, network_id, category):
559
    # TODO: Fix this..you need only IPv4 and only IPv6 network
560
    if network_id == "SNF:ANY_PUBLIC_IPV4":
561
        return create_public_ipv4_port(user_id, category=category)
562
    elif network_id == "SNF:ANY_PUBLIC_IPV6":
563
        return create_public_ipv6_port(user_id, category=category)
564
    elif network_id == "SNF:ANY_PUBLIC":
565
        try:
566
            return create_public_ipv4_port(user_id, category=category)
567
        except faults.Conflict:
568
            return create_public_ipv6_port(user_id, category=category)
569
    else:  # Case of network ID
570
        if category in ["user", "default"]:
571
            return _port_for_request(user_id, {"uuid": network_id})
572
        elif category == "admin":
573
            network = util.get_network(network_id, user_id, non_deleted=True)
574
            return _create_port(user_id, network)
575
        else:
576
            raise ValueError("Unknown category: %s" % category)
577

    
578

    
579
def create_public_ipv4_port(user_id, network=None, address=None,
580
                            category="user"):
581
    """Create a port in a public IPv4 network.
582

583
    Create a port in a public IPv4 network (that may also have an IPv6
584
    subnet). If the category is 'user' or 'default' this will try to use
585
    one of the users floating IPs. If the category is 'admin' will
586
    create a port to the public network (without floating IPs or quotas).
587

588
    """
589
    if category in ["user", "default"]:
590
        if address is None:
591
            ipaddress = ips.get_free_floating_ip(user_id, network)
592
        else:
593
            ipaddress = util.get_floating_ip_by_address(user_id, address,
594
                                                        for_update=True)
595
    elif category == "admin":
596
        if network is None:
597
            ipaddress = ips.allocate_public_ip(user_id)
598
        else:
599
            ipaddress = ips.allocate_ip(network, user_id)
600
    else:
601
        raise ValueError("Unknown category: %s" % category)
602
    if network is None:
603
        network = ipaddress.network
604
    return _create_port(user_id, network, use_ipaddress=ipaddress)
605

    
606

    
607
def create_public_ipv6_port(user_id, category=None):
608
    """Create a port in a public IPv6 only network."""
609
    networks = Network.objects.filter(public=True, deleted=False,
610
                                      drained=False, subnets__ipversion=6)\
611
                              .exclude(subnets__ipversion=4)
612
    if networks:
613
        return _create_port(user_id, networks[0])
614
    else:
615
        msg = "No available IPv6 only network!"
616
        log.error(msg)
617
        raise faults.Conflict(msg)
618

    
619

    
620
def create_ports_for_request(user_id, networks):
621
    """Create the server ports requested by the user.
622

623
    Create the ports for the new servers as requested in the 'networks'
624
    attribute. The networks attribute contains either a list of network IDs
625
    ('uuid') or a list of ports IDs ('port'). In case of network IDs, the user
626
    can also specify an IPv4 address ('fixed_ip'). In order to connect to a
627
    public network, the 'fixed_ip' attribute must contain the IPv4 address of a
628
    floating IP. If the network is public but the 'fixed_ip' attribute is not
629
    specified, the system will automatically reserve one of the users floating
630
    IPs.
631

632
    """
633
    return [_port_for_request(user_id, network) for network in networks]
634

    
635

    
636
def _port_for_request(user_id, network_dict):
637
    port_id = network_dict.get("port")
638
    network_id = network_dict.get("uuid")
639
    if port_id is not None:
640
        return util.get_port(port_id, user_id, for_update=True)
641
    elif network_id is not None:
642
        address = network_dict.get("fixed_ip")
643
        network = util.get_network(network_id, user_id, non_deleted=True)
644
        if network.public:
645
            if network.subnet4 is not None:
646
                if not "fixed_ip" in network_dict:
647
                    return create_public_ipv4_port(user_id, network)
648
                elif address is None:
649
                    msg = "Cannot connect to public network"
650
                    raise faults.BadRequest(msg % network.id)
651
                else:
652
                    return create_public_ipv4_port(user_id, network, address)
653
            else:
654
                raise faults.Forbidden("Cannot connect to IPv6 only public"
655
                                       " network %" % network.id)
656
        else:
657
            return _create_port(user_id, network, address=address)
658
    else:
659
        raise faults.BadRequest("Network 'uuid' or 'port' attribute"
660
                                " is required.")