#! /usr/bin/env python

# Copyright C 2008-2013 by Isilon Systems LLC. All rights not expressly
# granted herein are reserved.
# Created in the United States of America.
# 
# This digital media is protected by U.S. and international copyright and
# intellectual property laws. Unless otherwise specified, all information
# and screens appearing as part of this digital medium, including
# software, services, documents, text, images, icons, and logos design;
# the selection, assembly, arrangement, and design thereof; and the code
# that enables its presentation, are the sole property of Isilon Systems,
# Inc.
# 
# Except as otherwise required by applicable law, no part of any
# copyrighted material may be reproduced, modified, translated,
# distributed, published, or transmitted by any means, electronic or
# mechanical, for any purpose, without the prior written consent of Isilon
# Systems. 
#  
# Isilon and Isilon Systems are registered trademarks of Isilon Systems,
# Inc.  Legion is a trademark of Isilon Systems, Inc.  All other
# trademarks and registered trademarks are the property of their
# respective holders.

import errno
import os
import re
import sys
import syslog
import filecmp
import getopt
import time
import pickle
import xml.dom.minidom as minidom
import xml.etree.ElementTree as ElementTree
import xml.parsers.expat as expat
import datetime
import glob
import uuid

import isi.error
import isi.hw.hal as hal
import isi.hw.version as hwver
import isi.hw.check.lib.filenames as filenames
import isi.gconfig as gconfig
import isi.app.lib.procs as procs
import isi.net.flexnet as flexnet

# Kernel environment (kenv) commands used for setting and unsetting
# hardware (HAL) overrides needed during early hardware probing
KENV_SET = 'kenv isilon_hwversions%s="%s"'
KENV_UNSET = 'kenv -u isilon_hwversions%s'

# Only 1 BOM per receipt is allowed
MAX_ALLOWABLE_BOMS_PER_RECEIPT = 1

# ISO-8601 date/timestamp formats
# For commonality and non-ambiguity, use UTC / GMT (time zone 'Z') for
# date attributes
ISO8601_UTC_DATE_TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
# This format is used in filenames (i.e. Soft-AVL and As-Built Records (ABRs))
ISO8601_UTC_DATE_TIMESTAMP_FILENAME_FORMAT = '%Y%m%d%H%M'
ISO8601_UTC_DATE_TIMESTAMP_REGEX = '[0-9]{12}'

# Currently supported format versions 
SUPPORTED_RECEIPT_FORMAT_VERSION = '1.2'
SUPPORTED_SOFTAVL_FORMAT_VERSION = '1.1'
SUPPORTED_ASBUILT_FORMAT_VERSION = '1.2'

# Backward compatible as far back as these versions
OLDEST_SUPPORTED_RECEIPT_FORMAT_VERSION = '1.2'
OLDEST_SUPPORTED_SOFTAVL_FORMAT_VERSION = '1.1'
OLDEST_SUPPORTED_ASBUILT_FORMAT_VERSION = '1.2'

# Mount points, directories, et al. 
ROOT_DIR = os.path.sep
GCONFIG_DIR = 'etc'
GCONFIG_SUBDIR = 'gconfig'
MFG_DIR = 'mfg'
TMP_DIR = 'tmp'
TMP_SUBDIR = str(uuid.uuid4())
MFG_PARTITION = os.path.join(ROOT_DIR, 'dev', 'mirror', 'mfg')

# Sub-directorys under the "mfg" root directory
RECEIPT_SUBDIR = 'receipts'
SOFTAVL_SUBDIR = 'softAVLs'
ASBUILT_SUBDIR = 'asbuilts'
PROBE_SUBDIR = 'probeconfigs'

# Most current version of each of these files will always be copied to
# these filenames in the MFG subdirectories.
BASE_PROBE_FILENAME = 'probe_config'
LEGACY_PROBE_FILENAME = '%s.gc' % BASE_PROBE_FILENAME
CURRENT_PROBE_LEVEL = hal.getSupportedCtoProbeconfigLevel()
CURRENT_PROBE_FILENAME = '%s.%d.gc' % (BASE_PROBE_FILENAME, CURRENT_PROBE_LEVEL)
PROBE_LEVELS = [ 'legacy' ]
for level in range(0, (CURRENT_PROBE_LEVEL + 1)):
    PROBE_LEVELS.append('%d' % level)
RECEIPT_FILENAME = 'receipt.xml'
SOFTAVL_FILENAME = 'softAVL.xml'
ASBUILT_FILENAME = 'asBuilt.xml'
HISTORY_FILENAME = 'history.txt'
INVENTORY_FILENAME = 'inventory.p'      # pickle file for cache

# Wildcards (filenames) used for clean up
GC_EXT = '.gc'
XML_EXT = '.xml'
XML_BACKUP_EXT = XML_EXT + '.[0-9]*'
GC_BACKUP_EXT = GC_EXT + '.[0-9]*'
XML_BACKUP_FILES = '*' + XML_BACKUP_EXT
GC_BACKUP_FILES = '*' + GC_BACKUP_EXT
XML_ORDER_FILES = 'ORD*' + XML_EXT
XML_BTS_FILES = 'BTS*' + XML_EXT
XML_SAP_FILES = '[A-Z]'*3 + '[0-9]'*10 + XML_EXT
XML_PROPEL_FILES =          '[0-9]'*10 + XML_EXT
SOFTAVL_PREFIX = 'Soft-AVL_'
XML_SOFTAVL_FILES = SOFTAVL_PREFIX + '*' + XML_EXT
XML_ASBUILT_FILES = 'asbuilt_*' + XML_EXT

# Maximum no. of backup files (copies) for any given file
BACKUP_FILE_LIMIT = 100

# Maximum no. of canonical asbuilt(ABR) files to keep
ASBUILT_FILE_LIMIT = 10

# Token for UI
FILETYPE_PROBE = 'probeconfig'
FILETYPE_RECEIPT = 'receipt'
FILETYPE_SOFTAVL = 'softavl'
FILETYPE_ASBUILT = 'asbuilt'
ITEM_PROBE = 'probeconfig'
ITEM_RECEIPT = 'receipt'
ITEM_SOFTAVL = 'softavl'
ITEM_INVENTORY = 'inventory'
ITEM_ASBUILT = 'asbuilt'
ITEM_HISTORY = 'history'

# Miscellaneous
NOT_AVAILABLE = 'N/A'
KILOBYTE = 1024
MEGABYTE = KILOBYTE * 1024
GIGABYTE = MEGABYTE * 1024
TERABYTE = GIGABYTE * 1024
PIDFILE = os.path.join(ROOT_DIR, 'var', 'run', 'isi_inventory_tool.pid')
SECRET_SHOWME_DEBUG_LEVEL = 100
VALID_PARTNUM_LEN = 11  # 9-digits + 2 dashes (i.e. NNN-NNNN-NN)

# Globals
_g_debugLevel = 0
_g_asBuiltItemIndex = 0 # For debug only
_g_mfgFlag = False
_g_baseChassisBurnInFlag = False
_g_qtyToFulfill = 0
_g_validFileTypes = [
    FILETYPE_PROBE,
    FILETYPE_RECEIPT,
    FILETYPE_SOFTAVL,
    FILETYPE_ASBUILT,
    ]

_g_validItemTypes = [
    ITEM_PROBE,
    ITEM_RECEIPT,
    ITEM_SOFTAVL,
    ITEM_INVENTORY,
    ITEM_ASBUILT,
    ITEM_HISTORY,
    ]

CHELSIO_PCICONF_STR = 'cxgbc|t5nex0'
CHELSIO_IFCONFIG_STR = 'cxl|cxgb'

def get_onboard_nic_bus():
    supported_mobos = [
        hwver.MOBO_INTELS2600CP,
        hwver.MOBO_INTELS1400FP,
    ]
    if not hal.getMotherboardType() in supported_mobos:
        return None
    bus = None
    onboard_nic_handle = None
    handles = []
    handle = []
    cmd = '/usr/local/sbin/dmidecode -t 41'
    type_regex = re.compile('.*Type:\s+Ethernet')
    bus_regex = re.compile('.*Bus\s+Address:\s+(?P<group>\d{4}):(?P<bus>\d{2})'
                           ':(?P<device>\d{2})\.(?P<function>\d)')
    error, output = procs.get_cmd_output(cmd)
    # Each device reported by dmidecode is seperated by an empty string
    for line in output:
        if len(line) == 0:
            handles.append(handle)
            handle = []
        else:
            handle.append(line)
    # Find Onboard NIC device
    for handle in handles:
        for line in handle:
            match = type_regex.match(line)
            if match:
                onboard_nic_handle = handle
                break
    # Parse Onboard NIC device for Bus Address string
    if onboard_nic_handle:
        for line in onboard_nic_handle:
            match = bus_regex.match(line)
            if match:
                bus = int(match.group('bus'))
                break
    if bus is None:
        print 'ERROR: Unable to get Onboard NIC PCI Bus'
    return bus

ONBOARD_NIC_BUS = get_onboard_nic_bus()

_g_slotMaps = {
                hwver.MOBO_INTELS2600CP :
                  { 'slot' : [ 'pci0:6:0:X',
                               'pci0:129:0:0', # CPU 2 slot (boot drive carrier)
                               'pci0:3:0:X',
                               'pci0:2:0:X',
                               'pci0:5:0:X',
                               'pci0:1:0:X',
                             ],
                    'onboard' : [ 'pci0:%s:0:0' % ONBOARD_NIC_BUS,
                                  'pci0:%s:0:1' % ONBOARD_NIC_BUS,
                                ],
                  },
                hwver.MOBO_INTELS1400FP :
                  { 'slot' : [ 'pci0:4:0:X',
                               'pci0:129:0:0', # CPU 1 PCI-32 slot (boot drive carrier)
                               'pci0:2:0:X',
                               'pci0:10:0:X',
                               'pci0:5:0:X',
                             ],
                    'onboard' : [ 'pci0:%s:0:0' % ONBOARD_NIC_BUS,
                                  'pci0:%s:0:1' % ONBOARD_NIC_BUS,
                                ],
                  },
              }

_g_supportNpi = False

_g_restartFlag = False

_g_cmcFlashOutOfSync = False

# CMC flash support status
_g_supportsCMCFlash = hal.supportsChassisVpdFlash()

#======================================================================
#=============================== Misc =================================
#======================================================================

#----------------------------------------------------------------------
isOdd = lambda x: bool(x % 2)
isEven = lambda x: not bool(x % 2)

#----------------------------------------------------------------------
def log(msg, syslogLevel=None):
    print '%s' % msg
    if syslogLevel is not None:
        try:
            syslog.syslog(syslogLevel, msg)
        except:
            # don't blow up if syslog is not available
            pass

#----------------------------------------------------------------------
def partNoHasValidFormat(partNo):
    # Valid format is 'NNN-NNNN-NN'
    return (partNo.replace('-', '').isdigit() and
            len(partNo) == VALID_PARTNUM_LEN or
            skipEmcChassis()) # Bug 130949: don't validate EMC part numbers.

def skipEmcChassis():
    return hal.getChassisType(0) in [
            hwver.CHASSIS_EMC60BAY,
            hwver.CHASSIS_ISI24BAY_V2,
            hwver.CHASSIS_ISI36BAY_V3,
            ]

#----------------------------------------------------------------------
def createPidFile(pid):
    file(PIDFILE, 'w').write(pid)

#----------------------------------------------------------------------
def removePidFile():
    if os.path.exists(PIDFILE):
        os.unlink(PIDFILE)

#----------------------------------------------------------------------
def getCurrentDateTime(timeFunc=None):
    if isinstance(timeFunc, float):
        # Convert to gmtime
        gmt = time.gmtime(timeFunc)
    elif isinstance(timeFunc, time.struct_time):
        # Convert to gmtime; need seconds
        gmt = time.gmtime(time.mktime(timeFunc))
    elif timeFunc is None:
        gmt = time.gmtime()
    else:
        return None
    return time.strftime(ISO8601_UTC_DATE_TIMESTAMP_FORMAT, gmt)

#----------------------------------------------------------------------
def getIsoTimestampFromFilename(filename):
    isoTimestamp = None
    fmt = ISO8601_UTC_DATE_TIMESTAMP_FILENAME_FORMAT
    m = re.match('^.*%s(?P<value>[0-9]{12}).xml' % SOFTAVL_PREFIX, filename)
    if m:
        timestamp = m.group('value')
        isoTimestamp = datetime.datetime.strptime(timestamp, fmt)
    return isoTimestamp

#----------------------------------------------------------------------
W1, W2 = 2, 1
def indentWithLevel(level):
    return '%-*s' % (W1, '%*sL%s' % (level, '', level))

def prettyPrint(level=None, string=None, addLevel=False):
    indent = None
    if level is not None and string is not None:
        if addLevel:
            indent = indentWithLevel(level)
        if indent is not None:
            print indent, 
        print '%*s%*s' % (level, '', W2, string)

#----------------------------------------------------------------------
class ctoException(Exception):
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return repr(self.value)

#----------------------------------------------------------------------
def addHistory(message, result=None):
    status = 0
    historyPath = os.path.join(ROOT_DIR, MFG_DIR, HISTORY_FILENAME)

    fullMessage = '%s: %s' % (getCurrentDateTime(), message)
    if result is not None:
        if not isinstance(result, str):
            result = result and 'FAIL' or 'PASS'
        fullMessage += ' [%s]\n' % result
    else:
        fullMessage += '\n'

    syslog.syslog(syslog.LOG_INFO, fullMessage)

    try:
        status = mountMfg()
        if status == 0:
            try:
                with open(historyPath, 'a') as fd:
                    if fd:
                        fd.write(fullMessage)
                    else:
                        print ('ERROR: Unable to open "%s" to write "%s"' %
                              (historyPath, message))
                        status = -1

            except IOError, e:
                print ('ERROR: I/O error writing "%s" to "%s"' %
                      (message, historyPath))
                status = -1
    finally:
        unmountMfg()

    return status

def addDiskUsageHistory():
    # Expected mount point
    mountPoint = os.path.join(ROOT_DIR, MFG_DIR)
    cmd = "df -ik %s | tail -1 | sed 's/  */\ /g'" % mountPoint
    try:
        mountMfg()
        error, output = procs.get_cmd_output(cmd)
        if error:
            for line in output:
                print line
            print 'ERROR: Unable to determine %s disk usage' % mountPoint
        elif output:
            addHistory("df: %s" % output[0])
    finally:
        unmountMfg()

#----------------------------------------------------------------------
def doOverrides(function):
    num = 0
    status = 0
    firstOverride = True

    # These hardware (HAL) overrides allow the isi_fcb_vpd_tool and the
    # drive bay library (drive_bay.c) to function correctly on Wingfoot
    # and Blue Flame hardware before node capabilities and attributes 
    # are initialized. 
    halOverrides = []
    mobo = hal.getMotherboardType()
    sefc = hal.getSefcType()
    cmc_mobos = [
        hwver.MOBO_INTELS2600CP,
        hwver.MOBO_INTELS1400FP,
    ]
    if mobo in cmc_mobos:
        halOverrides = [
            'hal_supports_chassis_vpd_eeprom true',
            'hal_supports_front_panel_display true',
            'hal_supports_internal_boot_drives true',
            'hal_has_front_panel_display true',
            'hal_has_internal_boot_drives true',
            'hal_has_chassis_vpd_eeprom true',
            'hal_get_chassis_vpd_eeprom_addr 0',
            'hal_get_chassis_vpd_eeprom_bus 0',
            'hal_get_chassis_vpd_eeprom_size 57344',
            'hal_get_chassis_vpd_eeprom_max_block_size 128',
            'hal_get_reserved_vpd_eeprom_size 8192',
            'hal_supports_cmc true',
            'hal_supports_bmc true',
            ]
    else:
        halOverrides = [
            'hal_supports_i2c true',
            'hal_supports_exp_i2c true',
            'hal_supports_chassis_vpd_eeprom true',
            'hal_supports_front_panel_display true',
            'hal_supports_internal_boot_drives true',
            'hal_has_i2c true',
            'hal_has_exp_i2c true',
            'hal_has_front_panel_display true',
            'hal_has_internal_boot_drives true',
            'hal_has_chassis_vpd_eeprom true',
            'hal_get_chassis_vpd_eeprom_addr 0xA6',
            'hal_get_chassis_vpd_eeprom_size 65536',
            'hal_get_chassis_vpd_eeprom_max_block_size 128',
            'hal_get_chassis_vpd_eeprom_bus 128',
            ]

    for override in halOverrides:
        if firstOverride:
            firstOverride = False
            if function == 'set':
                cmd = KENV_SET % ('', override)
            else:
                cmd = KENV_UNSET % ''
        else:
            if function == 'set':
                cmd = KENV_SET % (num, override)
            else:
                cmd = KENV_UNSET % num

        num += 1

        (error, output) = procs.get_cmd_output(cmd)
        if error:
            print 'ERROR: "%s" failed for "%s"' % (cmd, override)
            for line in output:
                print line
            status = -1

    if _g_debugLevel > 0:
        print 'DEBUG: Hardware (HAL) overrides:'
        os.system('kenv | grep isilon_hwversion')

    return status

#----------------------------------------------------------------------
def hasSAS2Expander():
    hasExpander = False
    cmd = 'camcontrol inquiry ses0 -D'
    (error, output) = procs.get_cmd_output(cmd)
    if error and _g_debugLevel > 0:
        print 'DEBUG: camcontrol failed'
        for line in output:
            print line
    else:
        # Check if the expander is one of the ones used on a
        # Wingfoot, Blue Flame or BigFoot SEFC.
        #
        #       The Wingfoot SEFC expander reports:
        #           "<Isilon SAS2X24 0909>"
        #
        #       The Blue Flame SEFC expander reports:
        #           "<Isilon SAS2X36 0909>"
        #
        #       The BigFoot SEFC expander reports:
        #           "<Isilon SEFC SAS2X24 E0 0909>"
        #           "<Isilon SEFC SAS2X24 E1 0909>"
        #
        # NOTES: 1) The 0909 is a version number and changes as the
        #           firmware changes.
        #        2) SEFCs with multiple expanders, like BigFoot,
        #           report the expander number (i.e. E0/E1).
        #        3) Rather than reporting "SEFC SAS2X24", the
        #           original BigFoot P0 expander firmware reported 
        #           itself as "SEFC BIGFOOT", so we need to be 
        #           able to handle systems with the old firmware,
        #           at least for a while...
        for line in output:
            # We don't care about "BIGFOOT" now, just search for SAS2X
            if "SAS2X" in line:
                hasExpander = True
    if hasExpander:
        if _g_debugLevel > 0:
            print 'DEBUG: Found SAS2 expander'
    return hasExpander

def isNpiFile(filePath):
    status = 0

    cmd = '/bin/cat %s' % filePath
    (error, output) = procs.get_cmd_output(cmd)
    if error:
        for line in output:
            print line
        print 'ERROR: Unable to cat "%s"' % filePath
        status = -1

    if status == 0:
        for line in output:
            m = re.match('^.*NPI = true', line)
            if m:
                return True

    return False

def supportNpi():
    return _g_supportNpi

def getProbeConfigLevel(filePath):
    status = 0
    level = None

    cmd = '/bin/cat %s' % filePath
    (error, output) = procs.get_cmd_output(cmd)
    if error:
        for line in output:
            print line
        print 'ERROR: Unable to cat "%s"' % filePath
        status = -1

    if status == 0:
        for line in output:
            m = re.match('^level = (?P<level>\w+)', line)
            if m:
                level = m.group('level')
        
        if _g_debugLevel > 0:
            print ('DEBUG: Probe configuration file (%s), '
                   'levelString="%s", level="%s"' % 
                   (filePath, line, level))

        # No level is okay, as pre-6.5.5 we don't support level
        if level is None:
            if _g_debugLevel > 0: 
                print ('DEBUG: No level found on probe '
                       'configuration file ("%s")' % filePath)
            level = 'legacy'

    m = re.match('.*(?P<level>\d+).*', filePath)
    if m:
        filePath_level = m.group('level')
    else:
        #to match the legacy level
        filePath_level = 'legacy'

    if filePath_level != level:
        print ("WARNING: Probe configuration file (%s) gets overwritten with level: %s"
                % (filePath, level))

    return level

#----------------------------------------------------------------------
def getProbeConfigCurrentVersion():
    version = None

    # NOTE: We use the SVN revision that is embedded in the
    #       file to determine which copy of the file is the
    #       newest. The SVN revision is contained in the $ID
    #       string named "version".

    ctx = gconfig.open(hal.gcfg_probeconfig).ctx_new()
    versionString = ctx.read('version')
    m = re.match('^.*%s (?P<value>\w+)' % CURRENT_PROBE_FILENAME, versionString)
    if m:
        version = m.group('value')

    if _g_debugLevel > 0:
        print ('DEBUG: Probe configuration file (%s), '
               'versionString="%s", version="%s"' % 
               (currentProbeConfigFilePath(), versionString, version))

    if version is None:
        print ('WARNING: Unable to determine version of probe '
               'configuration file ("%s")' % currentProbeConfigFilePath())

    return version

#----------------------------------------------------------------------
def getProbeConfigVersion(filePath):
    status = 0
    version = None

    # NOTE: We use the SVN revision that is embedded in the
    #       file to determine which copy of the file is the
    #       newest. The SVN revision is contained in the $ID
    #       string named "version".

    cmd = '/bin/cat %s' % filePath
    (error, output) = procs.get_cmd_output(cmd)
    if error:
        for line in output:
            print line
        print 'ERROR: Unable to cat "%s"' % filePath
        status = -1

    if status == 0:
        for line in output:
            m = re.match('^.*%s.*.gc (?P<value>\w+)' % BASE_PROBE_FILENAME, 
                line)
            if m:
                version = m.group('value')
                break
        
        if _g_debugLevel > 0:
            print ('DEBUG: Probe configuration file (%s), '
                   'versionString="%s", version="%s"' % 
                   (filePath, line, version))

        if version is None:
            print ('WARNING: Unable to determine version of probe '
                   'configuration file ("%s")' % filePath)

    return version

#----------------------------------------------------------------------
def getSbbConfigurationDetails(partNo, mfgProbeConfig=False):
    sbbType = None
    sbbDetails = None
    ctx = None

    if not mfgProbeConfig:
        ctx = gconfig.open(hal.gcfg_probeconfig).ctx_new()
    else:
        try:
            status = mountMfg()
            if status == 0:
                mfgPath = mfgProbeConfigFilePath()
                treeSeed = gconfig.Tree('probeconfig').tree_seed()
                ctxTree = gconfig.create_tree(treeSeed, primary=mfgPath)
                ctx = gconfig.open(ctxTree).ctx_new()
        finally:
            unmountMfg()

    if ctx is None:
        return (None, None)

    keys = [ 'basesbbs', 'drivesbbs', 'memorysbbs', 'iosbbs' ]
    for key in keys:
        details = ctx.read(key)
        for detail in details:
            if partNo == detail['part_no']:
                sbbType = detail['type']
                sbbDetails = detail
                break

    return (sbbType, sbbDetails)

#----------------------------------------------------------------------
def getSbbMap(basePartNo, mfgProbeConfig=False):
    sbbMap = None
    ctx = None

    if not mfgProbeConfig:
        ctx = gconfig.open(hal.gcfg_probeconfig).ctx_new()
    else:
        try:
            status = mountMfg()
            if status == 0:
                mfgPath = mfgProbeConfigFilePath()
                treeSeed = gconfig.Tree('probeconfig').tree_seed()
                ctxTree = gconfig.create_tree(treeSeed, primary=mfgPath)
                ctx = gconfig.open(ctxTree).ctx_new()
        finally:
            unmountMfg()

    if ctx is None:
        return None

    details = ctx.read('sbbmaps')
    for detail in details:
        if basePartNo == detail['base_chassis']:
            sbbMap = detail
            break

    return sbbMap

#----------------------------------------------------------------------
def getIbNicMap(basePartNo, mfgProbeConfig=False):
    nicMap = None
    ctx = None

    if not mfgProbeConfig:
        ctx = gconfig.open(hal.gcfg_probeconfig).ctx_new()
    else:
        try:
            status = mountMfg()
            if status == 0:
                mfgPath = mfgProbeConfigFilePath()
                treeSeed = gconfig.Tree('probeconfig').tree_seed()
                ctxTree = gconfig.create_tree(treeSeed, primary=mfgPath)
                ctx = gconfig.open(ctxTree).ctx_new()
        finally:
            unmountMfg()

    if ctx is None:
        return None

    details = ctx.read('ibnicmaps')
    for detail in details:
        if basePartNo == detail['base_chassis']:
            nicMap = detail
            break

    return nicMap

#======================================================================
#=========================== End of Misc ==============================
#======================================================================

#======================================================================
#======================= Files & Filesystem ===========================
#======================================================================

#----------------------------------------------------------------------
def mfgIsMounted():
    status = True

    cmd = 'mount | grep "%s"' % MFG_PARTITION
    (error, output) = procs.get_cmd_output(cmd)
    # NOTE: If no grep matches are found, this command will throw an error,
    #       so ignore the error and just look at output
    if len(output) < 1:
        status = False

    return status

#----------------------------------------------------------------------
def mfgIsMountedOn():
    mountedOn = None

    cmd = 'mount | grep "%s"' % MFG_PARTITION
    (error, output) = procs.get_cmd_output(cmd)
    # NOTE: If no grep matches are found, this command will throw an error,
    #       so make sure there's no error before using output
    if len(output) > 0:
        m = re.match('^[^ ]+ on (?P<value>[^ ]+) .*', output[0])
        if m:
            mountedOn = m.group('value')

    return mountedOn

#----------------------------------------------------------------------
def mountMfg():
    status = 0
    needToMount = False
    
    # Expected mount point
    mountPoint = os.path.join(ROOT_DIR, MFG_DIR)

    if not mfgIsMounted():
        needToMount = True
    else:
        mountedOn = mfgIsMountedOn()
        # Check to see if the MFG partition has been mounted
        # on some mount point other than our expected one. If
        # so, un-mount it so that we can mount it where we
        # want it mounted...
        if mountedOn is not None and mountedOn != mountPoint:
            print ('WARNING: Unexpectedly found "%s" mounted on "%s"' % 
                   (MFG_PARTITION, mountedOn))
            print 'WARNING: Un-mounting "%s"...' % mountedOn
            print ''
            status = unmountMfg(mountedOn)
            if status == 0:
                needToMount = True
            else:
                print 'ERROR: Unable to un-mount "%s"' % mountedOn
                status = -1
    
    if status == 0 and needToMount:
        rootContents = os.listdir(ROOT_DIR)
        if not MFG_DIR in rootContents:
            os.mkdir(mountPoint)

        cmd = 'mount %s %s' % (MFG_PARTITION, mountPoint)
        (error, output) = procs.get_cmd_output(cmd)
        if error:
            for line in output:
                print line
            print ('ERROR: Unable to mount "%s" on "%s"' %
                   (MFG_PARTITION, mountPoint))
            # NOTE: Since the GEOM bootdisk startup includes a step
            #       to fsck all the UFS filesystems, including the
            #       MFG partition, there is no need to try an fsck
            #       here when/if the mount fails. It should always
            #       be a clean filesystem by the time this code ever
            #       has a chance to run.
            status = -1

    return status

#----------------------------------------------------------------------
def unmountMfg(unexpectedMountPoint=None):
    status = 0
    
    if not mfgIsMounted():
        mountPoint = None
    elif unexpectedMountPoint is not None:
        mountPoint = unexpectedMountPoint
    else:
        mountPoint = os.path.join(ROOT_DIR, MFG_DIR)

    if mountPoint:
        cmd = 'umount %s' % mountPoint
        (error, output) = procs.get_cmd_output(cmd)
        if error:
            for line in output:
                print line
            print 'ERROR: Unable to unmount "%s"' % mountPoint 
            status = -1

    return status

#----------------------------------------------------------------------
def backupFile(filePath):
    status = 0
    okToBackup = False
    newFilename = None
    dirName = os.path.dirname(filePath)
    filename = os.path.basename(filePath)

    # get the list of the existing backups of the given filename
    fns = glob.glob("%s.[0-9]*" % filePath)

    # empty list
    if not fns:
        newFilename = "%s.%s" % (filename, 1)
        okToBackup = True
    else:
        # sort the filenames first
        fns_sorted = sorted([int(fn.split('.')[-1]) for fn in fns])

        # has more files than the current limit
        if len(fns_sorted) >= BACKUP_FILE_LIMIT:
            print ('WARNING: Limit of %s reached for file %s' %
                   (BACKUP_FILE_LIMIT, filename))

            # delete old files
            fns_sorted_delete = fns_sorted[:-(BACKUP_FILE_LIMIT-1)]

            # empty list will simply quit the loop
            for fn in fns_sorted_delete:
                deleteFilename = "%s.%s" % (filename, fn)
                deletePath = os.path.join(dirName, deleteFilename)
                if _g_debugLevel > 0:
                    print 'DEBUG: Removing %s...' % deletePath
                try:
                    os.remove(deletePath)
                except OSError, e:
                    print ('ERROR: Unable to delete "%s", error %s ("%s")' %
                           (deletePath, e.errno, e.strerror))
                    return -1

        # if deletion is complete/skipped, setup new filename
        newFilename = "%s.%s" % (filename, fns_sorted[-1]+1)
        okToBackup = True

    if okToBackup:
        # Backup the file
        oldPath = os.path.join(dirName, filename)
        newPath = os.path.join(dirName, newFilename)
        if _g_debugLevel > 0:
            print 'DEBUG: Backing up "%s" ==> "%s"' %  (oldPath, newPath)
        try:
            os.rename(oldPath, newPath)
        except OSError, e:
            print ('ERROR: Unable to rename "%s" to "%s", error %s ("%s")' %
                   (oldPath, newPath, e.errno, e.strerror))
            status = -1

    return status

#----------------------------------------------------------------------
def makeDir(path, mode=0777):
    status = 0

    if not os.access(path, os.F_OK):
        os.mkdir(path, mode)

    return status
#----------------------------------------------------------------------
def getRandomName(dirPath, name):
    status = 1
    numberOfTries = 0

    name = str(uuid.uuid4())
    pathExists = os.path.exists('%s/%s' % (dirPath, name))
    while (pathExists and numberOfTries < 10):
        name = str(uuid.uuid4())
        pathExists = os.path.exists('%s/%s' % (dirPath, name))
        numberOfTries += 1

    if numberOfTries >= 10:
        print 'ERROR: Unable to create unique name within "%s"' % dirPath

    if not pathExists:
        status = 0

    return status
#----------------------------------------------------------------------
def copyFile(srcPath, destPath):
    status = 0

    # Don't bother if paths are identical
    if srcPath != destPath:
        try:
            status = mountMfg()
            if status == 0:
                # Copy the file
                cmd = '/bin/cp %s %s' % (srcPath, destPath)
                (error, output) = procs.get_cmd_output(cmd)
                if error:
                    for line in output:
                        print line
                    print ('ERROR: Unable to copy "%s" to "%s"' %
                           (srcPath, destPath))
                    status = -1
        finally:
            unmountMfg()

    return status

#----------------------------------------------------------------------
def backupFileIfDifferent(filePath, localPath):
    status = 0
    diff = FilesAreDifferent(filePath, localPath)
    if diff:
        if _g_debugLevel > 0:
            print 'DEBUG: Creating backup of "%s"' % localPath
        status = backupFile(localPath)
    return status

#----------------------------------------------------------------------
def FilesAreDifferent(file1Path, file2Path):
    diff = False
    # Don't bother if paths are identical
    if file1Path != file2Path:
        if (os.access(file1Path, os.F_OK) and
            os.access(file2Path, os.F_OK)):
            # Treat XML data files special
            if file1Path.endswith(".xml"):
                diff = xmlFilesAreDifferent(file1Path, file2Path)
            else:
                diff = not filecmp.cmp(file1Path, file2Path, False)
    return diff

#----------------------------------------------------------------------
# Report XML parsing errors to console via a "Warning" message:
# 1. Do not save to history.txt logfile, since these warnings WILL NOT
#    go away for given file(s). Don't want to spam the logfile.
# 2. Do not report as an "Error" to prevent concerning the user.
#    We do not fail anything in this code on parsing errors -- rather,
#    we ignore the files and assume the relevant files are different.
def xmlFilesAreDifferent(file1Path, file2Path, ignoreRootAttribs=True):
    diff = False
    if file1Path != file2Path:
        diff = True
        if (os.access(file1Path, os.F_OK) and
            os.access(file2Path, os.F_OK)):
            trees = []
            for path in [file1Path, file2Path]:
                try:
                    trees.append(ElementTree.parse(path))
                except expat.ExpatError as e:
                    print ('Warning: %s %s' % (path, e))
            # There are two (2) paths, file1Path and file2Path.
            # We attempt to parse each path into a tree, ElementTree.parse(),
            # which starts as an empty list, trees = [].
            # If we encounter an error (ExpatError) parsing a tree/path, we
            # report a "Warning" message - please refer to comments preceding
            # this function for more details. We report each such "Warning"
            # message separately for each file path which fails to parse,
            # independently. If both paths are parsed successfully, we will
            # have two (2) valid trees to compare -- Only perform the files
            # are different comparison if we have both trees to compare.
            RequiredNumberOfTreesForCompare = 2
            if len(trees) == RequiredNumberOfTreesForCompare:
                roots = [tree.getroot() for tree in trees]
                if ignoreRootAttribs:
                    [[r.set(key, '') for key in r.keys()] for r in roots]
                data = [ElementTree.tostring(root) for root in roots]
                diff = data[0] != data[1]
    return diff

#----------------------------------------------------------------------
def DeleteIdenticalFiles(dir, regex, consecutive=True, limit=None):
    # Could do these in 1-2 func lines, but for clarity do 1 op/line.
    def fileinfo(filename):
        filepath = os.path.join(dir, filename)
        return (filename, os.lstat(filepath), os.path.isfile(filepath))
    def filediff(filenames):
        paths = map(lambda f: os.path.join(dir, f), filenames)
        return (filenames, xmlFilesAreDifferent(paths[0], paths[1]))
    def filedelete(filename):
        filepath = os.path.join(dir, filename)
        cmd = '/bin/rm -r %s' % filepath
        error, output = procs.get_cmd_output(cmd)
        if error:
            for line in output:
                print line
            print 'ERROR: Unable to remove "%s"' % filepath
    def filelist():
        r1 = filter(lambda r: r[2], map(fileinfo, os.listdir(dir)))
        r2 = sorted(r1, key=lambda r: r[1].st_mtime, reverse=True)
        r3 = filter(lambda r: regex.match(r[0]), r2)
        r4 = map(lambda r: r[0], r3)
        return r4
    try:
        mountMfg()
        r4 = filelist()
        r5 = map(filediff, zip(*(r4[:-1], r4[1:])))
        r6 = filter(lambda f: f[1] == False, r5)
        r7 = map(lambda r: r[0][1], r6)
        rr = map(filedelete, r7)
        # BEFORE a (possible) non-consecutive exhaustive comparison
        # check, apply any file count limit filters (deletes).
        if limit is not None and limit > 0:
            r4 = filelist()
            dr = map(filedelete, r4[limit:])
        if not consecutive:
            # Caller wants us to delete ALL identical pairs.
            # This upper-triangular matrix check make take a long time
            # if the file count 'c' is high: c(c-1) ~ c^2 XML filediffs.
            r4 = filelist()
            c = len(r4)
            d1 = [[(i, j) for j in xrange(i+1, c)] for i in xrange(c)]
            d2 = reduce(lambda a, b: a + b, d1, [])
            d3 = map(filediff, [[r4[i] for i in idx] for idx in d2])
            d4 = filter(lambda f: f[1] == False, d3)
            d5 = map(lambda r: r[0][1], d4)
            dr = map(filedelete, d5)
    finally:
        unmountMfg()

#---------------------------------------------------------------------
def checkMfg():
    status = 1

    #If any of the files listed in '/etc/isilon_flash_files' is missing,
    #an attempt to save to FFS will fail. Check to ensure all files are
    #present before save
    cmd = '%s finitcto' % filenames.commands['fcb_vpd_tool']
    error, output = procs.get_cmd_output(cmd)
    if error:
        for line in output:
            print line
        print 'ERROR: Unable to do finit'
    else:
        status = 0

    if status == 0:
        finitFilePath = os.path.join(ROOT_DIR, GCONFIG_DIR,
                                     'isilon_flash_files')
        cmd = '/bin/cat %s' % finitFilePath
        error, output = procs.get_cmd_output(cmd)
        if error:
            for line in output:
                print line
            print 'ERROR: Unable to cat "%s"' % finitFilePath
            status = 1

    if status == 0:
        try:
            status = mountMfg()
            if status == 0:
                for line in output:
                    fileExists = os.path.exists('%s' % line)
                    if _g_debugLevel > 0:
                        if fileExists:
                            print line + ' exists'
                    if not fileExists:
                        print line + ' does not exist'
                        # Do not fail - save what files we can
        finally:
            unmountMfg()

    return status
#---------------------------------------------------------------------
def checkFFS(mode):
    status = 1
    numberOfTries = 0
    global TMP_SUBDIR

    #Sanity check
    if mode < 0 or mode > 1:
        print 'Error: Mode for checkFFS(mode) out of bounds'
        return status
    else:
        status = 0

    tmpPathExists = os.path.exists('/%s/%s' % (TMP_DIR, TMP_SUBDIR))
    if tmpPathExists:
        tmpDirPath = ('/%s' % TMP_DIR)
        status = getRandomName(tmpDirPath, TMP_SUBDIR)

    if status == 0:
        tmpPath = os.path.join(ROOT_DIR, TMP_DIR, TMP_SUBDIR)
        #Create the directory
        status = makeDir(tmpPath)

    if status == 0 and (mode == 0 or mode == 1):
        cmd = '%s fdump > /%s/%s/%s' % (filenames.commands['fcb_vpd_tool'],
                                        TMP_DIR, TMP_SUBDIR, 'fdump')
        error, output = procs.get_cmd_output(cmd)
        if error:
            for line in output:
                print line
            print 'ERROR: Unable to do fdump. Check CMC Flash init status'
            dirMade = False
            status = 1
        else:
            dirMade = True

        if status == 0 and mode == 1:
            fdumpFilePath = os.path.join(ROOT_DIR, TMP_DIR, TMP_SUBDIR, 'fdump')
            cmd = '/bin/cat %s' % fdumpFilePath
            error, output = procs.get_cmd_output(cmd)
            if error:
                for line in output:
                    print line
                print 'ERROR: Unable to cat "%s"' % fdumpFilePath
                status = 1
            else:
                for line in output:
                    isEmpty = re.search('Flash file system is empty', line)
                    if isEmpty:
                        print isEmpty.group()
                        break
                if not isEmpty:
                    for line in output:
                        receipt = re.search('^/mfg.+receipt.xml.*', line)
                        if receipt:
                            if _g_debugLevel > 0:
                                print receipt.group()
                            break
                    for line in output:
                        softAvl = re.search('^/mfg.+softAVL.xml.*', line)
                        if softAvl:
                            if _g_debugLevel > 0:
                                print softAvl.group()
                            break
                    if not receipt or not softAvl:
                        status = 1
                else:
                    status = 1
    if dirMade:
        #Clean up before exit
        cmd = '/bin/rm -r %s' % tmpPath
        error, output = procs.get_cmd_output(cmd)
        if error:
            for line in output:
                print line
            print 'ERROR: Unable to remove "%s"' % tmpPath

    return status

#----------------------------------------------------------------------
def restoreCTOtoTmp(md5 = False):
    status = 0
    numberOfTries = 0
    global TMP_SUBDIR

    cmd = '%s finitcto' % filenames.commands['fcb_vpd_tool']
    error, output = procs.get_cmd_output(cmd)
    if error:
        for line in output:
            print line
        print 'ERROR: Unable to do finit'
        status = 1

    if status == 0:
        if md5:
            cmd = '%s ftmp_md5' % filenames.commands['fcb_vpd_tool']
        else:
            cmd = '%s ftmp' % filenames.commands['fcb_vpd_tool']
        error, output = procs.get_cmd_output(cmd)

    # The above is a best effort command.  If we fail to restore a file, this
    # can't be seen as an error for the steps below it.
    if status == 0:
        #Check if path already exists, if it does, generate new random
        #directory name
        tmpPathExists = os.path.exists('/%s/%s' % (TMP_DIR, TMP_SUBDIR))
        while (tmpPathExists and numberOfTries < 10):
            TMP_SUBDIR = str(uuid.uuid4())
            tmpPathExists = os.path.exists('/%s/%s' % (TMP_DIR, TMP_SUBDIR))
            numberOfTries += 1

        if numberOfTries >= 10:
            status = 1
            print 'ERROR: Unable to create unique directory for ftmp'

    if status == 0:
        tmpPath = os.path.join(ROOT_DIR, TMP_DIR, TMP_SUBDIR)
        #Create the directory
        status = makeDir(tmpPath)

    if status == 0:
        finitFilePath = os.path.join(ROOT_DIR, GCONFIG_DIR,
                                     'isilon_flash_files')
        with open(finitFilePath, 'r') as f:
            lines = f.readlines()
            for line in lines:
                line = line.strip('\n')
                filename = line.split('/')[-1]
                filepath = os.path.join(ROOT_DIR, TMP_DIR, filename)
                if md5:
                    filepath = "%s.md5" % filepath
                if not os.path.exists(filepath):
                    print 'ERROR: File %s does not exist' % filepath
                    continue
                cmd = 'mv %s %s' % (filepath, tmpPath)
                error, output = procs.get_cmd_output(cmd)
                if error:
                    print 'ERROR: Unable to move "%s" to "%s"' % (filepath, tmpPath)

    return status

#----------------------------------------------------------------------
def getCTOFromCMCFFS(recover, pclevels=None):
    results = { 'pass' : [],
                'fail' : [],
              }

    # function can take a list or single var as input
    if isinstance(recover, list):
        recoverList = recover
    else:
        recoverList = [ recover ]

    #First read fdump for presence of Mandatory files (mode = 1)
    status = checkFFS(1)
    if status != 0:
        return None

    #Restore backup to /tmp/XXXX directory
    status = restoreCTOtoTmp()
    if status != 0:
        return None

    for fileType in recoverList:
        if fileType == FILETYPE_PROBE:
            for level in PROBE_LEVELS:
                if pclevels is not None and (level not in pclevels):
                    continue

                probePath = tmpProbeConfigFilePath(level)
                probeExists = os.access(probePath, os.F_OK)

                #If ProbeConfig file missing, issue warning
                if not probeExists:
                    warn = (level == CURRENT_PROBE_LEVEL)
                    print ('%s: "%s" does not exist in FFS' %
                        ('WARNING' if warn else 'NOTE', probePath))
                    status = 1
                else:
                    status = saveFile(fileType, probePath)

                if status != 0:
                    results['fail'].append(os.path.basename(probePath))
                else:
                    results['pass'].append(os.path.basename(probePath))
        elif fileType == FILETYPE_RECEIPT:
            receiptPath = tmpReceiptFilePath()
            status = saveFile(fileType, receiptPath)
            if status != 0:
                results['fail'].append(os.path.basename(receiptPath))
            else:
                results['pass'].append(os.path.basename(receiptPath))
        elif fileType == FILETYPE_SOFTAVL:
            softavlPath = tmpSoftAVLFilePath()
            status = saveFile(fileType, softavlPath)
            if status != 0:
                results['fail'].append(os.path.basename(softavlPath))
            else:
                results['pass'].append(os.path.basename(softavlPath))

    #Clean up before exit
    tmpPath = os.path.join(ROOT_DIR, TMP_DIR, TMP_SUBDIR)
    cmd = '/bin/rm -r %s' % tmpPath
    error, output = procs.get_cmd_output(cmd)
    if error:
        for line in output:
            print line
        print 'ERROR: Unable to remove "%s"' % tmpPath

    return results

#----------------------------------------------------------------------
def saveCTOtoCMCFFS():
    status = 0

    #Check flash init, mode is 0 for init status
    status = checkFFS(0)

    #Also check /mfg to ensure all files needed for fcopy are present
    if status == 0:
        status = checkMfg()

    if status == 0:
        log('Saving CTO files to backup location', syslog.LOG_NOTICE)
        cmd = '%s fcopy' % filenames.commands['fcb_vpd_tool']
        error, output = procs.get_cmd_output(cmd)
        if error:
            for line in output:
                log(line, syslog.LOG_NOTICE)
            log('ERROR: Unable to save CTO to CMC FFS', syslog.LOG_NOTICE)
            status = 1
        else:
            log('CTO save to backup location complete', syslog.LOG_NOTICE)
            if _g_debugLevel > 0:
                log('DEBUG: Saved to CMC FFS', syslog.LOG_NOTICE)
                log('DEBUG: Restoring saved files to tmp', syslog.LOG_NOTICE)
                status = restoreCTOtoTmp()
    else:
        log('CTO save skipped due to error', syslog.LOG_NOTICE)
        status = 0

    return status

#----------------------------------------------------------------------
def getLocalFilePaths(fileType, filePath):
    topDir = subDir = None
    topDir2 = subDir2 = None
    directory = localPath = None
    directory2 = localPath2 = None

    # Sanity check to make sure there's actually a filename in the path
    filename = os.path.basename(filePath)
    if filename is None:
        return {
        'dir' : [None, None],
        'path': [None, None],
        }

    if fileType in [ FILETYPE_RECEIPT, FILETYPE_SOFTAVL, FILETYPE_ASBUILT,
        FILETYPE_PROBE ]:

        topDir = MFG_DIR
        if fileType in [ FILETYPE_RECEIPT ]:
            subDir = RECEIPT_SUBDIR
        elif fileType in [ FILETYPE_SOFTAVL ]:
            subDir = SOFTAVL_SUBDIR
        elif fileType in [ FILETYPE_ASBUILT ]:
            subDir = ASBUILT_SUBDIR
        elif fileType in [ FILETYPE_PROBE ]:
            subDir = PROBE_SUBDIR
            # A copy also goes into the gconfig directory
            topDir2 = GCONFIG_DIR
            subDir2 = GCONFIG_SUBDIR

    if topDir is not None and subDir is not None:
        directory = os.path.join(ROOT_DIR, topDir, subDir)
        localPath = os.path.join(directory, filename)

    if topDir2 is not None and subDir2 is not None:
        directory2 = os.path.join(ROOT_DIR, topDir2, subDir2)
        localPath2 = os.path.join(directory2, filename)

    return {
        'dir' : [directory, directory2],
        'path': [localPath, localPath2],
        }
    
#----------------------------------------------------------------------
def saveFile(fileType, filePath, reImage=False):
    status = 0

    # Sanity check to make sure there's actually a filename in the path
    filename = os.path.basename(filePath)
    if filename is None:
        print 'ERROR: Path contains no filename'
        return -1

    local = getLocalFilePaths(fileType, filePath)
    directory, directory2 = local.get('dir', [None, None])
    localPath, localPath2 = local.get('path', [None, None])

    try:
        status = mountMfg()
        if status == 0:
            if directory is not None:
                status = makeDir(directory)
            if status == 0 and directory2 is not None:
                status = makeDir(directory2)
        if status == 0:
            if directory is not None and localPath is not None:
                status = backupFileIfDifferent(filePath, localPath)
                if status == 0 and filePath != localPath:
                    status = copyFile(filePath, '%s' % directory)
                    if status == 0:
                        _g_cmcFlashOutOfSync = True
        if status == 0:
            if directory2 is not None and localPath2 is not None:
                if status == 0:
                    status = backupFileIfDifferent(filePath, localPath2)
                if status == 0 and filePath != localPath2:
                    status = copyFile(filePath, '%s' % directory2)
        if status != 0:
            print 'ERROR: Unable to save "%s" file' % fileType
    finally:
        unmountMfg()

    return status

#----------------------------------------------------------------------
def tmpProbeConfigFilePath(fileLevel = None):
    tmpDir = os.path.join(ROOT_DIR, TMP_DIR, TMP_SUBDIR)
    if fileLevel is not None:
        if fileLevel == 'legacy':
            fileName = LEGACY_PROBE_FILENAME
        else:
            fileName = '%s.%s.gc' % (BASE_PROBE_FILENAME, fileLevel)
    else:
        fileName = CURRENT_PROBE_FILENAME
    return os.path.join(tmpDir, fileName)

#----------------------------------------------------------------------
def currentProbeConfigFilePath(fileLevel = None):
    gconfigDir = os.path.join(ROOT_DIR, GCONFIG_DIR, GCONFIG_SUBDIR)
    if fileLevel is not None:
        if fileLevel == 'legacy':
            fileName = LEGACY_PROBE_FILENAME
        else:
            fileName = '%s.%s.gc' % (BASE_PROBE_FILENAME, fileLevel)
    else:
        fileName = CURRENT_PROBE_FILENAME
    return os.path.join(gconfigDir, fileName)

#----------------------------------------------------------------------
def mfgProbeConfigFilePath(fileLevel = None):
    mfgDir = os.path.join(ROOT_DIR, MFG_DIR, PROBE_SUBDIR)
    if fileLevel is not None:
        if fileLevel == 'legacy':
            fileName = LEGACY_PROBE_FILENAME
        else:
            fileName = '%s.%s.gc' % (BASE_PROBE_FILENAME, fileLevel)
    else:
        fileName = CURRENT_PROBE_FILENAME
    return os.path.join(mfgDir, fileName)

#----------------------------------------------------------------------
def tmpReceiptFilePath():
    tmpDir = os.path.join(ROOT_DIR, TMP_DIR, TMP_SUBDIR)
    return os.path.join(tmpDir, RECEIPT_FILENAME)

#----------------------------------------------------------------------
def currentReceiptFilePath():
    mfgDir = os.path.join(ROOT_DIR, MFG_DIR, RECEIPT_SUBDIR)
    return os.path.join(mfgDir, RECEIPT_FILENAME)

#----------------------------------------------------------------------
def tmpSoftAVLFilePath():
    tmpDir = os.path.join(ROOT_DIR, TMP_DIR, TMP_SUBDIR)
    return os.path.join(tmpDir, SOFTAVL_FILENAME)

#----------------------------------------------------------------------
def currentSoftAVLFilePath():
    mfgDir = os.path.join(ROOT_DIR, MFG_DIR, SOFTAVL_SUBDIR)
    return os.path.join(mfgDir, SOFTAVL_FILENAME)

#----------------------------------------------------------------------
def currentAsBuiltFilePath():
    mfgDir = os.path.join(ROOT_DIR, MFG_DIR, ASBUILT_SUBDIR)
    return os.path.join(mfgDir, ASBUILT_FILENAME)

#----------------------------------------------------------------------
def cleanUp():
    status = 0

    # Receipts
    r1 = os.path.join(ROOT_DIR, MFG_DIR, RECEIPT_SUBDIR, XML_BACKUP_FILES)
    r2 = os.path.join(ROOT_DIR, MFG_DIR, RECEIPT_SUBDIR, XML_ORDER_FILES)
    r3 = os.path.join(ROOT_DIR, MFG_DIR, RECEIPT_SUBDIR, XML_BTS_FILES)
    r4 = os.path.join(ROOT_DIR, MFG_DIR, RECEIPT_SUBDIR, XML_SAP_FILES)
    r5 = os.path.join(ROOT_DIR, MFG_DIR, RECEIPT_SUBDIR, XML_PROPEL_FILES)

    # SoftAVLs
    s1 = os.path.join(ROOT_DIR, MFG_DIR, SOFTAVL_SUBDIR, XML_BACKUP_FILES)
    s2 = os.path.join(ROOT_DIR, MFG_DIR, SOFTAVL_SUBDIR, XML_SOFTAVL_FILES)

    # As Built Records (ABRs)
    a1 = os.path.join(ROOT_DIR, MFG_DIR, ASBUILT_SUBDIR, XML_BACKUP_FILES)
    a2 = os.path.join(ROOT_DIR, MFG_DIR, ASBUILT_SUBDIR, XML_ASBUILT_FILES)

    # Probe configurations
    p1 = os.path.join(ROOT_DIR, GCONFIG_DIR, GCONFIG_SUBDIR, GC_BACKUP_FILES)
    p2 = os.path.join(ROOT_DIR, MFG_DIR, PROBE_SUBDIR, GC_BACKUP_FILES)

    filenames = [ r1, r2, r3, r4, r5, s1, s2, a1, a2, p1, p2 ]

    try:
        status = mountMfg()
        if status == 0:
            for filename in filenames:
                if _g_debugLevel > 0:
                    print 'DEBUG: Removing "%s"' % filename
                cmd = '/bin/rm -f %s' % filename
                (error, output) = procs.get_cmd_output(cmd)
                if error:
                    for line in output:
                        print line
                    print 'ERROR: Unable to remove "%s"' % filename
                    status = -1
    finally:
        unmountMfg()

    addHistory('File clean-up', result = status)

    return status

#----------------------------------------------------------------------
def parseXmlFile(filePath):
    # Minimal (basic) error checking
    if not os.path.exists(filePath):
        print 'WARNING: "%s" does not exist' % filePath
        return None

    from xml.parsers.expat import ExpatError
    try:
        xmlDoc = minidom.parse(filePath)
    except ExpatError, e:
        print ('ERROR: XML error "%s" in "%s"' % (str(e), filePath))
        return None
    except IOError as e:
        print "ERROR: I/O error(%s): (%s)" % (e.errno, e.strerror)
        return None

    if xmlDoc is not None:
        # Filter (remove) any comments
        map(lambda child:
            child.nodeName == '#comment' and xmlDoc.removeChild(child),
            xmlDoc.childNodes)

    return xmlDoc

#======================================================================
#==================== End of Files & Filesystem =======================
#======================================================================

#======================================================================
#============================= Receipts ===============================
#======================================================================

#----------------------------------------------------------------------
class Receipt:
    def __init__(self, level, version, date, filename):

        self._level = level
        self._info = { 
            'version' : version, 
            'date' : date, 
            'filename' : filename,
            }
        self._boms = []
    
    def getLevel(self):
        return self._level

    def getVersion(self):
        return self._info['version']

    def getDate(self):
        return self._info['date']

    def getFilename(self):
        return self._info['filename']

    def addBom(self, bom):
        self._boms.append(bom)

    def hasBoms(self):
        return len(self._boms) > 0

    def getBoms(self):
        return self._boms
    
    def getLastBom(self):
        return self._boms[len(self._boms) - 1]
    
    def getNumBoms(self):
        return len(self._boms)

    def dump(self):
        print ''
        prettyPrint(self._level, '### Start of Receipt Dump ###')
        prettyPrint(self._level, 'Version:   "%s"' % self._info['version'])
        prettyPrint(self._level, 'Date:      "%s"' % self._info['date'])
        prettyPrint(self._level, 'Filename:  "%s"' % self._info['filename'])
        prettyPrint(self._level, '')
        for bom in self._boms:
            bom.dump()
        prettyPrint(self._level, '### End of Receipt Dump ###')
        print ''

#----------------------------------------------------------------------
class ReceiptBOM:
    def __init__(self, level):
        self._level = level
        self._info = {}
        self._sbbs = []

    def getLevel(self):
        return self._level

    def addSbb(self, sbb):
        self._sbbs.append(sbb)

    def hasSbbs(self):
        return len(self._sbbs) > 0

    def getSbbs(self):
        return self._sbbs

    def getLastSbb(self):
        return self._sbbs[len(self._sbbs) - 1]

    def getNumSbbs(self):
        return len(self._sbbs)

    def dump(self):
        prettyPrint(self._level, '>>> Receipt BOM <<<')
        for sbb in self._sbbs:
            sbb.dump()
        prettyPrint(self._level, '>>> Receipt BOM <<<')

#----------------------------------------------------------------------
class ReceiptSBB:
    def __init__(self, level, part, desc, qty):
        self._level = level
        self._info = {
            'part' : part, 
            'desc' : desc,
            'qty' : qty
            }

    def getLevel(self):
        return self._level

    def getPart(self):
        return self._info['part']

    def getDesc(self):
        return self._info['desc']

    def getQty(self):
        return self._info['qty']

    def dump(self):
        prettyPrint(self._level, 'Receipt SBB: P/N="%s", Description="%s", '
                    'Qty="%s"' % (self._info['part'], self._info['desc'],
                    self._info['qty']))

#----------------------------------------------------------------------
def walkReceipt(level, receipt, thisNode, prevNode=None):
    receiptVersion = receipt.getVersion()
    if prevNode is None:
        prevNodeName = None
    else:
        prevNodeName = prevNode.nodeName

    thisNodeName = thisNode.nodeName

    if _g_debugLevel > 1:
        print 'DEBUG: >>>>>>>>>> walkReceipt <<<<<<<<<<'
        print 'DEBUG: level ', level
        print 'DEBUG: thisNodeName ', thisNodeName
        print 'DEBUG: prevNodeName ', prevNodeName

    # Grab any attributes that we can at this level
    part = qty = desc = None
    if thisNode.hasAttribute('part'):
        part = thisNode.getAttribute('part')
    if thisNode.hasAttribute('desc'):
        desc = thisNode.getAttribute('desc')
    if thisNode.hasAttribute('qty'):
        qty = int(float(thisNode.getAttribute('qty')))

    if _g_debugLevel > 1:
        print 'DEBUG: part ', part
        print 'DEBUG: desc ', desc
        print 'DEBUG: qty ', qty

    if thisNodeName in [ 'bom' ]:
        thisBom = ReceiptBOM(level)
        receipt.addBom(thisBom)
    elif thisNodeName in [ 'sbb' ]:
        thisSbb = ReceiptSBB(level, part, desc, qty)
        if prevNodeName in [ 'bom' ]:
            bom = receipt.getLastBom()
            bom.addSbb(thisSbb)
        else:
            bom = receipt.getLastBom()
            sbb = bom.getLastSbb()
            sbb.addSbb(thisSbb)

    hasChild = thisNode.hasChildNodes and thisNode.hasChildNodes()
    children = hasChild and thisNode.childNodes or None
    if children:
        for child in children:
            childLevel = level + 1
            if child.nodeType is child.ELEMENT_NODE:
                walkReceipt(childLevel, receipt, child, thisNode)
            elif child.nodeType is child.ATTRIBUTE_NODE:
                raise ctoException('walkReceipt: detected attribute XML node')
            elif child.nodeType is child.TEXT_NODE:
                pass
            else:
                raise ctoException('walkReceipt: detected unknown XML node')

#----------------------------------------------------------------------
def sanityCheckReceipt(xmlDoc):

    # Make sure basic header information is present
    firstChild = xmlDoc.firstChild
    if firstChild.nodeName != 'receipt':
        raise ctoException('not a receipt')
    if not firstChild.hasAttribute('receiptFormatVersion'):
        raise ctoException('missing format version')
    if not firstChild.hasAttribute('date'):
        raise ctoException('missing date')
    if not firstChild.hasAttribute('receiptFileName'):
        raise ctoException('missing filename')

    # Make sure we support this version of the receipt format
    receiptVersion = firstChild.getAttribute('receiptFormatVersion')
    if float(receiptVersion) < float(OLDEST_SUPPORTED_RECEIPT_FORMAT_VERSION):
        raise ctoException('format version is too old -- Receipt="%1.1f", '
                           'oldest version supported by OneFS="%1.1f"' %
                           (float(receiptVersion), 
                           float(OLDEST_SUPPORTED_RECEIPT_FORMAT_VERSION)))
    if float(receiptVersion) > float(SUPPORTED_RECEIPT_FORMAT_VERSION):
        raise ctoException('format version is too new -- Receipt="%1.1f", '
                           'newest version supported by OneFS="%1.1f"' %
                           (float(receiptVersion), 
                           float(SUPPORTED_RECEIPT_FORMAT_VERSION)))

    # All receipts should have a BOM
    bomElements = xmlDoc.getElementsByTagName('bom')
    if len(bomElements) == 0:
        raise ctoException('no BOM')
    if bomElements.length > MAX_ALLOWABLE_BOMS_PER_RECEIPT:
        raise ctoException('more than %s BOM(s)' % 
                           MAX_ALLOWABLE_BOMS_PER_RECEIPT)

    # All receipts (BOMs) should contain SBBs and each SBB should
    # specifiy a part number, quantity and description
    sbbElements = xmlDoc.getElementsByTagName('sbb')
    if len(sbbElements) == 0:
        raise ctoException('no SBBs')
    for sbbElement in sbbElements:
        if not sbbElement.hasAttribute('part'):
            raise ctoException('"%s" -- no P/N' % sbbElement.toxml())
        if not sbbElement.hasAttribute('desc'):
            raise ctoException('"%s" -- no description' % sbbElement.toxml())
        if not sbbElement.hasAttribute('qty'):
            raise ctoException('"%s" -- no quantity' % sbbElement.toxml())

#----------------------------------------------------------------------
def importReceipt(fileType, filePath):
    status = 0

    xmlDoc = parseXmlFile(filePath)
    try:
        if xmlDoc is not None:
            sanityCheckReceipt(xmlDoc)
    except ctoException as e:
        print 'ERROR: Problem with receipt -- %s' % e.value
        return -1

    if xmlDoc is None:
        return -1

    firstChild = xmlDoc.firstChild
    version = firstChild.getAttribute('receiptFormatVersion')
    date = firstChild.getAttribute('date')
    filename = firstChild.getAttribute('receiptFileName')

    level = 0
    receipt = Receipt(level, version, date, filename)

    level += 1
    bomElements = xmlDoc.getElementsByTagName('bom')
    for bomElement in bomElements:
        walkReceipt(level, receipt, bomElement)

    if _g_debugLevel > 0:
        print 'DEBUG: Internal copy of receipt is as follows:'
        receipt.dump()

    # If we made it this far, it's ok to import the receipt
    status = saveFile(fileType, filePath)

    if status == 0:
        # Make this the current receipt
        mfgDir = os.path.join(ROOT_DIR, MFG_DIR, RECEIPT_SUBDIR)
        filename = os.path.basename(filePath)
        sourceFile = os.path.join(mfgDir, filename)
        destFile = os.path.join(mfgDir, RECEIPT_FILENAME)
        status = copyFile(sourceFile, destFile)

    return status

#----------------------------------------------------------------------
def getCurrentReceipt(filePath):
    status = 0
    receipt = None

    try:
        status = mountMfg()
        if status == 0:
            xmlDoc = parseXmlFile(filePath)
            try:
                if xmlDoc is not None:
                    sanityCheckReceipt(xmlDoc)
            except ctoException as e:
                print 'ERROR: Problem with receipt -- %s' % e.value
                status = -1

            if xmlDoc is None:
                status = -1

        if status == 0:
            firstChild = xmlDoc.firstChild
            version = firstChild.getAttribute('receiptFormatVersion')
            date = firstChild.getAttribute('date')
            filename = firstChild.getAttribute('receiptFileName')

            level = 0
            receipt = Receipt(level, version, date, filename)

            level += 1
            bomElements = xmlDoc.getElementsByTagName('bom')
            for bomElement in bomElements:
                walkReceipt(level, receipt, bomElement)

    finally:
        unmountMfg()

    return receipt

#======================================================================
#========================= End of Receipts ============================
#======================================================================

#======================================================================
#========================= Hardware Attributes ========================
#======================================================================

#----------------------------------------------------------------------
def getChassisType():
    chassisType = hal.getChassisType(0)
    if _g_debugLevel > 0:
        print 'DEBUG: chassisType is "%s"' % chassisType
    return chassisType

#----------------------------------------------------------------------
def getNumBatteries():
    numBatteries = hal.getNumBatteries()
    if _g_debugLevel > 0:
        print 'DEBUG: numBatteries is "%s"' % numBatteries
    return numBatteries

#----------------------------------------------------------------------
def getNvramType():
    nvramType = hal.getNvramType()
    if _g_debugLevel > 0:
        print 'DEBUG: nvramType is "%s"' % nvramType
    return nvramType

#----------------------------------------------------------------------
def getDramInfo():
    cmd = None
    numDimms = 0
    dramSize = 0
    dramConfig = ''

    unitConversion = { 'MB': MEGABYTE, 'GB' : GIGABYTE, 'TB' : TERABYTE }
    humanAbbr = (
        (1<<50L, 'PB'),
        (1<<40L, 'TB'),
        (1<<30L, 'GB'),
        (1<<20L, 'MB'),
        (1<<10L, 'kB'),
        (1, 'bytes'),
    )

    # Get the probe command for memory from the probe configuration
    ctx = gconfig.open(hal.gcfg_probeconfig).ctx_new()
    pcs = ctx.read('probeconfigs')
    for pc in pcs:
        if pc['name'] == 'Memory':
            cmd = pc['probe_cmd']
            break
    if cmd is None:
        raise ctoException('Unable to locate probe command for memory')
    
    (error, output) = procs.get_cmd_output(cmd)
    if error:
        print 'ERROR: Memory probe failed'
        for line in output:
            print line
    else:
        numDimms = len(output)
        if _g_debugLevel > 0:
            print 'DEBUG: numDimms %s' % numDimms
        for line in output:
            m0 = re.match('^.*Size: \d+ (?P<value>\w+)', line)
            if m0:
                units = m0.group('value')
                multiplier = unitConversion.get(units)
                if _g_debugLevel > 0:
                    print 'DEBUG: units %s' % units
                    print 'DEBUG: multiplier %s' % multiplier

            m1 = re.match('^.*Size: (?P<value>\d+)', line)
            if m1:
                size = m1.group('value')
                dramSize += long(size)
                if _g_debugLevel > 0:
                    print 'DEBUG: size %s' % size
                    print 'DEBUG: dramSize %s' % dramSize

    # Calculate actual size of (total) DRAM (in bytes)
    if dramSize > 0 and multiplier is not None:
        dramSize *= multiplier
        if _g_debugLevel > 0:
            print 'DEBUG: dramSize (after multiplier) %s' % dramSize

    # Size of a single DIMM, based on the fact that all DIMMs are the same size
    dimmSize = dramSize / numDimms

    # Generate configuration by combining numCpus, numDimms and DIMM size,
    # accounting for the fact that in multiple CPU configurations, the 
    # DIMMs are always spread evenly between all CPUs (symetrical)
    numCpus = hal.getCpuPackageCount()
    if _g_debugLevel > 0:
        print 'DEBUG: numCpus %s' % numCpus
    for factor, suffix in humanAbbr:
        if _g_debugLevel > 0:
            print 'DEBUG: factor %s, suffix %s' % (factor, suffix)
        if dimmSize >= factor:
            break

    if _g_debugLevel > 0:
        print 'DEBUG: dramSize (post factor) %s' % dramSize

    realDimmSize = '%.*f%s' % (0, dimmSize / factor , suffix)
    if _g_debugLevel > 0:
        print 'DEBUG: realDimmSize %s' % realDimmSize

    if numCpus > 0 and numDimms > 0 and realDimmSize > 0:
        if isOdd(numCpus):
            dramConfig = "%sx%sx%s" % (numCpus, numDimms, realDimmSize)
        else:
            dramConfig = "%sx%sx%s" % (numCpus, numDimms/numCpus, realDimmSize)
        if _g_debugLevel > 0:
            print 'DEBUG: dramConfig %s' % dramConfig

    return (dramSize, dramConfig)

#----------------------------------------------------------------------
def getDramSize():
    (dramSize, dramConfig) = getDramInfo()
    if _g_debugLevel > 0:
        print 'DEBUG: dramSize is "%s"' % dramSize
    return dramSize

#----------------------------------------------------------------------
def getDramConfig():
    (dramSize, dramConfig) = getDramInfo()
    if _g_debugLevel > 0:
        print 'DEBUG: dramConfig is "%s"' % dramConfig
    return dramConfig

#----------------------------------------------------------------------
def getSefcType():
    sefcType = hal.getSefcType()
    if _g_debugLevel > 0:
        print 'DEBUG: sefcType is "%s"' % sefcType
    return sefcType

#----------------------------------------------------------------------
def getInfinibandType():
    infinibandType = hal.getInfinibandType()
    if _g_debugLevel > 0:
        print 'DEBUG: infinibandType is "%s"' % infinibandType
    return infinibandType

#----------------------------------------------------------------------
def getNumAddonNics(typeName):
    numNics = 0

    # Get the probe command for the I/O cards from the probe configuration
    ctx = gconfig.open(hal.gcfg_probeconfig).ctx_new()
    pcs = ctx.read('probeconfigs')
    cmds = []
    for pc in pcs:
        m = re.match('^.* (?P<value>(10)?GigE)', pc['name'])
        if m and m.group('value') == typeName:
            cmds.append(pc['probe_cmd'])
    if len(cmds) == 0:
        raise ctoException('Unable to locate probe command for I/O')
    
    for cmd in cmds:
        (error, output) = procs.get_cmd_output(cmd)
        if error and _g_debugLevel > 0:
            print 'DEBUG:  I/O probe failed'
            for line in output:
                print line
        else:
            numNics += len(output)

    return numNics

#----------------------------------------------------------------------
def getNumAddonGigeNics():
    numNics = getNumAddonNics('GigE')
    if numNics == 0:
        print 'ERROR:  GigE I/O probes failed'
    if _g_debugLevel > 0:
        print 'DEBUG: numNics (GigE) is "%s"' % numNics
    return numNics

#----------------------------------------------------------------------
def getNumAddon10GigeNics():
    numNics = getNumAddonNics('10GigE')
    if numNics == 0:
        print 'ERROR:  10GigE I/O probes failed'
    if _g_debugLevel > 0:
        print 'DEBUG: numNics (10GigE) is "%s"' % numNics
    return numNics

#----------------------------------------------------------------------
def getNumSsds():
    numSsds = hal.getNumSsds()
    if _g_debugLevel > 0:
        print 'DEBUG: numSsds is "%s"' % numSsds
    return numSsds

#----------------------------------------------------------------------
def getNumHdds():
    numHdds = hal.getNumHdds()
    if _g_debugLevel > 0:
        print 'DEBUG: numHdds is "%s"' % numHdds
    return numHdds

#----------------------------------------------------------------------
def getLcdType():
    lcdType = hal.getLcdType()
    if _g_debugLevel > 0:
        print 'DEBUG: lcdType is "%s"' % lcdType
    return lcdType

#======================================================================
#======================= End of Hardware Attributes ===================
#======================================================================

#======================================================================
#============================= Inventory ==============================
#======================================================================

#----------------------------------------------------------------------
class Subassembly:
    def __init__(self, level, hwType, hwName, vendorId, serialNo, 
        model, revision, desc, firmware, meta):

        self._level = level
        self._info = {
            'hwType' : hwType,
            'hwName' : hwName,
            'vendorId' : vendorId,
            'serialNo' : serialNo,
            'model' : model,
            'revision' : revision,
            'desc' : desc,
            'firmware' : firmware,
            'meta' : meta,
            'partNos' : [],
            }
        self._isAccountedFor = False
        self._matchingPartNo = None

    def getLevel(self):
        return self._level

    def getHwType(self):
        return self._info['hwType']

    def getHwName(self):
        return self._info['hwName']

    def getVendorId(self):
        return self._info['vendorId']

    def getSerialNo(self):
        return self._info['serialNo']

    def getModel(self):
        return self._info['model']

    def getRevision(self):
        return self._info['revision']

    def getDesc(self):
        return self._info['desc']

    def getFirmware(self):
        return self._info['firmware']

    def getMeta(self):
        return self._info['meta']

    def addPartNos(self, partNos):
        for partNo in partNos:
            # Sanity check the format of each part number before adding
            if not partNoHasValidFormat(partNo):
                raise ctoException('P/N="%s" is not valid' % partNo)
            else:
                self._info['partNos'].append(partNo)

    def getNumPartNos(self):
        return len(self._info['partNos'])

    def getPartNos(self):
        return self._info['partNos']

    def matchesPartNo(self, partNo):
        part = None
        matches = False
        for subPartNo in self._info['partNos']:
            # If any of the P/Ns match, then we've got a match
            if partNo == str(subPartNo):
                part = str(subPartNo)
                matches = True
                break
        return (matches, part)

    def getTabLevel(self, partNo):
        partTab = None 
        checkForTabRegex = re.compile(".*\-(?P<tab>\d{2})$")
        match = checkForTabRegex.match(partNo)
        if match:
            partTab = match.group('tab')
        return partTab

    def getPartNumberWithoutTab(self, partNo):
        partNumber = None
        partNumberRegex = re.compile("(?P<pn>.*)\-\d{2}$")
        match = partNumberRegex.match(partNo)
        if match:
            partNumber = match.group('pn')
        return partNumber

    def isImplicitSubstitute(self, partNo):
        part = None
        validSub = False
        # Sanity check part number format since we rely upon
        # all 11 characters when we strip tab level
        if not partNoHasValidFormat(partNo):
            raise ctoException('P/N="%s" is not valid' % partNo)

        partTab = self.getTabLevel(partNo)
        if partTab is None:
            return (validSub, part)
        for subPartNo in self._info['partNos']:
            # Sanity check part number format
            if not partNoHasValidFormat(subPartNo):
                raise ctoException('P/N="%s" is not valid' % partNo)
            
            subTab = self.getTabLevel(subPartNo)
            if subTab is None:
                continue
            # Subassembly is implicitly allowed to be a substitute if it's
            # the same P/N with a higher (newer) tab level
            partNoWithoutTab = self.getPartNumberWithoutTab(partNo)
            subPartNoWithoutTab = self.getPartNumberWithoutTab(subPartNo)
            if ((partNoWithoutTab == subPartNoWithoutTab) and 
                int(subTab) > int(partTab)):
                part = str(subPartNo)
                validSub = True
                break
        return (validSub, part)

    def isAccountedFor(self):
        return self._isAccountedFor

    def assertIsAccountedFor(self):
        self._isAccountedFor = True

    def setMatchingPartNo(self, partNo):
        self._matchingPartNo = partNo

    def getMatchingPartNo(self):
        if self._matchingPartNo is None:
            # Make sure we return index of None if P/N is None
            index = None
        else:
            index = self._info['partNos'].index(self._matchingPartNo)
        return (index, self._matchingPartNo)

    def dump(self):
        prettyPrint(self._level, 'Subassembly: Type="%s", Name="%s", '
                    'Vendor ID="%s", S/N="%s", Model="%s", Revision="%s", '
                    'Description="%s", Firmware="%s", Meta Data="%s", '
                    'P/N(s)="%s", isAccountedFor="%s", Matching P/N="%s"' %
                    (self._info['hwType'], self._info['hwName'],
                    self._info['vendorId'], self._info['serialNo'],
                    self._info['model'], self._info['revision'],
                    self._info['desc'], self._info['firmware'],
                    self._info['meta'], ', '.join(self._info['partNos']),
                    self._isAccountedFor, self._matchingPartNo))

#----------------------------------------------------------------------
class Inventory:
    def __init__(self):
        self._date = getCurrentDateTime()
        self._subassemblies = []

    def addSubassembly(self, subassembly):
        self._subassemblies.append(subassembly)

    def hasSubassemblies(self):
        return len(self._subassemblies) > 0

    def getSubassemblies(self):
        return self._subassemblies

    def getLastSubassembly(self):
        return self._subassemblies[len(self._subassemblies) - 1]

    def getNumSubassemblies(self):
        return len(self._subassemblies)

    def dump(self):
        print('### Start of Inventory Dump ###')
        print('Inventory taken on %s' % self._date)
        for subassembly in self._subassemblies:
            subassembly.dump()
        print('### End of Inventory Dump ###')

#----------------------------------------------------------------------
def probeHardware(probeCmd, hwType, hwName):
    qty = 0
    startTime = None
    endTime = None

    if probeCmd and len(probeCmd) > 0:
        if _g_debugLevel > 0:
            print ('DEBUG: Probing for hwType="%s", hwName="%s"' %
                (hwType, hwName))
        # NOTE: Timing is for debug only
        startTime = time.time()
        (error, output) = procs.get_cmd_output(probeCmd)
        endTime = time.time()
        elapsedTime = endTime - startTime
        if error:
            if _g_debugLevel > 0:
                for line in output:
                    print line
                print ('DEBUG: Probe ("probeCmd=%s") failed for '
                       'hwType="%s", hwName="%s"' % (probeCmd, hwType, hwName))
        elif len(output) > 0:
            # Probe command produces one (1) line of output 
            # per instance of hardware
            qty = len(output)

    if _g_debugLevel > 0:
        print ('DEBUG: Found "%s" hwType="%s", hwName="%s"' %
               (qty, hwType, hwName))
        if startTime is not None and endTime is not None:
            print 'DEBUG: Probe took %.4f seconds' % elapsedTime

    return qty

#----------------------------------------------------------------------
def getHardwareVPD(vpdCmd, vpdFilter, hwName, qty):
    vpd = []

    if vpdCmd and len(vpdCmd) > 0 and vpdFilter and len(vpdFilter) > 0:
        cmd = '%s | %s' % (vpdCmd, vpdFilter)
        (error, output) = procs.get_cmd_output(cmd)
        if _g_debugLevel > 1:
            print ('DEBUG: Get VPD (vpdCmd="%s", vpdFilter="%s") '
                   'got following output:' % (vpdCmd, vpdFilter))
            for line in output:
                print ' -| %s' % line
        if error:
            if _g_debugLevel > 0:
                for line in output:
                    print line
                print ('DEBUG: Get VPD (vpdCmd="%s", vpdFilter="%s") '
                       'failed for hwName="%s"' % (vpdCmd, vpdFilter, hwName))
        elif len(output) == 0:
            # Sanity check - Although there was no error generated,
            #                make sure there was output produced
            print 'WARNING: Unable to read VPD for the following:'
            print ' hwName="%s"' % hwName
            print ' vpdCmd="%s"' % vpdCmd
            print ' vpdFilter="%s"' % vpdFilter
        elif len(output) > 0:
            for line in output:
                vpd.append(line)

    # If the hardware does not support this particular piece of VPD
    # (i.e. vpdCmd and/or vpdFilter are empty strings) or we were
    # unable to read the VPD from the hardware, force the VPD string(s)
    # to indicate "N/A" or "Not Available/Applicable" for the quantity
    # of parts we found during probe.
    if len(vpd) == 0:
        for i in range(qty):
            vpd.append(NOT_AVAILABLE)

    # Now explicity replace any NULL strings
    for vpdString in vpd:
        if vpdString == '':
            index = vpd.index('')
            vpd[index] = NOT_AVAILABLE

    return vpd

#----------------------------------------------------------------------
def generateHwCapabilities(mfgProbeConfig=False):
    status = 0
    hwCapabilities = []
    hwCapabilityDetails = {}

    baseDetails = None
    driveDetails = None
    memoryDetails = None
    ioDetails = None

    basePart = None
    drivePart = None
    memoryPart = None
    ioPart = None

    # Get our current receipt
    if status == 0:
        receipt = getCurrentReceipt(currentReceiptFilePath())
        if receipt is not None:
            if _g_debugLevel > 1:
                print 'DEBUG: Internal copy of receipt is as follows:'
                receipt.dump()
        else:
            raise ctoException('Chassis is missing a receipt')

    # Extract the SBBs from the receipt
    if status == 0:
        receiptBoms = receipt.getBoms()
        for receiptBom in receiptBoms:
            receiptSbbs = receiptBom.getSbbs()
            for receiptSbb in receiptSbbs:
                part = receiptSbb.getPart()
                # Node capabilities are tab level agnostic, so ignore the
                # tab level when looking up the SBB in the configuration
                part = part[:-3]
                # Get the configuration details for each of the SBBs
                (sbbType, sbbDetails) = getSbbConfigurationDetails(part, mfgProbeConfig)
                if sbbDetails is not None and sbbType is not None:
                    if sbbType == 'base_chassis_sbb':
                        basePart = part
                        baseDetails = sbbDetails
                    elif sbbType == 'drive_sbb':
                        drivePart = part
                        driveDetails = sbbDetails
                    elif sbbType == 'memory_sbb':
                        memoryPart = part
                        memoryDetails = sbbDetails
                    elif sbbType == 'io_sbb':
                        ioPart = part
                        ioDetails = sbbDetails
                    else:
                        raise ctoException('Unexpected SBB type - %s' % sbbType)

                    # Unless we run off the end of the receipt, once we
                    # have all of them, we're done
                    if (basePart is not None and drivePart is not None and 
                        memoryPart is not None and ioPart is not None):
                        break

    # Perform some basic sanity checks before continuing...
    #
    # NOTE: Because of how we set the part numbers and the details above,
    #       haveing a part number implies having the SBB details as well
    if not basePart:
        raise ctoException('Receipt is missing base chassis SBB')
    elif not _g_baseChassisBurnInFlag:
        if drivePart is None or memoryPart is None or ioPart is None:
            raise ctoException('Receipt is missing at least one (1) required '
                               'SBB (Drive="%s", Memory="%s", I/O="%s")' % 
                               (drivePart, memoryPart, ioPart))
    elif _g_baseChassisBurnInFlag:
        if (drivePart is not None or memoryPart is not None or 
            ioPart is not None):
            raise ctoException('Receipt contains at least one (1) unexpected '
                               'SBB for Base Chassis Burn-In. (Drive="%s", '
                               'Memory="%s", I/O="%s")' % 
                               (drivePart, memoryPart, ioPart))

    if _g_debugLevel > 0:
        print 'DEBUG: Base chassis SBB P/N', basePart
        print 'DEBUG: Drive configuration SBB P/N', drivePart
        print 'DEBUG: Memory configuration SBB P/N', memoryPart
        print 'DEBUG: I/O configuration SBB P/N', ioPart

    # Set base chassis capabilities
    hwCapabilityDetails = baseDetails

    # If we're doing Base Chassis Burn-In, then we have to map the 
    # base chassis SBB to the agreed upon drive, memory and I/O
    # SBBs for burn-in testing...
    if _g_baseChassisBurnInFlag:
        sbbMap = getSbbMap(basePart, mfgProbeConfig)
        if sbbMap is None:
            raise ctoException('Unable to map base chassis SBB for Base '
                               'Chassis Burn-In')
        drivePart = sbbMap['drive']
        memoryPart = sbbMap['memory']
        ioPart = sbbMap['io']

    # Before continuing, there are a few fields in the SBB configurations
    # (DRAM configuration and IB NIC type) that need to be resolved now 
    # since they are not known a priori (i.e. when creating the gconfig file)

    # DRAM configuration
    #
    # Configuration is comprised of numCpus, numDimms and DIMM size. 
    # Account for the fact that in multiple CPU configurations, the 
    # DIMMs should always be spread evenly between all CPUs (symetrical)
    numCpus = baseDetails['num_cpus']
    cpuString = '%sx' % numCpus
    if isOdd(numCpus):
        # String in probe configuration file assumes single CPU
        memoryDetails['dram_config'] = cpuString + memoryDetails['dram_config']
    else:
        origConfig = memoryDetails['dram_config']
        m = re.match('^(?P<no_dimms>\w+)x(?P<dimm_size>\w+)', origConfig)
        if m:
            numDimms = m.group('no_dimms')
            dimmSize = m.group('dimm_size')
        else:
            raise ctoException('Unable to parse dram_config ("%s") ' % 
                               configString)
        modifiedConfig = "%sx%s" % (int(numDimms)/numCpus, dimmSize)
        memoryDetails['dram_config'] = cpuString + modifiedConfig

    # Ethernet NIC names
    mobo = hal.getMotherboardType()
    slotmap = _g_slotMaps.get(mobo, None)
    nic_mapping = {}
    order_top = {}
    order_list = {}
    for typ in [ flexnet.NIC_TYPE_GIGE,
                 flexnet.NIC_TYPE_10_GIGE,
                 flexnet.NIC_TYPE_IB,
                 flexnet.NIC_TYPE_IB_QDR,
                 flexnet.NIC_TYPE_IB_FDR ]:
        # set all type ordering to 0
        order_top[typ] = 0
        order_list[typ] = list()
    if slotmap is not None:
        # This should only be carried out if we have a slot mapping, which
        # begins with the Canoe Pass (S2600CP) motherboard.
        ifaces = []
        cmd = '/sbin/ifconfig -l'
        (error, output) = procs.get_cmd_output(cmd)
        if error:
            print 'ERROR: ifconfig command failed'
            for line in output:
                print line
            output = None

        if output is not None:
            ifaces = output[0].split()

        for iface in ifaces:
            nic_type = hal.getNicTypeFromName(iface)
            if nic_type is None:
                # if we don't know, we can't do anything with it
                if not iface.startswith('lo'):
                    print 'ERROR: Unable to determine NIC type for %s' % iface
                continue

            # group is really type groupings, so for 1GigE and
            # 10GigE group can equate to those types.  IB
            # doesn't get a grouping currently
            group = -1
            if nic_type == flexnet.NIC_TYPE_GIGE:
                group = 0
            elif nic_type == flexnet.NIC_TYPE_10_GIGE:
                group = 1

            # Chelsio T3/T5 presents a single pci device with multiple interfaces
            cxgbN_re = re.compile('(%s)(?P<unit>[0-9]+)' % CHELSIO_IFCONFIG_STR)
            if cxgbN_re.match(iface):
                pciconf_cmd = '/usr/sbin/pciconf -l | grep -E "%s"' % CHELSIO_PCICONF_STR
            else:
                pciconf_cmd = '/usr/sbin/pciconf -l | grep %s' % iface
            (error, output) = procs.get_cmd_output(pciconf_cmd)
            if not error:
                line = output[0]
                m = re.match('^.*\@(?P<pcib>pci\d+:\d+:\d+:\d+):', line)
                if m:
                    pciinfo = m.group('pcib').split(':')

                    # search for interfaces onboard, not in a slot, this
                    # should snatch up the remainder of the devices.
                    for idx in range(0,len(slotmap['onboard'])):
                        slot = slotmap['onboard'][idx]
                        if slot is None:
                            continue
                        slotinfo = slot.split(':')
                        if pciinfo == slotinfo:
                            # pciinfo comes from pciconf itself
                            # type we get from HAL via name lookup
                            # group we got and explain above
                            # order is explained below

                            # regarding order, for onboard we map explicitly,
                            # as we know exactly what's there
                            order = idx
                            nic_mapping[iface] = {
                                                 'onboard' : True,
                                                 'slot'    : idx,
                                                 'pciinfo' : m.group('pcib'),
                                                 'type'    : nic_type,
                                                 'order'   : order,
                                                 'group'   : group,
                                                 }
                            order_top[nic_type] = max(order,
                                order_top[nic_type])
                            order_list[nic_type].append(order)
                        else:
                            continue

                    for idx in range(0,len(slotmap['slot'])):
                        slot = slotmap['slot'][idx]
                        if slot is None:
                            continue
                        slotinfo = slot.split(':')
                        if pciinfo[:-1] == slotinfo[:-1]:
                            # pciinfo comes from pciconf itself
                            # type we get from HAL via name lookup
                            #
                            # regarding order, for slots this can get a bit
                            # tricky.  What we do here is make the order
                            # base_order + slot + unit.  If we have gaps, we
                            # can collapse those after

                            # unit starts at 0, but if slotinfo has an 'X' in
                            # the specifier, we modify unit to be that number
                            unit = 0
                            cxgbN_m = cxgbN_re.match(iface)
                            if cxgbN_m:
                                unit = cxgbN_m.group('unit')
                                # historical, but cxgbX ports are mapped 1, 0
                                # so invert unit mapping
                                if unit == 0:
                                    unit = 1
                                elif unit == 1:
                                    unit = 0
                            elif 'X' in slotinfo:
                                unit = pciinfo[-1]
                            order = (order_top[nic_type]+1) + (4*idx)+int(unit)
                            nic_mapping[iface] = {
                                                 'onboard' : False,
                                                 'slot'    : idx,
                                                 'pciinfo' : m.group('pcib'),
                                                 'type'    : nic_type,
                                                 'order'   : order,
                                                 'group'   : group,
                                                 }
                            order_list[nic_type].append(order)
                        else:
                            continue

    # collapse ordering down so there are no gaps
    for key in order_list.keys():
        order_list[key].sort()
    for key in nic_mapping.keys():
        d = nic_mapping[key]
        d['order'] = order_list[d['type']].index(d['order'])
 
    # IB NIC type
    ibNicMap = getIbNicMap(basePart, mfgProbeConfig)
    if ibNicMap is None:
        raise ctoException('Unable to map base chassis SBB to IB NIC')

    # fill in netinfo details
    for interface in ioDetails['netinfo']:
        if interface['type'] < 0:
            interface['type'] = ibNicMap['nic_type']
        else:
            for k in nic_mapping.keys():
                v = nic_mapping[k]
                if v['type'] == interface['type']:
                    interface['name'] = k
                    interface['pciinfo'] = v.get('pciinfo', None)
                    interface['group'] = v.get('group', None)
                    interface['order'] = v.get('order', None)
                    del nic_mapping[k]
                    break

    # Set drive capabilities
    hwCapabilityDetails.update(driveDetails)
    
    # Set memory capabilities
    hwCapabilityDetails.update(memoryDetails)

    # Set I/O capabilities
    hwCapabilityDetails.update(ioDetails)

    return (status, hwCapabilities, hwCapabilityDetails)

#----------------------------------------------------------------------
def revertHwCapabilitiesToDefault():
    status = 0
    hwCapabilities = []
    hwCapabilityDetails = {}

    if _g_debugLevel > 0:
        print 'DEBUG: Reverting hardware capabilities to defaults'

    ctx = gconfig.open(hal.gcfg_hwcapabilities).ctx_new()
    ctx.revert_to_default('')
    try:
        ctx.commit()
    except isi.error.IsiSystemError, e:
        print ('Exception ("%s") while reverting hardware capabilities '
               'to defaults' % str(e))
        status = -1

    return (status, hwCapabilities, hwCapabilityDetails)

#----------------------------------------------------------------------
def writeUpdatedHwCapabilities(hwCapabilities, hwCapabilityDetails):
    status = 0

    if _g_debugLevel > 0:
        print 'DEBUG: Writing hardware capability cache'

    hwCapabilities.append(hwCapabilityDetails)
    ctx = gconfig.open(hal.gcfg_hwcapabilities).ctx_new()
    try:
        ctx.write('hwcapabilities', hwCapabilities)
        ctx.commit()
    except isi.error.IsiSystemError, e:
        print ('Exception ("%s") while writing and/or committing '
               'hardware capability cache' % str(e))
        status = -1

    return status

#----------------------------------------------------------------------
def revertHwAttributesToDefault():
    status = 0
    hwAttributes = []
    hwAttributeDetails = {}

    if _g_debugLevel > 0:
        print 'DEBUG: Reverting hardware attributes to defaults'

    ctx = gconfig.open(hal.gcfg_hwattributes).ctx_new()
    ctx.revert_to_default('')
    try:
        ctx.commit()
    except isi.error.IsiSystemError, e:
        print ('Exception ("%s") while reverting hardware attributes '
               'to defaults' % str(e))
        status = -1

    return (status, hwAttributes, hwAttributeDetails)

#----------------------------------------------------------------------
def writeUpdatedHwAttributes(hwAttributes, hwAttributeDetails):
    status = 0

    if _g_debugLevel > 0:
        print 'DEBUG: Writing hardware attribute cache'

    hwAttributes.append(hwAttributeDetails)
    ctx = gconfig.open(hal.gcfg_hwattributes).ctx_new()
    try:
        ctx.write('hwattributes', hwAttributes)
        ctx.commit()
    except isi.error.IsiSystemError, e:
        print ('Exception ("%s") while writing and/or committing '
               'hardware attribute cache' % str(e))
        status = -1

    return status

#----------------------------------------------------------------------
def generateInventory(updateHwCaches=False, softAVL=None, mfgProbeConfig=False):

    version = None
    usedOverrides = False
    ctx = None

    if updateHwCaches:
        (status, hwAttributes, hwAttributeDetails) = (
            revertHwAttributesToDefault())
        if status != 0:
            print 'ERROR: Unable to revert hardware attributes'
            return None

        (status, hwCapabilities, hwCapabilityDetails) = (
            revertHwCapabilitiesToDefault())
        if status != 0:
            print 'ERROR: Unable to revert hardware capabilities'
            return None

        (status, hwCapabilities, hwCapabilityDetails) = (
            generateHwCapabilities(mfgProbeConfig))
        if status != 0:
            raise ctoException('Unable to generate hardware capabilities')

    if updateHwCaches:
        hwCapabilityDetails['initted'] = True
        writeUpdatedHwCapabilities(hwCapabilities, hwCapabilityDetails)
        # Force HAL to see the updated capabilities
        hal.reInitialize()

    if hasSAS2Expander():
        if _g_debugLevel > 0:
            print 'DEBUG: Setting hardware (HAL) overrides'
        if doOverrides('set') != 0:
            print 'ERROR: Unable to set hardware HAL overrides'
        usedOverrides = True

    print 'Probing hardware...'
    inventory = Inventory()

    if not mfgProbeConfig:
        ctx = gconfig.open(hal.gcfg_probeconfig).ctx_new()
    else:
        try:
            status = mountMfg()
            if status == 0:
                mfgPath = mfgProbeConfigFilePath()
                treeSeed = gconfig.Tree('probeconfig').tree_seed()
                ctxTree = gconfig.create_tree(treeSeed, primary=mfgPath)
                ctx = gconfig.open(ctxTree).ctx_new()
        finally:
            unmountMfg()

    if ctx is None:
        print 'ERROR: Unable to open probeconfig tree'
        return None

    versionString = ctx.read('version')
    m = re.match('^.*%s (?P<value>\w+)' % CURRENT_PROBE_FILENAME, versionString)
    if m:
        version = m.group('value')

    if _g_debugLevel > 0:
        print ('DEBUG: Probe configuration file, versionString="%s", '
            'version="%s"' % (versionString, version))

    if version is None:
        print 'WARNING: Unable to determine version of probe configuration file'
        
    pcs = ctx.read('probeconfigs')
    for pc in pcs:
        hwType = pc['type']
        hwName = pc['name']
        qty = probeHardware(pc['probe_cmd'], hwType, hwName)
        if qty is not None and qty > 0:
            startTime = time.time()
            vendorIds = getHardwareVPD(pc['vendorid_cmd'], 
                                       pc['vendorid_filter'], pc['name'], qty)
            models = getHardwareVPD(pc['model_cmd'], 
                                    pc['model_filter'], pc['name'], qty)
            descriptions = getHardwareVPD(pc['description_cmd'], 
                                          pc['description_filter'], 
                                          pc['name'], qty)
            serialNos = getHardwareVPD(pc['serialno_cmd'], 
                                       pc['serialno_filter'], pc['name'], qty)
            revisions = getHardwareVPD(pc['revision_cmd'], 
                                       pc['revision_filter'], pc['name'], qty)
            firmwares = getHardwareVPD(pc['firmware_cmd'], 
                                       pc['firmware_filter'], pc['name'], qty)
            metas = getHardwareVPD(pc['meta_cmd'], pc['meta_filter'], 
                                   pc['name'], qty)
            endTime = time.time()
            elapsedTime = endTime - startTime
            if _g_debugLevel > 0:
                print 'DEBUG: Reading hardware VPD took %.4f' % elapsedTime

            for i in range(qty):
                # If we've been handed a SoftAVL, we can also attempt
                # to lookup the Isilon part number(s) for the hardware
                # and attach them to the inventory at this point. 
                partNos = []
                if softAVL is not None:
                    try:
                        vid = vendorIds[i]
                        # Only include the firmware ID in the comparison
                        # if the SoftAVL indicates that it is required
                        fid = None
                        if softAVL.vendorIdRequiresFirmwareId(vid):
                            if (firmwares[i] is not None and 
                                firmwares[i] != NOT_AVAILABLE):
                                fid = firmwares[i]
                        if _g_debugLevel > 1:
                            print ('DEBUG: Looking up Isilon P/N(s) for '
                                   'vid="%s", fid="%s" in softAVL' % (vid, fid))
                        partNos = softAVL.idsToItemIdentifiers(vid, fid)
                        if len(partNos) == 0:
                            print ('WARNING: softAVL is missing Isilon P/N(s) '
                                   'for vendor ID="%s", firmware ID="%s", ' 
                                   'hwType="%s", hwName="%s"' % 
                                   (vid, firmwares[i], hwType, hwName))

                        if _g_debugLevel > 1:
                            print 'DEBUG: Isilon P/N(s)="%s"' % partNos
                
                        subassembly = (Subassembly(0, hwType, hwName, 
                                       vendorIds[i], serialNos[i], models[i], 
                                       revisions[i], descriptions[i], 
                                       firmwares[i], metas[i]))
                    except IndexError:
                        if _g_debugLevel > 0:
                            print ('DEBUG: IndexError detected - Bad probe '
                                   'and/or VPD results - discarding '
                                   'subassembly')
                        subassembly = None
                        partNos = []
                        pass

                    # If we were able to get the part number(s), add them now
                    if subassembly is not None and len(partNos) > 0:
                        subassembly.addPartNos(partNos)

                    if subassembly is not None:
                        inventory.addSubassembly(subassembly)

            if updateHwCaches:
                # Assert all the attributes for this piece of hardware
                for keyString in pc['hwattributes']:
                    if keyString != '':
                        # Check if Boolean or non-Boolean attribute
                        if keyString.count('=') == 0:
                            if _g_debugLevel > 0:
                                print ('DEBUG: Hardware attribute "%s" '
                                       'asserted to True' % keyString)
                            # Boolean attribute
                            hwAttributeDetails[keyString] = True
                        else:
                            # Non-Boolean attribute
                            m = re.match('^(?P<key>\w+)\s*=\s*(?P<value>\w+)', 
                                         keyString)
                            if m:
                                key = m.group('key')
                                value = m.group('value')
                                # If the value is an integer, the just
                                # set the attribute to the value. Otherwise,
                                # assume its a function name and call the
                                # function to get the value.
                                if (isinstance(value, int) or 
                                    (isinstance(value, str) and 
                                     value.isdigit())):
                                    value = int(value)
                                else:
                                    function = value
                                    if function in globals():
                                        value = globals()[function]()
                                    else:
                                        print ('ERROR: Function "%s()" does '
                                               'not exist' % function)
                                # NOTE: Some of these attributes may actually
                                #       be overridden below. 
                                hwAttributeDetails[key] = value
                                if _g_debugLevel > 0:
                                    print ('DEBUG: Hardware attribute "%s" '
                                           'asserted to "%s"' % (key, value))

    if usedOverrides:
        if _g_debugLevel > 0:
            print 'DEBUG: Unsetting hardware (HAL) overrides'
        if doOverrides('unset') != 0:
            print 'ERROR: Unable to unset hardware HAL overrides'

    if updateHwCaches:
        hwAttributeDetails['initted'] = True
        writeUpdatedHwAttributes(hwAttributes, hwAttributeDetails)

    return inventory

#----------------------------------------------------------------------
def importProbeConfig(fileType, filePath):
    status = 0
    newVersion = None
    currentVersion = None
    forceImport = False

    if isNpiFile(filePath) and not supportNpi():
        print ('Blocking import of "%s", NPI probe_config files '
               'not supported.' % filePath)
        return -1

    fileLevel = getProbeConfigLevel(filePath)
    if fileLevel is None:
        print 'ERROR: Unable to determine level of "%s"' % filePath
        status = -1
        return status

    currentPath = currentProbeConfigFilePath(fileLevel)

    try:
        status = mountMfg()
        if status == 0:
            mfgPath = mfgProbeConfigFilePath(fileLevel)
    
            # If the copy on the MFG partition is missing, force
            # an import to repair it
            mfgExists = os.access(mfgPath, os.F_OK)
            if not mfgExists:
                print 'WARNING: "%s" does not exist' % mfgPath
                forceImport = True

        # Get the respective versions of each file
        currentVersion = getProbeConfigVersion(currentPath)
        if currentVersion is None:
            print 'ERROR: Unable to get version of "%s"' % currentPath
            status = -1

        if status == 0:
            newVersion = getProbeConfigVersion(filePath)
            if newVersion is None:
                print 'ERROR: Unable to get version of "%s"' % filePath
                status = -1

        if status == 0:
            # If requested file is newer or the same as the current
            # file, then allow the import
            if float(newVersion) >= float(currentVersion):
                if _g_debugLevel > 0:
                    print ('"%s" (%s) is newer than "%s" (%s)' %
                           (filePath, newVersion, currentPath, currentVersion))

                print ('Importing "%s" version "%s" level "%s"' % 
                    (filePath, newVersion, fileLevel))

                # In MFG, we are handed probe configuration files that are
                # not named using the default naming convention 
                # (probe_config.gc) so check if we've been handed one of 
                # those files, and if so, copy it to a temporary file with 
                # the correct name before importing it so that our backup 
                # scheme remains intact.
                filename = os.path.basename(filePath)
                if fileLevel == 'legacy':
                    desiredFileName = LEGACY_PROBE_FILENAME
                else:
                    desiredFileName = '%s.%s.gc' % (BASE_PROBE_FILENAME,
                        fileLevel)
                if filename != desiredFileName:
                    tempFile = os.path.join(ROOT_DIR, 'tmp', desiredFileName)
                    status = copyFile(filePath, tempFile)
                    if status == 0:
                        status = saveFile(fileType, tempFile)

                    # Regardless of the success of the above, try and
                    # clean up the temporary file
                    cmd = '/bin/rm -f %s' % tempFile
                    (error, output) = procs.get_cmd_output(cmd)
                    if error:
                        for line in output:
                            print line
                        print 'ERROR: Unable to remove "%s"' % tempFile
                else:
                    status = saveFile(fileType, filePath)
            # If the MFG partition is missing the file and we're forcing
            # the import, then import the current version
            elif forceImport:
                print ('Importing "%s" version "%s"' %
                       (currentPath, currentVersion))
                status = saveFile(fileType, currentPath)
            else:
                # NOTE: The scripts in the field packages (CTO conversion, 
                #       FRU and field upgrade) rely on the first two (2) words
                #       of the message below to know that the failure was due
                #       to an out-of-date file, so DO NOT change the message.
                print ('Blocking import of out-of-date probe configuration. '
                       'Current probe configuration ("%s") is version "%s" and '
                       'the one being imported is version "%s".' %
                       (currentProbeConfigFilePath(fileLevel), currentVersion, 
                       newVersion))
                status = -1
    finally:
        unmountMfg()

    return status

#======================================================================
#========================= End of Inventory ===========================
#======================================================================

#======================================================================
#============================== SoftAVL ===============================
#======================================================================

#----------------------------------------------------------------------
class SoftAVL:
    def __init__(self, level, version, date, filename):

        self._level = level
        self._info = { 
            'version' : version, 
            'date' : date, 
            'filename' : filename,
            }
        self._allowedSubstitutionLists = []
        self._blackLists = []
        self._brownLists = []
        self._items = []
        self._supplierIDLists = []
    
    def getLevel(self):
        return self._level

    def getVersion(self):
        return self._info['version']

    def getDate(self):
        return self._info['date']

    def getFilename(self):
        return self._info['filename']

    def addAllowedSubstitutionList(self, allowedSubstitutionList):
        self._allowedSubstitutionLists.append(allowedSubstitutionList)

    def hasAllowedSubstitutionLists(self):
        return len(self._allowedSubstitutionLists) > 0

    def getAllowedSubstitutionLists(self):
        return self._allowedSubstitutionLists

    def getLastAllowedSubstitutionList(self):
        return self._allowedSubstitutionLists[
            len(self._allowedSubstitutionLists) - 1]
        
    def getNumAllowedSubstitutionLists(self):
        return len(self._allowedSubstitutionLists)

    def addBlackList(self, blackList):
        self._blackLists.append(blackList)

    def hasBlackLists(self):
        return len(self._blackLists) > 0

    def getBlackLists(self):
        return self._blackLists

    def getLastBlackList(self):
        return self._blackLists[len(self._blackLists) - 1]

    def getNumBlackLists(self):
        return len(self._blackLists)

    def addBrownList(self, brownList):
        self._brownLists.append(brownList)

    def hasBrownLists(self):
        return len(self._brownLists) > 0

    def getBrownLists(self):
        return self._brownLists

    def getLastBrownList(self):
        return self._brownLists[len(self._brownLists) - 1]

    def getNumBrownLists(self):
        return len(self._brownLists)

    def addItems(self, items):
        self._items.append(items)

    def hasItems(self):
        return len(self._items) > 0

    def getItems(self):
        return self._items

    def getLastItems(self):
        return self._items[len(self._items) - 1]

    def getNumItems(self):
        return len(self._items)

    def addSupplierIDList(self, supplierIDList):
        self._supplierIDLists.append(supplierIDList)

    def hasSupplierIDLists(self):
        return len(self._supplierIDLists) > 0

    def getSupplierIDLists(self):
        return self._supplierIDLists

    def getLastSupplierIDList(self):
        return self._supplierIDLists[len(self._supplierIDLists) - 1]

    def getNumSupplierIDLists(self):
        return len(self._supplierIDList)

    def getMatchingItem(self, itemIdentifier):
        matchingItem = None
        for items in self._items:
            item = items.getMatchingItem(itemIdentifier)
            if item is not None:
                matchingItem = item
                break
        return matchingItem

    def isBlackListed(self, itemIdentifier):
        blackListed = False
        for blackList in self._blackLists:
            if blackList.getMatchingItem(itemIdentifier) is not None:
                blackListed = True
                break
        return blackListed

    def isBrownListed(self, itemIdentifier):
        brownListed = False
        for brownList in self._brownLists:
            if brownList.getMatchingItem(itemIdentifier) is not None:
                brownListed = True
                break
        return brownListed

    def shouldBeFulfilled(self, itemIdentifier):
        should = False
        for supplierIDList in self._supplierIDLists:
            # Any item that appears in the Supplier ID list
            # should be fulfilled
            if supplierIDList.hasItemIdentifier(itemIdentifier):
                should = True
                break
        return should

    def vendorIdRequiresFirmwareId(self, vendorID):
        requires = False
        for supplierIDList in self._supplierIDLists:
            if supplierIDList.vendorIdRequiresFirmwareId(vendorID):
                requires = True
        return requires

    def idsToItemIdentifiers(self, vendorID, firmwareID=None):
        itemIdentifiers = []
        for supplierIDList in self._supplierIDLists:
            items = supplierIDList.getMatchingItems(vendorID, firmwareID)
            if len(items) > 0:
                for item in items:
                    itemIdentifiers.append(item.getItemIdentifier())
        return itemIdentifiers

    def idsToItemDescriptions(self, vendorID, firmwareID=None):
        itemDescriptions = []
        for supplierIDList in self._supplierIDLists:
            items = supplierIDList.getMatchingItems(vendorID, firmwareID)
            if len(items) > 0:
                for item in items:
                    itemDescriptions.append(item.getDescription())
        return itemDescriptions

    def isAllowedSubstitute(self, itemIdentifier, subassembly):
        subPart = None
        allowedSubstitute = False
        for allowedSubstituteList in self._allowedSubstitutionLists:
            billOfMaterialItems = allowedSubstituteList.getBillOfMaterialItems()
            for billOfMaterialItem in billOfMaterialItems:
                # Ignore tab level
                if billOfMaterialItem.matchesItemIdentifier(itemIdentifier, 8):
                    partNos = subassembly.getPartNos()
                    for partNo in partNos:
                        # Ignore tab level
                        if (billOfMaterialItem.getMatchingAlternateItem(
                            partNo, 8) is not None): 

                            subPart = partNo
                            allowedSubstitute = True
                            break
        return (allowedSubstitute, subPart)

    def dump(self):
        print ''
        prettyPrint(self._level, '### Start of SoftAVL Dump ###')
        prettyPrint(self._level, 'Version:   "%s"' % self._info['version'])
        prettyPrint(self._level, 'Date:      "%s"' % self._info['date'])
        prettyPrint(self._level, 'Filename:  "%s"' % self._info['filename'])
        prettyPrint(self._level, '')
        for allowedSubstitutionList in self._allowedSubstitutionLists:
            allowedSubstitutionList.dump()
        for blackList in self._blackLists:
            blackList.dump()
        for brownList in self._brownLists:
            brownList.dump()
        for item in self._items:
            item.dump()
        for supplierIDList in self._supplierIDLists:
            supplierIDList.dump()
        prettyPrint(self._level, '### End of SoftAVL Dump ###')
        print ''

#----------------------------------------------------------------------
class AllowedSubstitutionList:
    def __init__(self, level):
        self._level = level
        self._info = {}
        self._billOfMaterialItems = []

    def getLevel(self):
        return self._level

    def addBillOfMaterialItem(self, billOfMaterialItem):
        self._billOfMaterialItems.append(billOfMaterialItem)

    def hasBillOfMaterialItems(self):
        return len(self._billOfMaterialItems) > 0

    def getBillOfMaterialItems(self):
        return self._billOfMaterialItems

    def getLastBillOfMaterialItem(self):
        return self._billOfMaterialItems[len(self._billOfMaterialItems) - 1]

    def getNumBillOfMaterialItems(self):
        return len(self._billOfMaterialItems)

    def dump(self):
        prettyPrint(self._level, '>>> AllowedSubstitutionList <<<')
        for billOfMaterialItem in self._billOfMaterialItems:
            billOfMaterialItem.dump()
        prettyPrint(self._level, '>>> AllowedSubstitutionList <<<')

#----------------------------------------------------------------------
class SoftAVLBillOfMaterialItem:
    def __init__(self, level, itemIdentifier, itemQuantity):

        self._level = level
        self._info = {
            'itemIdentifier' : itemIdentifier, 
            'itemQuantity' : itemQuantity 
            }
        self._alternateItems = []

    def getLevel(self):
        return self._level

    def getItemIdentifier(self):
        return self._info['itemIdentifier']

    def getItemQuantity(self):
        return self._info['itemQuantity']

    def matchesItemIdentifier(self, itemIdentifier, sig=11):
        # Use only requested number of significant digits
        return itemIdentifier == str(self._info['itemIdentifier'])

    def addAlternateItems(self, alternateItems):
        self._alternateItems.append(alternateItems)

    def hasAlternateItems(self):
        return len(self._alternateItems) > 0

    def getAlternateItems(self):
        return self._alternateItems

    def getLastAlternateItems(self):
        return self._alternateItems[len(self._alternateItems) - 1]

    def getNumAlternateItems(self):
        return len(self._alternateItems)

    def getMatchingAlternateItem(self, itemIdentifer, sig=11):
        matchingAlternateItem = None
        for alternateItems in self._alternateItems:
            item = alternateItems.getMatchingAlternateItem(itemIdentifer, sig)
            if item is not None:
                matchingAlternateItem = item
                break
        return matchingAlternateItem

    def dump(self):
        prettyPrint(self._level, 
                    'SoftAVL BillOfMaterialItem: itemIdentifier="%s", '
                    'itemQuantity="%s"' % (self._info['itemIdentifier'],
                    self._info['itemQuantity']))
        for alternateItems in self._alternateItems:
            alternateItems.dump()

#----------------------------------------------------------------------
class AlternateItems:
    def __init__(self, level):
        self._level = level
        self._info = {}
        self._alternateItems = []

    def getLevel(self):
        return self._level

    def addAlternateItem(self, alternateItem):
        self._alternateItems.append(alternateItem)

    def hasAlternateItems(self):
        return len(self._alternateItems) > 0

    def getAlternateItems(self):
        return self._alternateItems

    def getLastAlternateItem(self):
        return self._alternateItems[len(self._alternateItems) - 1]
    
    def getNumAlternateItems(self):
        return len(self._alternateItems)

    def getMatchingAlternateItem(self, itemIdentifer, sig=11):
        matchingAlternateItem = None
        for alternateItem in self._alternateItems:
            if alternateItem.matchesItemIdentifier(itemIdentifer, sig):
                matchingAlternateItem = alternateItem
                break
        return matchingAlternateItem

    def dump(self):
        prettyPrint(self._level, '>>> AlternateItems <<<')
        for alternateItem in self._alternateItems:
            alternateItem.dump()
        prettyPrint(self._level, '>>> AlternateItems <<<')

#----------------------------------------------------------------------
class AlternateItem:
    def __init__(self, level, itemIdentifier):

        self._level = level
        self._info = {
            'itemIdentifier' : itemIdentifier
            }

    def getLevel(self):
        return self._level

    def getItemIdentifier(self):
        return self._info['itemIdentifier']

    def matchesItemIdentifier(self, itemIdentifier, sig=11):
        # Use only requested number of significant digits
        return itemIdentifier == str(self._info['itemIdentifier'])

    def dump(self):
        prettyPrint(self._level, 'AlternateItem: itemIdentifier="%s"' % 
                    (self._info['itemIdentifier']))

#----------------------------------------------------------------------
class BlackList:
    def __init__(self, level):
        self._level = level
        self._info = {}
        self._items = []

    def getLevel(self):
        return self._level

    def addItem(self, item):
        self._items.append(item)

    def hasItems(self):
        return len(self._items) > 0

    def getItems(self):
        return self._items

    def getLastItem(self):
        return self._items[len(self._items) - 1]

    def getNumItems(self):
        return len(self._items)

    def getMatchingItem(self, itemIdentifer, sig=11):
        matchingItem = None
        for item in self._items:
            if item.matchesItemIdentifier(itemIdentifer, sig):
                matchingItem = item
                break
        return matchingItem

    def dump(self):
        prettyPrint(self._level, '>>> BlackList <<<')
        for item in self._items:
            item.dump()
        prettyPrint(self._level, '>>> BlackList <<<')

#----------------------------------------------------------------------
class BrownList:
    def __init__(self, level):
        self._level = level
        self._info = {}
        self._items = []

    def getLevel(self):
        return self._level

    def addItem(self, item):
        self._items.append(item)

    def hasItems(self):
        return len(self._items) > 0

    def getItems(self):
        return self._items

    def getLastItem(self):
        return self._items[len(self._items) - 1]

    def getNumItems(self):
        return len(self._items)

    def getMatchingItem(self, itemIdentifer, sig=11):
        matchingItem = None
        for item in self._items:
            if item.matchesItemIdentifier(itemIdentifer, sig):
                matchingItem = item
                break
        return matchingItem

    def dump(self):
        prettyPrint(self._level, '>>> BrownList <<<')
        for item in self._items:
            item.dump()
        prettyPrint(self._level, '>>> BrownList <<<')

#----------------------------------------------------------------------
class SoftAVLItem:
    def __init__(self, level, itemIdentifier, description):

        self._level = level
        self._info = {
            'itemIdentifier' : itemIdentifier, 
            'description' : description 
            }
        self._billOfMaterials = []
        self._supplierIDItems = []

    def getLevel(self):
        return self._level

    def getItemIdentifier(self):
        return self._info['itemIdentifier']

    def getDescription(self):
        return self._info['description']

    def matchesItemIdentifier(self, itemIdentifier, sig=11):
        # Use only requested number of significant digits
        return itemIdentifier == str(self._info['itemIdentifier'])

    def addBillOfMaterial(self, billOfMaterial):
        self._billOfMaterials.append(billOfMaterial)

    def hasBillOfMaterials(self):
        return len(self._billOfMaterials) > 0

    def getBillOfMaterials(self):
        return self._billOfMaterials

    def getLastBillOfMaterials(self):
        return self._billOfMaterials[len(self._billOfMaterials) - 1]

    def getNumBillOfMaterials(self):
        return len(self._billOfMaterials)

    def getMatchingBillOfMaterialItem(self, itemIdentifer, sig=11):
        matchingItem = None
        for billOfMaterial in self._billOfMaterials:
            billOfMaterialItems = billOfMaterial.getBillOfMaterialItems()
            for billOfMaterialItem in billOfMaterialItems:
                if billOfMaterialItem.matchesItemIdentifier(itemIdentifer, sig):
                    matchingItem = billOfMaterialItem
                    break
        return matchingItem

    def addSupplierIDItem(self, supplierIDItem):
        self._supplierIDItems.append(supplierIDItem)

    def hasSupplierIDItems(self):
        return len(self._supplierIDItems) > 0

    def getSupplierIDItems(self):
        return self._supplierIDItems

    def getLastSupplierIDItem(self):
        return self._supplierIDItems[len(self._supplierIDItems) - 1]

    def getNumSupplierIDItems(self):
        return len(self._supplierIDItems)

    def dump(self):
        prettyPrint(self._level, 'SoftAVL Item: itemIdentifier="%s", '
                    'Description="%s"' % (self._info['itemIdentifier'], 
                    self._info['description']))
        for billOfMaterial in self._billOfMaterials:
            billOfMaterial.dump()
        for supplierIDItem in self._supplierIDItems:
            supplierIDItem.dump()

#----------------------------------------------------------------------
class SoftAVLItems:
    def __init__(self, level):
        self._level = level
        self._info = {}
        self._items = []

    def getLevel(self):
        return self._level

    def addItem(self, item):
        self._items.append(item)

    def hasItems(self):
        return len(self._items) > 0

    def getItems(self):
        return self._items

    def getLastItem(self):
        return self._items[len(self._items) - 1]

    def getNumItems(self):
        return len(self._items)

    def getMatchingItem(self, itemIdentifer, sig=11):
        matchingItem = None
        for item in self._items:
            if item.matchesItemIdentifier(itemIdentifer, sig):
                matchingItem = item
                break
        return matchingItem

    def dump(self):
        prettyPrint(self._level, '>>> Items <<<')
        for item in self._items:
            item.dump()
        prettyPrint(self._level, '>>> Items <<<')

#----------------------------------------------------------------------
class SoftAVLBillOfMaterial:
    def __init__(self, level):
        self._level = level
        self._info = {}
        self._billOfMaterialItems = []

    def getLevel(self):
        return self._level

    def addBillOfMaterialItem(self, billOfMaterialItem):
        self._billOfMaterialItems.append(billOfMaterialItem)

    def hasBillOfMaterialItems(self):
        return len(self._billOfMaterialItems) > 0

    def getBillOfMaterialItems(self):
        return self._billOfMaterialItems

    def getBillOfMaterialItem(self):
        return self._billOfMaterialItems[len(self._billOfMaterialItems) - 1]

    def getNumBillOfMaterialItems(self):
        return len(self._billOfMaterialItems)

    def dump(self):
        prettyPrint(self._level, '>>> SoftAVL BillOfMaterial <<<')
        for billOfMaterialItem in self._billOfMaterialItems:
            billOfMaterialItem.dump()
        prettyPrint(self._level, '>>> SoftAVL BillOfMaterial <<<')

#----------------------------------------------------------------------
class SupplierIDList:
    def __init__(self, level):
        self._level = level
        self._info = {}
        self._items = []

    def getLevel(self):
        return self._level

    def addItem(self, item):
        self._items.append(item)

    def hasItems(self):
        return len(self._items) > 0

    def getItems(self):
        return self._items

    def getLastItem(self):
        return self._items[len(self._items) - 1]

    def getNumItems(self):
        return len(self._items)

    def hasItemIdentifier(self, itemIdentifier, sig=11):
        matches = False
        for item in self._items:
            if item.matchesItemIdentifier(itemIdentifier, sig):
                matches = True
                break
        return matches

    def vendorIdRequiresFirmwareId(self, vendorID):
        requires = False
        for item in self._items:
            supplierIDItems = item.getSupplierIDItems()
            for supplierIDItem in supplierIDItems:
                if (supplierIDItem.matchesIDs(vendorID) and
                    supplierIDItem.requiresFirmwareID()):
                    requires = True
        return requires

    def getMatchingItems(self, vendorID, firmwareID=None):
        matchingItems = []
        for item in self._items:
            supplierIDItems = item.getSupplierIDItems()
            for supplierIDItem in supplierIDItems:
                if supplierIDItem.matchesIDs(vendorID, firmwareID):
                    matchingItems.append(item)
        return matchingItems

    def dump(self):
        prettyPrint(self._level, '>>> SupplierIDList <<<')
        for item in self._items:
            item.dump()
        prettyPrint(self._level, '>>> SupplierIDList <<<')

#----------------------------------------------------------------------
class SupplierIDItem:
    def __init__(self, level):
        self._level = level
        self._info = {}
        self._idStrings = []

    def getLevel(self):
        return self._level

    def addIDStrings(self, idStrings):
        self._idStrings.append(idStrings)

    def hasIDStrings(self):
        return len(self._idStrings) > 0

    def getIDStrings(self):
        return self._idStrings

    def getLastIDStrings(self):
        return self._idStrings[len(self._idStrings) - 1]

    def getNumIDStrings(self):
        return len(self._idStrings)

    def requiresFirmwareID(self):
        allowAnyFirmware = False
        entriesWithFirmwareId = 0
        entriesWithoutFirmwareId = 0
        for idString in self._idStrings:
            if idString.getFirmwareID() != '':
                entriesWithFirmwareId += 1
            elif idString.getFirmwareID() == '':
                entriesWithoutFirmwareId += 1
        if entriesWithoutFirmwareId > 0 and entriesWithFirmwareId > 0:
            print ('WARNING: SoftAVL entry for vendor ID="%s" has %s '
                   'line(s) without and %s with firmware ID -- invoking '
                   'wildcard rule (allowing any firmware ID)!' %
                   (idString.getVendorID(), entriesWithoutFirmwareId, 
                   entriesWithFirmwareId))
            allowAnyFirmware = True
        requires = False
        if not allowAnyFirmware and entriesWithFirmwareId > 0:
            requires = True
        return requires

    def matchesIDs(self, vendorID, firmwareID=None):
        matches = False
        for idString in self._idStrings:
            if idString.matchesIDs(vendorID, firmwareID):
                matches = True
                break
        return matches
            
    def dump(self):
        prettyPrint(self._level, '>>> SupplierIDItem <<<')
        for idStrings in self._idStrings:
            idStrings.dump()
        prettyPrint(self._level, '>>> SupplierIDItem <<<')

#----------------------------------------------------------------------
class IDStrings:
    def __init__(self, level, vendorID, firmwareID):

        self._level = level
        self._info = {
            'vendorID' : vendorID, 
            'firmwareID' : firmwareID 
            }

    def getLevel(self):
        return self._level

    def getVendorID(self):
        return self._info['vendorID']

    def getFirmwareID(self):
        return self._info['firmwareID']

    def matchesIDs(self, vendorID, firmwareID=None):
        matches = False
        vendorIDMatches = (vendorID == str(self._info['vendorID']))
        if firmwareID is not None:
            firmwareIDMatches = (firmwareID == str(self._info['firmwareID']))
        else:
            firmwareIDMatches = True
        if vendorIDMatches and firmwareIDMatches:
            matches = True
        return matches

    def dump(self):
        prettyPrint(self._level, 'IDStrings: vendorID="%s", '
                    'firmwareID="%s"' % (self._info['vendorID'], 
                    self._info['firmwareID']))

#----------------------------------------------------------------------
def walkSoftAVL(level, softAVL, thisNode, prevNode=None):
    softAVLVersion = softAVL.getVersion()
    if prevNode is None:
        prevNodeName = None
    else:
        prevNodeName = prevNode.nodeName

    thisNodeName = thisNode.nodeName

    if _g_debugLevel > 1:
        print 'DEBUG: >>>>>>>>>> walkSoftAVL <<<<<<<<<<'
        print 'DEBUG: level ', level
        print 'DEBUG: thisNodeName ', thisNodeName
        print 'DEBUG: prevNodeName ', prevNodeName

    # Grab any attributes that we can at this level
    billOfMaterialItemIdentifier = itemIdentifier = itemQuantity = None
    description = vendorID = firmwareID = None
    if thisNode.hasAttribute('billOfMaterialItemIdentifier'):
        billOfMaterialItemIdentifier = (
            thisNode.getAttribute('billOfMaterialItemIdentifier'))
    if thisNode.hasAttribute('itemIdentifier'):
        itemIdentifier = thisNode.getAttribute('itemIdentifier')
    if thisNode.hasAttribute('description'):
        description = thisNode.getAttribute('description')
    if thisNode.hasAttribute('itemQuantity'):
        itemQuantity = int(float(thisNode.getAttribute('itemQuantity')))
    if thisNode.hasAttribute('vendorID'):
        vendorID = thisNode.getAttribute('vendorID')
    if thisNode.hasAttribute('firmwareID'):
        firmwareID = thisNode.getAttribute('firmwareID')

    if _g_debugLevel > 1:
        print ('DEBUG: billOfMaterialItemIdentifier ',
            billOfMaterialItemIdentifier)
        print 'DEBUG: itemIdentifier ', itemIdentifier
        print 'DEBUG: description ', description
        print 'DEBUG: itemQuantity ', itemQuantity
        print 'DEBUG: vendorID ', vendorID
        print 'DEBUG: firmwareID ', firmwareID

    if thisNodeName in [ 'AllowedSubstitutionList' ]:
        thisAllowedSubstitutionList = AllowedSubstitutionList(level)
        softAVL.addAllowedSubstitutionList(thisAllowedSubstitutionList)
    elif thisNodeName in [ 'BlackList' ]:
        thisBlackList = BlackList(level)
        softAVL.addBlackList(thisBlackList)
    elif thisNodeName in [ 'BrownList' ]:
        thisBrownList = BrownList(level)
        softAVL.addBrownList(thisBrownList)
    elif thisNodeName in [ 'Items' ]:
        thisItems = SoftAVLItems(level)
        softAVL.addItems(thisItems)
    elif thisNodeName in [ 'SupplierIDList' ]:
        thisSupplierIDList = SupplierIDList(level)
        softAVL.addSupplierIDList(thisSupplierIDList)
    elif thisNodeName in [ 'BillOfMaterialItem' ]:
        thisBillOfMaterialItem = SoftAVLBillOfMaterialItem(level,
            billOfMaterialItemIdentifier, itemQuantity)
        if prevNodeName in [ 'AllowedSubstitutionList' ]:
            allowedSubstitutionList = softAVL.getLastAllowedSubstitutionList()
            allowedSubstitutionList.addBillOfMaterialItem(
                thisBillOfMaterialItem)
        elif prevNodeName in [ 'BillOfMaterial' ]:
            items = softAVL.getLastItems()
            item = items.getLastItem()
            billOfMaterial = item.getLastBillOfMaterials()
            billOfMaterial.addBillOfMaterialItem(thisBillOfMaterialItem)
    elif thisNodeName in [ 'Item' ]:
        thisItem = SoftAVLItem(level, itemIdentifier, description)
        if prevNodeName in [ 'BlackList' ]:
            blackList = softAVL.getLastBlackList()
            blackList.addItem(thisItem)
        elif prevNodeName in [ 'BrownList' ]:
            brownList = softAVL.getLastBrownList()
            brownList.addItem(thisItem)
        elif prevNodeName in [ 'Items' ]:
            items = softAVL.getLastItems()
            items.addItem(thisItem)
        elif prevNodeName in [ 'SupplierIDList' ]:
            supplierIDList = softAVL.getLastSupplierIDList()
            supplierIDList.addItem(thisItem)
    elif thisNodeName in [ 'AlternateItems' ]:
        thisAlternateItems = AlternateItems(level)
        if prevNodeName in [ 'BillOfMaterialItem' ]:
            allowedSubstitutionList = softAVL.getLastAllowedSubstitutionList()
            billOfMaterialItem = (
                allowedSubstitutionList.getLastBillOfMaterialItem())
            billOfMaterialItem.addAlternateItems(thisAlternateItems)
    elif thisNodeName in [ 'BillOfMaterial' ]:
        thisBillOfMaterial = SoftAVLBillOfMaterial(level)
        if prevNodeName in [ 'Item' ]:
            items = softAVL.getLastItems()
            item = items.getLastItem()
            item.addBillOfMaterial(thisBillOfMaterial)
    elif thisNodeName in [ 'SupplierIDItem' ]:
        thisSupplierIDItem = SupplierIDItem(level)
        if prevNodeName in [ 'Item' ]:
            supplierIDList = softAVL.getLastSupplierIDList()
            item = supplierIDList.getLastItem()
            item.addSupplierIDItem(thisSupplierIDItem)
    elif thisNodeName in [ 'AlternateItem' ]:
        thisAlternateItem = AlternateItem(level, itemIdentifier)
        if prevNodeName in [ 'AlternateItems' ]:
            allowedSubstitutionList = softAVL.getLastAllowedSubstitutionList()
            billOfMaterialItem = (
                allowedSubstitutionList.getLastBillOfMaterialItem())
            alternateItems = billOfMaterialItem.getLastAlternateItems()
            alternateItems.addAlternateItem(thisAlternateItem)
    elif thisNodeName in [ 'IDStrings' ]:
        thisIDStrings = IDStrings(level, vendorID, firmwareID)
        if prevNodeName in [ 'SupplierIDItem' ]:
            supplierIDList = softAVL.getLastSupplierIDList()
            item = supplierIDList.getLastItem()
            supplierIDItem = item.getLastSupplierIDItem()
            supplierIDItem.addIDStrings(thisIDStrings)

    hasChild = thisNode.hasChildNodes and thisNode.hasChildNodes()
    children = hasChild and thisNode.childNodes or None
    if children:
        for child in children:
            childLevel = level + 1
            if child.nodeType is child.ELEMENT_NODE:
                walkSoftAVL(childLevel, softAVL, child, thisNode)
            elif child.nodeType is child.ATTRIBUTE_NODE:
                raise ctoException('walkSoftAVL: detected attribute XML node')
            elif child.nodeType is child.TEXT_NODE:
                pass
            else:
                raise ctoException('walkSoftAVL: detected unknown XML node')

#----------------------------------------------------------------------
def sanityCheckSoftAVL(xmlDoc):

    # Make sure basic header information is present
    firstChild = xmlDoc.firstChild
    if firstChild.nodeName != 'Soft-AVL':
        raise ctoException('not a SoftAVL')
    softAVLElements = xmlDoc.getElementsByTagName('Soft-AVL')
    for softAVLElement in softAVLElements:
        if not softAVLElement.hasAttribute('version'):
            raise ctoException('missing format version')
        if not softAVLElement.hasAttribute('date'):
            raise ctoException('missing date')
        if not softAVLElement.hasAttribute('filename'):
            raise ctoException('missing filename')

        # Make sure we support this version of the softAVL format
        softAVLVersion = softAVLElement.getAttribute('version')
        if (float(softAVLVersion) <
            float(OLDEST_SUPPORTED_SOFTAVL_FORMAT_VERSION)):
            raise ctoException('format version is too old -- SoftAVL="%1.1f", '
                               'oldest version supportedby OneFS="%1.1f"' % 
                               (float(softAVLVersion), 
                               float(OLDEST_SUPPORTED_SOFTAVL_FORMAT_VERSION)))
        if float(softAVLVersion) > float(SUPPORTED_SOFTAVL_FORMAT_VERSION):
            raise ctoException('format version is too new -- SoftAVL="%1.1f", '
                               'newest version supported by OneFS="%1.1f"' % 
                               (float(softAVLVersion), 
                               float(SUPPORTED_SOFTAVL_FORMAT_VERSION)))
    
    # All softAVLs must have an Allowed Substitution List, though it
    # is allowed to be empty
    allowedSubstitutionListElements = (
        xmlDoc.getElementsByTagName('AllowedSubstitutionList'))
    if len(allowedSubstitutionListElements) == 0:
        raise ctoException('missing allowed substitution list')

    # All softAVLs must have a Black List, though it is allowed to be empty
    blackListElements = xmlDoc.getElementsByTagName('BlackList')
    if len(blackListElements) == 0:
        raise ctoException('missing black list')

    # All Black listed items must have a description and item identifier
    for blackListElement in blackListElements:
        hasChild = (blackListElement.hasChildNodes and 
                    blackListElement.hasChildNodes())
        children = hasChild and blackListElement.childNodes or None
        if children:
            for child in children:
                if child.nodeType is child.ELEMENT_NODE:
                    if not child.getAttribute('description'):
                        raise ctoException('"%s" -- no description' %
                                           child.toxml())
                    if not child.getAttribute('itemIdentifier'):
                        raise ctoException('"%s" -- no item identifier' %
                                           child.toxml())

    # All softAVLs must have a Brown List, though it is allowed to be empty
    brownListElements = xmlDoc.getElementsByTagName('BrownList')
    if len(brownListElements) == 0:
        raise ctoException('missing brown list')

    # All Brown listed items must have a description and item identifier
    for brownListElement in brownListElements:
        hasChild = (brownListElement.hasChildNodes and
            brownListElement.hasChildNodes())
        children = hasChild and brownListElement.childNodes or None
        if children:
            for child in children:
                if child.nodeType is child.ELEMENT_NODE:
                    if not child.getAttribute('description'):
                        raise (ctoException('"%s" -- no description' %
                                            child.toxml()))
                    if not child.getAttribute('itemIdentifier'):
                        raise (ctoException('"%s" -- no item identifier' %
                                            child.toxml()))

    # All softAVLs must have a BOM ('Items') structure
    itemsElements = xmlDoc.getElementsByTagName('Items')
    if len(itemsElements) == 0:
        raise ctoException('missing items')

    # All items in the BOM structure must have an item identifier and
    # description
    itemElements = xmlDoc.getElementsByTagName('Item')
    for itemElement in itemElements:
        if not itemElement.hasAttribute('itemIdentifier'):
            raise (ctoException('"%s" -- no item identifier' %
                                itemElement.toxml()))
        if not itemElement.hasAttribute('description'):
            raise ctoException('"%s" -- no description' % itemElement.toxml())

#----------------------------------------------------------------------
def importSoftAVL(fileType, filePath):
    status = 0
    checkDateTime = False

    if isNpiFile(filePath) and not supportNpi():
        print ('Blocking import of "%s", NPI softAVL files '
               'not supported.' % filePath)
        return -1

    currentSoftAVL = getCurrentSoftAVL(currentSoftAVLFilePath())
    if currentSoftAVL is not None:
        checkDateTime = True
        filename = currentSoftAVL.getFilename()
        currentIsoTimestamp = getIsoTimestampFromFilename(filename)
        if currentIsoTimestamp is None:
            raise ctoException('Unable to extract ISO timestamp from '
                               'filename="%s"' % filename)

        if _g_debugLevel > 0:
            print 'Current SoftAVL is from "%s"' % currentIsoTimestamp

    xmlDoc = parseXmlFile(filePath)
    try:
        if xmlDoc is not None:
            sanityCheckSoftAVL(xmlDoc)
    except ctoException as e:
        print 'ERROR: Problem with SoftAVL -- %s' % e.value
        return -1

    if xmlDoc is None:
        return -1

    softAVLElements = xmlDoc.getElementsByTagName('Soft-AVL')
    for softAVLElement in softAVLElements:
        version = softAVLElement.getAttribute('version')
        date = softAVLElement.getAttribute('date')
        filename = softAVLElement.getAttribute('filename')

    # NOTE: Unlike the probe configuration file which has an 
    #       SVN version embedded in the file, the SoftAVL has
    #       no such version. Rather, it has a date/time stamp
    #       in its filename, so we use that to determine which
    #       copy of the file is the newest.
    newIsoTimestamp = getIsoTimestampFromFilename(filename)
    if newIsoTimestamp is None:
        raise ctoException('Unable to extract ISO timestamp from '
                           'filename="%s"' % filename)

    if _g_debugLevel > 0:
        print 'New SoftAVL is from "%s"' % newIsoTimestamp

    # Honor the import only if the file being imported is the same
    # as or newer than the current file
    if checkDateTime and newIsoTimestamp < currentIsoTimestamp:
        # NOTE: The scripts in the field packages (CTO conversion, 
        #       FRU and field upgrade) rely on the first two (2) words
        #       of the message below to know that the failure was due
        #       to an out-of-date file, so DO NOT change the message.
        print ('Blocking import of out-of-date SoftAVL. Current SoftAVL is '
               'from "%s" and the one being imported is from "%s".' %
               (currentIsoTimestamp, newIsoTimestamp))
        return -1

    level = 0
    softAVL = SoftAVL(level, version, date, filename)

    level += 1
    allowedSubstitutionListElements = (
        xmlDoc.getElementsByTagName('AllowedSubstitutionList'))
    for allowedSubstitutionListElement in allowedSubstitutionListElements:
        walkSoftAVL(level, softAVL, allowedSubstitutionListElement)

    blackListElements = xmlDoc.getElementsByTagName('BlackList')
    for blackListElement in blackListElements:
        walkSoftAVL(level, softAVL, blackListElement)

    brownListElements = xmlDoc.getElementsByTagName('BrownList')
    for brownListElement in brownListElements:
        walkSoftAVL(level, softAVL, brownListElement)

    itemsElements = xmlDoc.getElementsByTagName('Items')
    for itemsElement in itemsElements:
        walkSoftAVL(level, softAVL, itemsElement)

    supplierIDListElements = xmlDoc.getElementsByTagName('SupplierIDList')
    for supplierIDListElement in supplierIDListElements:
        walkSoftAVL(level, softAVL, supplierIDListElement)

    if _g_debugLevel > 0:
        print 'DEBUG: Internal copy of softAVL is as follows:'
        softAVL.dump()

    # If we made it this far, it's ok to import the SoftAVL
    status = saveFile(fileType, filePath)

    if status == 0:
        # Make this the current softAVL
        mfgDir = os.path.join(ROOT_DIR, MFG_DIR, SOFTAVL_SUBDIR)
        filename = os.path.basename(filePath)
        sourceFile = os.path.join(mfgDir, filename)
        destFile = os.path.join(mfgDir, SOFTAVL_FILENAME)
        status = copyFile(sourceFile, destFile)

    return status

#----------------------------------------------------------------------
def getCurrentSoftAVL(filePath):
    status = 0
    softAVL = None

    try:
        status = mountMfg()
        if status == 0:
            xmlDoc = parseXmlFile(filePath)
            try:
                if xmlDoc is not None:
                    sanityCheckSoftAVL(xmlDoc)
            except ctoException as e:
                print 'ERROR: Problem with SoftAVL -- %s' % e.value
                status = -1

            if xmlDoc is None:
                status = -1

        if status == 0:
            softAVLElements = xmlDoc.getElementsByTagName('Soft-AVL')
            for softAVLElement in softAVLElements:
                version = softAVLElement.getAttribute('version')
                date = softAVLElement.getAttribute('date')
                filename = softAVLElement.getAttribute('filename')

            level = 0
            softAVL = SoftAVL(level, version, date, filename)

            level += 1
            allowedSubstitutionListElements = (
                xmlDoc.getElementsByTagName('AllowedSubstitutionList'))
            for allowedSubstitutionListElement in (
                allowedSubstitutionListElements):
                walkSoftAVL(level, softAVL, allowedSubstitutionListElement)
    
            blackListElements = xmlDoc.getElementsByTagName('BlackList')
            for blackListElement in blackListElements:
                walkSoftAVL(level, softAVL, blackListElement)

            brownListElements = xmlDoc.getElementsByTagName('BrownList')
            for brownListElement in brownListElements:
                walkSoftAVL(level, softAVL, brownListElement)

            itemsElements = xmlDoc.getElementsByTagName('Items')
            for itemsElement in itemsElements:
                walkSoftAVL(level, softAVL, itemsElement)

            supplierIDListElements = (
                xmlDoc.getElementsByTagName('SupplierIDList'))
            for supplierIDListElement in supplierIDListElements:
                walkSoftAVL(level, softAVL, supplierIDListElement)

    finally:
        unmountMfg()

    return softAVL

#======================================================================
#========================== End of SoftAVL ============================
#======================================================================

#======================================================================
#========================= As Built Record ============================
#======================================================================

#----------------------------------------------------------------------
class AsBuilt:
    def __init__(self, level, date, asBuiltFilename, asBuiltVersion,
        receiptFilename, receiptVersion, softAVLFilename, softAVLVersion):

        self._level = level
        self._info = {
            'date' : date,
            'asBuiltFilename' : asBuiltFilename,
            'asBuiltVersion' : asBuiltVersion,
            'receiptFilename' : receiptFilename,
            'receiptVersion' : receiptVersion,
            'softAVLFilename' : softAVLFilename,
            'softAVLVersion' : softAVLVersion,
            }
        self._boms = []
        self._missings = []
        self._unexpecteds = []

    def getLevel(self):
        return self._level

    def getDate(self):
        return self._info['date']

    def getAsBuiltFilename(self):
        return self._info['asBuiltFilename']

    def getAsBuiltVersion(self):
        return self._info['asBuiltVersion']

    def getReceiptFilename(self):
        return self._info['receiptFilename']

    def getReceiptVersion(self):
        return self._info['receiptVersion']

    def getSoftAVLFilename(self):
        return self._info['softAVLFilename']

    def getSoftAVLVersion(self):
        return self._info['softAVLVersion']

    def hasBoms(self):
        return len(self._boms) > 0

    def addBom(self, bom):
        self._boms.append(bom)

    def getBoms(self):
        return self._boms
    
    def getLastBom(self):
        return self._boms[len(self._boms) - 1]

    def getNumBoms(self):
        return len(self._boms)

    def hasMissings(self):
        return len(self._missings) > 0

    def addMissing(self, missing):
        self._missings.append(missing)

    def getMissings(self):
        return self._missings
    
    def getLastMissing(self):
        return self._missings[len(self._missings) - 1]

    def getNumMissings(self):
        return len(self._missings)

    def hasUnexpecteds(self):
        return len(self._unexpecteds) > 0

    def addUnexpected(self, unexpected):
        self._unexpecteds.append(unexpected)

    def getUnexpecteds(self):
        return self._unexpecteds
    
    def getLastUnexpected(self):
        return self._unexpecteds[len(self._unexpecteds) - 1]

    def getNumUnexpecteds(self):
        return len(self._unexpecteds)

    def generateXML(self, filePath):
        from xml.dom.minidom import getDOMImplementation as getImpl
        doc = getImpl().createDocument(None, 'asbuilt', None)

        # Build the root element
        rootElement = doc.documentElement
        rootElement.setAttribute('date', '%s' % self._info['date'])
        rootElement.setAttribute('asbuiltFileName', '%s' %
                                 self._info['asBuiltFilename'])
        rootElement.setAttribute('asbuiltFormatVersion', '%s' %
                                 self._info['asBuiltVersion'])
        rootElement.setAttribute('receiptFileName', '%s' %
                                 self._info['receiptFilename'])
        rootElement.setAttribute('receiptFormatVersion', '%s' %
                                 self._info['receiptVersion'])
        rootElement.setAttribute('softAvlFileName', '%s' %
                                 self._info['softAVLFilename'])
        rootElement.setAttribute('softAvlFormatVersion', '%s' %
                                 self._info['softAVLVersion'])
    
        # Build the body elements, including the BOM, SBBs and items
        for bom in self._boms:
            bomElement = doc.createElement('bom')
            sbbs = bom.getSbbs()
            for sbb in sbbs:
                sbbElement = doc.createElement('sbb')
                sbbElement.setAttribute('part', sbb.getPart())
                sbbElement.setAttribute('desc', sbb.getDesc())
                if sbb.getQty():
                    sbbElement.setAttribute('qty', '%s' % sbb.getQty())
                sbbItems = sbb.getItems()
                for sbbItem in sbbItems:
                    walkAsBuiltItemXML(doc, sbbElement, sbbItem)
                bomElement.appendChild(sbbElement)
            rootElement.appendChild(bomElement)

        # Build the missing element
        attributeValue = 'False'
        missingElement = doc.createElement('missing')
        numMissings = self.getNumMissings()
        if numMissings > 0:
            attributeValue = 'True'
        missingElement.setAttribute('containsMissing', attributeValue)
        for missing in self._missings:
            itemElement = doc.createElement('item')
            itemElement.setAttribute('part', missing.getPart())
            itemElement.setAttribute('desc', missing.getDesc())
            if hasattr(missing, 'getQty') and missing.getQty():
                itemElement.setAttribute('qty', '%s' % missing.getQty())
            missingElement.appendChild(itemElement)
        rootElement.appendChild(missingElement)

        # Build the unexpected element
        attributeValue = 'False'
        unexpectedElement = doc.createElement('unexpected')
        numUnexpecteds = self.getNumUnexpecteds()
        if numUnexpecteds > 0:
            attributeValue = 'True'
        unexpectedElement.setAttribute('containsUnexpected', attributeValue)
        for unexpected in self._unexpecteds:
            itemElement = doc.createElement('item')
            itemElement.setAttribute('part', unexpected.getPart())
            itemElement.setAttribute('desc', unexpected.getDesc())
            if hasattr(unexpected, 'getQty') and unexpected.getQty():
                itemElement.setAttribute('qty', '%s' % unexpected.getQty())
            unexpectedElement.appendChild(itemElement)
        rootElement.appendChild(unexpectedElement)

        # Finally, convert to XML
        if _g_debugLevel > 0:
            print rootElement.toprettyxml(' ')

        with open(filePath, 'w') as writer:
            doc.writexml(writer, '', ' ', '\n')

    def dump(self):
        print ''
        prettyPrint(self._level, '### Start of AsBuilt Dump ###')
        prettyPrint(self._level, 'Date:             "%s"' % self._info['date'])
        prettyPrint(self._level, 'asBuiltFilename:  "%s"' %
                    self._info['asBuiltFilename'])
        prettyPrint(self._level, 'asBuiltVersion:  "%s"' %
                    self._info['asBuiltVersion'])
        prettyPrint(self._level, 'receiptFilename:  "%s"' %
                    self._info['receiptFilename'])
        prettyPrint(self._level, 'receiptVersion:  "%s"' %
                    self._info['receiptVersion'])
        prettyPrint(self._level, 'softAVLFilename:  "%s"' %
                    self._info['softAVLFilename'])
        prettyPrint(self._level, 'softAVLVersion:  "%s"' %
                    self._info['softAVLVersion'])
        prettyPrint(self._level, '')
        for bom in self._boms:
            bom.dump()
        prettyPrint(self._level, '')
        prettyPrint(self._level, '>>> Missing Items <<<')
        for missing in self._missings:
            missing.dump()
        prettyPrint(self._level, '>>> Missing Items <<<')
        prettyPrint(self._level, '')
        prettyPrint(self._level, '>>> Unexpected Items <<<')
        for unexpected in self._unexpecteds:
            unexpected.dump()
        prettyPrint(self._level, '>>> Unexpected Items <<<')
        prettyPrint(self._level, '### End of AsBuilt Dump ###')
        print ''

#----------------------------------------------------------------------
class AsBuiltBOM:
    def __init__(self, level):
        self._level = level
        self._info = {}
        self._sbbs = []

    def getLevel(self):
        return self._level

    def addSbb(self, sbb):
        self._sbbs.append(sbb)

    def hasSbbs(self):
        return len(self._sbbs) > 0

    def getSbbs(self):
        return self._sbbs

    def getLastSbb(self):
        return self._sbbs[len(self._sbbs) - 1]

    def getNumSbbs(self):
        return len(self._sbbs)

    def dump(self):
        prettyPrint(self._level, '>>> AsBuilt BOM <<<')
        for sbb in self._sbbs:
            sbb.dump()
        prettyPrint(self._level, '>>> AsBuilt BOM <<<')

#----------------------------------------------------------------------
class AsBuiltSBB:
    def __init__(self, level, part, desc, qty):
        self._level = level
        self._info = {
            'part' : part, 
            'desc' : desc,
            'qty' : qty
            }
        self._isDone = False
        self._items = []

    def getLevel(self):
        return self._level

    def getPart(self):
        return self._info['part']

    def getDesc(self):
        return self._info['desc']

    def getQty(self):
        return self._info['qty']

    def isDone(self):
        return self._isDone

    def assertIsDone(self):
        self._isDone = True

    def addItem(self, item):
        self._items.append(item)

    def hasItems(self):
        return len(self._items) > 0

    def getItems(self):
        return self._items

    def getLastItem(self):
        return self._items[len(self._items) - 1]

    def getNumItems(self):
        return len(self._items)

    def dump(self):
        prettyPrint(self._level, 'AsBuilt SBB: P/N="%s", Description="%s", '
                    'Qty="%s", isDone="%s"' % (self._info['part'], 
                    self._info['desc'],
                    self._info['qty'], self._isDone))
        for item in self._items:
            item.dump()

#----------------------------------------------------------------------
class AsBuiltItem:
    def __init__(self, level, part, desc, qty, vendorId=None, firmware=None):

        self._level = level
        self._info = {
            'part' : part, 
            'desc' : desc,
            'qty' : qty,
            # The following two (2) entries are used solely for reporting
            # extra information when we find unexpected items. The current
            # "Receipt and As Built Record Format Specifications Version 1.2.1"
            # defines an "item Element" as having the three (3) attributes
            # above (part, desc, qty), so there is no way in an ABR to report
            # information about what was found. These fields are used to at
            # least carry the information so that it can be reported as part
            # of a configuration check. (Refer also to Bugzilla 77260)
            'vendorId' : vendorId,
            'firmware' : firmware,
            }
        self._isDone = False
        self._isLeaf = False
        self._isDuplicate = False
        self._isAccountedFor = False
        self._items = []
        self._fulFilledBys = []

        # For debug only
        global _g_asBuiltItemIndex
        _g_asBuiltItemIndex += 1
        self._index = _g_asBuiltItemIndex

    def getLevel(self):
        return self._level

    def getPart(self):
        return self._info['part']

    def getDesc(self):
        return self._info['desc']

    def getQty(self):
        return self._info['qty']

    def getVendorId(self):
        return self._info['vendorId']

    def getFirmware(self):
        return self._info['firmware']

    def setDesc(self, desc):
        self._info['desc'] = desc

    def setQty(self, qty):
        self._info['qty'] = qty

    def setVendorId(self, vendorId):
        self._info['vendorId'] = vendorId

    def setFirmware(self, firmware):
        self._info['firmware'] = firmware

    def isDone(self):
        return self._isDone

    def assertIsDone(self):
        self._isDone = True

    def isLeaf(self):
        return self._isLeaf

    def assertIsLeaf(self):
        self._isLeaf = True

    def isDuplicate(self):
        return self._isDuplicate

    def assertIsDuplicate(self):
        self._isDuplicate = True

    def isAccountedFor(self):
        return self._isAccountedFor

    def assertIsAccountedFor(self):
        self._isAccountedFor = True

    def matchesPart(self, part):
        return part == str(self._info['part'])

    def addItem(self, item):
        self._items.append(item)

    def hasItems(self):
        return len(self._items) > 0

    def getItems(self):
        return self._items

    def getLastItem(self):
        return self._items[len(self._items) - 1]

    def getNumItems(self):
        return len(self._items)

    def addFulFilledBy(self, fulFilledBy):
        self._fulFilledBys.append(fulFilledBy)

    def hasFulFilledBys(self):
        return len(self._fulFilledBys) > 0

    def getFulFilledBys(self):
        return self._fulFilledBys

    def getLastFulFilledBy(self):
        return self._fulFilledBys[len(self._fulFilledBys) - 1]

    def getNumFulFilledBys(self):
        return len(self._fulFilledBys)

    def getMatchingItem(self, part):
        matchingItem = None
        for item in self._items:
            if item.matchesPart(part):
                matchingItem = item
                break
        return matchingItem

    def dump(self):
        align = self._level < 20 and '%-*s' % (20 - self._level, '') or ''
        prettyPrint(self._level,
                    ('AsBuilt Item: %sindex=%04d, P/N="%s", isDone=%-8s '
                    'isLeaf=%-8s isDuplicate=%-8s Qty="%s", Description="%s" '
                    'vendorId="%s", firmware="%s" '
                    'isDone="%s", isLeaf="%s", isDuplicate="%s", '
                    'isAccountedFor="%s" ') %
                    (align, self._index, self._info['part'],
                    '"%s",' % self._isDone,
                    '"%s",' % self._isLeaf,
                    '"%s",' % self._isDuplicate,
                    self._info['qty'], self._info['desc'],
                    self._info['vendorId'], self._info['firmware'],
                    self._isDone, self._isLeaf, self._isDuplicate,
                    self._isAccountedFor,
                    ))
        for item in self._items:
            item.dump()
        for fulFilledBy in self._fulFilledBys:
            fulFilledBy.dump()

#----------------------------------------------------------------------
class AsBuiltFulFilledBy:
    def __init__(self, level, part, description, vendorId, serialNo,
        model, revision, firmware, metaData):
        self._level = level
        self._info = {
            'part' : part, 
            'description' : description,
            'vendorId' : vendorId,
            'serialNo' : serialNo,
            'model' : model,
            'revision' : revision,
            'firmware' : firmware,
            'metaData' : metaData
            }
        self._isDone = False

    def getLevel(self):
        return self._level

    def getPart(self):
        return self._info['part']

    def getDescription(self):
        return self._info['description']

    def getVendorId(self):
        return self._info['vendorId']

    def getSerialNo(self):
        return self._info['serialNo']

    def getModel(self):
        return self._info['model']

    def getRevision(self):
        return self._info['revision']

    def getFirmware(self):
        return self._info['firmware']

    def getMetaData(self):
        return self._info['metaData']

    def isDone(self):
        return self._isDone

    def assertIsDone(self):
        self._isDone = True

    def dump(self):
        prettyPrint(self._level, 'AsBuilt FulFilledBy: P/N="%s", '
                    'Description="%s", Vendor ID="%s", S/N="%s", Model="%s", '
                    'Revision="%s", Firmware="%s", Meta Data="%s", '
                    'isDone="%s"' %
                    (self._info['part'], self._info['description'],
                    self._info['vendorId'], self._info['serialNo'],
                    self._info['model'], self._info['revision'],
                    self._info['firmware'], self._info['metaData'], 
                    self._isDone))

#----------------------------------------------------------------------
class AsBuiltMissing:
    def __init__(self, level):
        self._level = level
        self._info = {}
        self._items = []

    def getLevel(self):
        return self._level

    def addItem(self, item):
        self._items.append(item)

    def hasItems(self):
        return len(self._items) > 0

    def getItems(self):
        return self._items

    def getLastItem(self):
        return self._items[len(self._items) - 1]

    def getNumItems(self):
        return len(self._items)

    def dump(self):
        prettyPrint(self._level, '>>> AsBuiltMissing Items <<<')
        for item in self._items:
            item.dump()
        prettyPrint(self._level, '>>> AsBuiltMissing Items <<<')

#----------------------------------------------------------------------
class AsBuiltUnexpected:
    def __init__(self, level):
        self._level = level
        self._info = {}
        self._items = []

    def getLevel(self):
        return self._level

    def addItem(self, item):
        self._items.append(item)

    def hasItems(self):
        return len(self._items) > 0

    def getItems(self):
        return self._items

    def getLastItem(self):
        return self._items[len(self._items) - 1]

    def getNumItems(self):
        return len(self._items)

    def dump(self):
        prettyPrint(self._level, '>>> AsBuiltUnexpected Items <<<')
        for item in self._items:
            item.dump()
        prettyPrint(self._level, '>>> AsBuiltUnexpected Items <<<')

#----------------------------------------------------------------------
def explodeSbb(level, softAVL, sbb):
    status = 0
    sbbPart = sbb.getPart()
    sbbDesc = sbb.getDesc()
    sbbQty = sbb.getQty()

    if _g_debugLevel > 0:
        print 'DEBUG: Looking up "%s", "%s" in softAVL' % (sbbPart, sbbDesc)

    avlItem = softAVL.getMatchingItem(sbbPart)
    if avlItem:
        itemPart = avlItem.getItemIdentifier()
        itemDesc = avlItem.getDescription()
        if hasattr(avlItem, 'getQty'):
            itemQty = avlItem.getQty()
        else:
            itemQty = None

        # An artifact of the SoftAVL is that not all items have 
        # a quantity, so if this item is missing a quantity, 
        # fill it in using the quantity from the SBB
        if itemQty is None:
            itemQty = sbbQty

        asBuiltItem = AsBuiltItem(level, itemPart, itemDesc, itemQty)
        sbb.addItem(asBuiltItem)
        if _g_debugLevel > 0:
            print ('DEBUG: Added item "%s" to "%s" => index "%s"' %
                   (itemPart, sbbPart, _g_asBuiltItemIndex))

        level += 1
        billOfMaterials = avlItem.getBillOfMaterials()
        for billOfMaterial in billOfMaterials:
            if billOfMaterial.getNumBillOfMaterialItems() == 0:
                asBuiltItem.assertIsLeaf()
                break

            billOfMaterialItems = billOfMaterial.getBillOfMaterialItems()
            for billOfMaterialItem in billOfMaterialItems:
                bomItemPart = billOfMaterialItem.getItemIdentifier()
                if hasattr(billOfMaterialItem, 'getItemDescription'):
                    bomItemDesc = billOfMaterialItem.getItemDescription()
                else:
                    bomItemDesc = None
                bomItemQty = billOfMaterialItem.getItemQuantity() 

                asBuiltBillOfMaterialItem = AsBuiltItem(level, bomItemPart, 
                                                        bomItemDesc, bomItemQty)
                asBuiltItem.addItem(asBuiltBillOfMaterialItem)
                if _g_debugLevel > 0:
                    print ('DEBUG: Added BOM item "%s" to "%s" => '
                           'index "%s"' % 
                           (bomItemPart, itemPart, _g_asBuiltItemIndex))
                asBuiltItem.assertIsDone()
    else:
        raise ctoException('explodeSbb: softAVL missing "%s"' % sbbPart)
        status = -1
    
    if status == 0:
        sbb.assertIsDone()
    return status

#----------------------------------------------------------------------
def walkAsBuiltItemXML(doc, parentElement, thisItem, prevItem=None):

    # Don't generate XML for duplicate items
    if not thisItem.isDuplicate():
        itemElement = doc.createElement('item')
        itemElement.setAttribute('part', thisItem.getPart())
        if hasattr(thisItem, 'getDesc') and thisItem.getDesc():
            itemElement.setAttribute('desc', thisItem.getDesc())
        qty = None
        if hasattr(thisItem, 'getQty') and thisItem.getQty():
            qty = thisItem.getQty()
            itemElement.setAttribute('qty', '%s' % qty)

        workingItem = itemElement

        fulFilledBys = thisItem.getFulFilledBys()
        numFulFilledBys = len(fulFilledBys)
        for fulFilledBy in fulFilledBys:
            fulFilledByElement = doc.createElement('fulfilledBy')
            fulFilledByElement.setAttribute('part', 
                                            fulFilledBy.getPart())
            fulFilledByElement.setAttribute('description',
                                            fulFilledBy.getDescription())
            fulFilledByElement.setAttribute('vendorID', 
                                            fulFilledBy.getVendorId())
            fulFilledByElement.setAttribute('serial', 
                                            fulFilledBy.getSerialNo())
            fulFilledByElement.setAttribute('model', 
                                            fulFilledBy.getModel())
            fulFilledByElement.setAttribute('revision', 
                                            fulFilledBy.getRevision())
            fulFilledByElement.setAttribute('firmware', 
                                            fulFilledBy.getFirmware())
            fulFilledByElement.setAttribute('metaData', 
                                            fulFilledBy.getMetaData())
            workingItem.appendChild(fulFilledByElement)

            # When an item in the SoftAVL requires multiple instances
            # of the same hardware component (i.e. same P/N) to fulfill
            # it's parent item quantity, we need to clone the item
            # so that we can depict the correct parent/child relationship
            # in the ABR hierarchy.
            numFulFilledBys -= 1    # Don't need to clone the 1st item
            if qty is not None and qty == 1 and numFulFilledBys > 0:
                newItemElement = itemElement.cloneNode(False) # shallow clone
                parentElement.appendChild(newItemElement)
                workingItem = newItemElement
        
        parentElement.appendChild(itemElement)
    else:
        # When we mark duplicate items, it's always the
        # parent item that is marked as the dup, so skip
        # over the duplicate by pointing backwards to
        # the duplicate's parent.
        itemElement = parentElement

    hasItems = thisItem.hasItems and thisItem.hasItems()
    children = hasItems and thisItem.getItems() or None
    if children:
        for child in children:
            walkAsBuiltItemXML(doc, itemElement, child, thisItem)

#----------------------------------------------------------------------
def walkAsBuiltItem(level, softAVL, thisItem, prevItem=None):
    if _g_debugLevel > 0:
        print 'DEBUG: >>>>>>>>>> walkAsBuiltItem <<<<<<<<<<'
        print 'DEBUG: level', level

    global _g_asBuiltItemIndex # For debug only

    thisPart = thisItem.getPart()
    thisDesc = thisItem.getDesc()
    thisQty = thisItem.getQty()

    if prevItem is not None:
        prevPart = prevItem.getPart()
        prevDesc = prevItem.getDesc()
        prevQty = prevItem.getQty()
        if thisPart == prevPart:
            # An artifact of the SoftAVL is that not all items have 
            # a quantity and/or a description, so if either of these
            # items are missing either of those data, fill it in using
            # the data from the other item
            if prevDesc is None:
                prevItem.setDesc(thisDesc)
            elif thisDesc is None:
                thisItem.setDesc(prevDesc)
            if prevQty is None:
                prevItem.setQty(thisQty)
            elif thisQty is None:
                thisItem.setQty(prevQty)

    if _g_debugLevel > 0:
        p, i, d, l = (thisPart, thisItem._index, thisItem.isDone(),
                      thisItem.isLeaf())
        tinfo = ('thisPart "%s", index "%04d", isDone "%-5s", isLeaf "%s"' % 
                 (p, i, d, l))
        print 'DEBUG:', tinfo

    if thisItem.isDone():
        if _g_debugLevel > 0:
            print 'DEBUG: Skipping thisPart because it is already done!'
    elif thisItem.isLeaf():
        if _g_debugLevel > 0:
            print 'DEBUG: Skipping thisPart because it is a leaf!'
    else:
        if _g_debugLevel > 0:
            print ('DEBUG: Looking up "%s", "%s" in softAVL' %
                   (thisPart, thisDesc))

        avlItem = softAVL.getMatchingItem(thisPart)
        if avlItem:
            itemPart = avlItem.getItemIdentifier()
            itemDesc = avlItem.getDescription()
            if hasattr(avlItem, 'getQty'):
                itemQty = avlItem.getQty()
            else:
                itemQty = None

            asBuiltItem = AsBuiltItem(level, itemPart, itemDesc, itemQty)
            thisItem.addItem(asBuiltItem)
            if _g_debugLevel > 0:
                print ('DEBUG: Added item "%s" to "%s" => index "%s"' %
                       (itemPart, thisPart, _g_asBuiltItemIndex))

            # Whether we find BOM items below and add them, or we find
            # None (and therefore asBuiltItem is a Leaf, set below) --
            # we will be "done" with asBuiltItem in this step.
            # Assert Done now, before continuing. THIS IS CRITICAL!!!!
            if _g_debugLevel > 0:
                print ('DEBUG: Asserting done for item index "%s"' %
                       asBuiltItem._index)
            asBuiltItem.assertIsDone()

            bomLevel = level + 1
            billOfMaterials = avlItem.getBillOfMaterials()
            for billOfMaterial in billOfMaterials:
                if billOfMaterial.getNumBillOfMaterialItems() == 0:
                    asBuiltItem.assertIsLeaf()
                    break

                billOfMaterialItems = billOfMaterial.getBillOfMaterialItems()
                for billOfMaterialItem in billOfMaterialItems:
                    bomItemPart = billOfMaterialItem.getItemIdentifier()
                    if hasattr(billOfMaterialItem, 'getItemDescription'):
                        bomItemDesc = billOfMaterialItem.getItemDescription()
                    else:
                        bomItemDesc = None
                    bomItemQty = billOfMaterialItem.getItemQuantity() 

                    asBuiltBillOfMaterialItem = (
                        AsBuiltItem(bomLevel, bomItemPart, 
                                    bomItemDesc, bomItemQty))
                    asBuiltItem.addItem(asBuiltBillOfMaterialItem)
                    if _g_debugLevel > 0:
                        print ('DEBUG: Added BOM item "%s" to "%s" => '
                               'index "%s"' % 
                               (bomItemPart, itemPart, _g_asBuiltItemIndex))
        else:
            raise ctoException('walkAsBuiltItem: softAVL missing "%s"' % 
                               thisPart)

    thisItem.assertIsDone()

    hasItems = thisItem.hasItems and thisItem.hasItems()
    children = hasItems and thisItem.getItems() or None
    if children:
        if _g_debugLevel > 0:
            print ('DEBUG: thisItem "%s" has "%s" children, "%s" -> "%s"' %
                   (thisPart, len(children), thisItem._index,
                   [c._index for c in children]))
        childLevel = level + 1
        for child in children:
            walkAsBuiltItem(childLevel, softAVL, child, thisItem)

#----------------------------------------------------------------------
def walkAndMarkDuplicateItems(thisItem, prevItem=None):

    prevPart = None
    if prevItem is not None:
        prevPart = prevItem.getPart()
    
    thisPart = thisItem.getPart()

    # Note that the item which is a direct descendant (child) of the 
    # SBB is not marked as a duplicate. First, because this code
    # has no SBB context since we start the walk below the SBB and
    # second, and more importantly, we want the ABR to explicitly show
    # both the SBB and the level(s) below it.
    if thisPart == prevPart:
        if _g_debugLevel > 0:
            print 'DEBUG: Marking parent "%s" as duplicate item' % thisPart
        # Mark the parent as the duplicate, not the child
        prevItem.assertIsDuplicate()
    
    hasItems = thisItem.hasItems and thisItem.hasItems()
    children = hasItems and thisItem.getItems() or None
    if children:
        for child in children:
            walkAndMarkDuplicateItems(child, thisItem)

#----------------------------------------------------------------------
def walkAndFindMissingItems(thisItem, asBuilt, softAVL):

    # Calculate quantity to fulfill, but only for non-duplicate items
    if not thisItem.isDuplicate():
        # A count of zero signals the start of a new item to fulfill,
        # so either seed the count or continue the tally
        if _g_qtyToFulfill == 0:
            globals() ['_g_qtyToFulfill'] = thisItem.getQty()
        else:
            globals() ['_g_qtyToFulfill'] *= thisItem.getQty()

    part = thisItem.getPart()
    desc = thisItem.getDesc()

    # Skip over the duplicates and only check items that 
    # should be fulfilled
    shouldBeFulfilled = softAVL.shouldBeFulfilled(part)
    if not thisItem.isDuplicate() and shouldBeFulfilled:
        qty = _g_qtyToFulfill
        # Signal next item is start of new item to fulfill
        globals() ['_g_qtyToFulfill'] = 0

        if not thisItem.isAccountedFor():
            noMissing = qty - thisItem.getNumFulFilledBys()
            # If the number of missing items is negative, that means
            # there are too many of them, so don't count these as 
            # "missing". We want to treat them as "unexpected"...
            if noMissing > 0:
                if _g_debugLevel > 0:
                    print ('DEBUG: Found missing item "P/N=%s", "Desc=%s", '
                           'expected Qty="%s", actual Qty="%s"' % 
                           (part, desc, qty, noMissing))

                missing = AsBuiltMissing(0)
                missingItem = AsBuiltItem(0, part, desc, noMissing)
                asBuilt.addMissing(missingItem)
    
    hasItems = thisItem.hasItems and thisItem.hasItems()
    children = hasItems and thisItem.getItems() or None
    if children:
        for child in children:
            walkAndFindMissingItems(child, asBuilt, softAVL)
    elif not children and not shouldBeFulfilled:
        # if we don't have children, and we shouldn't fulfill anything, we will
        # be moving on and thus should reset fulfill qty
        if _g_debugLevel > 0:
            print ('DEBUG: Found item with no children that does not fulfill '
                   'anything P/N=%s", "Desc=%s"' % (part, desc))
        globals() ['_g_qtyToFulfill'] = 0

#----------------------------------------------------------------------
def walkAndAddFulfilledBys(thisItem, inventory, softAVL, prevItem=None):
    noFulfilled = 0

    # Calculate quantity to fulfill, but only for non-duplicate items
    if not thisItem.isDuplicate():
        # A count of zero signals the start of a new item to fulfill,
        # so either seed the count or continue the tally
        if _g_qtyToFulfill == 0:
            globals() ['_g_qtyToFulfill'] = thisItem.getQty()
        else:
            globals() ['_g_qtyToFulfill'] *= thisItem.getQty()

    # P/N to fulfill
    part = thisItem.getPart()

    # Only try and fulfill items that should be fulfilled
    if softAVL.shouldBeFulfilled(part) and not thisItem.isDuplicate():
        qty = _g_qtyToFulfill
        # Signal next item is start of new item to fulfill
        globals() ['_g_qtyToFulfill'] = 0

        # This P/N can be fulfilled by this subassembly if any 
        # of the following are true:
        #
        #   1) One of the subassembly's P/Ns matches exactly (all 9-digits)
        #   2) One of the subassembly's P/Ns is implicitly allowed to be 
        #      substituted because it's the same 7-digit P/N but a higher 
        #      (newer) tab level
        #   3) One of the subassembly's P/Ns is explicitly allowed to be 
        #      substituted via the Allowed Substitution List in the SoftAVL
        subassemblies = inventory.getSubassemblies()
        for subassembly in subassemblies:
            useIt, substitute, partToUse = False, False, None
            (eMatch, ePartNo) = subassembly.matchesPartNo(part)
            (iSub, iPartNo) = subassembly.isImplicitSubstitute(part)
            (aSub, aPartNo) = softAVL.isAllowedSubstitute(part, subassembly)
            if eMatch:
                # Exact match
                useIt, partToUse = True, ePartNo
            elif iSub:
                # Implied substitution
                useIt, substitute, partToUse = True, True, iPartNo
                subReason = '(Implied - Newer tab level)'
            elif aSub:
                # Allowed substitution
                useIt, substitute, partToUse = True, True, aPartNo
                subReason = '(Explicit - Allowed Substitution List)'

            if useIt:
                description = subassembly.getDesc()
                vendorId = subassembly.getVendorId()
                serialNo = subassembly.getSerialNo()
                model = subassembly.getModel()
                revision = subassembly.getRevision()
                firmware = subassembly.getFirmware()
                metaData = subassembly.getMeta()

                fulFilledByLevel = thisItem.getLevel() + 1
                fulFilledBy = AsBuiltFulFilledBy(fulFilledByLevel,
                                                 partToUse, description, 
                                                 vendorId, serialNo, 
                                                 model, revision, firmware, 
                                                 metaData)
                thisItem.addFulFilledBy(fulFilledBy)
                noFulfilled += 1
                
                # Account for this piece of hardware now that it was
                # used to fulfill an item
                subassembly.assertIsAccountedFor()
                subassembly.setMatchingPartNo(partToUse)

            if substitute and partToUse:
                print ('NOTICE: P/N="%s" substituted for P/N="%s" (%s)' %
                    (partToUse, part, subReason))
            
            # Once we've fulfilled the correct quantity of this item,
            # move on to the next item. If there are more subassemblies
            # of this type present, we want to leave them unaccounted
            # for so that they get counted as unexpected later on...
            if noFulfilled == qty:
                break

        # Account for this item once we've fulfilled its quantity
        if noFulfilled == qty:
            thisItem.assertIsAccountedFor()

    hasItems = thisItem.hasItems and thisItem.hasItems()
    children = hasItems and thisItem.getItems() or None
    if children:
        for child in children:
            walkAndAddFulfilledBys(child, inventory, softAVL, thisItem)

#----------------------------------------------------------------------
def walkItemIsDone(thisItem):
    isDone = thisItem.isDone()
    if not isDone:
        # Bail on the first item that is not done
        return False

    hasItems = thisItem.hasItems and thisItem.hasItems()
    children = hasItems and thisItem.getItems() or None
    if children:
        for child in children:
            walkItemIsDone(child)

    return True

#----------------------------------------------------------------------
def bomIsAllDone(asBuiltBom):
    allDone = True

    asBuiltSbbs = asBuiltBom.getSbbs()
    for asBuiltSbb in asBuiltSbbs:
        asBuiltSbbItems = asBuiltSbb.getItems()
        for asBuiltSbbItem in asBuiltSbbItems:
            if not asBuiltSbbItem.isDone():
                # Bail on the first SBB that is not done
                allDone = False
                break

            asBuiltSbbItems = asBuiltSbbItem.getItems()
            for asBuiltSbbItem in asBuiltSbbItems:
                if not walkItemIsDone(asBuiltSbbItem):
                    # Bail if any item is not done
                    allDone = False
                    break
                
    return allDone

#----------------------------------------------------------------------
def generateAsBuilt(filePath):
    asBuilt = None
    status = 0
    level = 0
    filename = os.path.basename(filePath)
    cacheFilePath = os.path.join(ROOT_DIR, MFG_DIR, INVENTORY_FILENAME)

    # We need our current receipt
    if status == 0:
        receipt = getCurrentReceipt(currentReceiptFilePath())
        if receipt is not None:
            if _g_debugLevel > 1:
                print 'DEBUG: Internal copy of receipt is as follows:'
                receipt.dump()
        else:
            status = -1

    # We need our current softAVL
    if status == 0:
        softAVL = getCurrentSoftAVL(currentSoftAVLFilePath())
        if softAVL is not None:
            if _g_debugLevel > 1:
                print 'DEBUG: Internal copy of softAVL is as follows:'
                softAVL.dump()
        else:
            status = -1

    # We need our current inventory
    if status == 0:
        try:
            status = mountMfg()
            if status == 0:
                # Use the cache if it exists, otherwise, re-generate
                if os.path.exists(cacheFilePath):
                    with open(cacheFilePath) as pickledInventory:
                        inventory = pickle.load(pickledInventory)
                else:
                    inventory = generateInventory(softAVL = softAVL)
        finally:
            unmountMfg()

        if inventory:
            if _g_debugLevel > 1:
                print 'DEBUG: Internal copy of inventory is as follows:'
                inventory.dump()
        else:
            status = -1

    if status == 0:
        date = getCurrentDateTime()
        asBuilt = AsBuilt(level, date, filename,
            SUPPORTED_ASBUILT_FORMAT_VERSION,
            receipt.getFilename(), receipt.getVersion(),
            softAVL.getFilename(), softAVL.getVersion())

        # Seed the AsBuilt Record (ABR) with the SBBs from the receipt
        receiptBoms = receipt.getBoms()
        level += 1
        for receiptBom in receiptBoms:
            level += 1
            asBuiltBom = AsBuiltBOM(level)
            receiptSbbs = receiptBom.getSbbs()
            for receiptSbb in receiptSbbs:
                part = receiptSbb.getPart()
                qty = receiptSbb.getQty()
                desc = receiptSbb.getDesc()
                asBuiltSbb = AsBuiltSBB(level, part, desc, qty)
                asBuiltBom.addSbb(asBuiltSbb)
            asBuilt.addBom(asBuiltBom)

        # Explode the SBBs using the SoftAVL
        asBuiltBoms = asBuilt.getBoms()
        for asBuiltBom in asBuiltBoms:
            level += 1
            asBuiltSbbs = asBuiltBom.getSbbs() 
            for asBuiltSbb in asBuiltSbbs:
                explodeSbb(level, softAVL, asBuiltSbb)

                allDone = False
                while not allDone:
                    sbbItemLevel = level + 1
                    asBuiltSbbItems = asBuiltSbb.getItems()
                    for asBuiltSbbItem in asBuiltSbbItems:
                        walkAsBuiltItem(sbbItemLevel, softAVL, asBuiltSbbItem)

                    if bomIsAllDone(asBuiltBom):
                        allDone = True

        # Mark duplicate items so we can filter them out when we
        # generate the the XML. Note that the duplicates are a 
        # side-effect of the structure in the SoftAVL.
        asBuiltBoms = asBuilt.getBoms()
        for asBuiltBom in asBuiltBoms:
            asBuiltSbbs = asBuiltBom.getSbbs() 
            for asBuiltSbb in asBuiltSbbs:
                asBuiltSbbItems = asBuiltSbb.getItems()
                for asBuiltSbbItem in asBuiltSbbItems:
                    walkAndMarkDuplicateItems(asBuiltSbbItem)

        # Add FulFilledBy elements
        asBuiltBoms = asBuilt.getBoms()
        for asBuiltBom in asBuiltBoms:
            asBuiltSbbs = asBuiltBom.getSbbs() 
            for asBuiltSbb in asBuiltSbbs:
                # Seed the quantity to fulfill with the SBB quantity
                globals() ['_g_qtyToFulfill'] = asBuiltSbb.getQty()
                asBuiltSbbItems = asBuiltSbb.getItems()
                for asBuiltSbbItem in asBuiltSbbItems:
                    walkAndAddFulfilledBys(asBuiltSbbItem, inventory, softAVL)

        # Locate items that are not accounted for and list them
        # as missing items
        asBuiltBoms = asBuilt.getBoms()
        for asBuiltBom in asBuiltBoms:
            asBuiltSbbs = asBuiltBom.getSbbs() 
            for asBuiltSbb in asBuiltSbbs:
                # Seed the quantity to fulfill with the SBB quantity
                globals() ['_g_qtyToFulfill'] = asBuiltSbb.getQty()
                asBuiltSbbItems = asBuiltSbb.getItems()
                for asBuiltSbbItem in asBuiltSbbItems:
                    walkAndFindMissingItems(asBuiltSbbItem, asBuilt, softAVL)

        # If we're doing a Base Chassis Burn-In, don't check for
        # unexpected items since manufacturing wants to load up
        # the base chassis with potentially unexpected hardware.
        if not _g_baseChassisBurnInFlag:
            # Locate subassemblies that are either not accounted for,
            # Black listed or Brown listed and list them as unexpected
            # items. Note that a walk is not needed in this case since 
            # the subassemblies is a flat list.
            subassemblies = inventory.getSubassemblies()
            for subassembly in subassemblies:
                # Attempt to add Isilon P/N and Description if possible
                vid = subassembly.getVendorId()
                # Only include the firmware ID in the comparison
                # if the SoftAVL indicates that it is required
                fid = None
                if softAVL.vendorIdRequiresFirmwareId(vid):
                    fid = subassembly.getFirmware()
                    if fid is not None and fid == NOT_AVAILABLE:
                        fid = None

                # All possible P/Ns
                partNos = softAVL.idsToItemIdentifiers(vid, fid)
                if len(partNos) == 0:
                    # If no P/Ns available, use Vendor ID
                    partNos.append(vid)

                # All possible Descriptions
                descs = softAVL.idsToItemDescriptions(vid, fid)
                if len(descs) == 0:
                    # If no descriptions available, use N/A 
                    descs.append(NOT_AVAILABLE)

                isUnexpected = False
                if not subassembly.isAccountedFor():
                    # Subassembly was not accounted for, so use 1st P/N
                    isUnexpected = True
                    partNo = partNos[0]
                    desc = descs[0]
                else:
                    # Subassembly was accounted for, so use the correct
                    # P/N and description when checking Black/Brown lists
                    (index, matchingPartNo) = subassembly.getMatchingPartNo()
                    partNo = matchingPartNo
                    desc = descs[index]

                    # Parts that are "Black listed" are not allowed to be
                    # used in either manufacturing or in the field. However,
                    # parts that are "Brown listed" can be used in the field,
                    # but not in manufacturing
                    if (softAVL.isBlackListed(partNo) or
                        _g_mfgFlag and softAVL.isBrownListed(partNo)):

                        isUnexpected = True

                # Once we get to this point, we've chosen a P/N and a
                # description to go into the ABR. If either or both
                # of those are N/A, replace them with something that
                # may actually be of some use to a human trying to 
                # read the ABR. We'll use the hardware type for the
                # P/N and the hardware name for the description...
                if partNo == NOT_AVAILABLE:
                    partNo = subassembly.getHwType()
                if desc == NOT_AVAILABLE:
                    desc = subassembly.getHwName()

                # Add item to ABR if we determined it was unexpected
                if isUnexpected:
                    unexpected = AsBuiltUnexpected(0)
                    unexpectedItem = AsBuiltItem(0, partNo, desc, 1, 
                        vid, fid) # qty 1
                    asBuilt.addUnexpected(unexpectedItem)

        if _g_debugLevel > 0:
            asBuilt.dump()

    result = (status != 0 or asBuilt is None) # True on failure
    addHistory('AsBuilt Generation', result = result)

    return asBuilt

#----------------------------------------------------------------------
def exportAsBuilt(fileType, filePath):
    import isi.misc
    status = 0

    asBuilt = generateAsBuilt(filePath)
    if asBuilt is None:
        print 'ERROR: Unable to export (generate) As Built Record (ABR)'
        status = -1
    else:
        # Passing a filePath to the generateXML() function
        # causes the ABR to get written to an XML file.
        asBuilt.generateXML(filePath)

        # Now save a local copy
        status = saveFile(fileType, filePath)

        if status == 0:
            # Make this the current asBuilt
            status = copyFile(filePath, currentAsBuiltFilePath())

            if status == 0 and isi.misc.ifs_mounted():
                # Cache on /ifs but ignore any error
                ignore = cacheFile(fileType)

        # Cleanup local copy repository (Mfg Partition).
        # Restrict total number of asbuit (ABR) files -- at least,
        # restrict the canonical asbuilt_<SERNO>_<TIMESTAMP>.xml files.
        asbuilt = re.compile(
            '%s_[a-zA-Z0-9]+_%s%s' %
            (FILETYPE_ASBUILT, ISO8601_UTC_DATE_TIMESTAMP_REGEX, XML_EXT))
        local = getLocalFilePaths(fileType, filePath)
        localdir = local.get('dir', [None, None])[0]
        DeleteIdenticalFiles(localdir, asbuilt, limit=ASBUILT_FILE_LIMIT)

    return status

#======================================================================
#====================== End of As Built Record ========================
#======================================================================

#======================================================================
#========================= User Interface =============================
#======================================================================

#----------------------------------------------------------------------
def importFile(fileType, filePath):
    status = 0

    if fileType in [ FILETYPE_PROBE ]:
        status = importProbeConfig(fileType, filePath)
    elif fileType in [ FILETYPE_RECEIPT ]:
        status = importReceipt(fileType, filePath)
    elif fileType in [ FILETYPE_SOFTAVL ]:
        status = importSoftAVL(fileType, filePath)
    elif fileType in [ FILETYPE_ASBUILT ]:
        print 'ERROR: Can not import a fileType="%s"' % fileType
        status = -1
    else:
        print 'ERROR: Invalid fileType'
        status = -1

    addHistory('File import, fileType="%s", filePath="%s"' %
               (fileType, filePath), result = status)

    return status

#----------------------------------------------------------------------
def exportFile(fileType, filePath):
    status = 0

    if fileType in [ FILETYPE_PROBE, FILETYPE_RECEIPT, FILETYPE_SOFTAVL ]:
        print ('ERROR: Can not export a fileType="%s". '
               'Use --display instead' % fileType)
        status = -1
    elif fileType in [ FILETYPE_ASBUILT ]:
        addDiskUsageHistory()
        status = exportAsBuilt(fileType, filePath)
    else:
        print 'ERROR: Invalid fileType'
        status = -1

    addHistory('File export, fileType="%s", filePath="%s"' %
               (fileType, filePath), result = status)

    return status

#----------------------------------------------------------------------
def secretShowMe():
    status = 0

    baseSerialNo = hal.getChassisSerialNumber(0)
    baseConfigNo = hal.getChassisConfigNumber(0)
    print 'Base chassis S/N is currently set to "%s"' % baseSerialNo
    print 'Base chassis P/N is currently set to "%s"' % baseConfigNo
    displayItem('receipt')

    return status

#----------------------------------------------------------------------
def _startUp(shell=True, reImage=False):
    status = 0

    bothExist = False
    mfgVersion = None
    currentVersion = None
    autoImportCurrent = False
    forceImportCurrent = False
    mfgProbeConfigLatest = False
    global _g_restartFlag
    global _g_supportsCMCFlash

    # Make sure the MFG partition is not mounted so we can check to see
    # if there's an inventory cache file present in the mount point
    # directory. If there is, remove it.  (Refer to Bugzilla 81749)
    unmountMfg()
    cacheFilePath = os.path.join(ROOT_DIR, MFG_DIR, INVENTORY_FILENAME)
    if os.path.exists(cacheFilePath):
        cmd = '/bin/rm -f %s' % cacheFilePath
        (error, output) = procs.get_cmd_output(cmd)
        if error:
            for line in output:
                print line
            print 'ERROR: Unable to remove "%s"' % cacheFilePath

    # for nodes that support it, restore any missing files to the /mfg partition
    # that we can from the CMC flash filesystem
    recoverList = []
    levelList = []
    if _g_supportsCMCFlash and not reImage:
        #Check for mandatory files in flash, mode = 1
        flashStatus = checkFFS(1)

        # look up mandatory files (receipt and softAVL)
        receipt = getCurrentReceipt(currentReceiptFilePath())
        softAVL = getCurrentSoftAVL(currentSoftAVLFilePath())
        if receipt is None:
            recoverList.append(FILETYPE_RECEIPT)
        if softAVL is None:
            recoverList.append(FILETYPE_SOFTAVL)

        # look up probe_configs
        try:
            status = mountMfg()
            if status == 0:
                for level in PROBE_LEVELS:
                    mfgPath = mfgProbeConfigFilePath(level)
                    mfgExists = os.access(mfgPath, os.F_OK)
                    if not mfgExists:
                        if FILETYPE_PROBE not in recoverList:
                            recoverList.append(FILETYPE_PROBE)
                        levelList.append(level)
        finally:
            unmountMfg()

        if len(recoverList) > 0:
            log('Restoring CTO data from flash', syslog.LOG_NOTICE)
            if _g_debugLevel > 0:
                log('DEBUG: recoverList is %s' % recoverList, syslog.LOG_INFO)
                log('DEBUG: levelList is %s' % levelList, syslog.LOG_INFO)

            result = getCTOFromCMCFFS(recover=recoverList, pclevels=levelList)
            if result is None:
                log('ERROR: could not execute flash restore for:',
                    syslog.LOG_NOTICE)
                for r in recoverList:
                    log(' - %s' % r, syslog.LOG_NOTICE)
            else:
                for name in result['pass']:
                    log(' - %10s SUCCESS' % name, syslog.LOG_NOTICE)
                for name in result['fail']:
                    log(' - %10s FAIL' % name, syslog.LOG_NOTICE)
        else:
            pass

    # NOTES: If for some reason, the probe configuration file is missing
    #        from the MFG partition, then we want to automatically force
    #        an import of the version that is in OneFS.
    #
    #        If OneFS was updated prior to this system boot, the
    #        copy of the probe configuration file that came with the
    #        update may be newer than the copy that was last imported,
    #        so if that is the case, automatically import the newer
    #        version.
    #
    #        We should do this for all possible probe_config files.  In the 
    #        event we downgrade later we want the MFG partition to be 
    #        up-to-date.
    for level in PROBE_LEVELS:
        status = 0
        bothExist = False
        mfgVersion = None
        currentVersion = None
        autoImportCurrent = False
        forceImportCurrent = False

        try:
            status = mountMfg()
            if status == 0:
                currentPath = currentProbeConfigFilePath(level)
                mfgPath = mfgProbeConfigFilePath(level)

                # Be pedantic and if for some reason we don't have a probe
                # configuration file on the MFG partition, force the one 
                # that is in OneFS to be imported.
                currentExists = os.access(currentPath, os.F_OK)
                if not currentExists:
                    # raise ctoException('_startUp: "%s" does not exist' % 
                    #                    currentPath)
                    print 'WARNING: "%s" does not exist' % currentPath

                mfgExists = os.access(mfgPath, os.F_OK)
                if not mfgExists:
                    print 'WARNING: "%s" does not exist' % mfgPath

                if currentExists and mfgExists:
                    bothExist = True

                if currentExists and not mfgExists:
                    forceImportCurrent = True
                    
                if currentExists:
                    currentVersion = getProbeConfigVersion(currentPath)
                    if currentVersion is None:
                        print ('ERROR: Unable to get version of "%s"' % 
                                currentPath)
                        status = -1
                    else:
                        if _g_debugLevel > 0:
                            print ('DEBUG: Version of "%s" is "%s"' % 
                                   (currentPath, currentVersion))

                if mfgExists:
                    mfgVersion = getProbeConfigVersion(mfgPath)
                    if mfgVersion is None:
                        print 'ERROR: Unable to get version of "%s"' % mfgPath
                        status = -1
                    else:
                        if _g_debugLevel > 0:
                            print ('DEBUG: Version of "%s" is "%s"' % 
                                   (mfgPath, mfgVersion))
        finally:
            # Even though it will get re-mounted below...
            unmountMfg()

        # If both files exist and we were able to get the version numbers of
        # each, then we can compare versions and figure out which one wins
        if bothExist and currentVersion is not None and mfgVersion is not None:
            if float(currentVersion) > float(mfgVersion):
                if _g_debugLevel > 0:
                    print ('"%s" (%s) is newer than "%s" (%s)' %
                           (currentPath, currentVersion, 
                           mfgPath, mfgVersion))
                autoImportCurrent = True
            elif float(mfgVersion) > float(currentVersion):
                try:
                    level =  int(level)
                    if (level == CURRENT_PROBE_LEVEL) and reImage:
                        mfgProbeConfigLatest = True
                except ValueError:
                    pass

        if forceImportCurrent or autoImportCurrent:
            if isNpiFile(currentPath) and not supportNpi():
                print ('Blocking import of "%s", NPI probe_config '
                       'files not supported.' % currentPath)
                continue
            # NOTE: We use "saveFile()" to import the file rather than
            #       "importProbeConfig()" because the later doesn't 
            #       realize that the "current" version is the one that
            #       needs to be imported.
            print ('Updating "%s" to version "%s"' % (mfgPath, currentVersion))
            status = saveFile(FILETYPE_PROBE, currentPath, reImage)
        if status != 0:
            break

    # Now that we are sure the copy of the probe configuration file
    # on the MFG partition is up-to-date, automatically copy it to
    # /etc/gconfig and make it the "current" version.
    if status == 0 and not reImage:
        try:
            status = mountMfg()
            if status == 0:
                srcPath = mfgProbeConfigFilePath()
                dstPath = currentProbeConfigFilePath()
                if _g_debugLevel > 0:
                    print 'DEBUG: Checking to see if "%s" exists' % srcPath
                if os.path.exists(srcPath):
                    print ('Ensuring "%s" is updated to latest '
                           'version ("%s")' % 
                           (dstPath, getProbeConfigVersion(srcPath)))
                    status = copyFile(srcPath, dstPath)
        finally:
            # Even though it will get re-mounted below...
            unmountMfg()
            if status != 0:
                return status

    # If we're missing the base chassis S/N, P/N or a receipt, stop dead
    # in our tracks and force recovery
    baseSerialNo = hal.getChassisSerialNumber(0)
    baseConfigNo = hal.getChassisConfigNumber(0)

    msg_shell = [
        'Chassis is missing a base S/N and/or a base SKU and/or a receipt.',
        'Both must be provided before the boot will be allowed to continue.',
        '',
        'Please contact EMC Isilon Technical Support for assistance.',
        'Entering zsh shell now.'
        ]
    msg_no_shell = [
        'Chassis is missing a base S/N and/or a base SKU and/or a receipt.',
        'Both must be provided before init will be allowed to continue.',
        ]
    receipt = getCurrentReceipt(currentReceiptFilePath())
    if ((baseSerialNo is None or baseConfigNo is None or receipt is None) and
        not reImage):
        if shell:
            for line in msg_shell:
                print line
            removePidFile()
            status = os.system('/usr/local/bin/zsh')
        else:
            for line in msg_no_shell:
                print line
            status = 1
        return status

    print 'Initializing hardware capability/attribute/inventory caches...'
 
    # We don't need a SoftAVL to generate hardware capabilities and
    # attributes, but we do need one if we are going to cache the
    # inventory. If the cached inventory is used to generate ABRs,
    # then we need the inventory to have Isilon P/Ns included, so 
    # attempt to get a SoftAVL.
    softAVL = getCurrentSoftAVL(currentSoftAVLFilePath())
    if softAVL is not None:
        if _g_debugLevel > 1:
            print 'DEBUG: Internal copy of softAVL is as follows:'
            softAVL.dump()

    inventory = generateInventory(updateHwCaches = True, softAVL = softAVL,
                                  mfgProbeConfig=mfgProbeConfigLatest)
    if inventory is None:
        print 'ERROR: Unable to generate inventory'
        status = -1

    if _g_debugLevel > 0:
        print 'DEBUG: Caching inventory...'
    # If the inventory was generated using a SoftAVL, then it has 
    # P/Ns included, so we can safely cache a copy.
    if softAVL is not None:
        try:
            status = mountMfg()
            if status == 0:
                with open(cacheFilePath, 'wb') as pickledSoftAVL:
                    pickle.dump(inventory, pickledSoftAVL)
        finally:
            unmountMfg()

    return status

#----------------------------------------------------------------------
def startUp(reImage=False):
    if reImage:
        shell = False
    else:
        shell = True
    status = _startUp(shell = shell, reImage = reImage)
    addHistory('Start up', result = status)
    return status

#----------------------------------------------------------------------
def reInit():
    status = _startUp(shell = False)
    addHistory('Re-Initialize', result = status)
    return status

#----------------------------------------------------------------------
def configCheck():
    status = 0

    # All As Built Records (ABRs) actually a filename, but in this,
    # case, we're not actually creating a file, we're just building
    # an ABR in memory so that we can do our comparisons, so just
    # pass in a bogus filename since it won't really hurt anything.
    filePath = '/tmp/asbuiltJunk'
    asBuilt = generateAsBuilt(filePath)
    if asBuilt is None:
        print 'ERROR: Unable to generate As Built Record (ABR)'
        status = -1
    else:
        # We need our current softAVL for Black/Brown list support
        # NOTE: We don't need to bother checking if we were able to
        #       get a SoftAVL here since generateAsBuilt() above would
        #       have failed if there wasn't one and we'd never end up
        #       here in that case.
        softAVL = getCurrentSoftAVL(currentSoftAVLFilePath())

        missingReports = []
        numMissings = asBuilt.getNumMissings()
        missings = asBuilt.getMissings()
        for missing in missings:
            part = missing.getPart()
            desc = missing.getDesc()
            qty = missing.getQty()
            missingReports.append('P/N="%s", Desc="%s", Qty="%s"' %
                                  (part, desc, qty))

        unexpectedReports = []
        numUnexpecteds = asBuilt.getNumUnexpecteds()
        unexpecteds = asBuilt.getUnexpecteds()
        for unexpected in unexpecteds:
            part = unexpected.getPart()
            desc = unexpected.getDesc()
            qty = unexpected.getQty()
            vid = unexpected.getVendorId()
            fid = unexpected.getFirmware()
            
            additionalInfo = ''
            if softAVL.isBlackListed(part):
                additionalInfo = '===> Black listed'
            elif _g_mfgFlag and softAVL.isBrownListed(part):
                additionalInfo = '===> Brown listed'
            
            unexpectedReports.append('P/N="%s", Desc="%s", Qty="%s", '
                                     'Vendor ID="%s", Firmware="%s" %s' % 
                                     (part, desc, qty, vid, fid, 
                                     additionalInfo))

        if numMissings > 0 or numUnexpecteds  > 0:
            print 'ERROR: Configuration check FAILED!!!'
            print ''
            if numMissings > 0:
                print ('%s missing BOM item(s). '
                       'Part breakdown is as follows:' % numMissings)
                num = 1
                for report in missingReports:
                    print ' %+2s) %s' % (num, report)
                    num += 1
                status = -1

            print''

            if numUnexpecteds  > 0:
                print ('%s unexpected BOM item(s). '
                       'Part breakdown is as follows:' % numUnexpecteds)
                num = 1
                for report in unexpectedReports:
                    print ' %+2s) %s' % (num, report)
                    num += 1
                status = -1
        else:
            print 'Configuration check PASSED'

    addHistory('Configuration Check', result = status)

    return status

#----------------------------------------------------------------------
def displayFile(filePath):
    status = 0

    if filePath is None:
        print 'ERROR: displayFile: missing filePath'
        status = -1
        return status

    try:
        status = mountMfg()
        if status == 0:
            if os.access(filePath, os.F_OK):
              output = []
              with open(filePath, 'r') as fd:
                  try:
                      if fd:
                          output = fd.readlines()
                  except IOError, e:
                      if e.errno == errno.EPIPE:
                          # In case output was piped into 'head'
                          pass
                      else:
                          raise
              for line in output:
                  print line.rstrip('\n\r')
            else:
                if _g_debugLevel > 0:
                    print 'DEBUG: "%s" does not exist' % filePath
                status = -1
    finally:
        unmountMfg()

    return status

#----------------------------------------------------------------------
def displayProbeConfig():
    status = 0

    filePath = currentProbeConfigFilePath()
    status = displayFile(filePath)
    if status != 0:
        print 'There is no probe configuration installed. One must be imported'
        status = -1

    return status

#----------------------------------------------------------------------
def displayReceipt():
    status = 0

    filePath = currentReceiptFilePath()
    status = displayFile(filePath)
    if status != 0:
        print 'There is no receipt installed. One must be imported'
        status = -1

    return status

#----------------------------------------------------------------------
def displaySoftAVL():
    status = 0

    filePath = currentSoftAVLFilePath()
    status = displayFile(filePath)
    if status != 0:
        print 'There is no softAVL installed. One must be imported'
        status = -1

    return status

#----------------------------------------------------------------------
def displayInventory():
    status = 0
    elapsedTime = None
    cacheFilePath = os.path.join(ROOT_DIR, MFG_DIR, INVENTORY_FILENAME)

    try:
        status = mountMfg()
        if status == 0:
            # Use the cache if it exists, otherwise, re-generate
            if os.path.exists(cacheFilePath):
                if _g_debugLevel > 0:
                    print 'DEBUG: Getting inventory from cache'
                with open(cacheFilePath) as pickledInventory:
                    inventory = pickle.load(pickledInventory)
            else:
                # NOTE: Timing is for debug only
                startTime = time.time()
                inventory = generateInventory()
                endTime = time.time()
                elapsedTime = endTime - startTime
    finally:
        unmountMfg()

    if inventory is None:
        print 'ERROR: Unable to display inventory'
        status = -1
    else:
        print 'Inventory for chassis "%s"' % hal.getChassisSerialNumber(0)
        inventory.dump()

    if _g_debugLevel > 0:
        if elapsedTime is not None:
            print 'DEBUG: Inventory took %.4f seconds to generate' % elapsedTime

    return status

#----------------------------------------------------------------------
def displayAsBuilt():
    status = 0

    filePath = currentAsBuiltFilePath()
    status = displayFile(filePath)
    if status != 0:
        print 'There is no As Built Record (ABR). One must be created.'

    return status

#----------------------------------------------------------------------
def displayCTOFileList():
    status = 0

    receiptSubdir = os.path.join(ROOT_DIR, MFG_DIR, RECEIPT_SUBDIR)
    softAVLSubdir = os.path.join(ROOT_DIR, MFG_DIR, SOFTAVL_SUBDIR)
    asBuiltSubdir = os.path.join(ROOT_DIR, MFG_DIR, ASBUILT_SUBDIR)
    probeSubdir = os.path.join(ROOT_DIR, MFG_DIR, PROBE_SUBDIR)
    gconfigSubdir = os.path.join(ROOT_DIR, GCONFIG_DIR, GCONFIG_SUBDIR)
    directories = [ receiptSubdir, softAVLSubdir, asBuiltSubdir, 
                    probeSubdir, gconfigSubdir ]
    try:
        status = mountMfg()
        if status == 0:
            for directory in directories:
                print '<<< %s >>>' % directory
                cmd = '/bin/ls -lt %s' % directory
                (error, output) = procs.get_cmd_output(cmd)
                if error:
                    for line in output:
                        print line
                    print 'ERROR: Unable to list "%s"' % directory
                    status = -1
                else:
                    for line in output:
                        print line
                    print ''
    finally:
        unmountMfg()

    return status

#----------------------------------------------------------------------
def displayHistory():
    status = 0
    filePath = os.path.join(ROOT_DIR, MFG_DIR, HISTORY_FILENAME)
    print 'Log of events:'
    status = displayFile(filePath)
    if status != 0:
        print 'There is no history available'
        status = -1

    if _g_debugLevel > 0:
        print ''
        print 'Files in MFG partition (locally on node)'
        displayCTOFileList()

    return status

#----------------------------------------------------------------------
def displayItem(itemType):
    status = 0

    if itemType in [ ITEM_PROBE ]:
        status = displayProbeConfig()
    elif itemType in [ ITEM_RECEIPT ]:
        status = displayReceipt()
    elif itemType in [ ITEM_SOFTAVL ]:
        status = displaySoftAVL()
    elif itemType in [ ITEM_INVENTORY ]:
        status = displayInventory()
    elif itemType in [ ITEM_ASBUILT ]:
        status = displayAsBuilt()
    elif itemType in [ ITEM_HISTORY ]:
        status = displayHistory()
    else:
        print 'ERROR: Invalid itemType'
        status = -1

    addHistory('Display item, itemType="%s"' % itemType, result = status)

    return status

#----------------------------------------------------------------------
def cacheFile(fileType):
    import isi.sys.version as sysver
    import isi.config.arrcfg as arrcfg
    import isi.misc

    status = 0

    if fileType in [ FILETYPE_PROBE ]:
        srcPath = currentProbeConfigFilePath()
    elif fileType in [ FILETYPE_RECEIPT ]:
        srcPath = currentReceiptFilePath()
    elif fileType in [ FILETYPE_SOFTAVL ]:
        srcPath = currentSoftAVLFilePath()
    elif fileType in [ FILETYPE_ASBUILT ]:
        srcPath = currentAsBuiltFilePath()
    else:
        print "ERROR: cache option is not valid on fileType: %s" % fileType
        return -1

    if not arrcfg.is_local_configured():
        print "Error caching %s: unconfigured local node" % fileType
        status = -1
    elif not isi.misc.ifs_ready():
        print "Error caching %s: /ifs not ready" % fileType
        status = -1
    else:
        try:
            dstDir = sysver.get_node_info_cache_dir(arrcfg.get_local_guid())
            dstPath = os.path.join(dstDir, os.path.basename(srcPath))
            status = copyFile(srcPath, dstPath)
        except Exception, e:
            print "Error caching %s: %s" % (fileType, str(e))
            status = -1

    return status

#----------------------------------------------------------------------
# NOTE: Do not advertise the "mfg" or "bcbi" options. These options are
#       intended for use by the manufacturing toolset, so do not tell the
#       general users about them in the usage message.
def usage():
    usage = (
'''Usage: %s [OPTION] [ARGS]

OPTIONS:
    -h, --help      Prints this text
    --startUp       Execute system startup tasks (used by system boot only)
    --reInit        Refresh hardware capability/attribute/inventory caches
    --cache         Cache a fileType in the node_info cache on /ifs

    --display       Display an item from the node (see ITEM TYPES below)
                    NOTE: Displays the current version of the item that is
                          stored on the node. It DOES NOT re-generate the item.
    --importFile    Import a file to the node (receipt, softAVL, probeconfig)
    --exportFile    Export a file from the node (asbuilt)
                    NOTE: This generates the new asbuilt (a.k.a ABR) and
                          exports it to the specified filePath.
    --configCheck   Perform configuration check

    --cleanUp       Remove all un-necessary (including backup) files created
                    by import/export, as well as the original copy of imported
                    files. All files that match the following patterns will be
                    removed: '%s', '%s', '%s',
                    '%s', '%s',
                    '%s' and '%s'.
    --saveCto       Save CTO files from MFG to backup location, if supported

ARGS:
    --itemType      Type of item to display
                      (see ITEM TYPES below)
    --fileType      Type of file to import/export
                      (see FILE TYPES below)
    --filePath      Full path including the desired filename
    --debugLevel    Set the debug level
                      (Higher values mean more verbosity)

FILE TYPES:
%s

ITEM TYPES:
%s

NOTE: Tool intended for use on CTO capable nodes and this node %s CTO capable.
''')

    cto = False
    if hal.supportsCto():
        cto = True
        ctoCapable = 'is'
    else:
        ctoCapable = 'is not'
    
    print usage % (sys.argv[0], XML_BACKUP_FILES, XML_ORDER_FILES, 
                   XML_BTS_FILES, XML_SAP_FILES, XML_PROPEL_FILES,
                   XML_SOFTAVL_FILES, XML_ASBUILT_FILES,
                   _g_validFileTypes, _g_validItemTypes, ctoCapable)

    note = ''
    if cto:
        msg = 'Node is CTO based.'
    else:
        # If HAL says the node is not CTO capable, it simply means
        # that the chassis does not have one of the specific base
        # chassis SKUs that make it a CTO base chassis. HAL currently
        # does not contain an API that specifically says that the node
        # is an 85X product (SKU based) node. If, in the future, we 
        # add such an API, the code below can be replaced with the new
        # API, but for now, since we are not developing any new 851
        # products, seems like not very much gain in doing so. 
        baseConfigNo = hal.getChassisConfigNumber(0)
        m1 = re.match('^85[01].*', baseConfigNo)
        m2 = re.match('^812.*', baseConfigNo)
        if m1 or m2:
            msg = 'Node is SKU based. SKU is %s.' % baseConfigNo
        if m2:
            note = '(WARNING: 812 SKUs are only valid for 3/4 chassis)'

    print '%s %s' % (msg, note)

#----------------------------------------------------------------------
def main(argv):
    global _g_cmcFlashOutOfSync
    global _g_supportsCMCFlash

    try:
        opt, args = getopt.getopt(argv, 'h',
                                  ['help', 'mfg', 'bcbi', 'startUp', 
                                   'reInit', 'display', 'importFile', 
                                   'exportFile', 'cache', 'fileType=', 
                                   'filePath=', 'itemType=', 'configCheck', 
                                   'cleanUp', 'debugLevel=', 'supportNpi',
                                   'reImage', 'saveCto', 'revert'])

    except getopt.GetoptError, err:
        print str(err)
        usage()
        sys.exit(1)

    if opt and len(opt) == 0 and args and len(args) == 0:
        usage()
        sys.exit(1)
        
    importFlag = False
    exportFlag = False
    cacheFlag = False
    startUpFlag = False
    reInitFlag = False
    configCheckFlag = False
    displayFlag = False
    cleanUpFlag = False
    reImageFlag = False
    revertFlag = False
    saveCTOFlag = False

    fileType = None
    filePath = None
    itemType = None

    optionCount = 0
    nonHelp = False

    for o, a in opt:
        if o in ('-h', '--help'):
            usage()
            sys.exit(0)
        elif o in ('--mfg'):
            nonHelp = True
            globals() ['_g_mfgFlag'] = True
        elif o in ('--bcbi'):
            nonHelp = True
            globals() ['_g_baseChassisBurnInFlag'] = True
        elif o in ('--startUp'):
            nonHelp = True
            startUpFlag = True
            optionCount += 1
        elif o in ('--reInit'):
            nonHelp = True
            reInitFlag = True
            optionCount += 1
        elif o in ('--revert'):
            nonHelp = True
            revertFlag = True
            optionCount += 1
        elif o in ('--supportNpi'):
            nonHelp = True
            globals() ['_g_supportNpi'] = True
        elif o in ('--display'):
            nonHelp = True
            displayFlag = True
            optionCount += 1
        elif o in ('--importFile'):
            nonHelp = True
            importFlag = True
            optionCount += 1
        elif o in ('--exportFile'):
            nonHelp = True
            exportFlag = True
            optionCount += 1
        elif o in ('--cache'):
            nonHelp = True
            cacheFlag = True
            optionCount += 1
        elif o in ('--configCheck'):
            nonHelp = True
            configCheckFlag = True
            optionCount += 1
        elif o in ('--cleanUp'):
            nonHelp = True
            cleanUpFlag = True
            optionCount += 1
        elif o in ('--fileType'):
            nonHelp = True
            fileType = str(a)
        elif o in ('--filePath'):
            nonHelp = True
            filePath = str(a)
        elif o in ('--itemType'):
            nonHelp = True
            itemType = str(a)
        elif o in ('--reImage'):
            nonHelp = True
            reImageFlag = True
        elif o in ('--saveCto'):
            nonHelp = True
            saveCTOFlag = True
        elif o in ('--debugLevel'):
            if not a.isdigit():
                print 'Invalid debugLevel %s' % a
                sys.exit(1)
            nonHelp = True
            globals() ['_g_debugLevel'] = int(a)
        else:
            nonHelp = True
            print 'Unhandled option ("%s")' % o
            sys.exit(1)

    # A special, hidden capability to allow QA to check the current
    # state of a node, even if it has not been converted to a CTO
    # node as of yet
    if _g_debugLevel == SECRET_SHOWME_DEBUG_LEVEL:
        status = secretShowMe()
        sys.exit(0)

    if not hal.supportsCto() and nonHelp:
        print '%s: Tool is only supported on CTO capable nodes' % sys.argv[0]
        sys.exit(1)

    # Sanity checks
    if optionCount > 1:
        print 'Only a single option is allowed'
        sys.exit(1)
    if importFlag or exportFlag:
        if fileType is not None and fileType not in _g_validFileTypes:
            print 'Invalid fileType'
            sys.exit(1)
        if fileType is None or filePath is None:
            print 'Imports and exports require both fileType and filePath'
            sys.exit(1)
    if displayFlag:
        if itemType is None:
            print 'Display requires itemType'
            sys.exit(1)
        if itemType and itemType not in _g_validItemTypes:
            print 'Invalid itemType'
            sys.exit(1)
    if cacheFlag:
        if fileType is not None and fileType not in _g_validFileTypes:
            print 'Invalid fileType'
        elif fileType is None:
            print 'Cache requires fileType'
            sys.exit(1)

    # Allow only a single copy of the tool to run at any given time
    pid = str(os.getpid())
    if os.path.exists(PIDFILE):
        print ('"%s" is already running, please try again '
               'in a few seconds' % sys.argv[0])
        sys.exit(1)
    else:
        createPidFile(pid)
    
    # Start logging
    syslog.openlog('isi_inventory_tool', 0, syslog.LOG_USER)
    syslog.syslog(syslog.LOG_INFO, 'Inventory tool started')

    status = None
    currentCommand = None
    try:
        # Option decode
        if importFlag:
            currentCommand = 'importFile'
            status = importFile(fileType, filePath)
    
        elif exportFlag:
            currentCommand = 'exportFile'
            status = exportFile(fileType, filePath)
    
        elif cacheFlag:
            currentCommand = 'cache'
            status = cacheFile(fileType)

        elif startUpFlag:
            currentCommand = 'startUp'
            status = startUp(reImage=reImageFlag)

        elif reInitFlag:
            currentCommand = 'reInit'
            status = reInit()

        elif configCheckFlag:
            currentCommand = 'configCheck'
            status = configCheck()

        elif displayFlag:
            currentCommand = 'display'
            status = displayItem(itemType)

        elif cleanUpFlag:
            currentCommand = 'cleanUp'
            status = cleanUp()
        elif revertFlag:
            currentCommand = 'revert'
            hwCapStatus, hwCap, hwCapDetails = revertHwCapabilitiesToDefault()
            hwAttrStatus, hwAttr, hwAttrDetails = revertHwAttributesToDefault()
            status = hwCapStatus or hwAttrStatus 

        # save CTO info to CMC flash, if supported and needed
        if saveCTOFlag and not reImageFlag:
            currentCommand = 'saveCTO'
            if not _g_supportsCMCFlash:
                print ('ERROR: %s command not supported on this platform.' %
                    currentCommand)
                removePidFile()
                sys.exit(1)

            if _g_debugLevel > 0:
                log('Performing CTO save operation, if needed',
                    syslog.LOG_NOTICE)

            # If mandatory files are missing or FFS is empty, always
            # save CTO information. Otherwise, only save if they are
            # out of sync.
            status = checkFFS(1)
            if status == 0:
                #Restore backup to /tmp/XXXX directory
                status = restoreCTOtoTmp(md5=True)
                if status == 0:
                    # MFG is still master copy, so if files exist that are
                    # 'different' we should do a save to flash
                    cur_receipt = currentReceiptFilePath()
                    tmp_receipt = tmpReceiptFilePath()
                    cur_softAVL = currentSoftAVLFilePath()
                    tmp_softAVL = tmpSoftAVLFilePath()
                    try:
                        status = mountMfg()
                        cmd = 'md5 -q %s | diff %s.md5 -' % (cur_receipt, tmp_receipt)
                        error, output = procs.get_cmd_output(cmd)
                        if error:
                            log('CTO file receipt out of sync with mfg',
                                syslog.LOG_NOTICE)
                            _g_cmcFlashOutOfSync = True
                        cmd = 'md5 -q %s | diff %s.md5 -' % (cur_softAVL, tmp_softAVL)
                        error, output = procs.get_cmd_output(cmd)
                        if error:
                            log('CTO file softAVL out of sync with mfg',
                                syslog.LOG_NOTICE)
                            _g_cmcFlashOutOfSync = True
                        for level in PROBE_LEVELS:
                            cur_probeconfig = mfgProbeConfigFilePath(level)
                            tmp_probeconfig = tmpProbeConfigFilePath(level)
                            cmd = 'md5 -q %s | diff %s.md5 -' % (cur_probeconfig, tmp_probeconfig)
                            error, output = procs.get_cmd_output(cmd)
                            if error:
                                log('CTO file probe_config (level %s) out of sync with mfg' % level,
                                    syslog.LOG_NOTICE)
                                _g_cmcFlashOutOfSync = True
                    except OSError:
                        log('CTO backup file(s) missing, save needed',
                            syslog.LOG_NOTICE)
                        _g_cmcFlashOutOfSync = True
                    finally:
                        unmountMfg()

                    # if we know MFG was updated, just do the save
                    if _g_cmcFlashOutOfSync:
                        status = saveCTOtoCMCFFS()
                    else:
                        log('CMC Flash is in sync with MFG, skipping save',
                            syslog.LOG_NOTICE)
                else:
                    log('ERROR: saveCTO aborted due to error',
                        syslog.LOG_NOTICE)
                    status = 1
            else:
                log('CTO mandatory backup file(s) missing, save needed',
                    syslog.LOG_NOTICE)
                status = saveCTOtoCMCFFS()


    # Since all calls to sys.exit() will generate this exception,
    # we have to register for it or we'll get tracebacks, even if
    # we don't do anything with the exception.
    except SystemExit:
        pass

    # In case someone gets impatient...
    except KeyboardInterrupt:
        msg = '"%s" command aborted by user (Control-C)' % currentCommand
        syslog.syslog(syslog.LOG_NOTICE, msg)
        print msg

        removePidFile()
        sys.exit(1)

    # In case we hit one of our own exceptions
    except ctoException as e:
        print 'CTO exception -- %s' % e.value

        # if debug is enabled, the user wants to see the traceback,
        # so oblige them...
        if _g_debugLevel > 0:
            raise

        # Make sure to clean up
        removePidFile()
        sys.exit(1)

    # Bucket for all other exception
    except:
        print 'Unexpected exception: ', sys.exc_info()[0]

        # if debug is enabled, the user wants to see the traceback,
        # so oblige them...
        if _g_debugLevel > 0:
            raise

        # Make sure to clean up
        removePidFile()
        sys.exit(1)

    # Paranoia - make sure we don't leave without cleaning up
    finally:
        removePidFile()

    syslog.closelog()

    # Catch-all -- if status is not set, nothing was processed
    if status is None:
        usage()
        removePidFile()
        sys.exit(0)

    # Clean up
    removePidFile()

    # Translate internal status into exit status
    sys.exit(status and 1 or 0)


#----------------------------------------------------------------------
if __name__ == '__main__':
    main(sys.argv[1:])
