Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (25.3 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
from synnefo.logic import rapi
19

    
20
log = logging.getLogger(__name__)
21

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

    
25

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

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

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

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

    
58

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

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

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

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

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

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

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

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

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

    
131
            return vm
132
        return wrapper
133
    return decorator
134

    
135

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

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

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

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

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

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

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

    
186
    return vm
187

    
188

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

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

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

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

    
208

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

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

    
239
    return jobID
240

    
241

    
242
@server_command("DESTROY")
243
def destroy(vm):
244
    # XXX: Workaround for race where OP_INSTANCE_REMOVE starts executing on
245
    # Ganeti before OP_INSTANCE_CREATE. This will be fixed when
246
    # OP_INSTANCE_REMOVE supports the 'depends' request attribute.
247
    if (vm.backendopcode == "OP_INSTANCE_CREATE" and
248
       vm.backendjobstatus not in rapi.JOB_STATUS_FINALIZED and
249
       backend.job_is_still_running(vm) and
250
       not backend.vm_exists_in_backend(vm)):
251
            raise faults.BuildInProgress("Server is being build")
252
    log.info("Deleting VM %s", vm)
253
    return backend.delete_instance(vm)
254

    
255

    
256
@server_command("START")
257
def start(vm):
258
    log.info("Starting VM %s", vm)
259
    return backend.startup_instance(vm)
260

    
261

    
262
@server_command("STOP")
263
def stop(vm):
264
    log.info("Stopping VM %s", vm)
265
    return backend.shutdown_instance(vm)
266

    
267

    
268
@server_command("REBOOT")
269
def reboot(vm, reboot_type):
270
    if reboot_type not in ("SOFT", "HARD"):
271
        raise faults.BadRequest("Malformed request. Invalid reboot"
272
                                " type %s" % reboot_type)
273
    log.info("Rebooting VM %s. Type %s", vm, reboot_type)
274

    
275
    return backend.reboot_instance(vm, reboot_type.lower())
276

    
277

    
278
@server_command("RESIZE")
279
def resize(vm, flavor):
280
    old_flavor = vm.flavor
281
    # User requested the same flavor
282
    if old_flavor.id == flavor.id:
283
        raise faults.BadRequest("Server '%s' flavor is already '%s'."
284
                                % (vm, flavor))
285
        return None
286
    # Check that resize can be performed
287
    if old_flavor.disk != flavor.disk:
288
        raise faults.BadRequest("Cannot resize instance disk.")
289
    if old_flavor.disk_template != flavor.disk_template:
290
        raise faults.BadRequest("Cannot change instance disk template.")
291

    
292
    log.info("Resizing VM from flavor '%s' to '%s", old_flavor, flavor)
293
    commission_info = {"cyclades.cpu": flavor.cpu - old_flavor.cpu,
294
                       "cyclades.ram": 1048576 * (flavor.ram - old_flavor.ram)}
295
    # Save serial to VM, since it is needed by server_command decorator
296
    vm.serial = quotas.issue_commission(user=vm.userid,
297
                                        source=quotas.DEFAULT_SOURCE,
298
                                        provisions=commission_info,
299
                                        name="resource: %s. resize" % vm)
300
    return backend.resize_instance(vm, vcpus=flavor.cpu, memory=flavor.ram)
301

    
302

    
303
@server_command("SET_FIREWALL_PROFILE")
304
def set_firewall_profile(vm, profile, nic):
305
    log.info("Setting VM %s, NIC %s, firewall %s", vm, nic, profile)
306

    
307
    if profile not in [x[0] for x in NetworkInterface.FIREWALL_PROFILES]:
308
        raise faults.BadRequest("Unsupported firewall profile")
309
    backend.set_firewall_profile(vm, profile=profile, nic=nic)
310
    return None
311

    
312

    
313
@server_command("CONNECT")
314
def connect(vm, network, port=None):
315
    if port is None:
316
        port = _create_port(vm.userid, network)
317
    associate_port_with_machine(port, vm)
318

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

    
321
    return backend.connect_to_network(vm, port)
322

    
323

    
324
@server_command("DISCONNECT")
325
def disconnect(vm, nic):
326
    log.info("Removing NIC %s from VM %s", nic, vm)
327
    return backend.disconnect_from_network(vm, nic)
328

    
329

    
330
def console(vm, console_type):
331
    """Arrange for an OOB console of the specified type
332

333
    This method arranges for an OOB console of the specified type.
334
    Only consoles of type "vnc" are supported for now.
335

336
    It uses a running instance of vncauthproxy to setup proper
337
    VNC forwarding with a random password, then returns the necessary
338
    VNC connection info to the caller.
339

340
    """
341
    log.info("Get console  VM %s, type %s", vm, console_type)
342

    
343
    # Use RAPI to get VNC console information for this instance
344
    if vm.operstate != "STARTED":
345
        raise faults.BadRequest('Server not in ACTIVE state.')
346

    
347
    if settings.TEST:
348
        console_data = {'kind': 'vnc', 'host': 'ganeti_node', 'port': 1000}
349
    else:
350
        console_data = backend.get_instance_console(vm)
351

    
352
    if console_data['kind'] != 'vnc':
353
        message = 'got console of kind %s, not "vnc"' % console_data['kind']
354
        raise faults.ServiceUnavailable(message)
355

    
356
    # Let vncauthproxy decide on the source port.
357
    # The alternative: static allocation, e.g.
358
    # sport = console_data['port'] - 1000
359
    sport = 0
360
    daddr = console_data['host']
361
    dport = console_data['port']
362
    password = util.random_password()
363

    
364
    if settings.TEST:
365
        fwd = {'source_port': 1234, 'status': 'OK'}
366
    else:
367
        fwd = request_vnc_forwarding(sport, daddr, dport, password)
368

    
369
    if fwd['status'] != "OK":
370
        raise faults.ServiceUnavailable('vncauthproxy returned error status')
371

    
372
    # Verify that the VNC server settings haven't changed
373
    if not settings.TEST:
374
        if console_data != backend.get_instance_console(vm):
375
            raise faults.ServiceUnavailable('VNC Server settings changed.')
376

    
377
    console = {
378
        'type': 'vnc',
379
        'host': getfqdn(),
380
        'port': fwd['source_port'],
381
        'password': password}
382

    
383
    return console
384

    
385

    
386
def rename(server, new_name):
387
    """Rename a VirtualMachine."""
388
    old_name = server.name
389
    server.name = new_name
390
    server.save()
391
    log.info("Renamed server '%s' from '%s' to '%s'", server, old_name,
392
             new_name)
393
    return server
394

    
395

    
396
@transaction.commit_on_success
397
def create_port(*args, **kwargs):
398
    return _create_port(*args, **kwargs)
399

    
400

    
401
def _create_port(userid, network, machine=None, use_ipaddress=None,
402
                 address=None, name="", security_groups=None,
403
                 device_owner=None):
404
    """Create a new port on the specified network.
405

406
    Create a new Port(NetworkInterface model) on the specified Network. If
407
    'machine' is specified, the machine will be connected to the network using
408
    this port. If 'use_ipaddress' argument is specified, the port will be
409
    assigned this IPAddress. Otherwise, an IPv4 address from the IPv4 subnet
410
    will be allocated.
411

412
    """
413
    if network.state != "ACTIVE":
414
        raise faults.Conflict("Cannot create port while network '%s' is in"
415
                              " '%s' status" % (network.id, network.state))
416
    elif network.action == "DESTROY":
417
        msg = "Cannot create port. Network %s is being deleted."
418
        raise faults.Conflict(msg % network.id)
419
    elif network.drained:
420
        raise faults.Conflict("Cannot create port while network %s is in"
421
                              " 'SNF:DRAINED' status" % network.id)
422

    
423
    ipaddress = None
424
    if use_ipaddress is not None:
425
        # Use an existing IPAddress object.
426
        ipaddress = use_ipaddress
427
        if ipaddress and (ipaddress.network_id != network.id):
428
            msg = "IP Address %s does not belong to network %s"
429
            raise faults.Conflict(msg % (ipaddress.address, network.id))
430
    else:
431
        # If network has IPv4 subnets, try to allocate the address that the
432
        # the user specified or a random one.
433
        if network.subnets.filter(ipversion=4).exists():
434
            ipaddress = ips.allocate_ip(network, userid=userid,
435
                                        address=address)
436
        elif address is not None:
437
            raise faults.BadRequest("Address %s is not a valid IP for the"
438
                                    " defined network subnets" % address)
439

    
440
    if ipaddress is not None and ipaddress.nic is not None:
441
        raise faults.Conflict("IP address '%s' is already in use" %
442
                              ipaddress.address)
443

    
444
    port = NetworkInterface.objects.create(network=network,
445
                                           state="DOWN",
446
                                           userid=userid,
447
                                           device_owner=None,
448
                                           name=name)
449

    
450
    # add the security groups if any
451
    if security_groups:
452
        port.security_groups.add(*security_groups)
453

    
454
    if ipaddress is not None:
455
        # Associate IPAddress with the Port
456
        ipaddress.nic = port
457
        ipaddress.save()
458

    
459
    if machine is not None:
460
        # Connect port to the instance.
461
        machine = connect(machine, network, port)
462
        jobID = machine.task_job_id
463
        log.info("Created Port %s with IP %s. Ganeti Job: %s",
464
                 port, ipaddress, jobID)
465
    else:
466
        log.info("Created Port %s with IP %s not attached to any instance",
467
                 port, ipaddress)
468

    
469
    return port
470

    
471

    
472
def associate_port_with_machine(port, machine):
473
    """Associate a Port with a VirtualMachine.
474

475
    Associate the port with the VirtualMachine and add an entry to the
476
    IPAddressLog if the port has a public IPv4 address from a public network.
477

478
    """
479
    if port.machine is not None:
480
        raise faults.Conflict("Port %s is already in use." % port.id)
481
    if port.network.public:
482
        ipv4_address = port.ipv4_address
483
        if ipv4_address is not None:
484
            ip_log = IPAddressLog.objects.create(server_id=machine.id,
485
                                                 network_id=port.network_id,
486
                                                 address=ipv4_address,
487
                                                 active=True)
488
            log.debug("Created IP log entry %s", ip_log)
489
    port.machine = machine
490
    port.state = "BUILD"
491
    port.device_owner = "vm"
492
    port.save()
493
    return port
494

    
495

    
496
@transaction.commit_on_success
497
def delete_port(port):
498
    """Delete a port by removing the NIC card from the instance.
499

500
    Send a Job to remove the NIC card from the instance. The port
501
    will be deleted and the associated IPv4 addressess will be released
502
    when the job completes successfully. Deleting port that is connected to
503
    a public network is allowed only if the port has an associated floating IP
504
    address.
505

506
    """
507

    
508
    if port.network.public and not port.ips.filter(floating_ip=True,
509
                                                   deleted=False).exists():
510
        raise faults.Forbidden("Cannot disconnect from public network.")
511

    
512
    if port.machine is not None:
513
        vm = disconnect(port.machine, port)
514
        log.info("Removing port %s, Job: %s", port, vm.task_job_id)
515
    else:
516
        backend.remove_nic_ips(port)
517
        port.delete()
518
        log.info("Removed port %s", port)
519

    
520
    return port
521

    
522

    
523
def create_instance_ports(user_id, networks=None):
524
    # First connect the instance to the networks defined by the admin
525
    forced_ports = create_ports_for_setting(user_id, category="admin")
526
    if networks is None:
527
        # If the user did not asked for any networks, connect instance to
528
        # default networks as defined by the admin
529
        ports = create_ports_for_setting(user_id, category="default")
530
    else:
531
        # Else just connect to the networks that the user defined
532
        ports = create_ports_for_request(user_id, networks)
533
    return forced_ports + ports
534

    
535

    
536
def create_ports_for_setting(user_id, category):
537
    if category == "admin":
538
        network_setting = settings.CYCLADES_FORCED_SERVER_NETWORKS
539
    elif category == "default":
540
        network_setting = settings.CYCLADES_DEFAULT_SERVER_NETWORKS
541
    else:
542
        raise ValueError("Unknown category: %s" % category)
543

    
544
    ports = []
545
    for network_ids in network_setting:
546
        # Treat even simple network IDs as group of networks with one network
547
        if type(network_ids) not in (list, tuple):
548
            network_ids = [network_ids]
549

    
550
        for network_id in network_ids:
551
            try:
552
                ports.append(_port_from_setting(user_id, network_id, category))
553
                break
554
            except faults.Conflict:
555
                # Try all network IDs in the network group
556
                pass
557

    
558
            # Diffrent exception for each category!
559
            if category == "admin":
560
                exception = faults.ServiceUnavailable
561
            else:
562
                exception = faults.Conflict
563
            raise exception("Cannot connect instance to any of the following"
564
                            " networks %s" % network_ids)
565
    return ports
566

    
567

    
568
def _port_from_setting(user_id, network_id, category):
569
    # TODO: Fix this..you need only IPv4 and only IPv6 network
570
    if network_id == "SNF:ANY_PUBLIC_IPV4":
571
        return create_public_ipv4_port(user_id, category=category)
572
    elif network_id == "SNF:ANY_PUBLIC_IPV6":
573
        return create_public_ipv6_port(user_id, category=category)
574
    elif network_id == "SNF:ANY_PUBLIC":
575
        try:
576
            return create_public_ipv4_port(user_id, category=category)
577
        except faults.Conflict:
578
            return create_public_ipv6_port(user_id, category=category)
579
    else:  # Case of network ID
580
        if category in ["user", "default"]:
581
            return _port_for_request(user_id, {"uuid": network_id})
582
        elif category == "admin":
583
            network = util.get_network(network_id, user_id, non_deleted=True)
584
            return _create_port(user_id, network)
585
        else:
586
            raise ValueError("Unknown category: %s" % category)
587

    
588

    
589
def create_public_ipv4_port(user_id, network=None, address=None,
590
                            category="user"):
591
    """Create a port in a public IPv4 network.
592

593
    Create a port in a public IPv4 network (that may also have an IPv6
594
    subnet). If the category is 'user' or 'default' this will try to use
595
    one of the users floating IPs. If the category is 'admin' will
596
    create a port to the public network (without floating IPs or quotas).
597

598
    """
599
    if category in ["user", "default"]:
600
        if address is None:
601
            ipaddress = ips.get_free_floating_ip(user_id, network)
602
        else:
603
            ipaddress = util.get_floating_ip_by_address(user_id, address,
604
                                                        for_update=True)
605
    elif category == "admin":
606
        if network is None:
607
            ipaddress = ips.allocate_public_ip(user_id)
608
        else:
609
            ipaddress = ips.allocate_ip(network, user_id)
610
    else:
611
        raise ValueError("Unknown category: %s" % category)
612
    if network is None:
613
        network = ipaddress.network
614
    return _create_port(user_id, network, use_ipaddress=ipaddress)
615

    
616

    
617
def create_public_ipv6_port(user_id, category=None):
618
    """Create a port in a public IPv6 only network."""
619
    networks = Network.objects.filter(public=True, deleted=False,
620
                                      drained=False, subnets__ipversion=6)\
621
                              .exclude(subnets__ipversion=4)
622
    if networks:
623
        return _create_port(user_id, networks[0])
624
    else:
625
        msg = "No available IPv6 only network!"
626
        log.error(msg)
627
        raise faults.Conflict(msg)
628

    
629

    
630
def create_ports_for_request(user_id, networks):
631
    """Create the server ports requested by the user.
632

633
    Create the ports for the new servers as requested in the 'networks'
634
    attribute. The networks attribute contains either a list of network IDs
635
    ('uuid') or a list of ports IDs ('port'). In case of network IDs, the user
636
    can also specify an IPv4 address ('fixed_ip'). In order to connect to a
637
    public network, the 'fixed_ip' attribute must contain the IPv4 address of a
638
    floating IP. If the network is public but the 'fixed_ip' attribute is not
639
    specified, the system will automatically reserve one of the users floating
640
    IPs.
641

642
    """
643
    return [_port_for_request(user_id, network) for network in networks]
644

    
645

    
646
def _port_for_request(user_id, network_dict):
647
    port_id = network_dict.get("port")
648
    network_id = network_dict.get("uuid")
649
    if port_id is not None:
650
        return util.get_port(port_id, user_id, for_update=True)
651
    elif network_id is not None:
652
        address = network_dict.get("fixed_ip")
653
        network = util.get_network(network_id, user_id, non_deleted=True)
654
        if network.public:
655
            if network.subnet4 is not None:
656
                if not "fixed_ip" in network_dict:
657
                    return create_public_ipv4_port(user_id, network)
658
                elif address is None:
659
                    msg = "Cannot connect to public network"
660
                    raise faults.BadRequest(msg % network.id)
661
                else:
662
                    return create_public_ipv4_port(user_id, network, address)
663
            else:
664
                raise faults.Forbidden("Cannot connect to IPv6 only public"
665
                                       " network %" % network.id)
666
        else:
667
            return _create_port(user_id, network, address=address)
668
    else:
669
        raise faults.BadRequest("Network 'uuid' or 'port' attribute"
670
                                " is required.")