diff --git a/charms_openstack/devices/__init__.py b/charms_openstack/devices/__init__.py new file mode 100644 index 0000000..9b088de --- /dev/null +++ b/charms_openstack/devices/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2016 Canonical Ltd +# +# 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. diff --git a/charms_openstack/devices/pci.py b/charms_openstack/devices/pci.py new file mode 100644 index 0000000..e4b0755 --- /dev/null +++ b/charms_openstack/devices/pci.py @@ -0,0 +1,474 @@ +import re +import os +import glob +import shlex +import subprocess + +import charmhelpers.core.decorators as decorators +import charmhelpers.core.hookenv as hookenv + + +def format_pci_addr(pci_addr): + """Pad a PCI address eg 0:0:1.1 becomes 0000:00:01.1 + + :param pci_addr: str + :return pci_addr: str + """ + domain, bus, slot_func = pci_addr.split(':') + slot, func = slot_func.split('.') + return '{}:{}:{}.{}'.format(domain.zfill(4), bus.zfill(2), slot.zfill(2), + func) + + +class VPECLIException(Exception): + def __init__(self, code, message): + self.code = code + self.message = message + + +class PCINetDevice(object): + + def __init__(self, pci_address): + """Class representing a PCI device + + :param pci_addr: str PCI address of device + """ + self.pci_address = pci_address + self.update_attributes() + + def update_attributes(self): + """Query the underlying system and update attributes of this device + """ + self.update_modalias_kmod() + self.update_interface_info() + + @property + def loaded_kmod(self): + """Return Kernel module this device is using + + :returns str: Kernel module + """ + cmd = ['lspci', '-ks', self.pci_address] + lspci_output = subprocess.check_output(cmd) + kdrive = None + for line in lspci_output.split('\n'): + if 'Kernel driver' in line: + kdrive = line.split(':')[1].strip() + hookenv.log('Loaded kmod for {} is {}'.format( + self.pci_address, kdrive)) + return kdrive + + def update_modalias_kmod(self): + """Set the default kernel module for this device + + If a device is orphaned it has no kernel module loaded to support it + so look up the device in modules.alias and set the kernel module + it needs""" + + cmd = ['lspci', '-ns', self.pci_address] + lspci_output = subprocess.check_output(cmd).split() + vendor_device = lspci_output[2] + vendor, device = vendor_device.split(':') + pci_string = 'pci:v{}d{}'.format(vendor.zfill(8), device.zfill(8)) + kernel_name = self.get_kernel_name() + alias_files = '/lib/modules/{}/modules.alias'.format(kernel_name) + kmod = None + with open(alias_files, 'r') as f: + for line in f.readlines(): + if pci_string in line: + kmod = line.split()[-1] + hookenv.log('module.alias kmod for {} is {}'.format( + self.pci_address, kmod)) + self.modalias_kmod = kmod + + def update_interface_info(self): + """Set the interface name, mac address and state properties of this + object""" + if self.loaded_kmod: + if self.loaded_kmod == 'igb_uio': + return self.update_interface_info_vpe() + else: + return self.update_interface_info_eth() + else: + self.interface_name = None + self.mac_address = None + self.state = 'unbound' + + def get_kernel_name(self): + """Return the kernel release of the running kernel + + :returns str: Kernel release + """ + return subprocess.check_output(['uname', '-r']).strip() + + def pci_rescan(self): + """Rescan of all PCI buses in the system, and + re-discover previously removed devices.""" + rescan_file = '/sys/bus/pci/rescan' + with open(rescan_file, 'w') as f: + f.write('1') + + def bind(self, kmod): + """Write PCI address to the bind file to cause the driver to attempt to + bind to the device found at the PCI address. This is useful for + overriding default bindings.""" + bind_file = '/sys/bus/pci/drivers/{}/bind'.format(kmod) + hookenv.log('Binding {} to {}'.format(self.pci_address, bind_file)) + with open(bind_file, 'w') as f: + f.write(self.pci_address) + self.pci_rescan() + self.update_attributes() + + def unbind(self): + """Write PCI address to the unbind file to cause the driver to attempt + to unbind the device found at at the PCI address.""" + if not self.loaded_kmod: + return + unbind_file = '/sys/bus/pci/drivers/{}/unbind'.format(self.loaded_kmod) + hookenv.log('Unbinding {} from {}'.format( + self.pci_address, unbind_file)) + with open(unbind_file, 'w') as f: + f.write(self.pci_address) + self.pci_rescan() + self.update_attributes() + + def update_interface_info_vpe(self): + """Query VPE CLI to set the interface name, mac address and state + properties of this device""" + vpe_devices = self.get_vpe_interfaces_and_macs() + device_info = {} + for interface in vpe_devices: + if self.pci_address == interface['pci_address']: + device_info['interface'] = interface['interface'] + device_info['macAddress'] = interface['macAddress'] + if device_info: + self.interface_name = device_info['interface'] + self.mac_address = device_info['macAddress'] + self.state = 'vpebound' + else: + self.interface_name = None + self.mac_address = None + self.state = None + + @decorators.retry_on_exception(5, base_delay=10, + exc_type=subprocess.CalledProcessError) + def get_vpe_cli_out(self): + """Query VPE CLI and dump interface information + + :returns str: confd_cli output""" + echo_cmd = [ + 'echo', '-e', 'show interfaces-state interface phys-address\nexit'] + cli_cmd = ['/opt/cisco/vpe/bin/confd_cli', '-N', '-C', '-u', 'system'] + echo = subprocess.Popen(echo_cmd, stdout=subprocess.PIPE) + cli_output = subprocess.check_output(cli_cmd, stdin=echo.stdout) + echo.wait() + echo.terminate + hookenv.log('confd_cli: ' + cli_output) + return cli_output + + def get_vpe_interfaces_and_macs(self): + """Parse output from VPE CLI and retrun list of interface data dicts + + :returns list: list of dicts of interface data + eg [ + { + 'interface': 'TenGigabitEthernet6/0/0', + 'macAddress': '84:b8:02:2a:5f:c3', + 'pci_address': '0000:06:00.0' + }, + { + 'interface': 'TenGigabitEthernet7/0/0', + 'macAddress': '84:b8:02:2a:5f:c4', + 'pci_address': '0000:07:00.0' + }, + ] + """ + cli_output = self.get_vpe_cli_out() + vpe_devs = [] + if 'local0' not in cli_output: + msg = ('local0 missing from confd_cli output, assuming things ' + 'went wrong') + raise VPECLIException(1, msg) + for line in cli_output.split('\n'): + if re.search(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', line, re.I): + interface, mac = line.split() + pci_addr = self.extract_pci_addr_from_vpe_interface(interface) + vpe_devs.append({ + 'interface': interface, + 'macAddress': mac, + 'pci_address': pci_addr, + }) + return vpe_devs + + def extract_pci_addr_from_vpe_interface(self, nic): + """Convert a str from nic postfix format to padded format + + :returns list: list of dicts of interface data + + eg 6/1/2 -> 0000:06:01.2""" + hookenv.log('Extracting pci address from {}'.format(nic)) + addr = re.sub(r'^.*Ethernet', '', nic, re.IGNORECASE) + bus, slot, func = addr.split('/') + domain = '0000' + pci_addr = format_pci_addr( + '{}:{}:{}.{}'.format(domain, bus, slot, func)) + hookenv.log('pci address for {} is {}'.format(nic, pci_addr)) + return pci_addr + + def update_interface_info_eth(self): + """Set the interface name, mac address and state + properties of this device if device is in sys fs""" + net_devices = self.get_sysnet_interfaces_and_macs() + for interface in net_devices: + if self.pci_address == interface['pci_address']: + self.interface_name = interface['interface'] + self.mac_address = interface['macAddress'] + self.state = interface['state'] + + def get_sysnet_interfaces_and_macs(self): + """Query sys fs and retrun list of interface data dicts + eg [ + { + 'interface': 'eth2', + 'macAddress': 'a8:9d:21:cf:93:fc', + 'pci_address': '0000:10:00.0', + 'state': 'up' + }, + { + 'interface': 'eth3', + 'macAddress': 'a8:9d:21:cf:93:fd', + 'pci_address': '0000:10:00.1', + 'state': 'down' + } + ] + """ + net_devs = [] + for sdir in glob.glob('/sys/class/net/*'): + sym_link = sdir + "/device" + if os.path.islink(sym_link): + fq_path = os.path.realpath(sym_link) + path = fq_path.split('/') + if 'virtio' in path[-1]: + pci_address = path[-2] + else: + pci_address = path[-1] + net_devs.append({ + 'interface': self.get_sysnet_interface(sdir), + 'macAddress': self.get_sysnet_mac(sdir), + 'pci_address': pci_address, + 'state': self.get_sysnet_device_state(sdir), + }) + return net_devs + + def get_sysnet_mac(self, sysdir): + """Extract MAC address from sys device file + + :returns str: mac address""" + mac_addr_file = sysdir + '/address' + with open(mac_addr_file, 'r') as f: + read_data = f.read() + mac = read_data.strip() + hookenv.log('mac from {} is {}'.format(mac_addr_file, mac)) + return mac + + def get_sysnet_device_state(self, sysdir): + """Extract device state from sys device file + + :returns str: device state""" + state_file = sysdir + '/operstate' + with open(state_file, 'r') as f: + read_data = f.read() + state = read_data.strip() + hookenv.log('state from {} is {}'.format(state_file, state)) + return state + + def get_sysnet_interface(self, sysdir): + """Extract device file from FQ path + + :returns str: interface name""" + return sysdir.split('/')[-1] + + +class PCINetDevices(object): + """PCINetDevices represents a collection of PCI Network devices on the + running system""" + + def __init__(self): + """Initialise a collection of PCINetDevice""" + pci_addresses = self.get_pci_ethernet_addresses() + self.pci_devices = [PCINetDevice(dev) for dev in pci_addresses] + + def get_pci_ethernet_addresses(self): + """Query lspci to retrieve a list of PCI address for devices of type + 'Ethernet controller' + + :returns list: List of PCI addresses of Ethernet controllers""" + cmd = ['lspci', '-m', '-D'] + lspci_output = subprocess.check_output(cmd) + pci_addresses = [] + for line in lspci_output.split('\n'): + columns = shlex.split(line) + if len(columns) > 1 and columns[1] == 'Ethernet controller': + pci_address = columns[0] + pci_addresses.append(format_pci_addr(pci_address)) + return pci_addresses + + def update_devices(self): + """Update attributes of each device in collection""" + for pcidev in self.pci_devices: + pcidev.update_attributes() + + def get_macs(self): + """MAC addresses of all devices in collection + + :returns list: List of MAC addresses""" + macs = [] + for pcidev in self.pci_devices: + if pcidev.mac_address: + macs.append(pcidev.mac_address) + return macs + + def get_device_from_mac(self, mac): + """Given a MAC address return the corresponding PCINetDevice + + :returns PCINetDevice""" + for pcidev in self.pci_devices: + if pcidev.mac_address == mac: + return pcidev + + def get_device_from_pci_address(self, pci_addr): + """Given a PCI address return the corresponding PCINetDevice + + :returns PCINetDevice""" + for pcidev in self.pci_devices: + if pcidev.pci_address == pci_addr: + return pcidev + + def rebind_orphans(self): + """Unbind orphaned devices from the kernel module they are currently + using and then bind it with its default kernel module""" + self.unbind_orphans() + self.bind_orphans() + + def unbind_orphans(self): + """Unbind orphaned devices from the kernel module they are currently + using""" + for orphan in self.get_orphans(): + orphan.unbind() + self.update_devices() + + def bind_orphans(self): + """Bind orphans with their default kernel module""" + for orphan in self.get_orphans(): + orphan.bind(orphan.modalias_kmod) + self.update_devices() + + def get_orphans(self): + """An 'orphan' is a device which is not fully setup. It may not be + associated with a kernel module or may lay a name or MAC address. + + :returns list: List of PCINetDevice""" + orphans = [] + for pcidev in self.pci_devices: + if not pcidev.loaded_kmod or pcidev.loaded_kmod == 'igb_uio': + if not pcidev.interface_name and not pcidev.mac_address: + orphans.append(pcidev) + return orphans + + +class PCIInfo(object): + + def __init__(self): + """Inspect the charm config option 'mac-network-map' against the MAC + addresses on the running system. + + Attributes: + user_requested_config dict Dictionary of MAC addresses and the + networks they are associated with. + local_macs list MAC addresses on local machine + pci_addresses list PCI Addresses of network devices on + local machine + vpe_dev_string str String containing PCI addresse in + format used by vpe.conf + local_mac_nets dict Dictionary of list of dicts with + interface and netork information + keyed on MAC address eg + { + 'mac1': [{'interface': 'eth0', 'net': 'net1'}, + {'interface': 'eth0', 'net': 'net2'}], + 'mac2': [{'interface': 'eth1', 'net': 'net1'}],} + """ + self.user_requested_config = self.get_user_requested_config() + net_devices = PCINetDevices() + self.local_macs = net_devices.get_macs() + self.pci_addresses = [] + self.local_mac_nets = {} + for mac in self.user_requested_config.keys(): + hookenv.log('Checking if {} is on this host'.format(mac)) + if mac in self.local_macs: + hookenv.log('{} is on this host'.format(mac)) + device = net_devices.get_device_from_mac(mac) + hookenv.log('{} is {} and is currently {}'.format(mac, + device.pci_address, device.interface_name)) + if device.state == 'up': + hookenv.log('Refusing to add {} to device list as it is ' + '{}'.format(device.pci_address, device.state)) + else: + self.pci_addresses.append(device.pci_address) + self.local_mac_nets[mac] = [] + for conf in self.user_requested_config[mac]: + self.local_mac_nets[mac].append({ + 'net': conf.get('net'), + 'interface': device.interface_name, + }) + if self.pci_addresses: + self.pci_addresses.sort() + self.vpe_dev_string = 'dev ' + ' dev '.join(self.pci_addresses) + else: + self.vpe_dev_string = 'no-pci' + hookenv.log('vpe_dev_string {}'.format(self.vpe_dev_string)) + + def parse_mmap_entry(self, conf): + """Extract mac and net pairs from list in the form + ['mac=mac1', 'net=net1'] + + :returns tuple: (mac, net) + """ + entry = {a.split('=')[0]: a.split('=')[1] for a in conf} + return entry['mac'], entry['net'] + + def get_user_requested_config(self): + ''' Parse the user requested config str + mac=;net= and return a dict keyed on mac address + + :returns dict: Dictionary of MAC addresses and the networks they are + associated with. eg + mac-network-map set to 'mac=mac1;net=net1 + mac=mac1;net=net2 + mac=mac2;net=net1' + returns: + { + 'mac1': [{'net': 'net1'}, {'net': 'net2'}], + 'mac2': [{'net': 'net1'}]} + } + ''' + mac_net_config = {} + mac_map = hookenv.config('mac-network-map') + if mac_map: + for conf_group in mac_map.split(): + try: + mac, net = self.parse_mmap_entry(conf_group.split(';')) + # Ignore bad config entries + except IndexError: + hookenv.log('Ignoring bad config entry {} in' + 'mac-network-map'.format(conf_group)) + continue + except KeyError: + hookenv.log('Ignoring bad config entry {} in' + 'mac-network-map'.format(conf_group)) + continue + try: + mac_net_config[mac].append({'net': net}) + except KeyError: + mac_net_config[mac] = [{'net': net}] + return mac_net_config diff --git a/charms_openstack/sdn/__init__.py b/charms_openstack/sdn/__init__.py new file mode 100644 index 0000000..9b088de --- /dev/null +++ b/charms_openstack/sdn/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2016 Canonical Ltd +# +# 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. diff --git a/charms_openstack/sdn/odl.py b/charms_openstack/sdn/odl.py new file mode 100644 index 0000000..56a0abb --- /dev/null +++ b/charms_openstack/sdn/odl.py @@ -0,0 +1,273 @@ +'''ODL Controller API integration''' +import requests +from jinja2 import Environment, FileSystemLoader +import charmhelpers.core.hookenv as hookenv +from charmhelpers.core.decorators import retry_on_exception + +TEMPLATE_DIR = 'charms_openstack/sdn/templates' + + +class ODLInteractionFatalError(Exception): + ''' Generic exception for failures in interaction with ODL ''' + pass + + +class ODLConfig(requests.Session): + """Class used for interacting with an ODL controller""" + + def __init__(self, username, password, host, port='8181'): + """Setup attributes for contacting ODLs http API""" + super(ODLConfig, self).__init__() + self.mount("http://", requests.adapters.HTTPAdapter(max_retries=5)) + self.base_url = 'http://{}:{}'.format(host, port) + self.auth = (username, password) + self.proxies = {} + self.timeout = 10 + self.conf_url = self.base_url + '/restconf/config' + self.oper_url = self.base_url + '/restconf/operational' + self.netmap_url = self.conf_url + '/neutron-device-map:neutron_net_map' + self.node_query_url = self.oper_url + '/opendaylight-inventory:nodes/' + yang_mod_path = ('/opendaylight-inventory:nodes/node/' + 'controller-config/yang-ext:mount/config:modules') + self.node_mount_url = self.conf_url + yang_mod_path + + @retry_on_exception(5, base_delay=30, + exc_type=requests.exceptions.ConnectionError) + def contact_odl(self, request_type, url, headers=None, data=None, + whitelist_rcs=None, retry_rcs=None): + """Send request to ODL controller and return the response + + :param request_type: str HTTP Request Methods (GET, POST, DELETE etc) + :param url: str URL to issue request against + :param headers: str HTTP Header to be sent in request + :param data: str Data to be sent in request + :param whitelist_rcs: List List of acceptable return codes. + :param retry_rcs: List List of return codes which should trigger a + retry + + :returns requests.Response: Response from request + """ + response = self.request(request_type, url, data=data, headers=headers) + ok_codes = [requests.codes.ok, requests.codes.no_content] + retry_codes = [requests.codes.service_unavailable] + if whitelist_rcs: + ok_codes.extend(whitelist_rcs) + if retry_rcs: + retry_codes.extend(retry_rcs) + if response.status_code not in ok_codes: + if response.status_code in retry_codes: + msg = "Recieved {} from ODL on {}".format(response.status_code, + url) + raise requests.exceptions.ConnectionError(msg) + else: + msg = "Contact failed status_code={}, {}".format( + response.status_code, url) + raise ODLInteractionFatalError(msg) + return response + + def get_networks(self): + """Query ODL for map of networks and physical hardware + + + :returns dict: neutron_net_map eg: + { + "physicalNetwork": [ + { + "name": "net_d12", + "device": [ + { + "device-name": "C240-M4-6", + "device-type": "vhostuser", + "interface": [ + { + "interface-name": "TenGigabitEthernet6/0/0", + "macAddress": "84:b8:02:2a:5f:c3" + } + ] + } + ] + }, + { + "name": "net_d11", + "device": [ + { + "device-name": "C240-M4-6", + "device-type": "vhostuser", + "interface": [ + { + "interface-name": "TenGigabitEthernet7/0/0", + "macAddress": "84:b8:02:2a:5f:c4" + } + ] + } + ] + } + } + """ + hookenv.log('Querying macs registered with odl') + # No netmap may have been registered yet, so 404 is ok + odl_req = self.contact_odl( + 'GET', self.netmap_url, whitelist_rcs=[requests.codes.not_found]) + if not odl_req: + hookenv.log('neutron_net_map not found in ODL') + return {} + odl_json = odl_req.json() + if odl_json.get('neutron_net_map'): + hookenv.log('neutron_net_map returned by ODL') + return odl_json['neutron_net_map'] + else: + hookenv.log('neutron_net_map NOT returned by ODL') + return {} + + def delete_net_device_entry(self, net, device_name): + """Delete device from network + + :param net: str Netork name that device should be deleted from + :param device_name: str Name of device to be deleted from network + """ + obj_url = self.netmap_url + \ + 'physicalNetwork/{}/device/{}'.format(net, device_name) + self.contact_odl('DELETE', obj_url) + + def get_odl_registered_nodes(self): + """Query ODL to retieve a list of registered servers + + :return List: List of registered servers + """ + hookenv.log('Querying nodes registered with odl') + odl_req = self.contact_odl('GET', self.node_query_url) + odl_json = odl_req.json() + odl_node_ids = [] + if odl_json.get('nodes'): + odl_nodes = odl_json['nodes'].get('node', []) + odl_node_ids = [entry['id'] for entry in odl_nodes] + hookenv.log( + 'Following nodes are registered: ' + ' '.join(odl_node_ids)) + return odl_node_ids + + def odl_register_node(self, device_name, ip): + """Register server with ODL + + :param device_name: str + :param ip: str + """ + hookenv.log('Registering node {} ({}) with ODL'.format( + device_name, ip)) + payload = self.render_node_xml(device_name, ip) + headers = {'Content-Type': 'application/xml'} + # Strictly a client should not retry on recipt of a bad_request (400) + # but ODL return 400s while it is initialising + self.contact_odl( + 'POST', self.node_mount_url, headers=headers, data=payload, + retry_rcs=[requests.codes.bad_request]) + + def odl_register_macs(self, device_name, network, interface, mac, + device_type='vhostuser'): + """Register a device as part of a network + + :param device_name: str Name of server device that has the device + :param interface: str Name of the device + :param mac: str MAC address of the device + :param device_type: str Device type + """ + hookenv.log('Registering {} and {} on {}'.format( + network, interface, mac)) + payload = self.render_mac_xml(device_name, network, interface, mac, + device_type) + headers = {'Content-Type': 'application/json'} + self.contact_odl( + 'POST', self.netmap_url, headers=headers, data=payload) + + def get_macs_networks(self, mac): + """List of networks a MAC address is registered with + + :returns str: List of Network names address is registered with + """ + registered_networks = self.get_networks() + nets = [] + phy_nets = registered_networks.get('physicalNetwork') + if phy_nets: + for network in phy_nets: + for device in network.get('device', []): + for interface in device['interface']: + if interface['macAddress'] == mac: + nets.append(network['name']) + return nets + + def is_device_registered(self, device_name): + """Is device registered in ODL + + :returns boolean: + """ + return device_name in self.get_odl_registered_nodes() + + def is_net_device_registered(self, net_name, device_name, interface_name, + mac, device_type='vhostuser'): + """Is device registered as part of a given network + + :param net_name,: str Name of network + :param device_name: str Name of server device that has the device + :param interface_name: str Name of the device + :param mac: str MAC address of the device + :param device_type: str Device type + + :returns boolean: + """ + networks = self.get_networks() + phy_nets = networks.get('physicalNetwork') + if phy_nets: + for net in phy_nets: + if net_name == net['name']: + for dev in net.get('device', []): + if device_name == dev['device-name'] \ + and dev['device-type'] == device_type: + for interface in dev['interface']: + if (interface_name == + interface['interface-name'] and + mac == interface['macAddress']): + return True + return False + + def render_node_xml(self, device_name, ip, user='admin', password='admin'): + """Return XML for rendering a node + + :param device_name: str Name of server to be registered + :param ip: str IP on server to be registered + :param user: str username for ODL controller to use to talk back to + server + :param password: str password for ODL controller to use to talk back to + server + + :returns str: XML for rendering a node + """ + env = Environment(loader=FileSystemLoader(TEMPLATE_DIR)) + template = env.get_template('odl_node_registration') + node_xml = template.render( + host=device_name, + ip=ip, + username=user, + password=password, + ) + return node_xml + + def render_mac_xml(self, device_name, network, interface, mac, + device_type='vhostuser'): + """Register a device as part of a network + + :param device_name: str Name of server device that has the device + :param network: str Name of the network for device to be registered + against + :param interface: str Name of the device + :param mac: str MAC address of the device + :param device_type: str Device type + """ + env = Environment(loader=FileSystemLoader(TEMPLATE_DIR)) + template = env.get_template('odl_mac_registration') + mac_xml = template.render( + host=device_name, + network=network, + interface=interface, + mac=mac, + device_type=device_type, + ) + return mac_xml diff --git a/charms_openstack/sdn/ovs.py b/charms_openstack/sdn/ovs.py new file mode 100644 index 0000000..5f76ac6 --- /dev/null +++ b/charms_openstack/sdn/ovs.py @@ -0,0 +1,33 @@ +import subprocess + +import charmhelpers.core.hookenv as hookenv + + +def set_manager(connection_url): + """Configure the OVSDB manager for the switch + + :param connection_url: str URL for OVS manager + """ + subprocess.check_call(['ovs-vsctl', 'set-manager', connection_url]) + + +@hookenv.cached +def _get_ovstbl(): + ovstbl = subprocess.check_output(['ovs-vsctl', 'get', + 'Open_vSwitch', '.', + '_uuid']).strip() + return ovstbl + + +def set_config(key, value, table='other_config'): + """Set key value pairs in a table + + :param key: str + :param value: str + :param table: str Table to apply setting to + """ + subprocess.check_call( + ['ovs-vsctl', 'set', + 'Open_vSwitch', _get_ovstbl(), + '{}:{}={}'.format(table, key, value)] + ) diff --git a/charms_openstack/sdn/templates/__init__.py b/charms_openstack/sdn/templates/__init__.py new file mode 100644 index 0000000..9b088de --- /dev/null +++ b/charms_openstack/sdn/templates/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2016 Canonical Ltd +# +# 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. diff --git a/charms_openstack/sdn/templates/odl_mac_registration b/charms_openstack/sdn/templates/odl_mac_registration new file mode 100644 index 0000000..1adf7c1 --- /dev/null +++ b/charms_openstack/sdn/templates/odl_mac_registration @@ -0,0 +1,17 @@ +{ + "neutron-device-map:physicalNetwork": { + "name": "{{ network }}", + "device": [ + { + "interface": [ + { + "macAddress": "{{ mac }}", + "interface-name": "{{ interface }}" + } + ], + "device-name": "{{ host }}", + "device-type": "vhostuser" + } + ] + } +} diff --git a/charms_openstack/sdn/templates/odl_node_registration b/charms_openstack/sdn/templates/odl_node_registration new file mode 100644 index 0000000..19260eb --- /dev/null +++ b/charms_openstack/sdn/templates/odl_node_registration @@ -0,0 +1,29 @@ + + prefix:sal-netconf-connector + {{ host }} +
{{ ip }}
+ 2022 + {{ username }} + {{ password }} + false + + prefix:netty-event-executor + global-event-executor + + + prefix:binding-broker-osgi-registry + binding-osgi-broker + + + prefix:dom-broker-osgi-registry + dom-broker + + + prefix:netconf-client-dispatcher + global-netconf-dispatcher + + + prefix:threadpool + global-netconf-processing-executor + +
diff --git a/test-requirements.txt b/test-requirements.txt index 3498869..f7e9bd4 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,7 @@ +simplejson +requests +httpretty +pep8 flake8>=2.2.4,<=2.4.1 os-testr>=0.4.1 paramiko<2.0 diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py index a20c038..bb7dd6a 100644 --- a/unit_tests/__init__.py +++ b/unit_tests/__init__.py @@ -21,6 +21,7 @@ charmhelpers = mock.MagicMock() sys.modules['apt_pkg'] = apt_pkg sys.modules['charmhelpers'] = charmhelpers sys.modules['charmhelpers.core'] = charmhelpers.core +sys.modules['charmhelpers.core.decorators'] = charmhelpers.core.decorators sys.modules['charmhelpers.core.hookenv'] = charmhelpers.core.hookenv sys.modules['charmhelpers.core.host'] = charmhelpers.core.host sys.modules['charmhelpers.core.templating'] = charmhelpers.core.templating @@ -39,3 +40,23 @@ sys.modules['charmhelpers.cli'] = charmhelpers.cli sys.modules['charmhelpers.contrib.hahelpers'] = charmhelpers.contrib.hahelpers sys.modules['charmhelpers.contrib.hahelpers.cluster'] = ( charmhelpers.contrib.hahelpers.cluster) + + +def _fake_retry(num_retries, base_delay=0, exc_type=Exception): + def _retry_on_exception_inner_1(f): + def _retry_on_exception_inner_2(*args, **kwargs): + return f(*args, **kwargs) + return _retry_on_exception_inner_2 + return _retry_on_exception_inner_1 + +mock.patch( + 'charmhelpers.core.decorators.retry_on_exception', + _fake_retry).start() + + +def _fake_cached(f): + return f + +mock.patch( + 'charmhelpers.core.hookenv.cached', + _fake_cached).start() diff --git a/unit_tests/odl_responses.py b/unit_tests/odl_responses.py new file mode 100644 index 0000000..b48ddd7 --- /dev/null +++ b/unit_tests/odl_responses.py @@ -0,0 +1,74 @@ +NEUTRON_NET_MAP = """ +{ + "neutron_net_map": { + "physicalNetwork": [ + { + "name": "net_d12", + "device": [ + { + "device-name": "C240-M4-6", + "device-type": "vhostuser", + "interface": [ + { + "interface-name": "TenGigabitEthernet6/0/0", + "macAddress": "84:b8:02:2a:5f:c3" + } + ] + } + ] + }, + { + "name": "net_d11", + "device": [ + { + "device-name": "C240-M4-6", + "device-type": "vhostuser", + "interface": [ + { + "interface-name": "TenGigabitEthernet7/0/0", + "macAddress": "84:b8:02:2a:5f:c4" + } + ] + } + ] + }, + { + "name": "net_d10", + "device": [ + { + "device-name": "C240-M4-6", + "device-type": "vhostuser", + "interface": [ + { + "interface-name": "TenGigabitEthernet6/0/0", + "macAddress": "84:b8:02:2a:5f:c3" + } + ] + } + ] + } + ] + } +}""" + +NEUTRON_NET_MAP_EMPTY = """ +{ + "neutron_net_map": { + "physicalNetwork": [ + ] + } +}""" + +ODL_REGISTERED_NODES = """ +{ + "nodes": { + "node": [ + { + "id": "C240-M4-6" + }, + { + "id": "controller-config" + } + ] + } +}""" diff --git a/unit_tests/pci_responses.py b/unit_tests/pci_responses.py new file mode 100644 index 0000000..ae81258 --- /dev/null +++ b/unit_tests/pci_responses.py @@ -0,0 +1,198 @@ +import copy +# flake8: noqa +LSPCI = """ +0000:00:00.0 "Host bridge" "Intel Corporation" "Haswell-E DMI2" -r02 "Intel Corporation" "Device 0000" +0000:00:03.0 "PCI bridge" "Intel Corporation" "Haswell-E PCI Express Root Port 3" -r02 "" "" +0000:00:03.2 "PCI bridge" "Intel Corporation" "Haswell-E PCI Express Root Port 3" -r02 "" "" +0000:00:05.0 "System peripheral" "Intel Corporation" "Haswell-E Address Map, VTd_Misc, System Management" -r02 "" "" +0000:00:05.1 "System peripheral" "Intel Corporation" "Haswell-E Hot Plug" -r02 "" "" +0000:00:05.2 "System peripheral" "Intel Corporation" "Haswell-E RAS, Control Status and Global Errors" -r02 "" "" +0000:00:05.4 "PIC" "Intel Corporation" "Haswell-E I/O Apic" -r02 -p20 "Intel Corporation" "Device 0000" +0000:00:11.0 "Unassigned class [ff00]" "Intel Corporation" "Wellsburg SPSR" -r05 "Intel Corporation" "Device 7270" +0000:00:11.4 "SATA controller" "Intel Corporation" "Wellsburg sSATA Controller [AHCI mode]" -r05 -p01 "Cisco Systems Inc" "Device 0067" +0000:00:16.0 "Communication controller" "Intel Corporation" "Wellsburg MEI Controller #1" -r05 "Intel Corporation" "Device 7270" +0000:00:16.1 "Communication controller" "Intel Corporation" "Wellsburg MEI Controller #2" -r05 "Intel Corporation" "Device 7270" +0000:00:1a.0 "USB controller" "Intel Corporation" "Wellsburg USB Enhanced Host Controller #2" -r05 -p20 "Intel Corporation" "Device 7270" +0000:00:1c.0 "PCI bridge" "Intel Corporation" "Wellsburg PCI Express Root Port #1" -rd5 "" "" +0000:00:1c.3 "PCI bridge" "Intel Corporation" "Wellsburg PCI Express Root Port #4" -rd5 "" "" +0000:00:1c.4 "PCI bridge" "Intel Corporation" "Wellsburg PCI Express Root Port #5" -rd5 "" "" +0000:00:1d.0 "USB controller" "Intel Corporation" "Wellsburg USB Enhanced Host Controller #1" -r05 -p20 "Intel Corporation" "Device 7270" +0000:00:1f.0 "ISA bridge" "Intel Corporation" "Wellsburg LPC Controller" -r05 "Intel Corporation" "Device 7270" +0000:00:1f.2 "SATA controller" "Intel Corporation" "Wellsburg 6-Port SATA Controller [AHCI mode]" -r05 -p01 "Cisco Systems Inc" "Device 0067" +0000:01:00.0 "PCI bridge" "Cisco Systems Inc" "VIC 82 PCIe Upstream Port" -r01 "" "" +0000:02:00.0 "PCI bridge" "Cisco Systems Inc" "VIC PCIe Downstream Port" -ra2 "" "" +0000:02:01.0 "PCI bridge" "Cisco Systems Inc" "VIC PCIe Downstream Port" -ra2 "" "" +0000:03:00.0 "Unclassified device [00ff]" "Cisco Systems Inc" "VIC Management Controller" -ra2 "Cisco Systems Inc" "Device 012e" +0000:04:00.0 "PCI bridge" "Cisco Systems Inc" "VIC PCIe Upstream Port" -ra2 "" "" +0000:05:00.0 "PCI bridge" "Cisco Systems Inc" "VIC PCIe Downstream Port" -ra2 "" "" +0000:05:01.0 "PCI bridge" "Cisco Systems Inc" "VIC PCIe Downstream Port" -ra2 "" "" +0000:05:02.0 "PCI bridge" "Cisco Systems Inc" "VIC PCIe Downstream Port" -ra2 "" "" +0000:05:03.0 "PCI bridge" "Cisco Systems Inc" "VIC PCIe Downstream Port" -ra2 "" "" +0000:06:00.0 "Ethernet controller" "Cisco Systems Inc" "VIC Ethernet NIC" -ra2 "Cisco Systems Inc" "Device 012e" +0000:07:00.0 "Ethernet controller" "Cisco Systems Inc" "VIC Ethernet NIC" -ra2 "Cisco Systems Inc" "Device 012e" +0000:08:00.0 "Fibre Channel" "Cisco Systems Inc" "VIC FCoE HBA" -ra2 "Cisco Systems Inc" "Device 012e" +0000:09:00.0 "Fibre Channel" "Cisco Systems Inc" "VIC FCoE HBA" -ra2 "Cisco Systems Inc" "Device 012e" +0000:0b:00.0 "RAID bus controller" "LSI Logic / Symbios Logic" "MegaRAID SAS-3 3108 [Invader]" -r02 "Cisco Systems Inc" "Device 00db" +0000:0f:00.0 "VGA compatible controller" "Matrox Electronics Systems Ltd." "MGA G200e [Pilot] ServerEngines (SEP1)" -r02 "Cisco Systems Inc" "Device 0101" +0000:10:00.0 "Ethernet controller" "Intel Corporation" "I350 Gigabit Network Connection" -r01 "Cisco Systems Inc" "Device 00d6" +0000:10:00.1 "Ethernet controller" "Intel Corporation" "I350 Gigabit Network Connection" -r01 "Cisco Systems Inc" "Device 00d6" +0000:7f:08.0 "System peripheral" "Intel Corporation" "Haswell-E QPI Link 0" -r02 "Intel Corporation" "Haswell-E QPI Link 0" +""" + +CONFD_CLI = """ +NAME PHYS ADDRESS +-------------------------------------------- +TenGigabitEthernet6/0/0 84:b8:02:2a:5f:c3 +TenGigabitEthernet7/0/0 84:b8:02:2a:5f:c4 +local0 - +""" +CONFD_CLI_ONE_MISSING = """ +NAME PHYS ADDRESS +-------------------------------------------- +TenGigabitEthernet6/0/0 84:b8:02:2a:5f:c3 +local0 - +""" +CONFD_CLI_INVMAC = """ +NAME PHYS ADDRESS +-------------------------------------------- +TenGigabitEthernet6/0/0 no:ta:va:li:dm:ac +TenGigabitEthernet7/0/0 84:b8:02:2a:5f:c4 +local0 - +""" +CONFD_CLI_NODEVS = """ +NAME PHYS ADDRESS +-------------------------------------------- +local0 - +""" +CONFD_CLI_NOLOCAL = """ +NAME PHYS ADDRESS +-------------------------------------------- +""" +SYS_TREE = { + '/sys/class/net/eth2': '../../devices/pci0000:00/0000:00:1c.4/0000:10:00.0/net/eth2', + '/sys/class/net/eth3': '../../devices/pci0000:00/0000:00:1c.4/0000:10:00.1/net/eth3', + '/sys/class/net/juju-br0': '../../devices/virtual/net/juju-br0', + '/sys/class/net/lo': '../../devices/virtual/net/lo', + '/sys/class/net/lxcbr0': '../../devices/virtual/net/lxcbr0', + '/sys/class/net/veth1GVRCF': '../../devices/virtual/net/veth1GVRCF', + '/sys/class/net/veth7AXEUK': '../../devices/virtual/net/veth7AXEUK', + '/sys/class/net/vethACOIJJ': '../../devices/virtual/net/vethACOIJJ', + '/sys/class/net/vethMQ819H': '../../devices/virtual/net/vethMQ819H', + '/sys/class/net/virbr0': '../../devices/virtual/net/virbr0', + '/sys/class/net/virbr0-nic': '../../devices/virtual/net/virbr0-nic', + '/sys/devices/pci0000:00/0000:00:1c.4/0000:10:00.0/net/eth2/device': '../../../0000:10:00.0', + '/sys/devices/pci0000:00/0000:00:1c.4/0000:10:00.1/net/eth3/device': '../../../0000:10:00.1', +} +LSPCI_KS_IGB_UNBOUND = """ +{} Ethernet controller: Intel Corporation I350 Gigabit Network Connection (rev 01) + Subsystem: Cisco Systems Inc Device 00d6 +""" +LSPCI_KS_IGB_BOUND = """ +{} Ethernet controller: Intel Corporation I350 Gigabit Network Connection (rev 01) + Subsystem: Cisco Systems Inc Device 00d6 + Kernel driver in use: igb +""" +LSPCI_KS_IGBUIO_BOUND = """ +{} Ethernet controller: Cisco Systems Inc VIC Ethernet NIC (rev a2) + Subsystem: Cisco Systems Inc VIC 1240 MLOM Ethernet NIC + Kernel driver in use: igb_uio +""" +LSPCI_KS = { + '0000:06:00.0': LSPCI_KS_IGBUIO_BOUND.format('06:00.0'), + '0000:10:00.0': LSPCI_KS_IGB_BOUND.format('10:00.0'), +} + +MODALIAS = """ +alias pci:v00001137d00000071sv*sd*bc*sc*i* enic +alias pci:v00001137d00000044sv*sd*bc*sc*i* enic +alias pci:v00001137d00000043sv*sd*bc*sc*i* enic +alias pci:v00008086d000010D6sv*sd*bc*sc*i* igb +alias pci:v00008086d000010A9sv*sd*bc*sc*i* igb +alias pci:v00008086d00001522sv*sd*bc*sc*i* igb +alias pci:v00008086d00001521sv*sd*bc*sc*i* igb +alias pci:v00008086d0000157Csv*sd*bc*sc*i* igb +""" +LSPCI_NS = { + '0000:06:00.0': "06:00.0 0200: 1137:0043 (rev a2)", + '0000:07:00.0': "07:00.0 0200: 1137:0043 (rev a2)", + '0000:10:00.0': "10:00.0 0200: 8086:1521 (rev 01)", + '0000:10:00.1': "10:00.1 0200: 8086:1521 (rev 01)", +} +FILE_CONTENTS = { + '/sys/class/net/eth2/address': 'a8:9d:21:cf:93:fc', + '/sys/class/net/eth3/address': 'a8:9d:21:cf:93:fd', + '/sys/class/net/eth2/operstate': 'up', + '/sys/class/net/eth3/operstate': 'down', + '/lib/modules/3.13.0-35-generic/modules.alias': MODALIAS, +} +COMMANDS = { + 'LSPCI_MD': ['lspci', '-m', '-D'], + 'LSPCI_KS': ['lspci', '-ks'], + 'LSPCI_NS': ['lspci', '-ns'], + 'UNAME_R': ['uname', '-r'], + 'CONFD_CLI': ['/opt/cisco/vpe/bin/confd_cli', '-N', '-C', '-u', 'system'], +} +NET_SETUP = { + 'LSPCI_MD': LSPCI, + 'UNAME_R': '3.13.0-35-generic', + 'CONFD_CLI': CONFD_CLI, + '0000:06:00.0': { + 'LSPCI_KS': LSPCI_KS_IGBUIO_BOUND.format('06:00.0'), + 'LSPCI_NS': "06:00.0 0200: 1137:0043 (rev a2)", + }, + '0000:07:00.0': { + 'LSPCI_KS': LSPCI_KS_IGBUIO_BOUND.format('07:00.0'), + 'LSPCI_NS': "07:00.0 0200: 1137:0043 (rev a2)", + }, + '0000:10:00.0': { + 'LSPCI_KS': LSPCI_KS_IGB_BOUND.format('10:00.0'), + 'LSPCI_NS': "10:00.0 0200: 8086:1521 (rev 01)", + }, + '0000:10:00.1': { + 'LSPCI_KS': LSPCI_KS_IGB_BOUND.format('10:00.1'), + 'LSPCI_NS': "10:00.1 0200: 8086:1521 (rev 01)", + }, +} +NET_SETUP_ORPHAN = copy.deepcopy(NET_SETUP) +NET_SETUP_ORPHAN['CONFD_CLI'] = CONFD_CLI_ONE_MISSING +NET_SETUP_ORPHAN['0000:07:00.0']['LSPCI_KS'] = LSPCI_KS_IGB_UNBOUND.format('07:00.0') +QN_CONF = """ +lc_procs = { svm_cleanup vpe confd orca } + +install_root = "/cisco" + +svm_cleanup = { + pgm = "$(install_root)/bin/svm_cleanup", + run_once = "yes", + max_synchronous_wait = "5.0", + console_output = "yes" +} + +vpe = { + pgm = "$(install_root)/bin/vpe", + args = "unix { nodaemon log /tmp/vpe.log cli-listen localhost:5002 full-coredump } api-trace { on } dpdk { socket-mem 1024 dev 0000:00:06.0 }", + max_cpu_percent = "111.0", + console_output = "yes", + crash_reset_all="yes" +} + +confd = { + pgm = "$(install_root)/bin/confd", + args = "--foreground -c $(install_root)/etc/confd/confd.conf", + max_cpu_percent = "111.0", + crash_reset_all="yes" +} + +orca = { + pgm = "$(install_root)/bin/orca", + args = "unix { nodaemon log /tmp/orca.log cli-listen localhost:5003 }", + console_output = "yes", + max_cpu_percent = "111.0", + crash_reset_all="yes" +""" +DPKG_L = """ +ii net-tools 1.60-25ubuntu2.1 amd64 The NET-3 networking toolkit +ii netbase 5.2 all Basic TCP/IP networking system +ii netcat-openbsd 1.105-7ubuntu1 amd64 TCP/IP swiss army knife +ii nova-common 1:2014.1.4-0ubuntu2.1.1~ppa201506221720 all OpenStack Compute - common files +""" diff --git a/unit_tests/test_charms_openstack_devices_pci.py b/unit_tests/test_charms_openstack_devices_pci.py new file mode 100644 index 0000000..1c35250 --- /dev/null +++ b/unit_tests/test_charms_openstack_devices_pci.py @@ -0,0 +1,558 @@ +# Copyright 2016 Canonical Ltd +# +# 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. + +# Note that the unit_tests/__init__.py has the following lines to stop +# side effects from the imorts from charm helpers. + +# sys.path.append('./lib') +# mock out some charmhelpers libraries as they have apt install side effects +# sys.modules['charmhelpers.contrib.openstack.utils'] = mock.MagicMock() +# sys.modules['charmhelpers.contrib.network.ip'] = mock.MagicMock() + +from __future__ import absolute_import +import mock + +import charms_openstack.devices.pci as pci +import unit_tests.pci_responses as pci_responses +import unit_tests.utils as utils + + +def mocked_subprocess(subproc_map=None): + def _subproc(cmd, stdin=None): + for key in pci_responses.COMMANDS.keys(): + if pci_responses.COMMANDS[key] == cmd: + return subproc_map[key] + elif pci_responses.COMMANDS[key] == cmd[:-1]: + return subproc_map[cmd[-1]][key] + + if not subproc_map: + subproc_map = pci_responses.NET_SETUP + return _subproc + + +class mocked_filehandle(object): + def _setfilename(self, fname, omode): + self.FILENAME = fname + + def _getfilecontents_read(self): + return pci_responses.FILE_CONTENTS[self.FILENAME] + + def _getfilecontents_readlines(self): + return pci_responses.FILE_CONTENTS[self.FILENAME].split('\n') + + +class PCIDevTest(utils.BaseTestCase): + + def test_format_pci_addr(self): + self.assertEqual(pci.format_pci_addr('0:0:1.1'), '0000:00:01.1') + self.assertEqual(pci.format_pci_addr( + '0000:00:02.1'), '0000:00:02.1') + + +class PCINetDeviceTest(utils.BaseTestCase): + + def test_init(self): + self.patch_object(pci.PCINetDevice, 'update_attributes') + a = pci.PCINetDevice('pciaddr') + self.update_attributes.assert_called_once_with() + self.assertEqual(a.pci_address, 'pciaddr') + + def test_update_attributes(self): + self.patch_object(pci.PCINetDevice, '__init__') + self.patch_object(pci.PCINetDevice, 'loaded_kmod') + self.patch_object(pci.PCINetDevice, 'update_modalias_kmod') + self.patch_object(pci.PCINetDevice, 'update_interface_info') + a = pci.PCINetDevice('pciaddr') + a.update_attributes() + self.update_modalias_kmod.assert_called_once_with() + self.update_interface_info.assert_called_once_with() + + def test_loaded_kmod(self): + self.patch_object(pci.PCINetDevice, 'update_attributes') + self.patch_object(pci, 'subprocess') + self.subprocess.check_output.side_effect = mocked_subprocess() + device = pci.PCINetDevice('0000:06:00.0') + self.assertEqual(device.loaded_kmod, 'igb_uio') + + def test_update_modalias_kmod(self): + self.patch_object(pci.PCINetDevice, 'update_attributes') + self.patch_object(pci, 'subprocess') + device = pci.PCINetDevice('0000:07:00.0') + self.subprocess.check_output.side_effect = mocked_subprocess() + with utils.patch_open() as (_open, _file): + super_fh = mocked_filehandle() + _file.readlines = mock.MagicMock() + _open.side_effect = super_fh._setfilename + _file.read.side_effect = super_fh._getfilecontents_read + _file.readlines.side_effect = super_fh._getfilecontents_readlines + device.update_modalias_kmod() + self.assertEqual(device.modalias_kmod, 'enic') + + def test_update_interface_info_call_vpeinfo(self): + self.patch_object(pci.PCINetDevice, 'update_interface_info_eth') + self.patch_object(pci.PCINetDevice, 'update_interface_info_vpe') + self.patch_object(pci.PCINetDevice, 'update_attributes') + self.patch_object(pci.PCINetDevice, 'get_kernel_name') + self.patch_object(pci.PCINetDevice, 'loaded_kmod', new='igb_uio') + self.patch_object(pci, 'subprocess') + self.get_kernel_name.return_value = '3.13.0-77-generic' + self.subprocess.check_output.side_effect = \ + mocked_subprocess() + dev6 = pci.PCINetDevice('0000:06:00.0') + dev6.update_interface_info() + self.update_interface_info_vpe.assert_called_with() + self.assertFalse(self.update_interface_info_eth.called) + + def test_update_interface_info_call_ethinfo(self): + self.patch_object(pci.PCINetDevice, 'update_interface_info_eth') + self.patch_object(pci.PCINetDevice, 'update_interface_info_vpe') + self.patch_object(pci.PCINetDevice, 'update_attributes') + self.patch_object(pci.PCINetDevice, 'get_kernel_name') + self.patch_object(pci.PCINetDevice, 'loaded_kmod', new='igb') + self.patch_object(pci, 'subprocess') + self.get_kernel_name.return_value = '3.13.0-77-generic' + self.subprocess.check_output.side_effect = \ + mocked_subprocess() + dev = pci.PCINetDevice('0000:10:00.0') + dev.update_interface_info() + self.update_interface_info_eth.assert_called_with() + self.assertFalse(self.update_interface_info_vpe.called) + + def test_test_update_interface_info_orphan(self): + self.patch_object(pci.PCINetDevice, 'update_interface_info_eth') + self.patch_object(pci.PCINetDevice, 'update_interface_info_vpe') + self.patch_object(pci.PCINetDevice, 'update_attributes') + self.patch_object(pci.PCINetDevice, 'get_kernel_name') + self.patch_object(pci, 'subprocess') + self.subprocess.check_output.side_effect = \ + mocked_subprocess( + subproc_map=pci_responses.NET_SETUP_ORPHAN) + dev = pci.PCINetDevice('0000:07:00.0') + dev.update_interface_info() + self.assertFalse(self.update_interface_info_vpe.called) + self.assertFalse(self.update_interface_info_eth.called) + self.assertEqual(dev.interface_name, None) + self.assertEqual(dev.mac_address, None) + + def test_get_kernel_name(self): + self.patch_object(pci.PCINetDevice, 'update_attributes') + self.patch_object(pci, 'subprocess') + dev = pci.PCINetDevice('0000:07:00.0') + self.subprocess.check_output.return_value = '3.13.0-55-generic' + self.assertEqual(dev.get_kernel_name(), '3.13.0-55-generic') + + def test_pci_rescan(self): + self.patch_object(pci.PCINetDevice, 'update_attributes') + self.patch_object(pci, 'subprocess') + dev = pci.PCINetDevice('0000:07:00.0') + with utils.patch_open() as (_open, _file): + dev.pci_rescan() + _open.assert_called_with('/sys/bus/pci/rescan', 'w') + _file.write.assert_called_with('1') + + def test_bind(self): + self.patch_object(pci.PCINetDevice, 'pci_rescan') + self.patch_object(pci.PCINetDevice, 'update_attributes') + dev = pci.PCINetDevice('0000:07:00.0') + with utils.patch_open() as (_open, _file): + dev.bind('enic') + _open.assert_called_with('/sys/bus/pci/drivers/enic/bind', 'w') + _file.write.assert_called_with('0000:07:00.0') + self.pci_rescan.assert_called_with() + self.update_attributes.assert_called_with() + + def test_unbind(self): + self.patch_object(pci.PCINetDevice, 'pci_rescan') + self.patch_object(pci.PCINetDevice, 'update_attributes') + self.patch_object(pci.PCINetDevice, 'loaded_kmod', new='igb_uio') + dev = pci.PCINetDevice('0000:07:00.0') + with utils.patch_open() as (_open, _file): + dev.unbind() + _open.assert_called_with( + '/sys/bus/pci/drivers/igb_uio/unbind', 'w') + _file.write.assert_called_with('0000:07:00.0') + self.pci_rescan.assert_called_with() + self.update_attributes.assert_called_with() + + def test_update_interface_info_vpe(self): + self.patch_object(pci.PCINetDevice, 'update_attributes') + self.patch_object(pci.PCINetDevice, 'get_vpe_interfaces_and_macs') + self.get_vpe_interfaces_and_macs.return_value = [ + { + 'interface': 'TenGigabitEthernet6/0/0', + 'macAddress': '84:b8:02:2a:5f:c3', + 'pci_address': '0000:06:00.0'}, + { + 'interface': 'TenGigabitEthernet7/0/0', + 'macAddress': '84:b8:02:2a:5f:c4', + 'pci_address': '0000:07:00.0'}] + dev = pci.PCINetDevice('0000:07:00.0') + dev.update_interface_info_vpe() + self.assertEqual('TenGigabitEthernet7/0/0', dev.interface_name) + self.assertEqual('84:b8:02:2a:5f:c4', dev.mac_address) + self.assertEqual('vpebound', dev.state) + + def test_update_interface_info_vpe_orphan(self): + self.patch_object(pci.PCINetDevice, 'update_attributes') + self.patch_object(pci.PCINetDevice, 'get_vpe_interfaces_and_macs') + self.get_vpe_interfaces_and_macs.return_value = [ + { + 'interface': 'TenGigabitEthernet6/0/0', + 'macAddress': '84:b8:02:2a:5f:c3', + 'pci_address': '0000:06:00.0'}] + dev = pci.PCINetDevice('0000:07:00.0') + dev.update_interface_info_vpe() + self.assertEqual(None, dev.interface_name) + self.assertEqual(None, dev.mac_address) + self.assertEqual(None, dev.state) + + def test_get_vpe_cli_out(self): + self.patch_object(pci.PCINetDevice, 'update_attributes') + self.patch_object(pci, 'subprocess') + self.subprocess.check_output.side_effect = \ + mocked_subprocess() + dev = pci.PCINetDevice('0000:07:00.0') + self.assertTrue('local0' in dev.get_vpe_cli_out()) + + def test_get_vpe_interfaces_and_macs(self): + self.patch_object(pci.PCINetDevice, 'get_vpe_cli_out') + self.patch_object(pci.PCINetDevice, 'update_attributes') + self.patch_object(pci, 'subprocess') + self.subprocess.check_output.side_effect = \ + mocked_subprocess() + self.get_vpe_cli_out.return_value = pci_responses.CONFD_CLI + dev = pci.PCINetDevice('0000:07:00.0') + vpe_devs = dev.get_vpe_interfaces_and_macs() + expect = [ + { + 'interface': 'TenGigabitEthernet6/0/0', + 'macAddress': '84:b8:02:2a:5f:c3', + 'pci_address': '0000:06:00.0' + }, + { + 'interface': 'TenGigabitEthernet7/0/0', + 'macAddress': '84:b8:02:2a:5f:c4', + 'pci_address': '0000:07:00.0' + }, + ] + self.assertEqual(vpe_devs, expect) + + def test_get_vpe_interfaces_and_macs_invalid_cli(self): + self.patch_object(pci.PCINetDevice, 'get_vpe_cli_out') + self.patch_object(pci.PCINetDevice, 'update_attributes') + self.patch_object(pci, 'subprocess') + self.subprocess.check_output.side_effect = \ + mocked_subprocess() + dev = pci.PCINetDevice('0000:07:00.0') + self.get_vpe_cli_out.return_value = pci_responses.CONFD_CLI_NOLOCAL + with self.assertRaises(pci.VPECLIException): + dev.get_vpe_interfaces_and_macs() + + def test_get_vpe_interfaces_and_macs_invmac(self): + self.patch_object(pci.PCINetDevice, 'get_vpe_cli_out') + self.patch_object(pci.PCINetDevice, 'update_attributes') + self.patch_object(pci, 'subprocess') + self.subprocess.check_output.side_effect = \ + mocked_subprocess() + dev = pci.PCINetDevice('0000:07:00.0') + self.get_vpe_cli_out.return_value = pci_responses.CONFD_CLI_INVMAC + vpe_devs = dev.get_vpe_interfaces_and_macs() + expect = [ + { + 'interface': 'TenGigabitEthernet7/0/0', + 'macAddress': '84:b8:02:2a:5f:c4', + 'pci_address': '0000:07:00.0' + }, + ] + self.assertEqual(vpe_devs, expect) + + def test_extract_pci_addr_from_vpe_interface(self): + self.patch_object(pci.PCINetDevice, 'update_attributes') + dev = pci.PCINetDevice('0000:07:00.0') + self.assertEqual(dev.extract_pci_addr_from_vpe_interface( + 'TenGigabitEthernet1/1/1'), '0000:01:01.1') + self.assertEqual(dev.extract_pci_addr_from_vpe_interface( + 'TenGigabitEtherneta/0/0'), '0000:0a:00.0') + self.assertEqual(dev.extract_pci_addr_from_vpe_interface( + 'GigabitEthernet0/2/0'), '0000:00:02.0') + + def test_update_interface_info_eth(self): + self.patch_object(pci.PCINetDevice, 'update_attributes') + self.patch_object(pci.PCINetDevice, 'get_sysnet_interfaces_and_macs') + dev = pci.PCINetDevice('0000:10:00.0') + self.get_sysnet_interfaces_and_macs.return_value = [ + { + 'interface': 'eth2', + 'macAddress': 'a8:9d:21:cf:93:fc', + 'pci_address': '0000:10:00.0', + 'state': 'up' + }, + { + 'interface': 'eth3', + 'macAddress': 'a8:9d:21:cf:93:fd', + 'pci_address': '0000:10:00.1', + 'state': 'down' + } + ] + dev.update_interface_info_eth() + self.assertEqual(dev.interface_name, 'eth2') + + def test_get_sysnet_interfaces_and_macs_virtio(self): + self.patch_object(pci.glob, 'glob') + self.patch_object(pci.os.path, 'islink') + self.patch_object(pci.os.path, 'realpath') + self.patch_object(pci.PCINetDevice, 'get_sysnet_device_state') + self.patch_object(pci.PCINetDevice, 'get_sysnet_mac') + self.patch_object(pci.PCINetDevice, 'get_sysnet_interface') + self.patch_object(pci.PCINetDevice, 'update_attributes') + dev = pci.PCINetDevice('0000:06:00.0') + self.glob.return_value = ['/sys/class/net/eth2'] + self.get_sysnet_interface.return_value = 'eth2' + self.get_sysnet_mac.return_value = 'a8:9d:21:cf:93:fc' + self.get_sysnet_device_state.return_value = 'up' + self.realpath.return_value = ('/sys/devices/pci0000:00/0000:00:07.0/' + 'virtio5') + self.islink.return_value = True + expect = { + 'interface': 'eth2', + 'macAddress': 'a8:9d:21:cf:93:fc', + 'pci_address': '0000:00:07.0', + 'state': 'up', + } + self.assertEqual(dev.get_sysnet_interfaces_and_macs(), [expect]) + + def test_get_sysnet_interfaces_and_macs(self): + self.patch_object(pci.glob, 'glob') + self.patch_object(pci.os.path, 'islink') + self.patch_object(pci.os.path, 'realpath') + self.patch_object(pci.PCINetDevice, 'get_sysnet_device_state') + self.patch_object(pci.PCINetDevice, 'get_sysnet_mac') + self.patch_object(pci.PCINetDevice, 'get_sysnet_interface') + self.patch_object(pci.PCINetDevice, 'update_attributes') + dev = pci.PCINetDevice('0000:06:00.0') + self.glob.return_value = ['/sys/class/net/eth2'] + self.get_sysnet_interface.return_value = 'eth2' + self.get_sysnet_mac.return_value = 'a8:9d:21:cf:93:fc' + self.get_sysnet_device_state.return_value = 'up' + self.realpath.return_value = ( + '/sys/devices/pci0000:00/0000:00:02.0/0000:02:00.0/0000:03:00.0/' + '0000:04:00.0/0000:05:01.0/0000:07:00.0') + self.islink.return_value = True + expect = { + 'interface': 'eth2', + 'macAddress': 'a8:9d:21:cf:93:fc', + 'pci_address': '0000:07:00.0', + 'state': 'up', + } + self.assertEqual(dev.get_sysnet_interfaces_and_macs(), [expect]) + + def test_get_sysnet_mac(self): + self.patch_object(pci.PCINetDevice, 'update_attributes') + device = pci.PCINetDevice('0000:10:00.1') + with utils.patch_open() as (_open, _file): + super_fh = mocked_filehandle() + _file.readlines = mock.MagicMock() + _open.side_effect = super_fh._setfilename + _file.read.side_effect = super_fh._getfilecontents_read + macaddr = device.get_sysnet_mac('/sys/class/net/eth3') + self.assertEqual(macaddr, 'a8:9d:21:cf:93:fd') + + def test_get_sysnet_device_state(self): + self.patch_object(pci.PCINetDevice, 'update_attributes') + device = pci.PCINetDevice('0000:10:00.1') + with utils.patch_open() as (_open, _file): + super_fh = mocked_filehandle() + _file.readlines = mock.MagicMock() + _open.side_effect = super_fh._setfilename + _file.read.side_effect = super_fh._getfilecontents_read + state = device.get_sysnet_device_state('/sys/class/net/eth3') + self.assertEqual(state, 'down') + + def test_get_sysnet_interface(self): + self.patch_object(pci.PCINetDevice, 'update_attributes') + device = pci.PCINetDevice('0000:10:00.1') + self.assertEqual( + device.get_sysnet_interface('/sys/class/net/eth3'), 'eth3') + + +class PCINetDevicesTest(utils.BaseTestCase): + + def test_init(self): + self.patch_object(pci.PCINetDevices, 'get_pci_ethernet_addresses') + self.patch_object(pci, 'PCINetDevice') + self.get_pci_ethernet_addresses.return_value = ['pciaddr'] + pci.PCINetDevices() + self.PCINetDevice.assert_called_once_with('pciaddr') + + def test_get_pci_ethernet_addresses(self): + self.patch_object(pci, 'subprocess') + self.patch_object(pci, 'PCINetDevice') + self.subprocess.check_output.side_effect = \ + mocked_subprocess() + a = pci.PCINetDevices() + self.assertEqual( + a.get_pci_ethernet_addresses(), + ['0000:06:00.0', '0000:07:00.0', '0000:10:00.0', '0000:10:00.1']) + + def test_update_devices(self): + pcinetdev = mock.MagicMock() + self.patch_object(pci.PCINetDevices, 'get_pci_ethernet_addresses') + self.patch_object(pci, 'PCINetDevice') + self.PCINetDevice.return_value = pcinetdev + self.get_pci_ethernet_addresses.return_value = ['pciaddr'] + a = pci.PCINetDevices() + a.update_devices() + pcinetdev.update_attributes.assert_called_once_with() + + def test_get_macs(self): + pcinetdev = mock.MagicMock() + self.patch_object(pci.PCINetDevices, 'get_pci_ethernet_addresses') + self.patch_object(pci, 'PCINetDevice') + self.PCINetDevice.return_value = pcinetdev + self.get_pci_ethernet_addresses.return_value = ['pciaddr'] + pcinetdev.mac_address = 'mac1' + a = pci.PCINetDevices() + self.assertEqual(a.get_macs(), ['mac1']) + + def test_get_device_from_mac(self): + pcinetdev = mock.MagicMock() + self.patch_object(pci.PCINetDevices, 'get_pci_ethernet_addresses') + self.patch_object(pci, 'PCINetDevice') + self.PCINetDevice.return_value = pcinetdev + self.get_pci_ethernet_addresses.return_value = ['pciaddr'] + pcinetdev.mac_address = 'mac1' + a = pci.PCINetDevices() + self.assertEqual(a.get_device_from_mac('mac1'), pcinetdev) + + def test_get_device_from_pci_address(self): + pcinetdev = mock.MagicMock() + self.patch_object(pci.PCINetDevices, 'get_pci_ethernet_addresses') + self.patch_object(pci, 'PCINetDevice') + self.PCINetDevice.return_value = pcinetdev + self.get_pci_ethernet_addresses.return_value = ['pciaddr'] + pcinetdev.pci_address = 'pciaddr' + a = pci.PCINetDevices() + self.assertEqual(a.get_device_from_pci_address('pciaddr'), pcinetdev) + + def test_rebind_orphans(self): + self.patch_object(pci.PCINetDevices, 'get_pci_ethernet_addresses') + self.patch_object(pci.PCINetDevices, 'unbind_orphans') + self.patch_object(pci.PCINetDevices, 'bind_orphans') + self.patch_object(pci, 'PCINetDevice') + self.get_pci_ethernet_addresses.return_value = [] + a = pci.PCINetDevices() + a.rebind_orphans() + self.unbind_orphans.assert_called_once_with() + self.bind_orphans.assert_called_once_with() + + def test_unbind_orphans(self): + orphan = mock.MagicMock() + self.patch_object(pci.PCINetDevices, 'get_pci_ethernet_addresses') + self.get_pci_ethernet_addresses.return_value = ['pciaddr'] + self.patch_object(pci.PCINetDevices, 'get_orphans') + self.patch_object(pci.PCINetDevices, 'update_devices') + self.patch_object(pci, 'PCINetDevice') + self.get_orphans.return_value = [orphan] + a = pci.PCINetDevices() + a.unbind_orphans() + orphan.unbind.assert_called_once_with() + self.update_devices.assert_called_once_with() + + def test_bind_orphans(self): + orphan = mock.MagicMock() + self.patch_object(pci.PCINetDevices, 'get_pci_ethernet_addresses') + self.get_pci_ethernet_addresses.return_value = ['pciaddr'] + self.patch_object(pci.PCINetDevices, 'get_orphans') + self.patch_object(pci.PCINetDevices, 'update_devices') + self.patch_object(pci, 'PCINetDevice') + self.get_orphans.return_value = [orphan] + orphan.modalias_kmod = 'kmod' + a = pci.PCINetDevices() + a.bind_orphans() + orphan.bind.assert_called_once_with('kmod') + self.update_devices.assert_called_once_with() + + def test_get_orphans(self): + pcinetdev = mock.MagicMock() + self.patch_object(pci.PCINetDevices, 'get_pci_ethernet_addresses') + self.patch_object(pci, 'PCINetDevice') + self.PCINetDevice.return_value = pcinetdev + self.get_pci_ethernet_addresses.return_value = ['pciaddr'] + pcinetdev.loaded_kmod = None + pcinetdev.interface_name = None + pcinetdev.mac_address = None + a = pci.PCINetDevices() + self.assertEqual(a.get_orphans(), [pcinetdev]) + + +class PCIInfoTest(utils.BaseTestCase): + + def dev_mock(self, state, pci_address, interface_name): + dev = mock.MagicMock() + dev.state = state + dev.pci_address = pci_address + dev.interface_name = interface_name + return dev + + def test_init(self): + net_dev_mocks = { + 'mac1': self.dev_mock('down', 'pciaddr0', 'eth0'), + 'mac2': self.dev_mock('down', 'pciaddr1', 'eth1'), + 'mac3': self.dev_mock('up', 'pciaddr3', 'eth2'), + } + net_devs = mock.MagicMock() + self.patch_object(pci.PCIInfo, 'get_user_requested_config') + self.patch_object(pci, 'PCINetDevices') + self.PCINetDevices.return_value = net_devs + net_devs.get_macs.return_value = net_dev_mocks.keys() + net_devs.get_device_from_mac.side_effect = lambda x: net_dev_mocks[x] + self.get_user_requested_config.return_value = { + 'mac1': [{'net': 'net1'}, {'net': 'net2'}], + 'mac2': [{'net': 'net1'}], + 'mac3': [{'net': 'net1'}]} + a = pci.PCIInfo() + expect = { + 'mac1': [{'interface': 'eth0', 'net': 'net1'}, + {'interface': 'eth0', 'net': 'net2'}], + 'mac2': [{'interface': 'eth1', 'net': 'net1'}]} + self.assertEqual(a.local_mac_nets, expect) + self.assertEqual(a.vpe_dev_string, 'dev pciaddr0 dev pciaddr1') + + def test_get_user_requested_config(self): + self.patch_object(pci.PCIInfo, '__init__') + self.patch_object(pci.hookenv, 'config') + self.config.return_value = ('mac=mac1;net=net1 mac=mac1;net=net2' + ' mac=mac2;net=net1') + a = pci.PCIInfo() + expect = { + 'mac1': [{'net': 'net1'}, {'net': 'net2'}], + 'mac2': [{'net': 'net1'}]} + self.assertEqual(a.get_user_requested_config(), expect) + + def test_get_user_requested_invalid_entries(self): + self.patch_object(pci.PCIInfo, '__init__') + self.patch_object(pci.hookenv, 'config') + self.config.return_value = ('ac=mac1;net=net1 randomstuff' + ' mac=mac2;net=net1') + a = pci.PCIInfo() + expect = {'mac2': [{'net': 'net1'}]} + self.assertEqual(a.get_user_requested_config(), expect) + + def test_get_user_requested_config_empty(self): + self.patch_object(pci.PCIInfo, '__init__') + self.patch_object(pci.hookenv, 'config') + self.config.return_value = None + a = pci.PCIInfo() + expect = {} + self.assertEqual(a.get_user_requested_config(), expect) diff --git a/unit_tests/test_charms_openstack_sdn_odl.py b/unit_tests/test_charms_openstack_sdn_odl.py new file mode 100644 index 0000000..c46eca8 --- /dev/null +++ b/unit_tests/test_charms_openstack_sdn_odl.py @@ -0,0 +1,187 @@ +import httpretty +import requests +import simplejson + +import unit_tests.odl_responses as odl_responses +import charms_openstack.sdn.odl as odl +import unit_tests.utils as utils + +NOT_JSON = "Im not json" + + +class ODLTest(utils.BaseTestCase): + + def setUp(self): + super(ODLTest, self).setUp() + self.odlc = odl.ODLConfig('bob', 'pword', '10.0.0.10', port='93') + self.patch_object(odl.hookenv, 'log') + + def test_base(self): + self.assertEqual(self.odlc.auth, ('bob', 'pword')) + self.assertEqual(self.odlc.base_url, 'http://10.0.0.10:93') + + @httpretty.activate + def test_contact_odl(self): + httpretty.register_uri(httpretty.GET, "http://10.0.0.10:93/geturl", + body='[{"title": "Test Data"}]', + content_type="application/json", status=200) + response = self.odlc.contact_odl('GET', 'http://10.0.0.10:93/geturl') + self.assertEqual(response.json(), [{"title": "Test Data"}]) + + @httpretty.activate + def test_contact_odl_empty(self): + url = 'http://10.0.0.10:93/puturl' + httpretty.register_uri(httpretty.PUT, url, + body='', status=204) + response = self.odlc.contact_odl('PUT', url) + self.assertEqual(response.status_code, 204) + + @httpretty.activate + def test_contact_odl_notfound(self): + httpretty.register_uri(httpretty.GET, "http://10.0.0.10:93/geturl", + status=404) + with self.assertRaises(odl.ODLInteractionFatalError): + self.odlc.contact_odl('GET', 'http://10.0.0.10:93/geturl') + + @httpretty.activate + def test_contact_odl_retry(self): + httpretty.register_uri(httpretty.GET, "http://10.0.0.10:93/geturl", + status=404) + with self.assertRaises(requests.exceptions.ConnectionError): + self.odlc.contact_odl( + 'GET', 'http://10.0.0.10:93/geturl', retry_rcs=[404]) + + @httpretty.activate + def test_get_networks(self): + url = self.odlc.netmap_url + httpretty.register_uri( + httpretty.GET, url, status=200, body=odl_responses.NEUTRON_NET_MAP) + nets = self.odlc.get_networks() + self.assertTrue('physicalNetwork' in nets.keys()) + self.assertEqual(len(nets['physicalNetwork']), 3) + net_names = [net['name'] for net in nets['physicalNetwork']] + for net in ['net_d10', 'net_d11', 'net_d12']: + self.assertTrue(net in net_names) + + @httpretty.activate + def test_get_networks_nonets(self): + url = self.odlc.netmap_url + httpretty.register_uri(httpretty.GET, url, status=200, body="{}") + nets = self.odlc.get_networks() + self.assertEqual(nets, {}) + + @httpretty.activate + def test_get_networks_no_neutron_map(self): + url = self.odlc.netmap_url + httpretty.register_uri(httpretty.GET, url, status=404) + nets = self.odlc.get_networks() + self.assertEqual(nets, {}) + + @httpretty.activate + def test_get_networks_notjson(self): + url = self.odlc.netmap_url + httpretty.register_uri(httpretty.GET, url, status=200, body=NOT_JSON) + with self.assertRaises(simplejson.JSONDecodeError): + self.odlc.get_networks() + + def test_delete_net_device_entry(self): + self.patch_object(odl.ODLConfig, 'contact_odl') + self.odlc.delete_net_device_entry('net_d10', 'mymachine') + url = self.odlc.netmap_url + 'physicalNetwork/net_d10/device/mymachine' + self.contact_odl.assert_called_with('DELETE', url) + + @httpretty.activate + def test_get_odl_registered_nodes(self): + url = self.odlc.node_query_url + httpretty.register_uri( + httpretty.GET, url, status=200, + body=odl_responses.ODL_REGISTERED_NODES) + nodes = self.odlc.get_odl_registered_nodes() + self.assertEqual(nodes, ['C240-M4-6', 'controller-config']) + + @httpretty.activate + def test_get_odl_registered_empty(self): + url = self.odlc.node_query_url + httpretty.register_uri(httpretty.GET, url, status=200, body="{}") + nodes = self.odlc.get_odl_registered_nodes() + self.assertEqual(nodes, []) + + @httpretty.activate + def test_get_odl_registered_notjson(self): + url = self.odlc.node_query_url + httpretty.register_uri(httpretty.GET, url, status=200, body=NOT_JSON) + with self.assertRaises(simplejson.JSONDecodeError): + self.odlc.get_odl_registered_nodes() + + def test_odl_register_node(self): + self.patch_object(odl.ODLConfig, 'contact_odl') + url = self.odlc.node_mount_url + self.odlc.odl_register_node('mymachine', '10.0.0.11') + reg_call = self.contact_odl.call_args_list[0] + self.assertTrue(reg_call[0], ('POST', url)) + + def test_odl_register_macs(self): + self.patch_object(odl.ODLConfig, 'contact_odl') + url = self.odlc.conf_url + self.odlc.odl_register_macs( + "C240-M4-6", "net_d1", "TenGigabitEthernet6/0/0", + "84:b8:02:2a:5f:c3") + reg_call = self.contact_odl.call_args_list[0] + self.assertTrue(reg_call[0], ('POST', url)) + + @httpretty.activate + def test_get_macs_networks(self): + url = self.odlc.netmap_url + httpretty.register_uri( + httpretty.GET, url, status=200, body=odl_responses.NEUTRON_NET_MAP) + nets = self.odlc.get_macs_networks('84:b8:02:2a:5f:c3') + self.assertEqual(nets, ['net_d12', 'net_d10']) + + @httpretty.activate + def test_get_macs_networks_nomatch(self): + url = self.odlc.netmap_url + httpretty.register_uri( + httpretty.GET, url, status=200, body=odl_responses.NEUTRON_NET_MAP) + nets = self.odlc.get_macs_networks('04:08:02:0a:0f:03') + self.assertEqual(nets, []) + + @httpretty.activate + def test_get_macs_networks_nonets(self): + url = self.odlc.netmap_url + httpretty.register_uri(httpretty.GET, url, status=200, body="{}") + nets = self.odlc.get_macs_networks('04:08:02:0a:0f:03') + self.assertEqual(nets, []) + + @httpretty.activate + def test_is_device_registered(self): + url = self.odlc.node_query_url + httpretty.register_uri( + httpretty.GET, url, status=200, + body=odl_responses.ODL_REGISTERED_NODES) + self.assertTrue(self.odlc.is_device_registered('C240-M4-6')) + + @httpretty.activate + def test_is_device_registered_false(self): + url = self.odlc.node_query_url + httpretty.register_uri( + httpretty.GET, url, status=200, + body=odl_responses.ODL_REGISTERED_NODES) + self.assertFalse(self.odlc.is_device_registered('B240-M4-7')) + + @httpretty.activate + def test_is_net_device_registered(self): + url = self.odlc.netmap_url + httpretty.register_uri( + httpretty.GET, url, status=200, body=odl_responses.NEUTRON_NET_MAP) + self.assertTrue(self.odlc.is_net_device_registered( + 'net_d10', 'C240-M4-6', 'TenGigabitEthernet6/0/0', + '84:b8:02:2a:5f:c3')) + + @httpretty.activate + def test_is_net_device_registered_false(self): + url = self.odlc.netmap_url + httpretty.register_uri( + httpretty.GET, url, status=200, body=odl_responses.NEUTRON_NET_MAP) + self.assertFalse(self.odlc.is_net_device_registered( + 'net_d510', 'C240-M4-6', 'TenGigabitEthernet6/0/0', + '84:b8:02:2a:5f:c3')) diff --git a/unit_tests/test_charms_openstack_sdn_ovs.py b/unit_tests/test_charms_openstack_sdn_ovs.py new file mode 100644 index 0000000..1a43d19 --- /dev/null +++ b/unit_tests/test_charms_openstack_sdn_ovs.py @@ -0,0 +1,51 @@ +# Copyright 2016 Canonical Ltd +# +# 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. + +# Note that the unit_tests/__init__.py has the following lines to stop +# side effects from the imorts from charm helpers. + +# sys.path.append('./lib') +# mock out some charmhelpers libraries as they have apt install side effects +# sys.modules['charmhelpers.contrib.openstack.utils'] = mock.MagicMock() +# sys.modules['charmhelpers.contrib.network.ip'] = mock.MagicMock() +from __future__ import absolute_import + +import unit_tests.utils as utils + +import charms_openstack.sdn.ovs as ovs + + +class TestCharmOpenStackSDNOVS(utils.BaseTestCase): + + def test_set_manager(self): + self.patch_object(ovs, 'subprocess') + ovs.set_manager('myurl') + self.subprocess.check_call.assert_called_once_with( + ['ovs-vsctl', 'set-manager', 'myurl']) + + def test__get_ovstbl(self): + self.patch_object(ovs, 'subprocess') + self.subprocess.check_output.return_value = 'ovstbl' + self.assertEqual(ovs._get_ovstbl(), 'ovstbl') + self.subprocess.check_output.assert_called_once_with( + ['ovs-vsctl', 'get', 'Open_vSwitch', '.', '_uuid']) + + def test_set_config(self): + self.patch_object(ovs, 'subprocess') + self.patch_object(ovs, '_get_ovstbl') + self._get_ovstbl.return_value = 'a_uuid' + ovs.set_config('mykey', 'myvalue', 'mytable') + self.subprocess.check_call.assert_called_once_with( + ['ovs-vsctl', 'set', 'Open_vSwitch', 'a_uuid', + 'mytable:mykey=myvalue'])