-
Notifications
You must be signed in to change notification settings - Fork 138
How to create a module Legacy
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 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.
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.
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:
- Create a directory inside
app/modules
namedard
- Create a file named
ard_model.php
inside theard
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!
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:
- Remove the hash from the server. You can do that by deleting your client machine from munkireport via the web interface.
- 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.
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();
}
}
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.
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).
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.
Now that we have some data collected, it's time to present the data.
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)
- General Upgrade Procedures
- How to Upgrade Versions
- Troubleshooting Upgrades
- Migrating sqlite to MySQL