
From: Aline Manera <alinefm@br.ibm.com> To avoid duplicating code in model and mockmodel, the code to interface resource was added to model_/interfaces.py. And the code related to network resource was added to model_/networks.py and the specific code for each backend (libvirt or mock) was added to model_/libvirtbackend.py and model_/mockbackend.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model.py | 2 +- src/kimchi/model_/interfaces.py | 48 ++++++++++++ src/kimchi/model_/libvirtbackend.py | 139 +++++++++++++++++++++++++++++++++++ src/kimchi/model_/mockbackend.py | 31 ++++++++ src/kimchi/model_/networks.py | 115 +++++++++++++++++++++++++++++ src/kimchi/networkxml.py | 6 +- 6 files changed, 337 insertions(+), 4 deletions(-) create mode 100644 src/kimchi/model_/interfaces.py create mode 100644 src/kimchi/model_/networks.py diff --git a/src/kimchi/model.py b/src/kimchi/model.py index 027d6d5..7b0eafc 100644 --- a/src/kimchi/model.py +++ b/src/kimchi/model.py @@ -818,7 +818,7 @@ class Model(object): ip.ip = ip.ip + 1 dhcp_start = str(ip.ip + ip.numhosts / 2) dhcp_end = str(ip.ip + ip.numhosts - 2) - params.update({'net': str(ip), + params.update({'subnet': str(ip), 'dhcp': {'range': {'start': dhcp_start, 'end': dhcp_end}}}) diff --git a/src/kimchi/model_/interfaces.py b/src/kimchi/model_/interfaces.py new file mode 100644 index 0000000..7b1d156 --- /dev/null +++ b/src/kimchi/model_/interfaces.py @@ -0,0 +1,48 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Adam Litke <agl@linux.vnet.ibm.com> +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +from kimchi import netinfo + +class Interfaces(object): + def __init__(self, backend): + self.backend = backend + + def get_list(self): + return list(set(netinfo.all_favored_interfaces()) - + set(self.get_used_ifaces())) + + def get_used_ifaces(self): + net_names = self.backend.get_networks() + interfaces = [] + for name in net_names: + net_dict = self.backend.get_network_by_name(name) + net_dict['interface'] and interfaces.append(net_dict['interface']) + + return interfaces + +class Interface(object): + def lookup(self, name): + try: + return netinfo.get_interface_info(name) + except ValueError, e: + raise NotFoundError(e) diff --git a/src/kimchi/model_/libvirtbackend.py b/src/kimchi/model_/libvirtbackend.py index ee263c6..630204b 100644 --- a/src/kimchi/model_/libvirtbackend.py +++ b/src/kimchi/model_/libvirtbackend.py @@ -24,6 +24,7 @@ import cherrypy import fnmatch import glob +import ipaddr import logging import os import platform @@ -36,6 +37,9 @@ from cherrypy.process.plugins import BackgroundTask from collections import defaultdict from kimchi import config +from kimchi import network +from kimchi import netinfo +from kimchi import networkxml from kimchi import xmlutils from kimchi.asynctask import AsyncTask from kimchi.featuretests import FeatureTests @@ -491,3 +495,138 @@ class LibvirtBackend(object): volume.delete(0) except libvirt.libvirtError as e: raise OperationFailed(e.get_error_message()) + + def create_network(self, params): + connection = params["connection"] + # set forward mode, isolated do not need forward + if connection != 'isolated': + params['forward'] = {'mode': connection} + + if connection == 'bridge': + if netinfo.is_bridge(iface): + params['bridge'] = iface + elif netinfo.is_bare_nic(iface) or netinfo.is_bonding(iface): + if params.get('vlan_id') is None: + params['forward']['dev'] = iface + else: + params['bridge'] = \ + self._create_vlan_tagged_bridge(str(iface), + str(params['vlan_id'])) + + xml = networkxml.to_network_xml(**params) + try: + conn = self.conn.get() + network = conn.networkDefineXML(xml) + network.setAutostart(True) + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def _create_vlan_tagged_bridge(self, interface, vlan_id): + br_name = '-'.join(('kimchi', interface, vlan_id)) + br_xml = networkxml.create_vlan_tagged_bridge_xml(br_name, interface, + vlan_id) + conn = self.conn.get() + conn.changeBegin() + try: + vlan_tagged_br = conn.interfaceDefineXML(br_xml) + vlan_tagged_br.create() + except libvirt.libvirtError as e: + conn.changeRollback() + raise OperationFailed(e.message) + else: + conn.changeCommit() + return br_name + + def get_networks(self): + conn = self.conn.get() + return conn.listNetworks() + conn.listDefinedNetworks() + + def get_network_by_name(self, name): + conn = self.conn.get() + net = conn.networkLookupByName(name) + xml = net.XMLDesc(0) + net_dict = self._get_network_from_xml(xml) + subnet = net_dict['subnet'] + dhcp = net_dict['dhcp'] + forward = net_dict['forward'] + interface = net_dict['bridge'] + + connection = forward['mode'] or "isolated" + # FIXME, if we want to support other forward mode well. + if connection == 'bridge': + # macvtap bridge + interface = interface or forward['interface'][0] + # exposing the network on linux bridge or macvtap interface + interface_subnet = network.get_dev_netaddr(interface) + subnet = subnet if subnet else interface_subnet + + # libvirt use format 192.168.0.1/24, standard should be 192.168.0.0/24 + # http://www.ovirt.org/File:Issue3.png + if subnet: + subnet = ipaddr.IPNetwork(subnet) + subnet = "%s/%s" % (subnet.network, subnet.prefixlen) + + return {'connection': connection, + 'interface': interface, + 'subnet': subnet, + 'dhcp': dhcp, + 'vms': self._get_vms_attach_to_network(name), + 'autostart': net.autostart() == 1, + 'state': net.isActive() and "active" or "inactive"} + + def _get_network_from_xml(self, xml): + address = xmlutils.xpath_get_text(xml, "/network/ip/@address") + address = address and address[0] or '' + netmask = xmlutils.xpath_get_text(xml, "/network/ip/@netmask") + netmask = netmask and netmask[0] or '' + net = address and netmask and "/".join([address, netmask]) or '' + + dhcp_start = xmlutils.xpath_get_text(xml, + "/network/ip/dhcp/range/@start") + dhcp_start = dhcp_start and dhcp_start[0] or '' + dhcp_end = xmlutils.xpath_get_text(xml, "/network/ip/dhcp/range/@end") + dhcp_end = dhcp_end and dhcp_end[0] or '' + dhcp = {'start': dhcp_start, 'end': dhcp_end} + + forward_mode = xmlutils.xpath_get_text(xml, "/network/forward/@mode") + forward_mode = forward_mode and forward_mode[0] or '' + forward_if = xmlutils.xpath_get_text(xml, + "/network/forward/interface/@dev") + forward_pf = xmlutils.xpath_get_text(xml, "/network/forward/pf/@dev") + bridge = xmlutils.xpath_get_text(xml, "/network/bridge/@name") + bridge = bridge and bridge[0] or '' + return {'subnet': net, 'dhcp': dhcp, 'bridge': bridge, + 'forward': {'mode': forward_mode, 'interface': forward_if, + 'pf': forward_pf}} + + def _get_vms_attach_to_network(self, network): + return [] + + def activate_network(self, name): + conn = self.conn.get() + net = conn.networkLookupByName(name) + net.create() + + def deactivate_network(self, name): + conn = self.conn.get() + net = conn.networkLookupByName(name) + net.destroy() + + def delete_network(self, name): + conn = self.conn.get() + net = conn.networkLookupByName(name) + self._remove_vlan_tagged_bridge(net) + net.undefine() + + def _remove_vlan_tagged_bridge(self, network): + try: + bridge = network.bridgeName() + except libvirt.libvirtError: + pass + else: + if self._is_vlan_tagged_bridge(bridge): + conn = self.conn.get() + iface = conn.interfaceLookupByName(bridge) + if iface.isActive(): + iface.destroy() + iface.undefine() diff --git a/src/kimchi/model_/mockbackend.py b/src/kimchi/model_/mockbackend.py index 1f93d36..4c0fdf8 100644 --- a/src/kimchi/model_/mockbackend.py +++ b/src/kimchi/model_/mockbackend.py @@ -30,11 +30,16 @@ from kimchi.asynctask import AsyncTask from kimchi.objectstore import ObjectStore class MockBackend(object): + _network_info = {'state': 'inactive', 'autostart': True, 'connection': '', + 'interface': '', 'subnet': '', + 'dhcp': {'start': '', 'stop': ''}} + def __init__(self, objstore_loc=None): self.objstore = ObjectStore(objstore_loc) self.next_taskid = 1 self.host_stats = self._get_host_stats() self._storagepools = {} + self._networks = {} def _get_host_stats(self): memory_stats = {'total': 3934908416L, @@ -150,6 +155,32 @@ class MockBackend(object): def delete_storagevolume(self, pool, name): del self._storagepools[pool]._volumes[name] + def create_network(self, params): + name = params['name'] + info = copy.deepcopy(self._network_info) + info.update(params) + self._networks[name] = info + + def get_networks(self): + return self._networks.keys() + + def get_network_by_name(self, name): + info = self._networks[name] + info['vms'] = self._get_vms_attach_to_a_network(name) + return info + + def _get_vms_attach_to_a_network(self, network): + return [] + + def activate_network(self, name): + self._networks[name]['state'] = 'active' + + def deactivate_network(self, name): + self._networks[name]['state'] = 'inactive' + + def delete_network(self, name): + del self._networks[name] + class MockStoragePool(object): def __init__(self, name): self.name = name diff --git a/src/kimchi/model_/networks.py b/src/kimchi/model_/networks.py new file mode 100644 index 0000000..dfefa81 --- /dev/null +++ b/src/kimchi/model_/networks.py @@ -0,0 +1,115 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Adam Litke <agl@linux.vnet.ibm.com> +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +from kimchi.model import interfaces + +class Networks(object): + def __init__(self, backend): + self.backend = backend + self.ifaces = interfaces.Interfaces(backend) + + def create(self, params): + name = params['name'] + if name in self.get_list(): + raise InvalidOperation("Network %s already exists" % name) + + connection = params['connection'] + # set subnet, bridge network do not need subnet + if connection in ["nat", 'isolated']: + self._set_network_subnet(params) + + # only bridge network need bridge(linux bridge) or interface(macvtap) + if connection == 'bridge': + iface = params.get('interface', None) + if iface is None: + raise MissingParameter("You need to specify interface to create" + " a bridged network.") + + if iface in self.ifaces.get_used_ifaces(): + raise InvalidParameter("interface '%s' already in use." % iface) + + if not (netinfo.is_bridge(iface) or netinfo.is_bare_nic(iface) or + netinfo.is_bonding(iface)): + raise InvalidParameter("The interface should be bare nic, bonding " + "or bridge device.") + + self.backend.create_network(params) + return name + + def get_list(self): + return sorted(self.backend.get_networks()) + + def _set_network_subnet(self, params): + netaddr = params.get('subnet', '') + net_addrs = [] + # lookup a free network address for nat and isolated automatically + if not netaddr: + for net_name in self.get_list(): + net = self.backend.get_network_by_name(net_name) + subnet = net['subnet'] + subnet and net_addrs.append(ipaddr.IPNetwork(subnet)) + + netaddr = network.get_one_free_network(net_addrs) + if not netaddr: + raise OperationFailed("can not find a free IP address for " + "network '%s'" % params['name']) + try: + ip = ipaddr.IPNetwork(netaddr) + except ValueError as e: + raise InvalidParameter("%s" % e) + + if ip.ip == ip.network: + ip.ip = ip.ip + 1 + + dhcp_start = str(ip.ip + ip.numhosts / 2) + dhcp_end = str(ip.ip + ip.numhosts - 2) + + params.update({'subnet': str(ip), + 'dhcp': {'range': {'start': dhcp_start, + 'end': dhcp_end}}}) + +class Network(Networks): + def _network_exist(self, name): + if name not in self.get_list(): + raise NotFoundError("Network '%s' not found.") + + return True + + def lookup(self, name): + if self._network_exist(name): + return self.backend.get_network_by_name(name) + + def activate(self, name): + if self._network_exist(name): + return self.backend.activate_network(name) + + def deactivate(self, name): + if self._network_exist(name): + return self.backend.deactivate_network(name) + + def delete(self, name): + if self.lookup(name)['state'] == 'active': + raise InvalidOperation("Unable to delete the active network %s" % + name) + + return self.backend.delete_network(name) diff --git a/src/kimchi/networkxml.py b/src/kimchi/networkxml.py index 63cb210..212b0ff 100644 --- a/src/kimchi/networkxml.py +++ b/src/kimchi/networkxml.py @@ -59,8 +59,8 @@ def _get_ip_xml(**kwargs): </ip> """ xml = "" - if 'net' in kwargs.keys(): - net = ipaddr.IPNetwork(kwargs['net']) + if 'subnet' in kwargs.keys(): + net = ipaddr.IPNetwork(kwargs['subnet']) address = str(net.ip) netmask = str(net.netmask) dhcp_params = kwargs.get('dhcp', {}) @@ -96,7 +96,7 @@ def to_network_xml(**kwargs): params = {'name': kwargs['name']} # None means is Isolated network, {} means default mode nat forward = kwargs.get('forward', {"mode": None}) - ip = {'net': kwargs['net']} if 'net' in kwargs else {} + ip = {'subnet': kwargs['subnet']} if 'subnet' in kwargs else {} ip['dhcp'] = kwargs.get('dhcp', {}) bridge = kwargs.get('bridge') params = {'name': kwargs['name'], -- 1.7.10.4