#! /usr/bin/python
#
# compare-cpu
#
# Determines if the specified xe host-cpu-info results 
# are compatible for XenServer CPU masking
#
# Copyright (c) 2010 Citrix Systems, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation; version 2.1 only. with the special
# exception on linking described in file LICENSE.
#
# 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 Lesser General Public License for more details.
#

def findValue(filename,value):
   file_handle = open(filename, 'r')
   file_line = file_handle.readline()
   while file_line != "":
      start = file_line.find(value)
      if start != -1:
          # Remove any EOL chars 
          if file_line.find("\n") != -1:
             file_line = file_line[0:file_line.find("\n")].rstrip()
          # +2 skips the colon and space
          return file_line[start+len(value)+2:len(file_line)]
      file_line = file_handle.readline()
   return ""

def printFmt(output1, output2):
   pad_max=22
   print output1.rjust(pad_max), output2

def compareCPUs(a_features, a_mask_level, b_features, b_mask_level, pool_mask):

   mask = ""
   compare_results = ["CPU 1 and CPU 2 have identical feature sets, no masking required",
                      "CPU 1 can be masked to CPU 2",
                      "CPU 1 cannot be masked to CPU 2"]

   # verify if a mask is required at all
   if a_features == b_features:
      return 0, compare_results[0]

   # remove separating dashes
   a_features2 = a_features.replace("-","")
   b_features2 = b_features.replace("-","")
   pool_mask2 = pool_mask.replace("-","")

   for i in range(0, 32, 16):

      # Convert desired portion of the features string to an int
      a_int = int(a_features2[i:i+16],16)
      b_int = int(b_features2[i:i+16],16)
      p_int = int(pool_mask2[i:i+16],16)

      # Apply pool mask 
      # Don't overwrite original a and b feature values - they are needed later because
      # pool mask only applies in the evaluation of the mask on join and therefore it
      # should not be included in the actual joint mask to be returned
      a_p_int = a_int & p_int
      b_p_int = b_int & p_int

      # Evaluate the first half
      if i==0:
         # If masking needed
         if a_p_int != b_p_int:
            # But not supported return not compatible
            if a_mask_level == "no":
               return 2, compare_results[2]
            # Masking needed, supported; verify if a can be masked to b
            elif a_p_int & b_p_int != b_p_int:
               # No common base mask
               return 2, compare_results[2]
         # Store the first half of the required mask with formatting
         mask = featureIntToString(a_int & b_int)
         mask += "-"

      # Evaluate the second half
      if i==16:
         # If masking needed
         if a_p_int != b_p_int:
            # But not supported return not compatible 
           if a_mask_level != "full":
              return 2, compare_results[2]
           # Masking needed, supported; verify a can be masked to b
           elif a_p_int & b_p_int == b_p_int:
              # Both halves can be masked OK
              mask += featureIntToString(a_int & b_int)
              return 1, compare_results[1], mask
           else:
              # No common extended mask
              return 2, compare_results[2]
         else:
              # Only first half must be masked, mask OK
              mask += featureIntToString(a_int & b_int)
              return 1, compare_results[1], mask


def findJointMask(a_features, a_mask_level, b_features, b_mask_level, pool_mask):

   joint_mask         = ""
   joint_mask_results = ["CPU 1 does not support masking",
                         "CPU 2 does not support masking",
                         "CPU 1 does not support extended masking",
                         "CPU 2 does not support extended masking",
                         "CPU 1 and CPU 2 support a common mask"]

   # remove separating dashes
   a_features2 = a_features.replace("-","")
   b_features2 = b_features.replace("-","")
   pool_mask2 = pool_mask.replace("-","")

   for i in range(0, 32, 16):

      # Convert desired portion of the features string to an int
      a_int = int(a_features2[i:i+16],16)
      b_int = int(b_features2[i:i+16],16)
      p_int = int(pool_mask2[i:i+16],16)

      # Apply pool mask 
      # Don't overwrite original a and b feature values - they are needed later because
      # pool mask only applies in the evaluation of the mask on join and therefore it
      # should not be included in the actual joint mask to be returned
      a_p_int = a_int & p_int
      b_p_int = b_int & p_int

      joint_mask_half_int = (a_p_int & b_p_int)

      # Evaluate the first half
      if i==0:
         # if a needs to be masked, verify it can be
         if joint_mask_half_int != a_p_int and a_mask_level != "base" and a_mask_level != "full":
            return 0, joint_mask_results[0]
         # if b needs to be masked, verify it can be
         if joint_mask_half_int != b_p_int and b_mask_level != "base" and b_mask_level != "full":
            return 1, joint_mask_results[1]
         
      # Evaluate the second half
      if i==16:
         # if a needs to be masked, verify it can be
         if joint_mask_half_int != a_p_int and a_mask_level != "full":
            return 2, joint_mask_results[2]
         # if b needs to be masked, verify it can be
         if joint_mask_half_int != b_p_int and b_mask_level != "full":
            return 3, joint_mask_results[3]

      # Store the half with formatting
      joint_mask += featureIntToString(a_int & b_int)
      if i==0:
         joint_mask += "-"

   return 4, joint_mask_results[4], joint_mask

def featureIntToString(features):
   # Removes the 0x, removes the L, pads with 0s to 16 bytes, and adds hyphens
   featureString = hex(features).lstrip("0x").rstrip("L").rjust(16,"0")
   featureString = featureString[0:8] + "-" + featureString[8:16]
   return featureString

import sys
from optparse import OptionParser,OptionGroup

# Parse cmd line arguments
parser=OptionParser(usage="usage: %prog -v -p pool_mask file1 file2 | -c features1 mask_level1 features2 mask_level2")
more_info=OptionGroup(parser, 
   "More information",
   "The values specified for the file1 and file2 arguments must contain the output from the xe host-cpu-info command on the joining host and existing pool master respectively. If using the -c option, specify the physical_features and maskable values from xe host-cpu-info on the joining host and existing pool master as the features1/mask_level1 and features2/mask_level2 arguments respectively.")
parser.add_option_group(more_info)            
parser.add_option("-p", "--pool_mask", 
  help="An optional 32 byte hex value for use as a general CPU mask, equivalent to pool.other-config:cpuid_feature_mask",
  default="ffffff7f-ffffffff-ffffffff-ffffffff")
parser.add_option("-v", "--verbose", 
  help="Display verbose output", 
  action="store_true", 
  dest="verbose")
parser.add_option("-c", "--cmdline", 
  help="Use feature and mask values specified on the command line rather than from a file", 
  action="store_true", 
  dest="cmdline", 
  default=False)

(options, args) =  parser.parse_args()

# Validate argument counts and filenames if specified
if options.cmdline:
   if len(args) !=4:
      parser.error("Incorrect number of arguments. The -c option requires 4 arguments.")
      exit(1)
else:
   if len(args) != 2:
      parser.error("Incorrect number of arguments. 2 arguments required.")
      exit(1)
   for i in args:
      try:
         file_handle = open(i, 'r')
      except IOError:
         print
         print "File %s does not exist" % (i)
         print
         exit(1)
      else:
         file_handle.close

# Parse/retrieve argument values
if options.cmdline:
   features = [args[0], args[2]]
   mask_levels = [args[1], args[3]]

else:
   features = [findValue(args[0], "physical_features"), findValue(args[1], "physical_features")]
   mask_levels = [findValue(args[0], "maskable"), findValue(args[1], "maskable")]
   modelnames = [findValue(args[0], "modelname"), findValue(args[1], "modelname")]

# Validate and display values
if options.verbose and not options.cmdline:
   print
   printFmt("file1:", args[0])
   printFmt("file2:", args[1])

if options.verbose:
   if options.cmdline:
      print
   printFmt("pool_mask:", options.pool_mask)

   if options.verbose:
      for i in [0,1]:
         print
         printFmt("CPU %s:" % (i+1), "")
         if not options.cmdline:
            printFmt("model name:", modelnames[i])
         printFmt("features:", features[i])
         printFmt("masking level:", mask_levels[i])

# Evaluate masking compatibility

results = compareCPUs(features[0], mask_levels[0], features[1], mask_levels[1], options.pool_mask)

print

if results[0] == 0:
   # Identical feature sets, no masking required
   printFmt("Result:", results[1])
   printFmt("Mask type:", "n/a")
   printFmt("Mask:", "n/a")

elif results[0] == 1:
   # CPU 1 can be masked to CPU 2 (use case 1)
   printFmt("Result:", "CPU 1 and CPU 2 are compatible for masking")
   printFmt("Mask type:","1 - CPU 1 has a superset of features to CPU 2")
   printFmt("Mask:","%s" % results[2])

else:
   results = findJointMask(features[0], mask_levels[0], features[1], mask_levels[1], options.pool_mask)
   # if joint mask is possible
   if results[0] == 4:
      # and joint mask is the same as joining features
      if results[2] == features[0]:
         # CPU 2 can be masked to CPU 1 (use case 2)
         printFmt("Result:", "CPU 1 and CPU 2 are compatible for masking")
         printFmt("Mask type:","2 - CPU 1 has a subset of features to CPU 2")
         printFmt("Mask:",results[2])
      else:
         # joint mask required on all hosts (use case 3)
         printFmt("Result:", "CPU 1 and CPU 2 are compatible for masking")
         printFmt("Mask type:","3 - CPU 1 and CPU 2 have a mutually exclusive set of features but support a common mask")
         printFmt("Mask:",results[2])
   else:
      # joint mask required but not supported
      printFmt("Result:", "CPU 1 and CPU 2 are not compatible for masking because %s" % results[1])

print

exit(0)
