#
# Module to probe Hardware info from the system
#
# Copyright (c) 2010 Red Hat, Inc.
#
# Authors: Pradeep Kilambi <pkilambi@redhat.com>
#
# This software is licensed to you under the GNU General Public License,
# version 2 (GPLv2). There is NO WARRANTY for this software, express or
# implied, including the implied warranties of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
# along with this software; if not, see
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
#
# Red Hat trademarks are not licensed under GPLv2. No permission is
# granted to use or replicate Red Hat trademarks that are incorporated
# in this software or its documentation.
#

import commands
import ethtool
import gettext
import logging
import os
import platform
import re
import socket
from subprocess import PIPE, Popen
import sys

_ = gettext.gettext

log = logging.getLogger('rhsm-app.' + __name__)


# Exception classes used by this module.
# from later versions of subprocess, but not there on 2.4, so include our version
class CalledProcessError(Exception):
    """This exception is raised when a process run by check_call() or
    check_output() returns a non-zero exit status.
    The exit status will be stored in the returncode attribute;
    check_output() will also store the output in the output attribute.
    """
    def __init__(self, returncode, cmd, output=None):
        self.returncode = returncode
        self.cmd = cmd
        self.output = output

    def __str__(self):
        return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)


class ClassicCheck:

    def is_registered_with_classic(self):
        try:
            sys.path.append('/usr/share/rhn')
            from up2date_client import up2dateAuth
        except ImportError:
            return False

        return up2dateAuth.getSystemId() is not None


# take a string like '1-4' and returns a list of
# ints like [1,2,3,4]
# 31-37 return [31,32,33,34,35,36,37]
def parse_range(range_str):
    range_list = range_str.split('-')
    start = int(range_list[0])
    end = int(range_list[-1])

    return range(start, end + 1)


# util to total up the values represented by a cpu siblings list
# ala /sys/devices/cpu/cpu0/topology/core_siblings_list
#
# which can be a comma seperated list of ranges
#  1,2,3,4
#  1-2, 4-6, 8-10, 12-14
#
def gather_entries(entries_string):
    entries = []
    entry_parts = entries_string.split(',')
    for entry_part in entry_parts:
        # return a list of enumerated items
        entry_range = parse_range(entry_part)
        for entry in entry_range:
            entries.append(entry)
    return entries


class GenericPlatformSpecificInfoProvider(object):
    """Default provider for platform without a specific platform info provider.

    ie, all platforms except those with DMI (ie, intel platforms)"""
    def __init__(self, hardware_info, dump_file=None):
        self.info = {}

    @staticmethod
    def log_warnings():
        pass


class Hardware:

    def __init__(self, prefix=None, testing=None):
        self.allhw = {}
        # prefix to look for /sys, for testing
        self.prefix = prefix or ''
        self.testing = testing or False

        self.no_dmi_arches = ['s390x', 'ppc64', 'ppc64le', 'ppc']
        # we need this so we can decide which of the
        # arch specific code bases to follow
        self.arch = self.get_arch()

        self.platform_specific_info_provider = self.get_platform_specific_info_provider()

    def get_uname_info(self):

        uname_data = os.uname()
        uname_keys = ('uname.sysname', 'uname.nodename', 'uname.release',
                      'uname.version', 'uname.machine')
        self.unameinfo = dict(zip(uname_keys, uname_data))
        self.allhw.update(self.unameinfo)
        return self.unameinfo

    def get_release_info(self):
        distro_keys = ('distribution.name', 'distribution.version',
                       'distribution.id', 'distribution.version.modifier')
        self.releaseinfo = dict(filter(lambda (key, value): value,
            zip(distro_keys, self.get_distribution())))
        self.allhw.update(self.releaseinfo)
        return self.releaseinfo

    def _open_release(self, filename):
        return open(filename, 'r')

    # Determine which rough arch we are, so we know where to
    # look for hardware info. Also support a test mode that
    # specifies the arch
    def get_arch(self, prefix=None, testing=None):
        if self.testing and self.prefix:
            arch_file = "%s/arch" % self.prefix
            if os.access(arch_file, os.R_OK):
                try:
                    f = open(arch_file, 'r')
                except IOError:
                    return platform.machine()
                buf = f.read().strip()
                f.close()
                return buf
            return platform.machine()
        return platform.machine()

    def get_platform_specific_info_provider(self):
        """
        Return a class that can be used to get firmware info specific to
        this systems platform.

        ie, DmiFirmwareInfoProvider on intel platforms, and a EmptyInfo otherwise.
        """
        # we could potential consider /proc/sysinfo as a FirmwareInfoProvider
        # but at the moment, it is just firmware/dmi stuff.
        if self.arch in self.no_dmi_arches:
            log.debug("Not looking for DMI info since it is not available on '%s'" % self.arch)
            platform_specific_info_provider = GenericPlatformSpecificInfoProvider
        else:
            try:
                from subscription_manager import dmiinfo
                platform_specific_info_provider = dmiinfo.DmiFirmwareInfoProvider
            except ImportError:
                log.warn("Unable to load dmidecode module. No DMI info will be collected")
                platform_specific_info_provider = GenericPlatformSpecificInfoProvider

        return platform_specific_info_provider

    def get_platform_specific_info(self):
        """Read and parse data that comes from platform specific interfaces.

        This is only dmi/smbios data for now (which isn't on ppc/s390).
        """

        if self.testing and self.prefix:
            dump_file = "%s/dmi.dump" % self.prefix
            platform_info = self.platform_specific_info_provider(self.allhw, dump_file=dump_file).info
        else:
            platform_info = self.platform_specific_info_provider(self.allhw).info

        self.allhw.update(platform_info)

    # this version os very RHEL/Fedora specific...
    def get_distribution(self):

        version = 'Unknown'
        distname = 'Unknown'
        dist_id = 'Unknown'
        version_modifier = ''

        if os.path.exists('/etc/os-release'):
            f = open('/etc/os-release', 'r')
            os_release = f.readlines()
            f.close()
            data = {'PRETTY_NAME': 'Unknown',
                    'NAME': distname,
                    'ID': 'Unknown',
                    'VERSION': dist_id,
                    'VERSION_ID': version,
                    'CPE_NAME': 'Unknown'}
            for line in os_release:
                split = map(lambda piece: piece.strip('"\n '), line.split('='))
                if len(split) != 2:
                    continue
                data[split[0]] = split[1]

            version = data['VERSION_ID']
            distname = data['NAME']
            dist_id = data['VERSION']
            dist_id_search = re.search('\((.*?)\)', dist_id)
            if dist_id_search:
                dist_id = dist_id_search.group(1)
            # Split on ':' that is not preceded by '\'
            vers_mod_data = re.split('(?<!\\\):', data['CPE_NAME'])
            if len(vers_mod_data) >= 6:
                version_modifier = vers_mod_data[5].lower().replace('\\:', ':')

        elif os.path.exists('/etc/redhat-release'):
            # from platform.py from python2.
            _lsb_release_version = re.compile(r'(.+)'
                                              ' release '
                                              '([\d.]+)'
                                              '\s*(?!\()(\S*)\s*'
                                              '[^(]*(?:\((.+)\))?')
            f = self._open_release('/etc/redhat-release')
            firstline = f.readline()
            f.close()

            m = _lsb_release_version.match(firstline)

            if m is not None:
                (distname, version, tmp_modifier, dist_id) = tuple(m.groups())
                if tmp_modifier:
                    version_modifier = tmp_modifier.lower()

        elif hasattr(platform, 'linux_distribution'):
            (distname, version, dist_id) = platform.linux_distribution()
            version_modifier = 'Unknown'

        return distname, version, dist_id, version_modifier

    def get_mem_info(self):
        self.meminfo = {}

        # most of this mem info changes constantly, which makes decding
        # when to update facts painful, so lets try to just collect the
        # useful bits

        useful = ["MemTotal", "SwapTotal"]
        try:
            parser = re.compile(r'^(?P<key>\S*):\s*(?P<value>\d*)\s*kB')
            memdata = open('/proc/meminfo')
            for info in memdata:
                match = parser.match(info)
                if not match:
                    continue
                key, value = match.groups(['key', 'value'])
                if key in useful:
                    nkey = '.'.join(["memory", key.lower()])
                    self.meminfo[nkey] = "%s" % int(value)
        except Exception, e:
            print _("Error reading system memory information:"), e
        self.allhw.update(self.meminfo)
        return self.meminfo

    def count_cpumask_entries(self, cpu, field):
        try:
            f = open("%s/topology/%s" % (cpu, field), 'r')
        except IOError:
            return None

        # ia64 entries seem to be null padded, or perhaps
        # that's a collection error
        # FIXME
        entries = f.read().rstrip('\n\x00')
        f.close()
        # these fields can exist, but be empty. For example,
        # thread_siblings_list from s390x-rhel64-zvm-2cpu-has-topo
        # test data

        if len(entries):
            cpumask_entries = gather_entries(entries)
            return len(cpumask_entries)
        # that field was empty
        return None

    # replace/add with getting CPU Totals for s390x
    def _parse_s390x_sysinfo_topology(self, cpu_count, sysinfo):
        # to quote lscpu.c:
        # CPU Topology SW:      0 0 0 4 6 4
        # /* s390 detects its cpu topology via /proc/sysinfo, if present.
        # * Using simply the cpu topology masks in sysfs will not give
        # * usable results since everything is virtualized. E.g.
        # * virtual core 0 may have only 1 cpu, but virtual core 2 may
        # * five cpus.
        # * If the cpu topology is not exported (e.g. 2nd level guest)
        # * fall back to old calculation scheme.
        # */
        for line in sysinfo:
            if line.startswith("CPU Topology SW:"):
                parts = line.split(':', 1)
                s390_topo_str = parts[1]
                topo_parts = s390_topo_str.split()

                # indexes 3/4/5 being books/sockets_per_book,
                # and cores_per_socket based on lscpu.c
                book_count = int(topo_parts[3])
                sockets_per_book = int(topo_parts[4])
                cores_per_socket = int(topo_parts[5])

                socket_count = book_count * sockets_per_book
                cores_count = socket_count * cores_per_socket

                return {'socket_count': socket_count,
                        'cores_count': cores_count,
                        'book_count': book_count,
                        'sockets_per_book': sockets_per_book,
                        'cores_per_socket': cores_per_socket}
        log.debug("Looking for 'CPU Topology SW' in sysinfo, but it was not found")
        return None

    def has_s390x_sysinfo(self, proc_sysinfo):
        if not os.access(proc_sysinfo, os.R_OK):
            return False

        return True

    def read_s390x_sysinfo(self, cpu_count, proc_sysinfo):
        lines = []
        try:
            f = open(proc_sysinfo, 'r')
        except IOError:
            return lines

        lines = f.readlines()
        f.close()
        return lines

    def read_physical_id(self, cpu_file):
        try:
            f = open("%s/physical_id" % cpu_file, 'r')
        except IOError:
            return None

        buf = f.read().strip()
        f.close()
        return buf

    def _ppc64_fallback(self, cpu_files):

        # ppc64, particular POWER5/POWER6 machines, show almost
        # no cpu information on rhel5. There is a "physical_id"
        # associated with each cpu that seems to map to a
        # cpu, in a socket
        log.debug("trying ppc64 specific cpu topology detection")
        # try to find cpuN/physical_id
        physical_ids = set()
        for cpu_file in cpu_files:
            physical_id = self.read_physical_id(cpu_file)
            # offline cpu's show physical id of -1. Normally
            # we count all present cpu's even if offline, but
            # in this case, we can't get any cpu info from the
            # cpu since it is offline, so don't count it
            if physical_id != '-1':
                physical_ids.add(physical_id)

        if physical_ids:
            # For rhel6 or newer, we have more cpu topology info
            # exposed by the kernel which will override this
            socket_count = len(physical_ids)
            # add marker here so we know we fail back to this
            log.debug("Using /sys/devices/system/cpu/cpu*/physical_id for cpu info on ppc64")
            return socket_count

        return None

    def check_for_cpu_topo(self, cpu_topo_dir):
        return os.access(cpu_topo_dir, os.R_OK)

    def get_cpu_info(self):
        self.cpuinfo = {}
        # we also have cpufreq, etc in this dir, so match just the numbs
        cpu_re = r'cpu([0-9]+$)'

        cpu_files = []
        sys_cpu_path = self.prefix + "/sys/devices/system/cpu/"
        for cpu in os.listdir(sys_cpu_path):
            if re.match(cpu_re, cpu):
                cpu_topo_dir = os.path.join(sys_cpu_path, cpu, "topology")

                # see rhbz#1070908
                # ppc64 machines running on LPARs will add
                # a sys cpu entry for every cpu thread on the
                # physical machine, regardless of how many are
                # allocated to the LPAR. This throws off the cpu
                # thread count, which throws off the cpu socket count.
                # The entries for the unallocated or offline cpus
                # do not have topology info however.
                # So, skip sys cpu entries without topology info.
                #
                # NOTE: this assumes RHEL6+, prior to rhel5, on
                # some arches like ppc and s390, there is no topology
                # info ever, so this will break.
                if self.check_for_cpu_topo(cpu_topo_dir):
                    cpu_files.append("%s/%s" % (sys_cpu_path, cpu))

        # for systems with no cpus
        if not cpu_files:
            return self.cpuinfo

        cpu_count = len(cpu_files)

        # see if we have a /proc/sysinfo ala s390, if so
        # prefer that info
        proc_sysinfo = self.prefix + "/proc/sysinfo"
        has_sysinfo = self.has_s390x_sysinfo(proc_sysinfo)

        # s390x can have cpu 'books'
        books = False

        cores_per_socket = None

        # assume each socket has the same number of cores, and
        # each core has the same number of threads.
        #
        # This is not actually true sometimes... *cough*s390x*cough*
        # but lscpu makes the same assumption

        threads_per_core = self.count_cpumask_entries(cpu_files[0],
                                                      'thread_siblings_list')
        cores_per_cpu = self.count_cpumask_entries(cpu_files[0],
                                                   'core_siblings_list')

        # if we find valid values in cpu/cpuN/topology/*siblings_list
        # sometimes it's not there, particularly on rhel5
        if threads_per_core and cores_per_cpu:
            cores_per_socket = cores_per_cpu / threads_per_core
            self.cpuinfo["cpu.topology_source"] = "kernel /sys cpu sibling lists"

            # rhel6 s390x can have /sys cpu topo, but we can't make assumption
            # about it being evenly distributed, so if we also have topo info
            # in sysinfo, prefer that
            if self.arch == "s390x" and has_sysinfo:
                # for s390x on lpar, try to see if /proc/sysinfo has any
                # topo info
                log.debug("/proc/sysinfo found, attempting to gather cpu topology info")
                sysinfo_lines = self.read_s390x_sysinfo(cpu_count, proc_sysinfo)
                if sysinfo_lines:
                    sysinfo = self._parse_s390x_sysinfo_topology(cpu_count, sysinfo_lines)

                    # verify the sysinfo has system level virt info
                    if sysinfo:
                        self.cpuinfo["cpu.topology_source"] = "s390x sysinfo"
                        socket_count = sysinfo['socket_count']
                        book_count = sysinfo['book_count']
                        sockets_per_book = sysinfo['sockets_per_book']
                        cores_per_socket = sysinfo['cores_per_socket']
                        threads_per_core = 1

                        # we can have a mismatch between /sys and /sysinfo. We
                        # defer to sysinfo in this case even for cpu_count
        #                cpu_count = sysinfo['cores_count'] * threads_per_core
                        books = True

        else:
            # we have found no valid socket information, I only know
            # the number of cpu's, but no threads, no cores, no sockets
            log.debug("No cpu socket information found")

            # how do we get here?
            #   no cpu topology info, ala s390x on rhel5,
            #   no sysinfo topology info, ala s390x with zvm on rhel5
            # we have no great topo info here,
            # assume each cpu thread = 1 core = 1 socket
            threads_per_core = 1
            cores_per_cpu = 1
            cores_per_socket = 1
            socket_count = None

            # lets try some arch/platform specific approaches
            if self.arch == "ppc64":
                socket_count = self._ppc64_fallback(cpu_files)

                if socket_count:
                    log.debug("Using ppc64 cpu physical id for cpu topology info")
                    self.cpuinfo["cpu.topology_source"] = "ppc64 physical_package_id"

            else:
                # all of our usual methods failed us...
                log.debug("No cpu socket info found for real or virtual hardware")
                # so we can track if we get this far
                self.cpuinfo["cpu.topology_source"] = "fallback one socket"
                socket_count = cpu_count

            # for some odd cases where there are offline ppc64 cpu's,
            # this can end up not being a whole number...
            cores_per_socket = cpu_count / socket_count

        if cores_per_socket and threads_per_core:
            # for s390x with sysinfo topo, we use the sysinfo numbers except
            # for cpu_count, which takes offline cpus into account. This is
            # mostly just to match lscpu behaviour here
            if self.cpuinfo["cpu.topology_source"] != "s390x sysinfo":
                socket_count = cpu_count / cores_per_socket / threads_per_core

        # s390 etc
        # for s390, socket calculations are per book, and we can have multiple
        # books, so multiply socket count by book count
        # see if we are on a s390 with book info
        # all s390 platforms show book siblings, even the ones that also
        # show sysinfo (lpar)... Except on rhel5, where there is no
        # cpu topology info with lpar
        #
        # if we got book info from sysinfo, prefer it
        book_siblings_per_cpu = None
        if not books:
            book_siblings_per_cpu = self.count_cpumask_entries(cpu_files[0],
                                                            'book_siblings_list')
            if book_siblings_per_cpu:
                book_count = cpu_count / book_siblings_per_cpu
                sockets_per_book = book_count / socket_count
                self.cpuinfo["cpu.topology_source"] = "s390 book_siblings_list"
                books = True

        # we should always know this...
        self.cpuinfo["cpu.cpu(s)"] = cpu_count

        # these may be unknown...
        if socket_count:
            self.cpuinfo['cpu.cpu_socket(s)'] = socket_count
        if cores_per_socket:
            self.cpuinfo['cpu.core(s)_per_socket'] = cores_per_socket
        if threads_per_core:
            self.cpuinfo["cpu.thread(s)_per_core"] = threads_per_core

        if book_siblings_per_cpu:
            self.cpuinfo["cpu.book(s)_per_cpu"] = book_siblings_per_cpu

        if books:
            self.cpuinfo["cpu.socket(s)_per_book"] = sockets_per_book
            self.cpuinfo["cpu.book(s)"] = book_count

        log.debug("cpu info: %s" % self.cpuinfo)
        self.allhw.update(self.cpuinfo)
        return self.cpuinfo

    def get_ls_cpu_info(self):
        # if we have `lscpu`, let's use it for facts as well, under
        # the `lscpu` name space
        if not os.access('/usr/bin/lscpu', os.R_OK):
            return

        self.lscpuinfo = {}
        # let us specify a test dir of /sys info for testing
        ls_cpu_path = 'LANG=en_US.UTF-8 /usr/bin/lscpu'
        ls_cpu_cmd = ls_cpu_path

        if self.testing:
            ls_cpu_cmd = "%s -s %s" % (ls_cpu_cmd, self.prefix)
        try:
            cpudata = commands.getstatusoutput(ls_cpu_cmd)[-1].split('\n')
            for info in cpudata:
                try:
                    key, value = info.split(":")
                    nkey = '.'.join(["lscpu", key.lower().strip().replace(" ", "_")])
                    self.lscpuinfo[nkey] = "%s" % value.strip()
                except ValueError:
                    # sometimes lscpu outputs weird things. Or fails.
                    #
                    pass
        except Exception, e:
            print _("Error reading system CPU information:"), e
        self.allhw.update(self.lscpuinfo)
        return self.lscpuinfo

    def get_network_info(self):
        self.netinfo = {}
        try:
            host = socket.gethostname()
            self.netinfo['network.hostname'] = host

            try:
                info = socket.getaddrinfo(host, None, socket.AF_INET, socket.SOCK_STREAM)
                ip_list = set([x[4][0] for x in info])
                self.netinfo['network.ipv4_address'] = ', '.join(ip_list)
            except Exception:
                self.netinfo['network.ipv4_address'] = "127.0.0.1"

            try:
                info = socket.getaddrinfo(host, None, socket.AF_INET6, socket.SOCK_STREAM)
                ip_list = set([x[4][0] for x in info])
                self.netinfo['network.ipv6_address'] = ', '.join(ip_list)
            except Exception:
                self.netinfo['network.ipv6_address'] = "::1"

        except Exception, e:
            print _("Error reading networking information:"), e
        self.allhw.update(self.netinfo)
        return self.netinfo

    def _should_get_mac_address(self, device):
        if (device.startswith('sit') or device.startswith('lo')):
            return False
        return True

    def get_network_interfaces(self):
        netinfdict = {}
        old_ipv4_metakeys = ['ipv4_address', 'ipv4_netmask', 'ipv4_broadcast']
        ipv4_metakeys = ['address', 'netmask', 'broadcast']
        ipv6_metakeys = ['address', 'netmask']
        try:
            interfaces_info = ethtool.get_interfaces_info(ethtool.get_devices())
            for info in interfaces_info:
                master = None
                mac_address = info.mac_address
                device = info.device
                # Omit mac addresses for sit and lo device types. See BZ838123
                # mac address are per interface, not per address
                if self._should_get_mac_address(device):
                    key = '.'.join(['net.interface', device, 'mac_address'])
                    netinfdict[key] = mac_address

                # all of our supported versions of python-ethtool support
                # get_ipv6_addresses
                for addr in info.get_ipv6_addresses():
                    # ethtool returns a different scope for "public" IPv6 addresses
                    # on different versions of RHEL.  EL5 is "global", while EL6 is
                    # "universe".  Make them consistent.
                    scope = addr.scope
                    if scope == 'universe':
                        scope = 'global'

                    # FIXME: this doesn't support multiple addresses per interface
                    # (it finds them, but collides on the key name and loses all
                    # but the last write). See bz #874735
                    for mkey in ipv6_metakeys:
                        key = '.'.join(['net.interface', info.device, 'ipv6_%s' % (mkey), scope])
                        # we could specify a default here... that could hide
                        # api breakage though and unit testing hw detect is... meh
                        attr = getattr(addr, mkey) or 'Unknown'
                        netinfdict[key] = attr

                # However, old version of python-ethtool do not support
                # get_ipv4_address
                #
                # python-ethtool's api changed between rhel6.3 and rhel6.4
                # (0.6-1.el6 to 0.6-2.el6)
                # (without revving the upstream version... bad python-ethtool!)
                # note that 0.6-5.el5 (from rhel5.9) has the old api
                #
                # previously, we got the 'ipv4_address' from the etherinfo object
                # directly. In the new api, that isn't exposed, so we get the list
                # of addresses on the interface, and populate the info from there.
                #
                # That api change as to address bz #759150. The bug there was that
                # python-ethtool only showed one ip address per interface. To
                # accomdate the finer grained info, the api changed...
                #
                # FIXME: see FIXME for get_ipv6_address, we don't record multiple
                # addresses per interface
                if hasattr(info, 'get_ipv4_addresses'):
                    for addr in info.get_ipv4_addresses():
                        for mkey in ipv4_metakeys:
                            # append 'ipv4_' to match the older interface and keeps facts
                            # consistent
                            key = '.'.join(['net.interface', info.device, 'ipv4_%s' % (mkey)])
                            attr = getattr(addr, mkey) or 'Unknown'
                            netinfdict[key] = attr
                # check to see if we are actually an ipv4 interface
                elif hasattr(info, 'ipv4_address'):
                    for mkey in old_ipv4_metakeys:
                        key = '.'.join(['net.interface', device, mkey])
                        attr = getattr(info, mkey) or 'Unknown'
                        netinfdict[key] = attr
                # otherwise we are ipv6 and we handled that already

                # bonded slave devices can have their hwaddr changed
                #
                # "master" here refers to the slave's master device.
                # If we find a master link, we are a  slave, and we need
                # to check the /proc/net/bonding info to see what the
                # "permanent" hw address are for this slave
                try:
                    master = os.readlink('/sys/class/net/%s/master' % info.device)
                #FIXME
                except Exception:
                    master = None

                if master:
                    master_interface = os.path.basename(master)
                    permanent_mac_addr = self._get_slave_hwaddr(master_interface, info.device)
                    key = '.'.join(['net.interface', info.device, "permanent_mac_address"])
                    netinfdict[key] = permanent_mac_addr

        except Exception:
            print _("Error reading network interface information:"), sys.exc_type
        self.allhw.update(netinfdict)
        return netinfdict

    # from rhn-client-tools  hardware.py
    # see bz#785666
    def _get_slave_hwaddr(self, master, slave):
        hwaddr = ""
        try:
            bonding = open('/proc/net/bonding/%s' % master, "r")
        except:
            return hwaddr

        slave_found = False
        for line in bonding.readlines():
            if slave_found and line.find("Permanent HW addr: ") != -1:
                hwaddr = line.split()[3].upper()
                break

            if line.find("Slave Interface: ") != -1:
                ifname = line.split()[2]
                if ifname == slave:
                    slave_found = True

        bonding.close()
        return hwaddr

    def get_virt_info(self):
        virt_dict = {}

        try:
            host_type = self._get_output('virt-what')
            # BZ1018807 xen can report xen and xen-hvm.
            # Force a single line
            host_type = ", ".join(host_type.splitlines())

            # If this is blank, then not a guest
            virt_dict['virt.is_guest'] = bool(host_type)
            if bool(host_type):
                virt_dict['virt.is_guest'] = True
                virt_dict['virt.host_type'] = host_type
            else:
                virt_dict['virt.is_guest'] = False
                virt_dict['virt.host_type'] = "Not Applicable"
        # TODO:  Should this only catch OSErrors?
        except Exception, e:
            # Otherwise there was an error running virt-what - who knows
            log.exception(e)
            virt_dict['virt.is_guest'] = 'Unknown'

        # xen dom0 is a guest for virt-what's purposes, but is a host for
        # our purposes. Adjust is_guest accordingly. (#757697)
        try:
            if virt_dict['virt.host_type'].find('dom0') > -1:
                virt_dict['virt.is_guest'] = False
        except KeyError:
            # if host_type is not defined, do nothing (#768397)
            pass

        log.info("virt.is_guest: %s" % virt_dict.get('virt.is_guest', 'Not Set'))
        log.info("virt.host_type: %s" % virt_dict.get('virt.host_type', 'Not Set'))

        self.allhw.update(virt_dict)
        return virt_dict

    def _get_output(self, cmd):
        log.debug("Running '%s'" % cmd)
        process = Popen([cmd], stdout=PIPE, stderr=PIPE)
        (std_output, std_error) = process.communicate()

        log.debug("%s stdout: %s" % (cmd, std_output))
        log.debug("%s stderr: %s" % (cmd, std_error))

        output = std_output.strip()

        returncode = process.poll()
        if returncode:
            raise CalledProcessError(returncode,
                                     cmd,
                                     output=output)

        return output

    def get_virt_uuid(self):
        """
        Given a populated fact list, add on a virt.uuid fact if appropriate.
        Partially adapted from Spacewalk's rhnreg.py, example hardware reporting
        found in virt-what tests
        """
        no_uuid_platforms = ['powervm_lx86', 'xen-dom0', 'ibm_systemz']

        self.allhw['virt.uuid'] = 'Unknown'

        try:
            for v in no_uuid_platforms:
                if self.allhw['virt.host_type'].find(v) > -1:
                    raise Exception(_("Virtualization platform does not support UUIDs"))
        except Exception, e:
            log.warn(_("Error finding UUID: %s"), e)
            return  # nothing more to do

        #most virt platforms record UUID via DMI/SMBIOS info.
        if 'dmi.system.uuid' in self.allhw:
            self.allhw['virt.uuid'] = self.allhw['dmi.system.uuid']

        #potentially override DMI-determined UUID with
        #what is on the file system (xen para-virt)
        try:
            uuid_file = open('/sys/hypervisor/uuid', 'r')
            uuid = uuid_file.read()
            uuid_file.close()
            self.allhw['virt.uuid'] = uuid.rstrip("\r\n")
        except IOError:
            pass

        log.info("virt.uuid: %s" % self.allhw.get('virt.uuid', 'Not Set'))

    def log_platform_firmware_warnings(self):
        "Log any warnings from firmware info gather,and/or clear them."
        self.get_platform_specific_info_provider().log_warnings()

    def get_all(self):
        hardware_methods = [self.get_uname_info,
                            self.get_release_info,
                            self.get_mem_info,
                            self.get_cpu_info,
                            self.get_ls_cpu_info,
                            self.get_network_info,
                            self.get_network_interfaces,
                            self.get_virt_info,
                            # this has to happen after everything else, since
                            # it expects to check virt and processor info
                            self.get_platform_specific_info]
        # try each hardware method, and try/except around, since
        # these tend to be fragile
        for hardware_method in hardware_methods:
            try:
                hardware_method()
            except Exception, e:
                log.warn("%s" % hardware_method)
                log.warn("Hardware detection failed: %s" % e)

        #we need to know the DMI info and VirtInfo before determining UUID.
        #Thus, we can't figure it out within the main data collection loop.
        if self.allhw.get('virt.is_guest'):
            self.get_virt_uuid()

        return self.allhw


if __name__ == '__main__':
    _LIBPATH = "/usr/share/rhsm"
    # add to the path if need be
    if _LIBPATH not in sys.path:
        sys.path.append(_LIBPATH)

    from subscription_manager import logutil
    logutil.init_logger()

    hw = Hardware(prefix=sys.argv[1], testing=True)

    if len(sys.argv) > 1:
        hw.prefix = sys.argv[1]
        hw.testing = True
    hw_dict = hw.get_all()

    # just show the facts collected, unless we specify data dir and well,
    # anything else
    if len(sys.argv) > 2:
        for hkey, hvalue in sorted(hw_dict.items()):
            print "'%s' : '%s'" % (hkey, hvalue)

    if not hw.testing:
        sys.exit(0)

    # verify the cpu socket info collection we use for rhel5 matches lscpu
    cpu_items = [('cpu.core(s)_per_socket', 'lscpu.core(s)_per_socket'),
                 ('cpu.cpu(s)', 'lscpu.cpu(s)'),
                 # NOTE: the substring is different for these two folks...
                 # FIXME: follow up to see if this has changed
                 ('cpu.cpu_socket(s)', 'lscpu.socket(s)'),
                 ('cpu.book(s)', 'lscpu.book(s)'),
                 ('cpu.thread(s)_per_core', 'lscpu.thread(s)_per_core'),
                 ('cpu.socket(s)_per_book', 'lscpu.socket(s)_per_book')]
    failed = False
    failed_list = []
    for cpu_item in cpu_items:
        value_0 = int(hw_dict.get(cpu_item[0], -1))
        value_1 = int(hw_dict.get(cpu_item[1], -1))

        #print "%s/%s: %s %s" % (cpu_item[0], cpu_item[1], value_0, value_1)

        if value_0 != value_1 and ((value_0 != -1) and (value_1 != -1)):
            failed_list.append((cpu_item[0], cpu_item[1], value_0, value_1))

    must_haves = ['cpu.cpu_socket(s)', 'cpu.cpu(s)', 'cpu.core(s)_per_socket', 'cpu.thread(s)_per_core']
    missing_set = set(must_haves).difference(set(hw_dict))

    if failed:
        print "cpu detection error"
    for failed in failed_list:
        print "The values %s %s do not match (|%s| != |%s|)" % (failed[0], failed[1],
                                                                failed[2], failed[3])
    if missing_set:
        for missing in missing_set:
            print "cpu info fact: %s was missing" % missing

    if failed:
        sys.exit(1)
