#!/usr/bin/env python3
# Copyright (c) 2013 Arista Networks, Inc.  All rights reserved.
# Arista Networks, Inc. Confidential and Proprietary.

# pylint: disable=consider-using-f-string

import os, re, sys
import optparse, Tac, Tracing # pylint: disable=deprecated-module
import time
from ModularSystemCardUtils import cardModelsCompatible, isCardModelValid

Tracing.traceSettingIs( ",".join( [ os.environ.get( "TRACE", "" ) ] +
                        [ 'manageLinecardInterfaces/035' ] ) )


__defaultTraceHandle__ = Tracing.Handle( "manageLinecardInterfaces" )
t0 = Tracing.trace0
t3 = Tracing.trace3
t5 = Tracing.trace5
t6 = Tracing.trace6

class CardInsertionConfig:
   """This class encapsulates the running configuration cleanup that gets done
      via agentSocket when a new card is inserted and has been determined
      to be incompatible with an old card in the same slot for which the running
      config was made.
   """
   def __init__( self, slotId_, slot_,
                 cardModelName_, oldCardModelName_, 
                 cardConfigFileName_, sysname_ ):
      self.slotId = slotId_
      self.slot = slot_
      self.cardModelName = cardModelName_
      self.oldCardModelName = oldCardModelName_
      self.cardConfigFileName = cardConfigFileName_
      self.sysname = sysname_

   def saveCliConfig( self ):
      t3( "saveCliConfig for card", self.slotId )

      # Remove the old model number information to persistent config file.
      if self.oldCardModelName:
         self._configurationTerminator()

      # Write the new card's model name to the card config file.
      try:
         with open( self.cardConfigFileName, 'w' ) as configFile:
            configFile.write( self.cardModelName )
            configFile.write( '\n' )
            t0( "Wrote model name", self.cardModelName, "to file", 
                self.cardConfigFileName )
      except OSError:
         t0( "saveCliConfig cannot open", self.cardConfigFileName, "for writing" )

   def _executeCliCommands( self, cmds ):
      t6( "Executing cmds ", cmds )

      outputs = []
      for cmd in cmds:
         try:
            cmd = cmd + "\n"
            output = Tac.run( [ "Cli", "-p", "15", "-A",
                                "-s", self.sysname, "-c" ] + [ cmd ],
                                stdout=Tac.CAPTURE )
            outputs.append( output )
         except Exception: # pylint: disable-msg=W0703
            t5( "Invalid command: %s" % cmd )
      return "\n".join( outputs )

   def _executeCliCommand( self, cmd ):
      return self._executeCliCommands( [ cmd ] ).splitlines()

   def _configurationTerminator( self ):
      # We are done remembering the previous linecard's model information.
      # Remove the configuration file.
      if os.path.exists( self.cardConfigFileName ):
         os.remove( self.cardConfigFileName )

   def _getWordCountOfCliCommand( self, cmd ):
      return int( self._executeCliCommand( "%s | wc -l" % cmd )[ 0 ] )

   def _getInterfacesInSlot( self ):
      # Get a list of currently configured interfaces in this slot
      cmd = 'show running-config | grep -w "^interface Ethernet%s"' \
            ' | grep -v "^>"' % self.slot
      output = self._executeCliCommand( cmd )
      # We need to remove sub-interfaces - if any - as we don't want to
      # touch the user configuration. If the sub-intf config is not required
      # then the user will have to remove it manually.
      intfListForProcessing = []
      for op in output:
         if '.' not in op:
            intfListForProcessing += [ op ]
      return intfListForProcessing

   def _getNumIntfCfgLinesForSlot( self ):
      cmd = r"show running-config section ^interface Ethernet%s | grep -v ^!" \
             " | grep -v '^>'" % self.slot
      numInterfaceConfigLines = self._getWordCountOfCliCommand( cmd )
      t5( 'Number of interface cfg lines for slot', self.slotId, 'is', 
          numInterfaceConfigLines )
      return numInterfaceConfigLines

   def _getNumIntfCfgSecHdrLinesForSlot( self ):
      cmd = r"show running-config | grep Ethernet%s | grep -v '^>'" % self.slot
      numInterfaceCfgSecHdrLines = self._getWordCountOfCliCommand( cmd )
      t5( 'Number of interface cfg section header lines for slot', self.slotId, 
          'is', numInterfaceCfgSecHdrLines )
      return numInterfaceCfgSecHdrLines

   def _removeAnyInterfacesInSlot( self ):
      intfs = self._getInterfacesInSlot()
      if not intfs:
         t0( "There are no Ethernet interfaces for", self.slotId )
         return

      # do the no interfaces equivalents
      cmds = [ "configure\n" + "no %s" % intf for intf in intfs ]
      self._executeCliCommands( cmds )
     
      # Do some post checking
      postCleanupNumIntfCfgLines = self._getNumIntfCfgLinesForSlot()
      postCleanupNumIntfCfgSecHdrLines = self._getNumIntfCfgSecHdrLinesForSlot()
      if ( postCleanupNumIntfCfgLines == 0 and
           postCleanupNumIntfCfgLines == postCleanupNumIntfCfgSecHdrLines ):
         t0( "All interface config cleaned up for slot", self.slotId )
         print( "Old Card Model:", self.oldCardModelName )
      else:
         t0( "There are still", postCleanupNumIntfCfgLines, 
             "lines in the config for slot", self.slotId )

   def cleanupCliConfig( self ):
      t3( "cleanupCliConfig for card", self.slotId )
      # These 2 lines are purely for tracing purposes
      self._getNumIntfCfgLinesForSlot()
      self._getNumIntfCfgSecHdrLinesForSlot()

      # Remove any interfaces still in the slot
      self._removeAnyInterfacesInSlot()

def parseCmdLineArgs():
   parser = optparse.OptionParser()
   parser.add_option( '--slot', dest='slotId', default=None,
                     help='Card name - ex: Linecard10' )
   parser.add_option( '--model', dest='cardModelName', default=None,
                     help='Card model name' )
   parser.add_option( '--portNumberingScheme', dest='portNumberingScheme',
                     default=None, help='Port NumberingScheme' )
   parser.add_option( '--standalone', action='store_true', dest='standalone',
                     help='Run this script in standalone mode.' )
   parser.add_option( "--sysname", dest='sysname', default=None,
                     help='Sysdb system name' )
   parser.add_option( '--persistentFileName', dest='persistentFileName', 
      default=None,
      help='Persistent file name for remembering current linecard model' )

   (options, args) = parser.parse_args(sys.argv)
   if len( args ) > 1:
      t0( "Invalid arguments:", args[ 1: ] )
      sys.exit( "Invalid arguments" )

   # Check for mandatory arguments depending on context.
   if not options.slotId:
      t0( "SlotId is a mandatory argument." )
      parser.print_help()
      sys.exit( "SlotId is a mandatory argument" )

   if not options.cardModelName:
      t0( "CardModelName is a mandatory argument." )
      parser.print_help()
      sys.exit( "CardModelName is a mandatory argument" )

   if options.standalone:
      if not options.sysname:
         t0( "Sysname is mandatory in standalone mode" )
         parser.print_help()
         sys.exit( "Sysname is mandatory in standalone mode" )

      if not options.persistentFileName:
         t0( "PersistentFileName is mandatory in standalone mode" )
         parser.print_help()
         sys.exit( "PersistentFileName is mandatory in standalone mode" )

   t3( "Card name is", options.slotId )
   t3( "Card model name is", options.cardModelName )
   t3( "Port Numbering Scheme is", options.portNumberingScheme )

   return options   

def main():
   options = parseCmdLineArgs()   
   # Ensure that the slotId parameter is of the format Linecard<digits>
   # pylint: disable-next=superfluous-parens
   assert( options.slotId.startswith( "Linecard" ) )
   slot = options.slotId[ 8: ]
   assert( slot.isdigit() ) # pylint: disable=superfluous-parens

   if options.standalone:
      configFileName = options.persistentFileName
   else:
      configFileName = "/persist/sys/" + options.slotId + ".cfg"

   oldCardModelName = None
   newCardModelName = options.cardModelName
   if options.portNumberingScheme:
      m = re.match( r'\w+_(\w+)', options.portNumberingScheme )
      assert m, "Port Numbering Scheme is in incorrect format"
      scheme = m.group( 1 ).upper()
      alternateModelName = options.cardModelName + '-' + scheme
      if isCardModelValid( alternateModelName ):
         newCardModelName = alternateModelName

   t0( "Handling insertion of card", options.slotId, 
       "model", options.cardModelName )


   # Read the card config file if present.
   try:
      with open( configFileName ) as configFile:
         oldCardModelName = configFile.readline()
         oldCardModelName = oldCardModelName.strip( '\n' )
         t0( "Old line card model", oldCardModelName, "new line card model",
             options.cardModelName )
   except OSError:
      t0( configFileName, "does not exist" )

   rv = None
   if oldCardModelName:
      invalidParameter, rv = cardModelsCompatible( oldCardModelName, 
                                                   newCardModelName )

      if invalidParameter:
         sys.exit( "%s is not a valid card model" % invalidParameter )
      if rv:
         # No change in cards from previous life.
         t0( "Card models are compatible. Do not cleanupCliConfig." )
   else:
      if not isCardModelValid( newCardModelName ):
         sys.exit( "%s is not a valid card model" % newCardModelName )

   sysname = 'ar' # default

   if options.standalone:
      sysname = options.sysname

   if oldCardModelName != newCardModelName:
      # Instantiate the card insertion config object.
      cardInsertionConfig = CardInsertionConfig( options.slotId,
                                                 slot,
                                                 newCardModelName,
                                                 oldCardModelName,
                                                 configFileName,
                                                 sysname )

      # Write the new model number information to persistent config file.
      # This happends regardless of whether the second step of configuration
      # cleanup completes or not, to ensure the system is sane and up-to-date
      # on next boot.
      cardInsertionConfig.saveCliConfig()
   
      # If previous persistent model information exists, clean it up.
      if oldCardModelName and not rv:
         cardInsertionConfig.cleanupCliConfig()

if __name__ == "__main__":
   begin = time.time()
   main()
   finish = time.time()
   t3( sys.argv[ 0 ], 'took', ( finish - begin ) )
