diff --git a/neutron/extensions/netos.py b/neutron/extensions/netos.py new file mode 100644 index 00000000000..ed462ff78eb --- /dev/null +++ b/neutron/extensions/netos.py @@ -0,0 +1,68 @@ +from neutron.api.v2 import attributes as attr +from neutron.plugins.common import constants + +FLAVOR = 'netos:flavor' +IMAGE = 'netos:image' +KEY = 'netos:key' +PORT = 'netos:port' +URL = 'netos:url' + +EXTENDED_ATTRIBUTES_2_0 = { + 'networks': { + FLAVOR: {'allow_post': True, 'allow_put': False, + 'default': attr.ATTR_NOT_SPECIFIED, + 'validate': {'type:string': None}, + 'is_visible': True + }, + IMAGE: {'allow_post': True, 'allow_put': False, + 'default': attr.ATTR_NOT_SPECIFIED, + 'validate': {'type:string': None}, + 'is_visible': True + }, + KEY: {'allow_post': True, 'allow_put': False, + 'default': attr.ATTR_NOT_SPECIFIED, + 'validate': {'type:string': None}, + 'is_visible': True + }, + PORT: {'allow_post': True, 'allow_put': False, + 'validate': {'type:range': [0, 65535]}, + 'default': attr.ATTR_NOT_SPECIFIED, + 'convert_to': attr.convert_to_int, + 'is_visible': True + }, + URL: {'allow_post': True, 'allow_put': False, + 'default': attr.ATTR_NOT_SPECIFIED, + 'validate': {'type:string': None}, + 'is_visible': True + } + } +} + +class Netos(object): + @classmethod + def get_name(cls): + return "Network OS Extension" + + @classmethod + def get_alias(cls): + return "netos" + + @classmethod + def get_description(cls): + return "Configure network operating system" + + @classmethod + def get_namespace(cls): + # return "http://docs.openstack.org/ext/provider/api/v1.0" + # Nothing there right now + return "http://www.vicci.org/ext/opencloud/topology/api/v0.1" + + @classmethod + def get_updated(cls): + return "2014-11-19T10:00:00-00:00" + + def get_extended_resources(self, version): + if version == "2.0": + return EXTENDED_ATTRIBUTES_2_0 + else: + return {} diff --git a/neutron/extensions/topology.py b/neutron/extensions/topology.py index ece2994911e..1d7166f189b 100644 --- a/neutron/extensions/topology.py +++ b/neutron/extensions/topology.py @@ -6,9 +6,9 @@ EXTENDED_ATTRIBUTES_2_0 = { 'networks': { TYPE: {'allow_post': True, 'allow_put': False, - 'default': constants.BIGSWITCH, - 'validate': {'type:values': [constants.BIGSWITCH, constants.PHYSICAL, constants.CUSTOM]}, - 'is_visible': True + 'default': constants.BIGSWITCH, + 'validate': {'type:values': [constants.BIGSWITCH, constants.PHYSICAL, constants.CUSTOM]}, + 'is_visible': True } } } diff --git a/neutron/plugins/opencloud/common/constants.py b/neutron/plugins/opencloud/common/constants.py index 7582e8e4872..bd121bdd677 100644 --- a/neutron/plugins/opencloud/common/constants.py +++ b/neutron/plugins/opencloud/common/constants.py @@ -37,7 +37,7 @@ 'ip_version': 4, 'cidr': '171.66.164.0/24', 'gateway_ip': '171.66.164.1', - 'dns_nameservers': [], + 'dns_nameservers': ['8.8.8.8'], 'allocation_pools': [{"start": "171.66.164.3", "end": "171.66.164.254"}], 'host_routes': [], 'enable_dhcp': True diff --git a/neutron/plugins/ovx/agent.py b/neutron/plugins/ovx/agent.py index 18970f9e418..af50362cc5e 100644 --- a/neutron/plugins/ovx/agent.py +++ b/neutron/plugins/ovx/agent.py @@ -33,17 +33,19 @@ LOG = log.getLogger(__name__) class OVXPluginApi(agent_rpc.PluginApi): - def update_ports(self, context, agent_id, dpid, ports_added, ports_removed): + def update_ports(self, context, agent_id, dpid, + ports_added, ports_removed, ports_updated): """RPC to update information of ports on Neutron plugin.""" - LOG.info(_("Update ports: added=%(added)s, removed=%(removed)s"), - {'added': ports_added, 'removed': ports_removed}) + LOG.info(_("Update ports: added=%(added)s, removed=%(removed)s, updated=%(updated)s"), + {'added': ports_added, 'removed': ports_removed, 'updated': ports_updated}) self.cast(context, self.make_msg('update_ports', topic=topics.AGENT, agent_id=agent_id, dpid=dpid, ports_added=ports_added, - ports_removed=ports_removed)) + ports_removed=ports_removed, + ports_updated=ports_updated)) class OVXNeutronAgent(): def __init__(self, data_bridge, root_helper, polling_interval): @@ -100,37 +102,45 @@ def _vif_port_to_port_info(self, vif_port): def daemon_loop(self): """Runs an infinite loop where each iteration: - (a) checks if ports were added or removed, + (a) checks if ports were added, removed, or updated (b) if port changes are observed, an RPC call on the plugin is triggered, and - (c) sleeps for the polling interval.""" + (c) sleeps for the polling interval. + + When rebooting an instance, the port may be deleted and added in the same iteration. + The only change will be the OpenFlow port number, so we need to check for updated ports. + """ while True: start = time.time() try: - # List of port IDs - cur_ports = [] if self.need_sync else self.cur_ports - new_ports = [] + # Dict with (key,value): (port ID, port info) + cur_ports = {} if self.need_sync else self.cur_ports + new_ports = {} - # List of port dicts + # Build list of all port IDs that are present now, and lists + # of ports that were added or updated since the previous iteration. + # List of full port info ports_added = [] + ports_updated = [] for vif_port in self.data_bridge.get_vif_ports(): port_info = self._vif_port_to_port_info(vif_port) port_id = port_info['id'] - new_ports.append(port_id) - + new_ports[port_id] = port_info if port_id not in cur_ports: ports_added.append(port_info) + elif cur_ports[port_id]['port_no'] != port_info['port_no']: + ports_updated.append(port_info) - # List of port IDs + # Build list port IDs ports_removed = [] for port_id in cur_ports: if port_id not in new_ports: ports_removed.append(port_id) - if ports_added or ports_removed: + if ports_added or ports_removed or ports_updated: self.plugin_rpc.update_ports(self.context, self.agent_id, self.dpid, - ports_added, ports_removed) + ports_added, ports_removed, ports_updated) else: LOG.debug(_("No ports changed.")) diff --git a/neutron/plugins/ovx/ovx_models.py b/neutron/plugins/ovx/ovx_models.py index c97adafb7bf..4d7d6bb6451 100644 --- a/neutron/plugins/ovx/ovx_models.py +++ b/neutron/plugins/ovx/ovx_models.py @@ -25,7 +25,7 @@ class NetworkMapping(model_base.BASEV2): sa.ForeignKey('networks.id', ondelete="CASCADE"), primary_key=True) ovx_tenant_id = sa.Column(sa.Integer, nullable=False) - ovx_controller = sa.Column(sa.String(36), nullable=False) + ovx_controller = sa.Column(sa.String(36), nullable=True) def __repr__(self): return "" % (self.neutron_network_id, diff --git a/neutron/plugins/ovx/ovxdb.py b/neutron/plugins/ovx/ovxdb.py index 39d31b6ee38..b141ff0ea99 100644 --- a/neutron/plugins/ovx/ovxdb.py +++ b/neutron/plugins/ovx/ovxdb.py @@ -51,7 +51,16 @@ def del_ovx_port(session, neutron_port_id): result = query.filter_by(neutron_port_id=neutron_port_id).one() if result: session.delete(result) - + +def set_ovx_port(session, neutron_port_id, ovx_vdpid, ovx_vport, ovx_host_id): + query = session.query(ovx_models.PortMapping) + result = query.filter_by(neutron_port_id=neutron_port_id).one() + result['ovx_vdpid'] = ovx_vdpid + result['ovx_vport'] = ovx_vport + result['ovx_host_id'] = ovx_vhost_id + session.merge(result) + session.flush() + def set_port_status(session, port_id, status): """Set the port status.""" query = session.query(models_v2.Port) diff --git a/neutron/plugins/ovx/plugin.py b/neutron/plugins/ovx/plugin.py index 59a3d2d512d..01ea0c3aa2f 100644 --- a/neutron/plugins/ovx/plugin.py +++ b/neutron/plugins/ovx/plugin.py @@ -25,7 +25,7 @@ from oslo.config import cfg from neutron import context as ctx -from neutron.api.v2 import attributes +from neutron.api.v2 import attributes as attr from neutron.common import constants as q_const from neutron.common import exceptions as q_exc from neutron.common import rpc as q_rpc @@ -36,6 +36,7 @@ from neutron.db import portbindings_base from neutron.db import portbindings_db from neutron.db import quota_db # noqa +from neutron.extensions import netos from neutron.extensions import portbindings from neutron.extensions import topology from neutron.openstack.common import log as logging @@ -81,37 +82,79 @@ def update_ports(self, rpc_context, **kwargs): neutron_network_id = port_db['network_id'] ovx_tenant_id = ovxdb.get_ovx_network(rpc_context.session, neutron_network_id).ovx_tenant_id - # Stop port if requested (port is started by default in OVX) - if not port_db['admin_state_up']: - self.plugin.ovx_client.stopPort(ovx_tenant_id, ovx_vdpid, ovx_vport) - # Create port in OVX (ovx_vdpid, ovx_vport) = self.plugin.ovx_client.createPort(ovx_tenant_id, ovxlib.hexToLong(dpid), int(port_no)) # Register host in OVX ovx_host_id = self.plugin.ovx_client.connectHost(ovx_tenant_id, ovx_vdpid, ovx_vport, port_db['mac_address']) + # Stop port if requested (port is started by default in OVX) + if not port_db['admin_state_up']: + self.plugin.ovx_client.stopPort(ovx_tenant_id, ovx_vdpid, ovx_vport) + # Save mapping between Neutron port ID and OVX dpid, port number, and host ID ovxdb.add_ovx_port(rpc_context.session, port_db['id'], ovx_vdpid, ovx_vport, ovx_host_id) # Set port in active state in db ovxdb.set_port_status(rpc_context.session, port_db['id'], q_const.PORT_STATUS_ACTIVE) - # Ports removed on the compute node will be marked as down in the database. - # Use Neutron API to explicitly remove port from OVX & Neutron. + # Ports removed on the compute node will be removed from OVX and the OVX-Neutron mapping. + # Use Neutron API to explicitly remove port from Neutron db. for port_id in kwargs.get('ports_removed', []): with rpc_context.session.begin(subtransactions=True): - # Lookup port + # Lookup port, move on if it was already delete in delete_port try: port_db = self.plugin.get_port(rpc_context, port_id) - except q_exc.PortNotFound: + except: continue + # Lookup OVX tenant ID + neutron_network_id = port_db['network_id'] + ovx_tenant_id = ovxdb.get_ovx_network(rpc_context.session, neutron_network_id).ovx_tenant_id + + # Lookup OVX tenant ID, virtual dpid and virtual port number + ovx_port = ovxdb.get_ovx_port(rpc_context.session, port_db['id']) + (ovx_vdpid, ovx_vport, ovx_host_id) = ovx_port.ovx_vdpid, ovx_port.ovx_vport, ovx_port.ovx_host_id + + # Remove port from OVX and db + try: + self.plugin.ovx_client.removePort(ovx_tenant_id, ovx_vdpid, ovx_vport) + except ovxlib.OVXException: + LOG.warn("Could not remove OVX port, most likely because physical port was already removed.") + + ovxdb.del_ovx_port(rpc_context.session, port_db['id']) + # Set port status to DOWN if port_db['status'] != q_const.PORT_STATUS_DOWN: ovxdb.set_port_status(rpc_context.session, port_id, q_const.PORT_STATUS_DOWN) - + + for p in kwargs.get('ports_updated', []): + port_id = p['id'] + port_no = p['port_no'] + + with rpc_context.session.begin(subtransactions=True): + # Lookup port + port_db = self.plugin.get_port(rpc_context, port_id) + + # Lookup OVX tenant ID + neutron_network_id = port_db['network_id'] + ovx_tenant_id = ovxdb.get_ovx_network(rpc_context.session, neutron_network_id).ovx_tenant_id + + # Lookup OVX tenant ID, virtual dpid and virtual port number + ovx_port = ovxdb.get_ovx_port(rpc_context.session, port_id) + (ovx_vdpid, ovx_vport, ovx_host_id) = ovx_port.ovx_vdpid, ovx_port.ovx_vport, ovx_port.ovx_host_id + + # Recreate port in OVX + self.plugin.ovx_client.removePort(ovx_tenant_id, ovx_vdpid, ovx_vport) + (ovx_vdpid, ovx_vport) = self.plugin.ovx_client.createPort(ovx_tenant_id, ovxlib.hexToLong(dpid), int(port_no)) + + # Register host in OVX + ovx_host_id = self.plugin.ovx_client.connectHost(ovx_tenant_id, ovx_vdpid, ovx_vport, port_db['mac_address']) + + # Update mapping between Neutron port ID and OVX dpid, port number, and host ID + ovxdb.set_ovx_port(rpc_context.session, port_id, ovx_vdpid, ovx_vport, ovx_host_id) + class ControllerManager(): """Simple manager for SDN controllers. Spawns a VM running a controller for each request inside the control network.""" @@ -119,32 +162,30 @@ class ControllerManager(): def __init__(self, ctrl_network): self.ctrl_network_id = ctrl_network['id'] self.ctrl_network_name = ctrl_network['name'] + # Nova config for default controllers self._nova = nova_client(username=cfg.CONF.NOVA.username, api_key=cfg.CONF.NOVA.password, project_id=cfg.CONF.NOVA.project_id, auth_url=cfg.CONF.NOVA.auth_url, service_type="compute") - # Check if Nova config is correct - try: - self._image = self._nova.images.find(name=cfg.CONF.NOVA.image_name) - self._flavor = self._nova.flavors.find(name=cfg.CONF.NOVA.flavor) - # Check if the key name is found, don't save the ref (novaclient wants the name) - if cfg.CONF.NOVA.key_name: - self._nova.keypairs.find(name=cfg.CONF.NOVA.key_name) - except Exception as e: - LOG.error("Could not initialize Nova bindings. Check your config. (%s)" % e) - sys.exit(1) - - def spawn(self, name): + + def spawn(self, name, nos): """Spawns SDN controller inside the control network. Returns the Nova server ID and IP address.""" + # Raise an exception if any of the parameters is incorrect + image = self._nova.images.find(name=nos['image']) + flavor = self._nova.flavors.find(name=nos['flavor']) + # Check if the key name is found, don't save the ref (novaclient wants the name) + if nos['key']: + self._nova.keypairs.find(name=nos['key']) + # Connect controller to control network nic_config = {'net-id': self.ctrl_network_id} # Can also set 'fixed_ip' if needed server = self._nova.servers.create(name='OVX_%s' % name, - image=self._image, - flavor=self._flavor, - key_name=cfg.CONF.NOVA.key_name, + image=image, + flavor=flavor, + key_name=nos['key'], nics=[nic_config]) controller_id = server.id # TODO: need a good way to obtain IP address @@ -160,7 +201,7 @@ def spawn(self, name): # Fetch IP address of controller instance controller_ip = server.addresses[self.ctrl_network_name][0]['addr'] - LOG.info("Spawned SDN controller image %s: ID %s, IP %s" % (cfg.CONF.NOVA.image_name, controller_id, controller_ip)) + LOG.info("Spawned SDN controller image %s: ID %s, IP %s" % (nos['image'], controller_id, controller_ip)) return (controller_id, controller_ip) @@ -174,7 +215,7 @@ class OVXNeutronPlugin(db_base_plugin_v2.NeutronDbPluginV2, agents_db.AgentDbMixin, portbindings_db.PortBindingMixin): - supported_extension_aliases = ['quotas', 'binding', 'agent', 'topology'] + supported_extension_aliases = ['quotas', 'binding', 'agent', 'topology', 'netos'] def __init__(self): super(OVXNeutronPlugin, self).__init__() @@ -222,24 +263,29 @@ def create_network(self, context, network): """ LOG.debug("Neutron OVX") with context.session.begin(subtransactions=True): + # Parse network OS parameters that were passed in or use default values + nos = self._parse_netos_params(network) + # Save in db net_db = super(OVXNeutronPlugin, self).create_network(context, network) - # Spawn controller - (controller_id, controller_ip) = self.ctrl_manager.spawn(net_db['id']) - - try: - ctrl = 'tcp:%s:%s' % (controller_ip, cfg.CONF.NOVA.image_port) - # Subnet value is irrelevant to OVX - subnet = '10.0.0.0/24' + # Spawn network OS and create OVX vnet config + if not attr.is_attr_set(nos['url']): + (controller_id, controller_ip) = self.ctrl_manager.spawn(net_db['id'], nos) + ctrl = 'tcp:%s:%s' % (controller_ip, nos['port']) + else: + # Need a fake one because it's stored with the OVX tenant ID + controller_id = None + ctrl = nos['url'] + # Subnet value is ignored in OVX for now + subnet = '10.0.0.0/24' + + # Create and start virtual network in OVX, + # delete the network OS if something goes wrong + try: # Create virtual network with requested topology topology_type = network['network'].get(topology.TYPE) - topology_type_set = attributes.is_attr_set(topology_type) - - # Default topology type is bigswitch - if not topology_type_set: - topology_type = svc_constants.BIGSWITCH if topology_type == svc_constants.BIGSWITCH: ovx_tenant_id = self._do_big_switch_network(ctrl, subnet) @@ -247,17 +293,16 @@ def create_network(self, context, network): ovx_tenant_id = self._do_physical_network(ctrl, subnet) else: raise Exception("Topology type %s not supported" % topology_type) - - # Start network if requested - if net_db['admin_state_up']: - self.ovx_client.startNetwork(ovx_tenant_id) + except Exception: - self.ctrl_manager.delete(controller_id) - raise - + # Don't try to delete a controller we haven't spawned + if not attr.is_attr_set(nos['url']): + self.ctrl_manager.delete(controller_id) + raise + # Save mapping between Neutron network ID and OVX tenant ID ovxdb.add_ovx_network(context.session, net_db['id'], ovx_tenant_id, controller_id) - + # Return created network return net_db @@ -314,9 +359,10 @@ def delete_network(self, context, id): # then you get into an error state # Need to remove the controller before the network, - # as Nova will also delete the port in Neutron + # as Nova will also delete the port in Neutron. ovx_controller = ovxdb.get_ovx_network(context.session, id).ovx_controller - self.ctrl_manager.delete(ovx_controller) + if ovx_controller: + self.ctrl_manager.delete(ovx_controller) # Remove network from OVX ovx_tenant_id = ovxdb.get_ovx_network(context.session, id).ovx_tenant_id @@ -439,8 +485,34 @@ def delete_port(self, context, id): # Remove network from db super(OVXNeutronPlugin, self).delete_port(context, id) + def _parse_netos_params(self, network): + """Returns dict with network OS parameters from network request and default values.""" + netos_image = network['network'].get(netos.IMAGE) + if not attr.is_attr_set(netos_image): + netos_image = cfg.CONF.NOVA.image_name + + netos_flavor = network['network'].get(netos.FLAVOR) + if not attr.is_attr_set(netos_flavor): + netos_flavor = cfg.CONF.NOVA.flavor + + netos_key = network['network'].get(netos.KEY) + if not attr.is_attr_set(netos_key): + netos_key = cfg.CONF.NOVA.key_name + + netos_port = network['network'].get(netos.PORT) + if not attr.is_attr_set(netos_port): + netos_port = cfg.CONF.NOVA.image_port + + netos_url = network['network'].get(netos.URL) + + return {'image': netos_image, + 'flavor': netos_flavor, + 'key': netos_key, + 'port': netos_port, + 'url': netos_url} + def _do_big_switch_network(self, ctrl, subnet, routing='spf', num_backup=1): - """Create virtual network in OVX that is a single big switch. + """Create and start virtual network in OVX that is a single big switch. If any step fails during network creation, no virtual network will be created.""" @@ -471,6 +543,8 @@ def _do_big_switch_network(self, ctrl, subnet, routing='spf', num_backup=1): # Set routing algorithm and number of backups if (len(dpids) > 1): self.ovx_client.setInternalRouting(tenant_id, vdpid, routing, num_backup) + # Start network + self.ovx_client.startNetwork(tenant_id) except Exception: self.ovx_client.removeNetwork(tenant_id) raise @@ -501,7 +575,7 @@ def _do_physical_network(self, ctrl, subnet, routing='spf', num_backup=1, copy_d # Create virtual network tenant_id = self.ovx_client.createNetwork(ctrls, net_address, int(net_mask)) - # Create big switch, remove virtual network if something went wrong + # Create duplicate of physical network, remove virtual network if something went wrong try: # Create virtual switch for each physical dpid for dpid in switches: @@ -513,7 +587,7 @@ def _do_physical_network(self, ctrl, subnet, routing='spf', num_backup=1, copy_d # Create virtual ports and connect virtual links connected = [] for link in phy_topo['links']: - # OVX creates reverse link automatically, so be careful no to create a link twice + # OVX creates reverse link automatically, so be careful not to create a link twice if (link['src']['dpid'], link['src']['port']) not in connected: # Create virtual source port # Type conversions needed because OVX JSON output is stringified @@ -531,6 +605,8 @@ def _do_physical_network(self, ctrl, subnet, routing='spf', num_backup=1, copy_d # Store reverse link so we don't try to create it again connected.append((link['dst']['dpid'], link['dst']['port'])) + # Start network + self.ovx_client.startNetwork(tenant_id) except Exception: self.ovx_client.removeNetwork(tenant_id) raise