Employer Supporting Materials for Christopher Hartman

UIPath Academy Certificates

Published Projects

I developed the following Drupal modules (PHP) and released them on Drupal.org.

  • NPR Player Pack – Provided playback support in various embedded audio players for NPR story audio imported from the NPR module. Ended support when I stopped using the NPR module.
  • PRS Satellite Monitor – Scrapes metering data from PRSS satellite receivers to assist with broadcast monitoring.

Documentation Examples

Redundant Storage Configuration Design

I designed and implemented this storage configuration used in a high-availability 2-node virtualization cluster.

Redundant Network Design for Cluster Communication and Fencing

I designed and implemented this network design for the same two-node cluster above. It provides redundant networking to each major networked component in the system, including fencing devices. Note: the Mike2 switch provides connectivity to non-cluster hosts.

Fencing is a process performed by healthy nodes in response to unresponsive nodes on the cluster. Healthy nodes will “fence” unresponsive nodes in order to maintain the precious quorum. This is sometimes referred to as STONITH- shoot the other not in the head. In order to fence, a node uses fencing devices, in this case an IPMI interface as the primary and a switched PDU as the secondary, to power off the unresponsive node and maintain quorum. This redundant network design ensures that each node has access to at least one fencing device at all times.

The following electrical diagram demonstrates the redundant electrical wiring required to enable the redundant fencing networking.

Code Examples

Program Log Parser

Drupal module that parses a PDF file from the station traffic system using OCR and generates sign-off tasks for board operators during their shifts.

; file: program_log.info
name = Program Log
description = Allows operators to approve and notate the daily program log.
package = WYSU Tools
core = 7.x
dependencies[] = wysutools ;custom module with station information
dependencies[] = date
dependencies[] = elysia_cron
<?php
// file: program_log.install

/**
 * Implements hook_install
 */
function program_log_install() {
  // Set some global variables
  variable_set('log_sendto', 'example@example.com');
  variable_set('log_subject', 'No Program Log for Tomorrow');
  variable_set('log_message', 'This is an automated message: There is no program log for tomorrow.');
}

/**
 * Implements hook_uninstall.
 */
function program_log_uninstall() {
  // Remove values
  variable_del('log_sendto');
  variable_del('log_subject');
  variable_del('log_message');
}

?>
<?php
// file: program_log.module

/**
 * Implements hook_help.
 *
 * Displays help and module information.
 *
 * @param path 
 *   Which path of the site we're using to display help
 * @param arg 
 *   Array that holds the current path as returned from arg() function
 */
function program_log_help($path, $arg) {
	switch ($path) {
		case "admin/help#program_log":
			return '<p>' . t("Approve and amend the daily program log.") . '</p>';
			break;
	}
}

function program_log_permission() {
  return array(
    'wysutools program log' => array(
      'title' => t('Access Daily Program Log'),
      'description' => t('View, amend, and sign the current program log. Requisite of "Administer Daily Program Log".'),
    ),
    'wysutools upload program log' => array(
      'title' => t('Upload Daily Program Log'),
      'description' => t('Upload new logs and delete unaltered logs. Requisite of "Administer Daily Program Log".'),
    ),
    'wysutools administer program log' => array(
      'title' => t('Administer Daily Program Log'),
      'description' => t('Override and delete all program logs, log amendments, and log signatures. Configure and test log email alerts.'),
    ),
  );
}


/**
 * Implements hook_menu()
 *
 */
function program_log_menu() {
	$items = array();
	// Log summary seen in the WYSU Tools Menu
	$items['wysutools/program_log'] = array(
		'title' => t('Daily Program Log'),
		'page callback' => 'log_review',
    'access arguments' => array('wysutools program log'),
		'type' => MENU_NORMAL_ITEM,
		'menu_name' => 'main-menu'
	);
	$items['wysutools/program_log/review'] = array(
		'title' => t("Today's Log"),
		'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -100,
	);
	$items['wysutools/program_log/review/%'] = array(
		'title' => t('Daily Program Log'),
		'page callback' => 'log_review',
    'page arguments' => array(3),
    'access arguments' => array('wysutools program log'),
		'type' => MENU_LOCAL_TASK,
    'context' => MENU_CONTEXT_INLINE,
    'tab_parent' => 'wysutools/program_log',
	);
	$items['wysutools/program_log/select'] = array(
		'title' => t('Review Another Log'),
		'page callback' => 'drupal_get_form',
    'page arguments' => array('_log_select_form'),
    'access arguments' => array('wysutools program log'),
		'type' => MENU_LOCAL_TASK,
    'weight' => -90,
	);
	$items['wysutools/program_log/upload'] = array(
		'title' => t('Upload PDF'),
		'page callback' => 'drupal_get_form',
    'page arguments' => array('_log_upload_form'),
    'access arguments' => array('wysutools upload program log'),
		'type' => MENU_LOCAL_TASK,
    'weight' => -80,
	);
	$items['wysutools/program_log/delete'] = array(
		'title' => t('Delete Log'),
		'page callback' => 'drupal_get_form',
    'page arguments' => array('_log_delete_form'),
    'access callback' => '_delete_log_form_access',
		'type' => MENU_LOCAL_TASK,
    'weight' => -70,
	);
	$items['wysutools/program_log/email/setup'] = array(
		'title' => t('Configure Email Alerts'),
		'page callback' => 'drupal_get_form',
    'page arguments' => array('_log_email_form'),
    'access arguments' => array('wysutools administer program log'),
		'type' => MENU_LOCAL_TASK,
    'weight' => -60,
	);
	$items['wysutools/program_log/email/test'] = array(
		'title' => t('Test Email Alerts'),
		'page callback' => 'drupal_get_form',
    'page arguments' => array('_log_test_email_form'),
    'access arguments' => array('wysutools administer program log'),
		'type' => MENU_LOCAL_TASK,
    'weight' => -50,
	);
	$items['wysutools/program_log/amend/%/%'] = array(
		'title' => t('Amend a Log Entry'),
		'page callback' => 'amend_log',
    'page arguments' => array(3,4),
		'access callback' => '_check_amend_perms',
    'access arguments' => array(3,4),
		'type' => MENU_LOCAL_TASK,
    'context' => MENU_CONTEXT_INLINE,
    'tab_parent' => 'wysutools/program_log',
	);
	$items['wysutools/program_log/approve/%/%'] = array(
		'title' => t('Approve a Log Entry'),
		'page callback' => '_approve_line',
    'page arguments' => array(3,4),
		'access callback' => '_check_approve_perms',
    'access arguments' => array(3,4),
		'type' => MENU_CALLBACK,
	);
	$items['wysutools/program_log/download/%'] = array(
		'title' => t('Download Program Log'),
		'page callback' => 'log_download',
    'page arguments' => array(3),
    'access arguments' => array('wysutools program log'),
		'type' => MENU_CALLBACK,
	);
	return $items;
}

/**
 * Helper function to deterine if a user can access the
 * log deletion form.
 */
function _delete_log_form_access() {
  $permissions = array('wysutools administer program log', 'wysutools upload program log');
  foreach ($permissions as $permission) {
    if (user_access($permission)) {
      return true;
    }
  }
  return false;
}

/**
 * Display the log review page.
 */
function log_review($date = null) {
  // Hack that redirects users to today's log
  // so page tabs are highlighted correctly
  if (arg(3) == date('Y-m-d')) {
    drupal_goto('wysutools/program_log');
    return null;
  }

  $date = $date ? $date : date('Y-m-d');
  $next = date('Y-m-d', strtotime($date) + 86400);
  $html = '';

  // Only an unaltered log should be used for permission checking.
  $log_unaltered = _get_log($date);
  // Markup the log before outputting it.
  $log = _markup_log($log_unaltered, 'html');
  if (!$log_unaltered) {
    drupal_set_message(t("No log exists for $date."), 'warning');
    return $html;
  }
  $html .= "<h2>$date</h2>";
  $html .= "<table style='margin: 0 0 0 0; border: 0;'><tr><td style='border: 0;'><a href=\"/wysutools/program_log/download/$date\">Download the original PDF log</a></td><td style='border: 0; text-align: right;'><a href=\"/wysutools/program_log/review/$next\">Next Day's Log</a></td></tr></table>";
  $html .= '<table><tr><th>Time</th><th>Item</th><th>Source</th><th>Duration</th><th>Approved&nbsp;by</th><th>Comment</th><th>Amend?</th><th>Approve?</th></tr>';
  $i = 0;
  $numlines = count($log->time);
  while ($i < $numlines) {
    $html .= "<tr id=\"line_$i\">";
    $html .= "<td>{$log->time[$i]}</td>";
    $html .= "<td>{$log->item[$i]}</td>";
    $html .= "<td>{$log->source[$i]}</td>";
    $html .= "<td>{$log->duration[$i]}</td>";
    $html .= "<td>{$log->signature[$i]}</td>";
    $html .= "<td>{$log->comment[$i]}</td>";

    $amend_link = '';
    $approve_link = '';
    // Hide these links unless the user is an admin, or there's a signature and it's today's log.
    if (user_access('wysutools administer program log') || (!$log_unaltered->signature[$i] && $date == date('Y-m-d'))) {
      $amend_link = "<a href=\"/wysutools/program_log/amend/$date/$i\">Amend</a>";
      $approve_link = "<a href=\"/wysutools/program_log/approve/$date/$i\">Approve</a>";
    }
    $html .= "<td>$amend_link</td><td>$approve_link</td></tr>";
    $i++;
  }
  $html .= '</table>';
  $html .= "<p style='text-align: right;'><a href=\"/wysutools/program_log/review/$next\">Next Day's Log</a></p>";

  return $html;
}

/**
 * Returns the PDF binary of the original log for the given date.
 */
function log_download($date) {
  $wysu = new WYSUAuth();
  $db = $wysu->db_connect();
  $table = _wysutools_variable_get('program log table');
  $qdate = $db->quote($date);

  $sql = "select pdf from $table where date = $qdate and active = 1 limit 1";
  $binary = $db->loadModule('Extended')->getRow($sql)->pdf;

  header('Cache-Control: no-cache private');
  header('Content-Description: File Transfer');
  header("Content-disposition: attachment; filename=program_log_{$date}.pdf");
  header('Content-Type: application/pdf');
  header('Content-Transfer-Encoding: binary');
  header('Content-Length: '. strlen($binary));
  echo $binary;
  exit;
}

/**
 *
 * Helper function that checks if a user is
 * allowed to amend a log.
 *
 * You may optionally provide a $log object
 * to check for signatures to avoid
 * unneccesary queries to the DB.
 *
 * Returns true if they are allowed. 
 * Returns false if they are not.
 *
 */
function _check_amend_perms($date, $line, $log = null) {
  $log = is_object($log) ? $log : _get_log($date, $line);
  $today = date('Y-m-d');
  // Is the log old?
  $is_old = ($today == $date) ? false : true;

  // Allow admins to do anything
  if (user_access('wysutools administer program log')) {
    return true;
  }
  // Return false if user isn't authorized
  if (!user_access('wysutools program log')) {
    return false;
  }

  // User is not an admin and further checks are required.
  if ($log->signature) {
    drupal_set_message(t("You're not allowed to amend a log that's already been approved."), 'error');
    return false;
  }
  if ($is_old) {
    drupal_set_message(t("You're not allowed to amend a log from any day but today."), 'error');
    return false;
  }

  return true;
}


/**
 *
 * Helper function that checks if a user is
 * allowed to sign a log.
 *
 * You may optionally provide a $log object
 * to check for signatures to avoid
 * unneccesary queries to the DB.
 *
 * Returns true if they are allowed. 
 * Returns false if they are not.
 *
 */
function _check_approve_perms($date, $line, $log = null) {
  $log = is_object($log) ? $log : _get_log($date, $line);
  $today = date('Y-m-d');
  // Is the log old?
  $is_old = ($today == $date) ? false : true;

  // Allow admins to do anything
  if (user_access('wysutools administer program log')) {
    return true;
  }

  // Return false if user isn't authorized
  if (!user_access('wysutools program log')) {
    return false;
  }

  // User is not an admin and further checks are required.
  if ($log->signature) {
    drupal_set_message(t("You're not allowed to approve a log that's already been approved."), 'error');
    return false;
  }
  if ($is_old) {
    drupal_set_message(t("You're not allowed to approve a log from any day but today."), 'error');
    return false;
  }

  return true;
}

/**
 * Displays the ammend log form
 */
function amend_log($date, $line) {
  return drupal_get_form('_amend_log_form', $date, $line);
}

/**
 *
 * Implements hook_form for log amendment
 *
 */
function _amend_log_form($form, &$form_state, $date, $line) {

  $log = _markup_log(_get_log($date, $line), 'html');
  $prefix = "<h2>$date</h2><table><tr><th>Time</th><th>Item</th><th>Source</th><th>Duration</th><th>Approved&nbsp;by</th><th>Comment</th></tr>";
  $prefix .= "<tr><td>{$log->time}</td><td>{$log->item}</td><td>{$log->source}</td>";
  $prefix .= "<td>{$log->duration}</td><td>{$log->signature}</td><td>{$log->comment}</td></tr>";
  $prefix .= "</table>";
  $approved = $log->signature ? 1 : 0;

	$form['comment'] = array(
		'#title' => 'Log Comment',
		'#type' => 'textfield',
    '#default_value' => t($log->comment),
    '#prefix' => $prefix,
    '#size' => 129,
	);
	$form['approve'] = array(
		'#title' => 'Approve this log?',
		'#type' => 'checkbox',
    '#default_value' => $approved,
	);
  $form['date'] = array(
    '#type' => 'hidden',
    '#value' => $date,
  );
  $form['line'] = array(
    '#type' => 'hidden',
    '#value' => $line,
  );
	$form['submit'] = array(
		'#type' => 'submit',
		'#value' => t('Submit'),
		'#required' => true,
	);
	return $form;
}

function _amend_log_form_validate($form, &$form_state) {
  // Check for HTML in the textfield
  if ($form_state['values']['comment'] != strip_tags($form_state['values']['comment'])) {
    form_set_error('comment', t('It appears your comment contains HTML or other restricted characters.'));
  }
}

function _amend_log_form_submit($form, &$form_state) {
  $line = $form_state['values']['line'];
  $date = $form_state['values']['date'];
  $log =_get_log($form_state['values']['date']);

  $log->comment[$line] = $form_state['values']['comment'];
  if ($form_state['values']['approve'] && !$log->signature[$line]) {
    // Sign the log with the current user.
    $log->signature[$line] = _name()->full . " @" . date('Y-m-d H:i:s');
  }
  elseif (!$form_state['values']['approve'] && $log->signature[$line]) {
    // Remove the signature
    $log->signature[$line] = '';
  }

  if (_update_log($date, $log)) {
    drupal_set_message(t('Log amended successfully.'), 'status');
  }
  else {
    drupal_set_message(t('There was an error amending the log.'), 'error');
  }

  drupal_goto("/wysutools/program_log/review/$date", _build_fragment($line));
}

/**
 * Helper function that returns an object with
 * the specified log's line number's data.
 * Always returns an object of strings
 * when a non-negative value is supplied as
 * $linenum. Returns an object of many-item
 * arrays if -1 is supplied.
 */

function _get_log($date = '1986-03-29', $linenum = -1) {
  $wysu = new WYSUAuth();
  $db = $wysu->db_connect();
  $table = _wysutools_variable_get('program log table');
  global $user;

  $date = $db->quote($date);

  $sql = "select log from $table where date = $date and active = 1";
  $result = $db->loadModule('Extended')->getRow($sql)->log;
  if (!$result) {
    return false;
  }

  // Return a single line
  if ($linenum >= 0) {
    $obj = new stdClass();
    $log = json_decode($result, TRUE);
    foreach ($log as $header => $values) {
      $obj->$header = $values[$linenum];
    }
    return $obj;
  }
  // Return all lines
  else {
    return json_decode($result);
  }
}

/**
 * Helper function that replaces
 * generic markup inserted during import
 * with case-specific markup such as HTML
 * or XML.
 *
 * obj    $log   : log object returned by _get_log().
 * string $type  : type of markup.
 *
 * return obj $log : log object with new markup.
 *
 */
function _markup_log($log, $type) {

  // Generic markup REGEX pattern that's being replaced
  $break = '+<linebreak>+';
  $copy_open = '+<readme>+';
  $copy_close = '+</readme>+';

  switch ($type) {
    case 'html':
      $break_replacement = '<br>';
      $copy_open_replacement = '<span style="font-weight: bold; font-size: larger">';
      $copy_close_replacement = '</span>';
    break;
    default:
      // If we don't understand the type, return the unaltered log.
      return $log;
    break;
  }

  // This will handle strings and arrays according to preg_replace() docs.
  $log->item = preg_replace($break, $break_replacement, $log->item);
  $log->item = preg_replace($copy_open, $copy_open_replacement, $log->item);
  $log->item = preg_replace($copy_close, $copy_close_replacement, $log->item);

  return $log;
}

/**
 *
 * Utility function that approves a line from a given log and returns
 * the user back to the previous page.
 *
 */
function _approve_line($date, $line) {
  $name = _name()->full;
  $name = trim($name) ? $name : _name()->username;
  $log = _get_log($date);
  $log->signature[$line] = $name . " @" . date('Y-m-d H:i:s');

  if (_update_log($date, $log)) {
    drupal_set_message(t('Log entry approved successfully.'), 'status');
  }
  else {
    drupal_set_message(t('There was an error approving the log entry.'), 'status');
  }

  drupal_goto("/wysutools/program_log/review/$date", _build_fragment($line));
}


/**
 *
 * Implements hook_form for log upload
 *
 */
function _log_upload_form($form, &$form_state) {
	$form['pdf'] = array(
		'#title' => 'Select a file',
		'#type' => 'file',
	);
  $form['overwrite'] = array(
    '#title' => t('Overwrite exisiting log'),
    '#type' => 'checkbox',
  );
	$form['date'] = array(
		'#title' => 'Log Date',
		'#type' => 'date_popup',
    '#date_format' => 'Y-m-d',
    '#required' => true,
    '#title_display' => 'invisible',
    '#default_value' => _next_log_date(),
	);
	$form['submit'] = array(
		'#type' => 'submit',
		'#value' => t('Submit'),
		'#required' => true,
	);
	return $form;
}

/**
 *
 * Implements hook_form_validate for log upload
 *
 */
function _log_upload_form_validate($form, &$form_state) {
  $log = _get_log($form_state['values']['date']);
  // First, check to see if a log with the same date exists
  if ($log && !$form_state['values']['overwrite']) {
    form_set_error('date', t('A log with that date exists. Change the date or check the overwrite box.'));
    return;
  }
  // If the log is signed, user must have admin priv to overwrite the log.
  if ($form_state['values']['overwrite']) {
    if (_is_log_signed($log) && !user_access('wysutools administer program log')) {
      form_set_error('overwrite', t('You do not have permission to overwrite a log that has been signed at least once already.'));
      return;
    }
  }

  $validators = array(
    'file_validate_extensions' => array('pdf'),
    'file_validate_size' => array(512000),
  );
  $file = file_save_upload('pdf', $validators, FALSE, FILE_EXISTS_REPLACE);
  if (is_object($file)) {
    $form_state['values']['pdf'] = $file;
  }
  else {
    form_set_error('pdf', t('Please specify a file to upload.'));
    return;
  }
}

/**
 *
 * Implements hook_form_submit for log upload
 * This utility function converts a PDF to plaintext CSV,
 * breaks each line into an array, adds several
 * columns for operator data on each line, and then
 * converts the entire array into an object of arrays
 * where each object member is a column header.
 *
 * We go from this: $log = array(col1_val, col2_val, col3_val, etc...)
 * To this: $log->col1 = array(val1, val2);
 *          $log->col2 = array(val1, val2);
 *          $log->col3 = array(val1, val2);
 *
 *
 */
function _log_upload_form_submit($form, &$form_state) {
  $wysu = new WYSUAuth();
  $db = $wysu->db_connect();
  $name = $db->quote(_name()->full);
  $author = $db->quote(_name()->username);
  $delim = ';____;';
  $table = _wysutools_variable_get('program log table');
  $date = $db->quote($form_state['values']['date']);
  $pdf = $form_state['values']['pdf'];
  $pdf_path = drupal_realpath($pdf->uri);
  $linebreak = '<linebreak>';
  $readme_open = '<readme>';
  $readme_close = '</readme>';
  $pdftotext = '/usr/bin/pdftotext';

  // Make sure the pdftotext binary exists.
  if (!file_exists($pdftotext)) {
    drupal_set_message(t("The 'pdftotext' binary cannot be found at %location. Is 'poppler-utils' installed?", array('%location' => $pdftotext)), 'error');
    drupal_goto("/wysutools/program_log/upload");
  }

  if ($fp = fopen($pdf_path, 'r')) {
    $pdf_bin = $db->quote(fread($fp, filesize($pdf_path)));
    fclose($fp);
    $pdf_text = shell_exec("$pdftotext -r 600 -layout $pdf_path -");
    $i = 0; // post-transform current line
    $lines = array();
    foreach (preg_split("/((\r?\n)|(\r\n?))/", $pdf_text) as $key => $line) {
      // Skip the first two lines
      if ($key <= 1) {
        continue; // skip everything below
      }
      // Strip columns of \f new page marker
      $line = preg_replace('/^\\f/', '', $line);
      // Strip columns of bookend whitespace
      $line = array_map('trim', explode('||', $line));
      $num_of_col = count($line);
      // Skip blank lines
      if (trim(implode('', $line)) == '') {
        continue;
      }
      // Normal lines always have something in column 1 and more than one column
      elseif ($line[0] && $num_of_col > 1 ) {
        // Add two blank array items:
        // 1. operator notes 2. operator signature
        array_push($line, "", "");
        $lines[$i] = $line;
        // OK to move to the next line
        $i++;
      }
      // Making it this far means the contents of the line
      // belong in column 2 of the current post-transform line
      else {
        $line = trim(implode('', $line));
        // Take the second column from the previous post-transform line
        // and add data to it
        $col = $lines[$i - 1][1] . " " . $delim . $line;
        // Splice in the new column
        array_splice($lines[$i - 1], 1, 1, trim($col));
      }
    }
    foreach ($lines as $key => $line) {
      $item = explode($delim, $line[1], 3);
      // Remove blanks
      $lines[$key][1] = '';
      $c = 0;
      while ($c <= 2) {
        // Never precede the first item with a <linebreak>
        // There will always be a first item
        if ($item[$c] && $c == 0) {
          // Bold lines that contain '##'
          if (strpos($item[$c], '##') !== FALSE) {
            $item[$c] = $readme_open . $item[$c] . $readme_close;
          }
          $lines[$key][1] .= $item[$c];
        }
        elseif ($item[$c] && $c == 2) {
          // The last array item should always be bolded
          $item[$c] = preg_replace("/$delim/", '', $item[$c]);
          $lines[$key][1] .= $linebreak . $readme_open . $item[$c] . $readme_close;
        }
        // If there's an item and it's not first,
        // append precede it with <linebreak>
        elseif ($item[$c]) {
          $lines[$key][1] .= $linebreak . $item[$c];
        }
        $c++;
      }
    }
    // Convert the array to an object so it's easier
    // to work with and so values are more explicit
    $obj = new stdClass();
    foreach ($lines as $col) {
      $obj->time[] = $col[0];
      $obj->item[] = $col[1];
      $obj->source[] = $col[2];
      $obj->duration[] = $col[3];
      $obj->signature[] = $col[4];
      $obj->comment[] = $col[5];
    }

    // Transform text more
    foreach ($obj->item as $key => $item) {
      // Move "Time left in break: NN:NN:NN" to the end
      $matches = array();
      if (preg_match('/Time left in break: ..:..:../', $obj->item[$key], $matches)) {
        // Remove match from original string.
        $obj->item[$key] = preg_replace("/{$matches[0]}/", '', $obj->item[$key]);
        // Append match to the end of the string.
        $obj->item[$key] .= $linebreak . $matches[0];
      }

      // Move "Break may occur..." to the end
      $matches = array();
      if (preg_match("=Break may occur between *..:..:.. *and *{$linebreak} *{$readme_open} *..:..:.. *{$readme_close}=", $obj->item[$key], $matches)) {
        // Remove match from original string.
        $obj->item[$key] = preg_replace("={$matches[0]}=", '', $obj->item[$key]);
        // Remove unnecessary linebreak from the match
        $matches[0] = preg_replace("={$linebreak}=", '', $matches[0]);
        // Remove unnecessary readme from the match
        $matches[0] = preg_replace("={$readme_open}=", '', $matches[0]);
        // Remove unnecessary /readme from the match
        $matches[0] = preg_replace("={$readme_close}=", '', $matches[0]);
        $obj->item[$key] .= $linebreak . $matches[0];
      }
      // Replace double line breaks with a single line break
      $obj->item[$key] = preg_replace("/{$linebreak} *{$linebreak}/", $linebreak, $obj->item[$key]);
    }

    $pdf_text = $db->quote(json_encode($obj));
    // Delete the file because we're storing the original in the database
    file_delete($pdf);
  }

  if ($form_state['values']['overwrite']) {
    //Deactivate all logs with the same date
    $sql = "update $table set active = 0 where date = $date";
    if ($db->exec($sql)) {
      drupal_set_message(t('There was an error overwriting the previous log.'), 'error');
    }
  }
  $sql = "insert into $table (date, author, name, pdf, log, active) values ($date, $author, $name, $pdf_bin, $pdf_text, 1)";
  if ($db->exec($sql)) {
    drupal_set_message(t('Log uploaded successfully.'), 'status');
  }
  else {
    drupal_set_message(t('There was an error uploading the log.'), 'error');
  }
  
  // Review the log that was just submitted
  drupal_goto("/wysutools/program_log/review/{$form_state['values']['date']}");
}

/**
 *
 * Implements hook_form for log selection
 *
 */
function _log_select_form() {
	$form['date'] = array(
		'#title' => 'Select a Log to Review',
		'#type' => 'date_popup',
		'#required' => true,
		'#date_format' => 'Y-m-d',
    '#default_value' => _last_log_date(),
	);
	$form['submit'] = array(
		'#type' => 'submit',
		'#value' => t('Submit'),
		'#required' => true
	);
	return $form;
}

function _log_select_form_validate($form, &$form_state) {
  // First, check to see if a log with the same date exists
  if (!_get_log($form_state['values']['date'])) {
    form_set_error('date', t('A log with that date does not exist.'));
  }
}

/**
 *
 * Implements hook_form_submit for log selection
 *
 */
function _log_select_form_submit($form, $form_state) {
  $path = "/wysutools/program_log/review/{$form_state['values']['date']}";
  drupal_goto($path);
}

/**
 * Implements hook_form for log deletion
 */
function _log_delete_form($form, &$form_state) {
	$form['date'] = array(
		'#title' => 'Select a Log to Delete',
		'#type' => 'date_popup',
		'#required' => true,
		'#date_format' => 'Y-m-d',
    '#default_value' => _last_log_date(),
	);
	$form['submit'] = array(
		'#type' => 'submit',
		'#value' => t('Submit'),
		'#required' => true
	);
	return $form;
}

function _log_delete_form_validate($form, &$form_state) {
  $date = $form_state['values']['date'];
  $log = _get_log($date);
  $field = 'date';

  // First, check to see if a log with the same date exists
  if (!$log) {
    form_set_error($field, t('A log with that date does not exist.'));
    return;
  }
  // Administrators can delete anything.
  if (user_access('wysutools administer program log')) {
    return;
  }
  // Traffic directors can only delete logs if it hasn't been altered yet.
  if (user_access('wysutools upload program log')) {
    if (_is_log_signed($log)) {
      form_set_error($field, t('You don\'t have permission to delete a log that has already been signed at least once.'));
    }
    return;
  }
}

function _log_delete_form_submit($form, &$form_state) {
  if (_deactivate_log($form_state['values']['date'])) {
    drupal_set_message(t('Log deleted successfully.'), 'status');
  }
  else {
    drupal_set_message(t('There was an error deleting the log.'), 'error');
  }

  drupal_goto('/wysutools/program_log/review');
}

/**
 * Helper function that returns true if a log has been signed.
 */
function _is_log_signed($log) {
  if (array_filter($log->signature)) {
    return true;
  }
  return false;
}

/**
 * Custom function that will delete the transmitter log with the provided ID
 *
 * @param id
 *   string- the date of the active log you wish to 'delete'
 *   Reruired date format: yyyy-mm-dd
 *
 */
function _deactivate_log($date) {
	global $user;
  $table = _wysutools_variable_get('program log table');
	$wysu = new WYSUAuth();
	$db = $wysu->db_connect();
	$date = $db->quote($date);

	$sql = "update $table set active = 0 where date = $date and active = 1 limit 1";
  return $db->exec($sql);
}

/**
 * Utility function that updates a log in the database
 */
function _update_log($date, $log) {
  $wysu = new WYSUAuth();
  $db = $wysu->db_connect();
  $log = $db->quote(json_encode($log));
  $date = $db->quote($date);
  $table = _wysutools_variable_get('program log table');
  $sql = "update $table set log = $log where date = $date";
  return $db->exec($sql);
}

function _build_fragment($line = 0) {
  $line = $line - 2;
  $line = ($line > 0) ? $line : 0;
  $fragment = ($line <= 0) ? array() : array('fragment' => "line_$line");

  return $fragment;
}

/**
 * Utility function that returns the date (yyyy-mm-dd)
 * of the last log in the DB.
 */
function _last_log_date() {
  $wysu = new WYSUAuth();
  $db = $wysu->db_connect();
  $table = _wysutools_variable_get('program log table');
  $sql = "select max(date) as date from $table where active = 1 limit 1";
  $date = $db->loadModule('Extended')->getRow($sql)->date;
  return $date;
}

/**
 * Utility function that returns the date (yyyy-mm-dd)
 * plus one day of the last log file
 */
function _next_log_date() {
  $date = _last_log_date();
  $date = strtotime("+1 day", strtotime($date));
  return date('Y-m-d', $date);
}


///////// Email Section /////////

/**
 * Implements hook_form for the email setup form.
 */
function _log_email_form($form, &$form_state) {
  $form = array();
  $form['sendto'] = array(
    '#title' => t('Send To'),
    '#type' => 'textarea',
    '#description' => t('List of email addresses that will receive alerts. One address per line.'),
    '#required' => true,
    '#default_value' => variable_get('log_sendto'),
  );
	$form['subject'] = array(
		'#title' => t('Subject'),
		'#type' => 'textfield',
    '#description' => t('Subject of the email alert.'),
    '#required' => true,
    '#default_value' => variable_get('log_subject'),
	);
  $form['message'] = array(
    '#title' => t('Email Message'),
    '#description' => t('Message sent when an email alert is triggered.'),
    '#type' => 'textarea',
    '#required' => true,
    '#default_value' => variable_get('log_message'),
  );
	$form['submit'] = array(
		'#type' => 'submit',
		'#value' => t('Submit'),
		'#required' => true,
	);
	return $form;

}

/**
 * Implements hook_form_validate for email setup form,
 */
function _log_email_form_validate($form, &$form_state) {
  // Check validity of each email address
  if (!_log_valid_email_address($form_state['values']['sendto'])) {
    form_set_error('sendto', t('One or more email addresses are invalid.'));
  }
}

/**
 * Implements hook_form_submit for email form.
 */
function _log_email_form_submit($form, &$form_state) {
  // Store values
  variable_set('log_sendto', $form_state['values']['sendto']);
  variable_set('log_subject', $form_state['values']['subject']);
  variable_set('log_message', $form_state['values']['message']);

  // Set message
  drupal_set_message(t('Email settings saved successfully.'), 'status');
  drupal_goto('wysutools/program_log/email/setup');
}

/**
 * Implements hook_form for the test alert form.
 */
function _log_test_email_form($form, &$form_state) {
	$form['submit'] = array(
		'#type' => 'submit',
		'#value' => t('Send Test Program Log Alert'),
    '#description' => t('Immediately send a test email alert to all recipients.'),
		'#required' => true,
	);
	return $form;
}

/**
 * Implements hook_form_validate for test alert form.
 */
function _log_test_email_form_validate($form, &$form_state) {
  // Check to make sure all data is stored
  $variables = array('log_sendto', 'log_subject', 'log_message');
  $field = 'submit';
  foreach ($variables as $var) {
    if (variable_get($var, '') == '') {
      form_set_error($field, t('One or more values have been left blank on the email form. Please check the Daily Program Log email configuration.'));
      break;
    }
  }
  if (!_log_valid_email_address(variable_get('log_sendto'))) {
    form_set_error($field, t('One or more email addresses are invalid. Please check the Daily Program Log email configuration.'));
  }
}

/**
 * Implements hook_form_submit for test email form.
 */
function _log_test_email_form_submit($form, &$form_state) {
  $message = _log_send_email('test');
  drupal_set_message(t('Email sent. Errors will be reported to watchdog. Status of test attempt: ') . $message['result'], 'status');
  drupal_goto('wysutools/program_log/email/test');
}

/**
 * Utility function that checks the validity of provided addresses.
 */
function _log_valid_email_address($sendto) {
  $sendto = explode("\n", $sendto);
  foreach ($sendto as $address) {
    if (valid_email_address(trim($address))) {
      return true;
    }
  }
  return false;
}

/**
 * Implements hook_cron
 *
 * This is what actually sends alerts. Use Elysia Cron to schedule checking at the desired time.
 */
function program_log_cron() {
  $tomorrow = new DateTime('tomorrow');
  $date = $tomorrow->format('Y-m-d'); 
  if (!_get_log($date)) {
    watchdog('program_log', 'A daily program log for tomorrow %date was not found. Sending email alert.', array('%date' => $date), WATCHDOG_ALERT);
    _log_send_email('alert');
  }
  else {
    watchdog('program_log', 'A daily program log for tomorrow %date was found. No alerts necessary.', array('%date' => $date), WATCHDOG_NOTICE);
  }
}

/**
 * Utility function that builds an email message and sends email.
 */
function _log_send_email($key) {
  $params = array();
  switch ($key) {
    case 'test':
      $params = array(
        'subject' => t('TESTING ' . variable_get('log_subject') . ' TESTING'),
        'body' => t("TESTING\n" . variable_get('log_message') . "\nTESTING"),
      );
    break;
    case 'alert':
      $params = array(
        'subject' => t(variable_get('log_subject')),
        'body' => t(variable_get('log_message')),
      );
    break;
  }
  // Build the address string.
  $addresses = explode("\n", variable_get('log_sendto'));
  $i = count($addresses);
  $to = '';
  foreach ($addresses as $address) {
    $address = trim($address);
    if ($address) {
      $to .= "<$address>";
      if ($i !== 1) {
        $to .= ',';
      }
    }
    $i--;
  }
  return drupal_mail('program_log', $key, $to, language_default(), $params);
}

/**
 * Implements hook_mail.
 */
function program_log_mail($key, &$message, $params) {
  switch ($key) {
    default:
      $message['subject'] = $params['subject'];
      $message['body'][] = $params['body'];
    break;
  }
}

Icecast/Audio File Silence Check

A Nagios silence-sensing check written in Bash. Analyzes audio from an Icecast stream and triggers an alert if no audio is detected above a given threshold for a given length.

#!/bin/bash
# check_silence
# Author: Chris Hartman, 2019

FILE="$1"
LENGTH=$2
PREROLL_LENGTH=$3
THRESHOLD=$4

if [ -z "$*" ]; then
	echo -e " Usage:   ./check_silence FILE LENGTH PREROLL_LENGTH THRESHOLD"
	echo -e ' Example: ./check_silence "http://server.com/stream.mp3" 60 20 -40'
	echo -e ' Explanations:'
	echo -e '  FILE :            Can be a local or remote, static or streaming file.'
	echo -e '  LENGTH :          Expressed in seconds. Minimum length of detectable silence.'
	echo -e '  PREROLL_LENGTH :  Expressed in seconds. Length of preroll where silence is never expected.'
	echo -e '  THRESHOLD :       Expressed as a negative number in decibels. Audio below this value is considered silence.'
	exit 3
fi

if [ -z "$(which ffmpeg)" ] || [ -z "$(which ffprobe)" ]; then
	echo -e "UNKNOWN - Missing ffmpeg or ffprobe binaries. Please install them."
	exit 3;
fi

# Make sure the file is accessible
ffprobe "$FILE" > /dev/null 2>&1
if [ $? -ne 0 ]; then
	echo -e "UNKNOWN - Could not stat file."
	exit 3
fi

# Add three seconds to total length to ensure the file does not end before silence can be detected
LENGTHPLUS=$(expr $LENGTH + $PREROLL_LENGTH + 3)
SILENCE=$(ffmpeg -t $LENGTHPLUS -i "$FILE" -af silencedetect=noise=${THRESHOLD}dB:duration=${LENGTH} -f null - 2>&1|grep silencedetect)
 
if [ -n "$SILENCE" ]; then
	echo -e "CRITICAL - Silence detected."
	exit 2
else 
	echo -e "OK - No silence detected."
	exit 0
fi

DNS Propagation Check

A Nagios DNS propagation check written in Powershell and Perl. Intended to be run daily via Task Scheduler, the Powershell script creates a unique and predictable DNS record in a given domain.

The Perl script runs on the Nagios server and polls a given DNS server for the expected record to verify that DNS updates are propagating throughout the network.

# daily-dns.ps1
# Indicate the zone in which to create the record
$zone = "example.com"
# Get the current date
$date = Get-Date -UFormat "%Y %m %d %j"
# Explode result into an array
$date = $date.split(' ')
# Parse array to get individual values. Remove leading zeros as the DNS server doesn't like them
# Split the year into two parts, first two digits then last two digits. Remove zeros
$year1 = $date[0].SubString(0,2).trimstart('0')
$year2 = $date[0].SubString(2,2).trimstart('0')
$month = $date[1].trimstart('0')
$day = $date[2].trimstart('0')
$yday = $date[3].trimstart('0')
 
$address = "$year1.$year2.$month.$day"
$prefix = "propagationtest"
 
# First, delete all propagation test records
$oldEntry = Get-DnsServerResourceRecord -ZoneName "$zone" -RRType A | Where-Object {$_.HostName -like "$prefix-*"}
if ($oldEntry.HostName -ne $null) {
  Remove-DnsServerResourceRecord -Force -ZoneName "$zone" -RRType "A" -Name $oldEntry.HostName
}
 
# Add the new record for today
Add-DnsServerResourceRecordA -Name $prefix-$yday -ZoneName "$zone" -IPv4Address $address -TimeToLive 23:59:59
#!/usr/bin/perl
# check_dns_propagation

# This script will check the automatically-create DNS record
# in a provided domain.
# String maniuplation of the date is to be consistent
# with the powershell date functions.

use strict;
use warnings;
use Net::DNS;

my ($server, $domain) = @ARGV;

(my $sec, my $min, my $hour, my $mday, my $mon, my $year, my $wday, my $yday, my $isdst) = localtime();

# Add 1900 to the year since localtime reports year since 1900
$year = $year + 1900;
# Split the year in half character-wise
my $year1 = substr($year, 0, 2);
my $year2 = substr($year, 2, 2);
# Remove leading zeros on our split year
($year1 = $year1) =~ s/^0+//g;
($year2 = $year2) =~ s/^0+//g;
# Add 1 since January = 0
$mon = $mon + 1;
# Add 1 since Jan 1 = 0
$yday = $yday + 1;
my $return = 0;

my $address = "$year1.$year2.$mon.$mday";
my $record = "propagationtest-$yday.$domain";

my $res = new Net::DNS::Resolver;
$res->nameservers($server);
my $packet = $res->query($record);

if (!$packet) {
  print "CRITICAL - Unable to lookup $record at $address on $server. Either the DNS server is down or the record doesn't exist.", "\n";
	$return = 2;
} else {
	foreach my $rr ($packet->answer) {
		if ($rr->address eq $address) {
			print "OK - Propagation test for $record at $address on $server succeeded.", "\n";
			$return = 0;
		} else {
			my $data = $rr->address;
			print "WARNING - $record exists on $server but does not have the correct value. $address != $data", "\n";
			$return = 1;
		}
	}
}

exit $return;

Transmission Telemetry Check

A Nagios check written in Perl and utilizing the cURL library. It authenticates via username/password or cookie to a broadcast metering device and pulls key metrics to compare against given thresholds.

#!/usr/bin/perl
# check_arc

=begin comment

Author: Chris Hartman, 2019

This script will check a single value from either the Meters or Status table
on the ARCSolo from Burk Technology.

Upon first execution, this script will use the provided credentials to generate
an authentication cookie for retrieving data from the ARCSolo. This cookie will
be stored in a cookie file to be reused on subsequent checks; this cookie is
necessary because if only the login credentials are used, it won't take long for
all existing sessions to be terminated by the Burk, most likely due to the
session table filling up.

Values for Meters are always numbers (integers or floats).
Values for Status are boolean. True will be converted to 1 and False to 0 for thresholds.
Values for Commands are not checked.

Important files:
  /Info.json - Generic info about the ARCSolo, Location, user, datetime, warpengine port and hash
  /ChannelConfig.json - Channel labels, warning/critical thresholds, etc etc
  /Nonce.json - Nonce value

=end comment
=cut

use strict;
use warnings;
use 5.010;
use JSON;
use Getopt::Long qw(GetOptions);
use WWW::Curl::Easy;
use Digest::MD5 qw(md5 md5_hex md5_base64);
use File::Touch;
#use Data::Dumper;

# User-provided variables
my $host = "127.0.0.1";
my $username = "username";
my $password = "password";
my $table = "meters";
my $channel = 1;
my $cookie = '/tmp/nagios_arcsolo.cookie';
my $warning = "9999";
my $critical = "99999";
if (
	!GetOptions(
		'host|H=s'      => \$host,
		'username|u=s'  => \$username,
		'password|p=s'  => \$password,
		'cookie|b=s'    => \$cookie,
		'table|t=s'      => \$table,
		'channel|m=i'    => \$channel,
		'warning|w=s'    => \$warning,
		'critical|c=s'  => \$critical
	) 
) {
	print "UNKNOWN\nUsage:\n  $0 -H HOST -u USERNAME -p PASSWORD -b COOKIE_FILE -t TABLE -m CHANNEL# -w RANGE -c RANGE\n";
	print "  $0 --host HOST --username USERNAME --password PASSWORD --cookie COOKIE_FILE --table TABLE --channel CHANNEL --warning RANGE --critical RANGE\n";
	exit 3;
}

# In-script variables
my $curl;
my $wLow = 0;
my $wHigh;
my $cLow = 0;
my $cHigh;
my $wInc = 0; # inclusive warning
my $cInc = 0; # inclusive critical

# Returns 1 for successful cookie creation
# Returns 0 for error
sub create_cookie {
	my $file = $_[0];
	if (-e $file) {
		unlink($file);
	}
	my @files = ($file);
	touch(@files);
	chmod 0600, $file;
	# Retrieve the nonce
	my $curl = WWW::Curl::Easy->new;
	$curl->setopt(CURLOPT_URL, "http://$host/nonce.json");
	my $nonce;
	$curl->setopt(CURLOPT_WRITEDATA, \$nonce);
	# Make the HTTP request
	if ($curl->perform != 0) {
		print "UNKNOWN - Failed to connect to $host to retrieve nonce.\n";
		exit 3;
	}
	$nonce = decode_json($nonce);
	$nonce = $nonce->{Nonce};
	
	# Build the login hash
	# This is all we need to authenticate
	my $hash;
	$hash = $nonce . $username . uc(md5_hex('Burk' . $password));
	$hash = uc(md5_hex($hash));
	$curl = WWW::Curl::Easy->new;
	$curl->setopt(CURLOPT_URL, "http://$host/Info.json");
	my $nothing;
	$curl->setopt(CURLOPT_WRITEDATA, \$nothing);
	$curl->setopt(CURLOPT_POST, 1); 
	$curl->setopt(CURLOPT_POSTFIELDS, "login=$hash");
	$curl->setopt(CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
	$curl->setopt(CURLOPT_COOKIEJAR, $file);
	$curl->perform;
	if (!-w $file) {
		return 0;
	}
	else {
		return 1;
	}
}

# Returns 1 for valid cookie
sub verify_cookie {
	my $file = $_[0];
	if (!-e $file) {
		return 0;
	}
	if (!-r $file) {
		return 0;
	}
	my $response;
	my $curl = WWW::Curl::Easy->new;
	$curl->setopt(CURLOPT_URL, "http://$host/Info.json");
	$curl->setopt(CURLOPT_COOKIEFILE, $file);
	$curl->setopt(CURLOPT_WRITEDATA, \$response);
	$curl->perform;
	eval {
		decode_json($response);
	};
	if ($@) {
		return 0;
	} else {
		return 1;
	}
}

## Warning Threshold
# Split warning thresholds
if (index($warning, ':') != -1) {
  # multipe args
  ($wLow, $wHigh) = split(/:/, $warning);
  if ($wLow eq '') {
    $wLow = 0;
  }
  if ($wHigh eq '') {
    $wHigh = 99999;
  }
}
else {
  # single arg
  $wHigh = $warning;
}
# Account for special characters in thresholds
if ($wLow eq '~') {
  # negative "infinity"
  $wLow = -9999;
}
if (index($wLow, '@') != -1) {
  # inclusive range
  $wInc = 1;
  # Remove '@' from variable
  $wLow =~ s/@//;
}

## Critical threshold
# Split critical thresholds
if (index($critical, ':') != -1) {
  # multipe args
  ($cLow, $cHigh) = split(/:/, $critical);
  if ($cLow eq '') {
    $cLow = 0;
  }
  if ($cHigh eq '') {
    $cHigh = 99999;
  }
}
else {
  # single arg
  $cHigh = $critical;
}
# Account for special characters in thresholds
if ($cLow eq '~') {
  # negative "infinity"
  $cLow = -9999;
}
if (index($cLow, '@') != -1) {
  # inclusive range
  $cInc = 1;
  # Remove '@' from variable
  $cLow =~ s/@//;
}

# Ensure start ranges are lower than end ranges
if ($wLow > $wHigh || $cLow > $cHigh) {
  say "UNKNOWN - Threshold starting values must be less-than-or-equal-to ending values.";
  exit 3;
}

if ($channel &lt; 1 || $channel > 16) {
  say "UNKNOWN - Channel must be inclusively between 1 and 16.";
  exit 3;
}
# Convert table name to all lower case for comparison
$table = lc($table);
if ($table ne 'meters' &amp;&amp; $table ne 'status') {
  say "UNKNOWN - Table must be 'meters' or 'status' only.";
  exit 3;
}
else {
  $table = ucfirst($table);
}

if (verify_cookie($cookie) == 0) {
	if (create_cookie($cookie) == 0) {
		print "UNKNOWN - Failed to create cookie file $cookie.\n";
		exit 3;
	}
}
$curl = WWW::Curl::Easy->new;
$curl->setopt(CURLOPT_URL, "http://$host/ChannelConfig.json");
$curl->setopt(CURLOPT_COOKIEFILE, $cookie);
my $info;
$curl->setopt(CURLOPT_WRITEDATA, \$info);
# Make the HTTP request
if ($curl->perform != 0) {
  print "UNKNOWN - Failed to connect to $host to retrieve channel info.\n";
  exit 3;
}
$info = decode_json($info);
my $label;
my $units;
my $min;
my $max;
if ($table eq "Meters") {
	$label = $info->{"${table}Cfg"}[$channel - 1]->{Label};
	$units = $info->{"${table}Cfg"}[$channel - 1]->{UnitsLabel};
	$min = $info->{"${table}Cfg"}[$channel - 1]->{Minimum};
	$max = $info->{"${table}Cfg"}[$channel - 1]->{Maximum};
}
elsif ($table eq "Status") {
	$label = $info->{"${table}Cfg"}[$channel - 1]->{OnLabel};
	$units = '';
	$min = 0;
	$max = 1;
}
# Remove Nagios-forbidden characters from label
$label =~ s/=//g;
$label =~ s/'//g;

# Retrieve the JSON using the hash
$curl = WWW::Curl::Easy->new;
$curl->setopt(CURLOPT_URL, "http://$host/Values.json");
$curl->setopt(CURLOPT_COOKIEFILE, $cookie);
my $json;
$curl->setopt(CURLOPT_WRITEDATA, \$json);

if ($curl->perform != 0) {
  print "UNKNWON - Could not connect to $host to retreive values.\n";
  exit 3;
}
eval {
  $json = decode_json($json);
};
if ($@) {
  print "UNKNOWN - Failed to retrieve values from $host. Check your username/password.\n";
  exit 3;
}

# Check values against threshold
# Subtract 1 to account for 0-start indexing
my $value = $json->{$table}[$channel - 1];

# Turn true and false string values to integers for comparison
if ($value eq 'true') {
  $value = 1;
}
elsif ($value eq 'false') {
  $value = 0;
}

# Check critical first
# Inclusive range
if ($cInc == 1) {
  if ($value >= $cLow &amp;&amp; $value &lt;= $cHigh) {
    say "CRITICAL - Channel #$channel: ${value}${units} | '$label'=${value}${units};$warning;$critical;$min;$max";
    exit 2;
  }
}
# exclusive range, more common
else {
  if ($value &lt; $cLow || $value > $cHigh) {
    say "CRITICAL - Channel #$channel: ${value}${units} | '$label'=${value}${units};$warning;$critical;$min;$max";
    exit 2;
  }
}
# Check critical first
# Inclusive range
if ($wInc == 1) {
  if ($value >= $wLow &amp;&amp; $value &lt;= $wHigh) {
    say "WARNING - Channel #$channel: ${value}${units} | '$label'=${value}${units};$warning;$critical;$min;$max";
    exit 1;
  }
}
# exclusive range, more common
else {
  if ($value &lt; $wLow || $value > $wHigh) {
    say "WARNING - Channel #$channel: ${value}${units} | '$label'=${value}${units};$warning;$critical;$min;$max";
    exit 1;
  }
}

say "OK - Channel #$channel: ${value}${units} | '$label'=${value}${units};$warning;$critical;$min;$max";
exit 0