Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (24.6 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("Can not 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("Can not 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("Can not 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("Can not resize instance disk.")
279
    if old_flavor.disk_template != flavor.disk_template:
280
        raise faults.BadRequest("Can not 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.BuildInProgress("Can not create port while network is in"
405
                                     " state %s" % network.state)
406
    if network.action == "DESTROY":
407
        msg = "Can not create port. Network %s is being deleted."
408
        raise faults.Conflict(msg % network.id)
409
    ipaddress = None
410
    if use_ipaddress is not None:
411
        # Use an existing IPAddress object.
412
        ipaddress = use_ipaddress
413
        if ipaddress and (ipaddress.network_id != network.id):
414
            msg = "IP Address %s does not belong to network %s"
415
            raise faults.Conflict(msg % (ipaddress.address, network.id))
416
    else:
417
        # If network has IPv4 subnets, try to allocate the address that the
418
        # the user specified or a random one.
419
        if network.subnets.filter(ipversion=4).exists():
420
            ipaddress = ips.allocate_ip(network, userid=userid,
421
                                        address=address)
422
        elif address is not None:
423
            raise faults.BadRequest("Address %s is not a valid IP for the"
424
                                    " defined network subnets" % address)
425

    
426
    if ipaddress is not None and ipaddress.nic is not None:
427
        raise faults.Conflict("IP address '%s' is already in use" %
428
                              ipaddress.address)
429

    
430
    port = NetworkInterface.objects.create(network=network,
431
                                           state="DOWN",
432
                                           userid=userid,
433
                                           device_owner=None,
434
                                           name=name)
435

    
436
    # add the security groups if any
437
    if security_groups:
438
        port.security_groups.add(*security_groups)
439

    
440
    if ipaddress is not None:
441
        # Associate IPAddress with the Port
442
        ipaddress.nic = port
443
        ipaddress.save()
444

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

    
455
    return port
456

    
457

    
458
def associate_port_with_machine(port, machine):
459
    """Associate a Port with a VirtualMachine.
460

461
    Associate the port with the VirtualMachine and add an entry to the
462
    IPAddressLog if the port has a public IPv4 address from a public network.
463

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

    
481

    
482
@transaction.commit_on_success
483
def delete_port(port):
484
    """Delete a port by removing the NIC card from the instance.
485

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

492
    """
493

    
494
    if port.network.public and not port.ips.filter(floating_ip=True,
495
                                                   deleted=False).exists():
496
        raise faults.Forbidden("Can not disconnect from public network.")
497

    
498
    if port.machine is not None:
499
        vm = disconnect(port.machine, port)
500
        log.info("Removing port %s, Job: %s", port, vm.task_job_id)
501
    else:
502
        backend.remove_nic_ips(port)
503
        port.delete()
504
        log.info("Removed port %s", port)
505

    
506
    return port
507

    
508

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

    
521

    
522
def create_ports_for_setting(user_id, category):
523
    if category == "admin":
524
        network_setting = settings.CYCLADES_FORCED_SERVER_NETWORKS
525
    elif category == "default":
526
        network_setting = settings.CYCLADES_DEFAULT_SERVER_NETWORKS
527
    else:
528
        raise ValueError("Unknown category: %s" % category)
529

    
530
    ports = []
531
    for network_ids in network_setting:
532
        # Treat even simple network IDs as group of networks with one network
533
        if type(network_ids) not in (list, tuple):
534
            network_ids = [network_ids]
535

    
536
        for network_id in network_ids:
537
            try:
538
                ports.append(_port_from_setting(user_id, network_id, category))
539
                break
540
            except faults.Conflict:
541
                # Try all network IDs in the network group
542
                pass
543

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

    
553

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

    
574

    
575
def create_public_ipv4_port(user_id, network=None, address=None,
576
                            category="user"):
577
    """Create a port in a public IPv4 network.
578

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

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

    
602

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

    
615

    
616
def create_ports_for_request(user_id, networks):
617
    """Create the server ports requested by the user.
618

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

628
    """
629
    return [_port_for_request(user_id, network) for network in networks]
630

    
631

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