#!/usr/bin/python

# instnum.py - parse and decode RHEL Installation Numbers
# Copyright (c) 2006 Red Hat, Inc.
# Authors: Dave Lehman <dlehman@redhat.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

import hmac
import sha
import re

##
## I18N
##
from rhpl.translate import _, N_
import rhpl.translate as translate
domain = "rhel-instnum"
translate.textdomain (domain)

"""
    TODO:

    NOTES:
        - it'd be nice to tweak the encoding map setup
        - there's some inconsistency WRT handling requests for invalid fields;
            some methods return None, others raise exceptions

"""
class InstallationNumberError(Exception):
    def __init__(self, args=None):
        Exception.__init__(self)
        self.args = args

class InstallationNumber:
    def __init__(self, rawString):
        self.rawString = ''         # the encoded IN string as hex digits
        self.rawDict = {}           # map of field names to decoded values
        self.formatMap = {}         # map of offset and length of each field
        self.keyMap = {}            # map of key offsets for each field

        # strip out any non-alphanumeric characters
        rawString = re.sub(r'[^a-zA-Z0-9]', '', rawString)

        # basic validation of the string
        try:
            int(rawString, 16)
        except ValueError:
            raise InstallationNumberError, "Not a valid hex string"
        except TypeError:
            raise InstallationNumberError, "Not a string"
        else:
            self.rawString = rawString

        # get the maps for this string format (based solely on string length)
        stringLen = len(self.rawString)
        if not formatMap.has_key(stringLen) or not keyMap.has_key(stringLen):
            raise InstallationNumberError, "Unsupported string length"
        else:
            self.formatMap = formatMap[stringLen]
            self.keyMap = keyMap[stringLen]

        # final validation is a checksum initial substring comparison
        if not self.verifyChecksum():
            raise InstallationNumberError, "Checksum verification failed"

        # parse the string and decrypt the values
        self.__parse()

    def __parse(self):
        """ parse the IN string and decrypt the values """
        rawString = self.rawString
        rawNum = 0
        value = None

        # make sure the key is first so any fields requiring decryption are ok
        maps = self.formatMap.items()
        keyMapEnt = (IN_KEY, self.formatMap[IN_KEY])
        i = maps.index(keyMapEnt)
        sortedMaps = [maps.pop(i)]
        sortedMaps.extend(maps)
        maps = sortedMaps

        # parse each of the fields
        for field, (offset, length) in maps:
            # first, convert the relevant portion of the string to an int
            startChar = self.__hexLen(offset, down=True)
            endChar = self.__hexLen(offset + length)
            rawNum = int(rawString[startChar:endChar], 16)

            # now, pluck out the bits we want
            offset -= (startChar * 4)
            pad = ((endChar - startChar) * 4) - offset - length
            mask = pow(2, length) - 1
            value = (rawNum >> pad) & mask

            # decrypt the value if necessary
            self.rawDict[field] = self.decrypt(field, value)

    def __hexLen(self, bits, down=None):
        """ convert a length in bits to a length in hex digits,
                with the option to round down instead of up 
        """
        length = bits / 4
        if bits % 4:
            if down is None:
                length += 1
        return length

    def verifyChecksum(self):
        """ compute and verify the keyed SHA1 checksum of the IN payload

            NOTE: we only use the raw string since __parse() hasn't been called

            returns a boolean value (True == success)
        """

        # grab the checksum
        csumMap = self.formatMap[IN_CHECKSUM]
        csumStart = self.__hexLen(csumMap[0], down=True)
        csumLen = self.__hexLen(csumMap[1]) # we'll use this later, too
        csumEnd = csumStart + csumLen
        checksum = self.rawString[csumStart:csumEnd]

        # grab the payload (everything after the checksum)
        payload = self.rawString[csumEnd:]

        # grab the key
        keyMapEnt = self.formatMap[IN_KEY]
        keyStart = self.__hexLen(keyMapEnt[0], down=True)
        keyEnd = keyStart + self.__hexLen(keyMapEnt[1])
        key = self.rawString[keyStart:keyEnd]

        # create an HMAC hash using an SHA1 digest ;  feed it %key and %payload
        mac = hmac.new(key, payload, sha)

        # compare the first %csumLen digits of our just-computed hash
        #   against the digits we recieved as part of the IN string
        rc = False
        if checksum == mac.hexdigest()[:csumLen]:
            rc = True

        return rc

    def encrypt(self, field, value=None):
        """ encrypt a field using the appropriate segment of the IN's key

            returns the encrypted value (raise KeyError on failure)
        """
        if not self.formatMap.has_key(field):
            raise KeyError, "Unknown field: '%s'" % (field,)

        if value is None:
            value = self.getRaw(field)

        if self.keyMap.has_key(field):
            key = self.getRaw(IN_KEY)
            keyLen = self.formatMap[IN_KEY][1]
            offset = self.keyMap[field]
            # grab the field length from the formatMap
            length = self.formatMap[field][1]
            pad = keyLen - offset - length
            mask = pow(2, length) - 1
            value ^= (key >> pad) & mask

        return value

    def decrypt(self, field, value):
        """ decrypt a field given its value using the appropriate segment
              of the IN's key
        """
        return self.encrypt(field, value)

    def getRaw(self, field):
        """ retrieve a field's value without decoding it

            invalid field returns None
        """
        return self.rawDict.get(field)

        # XXX not reached

        if not self.formatMap.has_key(field):
            raise ValueError, "Unknown field: %s" % (field,)

        # XXX should never get here
        raise InstallationNumberError, "missing field '%s'" % (fieldNamesMap[field],)

    def getRawAsString(self, field):
        """ return a field's raw value as a hex string """
        value = self.getRaw(field)
        if value is None:
            return None

        bits = self.formatMap[field][1]
        length = self.__hexLen(bits)
        # XXX is this really necessary?
        stringVal = eval('"%0' + str(length) + 'x" % (value,)')
        return stringVal
        
    def decode(self, field):
        """ translate a field from encoded value to human-readable value

            NOTE: this can return varying types depending on the field

            returns the field's decoded value (or None on error)
        """
        value = None
        product = self.getRaw(IN_PRODUCT)

        if encodingMaps.has_key(field) and self.getRaw(field) is not None:
            # if this field has an encodingMap, use it

            # handle special cases first
            if field == IN_PRODUCT:
                value = encodingMaps[field][product]
            elif field == IN_OPTIONS:
                value = combinationsDict[product][self.getRaw(field)]
            else:
                # let rip
                value = encodingMaps[field][product][self.getRaw(field)]
        elif self.getRaw(field) is not None:
            # return the hex substring
            value = self.getRawAsString(field)

        return value

    def get_summary_string(self):
        """ represent the object in a human-readable format """
        s = _("Product: RHEL %s\n" % (self.get_product_string().title(),))
        s += _("Type: %s\n" % (self.get_type_string(),))
        s += _("Options: %s\n" % (self.get_options_string(),))
        s += _("Allowed CPU Sockets: %s\n" % (self.get_socklimit_string(),))
        s += _("Allowed Virtual Instances: %s\n" % (self.get_virtlimit_string(),))
        s += _("Package Repositories: %s\n" % (self.get_repos_string(),))
        return s

    def __str__(self):
        """ return the original IN string """
        inum = ''
        i = 0
        j = 4
        while j <= len(self.rawString):
            if i != 0:
                inum += "-"
            inum += self.rawString[i:j]
            i += 4
            j += 4
        return inum

    def get_key(self):
        """ return the raw (integer) IN key """
        return self.getRaw(IN_KEY)

    def get_checksum(self):
        """ return the checksum portion of the IN """
        return self.getRaw(IN_CHECKSUM)

    def get_options(self):
        """ return the actual options bitfield """
        options = self.getRaw(IN_OPTIONS)
        product = self.get_product()
        return encodingMaps[IN_COMBINATIONS][product][options]

    def get_socklimit(self):
        """ return socklimit as an integer """
        return self.decode(IN_SOCKLIMIT)

    def get_virtlimit(self):
        """ return virtlimit as an integer """
        return self.decode(IN_VIRTLIMIT)

    def get_type(self):
        """ return the type of the IN as an integer """
        return self.getRaw(IN_TYPE)

    def get_product(self):
        """ return the product as an integer """
        return self.getRaw(IN_PRODUCT)

    def get_repos(self):
        """ return a list of applicable repos """
        product = self.get_product()
        optCodes = encodingMaps[IN_OPTIONS][product].keys()
        optCodes.sort()
        myOpts = self.get_options()
        myOptCodes = [code for code in optCodes if myOpts & code]
        repoMap = encodingMaps[IN_REPO][product]
        repos = []

        def resolve_repos(rmap, repo):
            if type(repo) == type(""):
                ret = [repo]
            if type(repo) == type([]):
                ret = []
                for r in repo:
                    ret.extend(resolve_repos(rmap, r))
            elif rmap.has_key(repo):
                ret = resolve_repos(rmap, rmap[repo])

            return ret

        for optCode in myOptCodes:
            if repoMap.has_key(optCode):
                for repo in repoMap[optCode]:
                    repos.extend(resolve_repos(repoMap, repo))

        return repos
        
    def get_key_string(self):
        """ return the key as a string """
        return self.decode(IN_KEY)

    def get_checksum_string(self):
        """ return the checksum as a string """
        return self.decode(IN_CHECKSUM)

    def get_options_string(self):
        """ return the options as space-delimited concatenation of 
            enabled option strings """
        product = self.get_product()
        optionsMap = encodingMaps[IN_OPTIONS][product]
        optCodes = optionsMap.keys()
        optCodes.sort()
        myOpts = self.get_options()
        strings = [optionsMap[code] for code in optCodes if myOpts & code]
        value = " ".join(strings)
        return value

    def get_socklimit_string(self):
        """ return the socklimit as a string """
        value = self.decode(IN_SOCKLIMIT)
        if value == -1:
            value = _("Unlimited")
        else:
            value = str(value)
        
        return value

    def get_virtlimit_string(self):
        """ return the virtlimit as a string """
        value = self.decode(IN_VIRTLIMIT)
        if value == -1:
            value = _("Unlimited")
        else:
            value = str(value)
        
        return value

    def get_type_string(self):
        """ return the type as a string """
        return self.decode(IN_TYPE)

    def get_product_string(self):
        """ return the product as a string """
        return self.decode(IN_PRODUCT)

    def get_repos_string(self):
        """ return the applicable repos as a space-delimited list """
        return " ".join(self.get_repos())

    def get_repos_dict(self):
        product = self.get_product()
        optionsMap = encodingMaps[IN_OPTIONS][product]
        repoMap = encodingMaps[IN_REPO][product]
        optCodes = optionsMap.keys()
        optCodes.sort()
        myOpts = self.get_options()
        repoDict = {}
        repoList = []

        def resolve_repos(rmap, repo):
            if type(repo) == type(""):
                ret = [repo]
            if type(repo) == type([]):
                ret = []
                for r in repo:
                    ret.extend(resolve_repos(rmap, r))
            elif rmap.has_key(repo):
                ret = resolve_repos(rmap, rmap[repo])

            return ret

        for code in optCodes:
            if myOpts & code and repoMap.has_key(code) and repoMap[code]:
                for repo in repoMap[code]:
                    repoList.extend(resolve_repos(repoMap, repo))

        for repo in repoList:
            for (code, repos) in repoMap.items():
                key = optionsMap[code]
                if key == "FullProd":
                    # XXX hack to keep the keys minimally descriptive
                    key = "Base"
                if repo in repos:
                    repoDict[key] = repo

        return repoDict

InstNum = InstallationNumber
InstNumError = InstallationNumberError

IN_KEY = 0
IN_CHECKSUM = 1
IN_OPTIONS = 2
IN_SOCKLIMIT = 3
IN_VIRTLIMIT = 4
IN_TYPE = 5
IN_PRODUCT = 6
IN_REPO = 7
IN_COMBINATIONS = 8

PROD_SERVER = 0x0
PROD_CLIENT = 0x1

fieldNamesMap = {
    IN_KEY          : "key",
    IN_CHECKSUM     : "checksum",
    IN_OPTIONS      : "options",
    IN_SOCKLIMIT    : "socklimit",
    IN_VIRTLIMIT    : "virtlimit",
    IN_TYPE         : "type",
    IN_PRODUCT      : "product",
    IN_REPO         : "repo",
    IN_COMBINATIONS : "combinations" }

"""
    Basic definition of the Entitlement Number formats and codes for RHEL5.
    
     We support three different Entitlement Number formats:
    * 16 hex digits for backwards compatibility with old RHEL/RHN.
    * 24 hex digits extended regcodes for future extendability and
       higher entropy.
    * 32 hex digits to be able to encode asset tags.
   
    The default in the RHEL5 time frame is expected to be 16 hex digits for now.
    32 is considered to be to long for customers to type in but it might come 
    back in later, automated usecases. So every implementation should be able
    to accept different EN formats.
    
    The formats are:
    
     K = Key          - Generated via the algorithm currently used in HACK for 
                        the whole regcode. 
     C = Checksum     - Keyed SHA of the ("encrypted") payload and the key. 
                        - Verification and additional entropy.
     O = Options      - Option encoding
     S = SocketLimit  - Number of physical CPUs allowed.
     V = VirtLimit    - Number of virtual instances allowed.
     T = EN Type      - Type of the EN
     P = Product Code - Looked up in a dictionary. Defines the interpretation 
                        of the rest of the payload digits.
    
    The product code and the length together define the format.
     
    16 hex digits: 
     KK KK KK CC OO OS VT PP
     |        |   |    || |- Product defining the format. 
     |        |   |    ||- Virt limit code used to look up the limit
     |        |   |    |   in a dictionary. The least bit actually is 
     |        |   |    |   used for the Type field.
     |        |   |    |- Socket (phys. CPUs) limit code used to look 
     |        |   |       up the limit in the list of all possible limits.
     |        |   |- Option code to look up the combination of 
     |        |      options in a dictionary.
     |        |- Checksum - Keyed hash Key and the other fields.
     |-  Key generated with Peter's algorithm


    On a bit level it looks like this:
  60   56   52   48   44   40   36   32   28   24   20   16   12    8    4    0
0    4    8    12   16   20   24   28   32   36   40   44   48   52   56   60   
KKKK KKKK KKKK KKKK KKKK KKKK CCCC CCCC OOOO OOOO OOOO OOSS SSVV VTTP PPPP PPPP

    Bits:
    Key part:
    [40 - 63]	- K 
		- Key generated with the old HACK algorithm. Source of entropy
		  and used to 'encrypt' parts of the payload.
    Checksum:
    [32 - 39]   - C
		- Checksum - first two hex digits of the keyed sha1 hash of the 
                  payload with the key.
    Payload:
    [18 - 31]   - O
		- Options - Index of the actual option combination in the list
                  of all possible combinations for the product. Xored with the 
                  highest 14 bits of the key.
    [14 - 17]   - S
		- Socket (physical CPU) limit. Encoded as the index of the 
                  limit in the list of all possible socket limits. Xored with
                  the next highest 4 bit of the key.
    [11 - 13]	- V
		- Virt Limit - Limit of the number of virtual instances allowed.
                  Encoded as the index of the limit in the list of all possible 
                  virtualization limits. Xored with the next highest 3 bits of 
                  the key.
    [9 - 10]    - T
                - EN type - 0 => does not create Entitlement,
                            1 => creates Entitlement,
                            2 => installeronly. 
                  Not Xored.
    [0 - 8]	- P
                - Product code - 0 for Server and 1 for Client. Defines the 
                  format. Xored with the lowest 9 bits of the key.

    24 digits:
    1  3  5  7  9  11 13 15 17 19 21 23
    KK KK KK KK KK CC CO OO OO SV TP PP
    
    32 digits:
    1  3  5  7  9  11 13 15 17 19 21 23 25 27 29 31
    KK KK KK KK KK KK KK CC CC OO OO OO OS SV TP PP
"""

# NOTE: the offsets are from the left -- deal with it
# toplevel key is length of IN string
formatMap = {
        #       field name  : (offset, length) in bits, that is
        16: {   IN_KEY      : (0, 24),
                IN_CHECKSUM : (24, 8),
                IN_OPTIONS  : (32, 14),
                IN_SOCKLIMIT: (46, 4),
                IN_VIRTLIMIT: (50, 3),
                IN_TYPE     : (53, 2),
                IN_PRODUCT  : (55, 9) },
        24: {   IN_KEY      : (0, 40),
                IN_CHECKSUM : (40, 12),
                IN_OPTIONS  : (52, 20),
                IN_SOCKLIMIT: (72, 4),
                IN_VIRTLIMIT: (76, 4),
                IN_TYPE     : (80, 4),
                IN_PRODUCT  : (84, 12) },
        32: {   IN_KEY      : (0, 56),
                IN_CHECKSUM : (56, 16),
                IN_OPTIONS  : (72, 24),
                IN_SOCKLIMIT: (96, 8),
                IN_VIRTLIMIT: (104, 4),
                IN_TYPE     : (108, 4),
                IN_PRODUCT  : (112, 16) } }

keyMap = {
        #       field name  : offset into key in bits (lengths from format map)
        16: {   IN_OPTIONS  : 0,
                IN_SOCKLIMIT: 14,
                IN_VIRTLIMIT: 18,
                IN_PRODUCT  : 15 },
        24: {   IN_OPTIONS  : 0,
                IN_SOCKLIMIT: 20,
                IN_VIRTLIMIT: 24,
                IN_PRODUCT  : 28 },
        32: {   IN_OPTIONS  : 0,
                IN_SOCKLIMIT: 24,
                IN_VIRTLIMIT: 32,
                IN_PRODUCT  : 40 } }

# not sure why I think this needs to go both ways
productMap = {  PROD_SERVER : 'server',
                PROD_CLIENT : 'client',
                'server'    : PROD_SERVER,
                'client'    : PROD_CLIENT }

optionsMap = {
	 PROD_SERVER : {
		0x1     : 'NoSLA',
		0x4     : 'Basic',
		0x8     : 'Standard',
		0x10    : 'Premium',
		0x20    : 'Devel',
		0x40    : 'Eval',
		0x80    : 'Level3',
		0x100   : 'FullProd',
		0x200   : 'Virt',
		0x400   : 'Cluster',
		0x800   : 'ClusterStorage',
		0x2000  : 'VirtPlatform',
		0x4000  : 'HPC',
		0x8000  : 'Directory',
		0x10000 : 'SMB' },
	 PROD_CLIENT : {
		0x1     : 'NoSLA',
		0x2     : 'Escalation',
		0x4     : 'Basic',
		0x8     : 'Standard',
		0x10    : 'Premium',
		0x20    : 'Devel',
		0x40    : 'Eval',
		0x80    : 'Level3',
		0x100   : 'FullProd',
                0x200   : 'Virt',
		0x1000  : 'Workstation',
		0x8000  : 'SMB' } }

repoMap = {
        PROD_SERVER : {
                0x100   : ['Server'],
                0x200   : ['VT'],
                0x400   : ['Cluster'],
                0x800   : [0x400, 'ClusterStorage'],
                0x2000  : [0x200,0x800],
                0x4000  : [],
                0x8000  : [],
                0x10000 : [] },
        PROD_CLIENT : {
                0x100   : ['Client'],
                0x200   : ['VT'],
                0x1000  : ['Workstation'],
                0x8000  : [] } }


# types - Different kinds of entitlement numbers. 
#     installeronly - the entitlement number is used for the installer only.
#                     E.g. generated by a Satellite. 
#     entitlement   - Full entitlement number that can create an entitlement
#     preentitled   - Entitlement number given
typeMap = {
        PROD_SERVER : { 0x0 : "No Entitlement",
                        0x1 : "Entitlement",
                        0x2 : "Installer Only" },
        PROD_CLIENT : { 0x0 : "No Entitlement",
                        0x1 : "Entitlement",
                        0x2 : "Installer Only" } }

# CPU configuration limits 0=unlimited
socklimitMap = {
        PROD_SERVER: [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 
                        None, None, None, -1],
        PROD_CLIENT: [1, 2, 4, 8, 16, None, None, None, None, None, 
                        None, None, None, None, None, -1] }


virtlimitMap = { 
        PROD_SERVER: [0, 4, 10, 128, None, None, None, -1],
        PROD_CLIENT: [0, 2, 4, 10, None, None, None, -1] }

# combinationsMap = all possible combinations of options the EN can encode
combinationsMap = {
     PROD_SERVER : {
        0 : 0x101, 1 : 0x301, 2 : 0x701, 3 : 0xb01, 4 : 0x4301, 5 : 0x8301, 6 : 0x10301, 7 : 0x4701, 
        8 : 0x8701, 9 : 0x10701, 10 : 0x18701, 11 : 0x4b01, 12 : 0x8b01, 13 : 0x10b01, 14 : 0x18b01, 15 : 0x18301, 
        16 : 0x501, 17 : 0x4501, 18 : 0x8501, 19 : 0x10501, 20 : 0x18501, 21 : 0x901, 22 : 0x4901, 23 : 0x8901, 
        24 : 0x10901, 25 : 0x18901, 26 : 0x4101, 27 : 0x8101, 28 : 0x10101, 29 : 0x18101, 30 : 0x2101, 31 : 0xa101, 
        32 : 0x12101, 33 : 0x1a101, 34 : 0x104, 35 : 0x304, 36 : 0x704, 37 : 0xb04, 38 : 0x4304, 39 : 0x8304, 
        40 : 0x10304, 41 : 0x4704, 42 : 0x8704, 43 : 0x10704, 44 : 0x18704, 45 : 0x4b04, 46 : 0x8b04, 47 : 0x10b04, 
        48 : 0x18b04, 49 : 0x18304, 50 : 0x504, 51 : 0x4504, 52 : 0x8504, 53 : 0x10504, 54 : 0x18504, 55 : 0x904, 
        56 : 0x4904, 57 : 0x8904, 58 : 0x10904, 59 : 0x18904, 60 : 0x4104, 61 : 0x8104, 62 : 0x10104, 63 : 0x18104, 
        64 : 0x2104, 65 : 0xa104, 66 : 0x12104, 67 : 0x1a104, 68 : 0x108, 69 : 0x308, 70 : 0x708, 71 : 0xb08, 
        72 : 0x4308, 73 : 0x8308, 74 : 0x10308, 75 : 0x4708, 76 : 0x8708, 77 : 0x10708, 78 : 0x18708, 79 : 0x4b08, 
        80 : 0x8b08, 81 : 0x10b08, 82 : 0x18b08, 83 : 0x18308, 84 : 0x508, 85 : 0x4508, 86 : 0x8508, 87 : 0x10508, 
        88 : 0x18508, 89 : 0x908, 90 : 0x4908, 91 : 0x8908, 92 : 0x10908, 93 : 0x18908, 94 : 0x4108, 95 : 0x8108, 
        96 : 0x10108, 97 : 0x18108, 98 : 0x2108, 99 : 0xa108, 100 : 0x12108, 101 : 0x1a108, 102 : 0x110, 103 : 0x310, 
        104 : 0x710, 105 : 0xb10, 106 : 0x4310, 107 : 0x8310, 108 : 0x10310, 109 : 0x4710, 110 : 0x8710, 111 : 0x10710, 
        112 : 0x18710, 113 : 0x4b10, 114 : 0x8b10, 115 : 0x10b10, 116 : 0x18b10, 117 : 0x18310, 118 : 0x510, 119 : 0x4510, 
        120 : 0x8510, 121 : 0x10510, 122 : 0x18510, 123 : 0x910, 124 : 0x4910, 125 : 0x8910, 126 : 0x10910, 127 : 0x18910, 
        128 : 0x4110, 129 : 0x8110, 130 : 0x10110, 131 : 0x18110, 132 : 0x2110, 133 : 0xa110, 134 : 0x12110, 135 : 0x1a110, 
        136 : 0x181, 137 : 0x381, 138 : 0x781, 139 : 0xb81, 140 : 0x4381, 141 : 0x8381, 142 : 0x10381, 143 : 0x4781, 
        144 : 0x8781, 145 : 0x10781, 146 : 0x18781, 147 : 0x4b81, 148 : 0x8b81, 149 : 0x10b81, 150 : 0x18b81, 151 : 0x18381, 
        152 : 0x581, 153 : 0x4581, 154 : 0x8581, 155 : 0x10581, 156 : 0x18581, 157 : 0x981, 158 : 0x4981, 159 : 0x8981, 
        160 : 0x10981, 161 : 0x18981, 162 : 0x4181, 163 : 0x8181, 164 : 0x10181, 165 : 0x18181, 166 : 0x2181, 167 : 0xa181, 
        168 : 0x12181, 169 : 0x1a181, 170 : 0x184, 171 : 0x384, 172 : 0x784, 173 : 0xb84, 174 : 0x4384, 175 : 0x8384, 
        176 : 0x10384, 177 : 0x4784, 178 : 0x8784, 179 : 0x10784, 180 : 0x18784, 181 : 0x4b84, 182 : 0x8b84, 183 : 0x10b84, 
        184 : 0x18b84, 185 : 0x18384, 186 : 0x584, 187 : 0x4584, 188 : 0x8584, 189 : 0x10584, 190 : 0x18584, 191 : 0x984, 
        192 : 0x4984, 193 : 0x8984, 194 : 0x10984, 195 : 0x18984, 196 : 0x4184, 197 : 0x8184, 198 : 0x10184, 199 : 0x18184, 
        200 : 0x2184, 201 : 0xa184, 202 : 0x12184, 203 : 0x1a184, 204 : 0x188, 205 : 0x388, 206 : 0x788, 207 : 0xb88, 
        208 : 0x4388, 209 : 0x8388, 210 : 0x10388, 211 : 0x4788, 212 : 0x8788, 213 : 0x10788, 214 : 0x18788, 215 : 0x4b88, 
        216 : 0x8b88, 217 : 0x10b88, 218 : 0x18b88, 219 : 0x18388, 220 : 0x588, 221 : 0x4588, 222 : 0x8588, 223 : 0x10588, 
        224 : 0x18588, 225 : 0x988, 226 : 0x4988, 227 : 0x8988, 228 : 0x10988, 229 : 0x18988, 230 : 0x4188, 231 : 0x8188, 
        232 : 0x10188, 233 : 0x18188, 234 : 0x2188, 235 : 0xa188, 236 : 0x12188, 237 : 0x1a188, 238 : 0x190, 239 : 0x390, 
        240 : 0x790, 241 : 0xb90, 242 : 0x4390, 243 : 0x8390, 244 : 0x10390, 245 : 0x4790, 246 : 0x8790, 247 : 0x10790, 
        248 : 0x18790, 249 : 0x4b90, 250 : 0x8b90, 251 : 0x10b90, 252 : 0x18b90, 253 : 0x18390, 254 : 0x590, 255 : 0x4590, 
        256 : 0x8590, 257 : 0x10590, 258 : 0x18590, 259 : 0x990, 260 : 0x4990, 261 : 0x8990, 262 : 0x10990, 263 : 0x18990, 
        264 : 0x4190, 265 : 0x8190, 266 : 0x10190, 267 : 0x18190, 268 : 0x2190, 269 : 0xa190, 270 : 0x12190, 271 : 0x1a190, 
        272 : 0x140, 273 : 0x340, 274 : 0x740, 275 : 0xb40, 276 : 0x4340, 277 : 0x8340, 278 : 0x10340, 279 : 0x4740, 
        280 : 0x8740, 281 : 0x10740, 282 : 0x18740, 283 : 0x4b40, 284 : 0x8b40, 285 : 0x10b40, 286 : 0x18b40, 287 : 0x18340, 
        288 : 0x540, 289 : 0x4540, 290 : 0x8540, 291 : 0x10540, 292 : 0x18540, 293 : 0x940, 294 : 0x4940, 295 : 0x8940, 
        296 : 0x10940, 297 : 0x18940, 298 : 0x4140, 299 : 0x8140, 300 : 0x10140, 301 : 0x18140, 302 : 0x2140, 303 : 0xa140, 
        304 : 0x12140, 305 : 0x1a140, 306 : 0x144, 307 : 0x344, 308 : 0x744, 309 : 0xb44, 310 : 0x4344, 311 : 0x8344, 
        312 : 0x10344, 313 : 0x4744, 314 : 0x8744, 315 : 0x10744, 316 : 0x18744, 317 : 0x4b44, 318 : 0x8b44, 319 : 0x10b44, 
        320 : 0x18b44, 321 : 0x18344, 322 : 0x544, 323 : 0x4544, 324 : 0x8544, 325 : 0x10544, 326 : 0x18544, 327 : 0x944, 
        328 : 0x4944, 329 : 0x8944, 330 : 0x10944, 331 : 0x18944, 332 : 0x4144, 333 : 0x8144, 334 : 0x10144, 335 : 0x18144, 
        336 : 0x2144, 337 : 0xa144, 338 : 0x12144, 339 : 0x1a144, 340 : 0x148, 341 : 0x348, 342 : 0x748, 343 : 0xb48, 
        344 : 0x4348, 345 : 0x8348, 346 : 0x10348, 347 : 0x4748, 348 : 0x8748, 349 : 0x10748, 350 : 0x18748, 351 : 0x4b48, 
        352 : 0x8b48, 353 : 0x10b48, 354 : 0x18b48, 355 : 0x18348, 356 : 0x548, 357 : 0x4548, 358 : 0x8548, 359 : 0x10548, 
        360 : 0x18548, 361 : 0x948, 362 : 0x4948, 363 : 0x8948, 364 : 0x10948, 365 : 0x18948, 366 : 0x4148, 367 : 0x8148, 
        368 : 0x10148, 369 : 0x18148, 370 : 0x2148, 371 : 0xa148, 372 : 0x12148, 373 : 0x1a148, 374 : 0x150, 375 : 0x350, 
        376 : 0x750, 377 : 0xb50, 378 : 0x4350, 379 : 0x8350, 380 : 0x10350, 381 : 0x4750, 382 : 0x8750, 383 : 0x10750, 
        384 : 0x18750, 385 : 0x4b50, 386 : 0x8b50, 387 : 0x10b50, 388 : 0x18b50, 389 : 0x18350, 390 : 0x550, 391 : 0x4550, 
        392 : 0x8550, 393 : 0x10550, 394 : 0x18550, 395 : 0x950, 396 : 0x4950, 397 : 0x8950, 398 : 0x10950, 399 : 0x18950, 
        400 : 0x4150, 401 : 0x8150, 402 : 0x10150, 403 : 0x18150, 404 : 0x2150, 405 : 0xa150, 406 : 0x12150, 407 : 0x1a150, 
        408 : 0x400, 409 : 0x800, 410 : 0x8000, 411 : 0x480, 412 : 0x880, 413 : 0x8080, 414 : 0x120, 
    },

     PROD_CLIENT : {
        0 : 0x101, 1 : 0x301, 2 : 0x8101, 3 : 0x1101, 4 : 0x8301, 5 : 0x1301, 6 : 0x9301, 7 : 0x9101, 
        8 : 0x102, 9 : 0x302, 10 : 0x8102, 11 : 0x1102, 12 : 0x8302, 13 : 0x1302, 14 : 0x9302, 15 : 0x9102, 
        16 : 0x104, 17 : 0x304, 18 : 0x8104, 19 : 0x1104, 20 : 0x8304, 21 : 0x1304, 22 : 0x9304, 23 : 0x9104, 
        24 : 0x108, 25 : 0x308, 26 : 0x8108, 27 : 0x1108, 28 : 0x8308, 29 : 0x1308, 30 : 0x9308, 31 : 0x9108, 
        32 : 0x110, 33 : 0x310, 34 : 0x8110, 35 : 0x1110, 36 : 0x8310, 37 : 0x1310, 38 : 0x9310, 39 : 0x9110, 
        40 : 0x181, 41 : 0x381, 42 : 0x8181, 43 : 0x1181, 44 : 0x8381, 45 : 0x1381, 46 : 0x9381, 47 : 0x9181, 
        48 : 0x182, 49 : 0x382, 50 : 0x8182, 51 : 0x1182, 52 : 0x8382, 53 : 0x1382, 54 : 0x9382, 55 : 0x9182, 
        56 : 0x184, 57 : 0x384, 58 : 0x8184, 59 : 0x1184, 60 : 0x8384, 61 : 0x1384, 62 : 0x9384, 63 : 0x9184, 
        64 : 0x188, 65 : 0x388, 66 : 0x8188, 67 : 0x1188, 68 : 0x8388, 69 : 0x1388, 70 : 0x9388, 71 : 0x9188, 
        72 : 0x190, 73 : 0x390, 74 : 0x8190, 75 : 0x1190, 76 : 0x8390, 77 : 0x1390, 78 : 0x9390, 79 : 0x9190, 
        80 : 0x140, 81 : 0x340, 82 : 0x8140, 83 : 0x1140, 84 : 0x8340, 85 : 0x1340, 86 : 0x9340, 87 : 0x9140, 
        88 : 0x142, 89 : 0x342, 90 : 0x8142, 91 : 0x1142, 92 : 0x8342, 93 : 0x1342, 94 : 0x9342, 95 : 0x9142, 
        96 : 0x144, 97 : 0x344, 98 : 0x8144, 99 : 0x1144, 100 : 0x8344, 101 : 0x1344, 102 : 0x9344, 103 : 0x9144, 
        104 : 0x148, 105 : 0x348, 106 : 0x8148, 107 : 0x1148, 108 : 0x8348, 109 : 0x1348, 110 : 0x9348, 111 : 0x9148, 
        112 : 0x150, 113 : 0x350, 114 : 0x8150, 115 : 0x1150, 116 : 0x8350, 117 : 0x1350, 118 : 0x9350, 119 : 0x9150, 
        120 : 0x120, 
    },

}

encodingMaps = {IN_OPTIONS       : optionsMap,
                IN_SOCKLIMIT     : socklimitMap,
                IN_VIRTLIMIT     : virtlimitMap,
                IN_TYPE          : typeMap,
                IN_PRODUCT       : productMap,
                IN_REPO          : repoMap,
                IN_COMBINATIONS  : combinationsMap }


def test_data_from_file(filename):
    buf = [l.strip() for l in open(filename)]

    in_strings = [x[1] for x in enumerate(buf) if x[0] % 3 == 0]
    values = [x[1] for x in enumerate(buf) if x[0] % 3 == 2]

    test_data = {}
    for k,v in zip(in_strings, values):
        (product, socklimit, virtlimit, opts_index, rest) = v.split(' ', 4)
        product = product.replace('rhel_', '')
        socklimit = int(socklimit)
        virtlimit = int(virtlimit)
        opts_index = int(opts_index)
        toks = rest.split()
        in_type = int(toks[-2])
        option_strings = eval(" ".join(toks[:-2]))

        test_data[k] = (product, socklimit, virtlimit, opts_index, 
                        option_strings, in_type)

    return test_data

def test_from_file(filename):
    test_data = test_data_from_file(filename)

    num_pass = 0
    num_total = len(test_data.keys())

    for in_string, in_data in test_data.items():
        rc = test(in_string, in_data)
        if rc:
            num_pass += 1

    pct = int(float(num_pass)/float(num_total) * 100)
    print "%d%% (%d/%d) Passed" % (pct, num_pass, num_total)

def test(in_string, test_data):
    (tproduct, tsocklimit, tvirtlimit,
     topts_index, topt_strings, ttype) = test_data

    inum = InstallationNumber(in_string)

    product = inum.get_product_string()
    socklimit = inum.get_socklimit()
    virtlimit = inum.get_virtlimit()
    opts = inum.get_options()
    opt_strings = inum.get_options_string().split()
    in_type = inum.get_type()

    err_str = "%s\n" % (in_string,)
    errors = 0

    if product != tproduct:
        err_str += "  product: '%s' != '%s'\n" % (product, tproduct)
        errors += 1

    if socklimit != tsocklimit:
        err_str += "  socklimit: %d != %d\n" % (socklimit, tsocklimit)
        errors += 1

    if virtlimit != tvirtlimit:
        err_str += "  virtlimit: %d != %d\n" % (virtlimit, tvirtlimit)
        errors += 1

    topts = encodingMaps[IN_COMBINATIONS][inum.get_product()][topts_index]
    if opts != topts:
        err_str += "  options: %x != %x\n" % (opts, topts)
        errors += 1

    topt_strings.sort()
    opt_strings.sort()
    if opt_strings != topt_strings:
        err_str += "  options: %s != %s\n" % (opt_strings, topt_strings)
        errors += 1

    if in_type != ttype:
        err_str += "  type: %d != %d\n" % (in_type, ttype)
        errors += 1

    if errors:
        print err_str
    
    return errors == 0


if __name__ == "__main__":
    import sys
    import os

    if len(sys.argv) < 2:
        sys.exit(1)

    if os.access(sys.argv[1], os.R_OK):
        td = test_from_file(sys.argv[1])
        sys.exit(0)

    in_string =  sys.argv[1]
    inum = InstallationNumber(in_string)

    print inum.get_summary_string()

    # return values of all the get_FOO and get_FOO_string methods
    for field in inum.formatMap.keys():
        name = fieldNamesMap[field]
        print "%s: %s '%s'" % (name,
                               eval('inum.get_' + name + '()'),
                               eval('inum.get_' + name + '_string()'))

    print
    print inum.get_repos_dict()

    print
    print inum

    """
    # this format is intended to ease visual comparison with the POC output
    print "%s %s %s %s [%s] %s" % (inum.get_product_string(),
                                   inum.get_socklimit(),
                                   inum.get_virtlimit(),
                                   inum.getRaw(IN_OPTIONS),
                                   inum.get_options_string(),
                                   inum.get_type())

    print
    """


