I've rewritten zfs-snmp, testers wanted.

Status
Not open for further replies.
Joined
Feb 4, 2017
Messages
3
So I've spent some time rewriting the zfs-snmp module that's included with FreeNAS. I've fixed a couple of bugs and rewritten it to take advantage of the pass_persist method, this makes it significantly faster and consumes far fewer resources than the previous method. I've also added replication monitoring to it, I've done this to make use of at work but I figured I'd contribute it back.

There are a couple of issues though, primarily the version of snmp_passpersist that's included with FreeNAS 11 is not compatible with Python 3.6 and needs to be updated to the latest version available on GitHub, also I'm not sure what to do with the license on it, I based it on this example https://github.com/nagius/cxm/blob/master/misc/snmp_xen.py and it's licensed under GPL but the original script is BSD licensed.

I've attached the rewritten version, if anyone wants to test it I would appreciate it. To test it replace the file /usr/local/bin/freenas-snmp/zfs-snmp with zfs-snmp located in the attached zip file and change the line
pass .1.3.6.1.4.1.25359.1 in /usr/local/etc/snmpd.conf to pass_persist .1.3.6.1.4.1.25359.1 then restart the snmpd service

/usr/local/lib/python3.6/site-packages/snmp_passpersist.py will also need replaced with the attached version.

If anyone has anything else they would like added to it please let me know and I'll see what I can do. Also is there anything in particular I need to do to commit this to the FreeNAS GitHub once I feel it's ready?

Code for anyone who doesn't trust random zip files from internet strangers.

zfs-snmp
Code:
#!/usr/local/bin/python3.6 -u
# Copyright (c) 2012, Jakob Borg
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#	* Redistributions of source code must retain the above copyright
#	  notice, this list of conditions and the following disclaimer.
#	* Redistributions in binary form must reproduce the above copyright
#	  notice, this list of conditions and the following disclaimer in the
#	  documentation and/or other materials provided with the distribution.
#	* The name of the author may not be used to endorse or promote products
#	  derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY JAKOB BORG ''AS IS'' AND ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
# EVENT SHALL JAKOB BORG BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
# OF SUCH DAMAGE.
# Initial code taken from: https://github.com/jm66/solaris-extra-snmp

import errno
import json
import os
import re
import socket
import subprocess
import sys
import time

import django
import libzfs
import syslog

import snmp_passpersist as snmp

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'freenasUI.settings')
sys.path.append("/usr/local/www")

django.setup()

from freenasUI.storage.models import Replication
from freenasUI.tools.arc_summary import get_Kstat, get_arc_efficiency

POOLING_INTERVAL = 10  # Update timer, in second
MAX_RETRY = 10  # Number of successives retry in case of error
OID_BASE = '.1.3.6.1.4.1.25359.1'
pp = None
ARC = get_arc_efficiency(get_Kstat())
FREENASSNMPDSOCK = '/var/run/freenas-snmpd.sock'
size_dict = {
   "K": 1024,
   "M": 1048576,
   "G": 1073741824,
   "T": 1099511627776
}


def get_from_freenas_snmpd_sock(val_to_obtain):
   data = b''
   try:
	  s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
	  s.connect(FREENASSNMPDSOCK)
	  s.sendall(val_to_obtain)
	  while True:
		 text = s.recv(4096)
		 if text == b'':
			break
		 else:
			data += text
   except socket.error:
	  pass
   finally:
	  s.close()
   try:
	  data = json.loads(data.decode('utf8'))
   except (ValueError, UnicodeDecodeError):
	  data = {}
   return data


def update_data():
   global pp

   all_iostat = zpoolio_interval("all")
   onesec_iostat = zpoolio_interval(1)
   zfs = libzfs.ZFS()
   pools = [pool for pool in zfs.pools]
   datasets = []
   zvols = []
   qs = Replication.objects.filter(repl_enabled=True)
   pp.add_gau('2.1.0', zfs_arc_size())
   pp.add_gau('2.2.0', zfs_arc_meta())
   pp.add_gau('2.3.0', zfs_arc_data())
   pp.add_cnt_32bit('2.4.0', zfs_arc_hits())
   pp.add_cnt_32bit('2.5.0', zfs_arc_misses())
   pp.add_gau('2.6.0', zfs_arc_c())
   pp.add_gau('2.7.0', zfs_arc_p())
   pp.add_str('2.8.0', zfs_arc_miss_percent())
   pp.add_str('2.9.0', zfs_arc_cache_hit_ratio())
   pp.add_str('2.10.0', zfs_arc_cache_miss_ratio())
   pp.add_cnt_32bit('3.1.0', zfs_l2arc_hits())
   pp.add_cnt_32bit('3.2.0', zfs_l2arc_misses())
   pp.add_cnt_32bit('3.3.0', zfs_l2arc_read())
   pp.add_cnt_32bit('3.4.0', zfs_l2arc_write())
   pp.add_gau('3.5.0', zfs_l2arc_size())
   pp.add_cnt_64bit('6.1.0', zfs_zilstat_ops1())
   pp.add_cnt_64bit('6.2.0', zfs_zilstat_ops5())
   pp.add_cnt_64bit('6.3.0', zfs_zilstat_ops10())

   for res in map(zfs_segregate, [pool.root_dataset for pool in pools]):
	  # Excluding the first item in the datasets list as its always the root_dataset
	  # and we already have the stats on that (i.e. the pool!)
	  datasets.extend(res[0][1:])
	  zvols.extend(res[1])

   for i, zpool in enumerate(pools):
	  stri = str(i + 1)
	  pool = zpool.name
	  pool_health = zpool.properties['health'].value
	  # Dividing by 1024 to ge to KB
	  pool_used = unprettyprint(zpool.root_dataset.properties['used'].value) / 1024
	  pool_available = unprettyprint(zpool.root_dataset.properties['available'].value) / 1024
	  pool_size = unprettyprint(zpool.properties['size'].value) / 1024

	  pp.add_str('1.1.' + stri, pool)
	  pp.add_cnt_64bit('1.2.' + stri, pool_available)
	  pp.add_cnt_64bit('1.3.' + stri, pool_used)
	  pp.add_str('1.4.' + stri, pool_health)
	  pp.add_cnt_64bit('1.5.' + stri, pool_size)
	  pp.add_gau('1.12.' + stri, pool_available / 1024)
	  pp.add_gau('1.13.' + stri, pool_used / 1024)
	  pp.add_gau('1.14.' + stri, pool_size / 1024)
	  pp.add_cnt_64bit('1.15.' + stri, all_iostat[pool]['opread'])
	  pp.add_cnt_64bit('1.16.' + stri, all_iostat[pool]['opwrite'])
	  pp.add_cnt_64bit('1.17.' + stri, all_iostat[pool]['bwread'])
	  pp.add_cnt_64bit('1.18.' + stri, all_iostat[pool]['bwrite'])
	  pp.add_cnt_64bit('1.19.' + stri, onesec_iostat[pool].get('opread', 0))
	  pp.add_cnt_64bit('1.20.' + stri, onesec_iostat[pool].get('opwrite', 0))
	  pp.add_cnt_64bit('1.21.' + stri, onesec_iostat[pool].get('bwread', 0))
	  pp.add_cnt_64bit('1.22.' + stri, onesec_iostat[pool].get('bwrite', 0))

   for i, zvol in enumerate(zvols):
	  stri = str(i + 1)
	  volsize = unprettyprint(zvol.properties['volsize'].value) / 1024
	  vol_used = unprettyprint(zvol.properties['used'].value) / 1024
	  vol_available = unprettyprint(zvol.properties['available'].value) / 1024
	  pp.add_str('5.1.' + stri, zvol.name)
	  pp.add_cnt_64bit('5.2.' + stri, vol_available)
	  pp.add_cnt_64bit('5.3.' + stri, vol_used)
	  pp.add_cnt_64bit('5.4.' + stri, volsize)
	  pp.add_gau('5.12.' + stri, vol_available / 1024)
	  pp.add_gau('5.13.' + stri, vol_used / 1024)
	  pp.add_gau('5.14.' + stri, volsize / 1024)

   for i, ds in enumerate(datasets):
	  stri = str(i + 1)
	  ds_used = unprettyprint(ds.properties['used'].value) / 1024
	  ds_available = unprettyprint(ds.properties['available'].value) / 1024
	  ds_size = ds_used + ds_available
	  pp.add_str('7.1.' + stri, ds.name)
	  pp.add_cnt_64bit('7.2.' + stri, ds_available)
	  pp.add_cnt_64bit('7.3.' + stri, ds_used)
	  pp.add_cnt_64bit('7.4.' + stri, ds_size)
	  pp.add_gau('7.12.' + stri, ds_available / 1024)
	  pp.add_gau('7.13.' + stri, ds_used / 1024)
	  pp.add_gau('7.14.' + stri, ds_size / 1024)

   for i, repl in enumerate(qs):
	  stri = str(i + 1)
	  name = repl.repl_filesystem
	  status = repl.status
	  enabled = repl.repl_enabled
	  remote_addr = repl.repl_remote
	  remote_zfs = repl.repl_zfs
	  last = repl.repl_lastsnapshot
	  pp.add_str('8.1.' + stri, name)
	  pp.add_str('8.2.' + stri, '%s@%s' % (remote_zfs, remote_addr))
	  pp.add_str('8.3.' + stri, enabled)
	  pp.add_str('8.4.' + stri, status)
	  pp.add_str('8.5.' + stri, last)


class ArgumentValidationError(ValueError):
   """
   Raised when the type of an argument to a function is not what it should be.
   """

   def __init__(self, arg_num, func_name, accepted_arg_type):
	  self.error = 'The {0} argument of {1}() is not a {2}'.format(
		 arg_num, func_name, accepted_arg_type)

   def __str__(self):
	  return self.error


def unprettyprint(ster):
   """
   Method to convert 1K --> 1024 and so on...
   """
   num = 0.0
   try:
	  num = float(ster)
   except:
	  try:
		 num = float(ster[:-1]) * size_dict[ster[-1]]
	  except:
		 pass
   return int(num)


def kstat(name):
   output = subprocess.getoutput("sysctl kstat." + name)
   try:
	  return int(re.split("\s+", output)[1])
   except:
	  return 0


def zfs_arc_size():
   # KB
   return kstat("zfs.misc.arcstats.size") / 1024


def zfs_arc_data():
   # KB
   return kstat("zfs.misc.arcstats.data_size") / 1024


def zfs_arc_meta():
   # KB
   return kstat("zfs.misc.arcstats.arc_meta_used") / 1024


def zfs_arc_hits():
   # 32 bit counter
   return kstat("zfs.misc.arcstats.hits") % 2 ** 32


def zfs_arc_misses():
   # 32 bit counter
   return kstat("zfs.misc.arcstats.misses") % 2 ** 32


def zfs_arc_miss_percent():
   # percentage (floating point precision wrapped as a string)
   arc_hits = kstat("zfs.misc.arcstats.hits")
   arc_misses = kstat("zfs.misc.arcstats.misses")
   arc_read = arc_hits + arc_misses
   if (arc_read > 0):
	  hit_percent = float(100 * arc_hits / arc_read)
	  miss_percent = 100 - hit_percent
	  return str(miss_percent)
   return "0"


def zfs_arc_c():
   # KB
   return kstat("zfs.misc.arcstats.c") / 1024


def zfs_arc_p():
   # KB
   return kstat("zfs.misc.arcstats.p") / 1024


def zfs_arc_cache_hit_ratio():
   # percentage (floating point precision wrapped as a string)
   return ARC['cache_hit_ratio']['per'][:-1]


def zfs_arc_cache_miss_ratio():
   # percentage (floating point precision wrapped as a string)
   return ARC['cache_miss_ratio']['per'][:-1]


def zilstatd_ops(interval):
   res = 0
   FSNMPDATA = get_from_freenas_snmpd_sock(b"get_all")
   try:
	  res = FSNMPDATA["zil_data"][str(interval)]['ops']
   except KeyError:
	  pass
   return res


# Note: Currently only 1 second interval and "all" is supported
# to add more make the appropriate worker in gui/tools/freenas-snmpd.py
def zpoolio_interval(interval):
   res = {}
   FSNMPDATA = get_from_freenas_snmpd_sock(b"get_all")
   try:
	  res = FSNMPDATA["zpool_data"][str(interval)]
   except KeyError:
	  pass
   return res


def zfs_zilstat_ops1():
   return zilstatd_ops(1)


def zfs_zilstat_ops5():
   return zilstatd_ops(5)


def zfs_zilstat_ops10():
   return zilstatd_ops(10)


def zfs_l2arc_size():
   # KB
   return kstat("zfs.misc.arcstats.l2_size") / 1024


def zfs_l2arc_hits():
   # 32 bit counter
   return kstat("zfs.misc.arcstats.l2_hits") % 2 ** 32


def zfs_l2arc_misses():
   # 32 bit counter
   return kstat("zfs.misc.arcstats.l2_misses") % 2 ** 32


def zfs_l2arc_write():
   # 32 bit KB counter
   return kstat("zfs.misc.arcstats.l2_write_bytes") / 1024 % 2 ** 32


def zfs_l2arc_read():
   # 32 bit KB counter
   return kstat("zfs.misc.arcstats.l2_read_bytes") / 1024 % 2 ** 32


def zfs_segregate(zfs_dataset):
   """
   A function to obtain and segregte all the datsets (children) and zvols
   in the provided (`zfs_dataset`) dataset. The best example to use this
   is to provide it with a zfs pool's root dataset and it will return a
   a tuple of (datsets, zvols) where each of them is a list.

   Note: Please make sure that the input to this function (`zfs_dataset`)
   is of type: libzfs.ZFSDataset
   """
   if type(zfs_dataset) is not libzfs.ZFSDataset:
	  raise ArgumentValidationError(1, 'zfs_segregate', libzfs.ZFSDataset)
   zvols = []
   datasets = []
   if zfs_dataset.properties['type'].value == 'volume':
	  # since zvols do not have children lets just return now
	  return datasets, [zfs_dataset]
   else:
	  datasets = [zfs_dataset]
   for x, y in map(zfs_segregate, list(zfs_dataset.children)):
	  datasets.extend(x)
	  zvols.extend(y)
   return datasets, zvols


def main():
   global pp
   retry_timestamp = int(time.time())
   retry_counter = MAX_RETRY
   while retry_counter > 0:
	  try:
		 syslog.syslog(syslog.LOG_WARNING, "Starting FreeNAS monitoring...")

		 # Load helpers
		 pp = snmp.PassPersist(OID_BASE)
		 pp.start(update_data, POOLING_INTERVAL)
	  except KeyboardInterrupt:
		 print("Exiting on user request.")
		 sys.exit(0)
	  except IOError as e:
		 if e.errno == errno.EPIPE:
			syslog.syslog(syslog.LOG_INFO, "Snmpd has close pipe, exiting...")
			sys.exit(0)
		 else:
			syslog.syslog(syslog.LOG_WARNING, "Updater thread has died: IOError: %s" % e)
	  except Exception as e:
		 syslog.syslog(syslog.LOG_WARNING, "Main thread has died: %s: %s" % (e.__class__.__name__, e))
	  else:
		 syslog.syslog(syslog.LOG_WARNING, "Updater thread has died: %s" % pp.error)

	  syslog.syslog(syslog.LOG_WARNING, "Restarting monitoring in 15 sec...")
	  time.sleep(15)

	  # Errors frequency detection
	  now = int(time.time())
	  if (now - 3600) > retry_timestamp:  # If the previous error is older than 1H
		 retry_counter = MAX_RETRY  # Reset the counter
	  else:
		 retry_counter -= 1  # Else countdown
	  retry_timestamp = now
   syslog.syslog(syslog.LOG_ERR, "Too many retries, aborting!")
   sys.exit(1)


if __name__ == "__main__":
   main()


snmp_passpersist.py
Code:
# -*- coding:utf-8 -*-

# snmp_passpersist.py - SNMP passPersist backend for Net-SNMP
# Copyleft 2010-2013 - Nicolas AGIUS <nicolas.agius@lps-it.fr>

###########################################################################
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
###########################################################################

"""
This 'snmp_passpersist' module is a python backend for snmp's "pass_persist" function.

It is critical that the python interpreter be invoked with unbuffered STDIN and
STDOUT by use of the -u switch in the shebang line.

All the methods are in the PassPersist class.
"""

import sys, time, threading, os

__all__ = ["Error", "ErrorValues", "Type", "TypeValues", "PassPersist"]

__author__ = "Nicolas Agius"
__license__ = "GPL"
__version__ = "1.3.0"
__email__ = "nicolas.agius@lps-it.fr"
__status__ = "Production"


class Error(object):
   """
   SET command requests errors.
   As listed in the man snmpd.conf(5) page
   """
   NotWritable = 'not-writable'
   WrongType = 'wrong-type'
   WrongValue = 'wrong-value'
   WrongLength = 'wrong-length'
   InconsistentValue = 'inconsistent-value'


ErrorValues = Error.__dict__.values()


class Type:
   """
   SET command requests value types.
   As listed in the man snmpd.conf(5) page
   """
   Integer = 'integer'
   Gauge = 'gauge'
   Counter = 'counter'
   TimeTicks = 'timeticks'
   IPAddress = 'ipaddress'
   OID = 'objectid'
   ObjectID = 'objectid'
   String = 'string'
   Octet = 'octet'


TypeValues = Type.__dict__.values()


class ResponseError(ValueError):
   """
   Wrong user function
   """


class PassPersist:
   """
   This class present a convenient way to creare a MIB subtree and expose it to snmp via it's passpersist protocol.
   Two thread are used, one for talking with snmpd and a second that trigger the update process at a fixed interval.

   The keyword 'DUMP' has been added to the protocol for testing purpose.

   Usage example: in a file /path/to/your/script.py :

   > #!/usr/bin/python -u
   > import snmp_passpersist as snmp
   >
   > def update():
   >  pp.add_int('0.1',123)
   >
   > pp=snmp.PassPersist(".1.3.6.1.3.53.8")
   > pp.start(update,30) # Every 30s

   With the folowing line in snmpd.conf :

   pass_persist	.1.3.6.1.3.53.8.0	 /path/to/your/script.py

   """

   @staticmethod
   def encode(string):
	  """
	  Encode the given string as an OID.

	  >>> import snmp_passpersist as snmp
	  >>> snmp.PassPersist.encode("hello")
	  '5.104.101.108.108.111'
	  >>>
	  """

	  result = ".".join([str(ord(s)) for s in string])
	  return "%s." % (len(string)) + result

   def __init__(self, base_oid):
	  """
	  Initialize internals structures.
	  base_oid is the OID prefix used for all entry (the root of the MIB tree).
	  """

	  self.data = dict()
	  self.data_idx = list()
	  self.pending = dict()
	  self.lock = threading.RLock()
	  if not base_oid.endswith("."):
		 base_oid += "."
	  self.base_oid = base_oid
	  self.setter = dict()
	  self.debug = False

   # The data structure is a dict that hold the unsorted MIB tree like this :
   # data = {
   #  '1.1': { 'type':'INTEGER', 'value':4 },
   #  '1.3.2.1':{ 'type':'STRING', 'value':'vm1' }
   #  }

   def get(self, oid):
	  """Return snmp value for the given OID."""
	  try:
		 self.lock.acquire()
		 if oid not in self.data:
			return "NONE"
		 else:
			return self.base_oid + oid + '\n' + self.data[oid]['type'] + '\n' + str(self.data[oid]['value'])
	  finally:
		 self.lock.release()

   def get_next(self, oid):
	  """Return snmp value for the next OID."""
	  try:  # Nested try..except because of Python 2.4
		 self.lock.acquire()
		 try:
			# remove trailing zeroes from the oid
			while len(oid) > 0 and oid[-2:] == ".0" and oid not in self.data:
			   oid = oid[:-2];
			return self.get(self.data_idx[self.data_idx.index(oid) + 1])
		 except ValueError:
			# Not found: try to match partial oid
			for real_oid in self.data_idx:
			   if real_oid.startswith(oid):
				  return self.get(real_oid)
			return "NONE"  # Unknown OID
		 except IndexError:
			return "NONE"  # End of MIB
	  finally:
		 self.lock.release()

   def get_first(self):
	  """Return snmp value for the first OID."""
	  try:  # Nested try..except because of Python 2.4
		 self.lock.acquire()
		 try:
			return self.get(self.data_idx[0])
		 except (IndexError, ValueError):
			return "NONE"
	  finally:
		 self.lock.release()

   def cut_oid(self, full_oid):
	  """
	  Remove the base OID from the given string.

	  >>> import snmp_passpersist as snmp
	  >>> pp=snmp.PassPersist(".1.3.6.1.3.53.8")
	  >>> pp.cut_oid(".1.3.6.1.3.53.8.28.12")
	  '28.12'
	  """
	  if not full_oid.startswith(self.base_oid.rstrip('.')):
		 return None
	  else:
		 return full_oid[len(self.base_oid):]

   def add_oid_entry(self, oid, type, value):
	  """General function to add an oid entry to the MIB subtree."""
	  if self.debug:
		 print('DEBUG: %s %s %s' % (oid, type, value))
	  self.pending[oid] = {'type': str(type), 'value': str(value)}

   def add_oid(self, oid, value):
	  """Short helper to add an object ID value to the MIB subtree."""
	  self.add_oid_entry(oid, 'OBJECTID', value)

   def add_int(self, oid, value):
	  """Short helper to add an integer value to the MIB subtree."""
	  self.add_oid_entry(oid, 'INTEGER', value)

   def add_oct(self, oid, value):
	  """Short helper to add an octet value to the MIB subtree."""
	  self.add_oid_entry(oid, 'OCTET', value)

   def add_str(self, oid, value):
	  """Short helper to add a string value to the MIB subtree."""
	  self.add_oid_entry(oid, 'STRING', value)

   def add_ip(self, oid, value):
	  """Short helper to add an IP address value to the MIB subtree."""
	  self.add_oid_entry(oid, 'IPADDRESS', value)

   def add_cnt_32bit(self, oid, value):
	  """Short helper to add a 32 bit counter value to the MIB subtree."""
	  # Truncate integer to 32bits max
	  self.add_oid_entry(oid, 'Counter32', int(value) % 4294967296)

   def add_cnt_64bit(self, oid, value):
	  """Short helper to add a 64 bit counter value to the MIB subtree."""
	  # Truncate integer to 64bits max
	  self.add_oid_entry(oid, 'Counter64', int(value) % 18446744073709551615)

   def add_gau(self, oid, value):
	  """Short helper to add a gauge value to the MIB subtree."""
	  self.add_oid_entry(oid, 'GAUGE', value)

   def add_tt(self, oid, value):
	  """Short helper to add a timeticks value to the MIB subtree."""
	  self.add_oid_entry(oid, 'TIMETICKS', value)

   def main_passpersist(self):
	  """
	  Main function that handle SNMP's pass_persist protocol, called by
	  the start method.
	  Direct call is unnecessary.
	  """
	  line = sys.stdin.readline().strip()
	  if not line:
		 raise EOFError()

	  if 'PING' in line:
		 print("PONG")
	  elif 'getnext' in line:
		 oid = self.cut_oid(sys.stdin.readline().strip())
		 if oid is None:
			print("NONE")
		 elif oid == "":
			# Fallback to the first entry
			print(self.get_first())
		 else:
			print(self.get_next(oid))
	  elif 'get' in line:
		 oid = self.cut_oid(sys.stdin.readline().strip())
		 if oid is None:
			print("NONE")
		 else:
			print(self.get(oid))
	  elif 'set' in line:
		 oid = sys.stdin.readline().strip()
		 typevalue = sys.stdin.readline().strip()
		 self.set(oid, typevalue)
	  elif 'DUMP' in line:  # Just for debbuging
		 from pprint import pprint
		 pprint(self.data)
	  else:
		 print("NONE")

	  sys.stdout.flush()

   def commit(self):
	  """
	  Commit change made by the add_* methods.
	  All previous values with no update will be lost.
	  This method is automatically called by the updater thread.
	  """

	  # Generate index before acquiring lock to keep locked section fast
	  # Works because this thread is the only writer of self.pending
	  pending_idx = sorted(self.pending.keys(), key=lambda k: tuple(int(part) for part in k.split('.')))

	  # Commit new data
	  try:
		 self.lock.acquire()
		 self.data = self.pending
		 self.pending = dict()
		 self.data_idx = pending_idx
	  finally:
		 self.lock.release()

   def main_update(self):
	  """
	  Main function called by the updater thread.
	  Direct call is unnecessary.
	  """
	  # Renice updater thread to limit overload
	  try:
		 os.nice(1)
	  except AttributeError as er:
		 pass  # os.nice is not available on windows
	  time.sleep(self.refresh)

	  try:
		 while True:
			# We pick a timestamp to take in account the time used by update()
			timestamp = time.time()

			# Update data with user's defined function
			self.update()

			# We use this trick because we cannot use signals in a backoffice threads
			# and alarm() mess up with readline() in the main thread.
			delay = (timestamp + self.refresh) - time.time()
			if delay > 0:
			   if delay > self.refresh:
				  time.sleep(self.refresh)
			   else:
				  time.sleep(delay)

			# Commit change exactly every 'refresh' seconds, whatever update() takes long.
			# Commited values are a bit old, but for RRD, punctuals values
			# are better than fresh-but-not-time-constants values.
			self.commit()

	  except Exception as e:
		 self.error = e
		 raise

   def get_setter(self, oid):
	  """
	  Retrieve the nearest parent setter function for an OID
	  """
	  if hasattr(self.setter, oid):
		 return self.setter[oid]
	  parents = [poid for poid in self.setter.keys() if oid.startswith(poid)]
	  if parents:
		 return self.setter[max(parents)]
	  return self.default_setter

   def register_setter(self, oid, setter_func):
	  """
	  Set reference to an user defined function for deal with set commands.
	  The user function receives the OID, type (see Type class) and value
	  and must return a true value on succes or one of errors in Error class
	  """
	  self.setter[oid] = setter_func

   def default_setter(self, oid, _type, value):
	  return Error.NotWritable

   def set(self, oid, typevalue):
	  """
	  Call the default or user setter function if available
	  """
	  success = False
	  type_ = typevalue.split()[0]
	  value = typevalue.lstrip(type_).strip().strip('"')
	  ret_value = self.get_setter(oid)(oid, type_, value)
	  if ret_value:
		 if ret_value in ErrorValues or ret_value == 'DONE':
			print(ret_value)
		 elif ret_value == True:
			print('DONE')
		 elif ret_value == False:
			print(Error.NotWritable)
		 else:
			raise RuntimeError("wrong return value: %s" % str(ret_value))
	  else:
		 print(Error.NotWritable)

   def start(self, user_func, refresh):
	  """
	  Start the SNMP's protocol handler and the updater thread
	  user_func is a reference to an update function, ran every 'refresh' seconds.
	  """
	  self.update = user_func
	  self.refresh = refresh
	  self.error = None

	  # First load
	  self.update()
	  self.commit()

	  # Start updater thread
	  up = threading.Thread(None, self.main_update, "Updater")
	  up.daemon = True
	  up.start()

	  # Main loop
	  while up.isAlive():  # Do not serve data if the Updater thread has died
		 try:
			self.main_passpersist()
		 except:
			raise

# vim: ts=4:sw=4:ai
 

Attachments

  • zfs-snmp.zip
    10.9 KB · Views: 425
Last edited:
Joined
Feb 4, 2017
Messages
3
Exceeded max character count on my fist post.

FREENAS-MIB.txt
Code:
FREENAS-MIB DEFINITIONS ::= BEGIN

-- Currently defines structures for monitoring the zfs-stats in freenas (more to come)
-- via snmp. Taken from https://github.com/jm66/solaris-extra-snmp. License below:
-- Copyright (c) 2012, Jakob Borg
-- All rights reserved.
-- 
-- Redistribution and use in source and binary forms, with or without
-- modification, are permitted provided that the following conditions are met:
--	* Redistributions of source code must retain the above copyright
--	  notice, this list of conditions and the following disclaimer.
--	* Redistributions in binary form must reproduce the above copyright
--	  notice, this list of conditions and the following disclaimer in the
--	  documentation and/or other materials provided with the distribution.
--	* The name of the author may not be used to endorse or promote products
--	  derived from this software without specific prior written permission.
--
-- THIS SOFTWARE IS PROVIDED BY JAKOB BORG ''AS IS'' AND ANY EXPRESS OR IMPLIED
-- WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-- MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
-- EVENT SHALL JAKOB BORG BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
-- EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
-- OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-- INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-- CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
-- IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
-- OF SUCH DAMAGE.

IMPORTS
	enterprises FROM RFC1155-SMI
	OBJECT-TYPE FROM RFC-1212
	DisplayString FROM RFC-1213;

freenas	OBJECT IDENTIFIER ::= {enterprises 25359}

zfs	 OBJECT IDENTIFIER ::= {freenas 1}

zpool   OBJECT IDENTIFIER ::= {zfs 1}

arc	 OBJECT IDENTIFIER ::= {zfs 2}

l2arc   OBJECT IDENTIFIER ::= {zfs 3}

vols	OBJECT IDENTIFIER ::= {zfs 5}

zil	 OBJECT IDENTIFIER ::= {zfs 6}

ds	  OBJECT IDENTIFIER ::= {zfs 7}

rep	OBJECT IDENTIFIER ::= {zfs 8}

zfsPoolName OBJECT-TYPE
	SYNTAX  DisplayString
	ACCESS  read-only
	STATUS  mandatory
	DESCRIPTION
		"The name of the filesystem."
	::= {zpool 1}

zfsPoolAvailableKB OBJECT-TYPE
	SYNTAX  GAUGE
	ACCESS  read-only
	STATUS  mandatory
	DESCRIPTION
		"The number of 1 KB blocks that are available for use."
	::= {zpool 2}

zfsPoolUsedKB OBJECT-TYPE
	SYNTAX  GAUGE
	ACCESS  read-only
	STATUS  mandatory
	DESCRIPTION
		"The number of 1 KB blocks that are in use."
	::= {zpool 3}

zfsPoolHealth OBJECT-TYPE
	SYNTAX  DisplayString
	ACCESS  read-only
	STATUS  mandatory
	DESCRIPTION
		"The current health of the containing pool, as reported
		by zpool status."
	::= {zpool 4}

zfsPoolSizeKB OBJECT-TYPE
		SYNTAX  INTEGER32
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"The zfs 'size' property of this zfs pool in KB"
	::= {zpool 5}

zfsPoolAvailableMB OBJECT-TYPE
	SYNTAX  GAUGE
	ACCESS  read-only
	STATUS  mandatory
	DESCRIPTION
		"The number of 1 MB blocks that are available for use.
		Useful if zfsPoolAvailableKB exceeds a 32 bit integer."
	::= {zpool 12}

zfsPoolUsedMB OBJECT-TYPE
	SYNTAX  GAUGE
	ACCESS  read-only
	STATUS  mandatory
	DESCRIPTION
		"The number of 1 MB blocks that are in use Useful if
		zfsPoolUsedKB exceeds a 32 bit integer."
	::= {zpool 13}

zfsPoolSizeMB OBJECT-TYPE
		SYNTAX  GAUGE
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"The zfs 'size' property of this zfs pool in MB. Useful if
			zfsPoolSizeKB exceeds a 32 bit integer."
		::= {zpool 14}

zfsPoolOpRead OBJECT-TYPE
		SYNTAX COUNTER64
		ACCESS read-only
		STATUS mandatory
		DESCRIPTION
			"The number of read I/O operations sent to the pool or device,
			including metadata requests (averaged since system booted)."
		::= {zpool 15}

zfsPoolOpWrite OBJECT-TYPE
		SYNTAX COUNTER64
		ACCESS read-only
		STATUS mandatory
		DESCRIPTION
			"The number of write I/O operations sent to the pool or device 
			(averaged since system booted)."
		::= {zpool 16}

zfsPoolBwRead OBJECT-TYPE
		SYNTAX COUNTER64
		ACCESS read-only
		STATUS mandatory
		DESCRIPTION
			"The bandwidth of all read operations (including metadata),
			expressed as units per second (averaged since system booted)"
		::= {zpool 17}

zfsPoolBwWrite OBJECT-TYPE
		SYNTAX COUNTER64
		ACCESS read-only
		STATUS mandatory
		DESCRIPTION
			"The bandwidth of all write operations, expressed as units per 
			second (averaged since system booted)."
		::= {zpool 18}

zfsPoolOpRead1sec OBJECT-TYPE
		SYNTAX COUNTER64
		ACCESS read-only
		STATUS mandatory
		DESCRIPTION
			"The number of read I/O operations sent to the pool or device,
			including metadata requests (over 1 second interval)."
		::= {zpool 19}

zfsPoolOpWrite1sec OBJECT-TYPE
		SYNTAX COUNTER64
		ACCESS read-only
		STATUS mandatory
		DESCRIPTION
			"The number of write I/O operations sent to the pool or device 
			(over 1 second interval)."
		::= {zpool 20}

zfsPoolBwRead1sec OBJECT-TYPE
		SYNTAX COUNTER64
		ACCESS read-only
		STATUS mandatory
		DESCRIPTION
			"The bandwidth of all read operations (including metadata),
			expressed as units per second (over 1 second interval)"
		::= {zpool 21}

zfsPoolBwWrite1sec OBJECT-TYPE
		SYNTAX COUNTER64
		ACCESS read-only
		STATUS mandatory
		DESCRIPTION
			"The bandwidth of all write operations, expressed as units per 
			second (over 1 second interval)."
		::= {zpool 22}

zfsArcSize OBJECT-TYPE
		SYNTAX GAUGE
		ACCESS read-only
		STATUS mandatory
		::= {arc 1}

zfsArcMeta OBJECT-TYPE
		SYNTAX GAUGE
		ACCESS read-only
		STATUS mandatory
		::= {arc 2}

zfsArcData OBJECT-TYPE
		SYNTAX GAUGE
		ACCESS read-only
		STATUS mandatory
		::= {arc 3}

zfsArcHits OBJECT-TYPE
		SYNTAX Counter32
		ACCESS read-only
		STATUS mandatory
		::= {arc 4}

zfsArcMisses OBJECT-TYPE
		SYNTAX Counter32
		ACCESS read-only
		STATUS mandatory
		::= {arc 5}

zfsArcC OBJECT-TYPE
		SYNTAX GAUGE
		ACCESS read-only
		STATUS mandatory
		::= {arc 6}

zfsArcP OBJECT-TYPE
		SYNTAX GAUGE
		ACCESS read-only
		STATUS mandatory
		::= {arc 7}

zfsArcMissPercent OBJECT-TYPE
		SYNTAX DisplayString
		ACCESS read-only
		STATUS mandatory
		DESCRIPTION
			"Arc Miss Percentage.
			(Note: Floating precision sent across SNMP as a String"
		::= {arc 8}

zfsArcCacheHitRatio OBJECT-TYPE
		SYNTAX DisplayString
		ACCESS read-only
		STATUS mandatory
		DESCRIPTION
			"Arc Cache Hit Ration Percentage.
			(Note: Floating precision sent across SNMP as a String"
		::= {arc 9}

zfsArcCacheMissRatio OBJECT-TYPE
		SYNTAX DisplayString
		ACCESS read-only
		STATUS mandatory
		DESCRIPTION
			"Arc Cache Miss Ration Percentage.
			(Note: Floating precision sent across SNMP as a String"
		::= {arc 10} 

zfsL2ArcHits OBJECT-TYPE
		SYNTAX Counter32
		ACCESS read-only
		STATUS mandatory
		::= {l2arc  1}

zfsL2ArcMisses OBJECT-TYPE
		SYNTAX Counter32
		ACCESS read-only
		STATUS mandatory
		::= {l2arc  2}

zfsL2ArcRead OBJECT-TYPE
		SYNTAX Counter32
		ACCESS read-only
		STATUS mandatory
		::= {l2arc  3}

zfsL2ArcWrite OBJECT-TYPE
		SYNTAX Counter32
		ACCESS read-only
		STATUS mandatory
		::= {l2arc  4}

zfsL2ArcSize OBJECT-TYPE
		SYNTAX GAUGE
		ACCESS read-only
		STATUS mandatory
		::= {l2arc  5}

zfsVolumeName OBJECT-TYPE
		SYNTAX  DisplayString
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"The name of the filesystem type Volume."
		::= {vols 1}

zfsVolumeAvailableKB OBJECT-TYPE
		SYNTAX  GAUGE
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"The number of 1 KB blocks that are available for use."
		::= {vols 2}

zfsVolumeUsedKB OBJECT-TYPE
		SYNTAX  GAUGE
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"The number of 1 KB blocks that are in use."
		::= {vols 3}

zfsVolumeSizeKB OBJECT-TYPE
		SYNTAX  GAUGE
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"The zfs 'volsize' property of this zvol in KB."
		::= {vols 4}

zfsVolumeAvailableMB OBJECT-TYPE
		SYNTAX  GAUGE
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"The number of 1 MB blocks that are available for use.
			Useful if zfsVolumeAvailableKB exceeds a 32 bit
				 integer."
		::= {vols 12}

zfsVolumeUsedMB OBJECT-TYPE
		SYNTAX  GAUGE
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"The number of 1 MB blocks that are in use Useful if
			zfsVolumeUsedKB exceeds a 32 bit integer."
		::= {vols 13}

zfsVolumeSizeMB OBJECT-TYPE
		SYNTAX  GAUGE
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"The zfs 'volsize' property of this zvol in KB. Useful if
			zfsVolumeSizeKB exceeds a 32 bit integer."
		::= {vols 14}

zfsZilstatOps1sec OBJECT-TYPE
		SYNTAX  COUNTER64
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"The ops column parsed from the command zilstat 1 1"
		::= {zil 1}

zfsZilstatOps5sec OBJECT-TYPE
		SYNTAX  COUNTER64
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"The ops column parsed from the command zilstat 5 1"
		::= {zil 2}

zfsZilstatOps10sec OBJECT-TYPE
		SYNTAX  COUNTER64
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"The ops column parsed from the command zilstat 10 1"
		::= {zil 3}

zfsDatasetName OBJECT-TYPE
		SYNTAX  DisplayString
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"The name of the dataset."
		::= {ds 1}

zfsDatasetAvailableKB OBJECT-TYPE
		SYNTAX  GAUGE
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"The number of 1 KB blocks that are available for use."
		::= {ds 2}

zfsDatasetUsedKB OBJECT-TYPE
		SYNTAX  GAUGE
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"The number of 1 KB blocks that are in use."
		::= {ds 3}

zfsDatasetSizeKB OBJECT-TYPE
		SYNTAX  GAUGE
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"The number of 1 KB blocks of the dataset."
		::= {ds 4}

zfsDatasetAvailableMB OBJECT-TYPE
		SYNTAX  GAUGE
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"The number of 1 MB blocks that are available for use.
			Useful if zfsDatasetAvailableKB exceeds a 32 bit
			integer."
		::= {ds 12}

zfsDatasetUsedMB OBJECT-TYPE
		SYNTAX  GAUGE
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"The number of 1 MB blocks that are in use. Useful if
			zfsDatasetUsedKB exceeds a 32 bit integer."
		::= {ds 13}

zfsDatasetSizeMB OBJECT-TYPE
		SYNTAX  GAUGE
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"The number of 1 MB blocks of the dataset. Useful if
			zfsDatasetSizeKB exceeds a 32 bit integer."
		::= {ds 14}

zfsReplicationDatasetName OBJECT-TYPE
		SYNTAX  STRING
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"The name of the dataset being replicated"
		::= {rep 1}

zfsReplicationDestination OBJECT-TYPE
		SYNTAX  STRING
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"Replication target ip address and dataset name"
		::= {rep 2}

zfsReplicationEnabled OBJECT-TYPE
		SYNTAX  STRING
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"Replication task enabled status"
		::= {rep 3}

zfsReplicationStatus OBJECT-TYPE
		SYNTAX  STRING
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"Dataset replication status"
		::= {rep 4}

zfsReplicationLastSent OBJECT-TYPE
		SYNTAX  STRING
		ACCESS  read-only
		STATUS  mandatory
		DESCRIPTION
			"Last dataset sent"
		::= {rep 5}

END

 

Ericloewe

Server Wrangler
Moderator
Joined
Feb 15, 2014
Messages
20,194
You might want to draw attention to this with a bug report/feature request at bugs.freenas.org - the dev team will probably be interested.
 
Status
Not open for further replies.
Top