diff --git a/README b/README index 19d15d095d0..f3e973faacd 100644 --- a/README +++ b/README @@ -1,25 +1,29 @@ # -- Welcome! - You have come across a cloud computing network fabric controller. It has identified - itself as "Quantum." It aims to tame your (cloud) networking! + You have come across a cloud computing network fabric controller. It has + identified itself as "Quantum." It aims to tame your (cloud) networking! # -- Basics: -1) Quantum REST API: Quantum supports a REST-ful programmatic interface to manage your - cloud networking fabric. +1) Quantum REST API: Quantum supports a REST-ful programmatic interface to + manage your cloud networking fabric. -2) Quantum Plugins: Quantum sports a plug-able architecture that allows Quantum's REST API - to be backed by various entities that can create a cloud-class virtual networking fabric. - The advantages of this plug-able architecture is two-folds: +2) Quantum Plugins: Quantum sports a plug-able architecture that allows + Quantum's REST API to be backed by various entities that can create a + cloud-class virtual networking fabric. The advantages of this plug-able + architecture is two-folds: - a) Allows for ANY open-source project or commercial vendor to write a Quantum plug-in. + a) Allows for ANY open-source project or commercial vendor to write a + Quantum plug-in. - b) Allows Quantum users to not be tied down to a single Quantum implementation and - enables them to switch out a plug-in by simple editing a config file - plugins.ini + b) Allows Quantum users to not be tied down to a single Quantum + implementation and enables them to switch out a plug-in by simple editing a + config file - plugins.ini # -- Dependencies - The following python packages are required to run quantum. These can be installed using pip: + The following python packages are required to run quantum. These can be + installed using pip: eventlet>=0.9.12 nose @@ -32,7 +36,9 @@ webob webtest -1) Install easy_install (there is probably a distribution specific package for this) +1) Install easy_install (there is probably a distribution specific package for +this) + 2) Install pip: $ easy_install pip==dev 3) Install packages with pip: @@ -40,14 +46,16 @@ # -- Configuring Quantum plug-in -1) Explore sample and real Quantum plug-ins in the quantum.plugins module. +1) Identify your desired plug-in. Choose a plugin from one of he options in + the quantum/plugins directory. -2) Or copy another Quantum plug-in into the quantum.plugins module. +2) Update plug-in configuration by editing the quantum/plugins.ini file and + modify "provider" property to point to the location of the Quantum plug-in. + It should specify the class path to the plugin and the class name (i.e. for + a plugin class MyPlugin in quantum/plugins/myplugin/myplugin.py the + provider would be: quantum.plugins.myplugin.myplugin.MyPlugin) -3) Update plug-in configuration by editing plugins.ini file and modify - "provider" property to point to the location of the Quantum plug-in. - -4) Read the plugin specific README, this is usually found in the same +3) Read the plugin specific README, this is usually found in the same directory as your Quantum plug-in, and follow configuration instructions. # -- Launching the Quantum Service @@ -63,8 +71,8 @@ Please refer to sample Web Service client code in: # -- CLI tools to program the Quantum-managed Cloud networking fabric -Quantum comes with a programmatic CLI that is driven by the Quantum Web Service -You can use the CLI by issuing the following command: +Quantum comes with a programmatic CLI that is driven by the Quantum Web +Service. You can use the CLI by issuing the following command: ~/src/quantum$ PYTHONPATH=.:$PYTHONPATH python quantum/cli.py @@ -85,16 +93,16 @@ well as sample plugins available in: There are a few requirements to writing your own plugin: -1) Your plugin should implement all methods defined in -../quantum/quantum/quantum_plugin_base.QuantumPluginBase class +1) Your plugin should implement all methods defined in the + quantum/quantum_plugin_base.QuantumPluginBase class -2) Copy your Quantum plug-in over to the ../quantum/quantum/plugins/.. directory +2) Copy your Quantum plug-in over to the quantum/quantum/plugins/.. directory 3) The next step is to edit the plugins.ini file in the same directory as QuantumPluginBase class and specify the location of your custom plugin as the "provider" -4) Launch the Quantum Service, and your plug-in is configured and ready to manage - a Cloud Networking Fabric. +4) Launch the Quantum Service, and your plug-in is configured and ready to + manage a Cloud Networking Fabric. diff --git a/bin/quantum b/bin/quantum index 0913c31c054..48abd18ae90 100755 --- a/bin/quantum +++ b/bin/quantum @@ -33,9 +33,10 @@ if os.path.exists(os.path.join(possible_topdir, 'quantum', '__init__.py')): gettext.install('quantum', unicode=1) -from quantum import service +from quantum import service from quantum.common import config + def create_options(parser): """ Sets up the CLI and config-file options that may be @@ -58,4 +59,3 @@ if __name__ == '__main__': service.wait() except RuntimeError, e: sys.exit("ERROR: %s" % e) - diff --git a/etc/quantum.conf.test b/etc/quantum.conf.test index 3e532b04dd2..b1c266246af 100644 --- a/etc/quantum.conf.test +++ b/etc/quantum.conf.test @@ -12,4 +12,4 @@ paste.app_factory = quantum.l2Network.service:app_factory bind_host = 0.0.0.0 # Port the bind the API server to -bind_port = 9696 \ No newline at end of file +bind_port = 9696 diff --git a/quantum/api/__init__.py b/quantum/api/__init__.py index 3665464c921..3bf7f113ad8 100644 --- a/quantum/api/__init__.py +++ b/quantum/api/__init__.py @@ -54,8 +54,9 @@ class APIRouterV01(wsgi.Router): mapper.resource('port', 'ports', controller=ports.Controller(), parent_resource=dict(member_name='network', - collection_name=\ - uri_prefix + 'networks')) + collection_name=uri_prefix +\ + 'networks')) + mapper.connect("get_resource", uri_prefix + 'networks/{network_id}/' \ 'ports/{id}/attachment{.format}', diff --git a/quantum/api/faults.py b/quantum/api/faults.py index a10364df19a..35f6c1073da 100644 --- a/quantum/api/faults.py +++ b/quantum/api/faults.py @@ -36,8 +36,7 @@ class Fault(webob.exc.HTTPException): 432: "portInUse", 440: "alreadyAttached", 470: "serviceUnavailable", - 471: "pluginFault" - } + 471: "pluginFault"} def __init__(self, exception): """Create a Fault for the given webob.exc.exception.""" @@ -52,7 +51,7 @@ class Fault(webob.exc.HTTPException): fault_data = { fault_name: { 'code': code, - 'message': self.wrapped_exc.explanation, + 'message': self.wrapped_exc.explanation, 'detail': self.wrapped_exc.detail}} # 'code' is an attribute on the fault tag itself metadata = {'application/xml': {'attributes': {fault_name: 'code'}}} diff --git a/quantum/api/networks.py b/quantum/api/networks.py index 98dd5e305d4..a24cf09ab50 100644 --- a/quantum/api/networks.py +++ b/quantum/api/networks.py @@ -44,63 +44,66 @@ class Controller(common.QuantumController): self._resource_name = 'network' super(Controller, self).__init__() - def index(self, req, tenant_id): + def index(self, request, tenant_id): """ Returns a list of network ids """ #TODO: this should be for a given tenant!!! - return self._items(req, tenant_id, is_detail=False) + return self._items(request, tenant_id, is_detail=False) - def _items(self, req, tenant_id, is_detail): + def _items(self, request, tenant_id, is_detail): """ Returns a list of networks. """ networks = self.network_manager.get_all_networks(tenant_id) - builder = networks_view.get_view_builder(req) + builder = networks_view.get_view_builder(request) result = [builder.build(network, is_detail)['network'] for network in networks] return dict(networks=result) - def show(self, req, tenant_id, id): + def show(self, request, tenant_id, id): """ Returns network details for the given network id """ try: network = self.network_manager.get_network_details( tenant_id, id) - builder = networks_view.get_view_builder(req) + builder = networks_view.get_view_builder(request) #build response with details result = builder.build(network, True) return dict(networks=result) except exception.NetworkNotFound as e: return faults.Fault(faults.NetworkNotFound(e)) - def create(self, req, tenant_id): + def create(self, request, tenant_id): """ Creates a new network for a given tenant """ #look for network name in request try: - req_params = \ - self._parse_request_params(req, self._network_ops_param_list) + request_params = \ + self._parse_request_params(request, + self._network_ops_param_list) except exc.HTTPError as e: return faults.Fault(e) network = self.network_manager.\ - create_network(tenant_id,req_params['network-name']) - builder = networks_view.get_view_builder(req) + create_network(tenant_id, + request_params['network-name']) + builder = networks_view.get_view_builder(request) result = builder.build(network) return dict(networks=result) - def update(self, req, tenant_id, id): + def update(self, request, tenant_id, id): """ Updates the name for the network with the given id """ try: - req_params = \ - self._parse_request_params(req, self._network_ops_param_list) + request_params = \ + self._parse_request_params(request, + self._network_ops_param_list) except exc.HTTPError as e: return faults.Fault(e) try: network = self.network_manager.rename_network(tenant_id, - id, req_params['network-name']) + id, request_params['network-name']) - builder = networks_view.get_view_builder(req) + builder = networks_view.get_view_builder(request) result = builder.build(network, True) return dict(networks=result) except exception.NetworkNotFound as e: return faults.Fault(faults.NetworkNotFound(e)) - def delete(self, req, tenant_id, id): + def delete(self, request, tenant_id, id): """ Destroys the network with the given id """ try: self.network_manager.delete_network(tenant_id, id) diff --git a/quantum/api/ports.py b/quantum/api/ports.py index 8e010063463..c2de0d75fbf 100644 --- a/quantum/api/ports.py +++ b/quantum/api/ports.py @@ -24,51 +24,49 @@ from quantum.common import exceptions as exception LOG = logging.getLogger('quantum.api.ports') + class Controller(common.QuantumController): """ Port API controller for Quantum API """ _port_ops_param_list = [{ 'param-name': 'port-state', 'default-value': 'DOWN', - 'required': False},] + 'required': False}, ] _attachment_ops_param_list = [{ 'param-name': 'attachment-id', - 'required': True},] + 'required': True}, ] _serialization_metadata = { "application/xml": { "attributes": { - "port": ["id","state"], - }, - }, - } + "port": ["id", "state"], }, }, } def __init__(self, plugin_conf_file=None): self._resource_name = 'port' super(Controller, self).__init__() - def index(self, req, tenant_id, network_id): + def index(self, request, tenant_id, network_id): """ Returns a list of port ids for a given network """ - return self._items(req, tenant_id, network_id, is_detail=False) + return self._items(request, tenant_id, network_id, is_detail=False) - def _items(self, req, tenant_id, network_id, is_detail): + def _items(self, request, tenant_id, network_id, is_detail): """ Returns a list of networks. """ - try : + try: ports = self.network_manager.get_all_ports(tenant_id, network_id) - builder = ports_view.get_view_builder(req) + builder = ports_view.get_view_builder(request) result = [builder.build(port, is_detail)['port'] for port in ports] return dict(ports=result) except exception.NetworkNotFound as e: return faults.Fault(faults.NetworkNotFound(e)) - def show(self, req, tenant_id, network_id, id): + def show(self, request, tenant_id, network_id, id): """ Returns port details for given port and network """ try: port = self.network_manager.get_port_details( tenant_id, network_id, id) - builder = ports_view.get_view_builder(req) + builder = ports_view.get_view_builder(request) #build response with details result = builder.build(port, True) return dict(ports=result) @@ -77,19 +75,19 @@ class Controller(common.QuantumController): except exception.PortNotFound as e: return faults.Fault(faults.PortNotFound(e)) - def create(self, req, tenant_id, network_id): + def create(self, request, tenant_id, network_id): """ Creates a new port for a given network """ #look for port state in request try: - req_params = \ - self._parse_request_params(req, self._port_ops_param_list) + request_params = \ + self._parse_request_params(request, self._port_ops_param_list) except exc.HTTPError as e: return faults.Fault(e) try: port = self.network_manager.create_port(tenant_id, - network_id, - req_params['port-state']) - builder = ports_view.get_view_builder(req) + network_id, + request_params['port-state']) + builder = ports_view.get_view_builder(request) result = builder.build(port) return dict(ports=result) except exception.NetworkNotFound as e: @@ -97,18 +95,18 @@ class Controller(common.QuantumController): except exception.StateInvalid as e: return faults.Fault(faults.RequestedStateInvalid(e)) - def update(self, req, tenant_id, network_id, id): + def update(self, request, tenant_id, network_id, id): """ Updates the state of a port for a given network """ #look for port state in request try: - req_params = \ - self._parse_request_params(req, self._port_ops_param_list) + request_params = \ + self._parse_request_params(request, self._port_ops_param_list) except exc.HTTPError as e: return faults.Fault(e) try: - port = self.network_manager.update_port(tenant_id,network_id, id, - req_params['port-state']) - builder = ports_view.get_view_builder(req) + port = self.network_manager.update_port(tenant_id, network_id, id, + request_params['port-state']) + builder = ports_view.get_view_builder(request) result = builder.build(port, True) return dict(ports=result) except exception.NetworkNotFound as e: @@ -118,7 +116,7 @@ class Controller(common.QuantumController): except exception.StateInvalid as e: return faults.Fault(faults.RequestedStateInvalid(e)) - def delete(self, req, tenant_id, network_id, id): + def delete(self, request, tenant_id, network_id, id): """ Destroys the port with the given id """ #look for port state in request try: @@ -132,7 +130,7 @@ class Controller(common.QuantumController): except exception.PortInUse as e: return faults.Fault(faults.PortInUse(e)) - def get_resource(self,req,tenant_id, network_id, id): + def get_resource(self, request, tenant_id, network_id, id): try: result = self.network_manager.get_interface_details( tenant_id, network_id, id) @@ -143,19 +141,19 @@ class Controller(common.QuantumController): return faults.Fault(faults.PortNotFound(e)) #TODO - Complete implementation of these APIs - def attach_resource(self,req,tenant_id, network_id, id): - content_type = req.best_match_content_type() - print "Content type:%s" %content_type + def attach_resource(self, request, tenant_id, network_id, id): + content_type = request.best_match_content_type() + print "Content type:%s" % content_type try: - req_params = \ - self._parse_request_params(req, + request_params = \ + self._parse_request_params(request, self._attachment_ops_param_list) except exc.HTTPError as e: return faults.Fault(e) try: self.network_manager.plug_interface(tenant_id, - network_id,id, - req_params['attachment-id']) + network_id, id, + request_params['attachment-id']) return exc.HTTPAccepted() except exception.NetworkNotFound as e: return faults.Fault(faults.NetworkNotFound(e)) @@ -167,10 +165,10 @@ class Controller(common.QuantumController): return faults.Fault(faults.AlreadyAttached(e)) #TODO - Complete implementation of these APIs - def detach_resource(self,req,tenant_id, network_id, id): + def detach_resource(self, request, tenant_id, network_id, id): try: self.network_manager.unplug_interface(tenant_id, - network_id,id) + network_id, id) return exc.HTTPAccepted() except exception.NetworkNotFound as e: return faults.Fault(faults.NetworkNotFound(e)) diff --git a/quantum/api/views/__init__.py b/quantum/api/views/__init__.py index ea910340037..cd6a1d3e299 100644 --- a/quantum/api/views/__init__.py +++ b/quantum/api/views/__init__.py @@ -13,4 +13,4 @@ # 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. \ No newline at end of file +# @author: Somik Behera, Nicira Networks, Inc. diff --git a/quantum/api/views/networks.py b/quantum/api/views/networks.py index 2a64d87f095..6630c6c39ac 100644 --- a/quantum/api/views/networks.py +++ b/quantum/api/views/networks.py @@ -33,17 +33,17 @@ class ViewBuilder(object): def build(self, network_data, is_detail=False): """Generic method used to generate a network entity.""" - print "NETWORK-DATA:%s" %network_data + print "NETWORK-DATA:%s" % network_data if is_detail: network = self._build_detail(network_data) else: network = self._build_simple(network_data) return network - + def _build_simple(self, network_data): """Return a simple model of a server.""" return dict(network=dict(id=network_data['net-id'])) - + def _build_detail(self, network_data): """Return a simple model of a server.""" return dict(network=dict(id=network_data['net-id'], diff --git a/quantum/api/views/ports.py b/quantum/api/views/ports.py index 6077214b68f..dabc7cf0f31 100644 --- a/quantum/api/views/ports.py +++ b/quantum/api/views/ports.py @@ -31,7 +31,7 @@ class ViewBuilder(object): def build(self, port_data, is_detail=False): """Generic method used to generate a port entity.""" - print "PORT-DATA:%s" %port_data + print "PORT-DATA:%s" % port_data if is_detail: port = self._build_detail(port_data) else: diff --git a/quantum/cli.py b/quantum/cli.py index a0121b0341a..43ddc677d81 100644 --- a/quantum/cli.py +++ b/quantum/cli.py @@ -31,25 +31,29 @@ from quantum.common.wsgi import Serializer FORMAT = "json" CONTENT_TYPE = "application/" + FORMAT + ### --- Miniclient (taking from the test directory) ### TODO(bgh): move this to a library within quantum class MiniClient(object): """A base client class - derived from Glance.BaseClient""" action_prefix = '/v0.1/tenants/{tenant_id}' + def __init__(self, host, port, use_ssl): self.host = host self.port = port self.use_ssl = use_ssl self.connection = None + def get_connection_type(self): if self.use_ssl: return httplib.HTTPSConnection else: return httplib.HTTPConnection + def do_request(self, tenant, method, action, body=None, headers=None, params=None): action = MiniClient.action_prefix + action - action = action.replace('{tenant_id}',tenant) + action = action.replace('{tenant_id}', tenant) if type(params) is dict: action += '?' + urllib.urlencode(params) try: @@ -67,6 +71,7 @@ class MiniClient(object): raise Exception("Server returned error: %s" % res.read()) except (socket.error, IOError), e: raise Exception("Unable to connect to server. Got error: %s" % e) + def get_status_code(self, response): if hasattr(response, 'status_int'): return response.status_int @@ -76,6 +81,7 @@ class MiniClient(object): ### -- Core CLI functions + def list_nets(manager, *args): tenant_id = args[0] networks = manager.get_all_networks(tenant_id) @@ -85,6 +91,7 @@ def list_nets(manager, *args): name = net["net-name"] print "\tNetwork ID:%s \n\tNetwork Name:%s \n" % (id, name) + def api_list_nets(client, *args): tenant_id = args[0] res = client.do_request(tenant_id, 'GET', "/networks." + FORMAT) @@ -98,11 +105,13 @@ def api_list_nets(client, *args): # name = n["net-name"] # LOG.info("\tNetwork ID:%s \n\tNetwork Name:%s \n" % (id, name)) + def create_net(manager, *args): tid, name = args new_net_id = manager.create_network(tid, name) print "Created a new Virtual Network with ID:%s\n" % new_net_id + def api_create_net(client, *args): tid, name = args data = {'network': {'network-name': '%s' % name}} @@ -119,11 +128,13 @@ def api_create_net(client, *args): return print "Created a new Virtual Network with ID:%s\n" % nid + def delete_net(manager, *args): tid, nid = args manager.delete_network(tid, nid) print "Deleted Virtual Network with ID:%s" % nid + def api_delete_net(client, *args): tid, nid = args res = client.do_request(tid, 'DELETE', "/networks/" + nid + "." + FORMAT) @@ -135,6 +146,7 @@ def api_delete_net(client, *args): else: print "Deleted Virtual Network with ID:%s" % nid + def detail_net(manager, *args): tid, nid = args iface_list = manager.get_network_details(tid, nid) @@ -142,6 +154,7 @@ def detail_net(manager, *args): for iface in iface_list: print "\tRemote interface:%s" % iface + def api_detail_net(client, *args): tid, nid = args res = client.do_request(tid, 'GET', @@ -163,11 +176,13 @@ def api_detail_net(client, *args): remote_iface = rd["attachment"] print "\tRemote interface:%s" % remote_iface + def rename_net(manager, *args): tid, nid, name = args manager.rename_network(tid, nid, name) print "Renamed Virtual Network with ID:%s" % nid + def api_rename_net(client, *args): tid, nid, name = args data = {'network': {'network-name': '%s' % name}} @@ -178,6 +193,7 @@ def api_rename_net(client, *args): LOG.debug(resdict) print "Renamed Virtual Network with ID:%s" % nid + def list_ports(manager, *args): tid, nid = args ports = manager.get_all_ports(tid, nid) @@ -185,6 +201,7 @@ def list_ports(manager, *args): for port in ports: print "\tVirtual Port:%s" % port["port-id"] + def api_list_ports(client, *args): tid, nid = args res = client.do_request(tid, 'GET', @@ -199,12 +216,14 @@ def api_list_ports(client, *args): for port in rd["ports"]: print "\tVirtual Port:%s" % port["id"] + def create_port(manager, *args): tid, nid = args new_port = manager.create_port(tid, nid) print "Created Virtual Port:%s " \ "on Virtual Network:%s" % (new_port, nid) + def api_create_port(client, *args): tid, nid = args res = client.do_request(tid, 'POST', @@ -218,12 +237,14 @@ def api_create_port(client, *args): print "Created Virtual Port:%s " \ "on Virtual Network:%s" % (new_port, nid) + def delete_port(manager, *args): tid, nid, pid = args - manager.delete_port(tid, nid,pid) + manager.delete_port(tid, nid, pid) LOG.info("Deleted Virtual Port:%s " \ "on Virtual Network:%s" % (pid, nid)) + def api_delete_port(client, *args): tid, nid, pid = args res = client.do_request(tid, 'DELETE', @@ -235,12 +256,14 @@ def api_delete_port(client, *args): LOG.info("Deleted Virtual Port:%s " \ "on Virtual Network:%s" % (pid, nid)) + def detail_port(manager, *args): tid, nid, pid = args port_detail = manager.get_port_details(tid, nid, pid) print "Virtual Port:%s on Virtual Network:%s " \ "contains remote interface:%s" % (pid, nid, port_detail) + def api_detail_port(client, *args): tid, nid, pid = args res = client.do_request(tid, 'GET', @@ -257,12 +280,14 @@ def api_detail_port(client, *args): print "Virtual Port:%s on Virtual Network:%s " \ "contains remote interface:%s" % (pid, nid, attachment) + def plug_iface(manager, *args): tid, nid, pid, vid = args manager.plug_interface(tid, nid, pid, vid) print "Plugged remote interface:%s " \ "into Virtual Network:%s" % (vid, nid) + def api_plug_iface(client, *args): tid, nid, pid, vid = args data = {'port': {'attachment-id': '%s' % vid}} @@ -277,12 +302,14 @@ def api_plug_iface(client, *args): return print "Plugged interface \"%s\" to port:%s on network:%s" % (vid, pid, nid) -def unplug_iface(manager, *args): + +def unplug_iface(manager, *args): tid, nid, pid = args manager.unplug_interface(tid, nid, pid) print "UnPlugged remote interface " \ "from Virtual Port:%s Virtual Network:%s" % (pid, nid) + def api_unplug_iface(client, *args): tid, nid, pid = args data = {'port': {'attachment-id': ''}} @@ -297,63 +324,53 @@ def api_unplug_iface(client, *args): return print "Unplugged interface from port:%s on network:%s" % (pid, nid) + commands = { "list_nets": { "func": list_nets, "api_func": api_list_nets, - "args": ["tenant-id"] - }, + "args": ["tenant-id"]}, "create_net": { "func": create_net, "api_func": api_create_net, - "args": ["tenant-id", "net-name"] - }, + "args": ["tenant-id", "net-name"]}, "delete_net": { "func": delete_net, "api_func": api_delete_net, - "args": ["tenant-id", "net-id"] - }, + "args": ["tenant-id", "net-id"]}, "detail_net": { "func": detail_net, "api_func": api_detail_net, - "args": ["tenant-id", "net-id"] - }, + "args": ["tenant-id", "net-id"]}, "rename_net": { "func": rename_net, "api_func": api_rename_net, - "args": ["tenant-id", "net-id", "new-name"] - }, + "args": ["tenant-id", "net-id", "new-name"]}, "list_ports": { "func": list_ports, "api_func": api_list_ports, - "args": ["tenant-id", "net-id"] - }, + "args": ["tenant-id", "net-id"]}, "create_port": { "func": create_port, "api_func": api_create_port, - "args": ["tenant-id", "net-id"] - }, + "args": ["tenant-id", "net-id"]}, "delete_port": { "func": delete_port, "api_func": api_delete_port, - "args": ["tenant-id", "net-id", "port-id"] - }, + "args": ["tenant-id", "net-id", "port-id"]}, "detail_port": { "func": detail_port, "api_func": api_detail_port, - "args": ["tenant-id", "net-id", "port-id"] - }, + "args": ["tenant-id", "net-id", "port-id"]}, "plug_iface": { "func": plug_iface, "api_func": api_plug_iface, - "args": ["tenant-id", "net-id", "port-id", "iface-id"] - }, + "args": ["tenant-id", "net-id", "port-id", "iface-id"]}, "unplug_iface": { "func": unplug_iface, "api_func": api_unplug_iface, - "args": ["tenant-id", "net-id", "port-id"] - }, - } + "args": ["tenant-id", "net-id", "port-id"]}, } + def help(): print "\nCommands:" @@ -361,6 +378,7 @@ def help(): print " %s %s" % (k, " ".join(["<%s>" % y for y in commands[k]["args"]])) + def build_args(cmd, cmdargs, arglist): args = [] orig_arglist = arglist[:] @@ -382,6 +400,7 @@ def build_args(cmd, cmdargs, arglist): return None return args + if __name__ == "__main__": usagestr = "Usage: %prog [OPTIONS] [args]" parser = OptionParser(usage=usagestr) @@ -421,7 +440,7 @@ if __name__ == "__main__": LOG.debug("Executing command \"%s\" with args: %s" % (cmd, args)) if not options.load_plugin: client = MiniClient(options.host, options.port, options.ssl) - if not commands[cmd].has_key("api_func"): + if "api_func" not in commands[cmd]: LOG.error("API version of \"%s\" is not yet implemented" % cmd) sys.exit(1) commands[cmd]["api_func"](client, *args) diff --git a/quantum/common/config.py b/quantum/common/config.py index cda7650781f..320c3b2cf0a 100644 --- a/quantum/common/config.py +++ b/quantum/common/config.py @@ -209,7 +209,7 @@ def find_config_file(options, args): fix_path(os.path.join('~', '.quantum')), fix_path('~'), os.path.join(FLAGS.state_path, 'etc'), - os.path.join(FLAGS.state_path, 'etc','quantum'), + os.path.join(FLAGS.state_path, 'etc', 'quantum'), '/etc/quantum/', '/etc'] for cfg_dir in config_file_dirs: @@ -244,12 +244,10 @@ def load_paste_config(app_name, options, args): problem loading the configuration file. """ conf_file = find_config_file(options, args) - print "Conf_file:%s" %conf_file if not conf_file: raise RuntimeError("Unable to locate any configuration file. " "Cannot load application %s" % app_name) try: - print "App_name:%s" %app_name conf = deploy.appconfig("config:%s" % conf_file, name=app_name) return conf_file, conf except Exception, e: @@ -257,7 +255,7 @@ def load_paste_config(app_name, options, args): % (conf_file, e)) -def load_paste_app(conf_file, app_name): +def load_paste_app(app_name, options, args): """ Builds and returns a WSGI app from a paste config file. @@ -278,16 +276,15 @@ def load_paste_app(conf_file, app_name): :raises RuntimeError when config file cannot be located or application cannot be loaded from config file """ - #conf_file, conf = load_paste_config(app_name, options, args) + conf_file, conf = load_paste_config(app_name, options, args) try: - conf_file = os.path.abspath(conf_file) app = deploy.loadapp("config:%s" % conf_file, name=app_name) except (LookupError, ImportError), e: raise RuntimeError("Unable to load %(app_name)s from " "configuration file %(conf_file)s." "\nGot: %(e)r" % locals()) - return app + return conf, app def get_option(options, option, **kwargs): diff --git a/quantum/common/exceptions.py b/quantum/common/exceptions.py index 7b9784b921c..4daf31762b9 100644 --- a/quantum/common/exceptions.py +++ b/quantum/common/exceptions.py @@ -25,7 +25,7 @@ import logging class QuantumException(Exception): """Base Quantum Exception - + Taken from nova.exception.NovaException To correctly use this class, inherit from it and define a 'message' property. That message will get printf'd @@ -45,6 +45,7 @@ class QuantumException(Exception): def __str__(self): return self._error_string + class ProcessExecutionError(IOError): def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None, description=None): @@ -84,11 +85,11 @@ class NetworkNotFound(NotFound): class PortNotFound(NotFound): message = _("Port %(port_id)s could not be found " \ "on network %(net_id)s") - + class StateInvalid(QuantumException): message = _("Unsupported port state: %(port_state)s") - + class NetworkInUse(QuantumException): message = _("Unable to complete operation on network %(net_id)s. " \ @@ -100,11 +101,13 @@ class PortInUse(QuantumException): "for network %(net_id)s. The attachment '%(att_id)s" \ "is plugged into the logical port.") + class AlreadyAttached(QuantumException): message = _("Unable to plug the attachment %(att_id)s into port " \ "%(port_id)s for network %(net_id)s. The attachment is " \ "already plugged into port %(att_port_id)s") - + + class Duplicate(Error): pass diff --git a/quantum/common/flags.py b/quantum/common/flags.py index 947999d0f22..5adf4c3854a 100644 --- a/quantum/common/flags.py +++ b/quantum/common/flags.py @@ -23,7 +23,7 @@ Global flags should be defined here, the rest are defined where they're used. """ import getopt -import os +import os import string import sys @@ -249,4 +249,3 @@ def DECLARE(name, module_string, flag_values=FLAGS): DEFINE_string('state_path', os.path.join(os.path.dirname(__file__), '../../'), "Top-level directory for maintaining quantum's state") - diff --git a/quantum/common/utils.py b/quantum/common/utils.py index 7ab4615b2c2..3a2455963f5 100644 --- a/quantum/common/utils.py +++ b/quantum/common/utils.py @@ -37,6 +37,7 @@ from exceptions import ProcessExecutionError TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" FLAGS = flags.FLAGS + def int_from_bool_as_string(subject): """ Interpret a string as a boolean and return either 1 or 0. @@ -188,6 +189,7 @@ def isotime(at=None): def parse_isotime(timestr): return datetime.datetime.strptime(timestr, TIME_FORMAT) + def getPluginFromConfig(file="config.ini"): Config = ConfigParser.ConfigParser() Config.read(file) diff --git a/quantum/common/wsgi.py b/quantum/common/wsgi.py index 4caeab9abca..23e7c1c3d0e 100644 --- a/quantum/common/wsgi.py +++ b/quantum/common/wsgi.py @@ -40,6 +40,7 @@ from quantum.common import exceptions as exception LOG = logging.getLogger('quantum.common.wsgi') + class WritableLogger(object): """A thin wrapper that responds to `write` and logs.""" @@ -126,7 +127,7 @@ class Request(webob.Request): """ parts = self.path.rsplit('.', 1) - LOG.debug("Request parts:%s",parts) + LOG.debug("Request parts:%s", parts) if len(parts) > 1: format = parts[1] if format in ['json', 'xml']: @@ -134,7 +135,7 @@ class Request(webob.Request): ctypes = ['application/json', 'application/xml'] bm = self.accept.best_match(ctypes) - LOG.debug("BM:%s",bm) + LOG.debug("BM:%s", bm) return bm or 'application/json' def get_content_type(self): @@ -336,21 +337,21 @@ class Controller(object): arg_dict = req.environ['wsgiorg.routing_args'][1] action = arg_dict['action'] method = getattr(self, action) - LOG.debug("ARG_DICT:%s",arg_dict) - LOG.debug("Action:%s",action) - LOG.debug("Method:%s",method) + LOG.debug("ARG_DICT:%s", arg_dict) + LOG.debug("Action:%s", action) + LOG.debug("Method:%s", method) LOG.debug("%s %s" % (req.method, req.url)) del arg_dict['controller'] del arg_dict['action'] if 'format' in arg_dict: del arg_dict['format'] - arg_dict['req'] = req + arg_dict['request'] = req result = method(**arg_dict) if type(result) is dict: content_type = req.best_match_content_type() - LOG.debug("Content type:%s",content_type) - LOG.debug("Result:%s",result) + LOG.debug("Content type:%s", content_type) + LOG.debug("Result:%s", result) default_xmlns = self.get_default_xmlns(req) body = self._serialize(result, content_type, default_xmlns) @@ -497,7 +498,7 @@ class Serializer(object): xmlns = metadata.get('xmlns', None) if xmlns: result.setAttribute('xmlns', xmlns) - LOG.debug("DATA:%s",data) + LOG.debug("DATA:%s", data) if type(data) is list: LOG.debug("TYPE IS LIST") collections = metadata.get('list_collections', {}) @@ -538,8 +539,7 @@ class Serializer(object): result.appendChild(node) else: # Type is atom - LOG.debug("TYPE IS ATOM:%s",data) + LOG.debug("TYPE IS ATOM:%s", data) node = doc.createTextNode(str(data)) result.appendChild(node) return result - diff --git a/quantum/db/api.py b/quantum/db/api.py index 1809af05768..989f0d6c38d 100644 --- a/quantum/db/api.py +++ b/quantum/db/api.py @@ -25,6 +25,7 @@ _ENGINE = None _MAKER = None BASE = models.BASE + def configure_db(options): """ Establish the database, create an engine if needed, and @@ -40,6 +41,7 @@ def configure_db(options): pool_recycle=3600) register_models() + def get_session(autocommit=True, expire_on_commit=False): """Helper method to grab session""" global _MAKER, _ENGINE @@ -50,24 +52,27 @@ def get_session(autocommit=True, expire_on_commit=False): expire_on_commit=expire_on_commit) return _MAKER() + def register_models(): """Register Models and create properties""" global _ENGINE assert _ENGINE BASE.metadata.create_all(_ENGINE) + def unregister_models(): """Unregister Models, useful clearing out data before testing""" global _ENGINE assert _ENGINE BASE.metadata.drop_all(_ENGINE) + def network_create(tenant_id, name): session = get_session() net = None try: net = session.query(models.Network).\ - filter_by(tenant_id=tenant_id,name=name).\ + filter_by(tenant_id=tenant_id, name=name).\ one() raise Exception("Network with name \"%s\" already exists" % name) except exc.NoResultFound: @@ -77,12 +82,14 @@ def network_create(tenant_id, name): session.flush() return net + def network_list(tenant_id): session = get_session() return session.query(models.Network).\ filter_by(tenant_id=tenant_id).\ all() + def network_get(net_id): session = get_session() try: @@ -92,11 +99,12 @@ def network_get(net_id): except exc.NoResultFound: raise Exception("No net found with id = %s" % net_id) + def network_rename(net_id, tenant_id, new_name): session = get_session() try: res = session.query(models.Network).\ - filter_by(tenant_id=tenant_id,name=new_name).\ + filter_by(tenant_id=tenant_id, name=new_name).\ one() except exc.NoResultFound: net = network_get(net_id) @@ -106,6 +114,7 @@ def network_rename(net_id, tenant_id, new_name): return net raise Exception("A network with name \"%s\" already exists" % new_name) + def network_destroy(net_id): session = get_session() try: @@ -118,6 +127,7 @@ def network_destroy(net_id): except exc.NoResultFound: raise Exception("No network found with id = %s" % net_id) + def port_create(net_id): session = get_session() with session.begin(): @@ -126,12 +136,14 @@ def port_create(net_id): session.flush() return port + def port_list(net_id): session = get_session() return session.query(models.Port).\ filter_by(network_id=net_id).\ all() + def port_get(port_id): session = get_session() try: @@ -141,6 +153,7 @@ def port_get(port_id): except exc.NoResultFound: raise Exception("No port found with id = %s " % port_id) + def port_set_attachment(port_id, new_interface_id): session = get_session() ports = None @@ -157,7 +170,9 @@ def port_set_attachment(port_id, new_interface_id): session.flush() return port else: - raise Exception("Port with attachment \"%s\" already exists" % (new_interface_id)) + raise Exception("Port with attachment \"%s\" already exists" + % (new_interface_id)) + def port_destroy(port_id): session = get_session() @@ -170,4 +185,3 @@ def port_destroy(port_id): return port except exc.NoResultFound: raise Exception("No port found with id = %s " % port_id) - diff --git a/quantum/db/models.py b/quantum/db/models.py index 28bc139b919..115ab282da8 100644 --- a/quantum/db/models.py +++ b/quantum/db/models.py @@ -25,12 +25,14 @@ from sqlalchemy.orm import relation BASE = declarative_base() + class Port(BASE): """Represents a port on a quantum network""" __tablename__ = 'ports' uuid = Column(String(255), primary_key=True) - network_id = Column(String(255), ForeignKey("networks.uuid"), nullable=False) + network_id = Column(String(255), ForeignKey("networks.uuid"), + nullable=False) interface_id = Column(String(255)) def __init__(self, network_id): @@ -38,7 +40,9 @@ class Port(BASE): self.network_id = network_id def __repr__(self): - return "" % (self.uuid, self.network_id, self.interface_id) + return "" % (self.uuid, self.network_id, + self.interface_id) + class Network(BASE): """Represents a quantum network""" @@ -56,4 +60,4 @@ class Network(BASE): def __repr__(self): return "" % \ - (self.uuid,self.name,self.tenant_id) + (self.uuid, self.name, self.tenant_id) diff --git a/quantum/manager.py b/quantum/manager.py index 9932f17af01..a9662d8eb6a 100644 --- a/quantum/manager.py +++ b/quantum/manager.py @@ -18,12 +18,14 @@ """ -Quantum's Manager class is responsible for parsing a config file and instantiating the correct -plugin that concretely implement quantum_plugin_base class +Quantum's Manager class is responsible for parsing a config file and +instantiating the correct plugin that concretely implement quantum_plugin_base +class. The caller should make sure that QuantumManager is a singleton. """ import gettext +import os gettext.install('quantum', unicode=1) import os @@ -33,16 +35,19 @@ from quantum_plugin_base import QuantumPluginBase CONFIG_FILE = "plugins.ini" + def find_config(basepath): for root, dirs, files in os.walk(basepath): if CONFIG_FILE in files: return os.path.join(root, CONFIG_FILE) return None + class QuantumManager(object): def __init__(self, config=None): if config == None: - self.configuration_file = find_config(os.path.abspath(os.path.dirname(__file__))) + self.configuration_file = find_config( + os.path.abspath(os.path.dirname(__file__))) else: self.configuration_file = config plugin_location = utils.getPluginFromConfig(self.configuration_file) @@ -58,4 +63,3 @@ class QuantumManager(object): def get_manager(self): return self.plugin - diff --git a/quantum/plugins/SamplePlugin.py b/quantum/plugins/SamplePlugin.py index f82397ebbbd..376456a6c81 100644 --- a/quantum/plugins/SamplePlugin.py +++ b/quantum/plugins/SamplePlugin.py @@ -17,33 +17,32 @@ from quantum.common import exceptions as exc + class QuantumEchoPlugin(object): """ QuantumEchoPlugin is a demo plugin that doesn't do anything but demonstrated the concept of a concrete Quantum Plugin. Any call to this plugin - will result in just a "print" to std. out with + will result in just a "print" to std. out with the name of the method that was called. """ - + def get_all_networks(self, tenant_id): """ Returns a dictionary containing all for - the specified tenant. + the specified tenant. """ print("get_all_networks() called\n") - - + def create_network(self, tenant_id, net_name): """ Creates a new Virtual Network, and assigns it a symbolic name. """ print("create_network() called\n") - - + def delete_network(self, tenant_id, net_id): """ Deletes the network with the specified network identifier @@ -51,38 +50,33 @@ class QuantumEchoPlugin(object): """ print("delete_network() called\n") - def get_network_details(self, tenant_id, net_id): """ Deletes the Virtual Network belonging to a the spec """ print("get_network_details() called\n") - - + def rename_network(self, tenant_id, net_id, new_name): """ Updates the symbolic name belonging to a particular Virtual Network. """ print("rename_network() called\n") - - + def get_all_ports(self, tenant_id, net_id): """ Retrieves all port identifiers belonging to the specified Virtual Network. """ print("get_all_ports() called\n") - - + def create_port(self, tenant_id, net_id): """ Creates a port on the specified Virtual Network. """ print("create_port() called\n") - - + def delete_port(self, tenant_id, net_id, port_id): """ Deletes a port on a specified Virtual Network, @@ -97,24 +91,21 @@ class QuantumEchoPlugin(object): Updates the state of a port on the specified Virtual Network. """ print("update_port() called\n") - - + def get_port_details(self, tenant_id, net_id, port_id): """ This method allows the user to retrieve a remote interface that is attached to this particular port. """ print("get_port_details() called\n") - - + def plug_interface(self, tenant_id, net_id, port_id, remote_interface_id): """ Attaches a remote interface to the specified port on the specified Virtual Network. """ print("plug_interface() called\n") - - + def unplug_interface(self, tenant_id, net_id, port_id): """ Detaches a remote interface from the specified port on the @@ -130,18 +121,17 @@ class DummyDataPlugin(object): hard-coded data structures to aid in quantum client/cli development """ - + def get_all_networks(self, tenant_id): """ Returns a dictionary containing all for - the specified tenant. + the specified tenant. """ - nets = {"001": "lNet1", "002": "lNet2" , "003": "lNet3"} + nets = {"001": "lNet1", "002": "lNet2", "003": "lNet3"} print("get_all_networks() called\n") return nets - - + def create_network(self, tenant_id, net_name): """ Creates a new Virtual Network, and assigns it @@ -150,8 +140,7 @@ class DummyDataPlugin(object): print("create_network() called\n") # return network_id of the created network return 101 - - + def delete_network(self, tenant_id, net_id): """ Deletes the network with the specified network identifier @@ -159,7 +148,6 @@ class DummyDataPlugin(object): """ print("delete_network() called\n") - def get_network_details(self, tenant_id, net_id): """ retrieved a list of all the remote vifs that @@ -168,16 +156,14 @@ class DummyDataPlugin(object): print("get_network_details() called\n") vifs_on_net = ["/tenant1/networks/net_id/portid/vif2.0"] return vifs_on_net - - + def rename_network(self, tenant_id, net_id, new_name): """ Updates the symbolic name belonging to a particular Virtual Network. """ print("rename_network() called\n") - - + def get_all_ports(self, tenant_id, net_id): """ Retrieves all port identifiers belonging to the @@ -186,8 +172,7 @@ class DummyDataPlugin(object): print("get_all_ports() called\n") port_ids_on_net = ["2", "3", "4"] return port_ids_on_net - - + def create_port(self, tenant_id, net_id): """ Creates a port on the specified Virtual Network. @@ -201,8 +186,7 @@ class DummyDataPlugin(object): Updates the state of a port on the specified Virtual Network. """ print("update_port() called\n") - - + def delete_port(self, tenant_id, net_id, port_id): """ Deletes a port on a specified Virtual Network, @@ -211,8 +195,7 @@ class DummyDataPlugin(object): is deleted. """ print("delete_port() called\n") - - + def get_port_details(self, tenant_id, net_id, port_id): """ This method allows the user to retrieve a remote interface @@ -221,24 +204,22 @@ class DummyDataPlugin(object): print("get_port_details() called\n") #returns the remote interface UUID return "/tenant1/networks/net_id/portid/vif2.1" - - + def plug_interface(self, tenant_id, net_id, port_id, remote_interface_id): """ Attaches a remote interface to the specified port on the specified Virtual Network. """ print("plug_interface() called\n") - - + def unplug_interface(self, tenant_id, net_id, port_id): """ Detaches a remote interface from the specified port on the specified Virtual Network. """ print("unplug_interface() called\n") - - + + class FakePlugin(object): """ FakePlugin is a demo plugin that provides @@ -248,72 +229,66 @@ class FakePlugin(object): #static data for networks and ports _port_dict_1 = { - 1 : {'port-id': 1, + 1: {'port-id': 1, 'port-state': 'DOWN', 'attachment': None}, - 2 : {'port-id': 2, - 'port-state':'UP', - 'attachment': None} - } + 2: {'port-id': 2, + 'port-state': 'UP', + 'attachment': None}} _port_dict_2 = { - 1 : {'port-id': 1, + 1: {'port-id': 1, 'port-state': 'UP', 'attachment': 'SomeFormOfVIFID'}, - 2 : {'port-id': 2, - 'port-state':'DOWN', - 'attachment': None} - } - _networks={'001': + 2: {'port-id': 2, + 'port-state': 'DOWN', + 'attachment': None}} + _networks = {'001': { - 'net-id':'001', - 'net-name':'pippotest', - 'net-ports': _port_dict_1 - }, + 'net-id': '001', + 'net-name': 'pippotest', + 'net-ports': _port_dict_1}, '002': { - 'net-id':'002', - 'net-name':'cicciotest', - 'net-ports': _port_dict_2 - }} - - + 'net-id': '002', + 'net-name': 'cicciotest', + 'net-ports': _port_dict_2}} + def __init__(self): - FakePlugin._net_counter=len(FakePlugin._networks) - + FakePlugin._net_counter = len(FakePlugin._networks) + def _get_network(self, tenant_id, network_id): network = FakePlugin._networks.get(network_id) if not network: raise exc.NetworkNotFound(net_id=network_id) return network - def _get_port(self, tenant_id, network_id, port_id): net = self._get_network(tenant_id, network_id) port = net['net-ports'].get(int(port_id)) if not port: raise exc.PortNotFound(net_id=network_id, port_id=port_id) return port - + def _validate_port_state(self, port_state): - if port_state.upper() not in ('UP','DOWN'): + if port_state.upper() not in ('UP', 'DOWN'): raise exc.StateInvalid(port_state=port_state) return True - + def _validate_attachment(self, tenant_id, network_id, port_id, remote_interface_id): network = self._get_network(tenant_id, network_id) for port in network['net-ports'].values(): if port['attachment'] == remote_interface_id: - raise exc.AlreadyAttached(net_id = network_id, - port_id = port_id, - att_id = port['attachment'], - att_port_id = port['port-id']) - + raise exc.AlreadyAttached(net_id=network_id, + port_id=port_id, + att_id=port['attachment'], + att_port_id=port['port-id']) + def get_all_networks(self, tenant_id): """ Returns a dictionary containing all for - the specified tenant. + the specified tenant. """ print("get_all_networks() called\n") return FakePlugin._networks.values() @@ -333,16 +308,16 @@ class FakePlugin(object): """ print("create_network() called\n") FakePlugin._net_counter += 1 - new_net_id=("0" * (3 - len(str(FakePlugin._net_counter)))) + \ + new_net_id = ("0" * (3 - len(str(FakePlugin._net_counter)))) + \ str(FakePlugin._net_counter) print new_net_id - new_net_dict={'net-id':new_net_id, - 'net-name':net_name, + new_net_dict = {'net-id': new_net_id, + 'net-name': net_name, 'net-ports': {}} - FakePlugin._networks[new_net_id]=new_net_dict + FakePlugin._networks[new_net_id] = new_net_dict # return network_id of the created network return new_net_dict - + def delete_network(self, tenant_id, net_id): """ Deletes the network with the specified network identifier @@ -360,7 +335,7 @@ class FakePlugin(object): return net # Network not found raise exc.NetworkNotFound(net_id=net_id) - + def rename_network(self, tenant_id, net_id, new_name): """ Updates the symbolic name belonging to a particular @@ -368,7 +343,7 @@ class FakePlugin(object): """ print("rename_network() called\n") net = self._get_network(tenant_id, net_id) - net['net-name']=new_name + net['net-name'] = new_name return net def get_all_ports(self, tenant_id, net_id): @@ -388,7 +363,7 @@ class FakePlugin(object): """ print("get_port_details() called\n") return self._get_port(tenant_id, net_id, port_id) - + def create_port(self, tenant_id, net_id, port_state=None): """ Creates a port on the specified Virtual Network. @@ -396,15 +371,15 @@ class FakePlugin(object): print("create_port() called\n") net = self._get_network(tenant_id, net_id) # check port state - # TODO(salvatore-orlando): Validate port state in API? + # TODO(salvatore-orlando): Validate port state in API? self._validate_port_state(port_state) ports = net['net-ports'] - new_port_id = max(ports.keys())+1 - new_port_dict = {'port-id':new_port_id, + new_port_id = max(ports.keys()) + 1 + new_port_dict = {'port-id': new_port_id, 'port-state': port_state, 'attachment': None} ports[new_port_id] = new_port_dict - return new_port_dict + return new_port_dict def update_port(self, tenant_id, net_id, port_id, port_state): """ @@ -414,8 +389,8 @@ class FakePlugin(object): port = self._get_port(tenant_id, net_id, port_id) self._validate_port_state(port_state) port['port-state'] = port_state - return port - + return port + def delete_port(self, tenant_id, net_id, port_id): """ Deletes a port on a specified Virtual Network, @@ -427,14 +402,13 @@ class FakePlugin(object): net = self._get_network(tenant_id, net_id) port = self._get_port(tenant_id, net_id, port_id) if port['attachment']: - raise exc.PortInUse(net_id=net_id,port_id=port_id, + raise exc.PortInUse(net_id=net_id, port_id=port_id, att_id=port['attachment']) try: net['net-ports'].pop(int(port_id)) - except KeyError: + except KeyError: raise exc.PortNotFound(net_id=net_id, port_id=port_id) - def plug_interface(self, tenant_id, net_id, port_id, remote_interface_id): """ Attaches a remote interface to the specified port on the @@ -446,10 +420,10 @@ class FakePlugin(object): remote_interface_id) port = self._get_port(tenant_id, net_id, port_id) if port['attachment']: - raise exc.PortInUse(net_id=net_id,port_id=port_id, + raise exc.PortInUse(net_id=net_id, port_id=port_id, att_id=port['attachment']) port['attachment'] = remote_interface_id - + def unplug_interface(self, tenant_id, net_id, port_id): """ Detaches a remote interface from the specified port on the @@ -460,5 +434,3 @@ class FakePlugin(object): # TODO(salvatore-orlando): # Should unplug on port without attachment raise an Error? port['attachment'] = None - - \ No newline at end of file diff --git a/quantum/plugins/__init__.py b/quantum/plugins/__init__.py index df928bbf1ca..7e695ff08d5 100644 --- a/quantum/plugins/__init__.py +++ b/quantum/plugins/__init__.py @@ -13,4 +13,4 @@ # 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. \ No newline at end of file +# @author: Somik Behera, Nicira Networks, Inc. diff --git a/quantum/plugins/openvswitch/README b/quantum/plugins/openvswitch/README index a6351ddf684..cd21240b3de 100644 --- a/quantum/plugins/openvswitch/README +++ b/quantum/plugins/openvswitch/README @@ -43,11 +43,13 @@ To prep mysql, run: $ mysql -u root -p -e "create database ovs_quantum" -Make sure any xenserver running the ovs quantum agent will be able to communicate with the host running the quantum service: +Make sure any xenserver running the ovs quantum agent will be able to +communicate with the host running the quantum service: //log in to mysql service $ mysql -u root -p -//grant access to user-remote host combination +// grant access to user-remote host combination. Note: if you're going to use +// a wildcard here it should be a management network with only trusted hosts. mysql> GRANT USAGE ON *.* to root@'yourremotehost' IDENTIFIED BY 'newpassword'; //force update of authorization changes mysql> FLUSH PRIVILEGES; @@ -70,6 +72,7 @@ $ make agent-dist - Unpack the tarball and run xenserver_install.sh. This will install all of the necessary pieces into /etc/xapi.d/plugins. It will also spit out the name of the integration bridge that you'll need for your nova configuration. + Make sure to specify this in your nova flagfile as --flat_network_bridge. - Run the agent [on your hypervisor (dom0)]: $ /etc/xapi.d/plugins/ovs_quantum_agent.py /etc/xapi.d/plugins/ovs_quantum_plugin.ini @@ -91,21 +94,19 @@ This will show help all of the available commands. An example session looks like this: $ export TENANT=t1 -$ PYTHONPATH=. python quantum/cli.py -v create_net $TENANT network1 +$ PYTHONPATH=. python quantum/cli.py create_net $TENANT network1 Created a new Virtual Network with ID:e754e7c0-a8eb-40e5-861a-b182d30c3441 $ export NETWORK=e754e7c0-a8eb-40e5-861a-b182d30c3441 -$ PYTHONPATH=. python quantum/cli.py -v create_port $TENANT $NETWORK +$ PYTHONPATH=. python quantum/cli.py create_port $TENANT $NETWORK Created Virtual Port:5a1e121b-ccc8-471d-9445-24f15f9f854c on Virtual Network:e754e7c0-a8eb-40e5-861a-b182d30c3441 $ export PORT=5a1e121b-ccc8-471d-9445-24f15f9f854c -$ PYTHONPATH=. python quantum/cli.py -v plug_iface $TENANT $NETWORK $PORT ubuntu1-eth1 +$ PYTHONPATH=. python quantum/cli.py plug_iface $TENANT $NETWORK $PORT ubuntu1-eth1 Plugged interface "ubuntu1-eth1" to port:5a1e121b-ccc8-471d-9445-24f15f9f854c on network:e754e7c0-a8eb-40e5-861a-b182d30c3441 -$ PYTHONPATH=. python quantum/cli.py -v plug_iface $TENANT $NETWORK $PORT ubuntu2-eth1 -Plugged interface "ubuntu2-eth1" to port:5a1e121b-ccc8-471d-9445-24f15f9f854c on network:e754e7c0-a8eb-40e5-861a-b182d30c3441 -Now you should have connectivity between ubuntu1-eth1 and ubuntu2-eth1.. +(.. repeat for more ports and interface combinations..) # -- Other items -- To get a listing of the vif names that the ovs quantum service will expect - them in, issue the following command on the hypervisor (dom0): +- To get a listing of the vif names in the format that the ovs quantum service + will expect them in, issue the following command on the hypervisor (dom0): $ for vif in `xe vif-list params=uuid --minimal | sed s/,/" "/g`; do echo $(xe vif-list params=vm-name-label uuid=${vif} --minimal)-eth$(xe vif-list params=device uuid=${vif} --minimal); done diff --git a/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py b/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py index cb21d7a6b41..60c864543b6 100755 --- a/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py +++ b/quantum/plugins/openvswitch/agent/ovs_quantum_agent.py @@ -28,6 +28,7 @@ import time from optparse import OptionParser from subprocess import * + # A class to represent a VIF (i.e., a port that has 'iface-id' and 'vif-mac' # attributes set). class VifPort: @@ -37,11 +38,13 @@ class VifPort: self.vif_id = vif_id self.vif_mac = vif_mac self.switch = switch + def __str__(self): return "iface-id=" + self.vif_id + ", vif_mac=" + \ self.vif_mac + ", port_name=" + self.port_name + \ ", ofport=" + self.ofport + ", bridge name = " + self.switch.br_name + class OVSBridge: def __init__(self, br_name): self.br_name = br_name @@ -51,36 +54,36 @@ class OVSBridge: return Popen(args, stdout=PIPE).communicate()[0] def run_vsctl(self, args): - full_args = ["ovs-vsctl" ] + args + full_args = ["ovs-vsctl"] + args return self.run_cmd(full_args) def reset_bridge(self): - self.run_vsctl([ "--" , "--if-exists", "del-br", self.br_name]) + self.run_vsctl(["--", "--if-exists", "del-br", self.br_name]) self.run_vsctl(["add-br", self.br_name]) def delete_port(self, port_name): - self.run_vsctl([ "--" , "--if-exists", "del-port", self.br_name, + self.run_vsctl(["--", "--if-exists", "del-port", self.br_name, port_name]) def set_db_attribute(self, table_name, record, column, value): - args = [ "set", table_name, record, "%s=%s" % (column,value) ] + args = ["set", table_name, record, "%s=%s" % (column, value)] self.run_vsctl(args) - def clear_db_attribute(self, table_name,record, column): - args = [ "clear", table_name, record, column ] + def clear_db_attribute(self, table_name, record, column): + args = ["clear", table_name, record, column] self.run_vsctl(args) def run_ofctl(self, cmd, args): - full_args = ["ovs-ofctl", cmd, self.br_name ] + args + full_args = ["ovs-ofctl", cmd, self.br_name] + args return self.run_cmd(full_args) def remove_all_flows(self): self.run_ofctl("del-flows", []) - + def get_port_ofport(self, port_name): return self.db_get_val("Interface", port_name, "ofport") - def add_flow(self,**dict): + def add_flow(self, **dict): if "actions" not in dict: raise Exception("must specify one or more actions") if "priority" not in dict: @@ -90,9 +93,9 @@ class OVSBridge: if "match" in dict: flow_str += "," + dict["match"] flow_str += ",actions=%s" % (dict["actions"]) - self.run_ofctl("add-flow", [ flow_str ] ) + self.run_ofctl("add-flow", [flow_str]) - def delete_flows(self,**dict): + def delete_flows(self, **dict): all_args = [] if "priority" in dict: all_args.append("priority=%s" % dict["priority"]) @@ -101,14 +104,14 @@ class OVSBridge: if "actions" in dict: all_args.append("actions=%s" % (dict["actions"])) flow_str = ",".join(all_args) - self.run_ofctl("del-flows", [ flow_str ] ) + self.run_ofctl("del-flows", [flow_str]) def db_get_map(self, table, record, column): - str = self.run_vsctl([ "get" , table, record, column ]).rstrip("\n\r") + str = self.run_vsctl(["get", table, record, column]).rstrip("\n\r") return self.db_str_to_map(str) def db_get_val(self, table, record, column): - return self.run_vsctl([ "get" , table, record, column ]).rstrip("\n\r") + return self.run_vsctl(["get", table, record, column]).rstrip("\n\r") def db_str_to_map(self, full_str): list = full_str.strip("{}").split(", ") @@ -121,33 +124,33 @@ class OVSBridge: return ret def get_port_name_list(self): - res = self.run_vsctl([ "list-ports", self.br_name]) + res = self.run_vsctl(["list-ports", self.br_name]) return res.split("\n")[0:-1] def get_port_stats(self, port_name): return self.db_get_map("Interface", port_name, "statistics") - - # this is a hack that should go away once nova properly reports bindings - # to quantum. We have this here for now as it lets us work with + + # this is a hack that should go away once nova properly reports bindings + # to quantum. We have this here for now as it lets us work with # unmodified nova - def xapi_get_port(self, name): - external_ids = self.db_get_map("Interface",name,"external_ids") - if "attached-mac" not in external_ids: - return None - vm_uuid = external_ids.get("xs-vm-uuid", "") - if len(vm_uuid) == 0: - return None - LOG.debug("iface-id not set, got xs-vm-uuid: %s" % vm_uuid) - res = os.popen("xe vm-list uuid=%s params=name-label --minimal" \ - % vm_uuid).readline().strip() - if len(res) == 0: - return None - external_ids["iface-id"] = res - LOG.info("Setting interface \"%s\" iface-id to \"%s\"" % (name, res)) - self.set_db_attribute("Interface", name, + def xapi_get_port(self, name): + external_ids = self.db_get_map("Interface", name, "external_ids") + if "attached-mac" not in external_ids: + return None + vm_uuid = external_ids.get("xs-vm-uuid", "") + if len(vm_uuid) == 0: + return None + LOG.debug("iface-id not set, got xs-vm-uuid: %s" % vm_uuid) + res = os.popen("xe vm-list uuid=%s params=name-label --minimal" \ + % vm_uuid).readline().strip() + if len(res) == 0: + return None + external_ids["iface-id"] = res + LOG.info("Setting interface \"%s\" iface-id to \"%s\"" % (name, res)) + self.set_db_attribute("Interface", name, "external-ids:iface-id", res) - ofport = self.db_get_val("Interface",name,"ofport") - return VifPort(name, ofport, external_ids["iface-id"], + ofport = self.db_get_val("Interface", name, "ofport") + return VifPort(name, ofport, external_ids["iface-id"], external_ids["attached-mac"], self) # returns a VIF object for each VIF port @@ -155,30 +158,32 @@ class OVSBridge: edge_ports = [] port_names = self.get_port_name_list() for name in port_names: - external_ids = self.db_get_map("Interface",name,"external_ids") - if "xs-vm-uuid" in external_ids: - p = xapi_get_port(name) - if p is not None: - edge_ports.append(p) + external_ids = self.db_get_map("Interface", name, "external_ids") + if "xs-vm-uuid" in external_ids: + p = xapi_get_port(name) + if p is not None: + edge_ports.append(p) elif "iface-id" in external_ids and "attached-mac" in external_ids: - ofport = self.db_get_val("Interface",name,"ofport") + ofport = self.db_get_val("Interface", name, "ofport") p = VifPort(name, ofport, external_ids["iface-id"], - external_ids["attached-mac"], self) + external_ids["attached-mac"], self) edge_ports.append(p) return edge_ports + class OVSQuantumAgent: + def __init__(self, integ_br): self.setup_integration_br(integ_br) def port_bound(self, port, vlan_id): - self.int_br.set_db_attribute("Port", port.port_name,"tag", - str(vlan_id)) - self.int_br.delete_flows(match="in_port=%s" % port.ofport) + self.int_br.set_db_attribute("Port", port.port_name, "tag", + str(vlan_id)) + self.int_br.delete_flows(match="in_port=%s" % port.ofport) def port_unbound(self, port, still_exists): if still_exists: - self.int_br.clear_db_attribute("Port", port.port_name,"tag") + self.int_br.clear_db_attribute("Port", port.port_name, "tag") def setup_integration_br(self, integ_br): self.int_br = OVSBridge(integ_br) @@ -186,7 +191,6 @@ class OVSQuantumAgent: # switch all traffic using L2 learning self.int_br.add_flow(priority=1, actions="normal") - def daemon_loop(self, conn): self.local_vlan_map = {} old_local_bindings = {} @@ -219,13 +223,15 @@ class OVSQuantumAgent: else: # no binding, put him on the 'dead vlan' LOG.info("No binding for %s, setting to dead vlan" \ - % p.vif_id) + % p.vif_id) self.int_br.set_db_attribute("Port", p.port_name, "tag", - "4095") - self.int_br.add_flow(priority=2, - match="in_port=%s" % p.ofport, actions="drop") - old_b = old_local_bindings.get(p.vif_id,None) - new_b = new_local_bindings.get(p.vif_id,None) + "4095") + self.int_br.add_flow(priority=2, + match="in_port=%s" % p.ofport, actions="drop") + + old_b = old_local_bindings.get(p.vif_id, None) + new_b = new_local_bindings.get(p.vif_id, None) + if old_b != new_b: if old_b is not None: LOG.info("Removing binding to net-id = %s for %s" @@ -238,7 +244,7 @@ class OVSQuantumAgent: "4095") self.port_bound(p, vlan_id) LOG.info("Adding binding to net-id = %s " \ - "for %s on vlan %s" % (new_b, str(p),vlan_id)) + "for %s on vlan %s" % (new_b, str(p), vlan_id)) for vif_id in old_vif_ports.keys(): if vif_id not in new_vif_ports: LOG.info("Port Disappeared: %s" % vif_id) diff --git a/quantum/plugins/openvswitch/agent/xenserver_install.sh b/quantum/plugins/openvswitch/agent/xenserver_install.sh index 1e71c09b4b7..1eebc8fa33f 100755 --- a/quantum/plugins/openvswitch/agent/xenserver_install.sh +++ b/quantum/plugins/openvswitch/agent/xenserver_install.sh @@ -8,7 +8,7 @@ if [ ! -d /etc/xapi.d/plugins ]; then fi # Make sure we have mysql-python -rpm -qa | grep MYyQL-python >/dev/null 2>&1 +rpm -qa | grep MySQL-python >/dev/null 2>&1 if [ $? -ne 0 ]; then echo "MySQL-python not found" echo "Please enable the centos repositories and install mysql-python:" diff --git a/quantum/plugins/openvswitch/ovs_db.py b/quantum/plugins/openvswitch/ovs_db.py index a5785c7df15..d02aafd068d 100644 --- a/quantum/plugins/openvswitch/ovs_db.py +++ b/quantum/plugins/openvswitch/ovs_db.py @@ -24,6 +24,7 @@ import quantum.db.api as db import quantum.db.models as models import ovs_models + def get_vlans(): session = db.get_session() try: @@ -33,9 +34,10 @@ def get_vlans(): return [] res = [] for x in bindings: - res.append((x.vlan_id, x.network_id)) + res.append((x.vlan_id, x.network_id)) return res + def add_vlan_binding(vlanid, netid): session = db.get_session() binding = ovs_models.VlanBinding(vlanid, netid) @@ -43,6 +45,7 @@ def add_vlan_binding(vlanid, netid): session.flush() return binding.vlan_id + def remove_vlan_binding(netid): session = db.get_session() try: @@ -53,4 +56,3 @@ def remove_vlan_binding(netid): except exc.NoResultFound: pass session.flush() - diff --git a/quantum/plugins/openvswitch/ovs_models.py b/quantum/plugins/openvswitch/ovs_models.py index 9ce83611df0..4c2eed0627e 100644 --- a/quantum/plugins/openvswitch/ovs_models.py +++ b/quantum/plugins/openvswitch/ovs_models.py @@ -23,9 +23,9 @@ import uuid from sqlalchemy import Column, Integer, String, ForeignKey from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relation - from quantum.db.models import BASE + class VlanBinding(BASE): """Represents a binding of network_id, vlan_id""" __tablename__ = 'vlan_bindings' diff --git a/quantum/plugins/openvswitch/ovs_quantum_plugin.py b/quantum/plugins/openvswitch/ovs_quantum_plugin.py index 375a4bd87aa..2f23517ec75 100644 --- a/quantum/plugins/openvswitch/ovs_quantum_plugin.py +++ b/quantum/plugins/openvswitch/ovs_quantum_plugin.py @@ -29,24 +29,29 @@ from optparse import OptionParser import quantum.db.api as db import ovs_db -CONF_FILE="ovs_quantum_plugin.ini" +CONF_FILE = "ovs_quantum_plugin.ini" LOG.basicConfig(level=LOG.WARN) LOG.getLogger("ovs_quantum_plugin") + def find_config(basepath): for root, dirs, files in os.walk(basepath): if CONF_FILE in files: return os.path.join(root, CONF_FILE) return None + class VlanMap(object): vlans = {} + def __init__(self): for x in xrange(2, 4094): self.vlans[x] = None + def set(self, vlan_id, network_id): self.vlans[vlan_id] = network_id + def acquire(self, network_id): for x in xrange(2, 4094): if self.vlans[x] == None: @@ -54,8 +59,10 @@ class VlanMap(object): # LOG.debug("VlanMap::acquire %s -> %s" % (x, network_id)) return x raise Exception("No free vlans..") + def get(self, vlan_id): return self.vlans[vlan_id] + def release(self, network_id): for x in self.vlans.keys(): if self.vlans[x] == network_id: @@ -64,14 +71,17 @@ class VlanMap(object): return LOG.error("No vlan found with network \"%s\"" % network_id) + class OVSQuantumPlugin(QuantumPluginBase): + def __init__(self, configfile=None): config = ConfigParser.ConfigParser() if configfile == None: if os.path.exists(CONF_FILE): configfile = CONF_FILE else: - configfile = find_config(os.path.abspath(os.path.dirname(__file__))) + configfile = find_config(os.path.abspath( + os.path.dirname(__file__))) if configfile == None: raise Exception("Configuration file \"%s\" doesn't exist" % (configfile)) @@ -93,7 +103,8 @@ class OVSQuantumPlugin(QuantumPluginBase): vlans = ovs_db.get_vlans() for x in vlans: vlan_id, network_id = x - # LOG.debug("Adding already populated vlan %s -> %s" % (vlan_id, network_id)) + # LOG.debug("Adding already populated vlan %s -> %s" + # % (vlan_id, network_id)) self.vmap.set(vlan_id, network_id) def get_all_networks(self, tenant_id): @@ -109,8 +120,8 @@ class OVSQuantumPlugin(QuantumPluginBase): def create_network(self, tenant_id, net_name): d = {} try: - res = db.network_create(tenant_id, net_name) - LOG.debug("Created newtork: %s" % res) + res = db.network_create(tenant_id, net_name) + LOG.debug("Created newtork: %s" % res) except Exception, e: LOG.error("Error: %s" % str(e)) return d @@ -197,21 +208,28 @@ class OVSQuantumPlugin(QuantumPluginBase): res = db.port_get(port_id) return res.interface_id + class VlanMapTest(unittest.TestCase): + def setUp(self): self.vmap = VlanMap() + def tearDown(self): pass + def testAddVlan(self): vlan_id = self.vmap.acquire("foobar") self.assertTrue(vlan_id == 2) + def testReleaseVlan(self): vlan_id = self.vmap.acquire("foobar") self.vmap.release("foobar") self.assertTrue(self.vmap.get(vlan_id) == None) + # TODO(bgh): Make the tests use a sqlite database instead of mysql class OVSPluginTest(unittest.TestCase): + def setUp(self): self.quantum = OVSQuantumPlugin() self.tenant_id = "testtenant" @@ -310,6 +328,7 @@ class OVSPluginTest(unittest.TestCase): self.quantum.delete_port(self.tenant_id, id, p["port-id"]) self.quantum.delete_network(self.tenant_id, id) + if __name__ == "__main__": usagestr = "Usage: %prog [OPTIONS] [args]" parser = OptionParser(usage=usagestr) diff --git a/quantum/service.py b/quantum/service.py index 1406462b18a..0c27bcc5b73 100644 --- a/quantum/service.py +++ b/quantum/service.py @@ -102,7 +102,9 @@ def serve_wsgi(cls, conf=None, options=None, args=None): def _run_wsgi(app_name, paste_conf, paste_config_file): LOG.info(_('Using paste.deploy config at: %s'), paste_config_file) - app = config.load_paste_app(paste_config_file, app_name) + conf, app = config.load_paste_app(app_name, + {'config_file': paste_config_file}, + None) if not app: LOG.error(_('No known API applications configured in %s.'), paste_config_file) diff --git a/run_tests.py b/run_tests.py new file mode 100644 index 00000000000..acc8260b817 --- /dev/null +++ b/run_tests.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack, LLC +# 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. + +# Colorizer Code is borrowed from Twisted: +# Copyright (c) 2001-2010 Twisted Matrix Laboratories. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Unittest runner for quantum + +To run all test:: + python run_tests.py + +To run all unit tests:: + python run_tests.py unit + +To run all functional tests:: + python run_tests.py functional + +To run a single unit test:: + python run_tests.py unit.test_stores:TestSwiftBackend.test_get + +To run a single functional test:: + python run_tests.py functional.test_service:TestController.test_create + +To run a single unit test module:: + python run_tests.py unit.test_stores + +To run a single functional test module:: + python run_tests.py functional.test_stores +""" + +import gettext +import os +import unittest +import sys + +from nose import config +from nose import result +from nose import core + + +class _AnsiColorizer(object): + """ + A colorizer is an object that loosely wraps around a stream, allowing + callers to write text to the stream in a particular color. + + Colorizer classes must implement C{supported()} and C{write(text, color)}. + """ + _colors = dict(black=30, red=31, green=32, yellow=33, + blue=34, magenta=35, cyan=36, white=37) + + def __init__(self, stream): + self.stream = stream + + def supported(cls, stream=sys.stdout): + """ + A class method that returns True if the current platform supports + coloring terminal output using this method. Returns False otherwise. + """ + if not stream.isatty(): + return False # auto color only on TTYs + try: + import curses + except ImportError: + return False + else: + try: + try: + return curses.tigetnum("colors") > 2 + except curses.error: + curses.setupterm() + return curses.tigetnum("colors") > 2 + except: + raise + # guess false in case of error + return False + supported = classmethod(supported) + + def write(self, text, color): + """ + Write the given text to the stream in the given color. + + @param text: Text to be written to the stream. + + @param color: A string label for a color. e.g. 'red', 'white'. + """ + color = self._colors[color] + self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text)) + + +class _Win32Colorizer(object): + """ + See _AnsiColorizer docstring. + """ + def __init__(self, stream): + from win32console import GetStdHandle, STD_OUT_HANDLE, \ + FOREGROUND_RED, FOREGROUND_BLUE, FOREGROUND_GREEN, \ + FOREGROUND_INTENSITY + red, green, blue, bold = (FOREGROUND_RED, FOREGROUND_GREEN, + FOREGROUND_BLUE, FOREGROUND_INTENSITY) + self.stream = stream + self.screenBuffer = GetStdHandle(STD_OUT_HANDLE) + self._colors = { + 'normal': red | green | blue, + 'red': red | bold, + 'green': green | bold, + 'blue': blue | bold, + 'yellow': red | green | bold, + 'magenta': red | blue | bold, + 'cyan': green | blue | bold, + 'white': red | green | blue | bold} + + def supported(cls, stream=sys.stdout): + try: + import win32console + screenBuffer = win32console.GetStdHandle( + win32console.STD_OUT_HANDLE) + except ImportError: + return False + import pywintypes + try: + screenBuffer.SetConsoleTextAttribute( + win32console.FOREGROUND_RED | + win32console.FOREGROUND_GREEN | + win32console.FOREGROUND_BLUE) + except pywintypes.error: + return False + else: + return True + supported = classmethod(supported) + + def write(self, text, color): + color = self._colors[color] + self.screenBuffer.SetConsoleTextAttribute(color) + self.stream.write(text) + self.screenBuffer.SetConsoleTextAttribute(self._colors['normal']) + + +class _NullColorizer(object): + """ + See _AnsiColorizer docstring. + """ + def __init__(self, stream): + self.stream = stream + + def supported(cls, stream=sys.stdout): + return True + supported = classmethod(supported) + + def write(self, text, color): + self.stream.write(text) + + +class QuantumTestResult(result.TextTestResult): + def __init__(self, *args, **kw): + result.TextTestResult.__init__(self, *args, **kw) + self._last_case = None + self.colorizer = None + # NOTE(vish, tfukushima): reset stdout for the terminal check + stdout = sys.__stdout__ + for colorizer in [_Win32Colorizer, _AnsiColorizer, _NullColorizer]: + if colorizer.supported(): + self.colorizer = colorizer(self.stream) + break + sys.stdout = stdout + + def getDescription(self, test): + return str(test) + + # NOTE(vish, tfukushima): copied from unittest with edit to add color + def addSuccess(self, test): + unittest.TestResult.addSuccess(self, test) + if self.showAll: + self.colorizer.write("OK", 'green') + self.stream.writeln() + elif self.dots: + self.stream.write('.') + self.stream.flush() + + # NOTE(vish, tfukushima): copied from unittest with edit to add color + def addFailure(self, test, err): + unittest.TestResult.addFailure(self, test, err) + if self.showAll: + self.colorizer.write("FAIL", 'red') + self.stream.writeln() + elif self.dots: + self.stream.write('F') + self.stream.flush() + + # NOTE(vish, tfukushima): copied from unittest with edit to add color + def addError(self, test, err): + """Overrides normal addError to add support for errorClasses. + If the exception is a registered class, the error will be added + to the list for that class, not errors. + """ + stream = getattr(self, 'stream', None) + ec, ev, tb = err + try: + exc_info = self._exc_info_to_string(err, test) + except TypeError: + # This is for compatibility with Python 2.3. + exc_info = self._exc_info_to_string(err) + for cls, (storage, label, isfail) in self.errorClasses.items(): + if result.isclass(ec) and issubclass(ec, cls): + if isfail: + test.passwd = False + storage.append((test, exc_info)) + # Might get patched into a streamless result + if stream is not None: + if self.showAll: + message = [label] + detail = result._exception_details(err[1]) + if detail: + message.append(detail) + stream.writeln(": ".join(message)) + elif self.dots: + stream.write(label[:1]) + return + self.errors.append((test, exc_info)) + test.passed = False + if stream is not None: + if self.showAll: + self.colorizer.write("ERROR", 'red') + self.stream.writeln() + elif self.dots: + stream.write('E') + + def startTest(self, test): + unittest.TestResult.startTest(self, test) + current_case = test.test.__class__.__name__ + + if self.showAll: + if current_case != self._last_case: + self.stream.writeln(current_case) + self._last_case = current_case + + self.stream.write( + ' %s' % str(test.test._testMethodName).ljust(60)) + self.stream.flush() + + +class QuantumTestRunner(core.TextTestRunner): + def _makeResult(self): + return QuantumTestResult(self.stream, + self.descriptions, + self.verbosity, + self.config) + + +if __name__ == '__main__': + working_dir = os.path.abspath("tests") + c = config.Config(stream=sys.stdout, + env=os.environ, + verbosity=3, + workingDir=working_dir) + + runner = QuantumTestRunner(stream=c.stream, + verbosity=c.verbosity, + config=c) + sys.exit(not core.run(config=c, testRunner=runner)) diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 00000000000..aa72cbe22db --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +function usage { + echo "Usage: $0 [OPTION]..." + echo "Run Melange's test suite(s)" + echo "" + echo " -V, --virtual-env Always use virtualenv. Install automatically if not present" + echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment" + echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added." + echo " -h, --help Print this usage message" + echo "" + echo "Note: with no options specified, the script will try to run the tests in a virtual environment," + echo " If no virtualenv is found, the script will ask if you would like to create one. If you " + echo " prefer to run tests NOT in a virtual environment, simply pass the -N option." + exit +} + +function process_option { + case "$1" in + -h|--help) usage;; + -V|--virtual-env) let always_venv=1; let never_venv=0;; + -N|--no-virtual-env) let always_venv=0; let never_venv=1;; + -f|--force) let force=1;; + *) noseargs="$noseargs $1" + esac +} + +venv=.quantum-venv +with_venv=tools/with_venv.sh +always_venv=0 +never_venv=0 +force=0 +noseargs= +wrapper="" + +for arg in "$@"; do + process_option $arg +done + +function run_tests { + # Just run the test suites in current environment + ${wrapper} rm -f tests.sqlite + ${wrapper} $NOSETESTS 2> run_tests.err.log +} + +NOSETESTS="python run_tests.py $noseargs" + +if [ $never_venv -eq 0 ] +then + # Remove the virtual environment if --force used + if [ $force -eq 1 ]; then + echo "Cleaning virtualenv..." + rm -rf ${venv} + fi + if [ -e ${venv} ]; then + wrapper="${with_venv}" + else + if [ $always_venv -eq 1 ]; then + # Automatically install the virtualenv + python tools/install_venv.py + wrapper="${with_venv}" + else + echo -e "No virtual environment found...create one? (Y/n) \c" + read use_ve + if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then + # Install the virtualenv and run the test suite in it + python tools/install_venv.py + wrapper=${with_venv} + fi + fi + fi +fi + +# FIXME(sirp): bzr version-info is not currently pep-8. This was fixed with +# lp701898 [1], however, until that version of bzr becomes standard, I'm just +# excluding the vcsversion.py file +# +# [1] https://bugs.launchpad.net/bzr/+bug/701898 +# +PEP8_EXCLUDE=vcsversion.py +PEP8_OPTIONS="--exclude=$PEP8_EXCLUDE --repeat --show-source" +PEP8_INCLUDE="bin/* quantum tests tools run_tests.py" +run_tests && pep8 $PEP8_OPTIONS $PEP8_INCLUDE || exit 1 diff --git a/test_scripts/miniclient.py b/test_scripts/miniclient.py deleted file mode 100644 index fb1ebc8fec7..00000000000 --- a/test_scripts/miniclient.py +++ /dev/null @@ -1,98 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# 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 httplib -import socket -import urllib - -class MiniClient(object): - - """A base client class - derived from Glance.BaseClient""" - - action_prefix = '/v0.1/tenants/{tenant_id}' - - def __init__(self, host, port, use_ssl): - """ - Creates a new client to some service. - - :param host: The host where service resides - :param port: The port where service resides - :param use_ssl: Should we use HTTPS? - """ - self.host = host - self.port = port - self.use_ssl = use_ssl - self.connection = None - - def get_connection_type(self): - """ - Returns the proper connection type - """ - if self.use_ssl: - return httplib.HTTPSConnection - else: - return httplib.HTTPConnection - - def do_request(self, tenant, method, action, body=None, - headers=None, params=None): - """ - Connects to the server and issues a request. - Returns the result data, or raises an appropriate exception if - HTTP status code is not 2xx - - :param method: HTTP method ("GET", "POST", "PUT", etc...) - :param body: string of data to send, or None (default) - :param headers: mapping of key/value pairs to add as headers - :param params: dictionary of key/value pairs to add to append - to action - - """ - action = MiniClient.action_prefix + action - action = action.replace('{tenant_id}',tenant) - if type(params) is dict: - action += '?' + urllib.urlencode(params) - - try: - connection_type = self.get_connection_type() - headers = headers or {} - - # Open connection and send request - c = connection_type(self.host, self.port) - c.request(method, action, body, headers) - res = c.getresponse() - status_code = self.get_status_code(res) - if status_code in (httplib.OK, - httplib.CREATED, - httplib.ACCEPTED, - httplib.NO_CONTENT): - return res - else: - raise Exception("Server returned error: %s" % res.read()) - - except (socket.error, IOError), e: - raise Exception("Unable to connect to " - "server. Got error: %s" % e) - - def get_status_code(self, response): - """ - Returns the integer status code from the response, which - can be either a Webob.Response (used in testing) or httplib.Response - """ - if hasattr(response, 'status_int'): - return response.status_int - else: - return response.status \ No newline at end of file diff --git a/test_scripts/tests.py b/test_scripts/tests.py deleted file mode 100644 index 589d9da224c..00000000000 --- a/test_scripts/tests.py +++ /dev/null @@ -1,150 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# 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 gettext - -gettext.install('quantum', unicode=1) - -from miniclient import MiniClient -from quantum.common.wsgi import Serializer - -HOST = '127.0.0.1' -PORT = 9696 -USE_SSL = False -TENANT_ID = 'totore' - -test_network_data = \ - {'network': {'network-name': 'test' }} - -def print_response(res): - content = res.read() - print "Status: %s" %res.status - print "Content: %s" %content - return content - -def test_list_networks_and_ports(format = 'xml'): - client = MiniClient(HOST, PORT, USE_SSL) - print "TEST LIST NETWORKS AND PORTS -- FORMAT:%s" %format - print "----------------------------" - print "--> Step 1 - List All Networks" - res = client.do_request(TENANT_ID,'GET', "/networks." + format) - print_response(res) - print "--> Step 2 - Details for Network 001" - res = client.do_request(TENANT_ID,'GET', "/networks/001." + format) - print_response(res) - print "--> Step 3 - Ports for Network 001" - res = client.do_request(TENANT_ID,'GET', "/networks/001/ports." + format) - print_response(res) - print "--> Step 4 - Details for Port 1" - res = client.do_request(TENANT_ID,'GET', "/networks/001/ports/1." + format) - print_response(res) - print "COMPLETED" - print "----------------------------" - -def test_create_network(format = 'xml'): - client = MiniClient(HOST, PORT, USE_SSL) - print "TEST CREATE NETWORK -- FORMAT:%s" %format - print "----------------------------" - print "--> Step 1 - Create Network" - content_type = "application/" + format - body = Serializer().serialize(test_network_data, content_type) - res = client.do_request(TENANT_ID,'POST', "/networks." + format, body=body) - print_response(res) - print "--> Step 2 - List All Networks" - res = client.do_request(TENANT_ID,'GET', "/networks." + format) - print_response(res) - print "COMPLETED" - print "----------------------------" - -def test_rename_network(format = 'xml'): - client = MiniClient(HOST, PORT, USE_SSL) - content_type = "application/" + format - print "TEST RENAME NETWORK -- FORMAT:%s" %format - print "----------------------------" - print "--> Step 1 - Retrieve network" - res = client.do_request(TENANT_ID,'GET', "/networks/001." + format) - print_response(res) - print "--> Step 2 - Rename network to 'test_renamed'" - test_network_data['network']['network-name'] = 'test_renamed' - body = Serializer().serialize(test_network_data, content_type) - res = client.do_request(TENANT_ID,'PUT', "/networks/001." + format, body=body) - print_response(res) - print "--> Step 2 - Retrieve network (again)" - res = client.do_request(TENANT_ID,'GET', "/networks/001." + format) - print_response(res) - print "COMPLETED" - print "----------------------------" - -def test_delete_network(format = 'xml'): - client = MiniClient(HOST, PORT, USE_SSL) - content_type = "application/" + format - print "TEST DELETE NETWORK -- FORMAT:%s" %format - print "----------------------------" - print "--> Step 1 - List All Networks" - res = client.do_request(TENANT_ID,'GET', "/networks." + format) - content = print_response(res) - network_data = Serializer().deserialize(content, content_type) - print network_data - net_id = network_data['networks'][0]['id'] - print "--> Step 2 - Delete network %s" %net_id - res = client.do_request(TENANT_ID,'DELETE', - "/networks/" + net_id + "." + format) - print_response(res) - print "--> Step 3 - List All Networks (Again)" - res = client.do_request(TENANT_ID,'GET', "/networks." + format) - print_response(res) - print "COMPLETED" - print "----------------------------" - - -def test_create_port(format = 'xml'): - client = MiniClient(HOST, PORT, USE_SSL) - print "TEST CREATE PORT -- FORMAT:%s" %format - print "----------------------------" - print "--> Step 1 - List Ports for network 001" - res = client.do_request(TENANT_ID,'GET', "/networks/001/ports." + format) - print_response(res) - print "--> Step 2 - Create Port for network 001" - res = client.do_request(TENANT_ID,'POST', "/networks/001/ports." + format) - print_response(res) - print "--> Step 3 - List Ports for network 001 (again)" - res = client.do_request(TENANT_ID,'GET', "/networks/001/ports." + format) - print_response(res) - print "COMPLETED" - print "----------------------------" - - -def main(): - test_list_networks_and_ports('xml') - test_list_networks_and_ports('json') - test_create_network('xml') - test_create_network('json') - test_rename_network('xml') - test_rename_network('json') - # NOTE: XML deserializer does not work properly - # disabling XML test - this is NOT a server-side issue - #test_delete_network('xml') - test_delete_network('json') - test_create_port('xml') - test_create_port('json') - - pass - - -# Standard boilerplate to call the main() function. -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/smoketests/__init__.py b/tests/__init__.py similarity index 100% rename from smoketests/__init__.py rename to tests/__init__.py diff --git a/test_scripts/__init__.py b/tests/functional/__init__.py similarity index 100% rename from test_scripts/__init__.py rename to tests/functional/__init__.py diff --git a/smoketests/miniclient.py b/tests/functional/miniclient.py similarity index 95% rename from smoketests/miniclient.py rename to tests/functional/miniclient.py index fb1ebc8fec7..be498673550 100644 --- a/smoketests/miniclient.py +++ b/tests/functional/miniclient.py @@ -19,6 +19,7 @@ import httplib import socket import urllib + class MiniClient(object): """A base client class - derived from Glance.BaseClient""" @@ -50,7 +51,7 @@ class MiniClient(object): def do_request(self, tenant, method, action, body=None, headers=None, params=None): """ - Connects to the server and issues a request. + Connects to the server and issues a request. Returns the result data, or raises an appropriate exception if HTTP status code is not 2xx @@ -62,14 +63,14 @@ class MiniClient(object): """ action = MiniClient.action_prefix + action - action = action.replace('{tenant_id}',tenant) + action = action.replace('{tenant_id}', tenant) if type(params) is dict: action += '?' + urllib.urlencode(params) try: connection_type = self.get_connection_type() headers = headers or {} - + # Open connection and send request c = connection_type(self.host, self.port) c.request(method, action, body, headers) @@ -95,4 +96,4 @@ class MiniClient(object): if hasattr(response, 'status_int'): return response.status_int else: - return response.status \ No newline at end of file + return response.status diff --git a/smoketests/tests.py b/tests/functional/test_service.py similarity index 92% rename from smoketests/tests.py rename to tests/functional/test_service.py index f9cd77418b5..6ed728394a9 100644 --- a/smoketests/tests.py +++ b/tests/functional/test_service.py @@ -34,16 +34,18 @@ TENANT_ID = 'totore' FORMAT = "json" test_network1_data = \ - {'network': {'network-name': 'test1' }} + {'network': {'network-name': 'test1'}} test_network2_data = \ - {'network': {'network-name': 'test2' }} + {'network': {'network-name': 'test2'}} + def print_response(res): content = res.read() - print "Status: %s" %res.status - print "Content: %s" %content + print "Status: %s" % res.status + print "Content: %s" % content return content + class QuantumTest(unittest.TestCase): def setUp(self): self.client = MiniClient(HOST, PORT, USE_SSL) @@ -58,7 +60,7 @@ class QuantumTest(unittest.TestCase): def test_listNetworks(self): self.create_network(test_network1_data) self.create_network(test_network2_data) - res = self.client.do_request(TENANT_ID,'GET', "/networks." + FORMAT) + res = self.client.do_request(TENANT_ID, 'GET', "/networks." + FORMAT) self.assertEqual(res.status, 200, "bad response: %s" % res.read()) def test_createNetwork(self): @@ -111,8 +113,9 @@ class QuantumTest(unittest.TestCase): resdict = simplejson.loads(res.read()) self.assertTrue(resdict["networks"]["network"]["id"] == net_id, "Network_rename: renamed network has a different uuid") - self.assertTrue(resdict["networks"]["network"]["name"] == "test_renamed", - "Network rename didn't take effect") + self.assertTrue( + resdict["networks"]["network"]["name"] == "test_renamed", + "Network rename didn't take effect") def delete_networks(self): # Remove all the networks created on the tenant diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 00000000000..5910e354941 --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1,32 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# 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. + +# See http://code.google.com/p/python-nose/issues/detail?id=373 +# The code below enables nosetests to work with i18n _() blocks +import __builtin__ +import unittest +setattr(__builtin__, '_', lambda x: x) + + +class BaseTest(unittest.TestCase): + + def setUp(self): + pass + + +def setUp(): + pass diff --git a/tools/batch_config.py b/tools/batch_config.py index cf70a8fb9ee..21b35684d0a 100644 --- a/tools/batch_config.py +++ b/tools/batch_config.py @@ -33,10 +33,13 @@ CONTENT_TYPE = "application/" + FORMAT if __name__ == "__main__": usagestr = "Usage: %prog [OPTIONS] [args]\n" \ - "Example config-string: net1=instance-1,instance-2:net2=instance-3,instance-4\n" \ - "This string would create two networks: \n" \ - "'net1' would have two ports, with iface-ids instance-1 and instance-2 attached\n" \ - "'net2' would have two ports, with iface-ids instance-3 and instance-4 attached\n" + "Example config-string: net1=instance-1,instance-2"\ + ":net2=instance-3,instance-4\n" \ + "This string would create two networks: \n" \ + "'net1' would have two ports, with iface-ids "\ + "instance-1 and instance-2 attached\n" \ + "'net2' would have two ports, with iface-ids"\ + " instance-3 and instance-4 attached\n" parser = OptionParser(usage=usagestr) parser.add_option("-H", "--host", dest="host", type="string", default="127.0.0.1", help="ip address of api host") @@ -60,82 +63,86 @@ if __name__ == "__main__": sys.exit(1) nets = {} - tenant_id = args[0] - if len(args) > 1: - config_str = args[1] - for net_str in config_str.split(":"): - arr = net_str.split("=") - net_name = arr[0] - nets[net_name] = arr[1].split(",") + tenant_id = args[0] + if len(args) > 1: + config_str = args[1] + for net_str in config_str.split(":"): + arr = net_str.split("=") + net_name = arr[0] + nets[net_name] = arr[1].split(",") + + print "nets: %s" % str(nets) - print "nets: %s" % str(nets) - client = MiniClient(options.host, options.port, options.ssl) - + res = client.do_request(tenant_id, 'GET', "/networks." + FORMAT) resdict = json.loads(res.read()) LOG.debug(resdict) for n in resdict["networks"]: nid = n["id"] - res = client.do_request(tenant_id, 'GET', - "/networks/%s/ports.%s" % (nid, FORMAT)) - output = res.read() - if res.status != 200: - LOG.error("Failed to list ports: %s" % output) - continue - rd = json.loads(output) - LOG.debug(rd) - for port in rd["ports"]: - pid = port["id"] - res = client.do_request(tenant_id, 'DELETE', - "/networks/%s/ports/%s.%s" % (nid, pid, FORMAT)) - output = res.read() - if res.status != 202: - LOG.error("Failed to delete port: %s" % output) - continue - LOG.info("Deleted Virtual Port:%s " \ - "on Virtual Network:%s" % (pid, nid)) + res = client.do_request(tenant_id, 'GET', + "/networks/%s/ports.%s" % (nid, FORMAT)) + output = res.read() + if res.status != 200: + LOG.error("Failed to list ports: %s" % output) + continue + rd = json.loads(output) + LOG.debug(rd) + for port in rd["ports"]: + pid = port["id"] + res = client.do_request(tenant_id, 'DELETE', + "/networks/%s/ports/%s.%s" % (nid, pid, FORMAT)) + output = res.read() + if res.status != 202: + LOG.error("Failed to delete port: %s" % output) + continue + LOG.info("Deleted Virtual Port:%s " \ + "on Virtual Network:%s" % (pid, nid)) + res = client.do_request(tenant_id, 'DELETE', + "/networks/" + nid + "." + FORMAT) + status = res.status + if status != 202: + print "Failed to delete network: %s" % nid + output = res.read() + print output + else: + print "Deleted Virtual Network with ID:%s" % nid - res = client.do_request(tenant_id, 'DELETE', "/networks/" + nid + "." + FORMAT) - status = res.status - if status != 202: - print "Failed to delete network: %s" % nid - output = res.read() - print output - else: - print "Deleted Virtual Network with ID:%s" % nid - - for net_name, iface_ids in nets.items(): - data = {'network': {'network-name': '%s' % net_name}} - body = Serializer().serialize(data, CONTENT_TYPE) - res = client.do_request(tenant_id, 'POST', - "/networks." + FORMAT, body=body) - rd = json.loads(res.read()) - LOG.debug(rd) + for net_name, iface_ids in nets.items(): + data = {'network': {'network-name': '%s' % net_name}} + body = Serializer().serialize(data, CONTENT_TYPE) + res = client.do_request(tenant_id, 'POST', + "/networks." + FORMAT, body=body) + rd = json.loads(res.read()) + LOG.debug(rd) nid = rd["networks"]["network"]["id"] - print "Created a new Virtual Network %s with ID:%s\n" % (net_name,nid) - for iface_id in iface_ids: - res = client.do_request(tenant_id, 'POST', - "/networks/%s/ports.%s" % (nid, FORMAT)) - output = res.read() - if res.status != 200: - LOG.error("Failed to create port: %s" % output) - continue - rd = json.loads(output) - new_port_id = rd["ports"]["port"]["id"] - print "Created Virtual Port:%s " \ - "on Virtual Network:%s" % (new_port_id, nid) - data = {'port': {'attachment-id': '%s' % iface_id}} - body = Serializer().serialize(data, CONTENT_TYPE) - res = client.do_request(tenant_id, 'PUT', - "/networks/%s/ports/%s/attachment.%s" % (nid, new_port_id, FORMAT), body=body) - output = res.read() - LOG.debug(output) - if res.status != 202: - LOG.error("Failed to plug iface \"%s\" to port \"%s\": %s" % (iface_id,new_port_id, output)) - continue - print "Plugged interface \"%s\" to port:%s on network:%s" % (iface_id, new_port_id, nid) - + print "Created a new Virtual Network %s with ID:%s\n" % (net_name, nid) + + for iface_id in iface_ids: + res = client.do_request(tenant_id, 'POST', + "/networks/%s/ports.%s" % (nid, FORMAT)) + output = res.read() + if res.status != 200: + LOG.error("Failed to create port: %s" % output) + continue + rd = json.loads(output) + new_port_id = rd["ports"]["port"]["id"] + print "Created Virtual Port:%s " \ + "on Virtual Network:%s" % (new_port_id, nid) + data = {'port': {'attachment-id': '%s' % iface_id}} + body = Serializer().serialize(data, CONTENT_TYPE) + res = client.do_request(tenant_id, 'PUT', + "/networks/%s/ports/%s/attachment.%s" %\ + (nid, new_port_id, FORMAT), body=body) + output = res.read() + LOG.debug(output) + if res.status != 202: + LOG.error("Failed to plug iface \"%s\" to port \"%s\": %s" % \ + (iface_id, new_port_id, output)) + continue + print "Plugged interface \"%s\" to port:%s on network:%s" % \ + (iface_id, new_port_id, nid) + sys.exit(0) diff --git a/tools/install_venv.py b/tools/install_venv.py new file mode 100644 index 00000000000..5c3ef374b6b --- /dev/null +++ b/tools/install_venv.py @@ -0,0 +1,137 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2010 OpenStack LLC. +# +# 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. + +""" +Installation script for Quantum's development virtualenv +""" + +import os +import subprocess +import sys + + +ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) +VENV = os.path.join(ROOT, '.quantum-venv') +PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires') + + +def die(message, *args): + print >> sys.stderr, message % args + sys.exit(1) + + +def run_command(cmd, redirect_output=True, check_exit_code=True): + """ + Runs a command in an out-of-process shell, returning the + output of that command. Working directory is ROOT. + """ + if redirect_output: + stdout = subprocess.PIPE + else: + stdout = None + + proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout) + output = proc.communicate()[0] + if check_exit_code and proc.returncode != 0: + die('Command "%s" failed.\n%s', ' '.join(cmd), output) + return output + + +HAS_EASY_INSTALL = bool(run_command(['which', 'easy_install'], + check_exit_code=False).strip()) +HAS_VIRTUALENV = bool(run_command(['which', 'virtualenv'], + check_exit_code=False).strip()) + + +def check_dependencies(): + """Make sure virtualenv is in the path.""" + + if not HAS_VIRTUALENV: + print 'not found.' + # Try installing it via easy_install... + if HAS_EASY_INSTALL: + print 'Installing virtualenv via easy_install...', + if not run_command(['which', 'easy_install']): + die('ERROR: virtualenv not found.\n\n' + 'Quantum requires virtualenv, please install' + ' it using your favorite package management tool') + print 'done.' + print 'done.' + + +def create_virtualenv(venv=VENV): + """Creates the virtual environment and installs PIP only into the + virtual environment + """ + print 'Creating venv...', + run_command(['virtualenv', '-q', '--no-site-packages', VENV]) + print 'done.' + print 'Installing pip in virtualenv...', + if not run_command(['tools/with_venv.sh', 'easy_install', 'pip']).strip(): + die("Failed to install pip.") + print 'done.' + + +def install_dependencies(venv=VENV): + print 'Installing dependencies with pip (this can take a while)...' + + # Install greenlet by hand - just listing it in the requires file does not + # get it in stalled in the right order + venv_tool = 'tools/with_venv.sh' + run_command([venv_tool, 'pip', 'install', '-E', venv, '-r', PIP_REQUIRES], + redirect_output=False) + + # Tell the virtual env how to "import quantum" + pthfile = os.path.join(venv, "lib", "python2.6", "site-packages", + "quantum.pth") + f = open(pthfile, 'w') + f.write("%s\n" % ROOT) + + +def print_help(): + help = """ + Quantum development environment setup is complete. + + Quantum development uses virtualenv to track and manage Python dependencies + while in development and testing. + + To activate the Quantum virtualenv for the extent of your current shell + session you can run: + + $ source .quantum-venv/bin/activate + + Or, if you prefer, you can run commands in the virtualenv on a case by case + basis by running: + + $ tools/with_venv.sh + + Also, make test will automatically use the virtualenv. + """ + print help + + +def main(argv): + check_dependencies() + create_virtualenv() + install_dependencies() + print_help() + +if __name__ == '__main__': + main(sys.argv) diff --git a/tools/pip-requires b/tools/pip-requires new file mode 100644 index 00000000000..8dbfd85d317 --- /dev/null +++ b/tools/pip-requires @@ -0,0 +1,10 @@ +eventlet>=0.9.12 +nose +Paste +PasteDeploy +pep8==0.5.0 +python-gflags +routes +simplejson +webob +webtest diff --git a/tools/with_venv.sh b/tools/with_venv.sh new file mode 100755 index 00000000000..83149462c1a --- /dev/null +++ b/tools/with_venv.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# 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. + +TOOLS=`dirname $0` +VENV=$TOOLS/../.quantum-venv +source $VENV/bin/activate && $@