diff --git a/ceilometer/hardware/__init__.py b/ceilometer/hardware/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ceilometer/hardware/inspector/__init__.py b/ceilometer/hardware/inspector/__init__.py new file mode 100644 index 000000000..f3353a306 --- /dev/null +++ b/ceilometer/hardware/inspector/__init__.py @@ -0,0 +1,29 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2014 Intel Corp. +# +# Author: Lianhao Lu +# +# 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. + +from stevedore import driver + + +def get_inspector(parsed_url, namespace='ceilometer.hardware.inspectors'): + """Get inspector driver and load it. + + :param parsed_url: urlparse.SplitResult object for the inspector + :param namespace: Namespace to use to look for drivers. + """ + loaded_driver = driver.DriverManager(namespace, parsed_url.scheme) + return loaded_driver.driver() diff --git a/ceilometer/hardware/inspector/base.py b/ceilometer/hardware/inspector/base.py new file mode 100644 index 000000000..0aea0f0de --- /dev/null +++ b/ceilometer/hardware/inspector/base.py @@ -0,0 +1,111 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2014 ZHAW SoE +# +# Authors: Lucas Graf +# Toni Zehnder +# +# 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. +"""Inspector abstraction for read-only access to hardware components""" + +import abc +import collections + +import six + +# Named tuple representing CPU statistics. +# +# cpu1MinLoad: 1 minute load +# cpu5MinLoad: 5 minute load +# cpu15MinLoad: 15 minute load +# +CPUStats = collections.namedtuple( + 'CPUStats', + ['cpu_1_min', 'cpu_5_min', 'cpu_15_min']) + +# Named tuple representing RAM statistics. +# +# total: Total Memory (bytes) +# used: Used Memory (bytes) +# +MemoryStats = collections.namedtuple('MemoryStats', ['total', 'used']) + +# Named tuple representing disks. +# +# device: the device name for the disk +# path: the path from the disk +# +Disk = collections.namedtuple('Disk', ['device', 'path']) + +# Named tuple representing disk statistics. +# +# size: storage size (bytes) +# used: storage used (bytes) +# +DiskStats = collections.namedtuple('DiskStats', ['size', 'used']) + + +# Named tuple representing an interface. +# +# name: the name of the interface +# mac: the MAC of the interface +# ip: the IP of the interface +# +Interface = collections.namedtuple('Interface', ['name', 'mac', 'ip']) + + +# Named tuple representing network interface statistics. +# +# bandwidth: current bandwidth (bytes/s) +# rx_bytes: total number of octets received (bytes) +# tx_bytes: total number of octets transmitted (bytes) +# error: number of outbound packets not transmitted because of errors +# +InterfaceStats = collections.namedtuple( + 'InterfaceStats', + ['bandwidth', 'rx_bytes', 'tx_bytes', 'error']) + + +@six.add_metaclass(abc.ABCMeta) +class Inspector(object): + @abc.abstractmethod + def inspect_cpu(self, host): + """Inspect the CPU statistics for a host. + + :param host: the target host + :return: iterator of CPUStats + """ + + @abc.abstractmethod + def inspect_disk(self, host): + """Inspect the disk statistics for a host. + + :param : the target host + :return: iterator of tuple (Disk, DiskStats) + """ + + @abc.abstractmethod + def inspect_memory(self, host): + """Inspect the ram statistics for a host. + + :param : the target host + :return: iterator of MemoryStats + """ + + @abc.abstractmethod + def inspect_network(self, host): + """Inspect the network interfaces for a host. + + :param : the target host + :return: iterator of tuple (Interface, InterfaceStats) + """ diff --git a/ceilometer/hardware/inspector/snmp.py b/ceilometer/hardware/inspector/snmp.py new file mode 100644 index 000000000..df88e82ab --- /dev/null +++ b/ceilometer/hardware/inspector/snmp.py @@ -0,0 +1,199 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2014 ZHAW SoE +# +# Authors: Lucas Graf +# Toni Zehnder +# +# 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. +"""Inspector for collecting data over SNMP""" + +import urlparse + +from ceilometer.hardware.inspector import base +from pysnmp.entity.rfc3413.oneliner import cmdgen + + +class SNMPException(Exception): + pass + + +def parse_snmp_return(ret): + """Check the return value of snmp operations + + :param ret: a tuple of (errorIndication, errorStatus, errorIndex, data) + returned by pysnmp + :return: a tuple of (err, data) + err: True if error found, or False if no error found + data: a string of error description if error found, or the + actual return data of the snmp operation + """ + err = True + (errIndication, errStatus, errIdx, varBinds) = ret + if errIndication: + data = errIndication + elif errStatus: + data = "%s at %s" % (errStatus.prettyPrint(), + errIdx and varBinds[int(errIdx) - 1] or "?") + else: + err = False + data = varBinds + return (err, data) + + +class SNMPInspector(base.Inspector): + #CPU OIDs + _cpu_1_min_load_oid = "1.3.6.1.4.1.2021.10.1.3.1" + _cpu_5_min_load_oid = "1.3.6.1.4.1.2021.10.1.3.2" + _cpu_15_min_load_oid = "1.3.6.1.4.1.2021.10.1.3.3" + #Memory OIDs + _memory_total_oid = "1.3.6.1.4.1.2021.4.5.0" + _memory_used_oid = "1.3.6.1.4.1.2021.4.6.0" + #Disk OIDs + _disk_index_oid = "1.3.6.1.4.1.2021.9.1.1" + _disk_path_oid = "1.3.6.1.4.1.2021.9.1.2" + _disk_device_oid = "1.3.6.1.4.1.2021.9.1.3" + _disk_size_oid = "1.3.6.1.4.1.2021.9.1.6" + _disk_used_oid = "1.3.6.1.4.1.2021.9.1.8" + #Network Interface OIDs + _interface_index_oid = "1.3.6.1.2.1.2.2.1.1" + _interface_name_oid = "1.3.6.1.2.1.2.2.1.2" + _interface_bandwidth_oid = "1.3.6.1.2.1.2.2.1.5" + _interface_mac_oid = "1.3.6.1.2.1.2.2.1.6" + _interface_ip_oid = "1.3.6.1.2.1.4.20.1.2" + _interface_received_oid = "1.3.6.1.2.1.2.2.1.10" + _interface_transmitted_oid = "1.3.6.1.2.1.2.2.1.16" + _interface_error_oid = "1.3.6.1.2.1.2.2.1.20" + #Default port and security name + _port = 161 + _security_name = 'public' + + def __init__(self): + super(SNMPInspector, self).__init__() + self._cmdGen = cmdgen.CommandGenerator() + + def _get_or_walk_oid(self, oid, host, get=True): + if get: + func = self._cmdGen.getCmd + ret_func = lambda x: x[0][1] + else: + func = self._cmdGen.nextCmd + ret_func = lambda x: x + ret = func(cmdgen.CommunityData(self._get_security_name(host)), + cmdgen.UdpTransportTarget((host.hostname, + host.port or self._port)), + oid) + (error, data) = parse_snmp_return(ret) + if error: + raise SNMPException("An error occurred, oid %(oid)s, " + "host %(host)s, %(err)s" % dict(oid=oid, + host=host.hostname, err=data)) + else: + return ret_func(data) + + def _get_value_from_oid(self, oid, host): + return self._get_or_walk_oid(oid, host, True) + + def _walk_oid(self, oid, host): + return self._get_or_walk_oid(oid, host, False) + + def inspect_cpu(self, host): + #get 1 minute load + cpu_1_min_load = \ + str(self._get_value_from_oid(self._cpu_1_min_load_oid, host)) + #get 5 minute load + cpu_5_min_load = \ + str(self._get_value_from_oid(self._cpu_5_min_load_oid, host)) + #get 15 minute load + cpu_15_min_load = \ + str(self._get_value_from_oid(self._cpu_15_min_load_oid, host)) + + yield base.CPUStats(cpu_1_min=float(cpu_1_min_load), + cpu_5_min=float(cpu_5_min_load), + cpu_15_min=float(cpu_15_min_load)) + + def inspect_memory(self, host): + #get total memory + total = self._get_value_from_oid(self._memory_total_oid, host) + #get used memory + used = self._get_value_from_oid(self._memory_used_oid, host) + + yield base.MemoryStats(total=int(total), used=int(used)) + + def inspect_disk(self, host): + disks = self._walk_oid(self._disk_index_oid, host) + + for disk in disks: + for object_name, value in disk: + path_oid = "%s.%s" % (self._disk_path_oid, str(value)) + path = self._get_value_from_oid(path_oid, host) + device_oid = "%s.%s" % (self._disk_device_oid, str(value)) + device = self._get_value_from_oid(device_oid, host) + size_oid = "%s.%s" % (self._disk_size_oid, str(value)) + size = self._get_value_from_oid(size_oid, host) + used_oid = "%s.%s" % (self._disk_used_oid, str(value)) + used = self._get_value_from_oid(used_oid, host) + + disk = base.Disk(device=str(device), + path=str(path)) + stats = base.DiskStats(size=int(size), + used=int(used)) + + yield (disk, stats) + + def inspect_network(self, host): + net_interfaces = self._walk_oid(self._interface_index_oid, host) + + for interface in net_interfaces: + for object_name, value in interface: + ip = self._get_ip_for_interface(host, value) + name_oid = "%s.%s" % (self._interface_name_oid, + str(value)) + name = self._get_value_from_oid(name_oid, host) + mac_oid = "%s.%s" % (self._interface_mac_oid, + str(value)) + mac = self._get_value_from_oid(mac_oid, host) + bw_oid = "%s.%s" % (self._interface_bandwidth_oid, + str(value)) + # bits/s to byte/s + bandwidth = self._get_value_from_oid(bw_oid, host) / 8 + rx_oid = "%s.%s" % (self._interface_received_oid, + str(value)) + rx_bytes = self._get_value_from_oid(rx_oid, host) + tx_oid = "%s.%s" % (self._interface_transmitted_oid, + str(value)) + tx_bytes = self._get_value_from_oid(tx_oid, host) + error_oid = "%s.%s" % (self._interface_error_oid, + str(value)) + error = self._get_value_from_oid(error_oid, host) + + adapted_mac = mac.prettyPrint().replace('0x', '') + interface = base.Interface(name=str(name), + mac=adapted_mac, + ip=str(ip)) + stats = base.InterfaceStats(bandwidth=int(bandwidth), + rx_bytes=int(rx_bytes), + tx_bytes=int(tx_bytes), + error=int(error)) + yield (interface, stats) + + def _get_security_name(self, host): + options = urlparse.parse_qs(host.query) + return options.get('security_name', [self._security_name])[-1] + + def _get_ip_for_interface(self, host, interface_id): + ip_addresses = self._walk_oid(self._interface_ip_oid, host) + for ip in ip_addresses: + for name, value in ip: + if value == interface_id: + return str(name).replace(self._interface_ip_oid + ".", "") diff --git a/ceilometer/tests/hardware/__init__.py b/ceilometer/tests/hardware/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ceilometer/tests/hardware/inspector/__init__.py b/ceilometer/tests/hardware/inspector/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ceilometer/tests/hardware/inspector/base.py b/ceilometer/tests/hardware/inspector/base.py new file mode 100644 index 000000000..4981a5135 --- /dev/null +++ b/ceilometer/tests/hardware/inspector/base.py @@ -0,0 +1,63 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2014 Intel Corp +# +# Authors: Lianhao Lu +# +# 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. + +from ceilometer.hardware.inspector import base + + +class InspectorBaseTest(object): + """Subclass must set self.inspector and self.host in + self.setUp() + """ + + cpu = [base.CPUStats(cpu_1_min=0.1, + cpu_5_min=0.2, + cpu_15_min=0.3), + ] + + network = [(base.Interface(name='eth0', + mac='112233445566', + ip='10.0.0.1'), + base.InterfaceStats(bandwidth=1250000 / 8, + rx_bytes=1000, + tx_bytes=2000, + error=1)), + ] + diskspace = [(base.Disk(device='/dev/sda1', path='/'), + base.DiskStats(size=1000, used=500), + ), + (base.Disk(device='/dev/sda2', path='/home'), + base.DiskStats(size=2000, used=1000), + ), + ] + memory = [base.MemoryStats(total=1000, used=500)] + + def test_inspect_cpu(self): + self.assertEqual(list(self.inspector.inspect_cpu(self.host)), + self.cpu) + + def test_inspect_network(self): + self.assertEqual(list(self.inspector.inspect_network(self.host)), + self.network) + + def test_inspect_disk(self): + self.assertEqual(list(self.inspector.inspect_disk(self.host)), + self.diskspace) + + def test_inspect_memory(self): + self.assertEqual(list(self.inspector.inspect_memory(self.host)), + self.memory) diff --git a/ceilometer/tests/hardware/inspector/test_inspector.py b/ceilometer/tests/hardware/inspector/test_inspector.py new file mode 100644 index 000000000..982b5d861 --- /dev/null +++ b/ceilometer/tests/hardware/inspector/test_inspector.py @@ -0,0 +1,34 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2014 Intel Corp +# +# Authors: Lianhao Lu +# +# 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. + +from ceilometer.hardware import inspector +from ceilometer.openstack.common import network_utils +from ceilometer.tests import base + + +class TestHardwareInspector(base.BaseTestCase): + def test_get_inspector(self): + url = network_utils.urlsplit("snmp://") + driver = inspector.get_inspector(url) + self.assertTrue(driver) + + def test_get_inspector_illegal(self): + url = network_utils.urlsplit("illegal://") + self.assertRaises(RuntimeError, + inspector.get_inspector, + url) diff --git a/ceilometer/tests/hardware/inspector/test_snmp.py b/ceilometer/tests/hardware/inspector/test_snmp.py new file mode 100644 index 000000000..391913837 --- /dev/null +++ b/ceilometer/tests/hardware/inspector/test_snmp.py @@ -0,0 +1,235 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2013 Intel Corp +# +# Authors: Lianhao Lu +# +# 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. +"""Tests for ceilometer/hardware/inspector/snmp/inspector.py +""" + +from ceilometer.hardware.inspector import snmp +from ceilometer.openstack.common.fixture import mockpatch +from ceilometer.openstack.common import network_utils +from ceilometer.tests import base as test_base +from ceilometer.tests.hardware.inspector import base + +Base = base.InspectorBaseTest + + +class FakeMac(object): + def __init__(self): + self.val = "0x%s" % Base.network[0][0].mac + + def prettyPrint(self): + return str(self.val) + +ins = snmp.SNMPInspector +GETCMD_MAP = { + ins._cpu_1_min_load_oid: (None, + None, + 0, + [('', + Base.cpu[0].cpu_1_min, + )], + ), + ins._cpu_5_min_load_oid: (None, + None, + 0, + [('', + Base.cpu[0].cpu_5_min, + )], + ), + ins._cpu_15_min_load_oid: (None, + None, + 0, + [('', + Base.cpu[0].cpu_15_min, + )], + ), + ins._memory_total_oid: (None, + None, + 0, + [('', + Base.memory[0].total, + )], + ), + ins._memory_used_oid: (None, + None, + 0, + [('', + Base.memory[0].used, + )], + ), + ins._disk_path_oid + '.1': (None, + None, + 0, + [('', + Base.diskspace[0][0].path, + )], + ), + ins._disk_device_oid + '.1': (None, + None, + 0, + [('', + Base.diskspace[0][0].device, + )], + ), + ins._disk_size_oid + '.1': (None, + None, + 0, + [('', + Base.diskspace[0][1].size, + )], + ), + ins._disk_used_oid + '.1': (None, + None, + 0, + [('', + Base.diskspace[0][1].used, + )], + ), + ins._disk_path_oid + '.2': (None, + None, + 0, + [('', + Base.diskspace[1][0].path, + )], + ), + ins._disk_device_oid + '.2': (None, + None, + 0, + [('', + Base.diskspace[1][0].device, + )], + ), + ins._disk_size_oid + '.2': (None, + None, + 0, + [('', + Base.diskspace[1][1].size, + )], + ), + ins._disk_used_oid + '.2': (None, + None, + 0, + [('', + Base.diskspace[1][1].used, + )], + ), + ins._interface_name_oid + '.1': (None, + None, + 0, + [('', + Base.network[0][0].name, + )], + ), + ins._interface_mac_oid + '.1': (None, + None, + 0, + [('', + FakeMac(), + )], + ), + ins._interface_bandwidth_oid + '.1': (None, + None, + 0, + [('', + Base.network[0][1].bandwidth * 8, + )], + ), + ins._interface_received_oid + '.1': (None, + None, + 0, + [('', + Base.network[0][1].rx_bytes, + )], + ), + ins._interface_transmitted_oid + '.1': (None, + None, + 0, + [('', + Base.network[0][1].tx_bytes, + )], + ), + ins._interface_error_oid + '.1': (None, + None, + 0, + [('', + Base.network[0][1].error, + )], + ), +} + +NEXTCMD_MAP = { + ins._disk_index_oid: (None, + None, + 0, + [[('1.3.6.1.4.1.2021.9.1.1.1', 1)], + [('1.3.6.1.4.1.2021.9.1.1.2', 2)]]), + ins._interface_index_oid: (None, + None, + 0, + [[('1.3.6.1.2.1.2.2.1.1.1', 1)], + ]), + ins._interface_ip_oid: (None, + None, + 0, + [[('1.3.6.1.2.1.4.20.1.2.10.0.0.1', + 1)], + ]), +} + + +def faux_getCmd(authData, transportTarget, oid): + try: + return GETCMD_MAP[oid] + except KeyError: + return ("faux_getCmd Error", None, 0, []) + + +def faux_nextCmd(authData, transportTarget, oid): + try: + return NEXTCMD_MAP[oid] + except KeyError: + return ("faux_nextCmd Error", None, 0, []) + + +class TestSNMPInspector(Base, test_base.BaseTestCase): + def setUp(self): + super(TestSNMPInspector, self).setUp() + self.inspector = snmp.SNMPInspector() + self.host = network_utils.urlsplit("snmp://localhost") + self.useFixture(mockpatch.PatchObject( + self.inspector._cmdGen, 'getCmd', new=faux_getCmd)) + self.useFixture(mockpatch.PatchObject( + self.inspector._cmdGen, 'nextCmd', new=faux_nextCmd)) + + def test_get_security_name(self): + self.assertEqual(self.inspector._get_security_name(self.host), + self.inspector._security_name) + host2 = network_utils.urlsplit("snmp://foo:80?security_name=fake") + self.assertEqual(self.inspector._get_security_name(host2), + 'fake') + + def test_get_cmd_error(self): + self.useFixture(mockpatch.PatchObject( + self.inspector, '_memory_total_oid', new='failure')) + + def get_list(func, *args, **kwargs): + return list(func(*args, **kwargs)) + + self.assertRaises(snmp.SNMPException, + get_list, + self.inspector.inspect_memory, + self.host) diff --git a/requirements.txt b/requirements.txt index 2df43d1df..b747d3ec0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,6 +16,7 @@ oslo.config>=1.2.0 pbr>=0.6,<1.0 pecan>=0.4.5 pymongo>=2.4 +pysnmp>=4.2.1,<5.0.0 python-ceilometerclient>=1.0.6 python-glanceclient>=0.9.0 python-keystoneclient>=0.6.0 diff --git a/setup.cfg b/setup.cfg index 8e0cacfe0..d839ccd41 100644 --- a/setup.cfg +++ b/setup.cfg @@ -126,6 +126,9 @@ ceilometer.compute.virt = libvirt = ceilometer.compute.virt.libvirt.inspector:LibvirtInspector hyperv = ceilometer.compute.virt.hyperv.inspector:HyperVInspector +ceilometer.hardware.inspectors = + snmp = ceilometer.hardware.inspector.snmp:SNMPInspector + ceilometer.transformer = accumulator = ceilometer.transformer.accumulator:TransformerAccumulator unit_conversion = ceilometer.transformer.conversions:ScalingTransformer