#!/usr/bin/env python
#
# This script provides the logic required to transform a 3/4 chassis
# into product, both at initial installation time, as well as during
# a disk tango operation. 
#

import re
import os
import sys
import getopt
import signal
from syslog import openlog, syslog, LOG_USER, LOG_EMERG, LOG_ALERT, \
    LOG_CRIT, LOG_ERR, LOG_WARNING, LOG_NOTICE, LOG_INFO, LOG_DEBUG

#
# Globals
#
_g_debug_flag = False
_g_dry_run_flag = False

#
# SKU prefixes that we support
#
VALID_3QTR_SKU_PREFIX = '812'
VALID_PRODUCT_SKU_PREFIX = '851'

#
# No. of blocks on 1, 2 and 3 TB drives, respectively
#
BLOCKS_1TB = 1953525168
BLOCKS_2TB = 3907029168
BLOCKS_3TB = 5860533168

#
# EEPROM key prefix used for legacy data
#
CHAS_VPD_EEPROM_KEY_PREFIX = '$LEGACY_/'

#
# Paths, filenames, etc.
#
CHAS_VPD_PATH = '/etc'
CHAS_SKU_FILENAME = 'isilon_system_config'
CHAS_SERIAL_FILENAME = 'isilon_serial_number'
FCB_VPD_TOOL_PATH = '/usr/bin/isi_hwtools/isi_fcb_vpd_tool'
RADISH_TOOL_PATH = '/usr/bin/isi_radish'
IFCONFIG_TOOL_PATH = '/sbin/ifconfig'
FSCK_TOOL_PATH = '/sbin/fsck'
MOUNT_TOOL_PATH = '/sbin/mount'
UMOUNT_TOOL_PATH = '/sbin/umount'
DONGLE_PART_PATH = '/dev/ad0s1e'
DONGLE_MOUNT_POINT = '/mnt'
INVENTORY_TOOL_PATH = '/usr/bin/isi_inventory_tool'

#
# SKUs we support (sans TAB level)
#
SKU_IQNL_3QTR               = '812-0001'    # IQNL series 3/4 chassis
SKU_IQX_3QTR                = '812-0002'    # IQX series 3/4 chassis
SKU_IQ108NL_3QTR            = '812-0003'    # IQ108NL series 3/4 chassis
SKU_IQ108000X_3QTR          = '812-0012'    # IQ108000X series 3/4 chassis

SKU_IQ36NL                  = '851-0073'    # IQ36NL
SKU_IQ36NL_10GIGE_CX4       = '851-0106'    # IQ36NL w/ 10GigE CX4
SKU_IQ36NL_10GIGE_SFP       = '851-0107'    # IQ36NL w/ 10GigE SFP+
SKU_IQ72NL                  = '851-0087'    # IQ72NL
SKU_IQ72NL_10GIGE_CX4       = '851-0108'    # IQ72NL w/ 10GigE CX4
SKU_IQ72NL_10GIGE_SFP       = '851-0109'    # IQ72NL w/ 10GigE SFP+
SKU_IQ108NL_GIGE            = '851-0133'    # IQ108NL w/ GigE
SKU_IQ108NL_10GIGE_CX4      = '851-0132'    # IQ108NL w/ 10GigE CX4
SKU_IQ108NL_10GIGE_SFP      = '851-0131'    # IQ108NL w/ 10GigE SFP+
SKU_IQ32000X_SSD_GIGE       = '851-0103'    # IQ32000X_SSD w/ GigE
SKU_IQ32000X_SSD_10GIGE_CX4 = '851-0104'    # IQ32000X_SSD w/ 10GigE CX4
SKU_IQ32000X_SSD_10GIGE_SFP = '851-0105'    # IQ32000X_SSD w/ 10GigE SFP+
SKU_IQ36000X_GIGE           = '851-0075'    # IQ36000X w/ GigE
SKU_IQ36000X_10GIGE_CX4     = '851-0090'    # IQ36000X w/ 10GigE CX4
SKU_IQ36000X_10GIGE_SFP     = '851-0091'    # IQ36000X w/ 10GigE SFP+
SKU_IQ72000X_GIGE           = '851-0088'    # IQ72000X w/ GigE
SKU_IQ72000X_10GIGE_CX4     = '851-0092'    # IQ72000X w/ 10GigE CX4
SKU_IQ72000X_10GIGE_SFP     = '851-0093'    # IQ72000X w/ 10GigE SFP+
SKU_IQ108000X_GIGE          = '851-0185'    # IQ108000X w/ GigE
SKU_IQ108000X_10GIGE_CX4    = '851-0184'    # IQ108000X w/ 10GigE CX4
SKU_IQ108000X_10GIGE_SFP    = '851-0183'    # IQ108000X w/ 10GigE SFP+

#
# Valid pairings of 3/4 SKUs and product SKUs for the 4U chassis
# with their respective product families
#
sku_product_family_translations = [
    # IQNL family
    { 
        'product_family'   : 'IQNL',
        'sku_pairings'     : { 'skus_3qtr'    : [ SKU_IQNL_3QTR, 
                                                  SKU_IQ108NL_3QTR ],
                               'skus_product' : [ SKU_IQ36NL, 
                                                  SKU_IQ36NL_10GIGE_CX4,
                                                  SKU_IQ36NL_10GIGE_SFP,
                                                  SKU_IQ72NL,
                                                  SKU_IQ72NL_10GIGE_CX4,
                                                  SKU_IQ72NL_10GIGE_SFP,
                                                  SKU_IQ108NL_GIGE,
                                                  SKU_IQ108NL_10GIGE_CX4,
                                                  SKU_IQ108NL_10GIGE_SFP ] },
    },

    # IQX family
    { 
        'product_family'   : 'IQX',
        'sku_pairings'     : { 'skus_3qtr'    : [ SKU_IQX_3QTR,
                                                  SKU_IQ108000X_3QTR ],
                               'skus_product' : [ SKU_IQ32000X_SSD_GIGE,
                                                  SKU_IQ32000X_SSD_10GIGE_CX4,
                                                  SKU_IQ32000X_SSD_10GIGE_SFP,
                                                  SKU_IQ36000X_GIGE, 
                                                  SKU_IQ36000X_10GIGE_CX4, 
                                                  SKU_IQ36000X_10GIGE_SFP, 
                                                  SKU_IQ72000X_GIGE,
                                                  SKU_IQ72000X_10GIGE_CX4, 
                                                  SKU_IQ72000X_10GIGE_SFP,
                                                  SKU_IQ108000X_GIGE,
                                                  SKU_IQ108000X_10GIGE_CX4,
                                                  SKU_IQ108000X_10GIGE_SFP ] },
    },
    ]

#
# Mapping of drive capacity, product family and network interface
# and SSD configuration to product SKU
#
sku_capacity_translations = [
    # Nodes with 1 TB drives
    {
        'drive_sizes'   : { 'blocks'                : BLOCKS_1TB },
        'product_skus'  : { 'IQNL_WO_NIC'           : SKU_IQ36NL,
                            'IQNL_W_10GIGE_CX4'     : SKU_IQ36NL_10GIGE_CX4,
                            'IQNL_W_10GIGE_SFP'     : SKU_IQ36NL_10GIGE_SFP,
                            'IQX_W_GIGE'            : SKU_IQ36000X_GIGE,
                            'IQX_W_10GIGE_CX4'      : SKU_IQ36000X_10GIGE_CX4,
                            'IQX_W_10GIGE_SFP'      : SKU_IQ36000X_10GIGE_SFP,
                            'IQX_W_SSD_GIGE'        : SKU_IQ32000X_SSD_GIGE,
                            'IQX_W_SSD_10GIGE_CX4'  : SKU_IQ32000X_SSD_10GIGE_CX4,
                            'IQX_W_SSD_10GIGE_SFP'  : SKU_IQ32000X_SSD_10GIGE_SFP,
                          }, 
    },

    # Nodes with 2 TB drives
    {
        'drive_sizes'   : { 'blocks'                : BLOCKS_2TB },
        'product_skus'  : { 'IQNL_WO_NIC'           : SKU_IQ72NL,
                            'IQNL_W_10GIGE_CX4'     : SKU_IQ72NL_10GIGE_CX4,
                            'IQNL_W_10GIGE_SFP'     : SKU_IQ72NL_10GIGE_SFP,
                            'IQX_W_GIGE'            : SKU_IQ72000X_GIGE,
                            'IQX_W_10GIGE_CX4'      : SKU_IQ72000X_10GIGE_CX4,
                            'IQX_W_10GIGE_SFP'      : SKU_IQ72000X_10GIGE_SFP,
                          }, 
    },

    # Nodes with 3 TB drives
    {
        'drive_sizes'   : { 'blocks'                : BLOCKS_3TB },
        'product_skus'  : { 'IQNL_W_GIGE'           : SKU_IQ108NL_GIGE,
                            'IQNL_W_10GIGE_CX4'     : SKU_IQ108NL_10GIGE_CX4,
                            'IQNL_W_10GIGE_SFP'     : SKU_IQ108NL_10GIGE_SFP,
                            'IQX_W_GIGE'            : SKU_IQ108000X_GIGE,
                            'IQX_W_10GIGE_CX4'      : SKU_IQ108000X_10GIGE_CX4,
                            'IQX_W_10GIGE_SFP'      : SKU_IQ108000X_10GIGE_SFP,
                          }, 
    },
    ]

#
# The twenty (20) X200 and S200 SKUs that were defined to be
# able to ship Wingfoot and Blue Flame nodes with 6.5.1.
#
X200_SKUS = [ '851-0112', '851-0113', '851-0114', '851-0115', '851-0116',
              '851-0117', '851-0118', '851-0119', '851-0120', '851-0121',
              '851-0122', '851-0123', '851-0124' ]

S200_SKUS = [ '851-0125', '851-0126', '851-0127', '851-0128', '851-0129',
              '851-0130', '851-0172' ]

#
# The two (2) base chassis SKUs for Belknap based Wingfoot and Blue Flame
#
# NOTE: We never shipped any RocketFuel based WF/BF with 6.5.1, so we only
#       have to deal with these two (2) SKUs.
#
X200_BASE_SKU = '400-0034'
S200_BASE_SKU = '400-0033'

#
# Initialize logging
#
def init_logging():

    openlog('isi_transform_tool', 0, LOG_USER)
    return
    
#
# Post message to stdout and /var/log/messages with the specified priority.
#
def logit(priority=None, msg=None):

    # Map standard syslog priorities into message tags used on stdout
    tag = {
        LOG_EMERG   : 'EMERGENCY',
        LOG_ALERT   : 'ALERT',
        LOG_CRIT    : 'CRITICAL',
        LOG_ERR     : 'ERROR',
        LOG_WARNING : 'WARNING',
        LOG_NOTICE  : 'NOTICE',
        LOG_INFO    : 'INFO',
        LOG_DEBUG   : 'DEBUG',
    } [priority]

    # Post message to stdout with tag
    if tag == 'INFO':
        print '%s: %s' % (tag, msg)
        # LOG_INFO doesn't make it to /var/log/messages up its priority
        priority = LOG_NOTICE
    elif tag != 'DEBUG':
        print '%s: %s' % (tag, msg)
    else:
        if _g_debug_flag is True:
            print '%s: %s' % (tag, msg)
    # Change msg from a list of strings to a string if necessary
    # for syslog call which expects to receive a string.  
    # I.e. the get_cmd_output call for isi_radish returns a list of 
    # strings so the disk's number of blocks can be parsed.  However when
    # isi_radish fails we need to convert the list to a string here.
    msgout = ''
    for line in msg:
        msgout += line

    # Post message to /var/log/messages
    syslog(priority, msgout)

    return

#
# Report this message only if the global debug flag is true
#
def DEBUG(msg=None):

    if _g_debug_flag is True:
        logit(LOG_DEBUG, msg)

    return

#
# Process an error from a call to "procs.get_cmd_output()"
#
def cmd_error(cmd=None, output=None):

    logit(LOG_ERR, 'COMMAND ERROR: "%s" failed' % cmd)

    # If there was output with the failure, display it as well
    if len(output) > 0:
        logit(LOG_ERR, 'OUTPUT FROM COMMAND:')
        logit(LOG_ERR, output)
    
    return

#
# Does this chassis type support transformation?
#
def chassis_supports_transformation():
    import isi.hw.hal as hal
    import isi.hw.version as hwver

    chas_type = hal.getChassisType(0)
    chas_name = hwver.chassisName(chas_type)
    product_name = hal.getChassisMarketingName(0)
    chas_name_mauna_loa = hwver.chassisName(hwver.CHASSIS_ISI36BAY)

    if chas_type != hwver.CHASSIS_ISI36BAY:
        logit(LOG_INFO, 'Chassis is a %s (%s), SKU transformation only '
                'supported on %s' %
                (chas_name, product_name, chas_name_mauna_loa))

        return 0
    
    return 1

#
# Return capacity of the specified drive bay in blocks
#
# bay is a string of the form 'bayX' (i.e. 'bay1', 'bay27', etc.)
#
def get_drive_capacity_in_blocks(bay=None):
    import isi.app.lib.procs as procs
    
    if bay is None:
        DEBUG('ARGUMENT ERROR: Drive bay not specified')
        raise Exception
    
    cmd = '%s -q %s' % (RADISH_TOOL_PATH, bay)
    (error, output) = procs.get_cmd_output(cmd)
    if error:
        cmd_error(cmd, output)
        return None

    p = re.compile('^.*, (?P<blocks>\d{1,12})')

    blocks = None
    for line in output:
        m = p.match(line)
        if m:
            blocks = m.group('blocks')
            if blocks is not None:
                break

    if blocks is None:
        return blocks
    return int(blocks)

#
# Return 10GigE media type
#
def get_10gige_media_type(method=None):
    import isi.app.lib.procs as procs

    if method is None:
        DEBUG('ARGUMENT ERROR: method not specified')
        raise Exception
    
    if method != 'ifconfig' and method != 'sysctl':
        DEBUG('ARGUMENT ERROR: %s is not a valid method' % method)
        raise Exception

    if method == 'ifconfig':
        cmd = '%s %s' % (IFCONFIG_TOOL_PATH, 'cxgb0')
        p = re.compile('^.*media: Ethernet 10Gbase-(?P<media>[^ <>]+)')
    elif method == 'sysctl':
        cmd = 'sysctl dev.cxgb.0.%desc'
        p = re.compile('^.*10GBASE-(?P<media>\w+)')

    (error, output) = procs.get_cmd_output(cmd)
    if error:
        cmd_error(cmd, output)
        return None

    media = None
    for line in output:
        m = p.match(line)
        if m:
            media = m.group('media')
            if media is not None:
                break

    return media

#
# Return network interface configuration 
#
def get_network_interface_configuration():
    
    cmd = '%s -l | grep %s > /dev/null' % (IFCONFIG_TOOL_PATH, 'em2')
    has_gige_nic = (os.system(cmd) == 0)
    cmd = '%s -l | grep %s > /dev/null' % (IFCONFIG_TOOL_PATH, 'cxgb0')
    has_10gige_nic = (os.system(cmd) == 0)

    if has_gige_nic:
        logit(LOG_INFO, 'Chassis has GigE add-on NIC')
        return 'GigE'
    elif has_10gige_nic:
        logit(LOG_INFO, 'Chassis has 10GigE add-on NIC')
        #
        # Bugzilla 60975 - Use 'sysctl' method to determine media
        #                  type to handle those cases when the 
        #                  GBIC modules are not installed at time
        #                  of transformation.
        #
        media = get_10gige_media_type('sysctl')
        logit(LOG_INFO, '10GigE NIC media type is "%s"' % media)
        if media is None:
            logit(LOG_ERR, 'Unable to determine 10GigE media type')
            return None
        elif media == 'CX4':
            return '10GigE CX4'
        #
        # Bugzilla 60975 - The 'sysctl' method will return 'R' for SFP+
        #                  and the 'ifconfig' method will return 'SR' or
        #                  'Twinax' for SFP+.
        #
        elif media == 'R' or media == 'SR' or media == 'Twinax':
            return '10GigE SFP'
        else:
            logit(LOG_ERR, 'Unrecognized 10GigE media type "%s"' % media)
            return None
    else:
        logit(LOG_INFO, 'Chassis does not have an add-on NIC')
        return None

#
# Is the SKU prefix (1st 3 digits) valid?
#
# sku is a string of the form 'XXX-XXXX' (i.e. '812-0001', '851-0073', etc.)
#
def sku_prefix_is_valid(sku=None):

    if sku is None:
        logit(LOG_ERR, 'ARGUMENT ERROR: SKU not specified')
        raise Exception

    if ((sku.find(VALID_3QTR_SKU_PREFIX) < 0) and 
        (sku.find(VALID_PRODUCT_SKU_PREFIX) < 0)):

        logit(LOG_ERR, 
                'ERROR: SKU "%s" does not have a valid prefix ("%s" or "%s")' %
                (sku, VALID_3QTR_SKU_PREFIX, VALID_PRODUCT_SKU_PREFIX))
        return False

    return True

#
# Is the SKU for a 3/4 chassis?
#
# sku is a string of the form 'XXX-XXXX' (i.e. '812-0001', '851-0073', etc.)
#
def sku_is_for_3qtr(sku=None):

    if sku is None:
        logit(LOG_ERR, 'ARGUMENT ERROR: SKU not specified')
        raise Exception

    if (sku.find(VALID_3QTR_SKU_PREFIX) < 0):
        return False

    return True

#
# Is the SKU for a product?
#
# sku is a string of the form 'XXX-XXXX' (i.e. '812-0001', '851-0073', etc.)
#
def sku_is_for_product(sku=None):

    if sku is None:
        logit(LOG_ERR, 'ARGUMENT ERROR: SKU not specified')
        raise Exception

    if (sku.find(VALID_PRODUCT_SKU_PREFIX) < 0):
        return False

    return True

#
# Is the SKU for an IQNL family product?
#
# sku is a string of the form 'XXX-XXXX' (i.e. '812-0001', '851-0073', etc.)
#
def sku_is_for_IQNL(sku=None):

    if sku is None:
        logit(LOG_ERR, 'ARGUMENT ERROR: SKU not specified')
        raise Exception

    if not sku_prefix_is_valid(sku):
        return False

    if not sku_is_for_product_family(sku, 'IQNL'):
        return False

    return True

#
# Is the SKU for an IQ family productX?
#
# sku is a string of the form 'XXX-XXXX' (i.e. '812-0001', '851-0073', etc.)
#
def sku_is_for_IQX(sku=None):

    if sku is None:
        logit(LOG_ERR, 'ARGUMENT ERROR: SKU not specified')
        raise Exception

    if not sku_prefix_is_valid(sku):
        return False

    if not sku_is_for_product_family(sku, 'IQX'):
        return False

    return True

#
# Is the specified SKU valid for the specified product family?
#
# sku is a string of the form 'XXX-XXXX' (i.e. '812-0001', '851-0073', etc.)
# product_family can be 'IQNL' or 'IQX'
#
def sku_is_for_product_family(sku=None, product_family=None):

    if sku is None:
        logit(LOG_ERR, 'ARGUMENT ERROR: SKU not specified')
        raise Exception

    if product_family != 'IQNL' and product_family != 'IQX':
        logit(LOG_ERR, 'ARGUMENT ERROR: "%s" is not a valid product family '
                '("%s" or "%s")' % (product_family, 'IQNL', 'IQX'))
        raise Exception

    if not sku_prefix_is_valid(sku):
        return False

    for sku_translation in sku_product_family_translations:
        if product_family is not sku_translation['product_family']:
            continue

        sku_pairings = sku_translation['sku_pairings']
        skus_3qtr = sku_pairings['skus_3qtr']
        skus_product = sku_pairings['skus_product']

        for my_sku in skus_3qtr:
            if sku.find(my_sku) >= 0:
                return True

        for my_sku in skus_product:
            if sku.find(my_sku) >= 0:
                return True

    return False

#
# Are the specified 3/4 chassis and product SKUs compatible?
#
# sku_3qtr and sku_product are strings of the form 'XXX-XXXX' 
# (i.e. '812-0001', '851-0073', etc.)
#
def skus_are_compatible(sku_3qtr=None, sku_product=None):

    if sku_3qtr is None or sku_product is None:
        logit(LOG_ERR, 'ARGUMENT ERROR: SKU not specified')
        raise Exception

    if not sku_prefix_is_valid(sku_3qtr):
        logit(LOG_ERR, 'ERROR: 3/4 SKU is not valid')
        return False

    if not sku_prefix_is_valid(sku_product):
        logit(LOG_ERR, 'ERROR: Product SKU is not valid')
        return False

    if not sku_is_for_3qtr(sku_3qtr):
        logit(LOG_ERR, '"%s" is not a 3/4 SKU' % sku_3qtr)
        return False

    if not sku_is_for_product(sku_product):
        logit(LOG_ERR, '"%s" is not a product SKU' % sku_product)
        return False

    if sku_is_for_IQNL(sku_3qtr) and not sku_is_for_IQNL(sku_product):
        return False

    if sku_is_for_IQX(sku_3qtr) and not sku_is_for_IQX(sku_product):
        return False

    return True

#
# Translate the specified SSD presence, drive capacity, network interface 
# configuration and product family into the correct product SKU. The network 
# interface configuration really refers to the type of "add-on" NIC that is 
# installed in the chassis. For NL series, this would be None, but for X 
# series, it could either be GigE or 10GigE.
#
# has_ssd (boolean) specifies whether the chassis contains any SSD drives
# capacity is the number of blocks in decimal (i.e. BLOCKS_1TB or 1953525168)
# nic can be None, 'GigE' or '10GigE'
# product_family can be 'IQNL' or 'IQX'
#
def translate_to_product_sku(has_ssd=None, capacity=None, nic=None, \
                             product_family=None):

    if has_ssd is None:
        logit(LOG_ERR, 'ARGUMENT ERROR: SSD presence not specified')
        raise Exception

    if capacity is None:
        logit(LOG_ERR, 'ARGUMENT ERROR: Capacity not specified')
        raise Exception

    if product_family is None:
        logit(LOG_ERR, 'ARGUMENT ERROR: Product family not specified')
        raise Exception

    for sku_capacity in sku_capacity_translations:
        drive_sizes = sku_capacity['drive_sizes']
        blocks = drive_sizes['blocks']

        sku = None
        if capacity == blocks:
            product_skus = sku_capacity['product_skus']
            if product_family == 'IQNL':
                if nic is None:
                    sku = product_skus['IQNL_WO_NIC']
                elif nic == 'GigE':
                    sku = product_skus['IQNL_W_GIGE']
                elif nic == '10GigE CX4':
                    sku = product_skus['IQNL_W_10GIGE_CX4']
                elif nic == '10GigE SFP':
                    sku = product_skus['IQNL_W_10GIGE_SFP']
                break
            elif product_family == 'IQX':
                if nic is None:
                    logit(LOG_ERR, 'ERROR: 3/4 IQX should have an add-on NIC')
                elif nic == 'GigE':
                    if has_ssd:
                        sku = product_skus['IQX_W_SSD_GIGE']
                    else:
                        sku = product_skus['IQX_W_GIGE']
                elif nic == '10GigE CX4':
                    if has_ssd:
                        sku = product_skus['IQX_W_SSD_10GIGE_CX4']
                    else:
                        sku = product_skus['IQX_W_10GIGE_CX4']
                elif nic == '10GigE SFP':
                    if has_ssd:
                        sku = product_skus['IQX_W_SSD_10GIGE_SFP']
                    else:
                        sku = product_skus['IQX_W_10GIGE_SFP']
                break
            
    return sku

#
# Is the /etc partition on the PATA dongle mounted?
#
def dongle_is_mounted():
    import isi.app.lib.procs as procs

    (error, output) = procs.get_cmd_output('%s | grep %s' % \
        (MOUNT_TOOL_PATH, DONGLE_PART_PATH))
    if error or len(output) == 0:
        # PATA dongle does not appear to be mounted
        mounted = False
    else:
        mounted = True
    return mounted

#
# Mount the /etc partition on the PATA dongle.
#
def mount_dongle():
    import isi.app.lib.procs as procs

    cmd = '%s %s' % (FSCK_TOOL_PATH, DONGLE_PART_PATH)
    (error, output) = procs.get_cmd_output(cmd)
    if error:
        cmd_error(cmd, output)
        return None

    cmd = '%s %s %s' % (MOUNT_TOOL_PATH, DONGLE_PART_PATH, DONGLE_MOUNT_POINT)
    (error, output) = procs.get_cmd_output(cmd)
    if error:
        cmd_error(cmd, output)
        return None

    return DONGLE_MOUNT_POINT

#
# Unmount /etc parition on the PATA dongle
#
def umount_dongle():
    import isi.app.lib.procs as procs

    cmd = '%s %s' % (UMOUNT_TOOL_PATH, DONGLE_MOUNT_POINT)
    (error, output) = procs.get_cmd_output(cmd)
    if error:
        cmd_error(cmd, output)
        return 1

    return 0
                    
#
# Return the specified chassis VPD from the specified location.
#
# type can be 'sku' or 'serial'
# location can be 'disk', 'eeprom' or 'dongle'.
#
def get_chassis_vpd(vpd_type=None, location=None):
    import isi.hw.dongle as hwdongle
    import isi.hw.version as hwver
    import isi.app.lib.procs as procs

    if vpd_type != 'sku' and vpd_type != 'serial':
        logit(LOG_ERR, 'ARGUMENT ERROR: "%s" is not a valid VPD type '
               '("%s" or "%s")' % (vpd_type, 'sku', 'serial'))
        raise Exception

    if location != 'disk' and location != 'eeprom' and location != 'dongle':
        logit(LOG_ERR, 'ARGUMENT ERROR: "%s" is not a valid SKU location '
               '("%s", "%s" or "%s")' % (location, 'disk', 'eeprom', 'dongle'))
        raise Exception
    
    if location == 'eeprom':
        if not hwdongle.eeprom_supported():
            logit(LOG_INFO, 'EEPROM is not supported on this chassis')
            return None
        if not hwdongle.eeprom_present():
            logit(LOG_INFO, 'EEPROM is not present on this chassis')
            return None
    elif location == 'dongle':
        if not hwdongle.dongle_supported():
            logit(LOG_INFO, 'PATA dongle is not supported on this chassis')
            return None
        if not hwdongle.dongle_present():
            logit(LOG_INFO, 'PATA dongle is not present on this chassis')
            return None

    if vpd_type == 'sku':
        vpd_type_str = 'SKU'
    elif vpd_type == 'serial':
        vpd_type_str = 'serial number'

    if location == 'disk':
        location_str = 'on disks'
    elif location == 'eeprom':
        location_str = 'in EEPROM'
    elif location == 'dongle':
        location_str = 'on PATA dongle'

    if vpd_type == 'sku':
        if location == 'disk':
            filename = (CHAS_VPD_PATH + '/' + CHAS_SKU_FILENAME)
        elif location == 'dongle':
            filename = (DONGLE_MOUNT_POINT + \
                        (CHAS_VPD_PATH + '/' + CHAS_SKU_FILENAME))
        elif location == 'eeprom':
            filename = None
            eeprom_grep_string = CHAS_SKU_FILENAME
    elif vpd_type == 'serial':
        if location == 'disk':
            filename = (CHAS_VPD_PATH + '/' + CHAS_SERIAL_FILENAME)
        elif location == 'dongle':
            filename = (DONGLE_MOUNT_POINT + \
                        (CHAS_VPD_PATH + '/' + CHAS_SERIAL_FILENAME))
        elif location == 'eeprom':
            filename = None
            eeprom_grep_string = CHAS_SERIAL_FILENAME

    if location == 'dongle':
        dongle = mount_dongle()
        if dongle is None:
            logit(LOG_INFO, 'Unable to mount PATA dongle partition')
            return None

    # See if the desired file even exists
    no_file = False
    if location == 'disk':
        if vpd_type == 'sku' and not disks_have_chassis_sku():
            logit(LOG_ERR, 'Disks do not contain a chassis SKU')
            no_file = True
        elif vpd_type == 'serial' and not disks_have_chassis_serial():
            logit(LOG_ERR, 'Disks do not contain a chassis serial number')
            no_file = True
    elif location == 'dongle':
        if vpd_type == 'sku' and not dongle_has_chassis_sku():
            logit(LOG_INFO, 'PATA dongle does not contain a chassis SKU')
            no_file = True
        elif vpd_type == 'serial' and not dongle_has_chassis_serial():
            logit(LOG_INFO, 'PATA dongle does not contain a chassis serial'
                    'number')
            no_file = True

    # If the file we're after doesn't exist, we're done
    if no_file:
        if location == 'dongle':
            if umount_dongle() != 0:
                logit(LOG_INFO, 'Unable to umount PATA dongle partition')
        return None

    if location == 'disk' or location == 'dongle':
        cmd = 'cat %s' % filename
    elif location == 'eeprom':
        cmd = '%s dump | grep %s' % (FCB_VPD_TOOL_PATH, eeprom_grep_string)
        
    (error, output) = procs.get_cmd_output(cmd)
    if error:
        cmd_error(cmd, output)
        # If dongle, we have to clean up before we leave
        if location == 'dongle':
            if umount_dongle() != 0:
                logit(LOG_INFO, 'Unable to umount PATA dongle partition')
        return None

    if len(output) == 0:
        DEBUG('"%s" resulted in no %s' % (cmd, vpd_type_str))
        # If dongle, we have to clean up before we leave
        if location == 'dongle':
            if umount_dongle() != 0:
                logit(LOG_INFO, 'Unable to umount PATA dongle partition')
        return None

    # Done with dongle filesystem now, clean up
    if location == 'dongle':
        if umount_dongle() != 0:
            logit(LOG_INFO, 'Unable to umount PATA dongle partition')
            return None

    if vpd_type == 'sku':
        match_key = 'sku'
        if location == 'disk' or location == 'dongle':
            p = re.compile('^(?P<sku>[0-9]{3}-[0-9]{4}-[0-9]{2}$)')
        elif location == 'eeprom':
            p = re.compile('^.*\[\'(?P<sku>[0-9]{3}-[0-9]{4}-[0-9]{2})')
    elif vpd_type == 'serial':
        match_key = 'serial'
        #
        # Serial number formats vary by platform
        #
        # Node Type             Prefix   Serial Number Format
        # ---------             ------   --------------------
        # Rainier                N/A         YYWWXXXXM
        # Ullr/Fuego Accelerator "A"        AYYWWXXXXM
        # Annapurna Accelerator  "A"       AYYWWXXXXXM
        # Graham                 "G"       GYYWWXXXXXM
        # Mauna Loa              "M"       MYYWWXXXXXM
        # Racer-X                "R"       RYYWWXXXXXM
        #
        # Note: This code does not deal with EX chassis VPD so there
        #       is no regular expressions that handle the 'D' prefix.
        #
        if location == 'disk' or location == 'dongle':
            if hwver.hwgen == hwver.HWGEN_RAINIER:
                p = re.compile('^(?P<serial>(0[3-9])([0-9]{2})' \
                               '([0-9]{4})[SsLlJj]$)')
            elif hwver.hwgen == hwver.HWGEN_ULLR:
                p = re.compile('^(?P<serial>[AKk](0[3-9])([0-9]{2})' \
                               '([0-9]{4})[SsLlJj]$)')
            else:
                p = re.compile('^(?P<serial>[AGgMmRr](0[3-9])([0-9]{2})' \
                               '([0-9]{5})[SsLlJj]$)')
        elif location == 'eeprom':
            # Only Annapurna, Graham, Mauna Loa and Racer-X have
            # EEPROM devices
            p = re.compile('^.*\[\'(?P<serial>[AGgMmRr](0[3-9])' \
                           '([0-9]{2})([0-9]{5})[SsLlJj])')

    vpd = None
    for line in output:
        m = p.match(line)
        if m:
            vpd = m.group(match_key)
            if vpd is not None:
                break

    if vpd is None:
        logit(LOG_INFO, 'Unable to locate chassis %s %s' % 
                (vpd_type_str, location_str))
        return None

    return vpd

#
# Return (extract) the part number from specified SKU
#
# sku is a string of the form 'XXX-XXXX' (i.e. '812-0001', '851-0073', etc.)
#
def extract_part_number_from_sku(sku=None):

    if sku is None:
        logit(LOG_ERR, 'ARGUMENT ERROR: SKU not specified')
        raise Exception

    p = re.compile('^(?P<part_number>[0-9]{3}-[0-9]{4})')
    m = p.match(sku)
    if m:
        part_number = m.group('part_number')

    if part_number is None:
        logit(LOG_ERR, 'Unable to locate part number in "%s"' % sku)
        return None

    return part_number

#
# Return (extract) the 'TAB' (revision) level from specified SKU
#
# sku is a string of the form 'XXX-XXXX' (i.e. '812-0001', '851-0073', etc.)
#
def extract_tab_level_from_sku(sku=None):

    if sku is None:
        logit(LOG_ERR, 'ARGUMENT ERROR: SKU not specified')
        raise Exception

    if not sku_prefix_is_valid(sku):
        return None

    p = re.compile('^[0-9]{3}-[0-9]{4}-(?P<tab>[0-9]{2})')
    m = p.match(sku)
    if m:
        tab = m.group('tab')

    if tab is None:
        logit(LOG_ERR, 'Unable to locate TAB level in "%s"' % sku)
        return None

    return tab
    
#
# Write the specified chassis SKU to the specified location.
#
# sku is a string of the form 'XXX-XXXX' (i.e. '812-0001', '851-0073', etc.)
# location can be 'disk' or 'eeprom'.
#
def write_chassis_sku(sku=None, location=None):
    import isi.app.lib.procs as procs

    if _g_dry_run_flag is True:
        print 'Dry run - bypass writing chassis SKU'
        return 0

    if sku is None:
        logit(LOG_ERR, 'ARGUMENT ERROR: SKU not specified')
        raise Exception

    if location != 'disk' and location != 'eeprom':
        logit(LOG_ERR, 'ARGUMENT ERROR: "%s" is not a valid SKU location '
               '("%s" or "%s")' % (location, 'disk', 'eeprom'))
        raise Exception

    if location == 'disk':
        cmd = 'echo %s > %s' % (sku, (CHAS_VPD_PATH + '/' + CHAS_SKU_FILENAME))
    elif location == 'eeprom':
        eeprom_key = (CHAS_VPD_EEPROM_KEY_PREFIX + \
                        (CHAS_VPD_PATH + '/' + CHAS_SKU_FILENAME))
        cmd = '%s write %s %s' % (FCB_VPD_TOOL_PATH, eeprom_key, sku)
        
    (error, output) = procs.get_cmd_output(cmd)
    if error:
        cmd_error(cmd, output)
        return 1
    
    return 0

#
# Push VPD from disks to chassis (eeprom/PATA dongle). 
#
def push_vpd_from_disk_to_chassis():
    import isi.hw.check.lib.filenames as files
    import isi.app.lib.procs as procs

    if _g_dry_run_flag is True:
        print 'Dry run - bypass pushing VPD from disk to chassis'
        return 0

    files.init()
    cmd = '%s -p' % files.commands['dongle_sync']
    (error, output) = procs.get_cmd_output(cmd)
    if error:
        cmd_error(cmd, output)
        return 1
    
    return 0
    
#
# Sync the VPD from the chassis (eeprom/PATA dongle) to the disks.
#
def sync_vpd_from_chassis_to_disk():
    import isi.hw.check.lib.filenames as files
    import isi.app.lib.procs as procs

    if _g_dry_run_flag is True:
        print 'Dry run - bypass pulling VPD from chassis to disk'
        return 0

    files.init()
    cmd = '%s' % files.commands['dongle_sync']
    (error, output) = procs.get_cmd_output(cmd)
    if error:
        cmd_error(cmd, output)
        return 1
    
    return 0
    
#
# Is this chassis a 3/4 chassis?
#
def chassis_is_3qtr():
    import isi.hw.hal as hal
    import isi.hw.version as hwver

    eeprom_sku = get_chassis_vpd('sku', 'eeprom')
    if eeprom_sku is None:
        logit(LOG_ERR, 'Unable to locate chassis SKU in EEPROM')
        return False

    if hal.getNvramType() != hwver.NVR_MT25208:
        logit(LOG_ERR, 'Unable to locate NVRAM (MT25208 based) card')
        return False

    logit(LOG_INFO, 'Chassis SKU read from EEPROM is "%s"' % eeprom_sku)

    if not sku_prefix_is_valid(eeprom_sku):
        return False

    if not sku_is_for_3qtr(eeprom_sku):
        logit(LOG_ERR, 'Chassis SKU read from EEPROM is not a 3/4 SKU')
        return False

    return True

#
# Do the disks have a chassis SKU?
#
def disks_have_chassis_sku():

    if os.path.exists((CHAS_VPD_PATH + '/' + CHAS_SKU_FILENAME)):
        return True

    return False

#
# Do the disks have a chassis serial number?
#
def disks_have_chassis_serial():

    if os.path.exists((CHAS_VPD_PATH + '/' + CHAS_SERIAL_FILENAME)):
        return True

    return False

#
# Does the PATA dongle have a chassis SKU?
#
def dongle_has_chassis_sku():

    if not dongle_is_mounted():
        DEBUG('PATA dongle filesystem is not mounted')
        raise Exception

    if os.path.exists((DONGLE_MOUNT_POINT + \
                        (CHAS_VPD_PATH + '/' + CHAS_SKU_FILENAME))):
        return True

    return False

#
# Does the PATA dongle have a chassis serial number?
#
def dongle_has_chassis_serial():

    if not dongle_is_mounted():
        DEBUG('PATA dongle filesystem is not mounted')
        raise Exception

    if os.path.exists((DONGLE_MOUNT_POINT + \
                        (CHAS_VPD_PATH + '/' + CHAS_SERIAL_FILENAME))):
        return True

    return False

#
# Does the chassis contain SSD drives?
#
def chassis_has_ssd():
    import isi.hw.bay as hwbay

    has_ssd = False
    nbays = hwbay.getCounts(0)[0]
    for bay in range (1, nbays):
        if hwbay.isSsd(bay):
            has_ssd = True
            break

    if has_ssd:
        logit(LOG_INFO, 'SSD drive found in bay %d' % bay)
    else:
        logit(LOG_INFO, 'No SSD drives found in chassis')
        
    return has_ssd

#
# Are these disks configured with a product SKU?
#
def disks_are_configured_with_product_sku():

    hdd_sku = get_chassis_vpd('sku', 'disk')
    if hdd_sku is None:
        logit(LOG_ERR, 'Unable to locate chassis SKU on disks')
        return False

    logit(LOG_INFO, 'Chassis SKU read from disks is "%s"' % hdd_sku)

    if not sku_prefix_is_valid(hdd_sku):
        return False

    if not sku_is_for_product(hdd_sku):
        logit(LOG_INFO, 'Chassis SKU read from disks is not a product SKU')
        return False

    return True

#
# Generate product SKU from 3/4 chassis SKU based on drive capacity
#
def generate_product_sku():
    import isi.hw.bay as hwbay

    capacity = None

    # Locate the 1st non-SSD drive to determine drive capacity
    nbays = hwbay.getCounts(0)[0]
    for bay in range (1, nbays):
        if not hwbay.isSsd(bay):
            logit(LOG_INFO, 'Basing node capacity on HDD drive in bay %d' % bay)
            capacity = get_drive_capacity_in_blocks(str(bay))
            if capacity != None:
                break

    if capacity is None:
        logit(LOG_ERR, 'ERROR: Unable to determine drive capacity')
        return None
    elif capacity == BLOCKS_1TB:
        terabytes = '1'
    elif capacity == BLOCKS_2TB:
        terabytes = '2'
    elif capacity == BLOCKS_3TB:
        terabytes = '3'
    else:
        logit(LOG_ERR, 'ERROR: Drive capacity (%d blocks) is not valid' %
                capacity)
        return None

    logit(LOG_INFO, 'Drive capacity is %sTB' % terabytes)

    eeprom_sku = get_chassis_vpd('sku', 'eeprom')
    if eeprom_sku is None:
        logit(LOG_ERR, 'ERROR: Unable to locate chassis SKU in EEPROM')
        return None

    if sku_is_for_IQNL(eeprom_sku):
        family = 'IQNL'
    elif sku_is_for_IQX(eeprom_sku):
        family = 'IQX'

    nic = get_network_interface_configuration()
    if family == 'IQX' and nic is None:
        logit(LOG_ERR, 'A 3/4 chassis for IQX should always have an add-on NIC')
        logit(LOG_ERR, 'Please add the correct NIC and try again')
        return None

    has_ssd = chassis_has_ssd()

    sku = translate_to_product_sku(has_ssd, capacity, nic, family)
    if sku is None:
        if has_ssd:
            ssd_str = 'SSD'
        else:
            ssd_str = "no SSD"

        logit(LOG_ERR, 'ERROR: Unable to translate "%s" into product for '
                '%sTB drives with %s NIC and %s' % \
                (eeprom_sku, terabytes, nic, ssd_str))
        return None

    tab = extract_tab_level_from_sku(eeprom_sku)
    if tab is None:
        logit(LOG_ERR, 'ERROR: Unable to extract TAB level from "%s"' %
                eeprom_sku)
        return None

    logit(LOG_INFO, 'TAB level of chassis is "%s"' % tab)

    product_sku = sku + '-' + tab
    logit(LOG_INFO, 'Product SKU for %sTB drive capacity is "%s"' %
            (terabytes, product_sku))

    return product_sku

#
# Is chassis SKU in sync (disks match EEPROM)?
#
def chassis_sku_in_sync():

    logit(LOG_INFO, 'Checking if chassis SKU is in sync')
    eeprom_sku = get_chassis_vpd('sku', 'eeprom')
    if eeprom_sku is None:
        logit(LOG_ERR, 'ERROR: Unable to locate chassis SKU in EEPROM')
        return False

    hdd_sku = get_chassis_vpd('sku', 'disk')
    if hdd_sku is None:
        logit(LOG_ERR, 'ERROR: Unable to locate chassis SKU on disks')
        return False

    logit(LOG_INFO, 'Chassis SKU read from EEPROM is "%s"' % eeprom_sku)
    logit(LOG_INFO, 'Chassis SKU read from disks is "%s"' % hdd_sku)

    if hdd_sku != eeprom_sku:
        logit(LOG_ERR, 'ERROR: Chassis SKU on disk and in EEPROM do not match '
                '(disk has "%s", EEPROM has "%s")' % (hdd_sku, eeprom_sku))
        return False

    return True

#
# Check if chassis requires transformation. A chassis requires
# transformation when it is one of the support chassis types
# and is currently configured as a 3/4 chassis.
#
def needs_transformation():

    if not chassis_supports_transformation():
        logit(LOG_ERR, 'Chassis does not support transformation')
        rc = 0
    elif not chassis_is_3qtr():
        logit(LOG_ERR, 'Chassis is not a 3/4 chassis')
        rc = 0
    else:
        rc = 1
    
    if rc == 0:
        logit(LOG_NOTICE, 'Chassis does not appear to require SKU '
                'transformation')
    else:
        logit(LOG_NOTICE, 'Chassis requires SKU transformation')

    return rc

#
# Is /ifs mounted?
#
def ifs_is_mounted():
    
    if not os.path.ismount("/ifs"):
        logit(LOG_ERR, '/ifs is not mounted')
        return False

    return True

#
# Confirm that this is a 3/4 chassis and that it is compatible
# with the product configured on the disks that are installed.
# Also confirm that we can locate the expected NVRAM card in
# the system.
#
# Intended for use during disk tango operation.
#
def confirm():

    if not chassis_supports_transformation():
        logit(LOG_ERR, 'Chassis does not support transformation')
        return 1

    if not chassis_is_3qtr():
        logit(LOG_ERR, 'Chassis is not a 3/4 chassis')
        return 1

    if not disks_have_chassis_sku():
        logit(LOG_ERR, 'Disks have no chassis SKU')
        return 1

    if not disks_are_configured_with_product_sku():
        logit(LOG_ERR, 'Disks are not configured with a product SKU')
        return 1

    eeprom_sku = get_chassis_vpd('sku', 'eeprom')
    if eeprom_sku is None:
        logit(LOG_ERR, 'Unable to locate chassis SKU in EEPROM')
        return 1

    hdd_sku = get_chassis_vpd('sku', 'disk')
    if hdd_sku is None:
        logit(LOG_ERR, 'Unable to locate chassis SKU on disks')
        return 1

    if not skus_are_compatible(eeprom_sku, hdd_sku):
        logit(LOG_ERR, '"%s" is not compatible with "%s"' % 
            (eeprom_sku, hdd_sku))
        return 1

    return 0

#
# Convert this 3/4 chassis into the appropriate product by
# transforming the 3/4 chassis SKU into a product SKU and
# updating all chassis VPD appropriately, both in EEPROM and
# on disks. 
#
# Intended for use during initial installation.
#
def install():

    if not chassis_supports_transformation():
        logit(LOG_ERR, 'Chassis does not support transformation')
        return 1

    if not chassis_is_3qtr():
        logit(LOG_ERR, 'Chassis is not a 3/4 chassis')
        return 1

    if disks_have_chassis_sku() and disks_are_configured_with_product_sku():
        logit(LOG_ERR, 'Disks already configured with product SKU "%s"' %
            get_chassis_vpd('sku', 'disk'))
        logit(LOG_ERR, 'Does not appear that these disks are install disks!')
        return 1

    product_sku = generate_product_sku()
    if product_sku is None:
        logit(LOG_ERR, 'Unable to generate product SKU')
        return 1

    logit(LOG_INFO, 'Writing product SKU "%s" to disks' % product_sku)
    if write_chassis_sku(product_sku, 'disk') != 0:
        logit(LOG_ERR, 'Unable to write product SKU ("%s") to disks' %
            product_sku)
        return 1

    logit(LOG_INFO, 'Pushing VPD from disks into chassis')
    if push_vpd_from_disk_to_chassis() != 0:
        logit(LOG_ERR, 'Unable to push VPD from disks into chassis')
        return 1

    logit(LOG_INFO, 'Syncing VPD from chassis to disks')
    if sync_vpd_from_chassis_to_disk() != 0:
        logit(LOG_ERR, 'Unable to sync VPD from chassis to disks')
        return 1

    if not chassis_sku_in_sync():
        logit(LOG_ERR, 'Chassis SKU not in sync between disks and EEPROM')
        return 1

    logit(LOG_NOTICE, 'Initial install SKU (%s) transformation completed '
                      'successfully' % product_sku)
    return 0

#
# Convert this 3/4 chassis into product that matches what's
# configured on the disks. In addition, update the chassis
# VPD to contain the correct NVRAM VPD for the currently
# installed NVRAM card.
#
# Intended for use during disk tango operations.
#
def tango():
    import isi.hw.check.lib.mellanox as mellanox

    if not chassis_supports_transformation():
        logit(LOG_ERR, 'Chassis does not support transformation')
        return 1

    if confirm() != 0:
        logit(LOG_ERR, 'Chassis and disks are not compatible for disk tango')
        return 1

    if not ifs_is_mounted():
        logit(LOG_ERR, '/ifs is not mounted so disk tango is not complete')
        return 1

    logit(LOG_INFO, 'Chassis and disks are compatible for disk tango')
    
    product_sku = generate_product_sku()
    if product_sku is None:
        logit(LOG_ERR, 'Unable to generate product SKU')
        return 1

    hdd_sku = get_chassis_vpd('sku', 'disk')
    if hdd_sku is None:
        logit(LOG_ERR, 'Unable to locate chassis SKU on disks')
        return 1

    logit(LOG_INFO, 'Chassis SKU read from disks is "%s"' % hdd_sku)

    product_pn = extract_part_number_from_sku(product_sku)
    if product_pn is None:
        logit(LOG_ERR, 'Unable to locate P/N in product SKU "%s"' % product_sku)
        return 1

    hdd_pn = extract_part_number_from_sku(hdd_sku)
    if hdd_pn is None:
        logit(LOG_ERR, 'Unable to locate P/N in chassis SKU on disk "%s"' %
            hdd_sku)
        return 1

    if product_pn != hdd_pn:
        logit(LOG_ERR, 'Generated product SKU does not match product SKU '
              'on disks')
        return 1

    logit(LOG_INFO, 'Reading VPD from chassis to disks')
    if sync_vpd_from_chassis_to_disk() != 0:
        logit(LOG_ERR, 'Unable to read VPD from chassis to disks')
        return 1

    logit(LOG_INFO, 'Writing product SKU "%s" to disks' % product_sku)
    if write_chassis_sku(product_sku, 'disk') != 0:
        logit(LOG_ERR, 'Unable to write product SKU ("%s") to disks' %
            product_sku)
        return 1

    logit(LOG_INFO, 'Pushing VPD from disks to chassis')
    if push_vpd_from_disk_to_chassis() != 0:
        logit(LOG_ERR, 'Unable to push VPD from disks to chassis')
        return 1

    if not chassis_sku_in_sync():
        logit(LOG_ERR, 'Chassis SKU not in sync between disks and EEPROM')
        return 1

    logit(LOG_INFO, 'Updating chassis VPD with NVRAM VPD')
    flint = mellanox.Flint()

    #
    # Note: Return codes from chassis_vpd_data_sync() are:
    #   
    #        0 = VPD already matches, no sync required
    #        1 = VPD was successfully synchronized
    #       -1 = Error duing synchronization
    #
    rc = flint.chassis_vpd_data_sync()
    if rc == 0:
        logit(LOG_INFO, 'Chassis VPD already contains correct NVRAM VPD, '
              'no update required')
    elif rc == 1:
        logit(LOG_INFO, 'Chassis VPD successfully synchronized with NVRAM VPD')
    elif rc == -1:
        logit(LOG_ERR, 'Unable to update chassis VPD with NVRAM VPD')
        return 1

    logit(LOG_NOTICE, 'Disk tango SKU transformation completed successfully')
    return 0

#
# Display the specified messages on the LCD and serial console
# respectively, instructing the user to enter a command to
# drop into single user mode and thus, a shell.
#
def drop_into_single_user(lcd_msg=None, console_msg=None):
    import isi.ui.lcd.screens as screens
    import isi.ui.lcd.library as lcdlibrary
    import isi.app.lib.procs as procs

    def kill_other(pid):
        try:
            os.kill(pid, signal.SIGINT)
        except os.error:
            pass

    def handle_sigterm(signal_number, stack_frame):
        sys.exit(0)

    #
    # Create one process to handle the LCD and another to handle 
    # the serial console
    #
    pid = os.fork()
    signal.signal(signal.SIGINT, handle_sigterm)

    # Child handles LCD
    if pid == 0:
        while True:
            screen = screens.StaticScreen(lcd_msg)
            screen._can_exit = False
            screen._timeout = 10

            #
            # When we actually drop into single user mode, the LCD
            # daemon gets stopped, so catch the exception and restart
            # the daemon so we can keep the message refreshed on the
            # screen.
            #
            try:
                lcdlibrary.show(screen)
            except:
                os.system("/usr/bin/isi_lcd_d start")
            
        sys.exit(0)

    # Parent handles serial console
    while True:
        print console_msg
        print "Type 'prompt' to drop into single user mode and enter a shell."

        try:
            val = raw_input('--> ')
        except KeyboardInterrupt:
            break
        val = str.lower(val)
        if val == "prompt":
            procs.get_cmd_output("/usr/sbin/isi_stop_boot")
            # never returns
        else:
            print ""

    # Kill the LCD handler
    kill_other(pid)
    sys.exit(0)

#
# Check if chassis requires conversion. A chassis requires conversion 
# when it is one of the nineteen (19) Wingfoot or Blue Flame SKU based 
# chassis that were shipped with 6.5.1. However, starting with 6.5.2, 
# all WF/BF nodes will be built using CTO, therefore, we need to locate 
# each of the nodes that were built as SKU based products and convert 
# them to use the CTO (receipt) based process. 
#
def needs_conversion():

    # Make sure we're starting with a node that's in a good state
    if not chassis_sku_in_sync():
        logit(LOG_ERR, 'Chassis SKU not in sync between disks and EEPROM')
        return 0

    # Now that we're sure they are in sync, it's quicker to read SKU from disk
    hdd_sku = get_chassis_vpd('sku', 'disk')
    if hdd_sku is None:
        # Technically redundant because of call to chassis_sku_in_sync()
        logit(LOG_ERR, 'ERROR: Unable to locate chassis SKU on disks')
        return 0

    logit(LOG_INFO, 'Chassis SKU read from disks is "%s"' % hdd_sku)

    # Ignore TAB level when searching for product SKUs
    product_sku = extract_part_number_from_sku(hdd_sku)
    if product_sku is None:
        logit(LOG_ERR, 'Unable to locate P/N in product SKU "%s"' % product_sku)
        return 0

    # Check both X200 and S200 lists
    rc = 0
    for sku in X200_SKUS:
        if sku == product_sku:
            rc = 1
            break
    # Wasn't an X200, now check S200
    if rc == 0:
        for sku in S200_SKUS:
            if sku == product_sku:
                rc = 1
                break

    return rc

#
# Convert this SKU-based node into a CTO node by transforming the
# chassis product SKU into the appropriate CTO base chassis SKU.
#
# Intended for (manual) use when one (1) of the nineteen (19) Wingfoot
# or Blue Flame product SKUs are encountered in the field.
#
def convert():
    import isi.hw.hal as hal
    import isi.app.lib.procs as procs

    # Make sure everything is copasetic before we begin
    if not needs_conversion():
        logit(LOG_NOTICE, 'Node does not require conversion')
        return 1

    hdd_sku = get_chassis_vpd('sku', 'disk')
    if hdd_sku is None:
        # Technically redundant because of call to needs_conversion()
        logit(LOG_ERR, 'ERROR: Unable to locate chassis SKU on disks')
        return 1

    logit(LOG_INFO, 'Chassis SKU read from disks is "%s"' % hdd_sku)

    # For purposes of locating the nodes, we ignore the TAB level
    product_pn = extract_part_number_from_sku(hdd_sku)
    if product_pn is None:
        logit(LOG_ERR, 'Unable to locate P/N in product SKU "%s"' % hdd_sku)
        return 1

    # Translate from product SKU to base chassis SKU for CTO
    base_pn = None
    for sku in X200_SKUS:
        if sku == product_pn:
            base_pn = X200_BASE_SKU
            break

    if base_pn is None:
        for sku in S200_SKUS:
            if sku == product_pn:
                base_pn = S200_BASE_SKU
                break

    if base_pn is None:
        logit(LOG_ERR, 'Unable to convert "%s" to base chassis P/N' % 
            product_pn)
        return 1

    # Grab the TAB level of the product SKU
    tab = extract_tab_level_from_sku(hdd_sku)
    if tab is None:
        logit(LOG_ERR, 'ERROR: Unable to extract TAB level from "%s"' % hdd_sku)
        return 1

    logit(LOG_INFO, 'TAB level of product is "%s"' % tab)

    # Merge tab level of product into base chassis SKU
    base_chassis_sku = base_pn + '-' + tab

    logit(LOG_INFO, 'Writing base chassis SKU "%s" to disks' % base_chassis_sku)
    if write_chassis_sku(base_chassis_sku, 'disk') != 0:
        logit(LOG_ERR, 'Unable to write base chassis SKU ("%s") to disks' %
            base_chassis_sku)
        return 1

    # Once we put the new SKU on the disks, we need to generate
    # hardware capabilities and attributes and kick HAL before
    # we write the VPD to the chassis
    if _g_dry_run_flag is True:
        print 'Dry run - bypass isi_inventory_tool --reInit'
    else:
        logit(LOG_INFO, 'Running isi_inventory_tool --reInit')
        cmd = '%s --reInit' % INVENTORY_TOOL_PATH
        output = procs.get_cmd_output(cmd)[1]
        # Ignore the failure we're sure to get since we know the
        # chassis does not yet have all of its CTO information
        # (receipt, softAVL, etc.)
        for lines in output:
            print lines

    if _g_dry_run_flag is True:
        print 'Dry run - bypass HAL re-initialization'
    else:
        logit(LOG_INFO, 'Re-initializing HAL')
        if hal.reInitialize() != 0:
            logit(LOG_ERR, 'HAL re-initialization failed')
            return 1

    logit(LOG_INFO, 'Pushing VPD from disks into chassis')
    if push_vpd_from_disk_to_chassis() != 0:
        logit(LOG_ERR, 'Unable to push VPD from disks into chassis')
        return 1

    logit(LOG_INFO, 'Syncing VPD from chassis to disks')
    if sync_vpd_from_chassis_to_disk() != 0:
        logit(LOG_ERR, 'Unable to sync VPD from chassis to disks')
        return 1

    if not chassis_sku_in_sync():
        logit(LOG_ERR, 'Chassis SKU not in sync between disks and EEPROM')
        return 1

    logit(LOG_NOTICE, 'CTO conversion completed successfully')

    logit(LOG_WARNING, 'CTO conversion has caused the chassis SKU to change.')
    logit(LOG_WARNING, 'Changing the chassis SKU requires a reboot before ')
    logit(LOG_WARNING, 'the change will take effect. Please reboot as soon')
    logit(LOG_WARNING, 'as possible!')

    return 0

#
# Print help 
#
# error is a string that will be printed along with the usage message
#
def usage(error=None):
    if error:
        print 'ERROR: %s' % error.capitalize()

    print 'Usage: %s [-h,--help] [-n,--need] [-c,--confirm]\n' \
          '                            [-i,--install] [-t,--tango]' \
          ' [-d,--debug]\n' \
          '                            [-z,--convert] [-x,--dryrun]' % \
        sys.argv[0]
    print ''
    print '      -h,--help       Show this message'
    print '      -n,--need       Check if chassis is in need of transformation'
    print '                      or CTO conversion'
    print '      -c,--confirm    Verify 3/4 chassis matches product SKU on'
    print '                      disks'
    print '      -i,--install    Transform 3/4 chassis SKU to product SKU'
    print '      -t,--tango      Transform 3/4 chassis SKU to product SKU and '
    print '                      update chassis VPD with current NVRAM VPD'
    print '      -d,--debug      Enable debugging messages'
    print '      -z,--convert    Convert SKU-based node to CTO node'
    print '      -x,--dryrun     Dry run of action. Use with -i, -t, -r & -z'

    return 1

#
# Main program
#
def main(argv=None):

    result = -1

    if argv is None:
        argv = sys.argv

    try:
        (opts, args) = getopt.getopt(argv[1:], 'hncitmrdzx',
        ['help', 'need', 'confirm', 'install', 'tango', \
         'debug', 'convert', 'dryrun' ])

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

    if args:
        usage('Unexpected arguments: %s' % ' '.join(args))
        sys.exit(1)

    init_logging()

    # So debug option can appear anywhere in the command options
    globals() ['_g_debug_flag'] = False
    globals() ['_g_dry_run_flag'] = False
    for (opt, _) in opts:
        if opt in ('-d', '--debug'):
            globals() ['_g_debug_flag'] = True
        if opt in ('-x', '--dryrun'):
            globals() ['_g_dry_run_flag'] = True

    if _g_dry_run_flag is True:
        print 'Dry run starting...'

    for (opt, _) in opts:
        if opt in ('-h', '--help'):
            result = usage()
        elif opt in ('-c', '--confirm'):
            result = confirm()
        elif opt in ('-i', '--install'):
            result = install()
        elif opt in ('-t', '--tango'):
            result = tango()
        elif opt in ('-n', '--need'):
            result = needs_transformation()
        elif opt in ('-z', '--convert'):
            result = convert()

    if _g_dry_run_flag is True:
        print 'Dry run complete. No actions taken.'

    if result < 0:
        result = usage()

    sys.exit(result)

if __name__ == '__main__':
    sys.exit(main())

# vim: et sts=4 sw=4
