content: # Copyright 2015 Spotify AB. All rights reserved.
  #
  # The contents of this file are licensed under the Apache License, Version 2.0
  # (the "License"); you may not use this file except in compliance with the
  # License. You may obtain a copy of the License at
  #
  # http://www.apache.org/licenses/LICENSE-2.0
  #
  # Unless required by applicable law or agreed to in writing, software
  # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  # License for the specific language governing permissions and limitations under
  # the License.
  
  """
  Napalm driver for Arista EOS.
  
  Read napalm.readthedocs.org for more information.
  """
  
  # std libs
  import re
  import time
  import importlib
  import inspect
  import ipaddress
  import json
  import socket
  
  from datetime import datetime
  from collections import defaultdict
  
  # third party libs
  import pyeapi
  from pyeapi.eapilib import ConnectionError, EapiConnection
  from netmiko import ConfigInvalidException
  from typing import Dict
  
  # NAPALM base
  import napalm.base.helpers
  from napalm.base.netmiko_helpers import netmiko_args
  from napalm.base.base import NetworkDriver, models
  from napalm.base.utils import string_parsers
  from napalm.base.exceptions import (
      CommitError,
      ConnectionException,
      MergeConfigException,
      ReplaceConfigException,
      SessionLockedException,
      CommandErrorException,
      UnsupportedVersion,
  )
  from napalm.eos.constants import LLDP_CAPAB_TRANFORM_TABLE
  from napalm.eos.utils.versions import EOSVersion
  import napalm.base.constants as c
  
  # local modules
  # here add local imports
  # e.g. import napalm.eos.helpers etc.
  
  
  class EOSDriver(NetworkDriver):
      """Napalm driver for Arista EOS."""
  
      SUPPORTED_OC_MODELS = []
  
      HEREDOC_COMMANDS = [
          ("banner login", 1),
          ("banner motd", 1),
          ("comment", 1),
          ("protocol https certificate", 2),
      ]
  
      _RE_BGP_INFO = re.compile(
          r"BGP neighbor is (?P<neighbor>.*?), remote AS (?P<as>.*?), .*"
      )  # noqa
      _RE_BGP_RID_INFO = re.compile(
          r".*BGP version 4, remote router ID (?P<rid>.*?), VRF (?P<vrf>.*?)$"
      )  # noqa
      _RE_BGP_DESC = re.compile(r"\s+Description: (?P<description>.*?)$")
      _RE_BGP_LOCAL = re.compile(r"Local AS is (?P<as>.*?),.*")
      _RE_BGP_PREFIX = re.compile(
          r"(\s*?)(?P<af>IPv[46]) (Unicast|6PE):\s*(?P<sent>\d+)\s*(?P<received>\d+)"
      )  # noqa
      _RE_SNMP_COMM = re.compile(
          r"""^snmp-server\s+community\s+(?P<community>\S+)
                                  (\s+view\s+(?P<view>\S+))?(\s+(?P<access>ro|rw)?)
                                  (\s+ipv6\s+(?P<v6_acl>\S+))?(\s+(?P<v4_acl>\S+))?$""",
          re.VERBOSE,
      )
  
      def __init__(self, hostname, username, password, timeout=60, optional_args=None):
          """
          Initialize EOS Driver.
  
          Optional args:
              * lock_disable (True/False): force configuration lock to be disabled (for external lock
                  management).
              * force_cfg_session_invalid (True/False): force invalidation of the config session
                  in case of failure.
              * enable_password (True/False): Enable password for privilege elevation
              * eos_autoComplete (True/False): Allow for shortening of cli commands
              * transport (string): transport, eos_transport is a fallback for compatibility.
                  - ssh (uses Netmiko)
                  - socket
                  - http_local
                  - http
                  - https
                  - https_certs
                  - A subclass of EapiConnection
                  - a string that identifies a module and class that is a subclass of EapiConnection
                  (from: https://github.com/arista-eosplus/pyeapi/blob/develop/pyeapi/client.py#L115)
  
                  transport is the preferred method
              * eos_transport (string): pyeapi transport, defaults to https
                  eos_transport for backwards compatibility
  
          """
          self.device = None
          self.hostname = hostname
          self.username = username
          self.password = password
          self.timeout = timeout
          self.config_session = None
          self.locked = False
  
          self.platform = "eos"
          self.profile = [self.platform]
          self.optional_args = optional_args or {}
  
          self.enablepwd = self.optional_args.pop("enable_password", "")
          self.eos_autoComplete = self.optional_args.pop("eos_autoComplete", None)
  
          # Define locking method
          self.lock_disable = self.optional_args.pop("lock_disable", False)
  
          self.force_cfg_session_invalid = self.optional_args.pop(
              "force_cfg_session_invalid", False
          )
  
          # eos_transport is there for backwards compatibility, transport is the preferred method
          transport = self.optional_args.get(
              "transport", self.optional_args.get("eos_transport", "https")
          )
          self.transport = transport
  
          if transport == "ssh":
              self._process_optional_args_ssh(self.optional_args)
          else:
              self._process_optional_args_eapi(self.optional_args)
  
      def _process_optional_args_ssh(self, optional_args):
          self.transport_class = None
          self.netmiko_optional_args = netmiko_args(optional_args)
  
      def _process_optional_args_eapi(self, optional_args):
          # Parse pyeapi transport class
          self.transport_class = self._parse_transport(self.transport)
  
          # ([1:]) to omit self
          init_args = inspect.getfullargspec(self.transport_class.__init__)[0][1:]
          filter_args = ["host", "username", "password", "timeout"]
          init_args.append("enforce_verification")  # Not an arg for unknown reason
          self.eapi_kwargs = {
              k: v
              for k, v in optional_args.items()
              if k in init_args and k not in filter_args
          }
  
      def _parse_transport(self, transport):
          if inspect.isclass(transport) and issubclass(transport, EapiConnection):
              # Subclass of EapiConnection
              return transport
          elif "." in transport:
              # Try to resolve string import to module and getattr to class
              try:
                  comps = transport.split(".")
                  module = importlib.import_module(comps[0])
                  for comp in comps[1:]:
                      module = getattr(module, comp)
                  if issubclass(module, EapiConnection):
                      return module
              except ImportError:
                  # String doesn't start with a module, fallthrough to pyeapi defined strings
                  pass
              except AttributeError:
                  # Error walking from module to class, fallthrough to pyeapi defined strings
                  pass
          try:
              return pyeapi.client.TRANSPORTS[transport]
          except KeyError:
              # All methods failed, raise exception
              raise ConnectionException("Unknown transport: {}".format(transport))
  
      def open(self):
          """Implementation of NAPALM method open."""
          if self.transport == "ssh":
              self.device = self._netmiko_open(
                  device_type="arista_eos",
                  netmiko_optional_args=self.netmiko_optional_args,
              )
          else:
              try:
                  connection = self.transport_class(
                      host=self.hostname,
                      username=self.username,
                      password=self.password,
                      timeout=self.timeout,
                      **self.eapi_kwargs,
                  )
  
                  if self.device is None:
                      self.device = pyeapi.client.Node(
                          connection, enablepwd=self.enablepwd
                      )
                  # does not raise an Exception if unusable
  
              except ConnectionError as ce:
                  # and this is raised either if device not avaiable
                  # either if HTTP(S) agent is not enabled
                  # show management api http-commands
                  raise ConnectionException(str(ce))
  
          # endif self.transport
  
          sh_ver = self._run_commands(["show version"])
          self._eos_version = EOSVersion(sh_ver[0]["version"])
          if self._eos_version < EOSVersion("4.23.0"):
              raise UnsupportedVersion(self._eos_version)
  
      def close(self):
          """Implementation of NAPALM method close."""
          self.discard_config()
          if self.transport == "ssh":
              self._netmiko_close()
          elif hasattr(self.device.connection, "close") and callable(
              self.device.connection.close
          ):
              self.device.connection.close()
  
      def is_alive(self):
          if self.transport == "ssh":
              null = chr(0)
              if self.device is None:
                  return {"is_alive": False}
              try:
                  # Try sending ASCII null byte to maintain the connection alive
                  self.device.write_channel(null)
                  return {"is_alive": self.device.remote_conn.transport.is_active()}
              except (socket.error, EOFError):
                  # If unable to send, we can tell for sure that the connection is unusable
                  return {"is_alive": False}
  
          if hasattr(self.device.connection, "is_alive") and callable(
              self.device.connection.is_alive
          ):
              return self.device.connection.is_alive()
          return {"is_alive": True}  # always true as eAPI is HTTP-based
  
      def _run_commands(self, commands, **kwargs):
          if self.transport == "ssh":
              ret = []
              for command in commands:
                  if kwargs.get("encoding") == "text":
                      cmd_output = self._netmiko_device.send_command(command).replace(
                          "% Invalid input", ""
                      )
                      ret.append({"output": cmd_output})
                      continue
  
                  cmd_pipe = command + " | json"
                  cmd_txt = self._netmiko_device.send_command(cmd_pipe)
                  try:
                      cmd_json = json.loads(cmd_txt)
                  except json.decoder.JSONDecodeError:
                      cmd_json = {}
                  ret.append(cmd_json)
              return ret
          else:
              return self.device.run_commands(commands, **kwargs)
  
      def _obtain_lock(self, wait_time=None):
          """
          EOS internally creates config sessions when using commit-confirm.
  
          This can cause issues obtaining the configuration lock:
  
          cfg-2034--574620864-0 completed
          cfg-2034--574620864-1 pending
          """
          if wait_time:
              start_time = time.time()
              while time.time() - start_time < wait_time:
                  try:
                      self._lock()
                      return
                  except SessionLockedException:
                      time.sleep(1)
  
          # One last try
          return self._lock()
  
      def _lock(self):
          sess = self._run_commands(["show configuration sessions"])[0]["sessions"]
          if [
              k
              for k, v in sess.items()
              if v["state"] == "pending" and k != self.config_session
          ]:
              raise SessionLockedException("Session is already in use")
  
      def _get_pending_commits(self):
          """
          Return a dictionary of configuration sessions with pending commit confirms and
          corresponding time when confirm needs to happen by (rounded to nearest second).
  
          Example:
          {'napalm_607123': 522}
          """
          config_sessions = self._run_commands(["show configuration sessions detail"])
          config_sessions = config_sessions[0]["sessions"]
          # Arista reports the commitBy time relative to uptime of the box... :-(
          uptime = self._run_commands(["show version"])
          uptime = uptime[0].get("uptime", -1)
  
          pending_commits = {}
          for session_name, session_dict in config_sessions.items():
              if "pendingCommitTimer" in session_dict["state"]:
                  commit_by = session_dict.get("commitBy", -1)
                  # Set to -1 if something went wrong in the calculation.
                  if commit_by == -1 or uptime == -1:
                      pending_commits[session_name] = -1
                  elif uptime >= commit_by:
                      pending_commits[session_name] = -1
                  else:
                      confirm_by_seconds = commit_by - uptime
                      pending_commits[session_name] = round(confirm_by_seconds)
  
          return pending_commits
  
      @staticmethod
      def _multiline_convert(config, start="banner login", end="EOF", depth=1):
          """Converts running-config HEREDOC into EAPI JSON dict"""
          ret = list(config)  # Don't modify list in-place
          try:
              s = ret.index(start)
              e = s
              while depth:
                  e = ret.index(end, e + 1)
                  depth = depth - 1
          except ValueError:  # Couldn't find end, abort
              return ret
          ret[s] = {"cmd": ret[s], "input": "\n".join(ret[s + 1 : e])}
          del ret[s + 1 : e + 1]
  
          return ret
  
      @staticmethod
      def _mode_comment_convert(commands):
          """
          EOS has the concept of multi-line mode comments, shown in the running-config
          as being inside a config stanza (router bgp, ACL definition, etc) and beginning
          with the normal level of spaces and '!!', followed by comments.
  
          Unfortunately, pyeapi does not accept mode comments in this format, and have to be
          converted to a specific type of pyeapi call that accepts multi-line input
  
          Copy the config list into a new return list, converting consecutive lines starting with
          "!!" into a single multiline comment command
  
          :param commands: List of commands to be sent to pyeapi
          :return: Converted list of commands to be sent to pyeapi
          """
  
          ret = []
          comment_count = 0
          for idx, element in enumerate(commands):
              # Check first for stringiness, as we may have dicts in the command list already
              if isinstance(element, str) and element.startswith("!!"):
                  comment_count += 1
                  continue
              else:
                  if comment_count > 0:
                      # append the previous comment
                      ret.append(
                          {
                              "cmd": "comment",
                              "input": "\n".join(
                                  map(
                                      lambda s: s.lstrip("! "),
                                      commands[idx - comment_count : idx],
                                  )
                              ),
                          }
                      )
                      comment_count = 0
                  ret.append(element)
  
          return ret
  
      def _load_config(self, filename=None, config=None, replace=True):
          if self.config_session is None:
              self.config_session = "napalm_{}".format(datetime.now().microsecond)
  
          if not self.lock_disable:
              self._obtain_lock(wait_time=10)
  
          commands = []
          commands.append("configure session {}".format(self.config_session))
          if replace:
              commands.append("rollback clean-config")
  
          if filename is not None:
              with open(filename, "r") as f:
                  lines = f.readlines()
          else:
              if isinstance(config, list):
                  lines = config
              else:
                  lines = config.splitlines()
  
          for line in lines:
              line = line.strip()
              if line.startswith("!") and not line.startswith("!!"):
                  continue
              commands.append(line)
  
          if self.transport != "ssh":
              for start, depth in [
                  (s, d) for (s, d) in self.HEREDOC_COMMANDS if s in commands
              ]:
                  commands = self._multiline_convert(commands, start=start, depth=depth)
  
              commands = self._mode_comment_convert(commands)
  
          if self.transport == "ssh":
              try:
                  self.device.send_command_expect(commands[0], expect_string=r"\)#")
                  self.device.send_config_set(commands, enter_config_mode=False)
              except ConfigInvalidException as e:
                  self.discard_config()
                  msg = str(e)
                  if replace:
                      raise ReplaceConfigException(msg)
                  else:
                      raise MergeConfigException(msg)
              return None
  
          try:
              if not any(cmd == "end" for cmd in commands):
                  commands.append("end")  # exit config mode
              if self.eos_autoComplete is not None:
                  self._run_commands(
                      commands,
                      autoComplete=self.eos_autoComplete,
                  )
              else:
                  self._run_commands(commands)
          except pyeapi.eapilib.CommandError as e:
              self.discard_config()
              msg = str(e)
              if replace:
                  raise ReplaceConfigException(msg)
              else:
                  raise MergeConfigException(msg)
  
      def load_replace_candidate(self, filename=None, config=None):
          """Implementation of NAPALM method load_replace_candidate."""
          self._load_config(filename, config, True)
  
      def load_merge_candidate(self, filename=None, config=None):
          """Implementation of NAPALM method load_merge_candidate."""
          self._load_config(filename, config, False)
  
      def compare_config(self):
          """Implementation of NAPALM method compare_config."""
          if self.config_session is None:
              return ""
          else:
              commands = ["show session-config named %s diffs" % self.config_session]
              result = self._run_commands(commands, encoding="text")[0]["output"]
  
              result = "\n".join(result.splitlines()[2:])
  
              return result.strip()
  
      def commit_config(self, message="", revert_in=None):
          """Implementation of NAPALM method commit_config."""
  
          if message:
              raise NotImplementedError(
                  "Commit message not implemented for this platform"
              )
  
          if revert_in is not None:
              if self.has_pending_commit():
                  raise CommitError("Pending commit confirm already in process!")
  
              commands = [
                  "copy startup-config flash:rollback-0",
                  "configure session {} commit timer {}".format(
                      self.config_session,
                      time.strftime("%H:%M:%S", time.gmtime(revert_in)),
                  ),
              ]
              self._run_commands(commands, encoding="text")
          else:
              commands = [
                  "copy startup-config flash:rollback-0",
                  "configure session {} commit".format(self.config_session),
                  "write memory",
              ]
  
              self._run_commands(commands, encoding="text")
              self.config_session = None
  
      def has_pending_commit(self):
          """Boolean indicating if there is a commit-confirm in process."""
          pending_commits = self._get_pending_commits()
          # pending_commits will return an empty dict, if there are no commit-confirms pending.
          return bool(pending_commits)
  
      def confirm_commit(self):
          """Send final commit to confirm an in-proces commit that requires confirmation."""
          pending_commits = self._get_pending_commits()
          # The specific 'config_session' must show up as a pending commit.
          if pending_commits.get(self.config_session):
              commands = [
                  "configure session {} commit".format(self.config_session),
                  "write memory",
              ]
              self._run_commands(commands, encoding="text")
              self.config_session = None
          else:
              raise CommitError("No pending commit-confirm found!")
  
      def discard_config(self):
          """Implementation of NAPALM method discard_config."""
          if self.config_session is not None:
              try:
                  commands = [f"configure session {self.config_session} abort"]
                  self._run_commands(commands, encoding="text")
              except Exception:
                  # If discard fails, you might want to invalidate the config_session (esp. Salt)
                  # The config_session in EOS is used as the config lock.
                  if self.force_cfg_session_invalid:
                      self.config_session = None
                  raise
              self.config_session = None
  
      def rollback(self):
          """Implementation of NAPALM method rollback."""
  
          # Commit-confirm check and abort
          pending_commits = self._get_pending_commits()
          if pending_commits:
              # Make sure pending commit matches self.config_session
              if pending_commits.get(self.config_session):
                  commands = [
                      "configure session {} abort".format(self.config_session),
                      "write memory",
                  ]
              else:
                  msg = "Current config session not found as pending commit-confirm"
                  raise CommitError(msg)
  
          # Standard rollback
          else:
              commands = ["configure replace flash:rollback-0", "write memory"]
  
          self._run_commands(commands, encoding="text")
          self.config_session = None
  
      def get_facts(self):
          """Implementation of NAPALM method get_facts."""
          commands = ["show version", "show hostname", "show interfaces"]
  
          result = self._run_commands(commands)
  
          version = result[0]
          hostname = result[1]
          interfaces_dict = result[2]["interfaces"]
  
          uptime = time.time() - version["bootupTimestamp"]
  
          interfaces = [i for i in interfaces_dict.keys() if "." not in i]
          interfaces = string_parsers.sorted_nicely(interfaces)
  
          return {
              "hostname": hostname["hostname"],
              "fqdn": hostname["fqdn"],
              "vendor": "Arista",
              "model": version["modelName"],
              "serial_number": version["serialNumber"],
              "os_version": version["internalVersion"],
              "uptime": float(uptime),
              "interface_list": interfaces,
          }
  
      def get_interfaces(self):
          commands = ["show interfaces"]
          output = self._run_commands(commands)[0]
  
          interfaces = {}
  
          for interface, values in output["interfaces"].items():
              interfaces[interface] = {}
  
              if values["lineProtocolStatus"] == "up":
                  interfaces[interface]["is_up"] = True
                  interfaces[interface]["is_enabled"] = True
              else:
                  interfaces[interface]["is_up"] = False
                  if values["interfaceStatus"] == "disabled":
                      interfaces[interface]["is_enabled"] = False
                  else:
                      interfaces[interface]["is_enabled"] = True
  
              interfaces[interface]["description"] = values["description"]
  
              interfaces[interface]["last_flapped"] = values.pop(
                  "lastStatusChangeTimestamp", -1.0
              )
  
              interfaces[interface]["mtu"] = int(values["mtu"])
              #            interfaces[interface]["speed"] = float(values["bandwidth"] * 1e-6)
              interfaces[interface]["speed"] = float(values["bandwidth"] / 1000000.0)
              interfaces[interface]["mac_address"] = napalm.base.helpers.convert(
                  napalm.base.helpers.mac, values.pop("physicalAddress", "")
              )
  
          return interfaces
  
      def get_lldp_neighbors(self):
          commands = ["show lldp neighbors"]
          output = self._run_commands(commands)[0]["lldpNeighbors"]
  
          lldp = {}
  
          for n in output:
              if n["port"] not in lldp.keys():
                  lldp[n["port"]] = []
  
              lldp[n["port"]].append(
                  {"hostname": n["neighborDevice"], "port": n["neighborPort"]}
              )
  
          return lldp
  
      def get_interfaces_counters(self):
          commands = ["show interfaces"]
          output = self._run_commands(commands)
          interface_counters = defaultdict(dict)
          for interface, data in output[0]["interfaces"].items():
              if data["hardware"] == "subinterface":
                  # Subinterfaces will never have counters so no point in parsing them at all
                  continue
              counters = data.get("interfaceCounters", {})
              interface_counters[interface].update(
                  tx_octets=counters.get("outOctets", -1),
                  rx_octets=counters.get("inOctets", -1),
                  tx_unicast_packets=counters.get("outUcastPkts", -1),
                  rx_unicast_packets=counters.get("inUcastPkts", -1),
                  tx_multicast_packets=counters.get("outMulticastPkts", -1),
                  rx_multicast_packets=counters.get("inMulticastPkts", -1),
                  tx_broadcast_packets=counters.get("outBroadcastPkts", -1),
                  rx_broadcast_packets=counters.get("inBroadcastPkts", -1),
                  tx_discards=counters.get("outDiscards", -1),
                  rx_discards=counters.get("inDiscards", -1),
                  tx_errors=counters.get("totalOutErrors", -1),
                  rx_errors=counters.get("totalInErrors", -1),
              )
          return interface_counters
  
      def get_bgp_neighbors(self) -> Dict[str, models.BGPStateNeighborsPerVRFDict]:
          cmd_outputs = self._run_commands(
              [
                  "show ip bgp summary vrf all",
                  "show ipv6 bgp summary vrf all",
                  "show ip bgp neighbors vrf all",
                  "show ipv6 bgp peers vrf all",
              ],
              encoding="json",
          )
  
          bgp_counters = defaultdict(
              lambda: models.BGPStateNeighborsPerVRFDict(
                  peers=models.BGPStateNeighborDict()  # type: ignore
              )  # type: ignore
          )  # type: ignore
          # Iterate IPv4 and IPv6 neighbor details
          for cmd in cmd_outputs[2:]:
              for vrf_name, vrf_data in cmd["vrfs"].items():
                  vrf = bgp_counters[vrf_name]
                  for peer in vrf_data["peerList"]:
                      peer_ip = napalm.base.helpers.ip(peer["peerAddress"])
                      v4_summary = cmd_outputs[0]["vrfs"][vrf_name]["peers"].get(
                          peer_ip, {}
                      )
                      v6_summary = cmd_outputs[1]["vrfs"][vrf_name]["peers"].get(
                          peer_ip, {}
                      )
                      local_as = napalm.base.helpers.as_number(peer["localAsn"])
                      remote_as = napalm.base.helpers.as_number(peer["asn"])
                      remote_id = napalm.base.helpers.ip(peer["routerId"])
                      if peer["state"] == "Idle":
                          is_enabled = (
                              True
                              if peer["idleReason"] != "Administratively shut down"
                              else False
                          )
                      else:
                          is_enabled = True
                      is_up = peer["state"] == "Established"
                      description = peer.get("description", "")
                      uptime = int(peer.get("establishedTime", -1))
                      v4: models.BGPStateAddressFamilyDict = {
                          "received_prefixes": peer["prefixesReceived"],
                          "accepted_prefixes": (
                              v4_summary["prefixAccepted"] if v4_summary else 0
                          ),
                          "sent_prefixes": peer["prefixesSent"],
                      }
                      v6: models.BGPStateAddressFamilyDict = {
                          "received_prefixes": peer["v6PrefixesReceived"],
                          "accepted_prefixes": (
                              v6_summary["prefixAccepted"] if v6_summary else 0
                          ),
                          "sent_prefixes": peer["v6PrefixesSent"],
                      }
                      peer_data: models.BGPStateNeighborDict = {
                          "local_as": local_as,
                          "remote_as": remote_as,
                          "remote_id": remote_id,
                          "is_up": is_up,
                          "is_enabled": is_enabled,
                          "description": description,
                          "uptime": uptime,
                          "address_family": {
                              "ipv4": v4,
                              "ipv6": v6,
                          },
                      }
                      vrf["peers"][peer_ip] = peer_data
  
          # Iterate IPv4 and IPv6 summary details for router-id assignment
          for cmd in cmd_outputs[:2]:
              for vrf_name, vrf_data in cmd["vrfs"].items():
                  bgp_counters[vrf_name]["router_id"] = napalm.base.helpers.ip(
                      vrf_data["routerId"]
                  )
  
          if "default" in bgp_counters:
              bgp_counters["global"] = bgp_counters.pop("default")
          return dict(bgp_counters)
  
      def get_environment(self):
          def extract_temperature_data(data):
              for s in data:
                  temp = s["currentTemperature"] if "currentTemperature" in s else 0.0
                  name = s["name"]
                  values = {
                      "temperature": temp,
                      "is_alert": temp > s["overheatThreshold"],
                      "is_critical": temp > s["criticalThreshold"],
                  }
                  yield name, values
  
          sh_version_out = self._run_commands(["show version"])
          is_veos = sh_version_out[0]["modelName"].lower() in ["veos", "ceoslab"]
          commands = [
              "show system environment cooling",
              "show system environment temperature",
          ]
          if not is_veos:
              commands.append("show system environment power")
              fans_output, temp_output, power_output = self._run_commands(commands)
          else:
              fans_output, temp_output = self._run_commands(commands)
          environment_counters = {"fans": {}, "temperature": {}, "power": {}, "cpu": {}}
          cpu_output = self._run_commands(["show processes top once"], encoding="text")[
              0
          ]["output"]
          for slot in fans_output["fanTraySlots"]:
              environment_counters["fans"][slot["label"]] = {
                  "status": slot["status"] == "ok"
              }
          # First check FRU's
          for fru_type in ["cardSlots", "powerSupplySlots"]:
              for fru in temp_output[fru_type]:
                  t = {
                      name: value
                      for name, value in extract_temperature_data(fru["tempSensors"])
                  }
                  environment_counters["temperature"].update(t)
          # On board sensors
          parsed = {n: v for n, v in extract_temperature_data(temp_output["tempSensors"])}
          environment_counters["temperature"].update(parsed)
          if not is_veos:
              for psu, data in power_output["powerSupplies"].items():
                  environment_counters["power"][psu] = {
                      "status": data.get("state", "ok") == "ok",
                      "capacity": data.get("capacity", -1.0),
                      "output": data.get("outputPower", -1.0),
                  }
          cpu_lines = cpu_output.splitlines()
          # Matches either of
          # Cpu(s):  5.2%us,  1.4%sy,  0.0%ni, 92.2%id,  0.6%wa,  0.3%hi,  0.4%si,  0.0%st ( 4.16 > )
          # %Cpu(s):  4.2 us,  0.9 sy,  0.0 ni, 94.6 id,  0.0 wa,  0.1 hi,  0.2 si,  0.0 st ( 4.16 < )
          m = re.match(".*ni, (?P<idle>.*).id.*", cpu_lines[2])
          environment_counters["cpu"][0] = {
              "%usage": round(100 - float(m.group("idle")), 1)
          }
          # Matches either of
          # Mem:   3844356k total,  3763184k used,    81172k free,    16732k buffers ( 4.16 > )
          # KiB Mem:  32472080 total,  5697604 used, 26774476 free,   372052 buffers ( 4.16 < )
          # MiB Mem :   3889.2 total,    150.3 free,   1104.5 used,   2634.4 buff/cache (4.27 < )
          mem_regex = (
              r"^(?:(?P<unit>\S+)\s+)?Mem\s*:"
              r"\s+(?P<total>[0-9.]+)[k\s]+total,"
              r"(?:\s+(?P<used1>[0-9.]+)[k\s]+used,)?"
              r"\s+(?P<free>[0-9.]+)[k\s]+free,"
              r"(?:\s+(?P<used2>[0-9.]+)[k\s]+used,)?"
              r".*"
          )
          m = re.match(mem_regex, cpu_lines[3])
  
          def _parse_memory(unit, total, used):
              if unit == "MiB":
                  return {
  