Skip to content

How to create a module Legacy

eholtam edited this page Mar 4, 2020 · 2 revisions

NOTE: The way modules work has changed - this guide needs updating!

For this howto, we're going to create a module that collects the ARD info fields as requested in issue #3. Make sure you have munkireport setup properly on the client you're developing on.

The data

The ARD info fields are stored in /Library/Preferences/com.apple.RemoteDesktop.plist. There are four possible keys in this file: Text1, Text2, Text3 and Text4. For this module, collecting the data is easy: the data is already present in a parseable form. We just need to point munkireport to the file and we're set.

Configure the client

Munkireport stores its configuration in /Library/Preferences/MunkiReport.plist. The items that the client uploads to the server are in a dict called ReportItems.

We'll use ard_model as key (more on that later), and the path to the Remote Desktop preferences as value:

sudo defaults write /Library/Preferences/MunkiReport ReportItems -dict-add ard_model "/Library/Preferences/com.apple.RemoteDesktop.plist"

Munkireport now 'knows' about the ARD preference file, and it will try to put it on the server during postflight:

/usr/local/munki/postflight 
Requesting ard_model
Need to update ard_model
Sending items
Server: Starting: ard_model
Server: Model not found: ard_model

As you can see, the client tries to check in the file on the server, but the server responds that it does not know about this data.

The server

Now let's move to the server, I assume you have write access to your (development) server. We're going to create the ard_module that will collect the plist that the client sends. Here are the steps:

  1. Create a directory inside app/modules named ard
  2. Create a file named ard_model.php inside the ard directory. Files ending with _model.php have a special meaning in munkireport, they are automatically found when a class ending with _model is instantiated.

When you run the postflight again, you'll see that the server response has changed:

/usr/local/munki/postflight 
...
Server: Class not found: ard_model

The server found the file, but can't find the class. We're going to provide the class now, paste the following php code in ard_model.php:

<?php
class Ard_model extends Model {

}

We'll run the postflight again to see what happens:

/usr/local/munki/postflight 
...
Server: No process method in: ard_model

The server found the file, the class, but now it looks for a method called process(). The process handler is the class method that receives the data from the client. If you have to filter the data or change it in a format you want for the database, you have to do it here. We're going to add a process() method and see what happens:

<?php
class Ard_model extends Model {

	function process()
	{
		echo "We're in a process!\n";
	}
}

It's just an simple method that echoes a string. Lets fire up the postflight:

/usr/local/munki/postflight 
Requesting ard_model
Need to update ard_model
Server: Starting: ard_model
We're in a process!

Hooray! We reached the process handler. Pat yourself on the back for this achievement!

Caching

But now something strange is going on: if you run postflight again, you'll see this:

/usr/local/munki/postflight 
Requesting ard_model
No changes

What happened?

Munkireport sends an md5 hash of the file to the server, the server checks this hash against a database. If the hash has not changed, the server signals 'No changes'. The client then aborts the upload of the file. This reduces the network traffic and server load for files that hardly ever change. Ok, now what? You can do two things:

  1. Remove the hash from the server. You can do that by deleting your client machine from munkireport via the web interface.
  2. Change /Library/Preferences/com.apple.RemoteDesktop.plist. This will change the md5 hash for this file.

I'm going for #2, as I'm already in working in the terminal. The following line puts the current date and time in Text4:

sudo defaults write /Library/Preferences/com.apple.RemoteDesktop Text4 "$(date '+%Y-%m-%d  %H:%M:%S')"

Now that we have caching under control, let's move on.

Data

Lets change our process function to look like this:

<?php
class Ard_model extends Model {

	function process($data)
	{
		echo "$data\n";
	}
}

We run postflight (if it says No changes, we run the date writing command from above again). Now we see something like this:

/usr/local/munki/postflight 
Requesting ard_model
Need to update ard_model
Sending items
Server: Starting: ard_model
bplist00?_DOCAllowRemoteConnectionsUText4_RestrictedFeatureLis_2013-11-13  15:03:01?										 +1IJa????????????????????????????#?

The garbled string you see that starts with bplist is a binary plist, which is the way OSX stores most of it's preferences. To decode it, we can use a library that comes with munkireport: CFPropertyList.php

Change the process() method to:

<?php
class Ard_model extends Model {

	function process($data)
	{
		require_once(APP_PATH . 'lib/CFPropertyList/CFPropertyList.php');
		$parser = new CFPropertyList();
		$parser->parse($data);
		
		print_r($parser->toArray());

		echo "\n";
	}
}

Run postflight again, and you should see the ARD file extracted to a nice php array:

/usr/local/munki/postflight
Requesting ard_model
Need to update ard_model
Sending items
Server: Starting: ard_model
Array
(
    [Text4] => 2013-11-13  21:27:51
    [DOCAllowRemoteConnections] => 
    [RestrictedFeatureList] => Array
        (
            [0] => 1
            [1] => 1
            [2] => 1
            [3] => 1
            [4] => 1
            [5] => 1
            [6] => 1
            [7] => 1
            [8] => 1
            [9] => 1
            [10] => 1
            [11] => 1
            [12] => 1
            [13] => 1
            [14] => 1
            [15] => 1
            [16] => 1
            [17] => 1
            [18] => 
            [19] => 
            [20] => 
            [21] => 
            [22] => 
            [23] => 
            [24] => 
            [25] => 
            [26] => 
            [27] => 
        )

)

We're almost there, we only need Text1, Text2, Text3 and Text4 so we'll store those as Class variables (You'll see why in the next part). And we'll add a call to save(). We change the code to look like this:

<?php
class Ard_model extends Model {

	function process($data)
	{
		require_once(APP_PATH . 'lib/CFPropertyList/CFPropertyList.php');
		$parser = new CFPropertyList();
		$parser->parse($data);
		
		$plist = $parser->toArray();

		foreach(array('Text1', 'Text2', 'Text3', 'Text4') AS $item)
		{
			if (isset($plist[$item]))
			{
				$this->$item = $plist[$item];
			}
			else
			{
				$this->$item = '';
			}
		}

		$this->save();
	}
}

The database

Now we have the data we need, let's store it in the database. To do this we need to describe the data we want to store. The description is done in the Class constructor:

<?php
class Ard_model extends Model {

	function __construct($serial='')
	{
		parent::__construct('id', 'ard'); //primary key, tablename
		$this->rs['id'] = 0;
		$this->rs['serial_number'] = ''; $this->rt['serial_number'] = 'VARCHAR(255) UNIQUE';
		$this->rs['Text1'] = '';
		$this->rs['Text2'] = '';
		$this->rs['Text3'] = '';
		$this->rs['Text4'] = '';
	}

As you can see, the constructor is expecting to be passed a serial number, munkireport will pass that to the constructor when the client checks in.

parent::__construct('id', 'ard'); //primary key, tablename

Here we're calling the parent constructor (from the Model Class), and pass in the primary key (id) and tablename (lowercase, no spaces). The tablename is the name of the database table where the data is stored, make sure that this name is not in use.

$this->rs['serial_number'] = ''; $this->rt['serial_number'] = 'VARCHAR(255) UNIQUE';

Here we define a column called 'serial_number' that has string as type. We override the automatic type detection (See Models,-Views,-Controllers#model) and specify that the serial_number column has to have unique entries.

$this->rs['Text1'] = '';
$this->rs['Text2'] = '';
$this->rs['Text3'] = '';
$this->rs['Text4'] = '';

Here we set the four columns for the info fields.

Create the table

Now we described the data, but when we instantiate the model nothing really happens yet; we need to create the table in the database. We could create the table manually, but it is easier to use the munkireport built-in function create_table(). This function checks if the table exists so it is ok to call it every time.

<?php
class Ard_model extends Model {

	function __construct($serial='')
	{
		parent::__construct('id', 'ard'); //primary key, tablename
		$this->rs['id'] = 0;
		$this->rs['serial_number'] = $serial; $this->rt['serial_number'] = 'VARCHAR(255) UNIQUE';
		$this->rs['Text1'] = '';
		$this->rs['Text2'] = '';
		$this->rs['Text3'] = '';
		$this->rs['Text4'] = '';		   
		
		// Create table if it does not exist
		$this->create_table();
		
		if ($serial)
		{
			$this->retrieve_one('serial_number=?', $serial);
		}
		
		$this->serial = $serial;
		  
	}

As you can see, I added 4 extra lines of code:

if ($serial)
{
	$this->retrieve_one('serial_number=?', $serial);
}

This will retrieve the record for this serial_number if the serial_number is passed in the constructor. We'll use this later.

$this->serial = $serial;

This will set the serial number (so it is set if the database record is empty).

The complete model

We're done, here is the complete model with all the needed parts together:

<?php
class Ard_model extends Model {

	function __construct($serial='')
	{
		parent::__construct('id', 'ard'); //primary key, tablename
		$this->rs['id'] = 0;
		$this->rs['serial_number'] = $serial; $this->rt['serial_number'] = 'VARCHAR(255) UNIQUE';
		$this->rs['Text1'] = '';
		$this->rs['Text2'] = '';
		$this->rs['Text3'] = '';
		$this->rs['Text4'] = '';		   
		
		// Create table if it does not exist
		$this->create_table();
		
		if ($serial)
		{
			$this->retrieve_one('serial_number=?', $serial);
		}
		
		$this->serial = $serial;
		  
	}

	function process($data)
	{
		require_once(APP_PATH . 'lib/CFPropertyList/CFPropertyList.php');
		$parser = new CFPropertyList();
		$parser->parse($data);
		
		$plist = $parser->toArray();

		foreach(array('Text1', 'Text2', 'Text3', 'Text4') AS $item)
		{
			if (isset($plist[$item]))
			{
				$this->$item = $plist[$item];
			}
			else
			{
				$this->$item = '';
			}
		}
		
		$this->save();
	}
}

When you run the postflight script on the client, the data gets into the database.

Display the data

Now that we have some data collected, it's time to present the data.

Listing

To make searching easy, we're creating a list view. List views live in views/listings and are automatically added to the listing menu item.

Create the following file: views/listing/ard.php and paste the following code:

<?php $this->view('partials/head'); ?>

<?php //Initialize models needed for the table
new Machine_model;
new Reportdata_model;
new Ard_model;
?>

<div class="container">

  <div class="row">

  	<div class="col-lg-12">

		  <h3><span data-i18n="listing.ard.title"></span> <span id="total-count" class='label label-primary'>…</span></h3>
		  
		  <table class="table table-striped table-condensed table-bordered">
		    <thead>
		      <tr>
		      	<th data-i18n="listing.computername" data-colname='machine.computer_name'></th>
		        <th data-i18n="serial" data-colname='reportdata.serial_number'></th>
		        <th data-i18n="listing.username" data-colname='reportdata.long_username'></th>
		        <th data-i18n="listing.ard.text" data-i18n-options='{"number":1}' data-colname='ard.Text1'></th>
		        <th data-i18n="listing.ard.text" data-i18n-options='{"number":2}' data-colname='ard.Text2'></th>
		        <th data-i18n="listing.ard.text" data-i18n-options='{"number":3}' data-colname='ard.Text3'></th>
		        <th data-i18n="listing.ard.text" data-i18n-options='{"number":4}' data-colname='ard.Text4'></th>
		      </tr>
		    </thead>
		    <tbody>
		    	<tr>
					<td data-i18n="listing.loading" colspan="7" class="dataTables_empty"></td>
				</tr>
		    </tbody>
		  </table>
    </div> <!-- /span 12 -->
  </div> <!-- /row -->
</div>  <!-- /container -->

<script type="text/javascript">

	$(document).on('appUpdate', function(e){

		var oTable = $('.table').DataTable();
		oTable.ajax.reload();
		return;

	});

	$(document).on('appReady', function(e, lang) {
		// Get column names from data attribute
		var columnDefs = [],
            col = 0; // Column counter
		$('.table th').map(function(){
              columnDefs.push({name: $(this).data('colname'), targets: col, render: $.fn.dataTable.render.text()});
              col++;
		});
	    oTable = $('.table').dataTable( {
	        columnDefs: columnDefs,
	        ajax: {
                url: "<?php echo url('datatables/data'); ?>",
                type: "POST"
            },
            dom: mr.dt.buttonDom,
            buttons: mr.dt.buttons,
	        createdRow: function( nRow, aData, iDataIndex ) {
	        	// Update name in first column to link
	        	var name=$('td:eq(0)', nRow).html();
	        	if(name == ''){name = "No Name"};
	        	var sn=$('td:eq(1)', nRow).html();
	        	var link = get_client_detail_link(name, sn, '<?php echo url(); ?>/');
	        	$('td:eq(0)', nRow).html(link);

	        }
	    });
	});
</script>

<?php $this->view('partials/foot'); ?>

(and here I should explain how this works)

Clone this wiki locally