text: import logging
  import re
  
  import pynetbox
  
  from netbox_agent.config import config
  from netbox_agent.config import netbox_instance as nb
  from netbox_agent.lshw import LSHW
  from netbox_agent.misc import get_vendor, is_tool
  from netbox_agent.raid.hp import HPRaid
  from netbox_agent.raid.omreport import OmreportRaid
  from netbox_agent.raid.storcli import StorcliRaid
  
  INVENTORY_TAG = {
      'cpu': {'name': 'hw:cpu', 'slug': 'hw-cpu'},
      'disk': {'name': 'hw:disk', 'slug': 'hw-disk'},
      'interface': {'name': 'hw:interface', 'slug': 'hw-interface'},
      'memory': {'name': 'hw:memory', 'slug': 'hw-memory'},
      'motherboard': {'name': 'hw:motherboard', 'slug': 'hw-motherboard'},
      'raid_card': {'name': 'hw:raid_card', 'slug': 'hw-raid-card'},
  }
  
  
  class Inventory():
      """
      Better Inventory items coming, see:
      - https://github.com/netbox-community/netbox/issues/3087
      - https://github.com/netbox-community/netbox/issues/3333
  
      This class implements for:
      * memory
      * cpu
      * raid cards
      * disks
  
      methods that:
      * get local item
      * get netbox item
      * create netbox item
      * update netbox item
  
      Known issues:
      - no scan of non-raid devices
      - no scan of NVMe devices
      """
  
      def __init__(self, server):
          self.create_netbox_tags()
          self.server = server
          netbox_server = self.server.get_netbox_server()
  
          self.device_id = netbox_server.id if netbox_server else None
          self.raid = None
          self.disks = []
  
          self.lshw = LSHW()
  
      def create_netbox_tags(self):
          for key, tag in INVENTORY_TAG.items():
              nb_tag = nb.extras.tags.get(
                  name=tag['name']
              )
              if not nb_tag:
                  nb_tag = nb.extras.tags.create(
                      name=tag['name'],
                      slug=tag['slug'],
                      comments=tag['name'],
                  )
  
      def find_or_create_manufacturer(self, name):
          if name is None:
              return None
  
          manufacturer = nb.dcim.manufacturers.get(
              name=name,
          )
          if not manufacturer:
              logging.info('Creating missing manufacturer {name}'.format(name=name))
              manufacturer = nb.dcim.manufacturers.create(
                  name=name,
                  slug=re.sub('[^A-Za-z0-9]+', '-', name).lower(),
              )
  
              logging.info('Creating missing manufacturer {name}'.format(name=name))
  
          return manufacturer
  
      def get_netbox_inventory(self, device_id, tag):
          try:
              items = nb.dcim.inventory_items.filter(
                  device_id=device_id,
                  tag=tag
              )
          except pynetbox.core.query.RequestError:
              logging.info('Tag {tag} is missing, returning empty array.'.format(tag=tag))
              items = []
  
          return items
  
      def create_netbox_inventory_item(self, device_id, tags, vendor, name, serial, description):
          manufacturer = self.find_or_create_manufacturer(vendor)
  
          _ = nb.dcim.inventory_items.create(
              device=device_id,
              manufacturer=manufacturer.id,
              discovered=True,
              tags=tags,
              name='{}'.format(name),
              serial='{}'.format(serial),
              description=description
          )
  
          logging.info('Creating inventory item {} {}/{} {} '.format(
              vendor,
              name,
              serial,
              description)
          )
  
      def get_hw_motherboards(self):
          motherboards = []
  
          m = {}
          m['serial'] = self.lshw.motherboard_serial
          m['vendor'] = self.lshw.vendor
          m['name'] = '{} {}'.format(self.lshw.vendor, self.lshw.motherboard)
          m['description'] = '{} Motherboard'.format(self.lshw.motherboard)
  
          motherboards.append(m)
  
          return motherboards
  
      def do_netbox_motherboard(self):
  
          motherboards = self.get_hw_motherboards()
          nb_motherboards = self.get_netbox_inventory(
              device_id=self.device_id,
              tag=INVENTORY_TAG['motherboard']['slug'])
  
          for nb_motherboard in nb_motherboards:
              if nb_motherboard.serial not in [x['serial'] for x in motherboards]:
                  logging.info('Deleting unknown motherboard {vendor} {motherboard}/{serial}'.format(
                      motherboard=self.lshw.motherboard,
                      serial=nb_motherboard.serial,
                  ))
                  nb_motherboard.delete()
  
          # create interfaces that are not in netbox
          for motherboard in motherboards:
              if motherboard.get('serial') not in [x.serial for x in nb_motherboards]:
                  self.create_netbox_inventory_item(
                      device_id=self.device_id,
                      tags=[INVENTORY_TAG['motherboard']['name']],
                      vendor='{}'.format(motherboard.get('vendor', 'N/A')),
                      serial='{}'.format(motherboard.get('serial', 'No SN')),
                      name='{}'.format(motherboard.get('name')),
                      description='{}'.format(motherboard.get('description'))
                  )
  
      def create_netbox_interface(self, iface):
          manufacturer = self.find_or_create_manufacturer(iface["vendor"])
          _ = nb.dcim.inventory_items.create(
              device=self.device_id,
              manufacturer=manufacturer.id,
              discovered=True,
              tags=[INVENTORY_TAG['interface']['name']],
              name="{}".format(iface['product']),
              serial='{}'.format(iface['serial']),
              description='{} {}'.format(iface['description'], iface['name'])
          )
  
      def do_netbox_interfaces(self):
          nb_interfaces = self.get_netbox_inventory(
              device_id=self.device_id,
              tag=INVENTORY_TAG['interface']['slug'])
          interfaces = self.lshw.interfaces
  
          # delete interfaces that are in netbox but not locally
          # use the serial_number has the comparison element
          for nb_interface in nb_interfaces:
              if nb_interface.serial not in [x['serial'] for x in interfaces]:
                  logging.info('Deleting unknown interface {serial}'.format(
                      serial=nb_interface.serial,
                  ))
                  nb_interface.delete()
  
          # create interfaces that are not in netbox
          for iface in interfaces:
              if iface.get('serial') not in [x.serial for x in nb_interfaces]:
                  self.create_netbox_interface(iface)
  
      def create_netbox_cpus(self):
          for cpu in self.lshw.get_hw_linux('cpu'):
              manufacturer = self.find_or_create_manufacturer(cpu["vendor"])
              _ = nb.dcim.inventory_items.create(
                  device=self.device_id,
                  manufacturer=manufacturer.id,
                  discovered=True,
                  tags=[INVENTORY_TAG['cpu']['name']],
                  name=cpu['product'],
                  description='CPU {}'.format(cpu['location']),
                  # asset_tag=cpu['location']
              )
  
              logging.info('Creating CPU model {}'.format(cpu['product']))
  
      def do_netbox_cpus(self):
          cpus = self.lshw.get_hw_linux('cpu')
          nb_cpus = self.get_netbox_inventory(
              device_id=self.device_id,
              tag=INVENTORY_TAG['cpu']['slug'],
          )
  
          if not len(nb_cpus) or \
             len(nb_cpus) and len(cpus) != len(nb_cpus):
              for x in nb_cpus:
                  x.delete()
  
              self.create_netbox_cpus()
  
      def get_raid_cards(self):
          raid_class = None
          if self.server.manufacturer == 'Dell':
              if is_tool('omreport'):
                  raid_class = OmreportRaid
              if is_tool('storcli'):
                  raid_class = StorcliRaid
          elif self.server.manufacturer == 'HP':
              if is_tool('ssacli'):
                  raid_class = HPRaid
  
          if not raid_class:
              return []
  
          self.raid = raid_class()
          controllers = self.raid.get_controllers()
          if len(self.raid.get_controllers()):
              return controllers
  
      def create_netbox_raid_card(self, raid_card):
          manufacturer = self.find_or_create_manufacturer(
              raid_card.get_manufacturer()
          )
  
          name = raid_card.get_product_name()
          serial = raid_card.get_serial_number()
          nb_raid_card = nb.dcim.inventory_items.create(
              device=self.device_id,
              discovered=True,
              manufacturer=manufacturer.id if manufacturer else None,
              tags=[INVENTORY_TAG['raid_card']['name']],
              name='{}'.format(name),
              serial='{}'.format(serial),
              description='RAID Card',
          )
          logging.info('Creating RAID Card {name} (SN: {serial})'.format(
              name=name,
              serial=serial,
          ))
          return nb_raid_card
  
      def do_netbox_raid_cards(self):
          """
          Update raid cards in netbobx
          Since we only push:
          * Name
          * Manufacturer
          * Serial
  
          We only need to handle destroy and new cards
          """
  
          nb_raid_cards = self.get_netbox_inventory(
              device_id=self.device_id,
              tag=[INVENTORY_TAG['raid_card']['slug']]
          )
          raid_cards = self.get_raid_cards()
  
          # delete cards that are in netbox but not locally
          # use the serial_number has the comparison element
          for nb_raid_card in nb_raid_cards:
              if nb_raid_card.serial not in [x.get_serial_number() for x in raid_cards]:
                  logging.info('Deleting unknown locally RAID Card {serial}'.format(
                      serial=nb_raid_card.serial,
                  ))
                  nb_raid_card.delete()
  
          # create card that are not in netbox
          for raid_card in raid_cards:
              if raid_card.get_serial_number() not in [x.serial for x in nb_raid_cards]:
                  self.create_netbox_raid_card(raid_card)
  
      def is_virtual_disk(self, disk):
          logicalname = disk.get('logicalname')
          description = disk.get('description')
          size = disk.get('size')
          product = disk.get('product')
  
          non_raid_disks = [
              'MR9361-8i',
          ]
  
          if size is None and logicalname is None or \
             'virtual' in product.lower() or 'logical' in product.lower() or \
             product in non_raid_disks or \
             description == 'SCSI Enclosure' or \
             'volume' in description.lower():
              return True
          return False
  
      def get_hw_disks(self):
          disks = []
  
          for disk in self.lshw.get_hw_linux("storage"):
              if self.is_virtual_disk(disk):
                  continue
  
              logicalname = disk.get('logicalname')
              description = disk.get('description')
              size = disk.get('size', 0)
              product = disk.get('product')
              serial = disk.get('serial')
  
              d = {}
              d["name"] = ""
              d['Size'] = '{} GB'.format(int(size / 1024 / 1024 / 1024))
              d['logicalname'] = logicalname
              d['description'] = description
              d['SN'] = serial
              d['Model'] = product
              if disk.get('vendor'):
                  d['Vendor'] = disk['vendor']
              else:
                  d['Vendor'] = get_vendor(disk['product'])
              disks.append(d)
  
          for raid_card in self.get_raid_cards():
              disks += raid_card.get_physical_disks()
  
          # remove duplicate serials
          seen = set()
          uniq = [x for x in disks if x['SN'] not in seen and not seen.add(x['SN'])]
          return uniq
  
      def create_netbox_disk(self, disk):
          manufacturer = None
          if "Vendor" in disk:
              manufacturer = self.find_or_create_manufacturer(disk["Vendor"])
  
          logicalname = disk.get('logicalname')
          desc = disk.get('description')
          # nonraid disk
          if logicalname and desc:
              if type(logicalname) is list:
                  logicalname = logicalname[0]
              name = '{} - {} ({})'.format(
                  desc,
                  logicalname,
                  disk.get('Size', 0))
              description = 'Device {}'.format(disk.get('logicalname', 'Unknown'))
          else:
              name = '{} ({})'.format(disk['Model'], disk['Size'])
              description = '{}'.format(disk['Type'])
  
          _ = nb.dcim.inventory_items.create(
              device=self.device_id,
              discovered=True,
              tags=[INVENTORY_TAG['disk']['name']],
              name=name,
              serial=disk['SN'],
              part_id=disk['Model'],
              description=description,
              manufacturer=manufacturer.id if manufacturer else None
          )
  
          logging.info('Creating Disk {model} {serial}'.format(
              model=disk['Model'],
              serial=disk['SN'],
          ))
  
      def do_netbox_disks(self):
          nb_disks = self.get_netbox_inventory(
              device_id=self.device_id,
              tag=INVENTORY_TAG['disk']['slug'])
          disks = self.get_hw_disks()
  
          # delete disks that are in netbox but not locally
          # use the serial_number has the comparison element
          for nb_disk in nb_disks:
              if nb_disk.serial not in [x['SN'] for x in disks if x.get('SN')]:
                  logging.info('Deleting unknown locally Disk {serial}'.format(
                      serial=nb_disk.serial,
                  ))
                  nb_disk.delete()
  
          # create disks that are not in netbox
          for disk in disks:
              if disk.get('SN') not in [x.serial for x in nb_disks]:
                  self.create_netbox_disk(disk)
  
      def create_netbox_memory(self, memory):
          manufacturer = self.find_or_create_manufacturer(memory['vendor'])
          name = 'Slot {} ({}GB)'.format(memory['slot'], memory['size'])
          nb_memory = nb.dcim.inventory_items.create(
              device=self.device_id,
              discovered=True,
              manufacturer=manufacturer.id,
              tags=[INVENTORY_TAG['memory']['name']],
              name=name,
              part_id=memory['product'],
              serial=memory['serial'],
              description=memory['description'],
          )
  
          logging.info('Creating Memory {location} {type} {size}GB'.format(
              location=memory['slot'],
              type=memory['product'],
              size=memory['size'],
          ))
  
          return nb_memory
  
      def do_netbox_memories(self):
          memories = self.lshw.memories
          nb_memories = self.get_netbox_inventory(
              device_id=self.device_id,
              tag=INVENTORY_TAG['memory']['slug']
          )
  
          for nb_memory in nb_memories:
              if nb_memory.serial not in [x['serial'] for x in memories]:
                  logging.info('Deleting unknown locally Memory {serial}'.format(
                      serial=nb_memory.serial,
                  ))
                  nb_memory.delete()
  
          for memory in memories:
              if memory.get('serial') not in [x.serial for x in nb_memories]:
                  self.create_netbox_memory(memory)
  
      def create(self):
          if config.inventory is None:
              return False
          self.do_netbox_cpus()
          self.do_netbox_memories()
          self.do_netbox_raid_cards()
          self.do_netbox_disks()
          self.do_netbox_interfaces()
          self.do_netbox_motherboard()
          return True
  
      def update(self):
          if config.inventory is None or config.update_inventory is None:
              return False
          self.do_netbox_cpus()
          self.do_netbox_memories()
          self.do_netbox_raid_cards()
          self.do_netbox_disks()
          self.do_netbox_interfaces()
          self.do_netbox_motherboard()
          return True
  