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

import os, sys
import Agent
import Cell
import BothTrace
import Tac
import Tracing
from GenericReactor import GenericReactor
from ControllerSslReactorLib import ControllerSslProfileStateReactor

__defaultTraceHandle__ = Tracing.Handle( "ControllerOob" )

AGENT_BINARY_NAME = "ControllerOob"
Constants = Tac.Type( "Controller::Constants" )
macAddrZero = Tac.Value( "Arnet::EthAddr" ).stringValue
SslProfileDisableReason = Tac.Type( "Controller::SslProfileStatus::DisableReason" )
ProfileState = Tac.Type( "Mgmt::Security::Ssl::ProfileState" )

bt1 = BothTrace.tracef1
bt5 = BothTrace.tracef5
bv = BothTrace.Var

class ClusterConfigMonitor:
   def __init__( self ):
      # TODO
      pass
   def doCleanup( self ):
      pass

class OobConfigMonitor:
   def __init__( self, oobConfig, oobStatus, clusterStatus,
                 bridgingConfig, serviceConfigDir,
                 overrideServiceConfigDir, publishStatus,
                 mgmtSecSslStatus, root ):
      self.oobConfig = oobConfig
      self.oobStatus = oobStatus
      self.clusterStatus = clusterStatus
      self.bridgingConfig = bridgingConfig
      self.serviceConfigDir = serviceConfigDir
      self.overrideServiceConfigDir = overrideServiceConfigDir
      self.publishStatus = publishStatus
      self.mgmtSecSslStatus = mgmtSecSslStatus
      self.root = root
      self.connectionConfig = Tac.newInstance( "Controller::ConnectionConfig", "" )
      self.sslProfileConfigReactor = GenericReactor( self.oobConfig.sslProfileConfig,
                                                     [ 'sslProfileName' ],
                                                     self.handleConfigChange )
      self.connectionPreserveReactor = GenericReactor( self.oobConfig,
                                       [ 'connectionPreserve' ],
                                       self.handleConnectionPreserveChange )
      self.sslProfileConfig = None
      self.sslProfileStateReactor = None
      self.handleConfigChange()

   # Create reactor for SSL profile status changes 
   def handleSsl( self ):
      bt5()
      self.sslProfileStateReactor = ControllerSslProfileStateReactor(
                                self.mgmtSecSslStatus,
                                self.oobConfig.sslProfileConfig.sslProfileName,
                                self.handleConfigChange,
                                self.handleConfigChange,
                                self.handleConfigChange,
                                callBackNow=False )
      state = self.sslProfileStateReactor.profileState()
      bt5( "SSL profile name passed is:",
           bv( self.oobConfig.sslProfileConfig.sslProfileName ) )
      errCode = None
      if not state:
         bt5( "SSL profile does not exist" )
         errCode = SslProfileDisableReason.profileNotExist
      elif state == ProfileState.valid:
         if not self.sslProfileStateReactor.dhparamsReady():
            bt5( "DH params not ready" )
            errCode = SslProfileDisableReason.dhparamsNotReady
         elif not self.sslProfileStateReactor.certKeyPath():
            bt5( "Certificate not configured in SSL profile" )
            errCode = SslProfileDisableReason.certNotConfigured
         elif not self.sslProfileStateReactor.trustedCertsPath():
            bt5( "Trusted certificates not configured in SSL profile" )
            errCode = SslProfileDisableReason.trustedCertNotConfigured
         else:
            bt5( "SSL profile is valid and ready to use" )
            errCode = SslProfileDisableReason.noError
      else:
         bt1( "SSL profile is invalid" )
         errCode = SslProfileDisableReason.profileInvalid
      if not self.oobStatus.sslProfileStatus:
         self.oobStatus.sslProfileStatus = (
               self.oobConfig.sslProfileConfig.sslProfileName, )
      if errCode == SslProfileDisableReason.noError:
         bt5( "Creating oobStatus.sslProfileStatus.sslProfileConfig from "
              "self.oobConfig.sslProfileConfig.sslProfileName" )
         self.oobStatus.sslProfileStatus.sslProfileConfig = (
               self.oobConfig.sslProfileConfig.sslProfileName, )
         bt5( "Created with name:",
              bv( self.oobStatus.sslProfileStatus.sslProfileConfig.sslProfileName ) )
         self.sslProfileConfig = self.oobStatus.sslProfileStatus.sslProfileConfig
         self.sslProfileConfig.tlsVersion = \
               self.sslProfileStateReactor.tlsVersion()
         self.sslProfileConfig.fipsMode = \
               self.sslProfileStateReactor.fipsMode()
         bt5( "sslProfileStateReactor certKeyPath:",
              bv( self.sslProfileStateReactor.certKeyPath() ) )
         self.sslProfileConfig.certKeyPath = \
               self.sslProfileStateReactor.certKeyPath()
         self.sslProfileConfig.trustedCertsPath = \
               self.sslProfileStateReactor.trustedCertsPath()
         self.sslProfileConfig.dhParamPath = \
            self.sslProfileStateReactor.dhParamPath()
         self.sslProfileConfig.crlsPath = \
               self.sslProfileStateReactor.crlsPath()
         self.sslProfileConfig.cipherSuite = \
               self.sslProfileStateReactor.cipherSuite()
         self.sslProfileConfig.cipherSuiteV1_3 = \
               self.sslProfileStateReactor.cipherSuiteV1_3()
         self.connectionConfig.sslProfileConfig = self.sslProfileConfig

         bt5( "Creating SSL profile :",
              bv( self.connectionConfig.sslProfileConfig.sslProfileName ) )
      self.oobStatus.sslProfileStatus.enabled = \
            errCode == SslProfileDisableReason.noError
      self.oobStatus.sslProfileStatus.disableReason = errCode
         
   def doCleanup( self ):
      bt5()
      if self.root.oobServerSm:
         self.root.oobServerSm.doCleanup()
         self.root.oobServerSm = None
      if self.sslProfileStateReactor:
         self.sslProfileStateReactor.close()
      self.sslProfileStateReactor = None
      self.connectionConfig.sslProfileConfig = None
      self.oobStatus.sslProfileStatus = None

   def sslEnabled( self ):
      bt5()
      if self.oobConfig.sslProfileConfig.sslProfileName:
         bt5( bv( "SSL profile name : %s" % # pylint: disable=consider-using-f-string
                  self.oobConfig.sslProfileConfig.sslProfileName ) )
         return True
      return False

   def handleConnectionPreserveChange( self, notifiee=None ):
      bt5()
      if not self.oobConfig.connectionPreserve:
         self.oobStatus.inactive.clear()

   def handleConfigChange( self, notifiee=None ):
      bt5()
      self.doCleanup()
      self.connectionConfig.port = Constants.controllerOobPort
      if self.sslEnabled():
         self.handleSsl()
         if not self.oobStatus.sslProfileStatus.enabled:
            return
      assert self.clusterStatus
      bt5( "Creating oobServerSm" )
      if self.connectionConfig.sslProfileConfig:
         bt5( "SSL CertKeyPath (from connectionConfig) = ", bv(
              self.connectionConfig.sslProfileConfig.certKeyPath ) )
      self.root.oobServerSm = ( self.connectionConfig, self.oobConfig,
                                self.oobStatus, self.clusterStatus,
                                self.serviceConfigDir,
                                self.overrideServiceConfigDir,
                                self.publishStatus )

class ControllerOobAgent( Agent.Agent ):
   def __init__( self, entityMgr, plugins=None, pluginPath=None ):
      Agent.Agent.__init__( self, entityMgr, agentName="ControllerOob" )
      # pylint: disable-next=consider-using-f-string
      qtfile = "{}{}.qt".format( self.agentName, "-%d" if "QUICKTRACEDIR"
                                 not in os.environ else "" )
      BothTrace.initialize( qtfile, "8,1024,8,8,8,1024,8,8,1024,8", 
                            maxStringLen=80 )
      bt1()

      self.entityMgr = entityMgr
      self.clusterConfigDir = None
      self.clusterStatusDir = None
      self.serviceMountConfigDir = None
      self.serviceMountStatusDir = None
      self.proxyServiceMountConfigDir = None
      self.proxyServiceMountStatusDir = None
      self.ipStatus = None
      self.serviceConfig = None
      self.clusterConfig = None
      self.clusterStatus = None
      self.oobConfig = None
      self.oobStatus = None
      self.oobConfigMonitor = None
      self.clusterConfigMonitor = None
      self.overrideServiceConfig = None
      self.publishStatus = None
      self.bridgingConfig = None
      self.msgSenderReceiver = None
      self.controllerVersionNegotiator = None
      self.heartbeatMessenger = None
      self.bridgeMacAddrReactor = None
      self.enabledReactor = None
      self.mgmtSecSslStatus = None
      self.root = None
      self.recordedControllerUuid = None
      self.controllerdbStatus = None
      self.uuidReactor = None
      self.cliRequestDir = None

   def doInit( self, entityMgr ): # pylint: disable=arguments-renamed
      bt5()
      mg = entityMgr.mountGroup()
      self.controllerdbStatus = mg.mount( "controller/status",
                                          "Controllerdb::Status", "r" )
      self.serviceConfig = mg.mount( "controller/service/config",
                                     "Controller::ServiceConfigDir", "r" )
      self.overrideServiceConfig = mg.mount( "controller/debug/overrideService",
                                             "Controller::OverrideServiceConfigDir",
                                             "r" )
      self.publishStatus = mg.mount( "controller/switchpublish/status",
                                     "Controllerdb::PublishStatus", "r" )
      self.bridgingConfig = mg.mount( "bridging/config", "Bridging::Config", "r" )
      self.mgmtSecSslStatus = mg.mount ( "mgmt/security/ssl/status",
                                         "Mgmt::Security::Ssl::Status", "r" )
      # mounts required for cluster
      self.clusterConfigDir = mg.mount( "controller/cluster/configDir", 
            "ControllerCluster::ClusterConfigDir", "r" )
      self.clusterStatusDir = mg.mount( "controller/cluster/statusDir", 
            "ControllerCluster::ClusterStatusDir", "w" )
      Tac.Type( "Ira::IraIpStatusMounter" ).doMountEntities( mg.cMg_, True, False )
      self.ipStatus = mg.mount( "ip/status", "Ip::Status", "r" )
      self.serviceMountConfigDir = mg.mount( "controller/mount/config/service",
                                             "Tac::Dir", "ri" )
      self.serviceMountStatusDir = mg.mount( "controller/mount/status/service",
                                             "Tac::Dir", "ri" )
      self.proxyServiceMountConfigDir = mg.mount( 
                                        "controller/mount/config/proxy/service",
                                        "Tac::Dir", "wi" )
      self.proxyServiceMountStatusDir = mg.mount( 
                                        "controller/mount/status/proxy/service",
                                        "Tac::Dir", "wi" )
      self.cliRequestDir = mg.mount(
            Cell.path( 'agent/commandRequest/config/controllerOob' ),
            'Tac::Dir', 'ri' )

      def _finish():
         bt5()
         self.uuidReactor = GenericReactor( self.controllerdbStatus,
                                            [ "controllerUuid" ],
                                            self.handleControllerUuid,
                                            callBackNow=True )
      mg.close( _finish )

   def handleControllerUuid( self, notifiee=None ):
      controllerUuid = self.controllerdbStatus.controllerUuid
      bt5( "uuid=", bv( controllerUuid ),
           "last=", bv( self.recordedControllerUuid ) )
      if controllerUuid and controllerUuid != self.recordedControllerUuid:
         if self.recordedControllerUuid:
            # Passive mounts will not work if the agent re-'exec's itself. So,
            # 'exit'ing only to be brought up to life immediately by ProcMgr.
            # Exit with '0' because the agent is terminating gracefully.
            bt1( "Dying gracefully.." )
            sys.exit( 0 )
         else:
            self.recordedControllerUuid = controllerUuid
            self._finishUuid()
      else:
         bt5( "Controller UUID not established yet ",
              bv( self.recordedControllerUuid )) 

   def _finishUuid( self ):
      bt5()
      local = self.entityMgr.root().parent
      self.root = local.newEntity( "ControllerOob::Root", "root" )
      clusterName = Constants.clusterDefaultName
      self.clusterConfig = self.clusterConfigDir.config[ clusterName ]
      self.clusterStatus = self.clusterStatusDir.status[ clusterName ]
      self.clusterStatus.controllerUUID = self.recordedControllerUuid
      self.clusterStatus.localIp = "0.0.0.0"
      self.oobConfig = self.clusterConfig.oobConfig
      self.oobStatus = self.clusterStatus.oobStatus
      self.oobStatus.controllerUuid = self.recordedControllerUuid
      self.doCleanup()
      self.bridgeMacAddrReactor = GenericReactor(
         self.bridgingConfig,
         [ "bridgeMacAddr" ],
         self.handleBridgeMacAddr )
      self.enabledReactor = GenericReactor( self.clusterConfig,
                                            [ 'enabled' ],
                                            self.handleEnabled,
                                            callBackNow=True )
      self.root.oobAgentRequestDirSm = ( "ControllerOob::OobAgentRequestDirSm",
                                         self.cliRequestDir )
      agentRequestDirSm = self.root.oobAgentRequestDirSm
      agentRequestDirSm.cliRequestDirSm = ()
      agentRequestDirSm.oobClearConnectionCallback = ( "", "clearConnection",
                                                self.clusterStatusDir )
      agentRequestDirSm.cliRequestDirSm.addAgentCommandCallback(
            agentRequestDirSm.oobClearConnectionCallback )


   def _bridgeMacAddr( self ):
      addr = str( self.bridgingConfig.bridgeMacAddr )
      # pylint: disable-next=consider-using-f-string
      bt5( bv( "bridgeMac=%s" % ( addr ) ) )
      self.oobStatus.systemId = Tac.Value( "Controller::SystemId", addr )
      if addr == macAddrZero:
         addr = None
      return addr

   def handleBridgeMacAddr( self, notifiee=None ):
      addr = self._bridgeMacAddr()
      if addr:
         self.handleEnabled()

   def handleEnabled( self, notifiee=None ):
      enabled = self.clusterConfig.enabled
      ready = self.clusterConfig.enabled and self._bridgeMacAddr() is not None
      bt5( "CVX enabled: ", bv( enabled ), "ready: ", bv( ready ) )
      if ready:
         self.doSetup()
      else:
         self.doCleanup()
      self.clusterStatus.enabled = enabled

   def doSetupClusterRoot( self, ):
      assert self.root
      self.root.clusterRootSm = ( self.oobStatus.systemId, self.ipStatus,
                                  self.clusterConfigDir, self.clusterStatusDir,
                                  self.serviceConfig, self.serviceMountConfigDir,
                                  self.serviceMountStatusDir, 
                                  self.proxyServiceMountConfigDir,
                                  self.proxyServiceMountStatusDir,
                                  self.controllerdbStatus )

   def doSetup( self ):
      self.oobConfigMonitor = OobConfigMonitor( self.oobConfig, self.oobStatus,
                                                self.clusterStatus,
                                                self.bridgingConfig,
                                                self.serviceConfig,
                                                self.overrideServiceConfig,
                                                self.publishStatus,
                                                self.mgmtSecSslStatus, self.root )
      self.clusterConfigMonitor = ClusterConfigMonitor()
      self.doSetupClusterRoot()

   def doCleanup( self ):
      if self.oobConfigMonitor:
         self.oobConfigMonitor.doCleanup()
         self.oobConfigMonitor = None
      if self.clusterConfigMonitor:
         self.clusterConfigMonitor.doCleanup()
         self.clusterConfigMonitor = None
      self.oobStatus.established.peer.clear()
      self.oobStatus.sslProfileStatus = None

def main():
   container = Agent.AgentContainer( [ ControllerOobAgent, ], scheduledAttrLog=True,
                                     agentTitle='ControllerOob' )
   container.runAgents()

if __name__ == "__main__":
   main()
