]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Finishing cli work
authorSalvatore Orlando <salvatore.orlando@eu.citrix.com>
Thu, 25 Aug 2011 17:50:05 +0000 (18:50 +0100)
committerSalvatore Orlando <salvatore.orlando@eu.citrix.com>
Thu, 25 Aug 2011 17:50:05 +0000 (18:50 +0100)
Fixing bug with XML deserialization

1  2 
bin/cli
quantum/api/attachments.py
quantum/api/views/networks.py
quantum/api/views/ports.py
quantum/cli_lib.py
quantum/cli_output.template
quantum/client.py
tests/unit/test_cli.py
tools/pip-requires

diff --cc bin/cli
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..20ef7b2aeb78e675b9cf8f672647e7c7913a4099
new file mode 100755 (executable)
--- /dev/null
--- /dev/null
+++ b/bin/cli
@@@ -1,0 -1,0 +1,165 @@@
++#!/usr/bin/env python
++# vim: tabstop=4 shiftwidth=4 softtabstop=4
++
++# Copyright 2011 Nicira Networks, Inc.
++# Copyright 2011 Citrix Systems
++#
++#    Licensed under the Apache License, Version 2.0 (the "License"); you may
++#    not use this file except in compliance with the License. You may obtain
++#    a copy of the License at
++#
++#         http://www.apache.org/licenses/LICENSE-2.0
++#
++#    Unless required by applicable law or agreed to in writing, software
++#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
++#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
++#    License for the specific language governing permissions and limitations
++#    under the License.
++# @author: Somik Behera, Nicira Networks, Inc.
++# @author: Brad Hall, Nicira Networks, Inc.
++# @author: Salvatore Orlando, Citrix
++
++import Cheetah.Template as cheetah_template
++import gettext
++import logging
++import logging.handlers
++import os
++import sys
++
++from optparse import OptionParser
++
++
++possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
++                                   os.pardir,
++                                   os.pardir))
++if os.path.exists(os.path.join(possible_topdir, 'quantum', '__init__.py')):
++    sys.path.insert(0, possible_topdir)
++
++gettext.install('quantum', unicode=1)
++
++from quantum import cli_lib
++from quantum.client import Client
++
++#Configure logger for client - cli logger is a child of it
++#NOTE(salvatore-orlando): logger name does not map to package
++#this is deliberate. Simplifies logger configuration
++LOG = logging.getLogger('quantum')
++FORMAT = 'json'
++commands = {
++  "list_nets": {
++    "func": cli_lib.list_nets,
++    "args": ["tenant-id"]},
++  "create_net": {
++    "func": cli_lib.create_net,
++    "args": ["tenant-id", "net-name"]},
++  "delete_net": {
++    "func": cli_lib.delete_net,
++    "args": ["tenant-id", "net-id"]},
++  "detail_net": {
++    "func": cli_lib.detail_net,
++    "args": ["tenant-id", "net-id"]},
++  "rename_net": {
++    "func": cli_lib.rename_net,
++    "args": ["tenant-id", "net-id", "new-name"]},
++  "list_ports": {
++    "func": cli_lib.list_ports,
++    "args": ["tenant-id", "net-id"]},
++  "create_port": {
++    "func": cli_lib.create_port,
++    "args": ["tenant-id", "net-id"]},
++  "delete_port": {
++    "func": cli_lib.delete_port,
++    "args": ["tenant-id", "net-id", "port-id"]},
++  "set_port_state": {
++    "func": cli_lib.set_port_state,
++    "args": ["tenant-id", "net-id", "port-id", "new_state"]},
++  "detail_port": {
++    "func": cli_lib.detail_port,
++    "args": ["tenant-id", "net-id", "port-id"]},
++  "plug_iface": {
++    "func": cli_lib.plug_iface,
++    "args": ["tenant-id", "net-id", "port-id", "iface-id"]},
++  "unplug_iface": {
++    "func": cli_lib.unplug_iface,
++    "args": ["tenant-id", "net-id", "port-id"]}, }
++
++
++def help():
++    print "\nCommands:"
++    for k in commands.keys():
++        print "    %s %s" % (k,
++          " ".join(["<%s>" % y for y in commands[k]["args"]]))
++
++
++def build_args(cmd, cmdargs, arglist):
++    args = []
++    orig_arglist = arglist[:]
++    try:
++        for x in cmdargs:
++            args.append(arglist[0])
++            del arglist[0]
++    except:
++        LOG.error("Not enough arguments for \"%s\" (expected: %d, got: %d)" % (
++          cmd, len(cmdargs), len(orig_arglist)))
++        print "Usage:\n    %s %s" % (cmd,
++          " ".join(["<%s>" % y for y in commands[cmd]["args"]]))
++        return None
++    if len(arglist) > 0:
++        LOG.error("Too many arguments for \"%s\" (expected: %d, got: %d)" % (
++          cmd, len(cmdargs), len(orig_arglist)))
++        print "Usage:\n    %s %s" % (cmd,
++          " ".join(["<%s>" % y for y in commands[cmd]["args"]]))
++        return None
++    return args
++
++
++if __name__ == "__main__":
++    usagestr = "Usage: %prog [OPTIONS] <command> [args]"
++    parser = OptionParser(usage=usagestr)
++    parser.add_option("-H", "--host", dest="host",
++      type="string", default="127.0.0.1", help="ip address of api host")
++    parser.add_option("-p", "--port", dest="port",
++      type="int", default=9696, help="api poort")
++    parser.add_option("-s", "--ssl", dest="ssl",
++      action="store_true", default=False, help="use ssl")
++    parser.add_option("-v", "--verbose", dest="verbose",
++      action="store_true", default=False, help="turn on verbose logging")
++    parser.add_option("-f", "--logfile", dest="logfile",
++      type="string", default="syslog", help="log file path")
++    options, args = parser.parse_args()
++
++    if options.verbose:
++        LOG.setLevel(logging.DEBUG)
++    else:
++        LOG.setLevel(logging.WARN)
++    #logging.handlers.WatchedFileHandler
++
++    if options.logfile == "syslog":
++        LOG.addHandler(logging.handlers.SysLogHandler(address='/dev/log'))
++    else:
++        LOG.addHandler(logging.handlers.WatchedFileHandler(options.logfile))
++        # Set permissions on log file
++        os.chmod(options.logfile, 0644)
++
++    if len(args) < 1:
++        parser.print_help()
++        help()
++        sys.exit(1)
++
++    cmd = args[0]
++    if cmd not in commands.keys():
++        LOG.error("Unknown command: %s" % cmd)
++        help()
++        sys.exit(1)
++
++    args = build_args(cmd, commands[cmd]["args"], args[1:])
++    if not args:
++        sys.exit(1)
++    LOG.info("Executing command \"%s\" with args: %s" % (cmd, args))
++
++    client = Client(options.host, options.port, options.ssl,
++                    args[0], FORMAT)
++    commands[cmd]["func"](client, *args)
++
++    LOG.info("Command execution completed")
++    sys.exit(0)
index 0000000000000000000000000000000000000000,14f4d940586d7e9c7ae3fd8052e39ca43e645955..e40aabba46ec2ab899b7471228cc596a812b8937
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,86 +1,87 @@@
+ # Copyright 2011 Citrix Systems.
+ # All Rights Reserved.
+ #
+ #    Licensed under the Apache License, Version 2.0 (the "License"); you may
+ #    not use this file except in compliance with the License. You may obtain
+ #    a copy of the License at
+ #
+ #         http://www.apache.org/licenses/LICENSE-2.0
+ #
+ #    Unless required by applicable law or agreed to in writing, software
+ #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ #    License for the specific language governing permissions and limitations
+ #    under the License.
+ import logging
+ from webob import exc
+ from quantum.api import api_common as common
+ from quantum.api import faults
+ from quantum.api.views import attachments as attachments_view
+ from quantum.common import exceptions as exception
+ LOG = logging.getLogger('quantum.api.ports')
+ class Controller(common.QuantumController):
+     """ Port API controller for Quantum API """
+     _attachment_ops_param_list = [{
+         'param-name': 'id',
+         'required': True}, ]
+     _serialization_metadata = {
+         "application/xml": {
+             "attributes": {
+                 "attachment": ["id"], }
+         },
+     }
+     def __init__(self, plugin):
+         self._resource_name = 'attachment'
+         super(Controller, self).__init__(plugin)
+     def get_resource(self, request, tenant_id, network_id, id):
+         try:
+             att_data = self._plugin.get_port_details(
+                             tenant_id, network_id, id)
+             builder = attachments_view.get_view_builder(request)
+             result = builder.build(att_data)['attachment']
+             return dict(attachment=result)
+         except exception.NetworkNotFound as e:
+             return faults.Fault(faults.NetworkNotFound(e))
+         except exception.PortNotFound as e:
+             return faults.Fault(faults.PortNotFound(e))
+     def attach_resource(self, request, tenant_id, network_id, id):
+         try:
+             request_params = \
+                 self._parse_request_params(request,
+                                            self._attachment_ops_param_list)
+         except exc.HTTPError as e:
+             return faults.Fault(e)
+         try:
++            LOG.debug("PLUGGING INTERFACE:%s", request_params['id'])
+             self._plugin.plug_interface(tenant_id, network_id, id,
+                                         request_params['id'])
+             return exc.HTTPNoContent()
+         except exception.NetworkNotFound as e:
+             return faults.Fault(faults.NetworkNotFound(e))
+         except exception.PortNotFound as e:
+             return faults.Fault(faults.PortNotFound(e))
+         except exception.PortInUse as e:
+             return faults.Fault(faults.PortInUse(e))
+         except exception.AlreadyAttached as e:
+             return faults.Fault(faults.AlreadyAttached(e))
+     def detach_resource(self, request, tenant_id, network_id, id):
+         try:
+             self._plugin.unplug_interface(tenant_id,
+                                           network_id, id)
+             return exc.HTTPNoContent()
+         except exception.NetworkNotFound as e:
+             return faults.Fault(faults.NetworkNotFound(e))
+         except exception.PortNotFound as e:
+             return faults.Fault(faults.PortNotFound(e))
Simple merge
Simple merge
index 5ebdde26830e287e1041484ef78f2a0d7808513b,0000000000000000000000000000000000000000..cea8d3ae72141eba68a1b7ada4a15f1204659083
mode 100644,000000..100755
--- /dev/null
@@@ -1,346 -1,0 +1,231 @@@
- from client import Client
- from optparse import OptionParser
++#!/usr/bin/env python
 +# vim: tabstop=4 shiftwidth=4 softtabstop=4
 +
 +# Copyright 2011 Nicira Networks, Inc.
 +# Copyright 2011 Citrix Systems
 +#
 +#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 +#    not use this file except in compliance with the License. You may obtain
 +#    a copy of the License at
 +#
 +#         http://www.apache.org/licenses/LICENSE-2.0
 +#
 +#    Unless required by applicable law or agreed to in writing, software
 +#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 +#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 +#    License for the specific language governing permissions and limitations
 +#    under the License.
 +# @author: Somik Behera, Nicira Networks, Inc.
 +# @author: Brad Hall, Nicira Networks, Inc.
 +# @author: Salvatore Orlando, Citrix
 +
 +import Cheetah.Template as cheetah_template
 +import logging
 +import os
 +import sys
 +
- CLI_TEMPLATE = "../quantum/cli_output.template"
- LOG = logging.getLogger('cli')
 +FORMAT = "json"
-     msg_1 = "Command failed with error code: %s" % (status_code or '<missing>')
-     msg_2 = "Error message:%s" % (message or '<missing>')
-     LOG.exception(msg_1 + "-" + msg_2)
-     print msg_1
-     print msg_2
++CLI_TEMPLATE = "cli_output.template"
++LOG = logging.getLogger('quantum.cli_lib')
 +
 +
 +def _handle_exception(ex):
++    LOG.exception(sys.exc_info())
++    print "Exception:%s - %s" % (sys.exc_info()[0], sys.exc_info()[1])
 +    status_code = None
 +    message = None
 +    # Retrieve dict at 1st element of tuple at last argument
 +    if ex.args and isinstance(ex.args[-1][0], dict):
 +        status_code = ex.args[-1][0].get('status_code', None)
 +        message = ex.args[-1][0].get('message', None)
-     template_file = open(CLI_TEMPLATE).read()
++        msg_1 = "Command failed with error code: %s" \
++                % (status_code or '<missing>')
++        msg_2 = "Error message:%s" % (message or '<missing>')
++        LOG.exception(msg_1 + "-" + msg_2)
++        print msg_1
++        print msg_2
 +
 +
 +def prepare_output(cmd, tenant_id, response):
 +    """ Fills a cheetah template with the response """
 +    #add command and tenant to response for output generation
++    LOG.debug("Preparing output for response:%s", response)
 +    response['cmd'] = cmd
 +    response['tenant_id'] = tenant_id
-     data = {'network': {'net-name': name}}
++    template_path = os.path.join(os.path.dirname(__file__), CLI_TEMPLATE)
++    template_file = open(template_path).read()
 +    output = str(cheetah_template.Template(template_file,
 +                                           searchList=response))
 +    LOG.debug("Finished preparing output for command:%s", cmd)
 +    return output
 +
 +
 +def list_nets(client, *args):
 +    tenant_id = args[0]
 +    res = client.list_networks()
 +    LOG.debug("Operation 'list_networks' executed.")
 +    output = prepare_output("list_nets", tenant_id, res)
 +    print output
 +
 +
 +def create_net(client, *args):
 +    tenant_id, name = args
-         new_net_id = res["networks"]["network"]["id"]
++    data = {'network': {'name': name}}
 +    new_net_id = None
 +    try:
 +        res = client.create_network(data)
-         res = client.list_network_details(network_id)["networks"]["network"]
-         LOG.debug("Operation 'list_network_details' executed.")
++        new_net_id = res["network"]["id"]
 +        LOG.debug("Operation 'create_network' executed.")
 +        output = prepare_output("create_net", tenant_id,
 +                                dict(network_id=new_net_id))
 +        print output
 +    except Exception as ex:
 +        _handle_exception(ex)
 +
 +
 +def delete_net(client, *args):
 +    tenant_id, network_id = args
 +    try:
 +        client.delete_network(network_id)
 +        LOG.debug("Operation 'delete_network' executed.")
 +        output = prepare_output("delete_net", tenant_id,
 +                            dict(network_id=network_id))
 +        print output
 +    except Exception as ex:
 +        _handle_exception(ex)
 +
 +
 +def detail_net(client, *args):
 +    tenant_id, network_id = args
 +    try:
-         res['ports'] = ports
-         for port in ports["ports"]:
-             att_data = client.list_port_attachments(network_id, port['id'])
-             LOG.debug("Operation 'list_attachments' executed.")
-             port['attachment'] = att_data["attachment"]
++        #NOTE(salvatore-orlando): Implementing non-efficient version
++        #for now, at least until plugins will not provide correct behaviour
++        #for the show_network_details operation
++        res = client.show_network_details(network_id)["network"]
++        LOG.debug("Operation 'show_network_details' executed.")
 +        ports = client.list_ports(network_id)
 +        LOG.debug("Operation 'list_ports' executed.")
-     data = {'network': {'net-name': '%s' % name}}
++        res.update(ports)
++        for port in ports['ports']:
++            att_data = client.show_port_attachment(network_id, port['id'])
++            LOG.debug("Operation 'show_port_attachment' executed.")
++            port['attachment'] = att_data['attachment'].get('id', None)
 +        output = prepare_output("detail_net", tenant_id, dict(network=res))
 +        print output
 +    except Exception as ex:
 +        _handle_exception(ex)
 +
 +
 +def rename_net(client, *args):
 +    tenant_id, network_id, name = args
-         new_port_id = res["ports"]["port"]["id"]
++    data = {'network': {'name': '%s' % name}}
 +    try:
 +        client.update_network(network_id, data)
 +        LOG.debug("Operation 'update_network' executed.")
 +        # Response has no body. Use data for populating output
 +        data['network']['id'] = network_id
 +        output = prepare_output("rename_net", tenant_id, data)
 +        print output
 +    except Exception as ex:
 +        _handle_exception(ex)
 +
 +
 +def list_ports(client, *args):
 +    tenant_id, network_id = args
 +    try:
 +        ports = client.list_ports(network_id)
 +        LOG.debug("Operation 'list_ports' executed.")
 +        data = ports
 +        data['network_id'] = network_id
 +        output = prepare_output("list_ports", tenant_id, data)
 +        print output
 +    except Exception as ex:
 +        _handle_exception(ex)
 +
 +
 +def create_port(client, *args):
 +    tenant_id, network_id = args
 +    try:
 +        res = client.create_port(network_id)
 +        LOG.debug("Operation 'create_port' executed.")
-         port = client.list_port_details(network_id, port_id)["port"]
++        new_port_id = res["port"]["id"]
 +        output = prepare_output("create_port", tenant_id,
 +                                dict(network_id=network_id,
 +                                     port_id=new_port_id))
 +        print output
 +    except Exception as ex:
 +        _handle_exception(ex)
 +
 +
 +def delete_port(client, *args):
 +    tenant_id, network_id, port_id = args
 +    try:
 +        client.delete_port(network_id, port_id)
 +        LOG.debug("Operation 'delete_port' executed.")
 +        output = prepare_output("delete_port", tenant_id,
 +                                dict(network_id=network_id,
 +                                     port_id=port_id))
 +        print output
 +    except Exception as ex:
 +        _handle_exception(ex)
 +        return
 +
 +
 +def detail_port(client, *args):
 +    tenant_id, network_id, port_id = args
 +    try:
-     data = {'port': {'port-state': '%s' % new_state}}
++        port = client.show_port_details(network_id, port_id)["port"]
 +        LOG.debug("Operation 'list_port_details' executed.")
 +        #NOTE(salvatore-orland): current API implementation does not
 +        #return attachment with GET operation on port. Once API alignment
 +        #branch is merged, update client to use the detail action
 +        port['attachment'] = '<unavailable>'
 +        output = prepare_output("detail_port", tenant_id,
 +                                dict(network_id=network_id,
 +                                     port=port))
 +        print output
 +    except Exception as ex:
 +        _handle_exception(ex)
 +
 +
 +def set_port_state(client, *args):
 +    tenant_id, network_id, port_id, new_state = args
-         data = {'port': {'attachment': '%s' % attachment}}
++    data = {'port': {'state': '%s' % new_state}}
 +    try:
 +        client.set_port_state(network_id, port_id, data)
 +        LOG.debug("Operation 'set_port_state' executed.")
 +        # Response has no body. Use data for populating output
 +        data['network_id'] = network_id
 +        data['port']['id'] = port_id
 +        output = prepare_output("set_port_state", tenant_id, data)
 +        print output
 +    except Exception as ex:
 +        _handle_exception(ex)
 +
 +
 +def plug_iface(client, *args):
 +    tenant_id, network_id, port_id, attachment = args
 +    try:
- commands = {
-   "list_nets": {
-     "func": list_nets,
-     "args": ["tenant-id"]},
-   "create_net": {
-     "func": create_net,
-     "args": ["tenant-id", "net-name"]},
-   "delete_net": {
-     "func": delete_net,
-     "args": ["tenant-id", "net-id"]},
-   "detail_net": {
-     "func": detail_net,
-     "args": ["tenant-id", "net-id"]},
-   "rename_net": {
-     "func": rename_net,
-     "args": ["tenant-id", "net-id", "new-name"]},
-   "list_ports": {
-     "func": list_ports,
-     "args": ["tenant-id", "net-id"]},
-   "create_port": {
-     "func": create_port,
-     "args": ["tenant-id", "net-id"]},
-   "delete_port": {
-     "func": delete_port,
-     "args": ["tenant-id", "net-id", "port-id"]},
-   "set_port_state": {
-     "func": set_port_state,
-     "args": ["tenant-id", "net-id", "port-id", "new_state"]},
-   "detail_port": {
-     "func": detail_port,
-     "args": ["tenant-id", "net-id", "port-id"]},
-   "plug_iface": {
-     "func": plug_iface,
-     "args": ["tenant-id", "net-id", "port-id", "iface-id"]},
-   "unplug_iface": {
-     "func": unplug_iface,
-     "args": ["tenant-id", "net-id", "port-id"]}, }
- def help():
-     print "\nCommands:"
-     for k in commands.keys():
-         print "    %s %s" % (k,
-           " ".join(["<%s>" % y for y in commands[k]["args"]]))
- def build_args(cmd, cmdargs, arglist):
-     args = []
-     orig_arglist = arglist[:]
-     try:
-         for x in cmdargs:
-             args.append(arglist[0])
-             del arglist[0]
-     except:
-         LOG.error("Not enough arguments for \"%s\" (expected: %d, got: %d)" % (
-           cmd, len(cmdargs), len(orig_arglist)))
-         print "Usage:\n    %s %s" % (cmd,
-           " ".join(["<%s>" % y for y in commands[cmd]["args"]]))
-         return None
-     if len(arglist) > 0:
-         LOG.error("Too many arguments for \"%s\" (expected: %d, got: %d)" % (
-           cmd, len(cmdargs), len(orig_arglist)))
-         print "Usage:\n    %s %s" % (cmd,
-           " ".join(["<%s>" % y for y in commands[cmd]["args"]]))
-         return None
-     return args
- if __name__ == "__main__":
-     usagestr = "Usage: %prog [OPTIONS] <command> [args]"
-     parser = OptionParser(usage=usagestr)
-     parser.add_option("-H", "--host", dest="host",
-       type="string", default="127.0.0.1", help="ip address of api host")
-     parser.add_option("-p", "--port", dest="port",
-       type="int", default=9696, help="api poort")
-     parser.add_option("-s", "--ssl", dest="ssl",
-       action="store_true", default=False, help="use ssl")
-     parser.add_option("-v", "--verbose", dest="verbose",
-       action="store_true", default=False, help="turn on verbose logging")
-     parser.add_option("-lf", "--logfile", dest="logfile",
-       type="string", default="syslog", help="log file path")
-     options, args = parser.parse_args()
-     if options.verbose:
-         LOG.setLevel(logging.DEBUG)
-     else:
-         LOG.setLevel(logging.WARN)
-     #logging.handlers.WatchedFileHandler
-     if options.logfile == "syslog":
-         LOG.addHandler(logging.handlers.SysLogHandler(address='/dev/log'))
-     else:
-         LOG.addHandler(logging.handlers.WatchedFileHandler(options.logfile))
-         # Set permissions on log file
-         os.chmod(options.logfile, 0644)
-     if len(args) < 1:
-         parser.print_help()
-         help()
-         sys.exit(1)
-     cmd = args[0]
-     if cmd not in commands.keys():
-         LOG.error("Unknown command: %s" % cmd)
-         help()
-         sys.exit(1)
-     args = build_args(cmd, commands[cmd]["args"], args[1:])
-     if not args:
-         sys.exit(1)
-     LOG.info("Executing command \"%s\" with args: %s" % (cmd, args))
-     client = Client(options.host, options.port, options.ssl,
-                     args[0], FORMAT)
-     commands[cmd]["func"](client, *args)
-     LOG.info("Command execution completed")
-     sys.exit(0)
++        data = {'attachment': {'id': '%s' % attachment}}
 +        client.attach_resource(network_id, port_id, data)
 +        LOG.debug("Operation 'attach_resource' executed.")
 +        output = prepare_output("plug_interface", tenant_id,
 +                                dict(network_id=network_id,
 +                                     port_id=port_id,
 +                                     attachment=attachment))
 +        print output
 +    except Exception as ex:
 +        _handle_exception(ex)
 +
 +
 +def unplug_iface(client, *args):
 +    tenant_id, network_id, port_id = args
 +    try:
 +        client.detach_resource(network_id, port_id)
 +        LOG.debug("Operation 'detach_resource' executed.")
 +        output = prepare_output("unplug_interface", tenant_id,
 +                                dict(network_id=network_id,
 +                                     port_id=port_id))
 +        print output
 +    except Exception as ex:
 +        _handle_exception(ex)
index e2857bdd21f812f532384007eba05f1fecd1e4c3,0000000000000000000000000000000000000000..12f394e19e4e9353f74f91e3f143c30d919991e6
mode 100644,000000..100644
--- /dev/null
@@@ -1,53 -1,0 +1,53 @@@
- \tNetwork ID: $network.id
 +## Cheetah template for cli output
 +#if $cmd == 'list_nets'
 +Virtual Networks on Tenant $tenant_id
 +#for $network in $networks
- #for $port in $network.port
- \tLogical Port $port.id: $port.attachment
++    Network ID: $network.id
 +#end for 
 +#elif $cmd == 'create_net'
 +Created a new Virtual Network with ID: $network_id for Tenant $tenant_id
 +#elif $cmd == 'delete_net'
 +Deleted Virtual Network with ID: $network_id for Tenant $tenant_id
 +#elif $cmd == 'detail_net'
 +Network: $network.name ($network.id)
 +Remote Interfaces on Virtual Network
- New name is: $network['net-name']
++#for $port in $network.ports
++    Logical Port $port.id: $port.attachment
 +#end for
 +#elif $cmd == 'rename_net'
 +Renamed Virtual Network with ID: $network.id for Tenant $tenant_id, 
- \tLogical Port: $port.id
++New name is: $network.name
 +#elif $cmd == 'list_ports'
 +Ports on Virtual Network: $network_id
 +#for $port in $ports
- New state is: $port['port-state']
++    Logical Port: $port.id
 +#end for
 +#elif $cmd == 'create_port'
 +Created new Logical Port with ID: $port_id 
 +on Virtual Network: $network_id 
 +for tenant: $tenant_id 
 +#elif $cmd == 'delete_port'
 +Deleted Logical Port with ID: $port_id
 +on Virtual Network: $network_id
 +for tenant: $tenant_id
 +#elif $cmd == 'set_port_state'
 +Updated state for Logical Port with ID: $port.id
++New state is: $port.state
 +on Virtual Network: $network_id
 +for tenant: $tenant_id
 +#elif $cmd == 'detail_port'
 +Logical Port ID: $port.id
 +On Virtual Network: $network_id
 +Administrative State: $port.state
 +Remote Interface: $port.attachment
 +#elif $cmd == 'plug_iface'
 +Plugged interface $attachment
 +into Logical Port: $port_id 
 +on Virtual Network: $network_id
 +for Tenant: $tenant_id
 +#elif $cmd == 'unplug_iface'
 +Unplugged interface from Logical Port: $port_id
 +on Virtual Network: $network_id
 +for Tenant: $tenant_id
 +#end if
 +
index 5898d92b71733ad4d926c9b59926aef3d2dec43f,c75b5a243e92d401b87e9f945bf13965493c6a4b..f55244c6e61b94a4599e76c7dd2d4ae713f19a56
@@@ -20,16 -19,24 +20,26 @@@ import loggin
  import httplib
  import socket
  import urllib
 -from quantum.common.wsgi import Serializer
 +
+ from quantum.common import exceptions
 +from quantum.common.wsgi import Serializer
  
- LOG = logging.getLogger('client')
++LOG = logging.getLogger('quantum.client')
+ EXCEPTIONS = {
+     400: exceptions.BadInputError,
+     401: exceptions.NotAuthorized,
+     420: exceptions.NetworkNotFound,
+     421: exceptions.NetworkInUse,
+     430: exceptions.PortNotFound,
+     431: exceptions.StateInvalid,
+     432: exceptions.PortInUse,
+     440: exceptions.AlreadyAttached}
  
  
- class api_call(object):
+ class ApiCall(object):
      """A Decorator to add support for format and tenant overriding"""
-     def __init__(self, f):
-         self.f = f
+     def __init__(self, function):
+         self.function = function
  
      def __get__(self, instance, owner):
          def with_params(*args, **kwargs):
@@@ -51,9 -60,7 +63,18 @@@ class Client(object)
  
      """A base client class - derived from Glance.BaseClient"""
  
-     action_prefix = '/v0.1/tenants/{tenant_id}'
-     """Action query strings"""
++    #Metadata for deserializing xml
++    _serialization_metadata = {
++        "application/xml": {
++            "attributes": {
++                "network": ["id", "name"],
++                "port": ["id", "state"],
++                "attachment": ["id"]},
++            "plurals": {"networks": "network",
++                        "ports": "port"}},
++    }
++
+     # Action query strings
      networks_path = "/networks"
      network_path = "/networks/%s"
      ports_path = "/networks/%s/ports"
          else:
              return httplib.HTTPConnection
  
 +    def _send_request(self, conn, method, action, body, headers):
 +        # Salvatore: Isolating this piece of code in its own method to
 +        # facilitate stubout for testing
++        if self.logger:
++            self.logger.debug("Quantum Client Request:\n" \
++                    + method + " " + action + "\n")
++            if body:
++                self.logger.debug(body)
 +        conn.request(method, action, body, headers)
 +        return conn.getresponse()
 +
      def do_request(self, method, action, body=None,
                     headers=None, params=None):
          """
  
          if type(params) is dict:
              action += '?' + urllib.urlencode(params)
 -
+         if body:
+             body = self.serialize(body)
          try:
              connection_type = self.get_connection_type()
              headers = headers or {"Content-Type":
              certs = dict((x, certs[x]) for x in certs if certs[x] != None)
  
              if self.use_ssl and len(certs):
-                 c = connection_type(self.host, self.port, **certs)
+                 conn = connection_type(self.host, self.port, **certs)
              else:
-                 c = connection_type(self.host, self.port)
-             res = self._send_request(c, method, action, body, headers)
+                 conn = connection_type(self.host, self.port)
 -
 -            if self.logger:
 -                self.logger.debug("Quantum Client Request:\n" \
 -                        + method + " " + action + "\n")
 -                if body:
 -                    self.logger.debug(body)
 -
 -            conn.request(method, action, body, headers)
 -            res = conn.getresponse()
++            res = self._send_request(conn, method, action, body, headers)
              status_code = self.get_status_code(res)
+             data = res.read()
+             if self.logger:
+                 self.logger.debug("Quantum Client Reply (code = %s) :\n %s" \
+                         % (str(status_code), data))
              if status_code in (httplib.OK,
                                 httplib.CREATED,
                                 httplib.ACCEPTED,
                                 httplib.NO_CONTENT):
-                 return self.deserialize(res)
+                 return self.deserialize(data, status_code)
              else:
-                 # Create exception with HTTP status code and message
 +                error_message = res.read()
 +                LOG.debug("Server returned error: %s", status_code)
 +                LOG.debug("Error message: %s", error_message)
++                # Create exception with HTTP status code and message
+                 if res.status in EXCEPTIONS:
+                     raise EXCEPTIONS[res.status]()
 -                raise Exception("Server returned error: %s" % res.read())
 -
++                # Add error code and message to exception arguments
 +                ex = Exception("Server returned error: %s" % status_code)
 +                ex.args = ([dict(status_code=status_code,
 +                                 message=error_message)],)
 +                raise ex
          except (socket.error, IOError), e:
--            raise Exception("Unable to connect to "
--                            "server. Got error: %s" % e)
++            msg = "Unable to connect to server. Got error: %s" % e
++            LOG.exception(msg)
++            raise Exception(msg)
  
      def get_status_code(self, response):
          """
              return response.status
  
      def serialize(self, data):
-         if type(data) is dict:
+         """
+         Serializes a dictionary with a single key (which can contain any
+         structure) into either xml or json
+         """
+         if data is None:
+             return None
+         elif type(data) is dict:
              return Serializer().serialize(data, self.content_type())
+         else:
+             raise Exception("unable to deserialize object of type = '%s'" \
+                                 % type(data))
  
-     def deserialize(self, data):
-         if self.get_status_code(data) in (202, 204):
-             return data.read()
-         return Serializer().deserialize(data.read(), self.content_type())
+     def deserialize(self, data, status_code):
+         """
+         Deserializes a an xml or json string into a dictionary
+         """
 -        if status_code == 202:
++        if status_code in (202, 204):
+             return data
 -        return Serializer().deserialize(data, self.content_type())
++        #server.networks.Controller._serialization_metadata
++        return Serializer(self._serialization_metadata).\
++                    deserialize(data, self.content_type())
  
      def content_type(self, format=None):
+         """
+         Returns the mime-type for either 'xml' or 'json'.  Defaults to the
+         currently set format
+         """
          if not format:
              format = self.format
          return "application/%s" % (format)
index 2b814eadabfb772b3ce64602b3be84ee75838632,0000000000000000000000000000000000000000..6c71e2cbade79ccc09661d4f002daa9cdf61909a
mode 100644,000000..100644
--- /dev/null
@@@ -1,320 -1,0 +1,408 @@@
- from quantum import cli
 +# vim: tabstop=4 shiftwidth=4 softtabstop=4
 +
 +# Copyright 2010-2011 ????
 +# All Rights Reserved.
 +#
 +#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 +#    not use this file except in compliance with the License. You may obtain
 +#    a copy of the License at
 +#
 +#         http://www.apache.org/licenses/LICENSE-2.0
 +#
 +#    Unless required by applicable law or agreed to in writing, software
 +#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 +#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 +#    License for the specific language governing permissions and limitations
 +#    under the License.
 +#    @author: Salvatore Orlando, Citrix Systems
 +
 +""" Module containing unit tests for Quantum
 +    command line interface
 +
 +"""
 +
 +
 +import logging
 +import sys
 +import unittest
 +
 +from quantum import api as server
-                             'net-name': nw_list[0].name}
++from quantum import cli_lib as cli
 +from quantum.client import Client
 +from quantum.db import api as db
 +from tests.unit.client_tools import stubs as client_stubs
 +
 +LOG = logging.getLogger('quantum.tests.test_cli')
 +FORMAT = 'json'
 +
 +
 +class CLITest(unittest.TestCase):
 +
 +    def setUp(self):
 +        """Prepare the test environment"""
 +        options = {}
 +        options['plugin_provider'] = 'quantum.plugins.SamplePlugin.FakePlugin'
 +        self.api = server.APIRouterV01(options)
 +
 +        self.tenant_id = "test_tenant"
 +        self.network_name_1 = "test_network_1"
 +        self.network_name_2 = "test_network_2"
 +        # Prepare client and plugin manager
 +        self.client = Client(tenant=self.tenant_id, format=FORMAT,
 +                             testingStub=client_stubs.FakeHTTPConnection)
 +        # Redirect stdout
 +        self.fake_stdout = client_stubs.FakeStdout()
 +        sys.stdout = self.fake_stdout
 +
 +    def tearDown(self):
 +        """Clear the test environment"""
 +        db.clear_db()
 +        sys.stdout = sys.__stdout__
 +
 +    def _verify_list_networks(self):
 +            # Verification - get raw result from db
 +            nw_list = db.network_list(self.tenant_id)
 +            networks = [dict(id=nw.uuid, name=nw.name) for nw in nw_list]
 +            # Fill CLI template
 +            output = cli.prepare_output('list_nets', self.tenant_id,
 +                                        dict(networks=networks))
 +            # Verify!
 +            # Must add newline at the end to match effect of print call
 +            self.assertEquals(self.fake_stdout.make_string(), output + '\n')
 +
 +    def _verify_create_network(self):
 +            # Verification - get raw result from db
 +            nw_list = db.network_list(self.tenant_id)
 +            if len(nw_list) != 1:
 +                self.fail("No network created")
 +            network_id = nw_list[0].uuid
 +            # Fill CLI template
 +            output = cli.prepare_output('create_net', self.tenant_id,
 +                                        dict(network_id=network_id))
 +            # Verify!
 +            # Must add newline at the end to match effect of print call
 +            self.assertEquals(self.fake_stdout.make_string(), output + '\n')
 +
 +    def _verify_delete_network(self, network_id):
 +            # Verification - get raw result from db
 +            nw_list = db.network_list(self.tenant_id)
 +            if len(nw_list) != 0:
 +                self.fail("DB should not contain any network")
 +            # Fill CLI template
 +            output = cli.prepare_output('delete_net', self.tenant_id,
 +                                        dict(network_id=network_id))
 +            # Verify!
 +            # Must add newline at the end to match effect of print call
 +            self.assertEquals(self.fake_stdout.make_string(), output + '\n')
 +
 +    def _verify_rename_network(self):
 +            # Verification - get raw result from db
 +            nw_list = db.network_list(self.tenant_id)
 +            network_data = {'id': nw_list[0].uuid,
-             
++                            'name': nw_list[0].name}
 +            # Fill CLI template
 +            output = cli.prepare_output('rename_net', self.tenant_id,
 +                                        dict(network=network_data))
 +            # Verify!
 +            # Must add newline at the end to match effect of print call
 +            self.assertEquals(self.fake_stdout.make_string(), output + '\n')
-             
++
++    def _verify_detail_network(self):
++            # Verification - get raw result from db
++            nw = db.network_list(self.tenant_id)[0]
++            network = dict(id=nw.uuid, name=nw.name)
++            ports = db.port_list(nw['uuid'])
++            for port in ports:
++                port = db.port_get(port['uuid'], nw['uuid'])
++            network['ports'] = [dict(id=port['uuid'],
++                                     state=port['state'],
++                                     attachment=port['interface_id'])
++                                for port in ports]
++            # Fill CLI template
++            output = cli.prepare_output('detail_net', self.tenant_id,
++                                        dict(network=network))
++            # Verify!
++            # Must add newline at the end to match effect of print call
++            self.assertEquals(self.fake_stdout.make_string(), output + '\n')
++
 +    def _verify_list_ports(self, network_id):
 +            # Verification - get raw result from db
 +            port_list = db.port_list(network_id)
 +            ports = [dict(id=port.uuid, state=port.state)
 +                     for port in port_list]
 +            # Fill CLI template
 +            output = cli.prepare_output('list_ports', self.tenant_id,
 +                                        dict(network_id=network_id,
 +                                             ports=ports))
 +            # Verify!
 +            # Must add newline at the end to match effect of print call
 +            self.assertEquals(self.fake_stdout.make_string(), output + '\n')
-             
++
 +    def _verify_create_port(self, network_id):
 +            # Verification - get raw result from db
 +            port_list = db.port_list(network_id)
 +            if len(port_list) != 1:
 +                self.fail("No port created")
 +            port_id = port_list[0].uuid
 +            # Fill CLI template
 +            output = cli.prepare_output('create_port', self.tenant_id,
 +                                        dict(network_id=network_id,
 +                                             port_id=port_id))
 +            # Verify!
 +            # Must add newline at the end to match effect of print call
 +            self.assertEquals(self.fake_stdout.make_string(), output + '\n')
 +
 +    def _verify_delete_port(self, network_id, port_id):
 +            # Verification - get raw result from db
 +            port_list = db.port_list(network_id)
 +            if len(port_list) != 0:
 +                self.fail("DB should not contain any port")
 +            # Fill CLI template
 +            output = cli.prepare_output('delete_port', self.tenant_id,
 +                                        dict(network_id=network_id,
 +                                             port_id=port_id))
 +            # Verify!
 +            # Must add newline at the end to match effect of print call
 +            self.assertEquals(self.fake_stdout.make_string(), output + '\n')
-             port_data = {'id': port.uuid, 'port-state': port.state}
++
 +    def _verify_set_port_state(self, network_id, port_id):
 +            # Verification - get raw result from db
 +            port = db.port_get(port_id, network_id)
-                          'attachment':'<unavailable>'}
++            port_data = {'id': port.uuid, 'state': port.state}
 +            # Fill CLI template
 +            output = cli.prepare_output('set_port_state', self.tenant_id,
 +                                        dict(network_id=network_id,
 +                                             port=port_data))
 +            # Verify!
 +            # Must add newline at the end to match effect of print call
 +            self.assertEquals(self.fake_stdout.make_string(), output + '\n')
 +
 +    def _verify_detail_port(self, network_id, port_id):
 +            # Verification - get raw result from db
 +            # TODO(salvatore-orlando): Must resolve this issue with
 +            # attachment in separate bug fix.
 +            port = db.port_get(port_id, network_id)
 +            port_data = {'id': port.uuid, 'state': port.state,
-         # Load some data into the datbase
-         net = db.network_create(self.tenant_id, self.network_name_1)
-         db.port_create(net['uuid'])
-         port = db.port_create(net['uuid'])
-         cli.detail_net(self.client, self.tenant_id, net['uuid'])
-         db.port_set_attachment(port['uuid'], net['uuid'], "test_iface_id")
++                         'attachment': '<unavailable>'}
 +            # Fill CLI template
 +            output = cli.prepare_output('detail_port', self.tenant_id,
 +                                        dict(network_id=network_id,
 +                                             port=port_data))
 +            # Verify!
 +            # Must add newline at the end to match effect of print call
 +            self.assertEquals(self.fake_stdout.make_string(), output + '\n')
 +
++    def _verify_plug_iface(self, network_id, port_id):
++            # Verification - get raw result from db
++            port = db.port_get(port_id, network_id)
++            # Fill CLI template
++            output = cli.prepare_output("plug_interface", self.tenant_id,
++                                        dict(network_id=network_id,
++                                             port_id=port['uuid'],
++                                             attachment=port['interface_id']))
++            # Verify!
++            # Must add newline at the end to match effect of print call
++            self.assertEquals(self.fake_stdout.make_string(), output + '\n')
++
++    def _verify_unplug_iface(self, network_id, port_id):
++            # Verification - get raw result from db
++            port = db.port_get(port_id, network_id)
++            # Fill CLI template
++            output = cli.prepare_output("unplug_interface", self.tenant_id,
++                                        dict(network_id=network_id,
++                                             port_id=port['uuid']))
++            # Verify!
++            # Must add newline at the end to match effect of print call
++            self.assertEquals(self.fake_stdout.make_string(), output + '\n')
++
 +    def test_list_networks(self):
 +        try:
 +            # Pre-populate data for testing using db api
 +            db.network_create(self.tenant_id, self.network_name_1)
 +            db.network_create(self.tenant_id, self.network_name_2)
 +
 +            cli.list_nets(self.client, self.tenant_id)
 +        except:
 +            LOG.exception("Exception caught: %s", sys.exc_info())
 +            self.fail("test_list_networks failed due to an exception")
 +
 +        LOG.debug("Operation completed. Verifying result")
 +        LOG.debug(self.fake_stdout.content)
 +        self._verify_list_networks()
 +
 +    def test_create_network(self):
 +        try:
 +            cli.create_net(self.client, self.tenant_id, "test")
 +        except:
 +            LOG.exception("Exception caught: %s", sys.exc_info())
 +            self.fail("test_create_network failed due to an exception")
 +
 +        LOG.debug("Operation completed. Verifying result")
 +        LOG.debug(self.fake_stdout.content)
 +        self._verify_create_network()
 +
 +    def test_delete_network(self):
 +        try:
 +            db.network_create(self.tenant_id, self.network_name_1)
 +            network_id = db.network_list(self.tenant_id)[0]['uuid']
 +            cli.delete_net(self.client, self.tenant_id, network_id)
 +        except:
 +            LOG.exception("Exception caught: %s", sys.exc_info())
 +            self.fail("test_delete_network failed due to an exception")
 +
 +        LOG.debug("Operation completed. Verifying result")
 +        LOG.debug(self.fake_stdout.content)
 +        self._verify_delete_network(network_id)
 +
 +    def test_detail_network(self):
-             
++        try:
++            # Load some data into the datbase
++            net = db.network_create(self.tenant_id, self.network_name_1)
++            db.port_create(net['uuid'])
++            # Create a 2nd port and plug attachment in it
++            port = db.port_create(net['uuid'])
++            db.port_set_attachment(port['uuid'], net['uuid'], "test_iface_id")
++            cli.detail_net(self.client, self.tenant_id, net['uuid'])
++        except:
++            LOG.exception("Exception caught: %s", sys.exc_info())
++            self.fail("test_detail_network failed due to an exception")
++
++        LOG.debug("Operation completed. Verifying result")
++        LOG.debug(self.fake_stdout.content)
++        self._verify_detail_network()
 +
 +    def test_rename_network(self):
 +        try:
 +            net = db.network_create(self.tenant_id, self.network_name_1)
 +            network_id = net['uuid']
 +            cli.rename_net(self.client, self.tenant_id,
 +                           network_id, self.network_name_2)
 +        except:
 +            LOG.exception("Exception caught: %s", sys.exc_info())
 +            self.fail("test_rename_network failed due to an exception")
 +
 +        LOG.debug("Operation completed. Verifying result")
 +        LOG.debug(self.fake_stdout.content)
 +        self._verify_rename_network()
-             
++
 +    def test_list_ports(self):
 +        try:
 +            # Pre-populate data for testing using db api
 +            net = db.network_create(self.tenant_id, self.network_name_1)
 +            network_id = net['uuid']
 +            db.port_create(network_id)
 +            db.port_create(network_id)
 +            cli.list_ports(self.client, self.tenant_id, network_id)
 +        except:
 +            LOG.exception("Exception caught: %s", sys.exc_info())
 +            self.fail("test_list_ports failed due to an exception")
-         self._verify_list_ports(network_id)    
-         
++
 +        LOG.debug("Operation completed. Verifying result")
 +        LOG.debug(self.fake_stdout.content)
-         
++        self._verify_list_ports(network_id)
++
 +    def test_create_port(self):
 +        network_id = None
 +        try:
 +            # Pre-populate data for testing using db api
 +            net = db.network_create(self.tenant_id, self.network_name_1)
 +            network_id = net['uuid']
 +            cli.create_port(self.client, self.tenant_id, network_id)
 +        except:
 +            LOG.exception("Exception caught: %s", sys.exc_info())
 +            self.fail("test_create_port failed due to an exception")
 +
 +        LOG.debug("Operation completed. Verifying result")
 +        LOG.debug(self.fake_stdout.content)
 +        self._verify_create_port(network_id)
-         
++
 +    def test_delete_port(self):
 +        network_id = None
 +        port_id = None
 +        try:
 +            # Pre-populate data for testing using db api
 +            net = db.network_create(self.tenant_id, self.network_name_1)
 +            network_id = net['uuid']
 +            port = db.port_create(network_id)
 +            port_id = port['uuid']
 +            cli.delete_port(self.client, self.tenant_id, network_id, port_id)
 +        except:
 +            LOG.exception("Exception caught: %s", sys.exc_info())
 +            self.fail("test_delete_port failed due to an exception")
 +
 +        LOG.debug("Operation completed. Verifying result")
 +        LOG.debug(self.fake_stdout.content)
 +        self._verify_delete_port(network_id, port_id)
 +
 +    def test_set_port_state(self):
 +        try:
 +            net = db.network_create(self.tenant_id, self.network_name_1)
 +            network_id = net['uuid']
 +            port = db.port_create(network_id)
 +            port_id = port['uuid']
 +            # Default state is DOWN - change to ACTIVE.
 +            cli.set_port_state(self.client, self.tenant_id, network_id,
 +                               port_id, 'ACTIVE')
 +        except:
 +            LOG.exception("Exception caught: %s", sys.exc_info())
 +            self.fail("test_set_port_state failed due to an exception")
 +
 +        LOG.debug("Operation completed. Verifying result")
 +        LOG.debug(self.fake_stdout.content)
 +        self._verify_set_port_state(network_id, port_id)
++
 +    def test_detail_port(self):
 +        network_id = None
 +        port_id = None
 +        try:
 +            # Pre-populate data for testing using db api
 +            net = db.network_create(self.tenant_id, self.network_name_1)
 +            network_id = net['uuid']
 +            port = db.port_create(network_id)
 +            port_id = port['uuid']
 +            cli.detail_port(self.client, self.tenant_id, network_id, port_id)
 +        except:
 +            LOG.exception("Exception caught: %s", sys.exc_info())
 +            self.fail("test_detail_port failed due to an exception")
 +
 +        LOG.debug("Operation completed. Verifying result")
 +        LOG.debug(self.fake_stdout.content)
 +        self._verify_detail_port(network_id, port_id)
++
++    def test_plug_iface(self):
++        network_id = None
++        port_id = None
++        try:
++            # Load some data into the datbase
++            net = db.network_create(self.tenant_id, self.network_name_1)
++            network_id = net['uuid']
++            port = db.port_create(net['uuid'])
++            port_id = port['uuid']
++            cli.plug_iface(self.client, self.tenant_id, network_id,
++                           port_id, "test_iface_id")
++        except:
++            LOG.exception("Exception caught: %s", sys.exc_info())
++            self.fail("test_plug_iface failed due to an exception")
++
++        LOG.debug("Operation completed. Verifying result")
++        LOG.debug(self.fake_stdout.content)
++        self._verify_plug_iface(network_id, port_id)
++
++    def test_unplug_iface(self):
++        network_id = None
++        port_id = None
++        try:
++            # Load some data into the datbase
++            net = db.network_create(self.tenant_id, self.network_name_1)
++            network_id = net['uuid']
++            port = db.port_create(net['uuid'])
++            port_id = port['uuid']
++            db.port_set_attachment(port_id, network_id, "test_iface_id")
++            cli.unplug_iface(self.client, self.tenant_id, network_id, port_id)
++        except:
++            LOG.exception("Exception caught: %s", sys.exc_info())
++            self.fail("test_plug_iface failed due to an exception")
++
++        LOG.debug("Operation completed. Verifying result")
++        LOG.debug(self.fake_stdout.content)
++        self._verify_unplug_iface(network_id, port_id)
Simple merge