-
Notifications
You must be signed in to change notification settings - Fork 64
Plugin Development Overview
The plugin development process begins with creating a properly structured plugin directory in a place where Sal can find it. This directory will contain several files. First, a Yapsy plugin file to specify some metadata about the plugin and allow the plugin system to find the plugin should be created. The plugin file itself is a python file with a single class definition, as specified in the Yapsy file. The plugin will need a Django template file to render its output for display. Finally, scripts may optionally include any number of scripts for clients to run, which can then submit custom data to the Sal database for the plugin to use in generating its output.
The plugin folder structure should be named for the plugin it contains and should be structured as below (using "encryption" as the example plugin's name).
- encryption (dir)
- encryption.py (Plugin python module)
- encryption.yapsy-plugin (Yapsy metadata file)
- scripts (dir) (optional)
- script.py (Any name, any number, executable script files)
- ...
- templates (dir)
- encryption.html (Django template file)
The .yapsy-plugin
is how Sal knows about your plugin. The filename should match the name of your plugin. The file itself is in the Python INI format. There is one required section: [Core]
and the two required key-value pairs: Name
and Module
. Module
should match the filename of the plugin's python module. Name
is the name of the class contained in the plugin file.
You can also include other data about the plugin, like in the example below.
[Core]
Module = encryption
Name = Encryption
[Documentation]
Author = Graham Gilbert
Version = 0.1
Website = http://grahamgilbert.com
For normal use, plugins are found by Sal using the sal.plugin.PluginManager
. Sal calls the widget_content
method on the plugin, which in turn calls:
get_queryset
get_context
get_template
Each of these methods is a potential override point to customize behavior. however, in most cases (e.g. all of the builtin plugins), overriding just the get_context
method is sufficient. Please see the source for details on the other methods.
Next, Sal renders the plugin's template with the context data, returned from the previously mentioned methods: return template.render(context)
.
Widget
and ReportPlugin
plugins may also present machine list links. In this case, Sal's machine_list
view will find the plugin by name, and then use the request URL parameters to call the plugin's filter_machines
method, which ultimately calls your overridden filter
method.
There are lots of minor details to this process, all of which you can learn about by reading the source.
Most of the work in preprocessing data for plugins is handled by the Widget
, ReportPlugin
, and DetailPlugin
classes, so creating a plugin is primarily about boilerplate class setup and customization for just those features which make the plugin unique.
Sal plugins should import sal.plugin
. This module is what gives the plugin access to plugin base classes.
import sal.plugin
Each plugin module should specify one class; a subclass of one of Sal's plugin base classes. As discussed on the Plugin Overview, there are three kinds of plugins: Widgets, Reports, and Machine Detail Plugins. These types are represented by three classes in the sal.plugin
module:
Type | Base Class |
---|---|
Widget | Widget |
Report | ReportPlugin |
Machine Detail Plugin | DetailPlugin |
For example, the Encryption plugin is a Widget
:
class Encryption(sal.plugin.Widget):
# ...
One small gotcha here; you must import sal.plugin
and specify it in full here due to the way Yapsy finds and loads plugins. If you copy the above, you will be fine. If you from sal.plugin import Widget
and then just use class Encryption(Widget):
you will be in a world of hurt.
Sal Plugin
s have several metadata attributes which you can either set, or accept the default values.
Attribute Name | Python data type | Default Value | Description |
---|---|---|---|
description |
str |
'' (empty string) |
A short description, displayed on the plugin settings pages. |
only_use_deployed_machines |
bool |
True |
Whether to get only deployed machines (which is what most plugins use), or whether to get all machines (example, the status plugin) in the queryset handed to the plugin. |
model |
django.db.model |
sal.server.models.Machine |
Determines which model the plugin should use for its get_queryset call. If used to set anything other than Machine , you will probably have to override get_queryset to handle the different model structure. |
supported_os_families |
list of str
|
['Darwin'] |
Which OS families this plugin's data should be filtered to, as well as which client machines should process this plugin's scripts. It is best to use sal.plugin.OSFamilies.darwin , sal.plugin.OSFamilies.linux , etc, rather than specifying the OS name directly. |
template |
str |
'' (empty string) |
Relative path to the plugin's template file. If left blank, the plugin will try to use <plugin_name>/templates/<plugin_name>.html . |
widget_width |
int |
4 for Widget and DetailPlugin , 12 for ReportPlugin
|
Number of layout columns the plugin output should use |
All Plugin
subclasses define a get_context(queryset, **kwargs)
method. Additionally, Widget
and Report
may present links to a list of machines which match some subelement of the plugin's data. For example, the Encryption plugin links for seeing a list of all encrypted machines, all unencrypted machines, and all machines which have an unknown encryption status. This is accomplished by overriding the filter(machines, data)
method.
The main work done by plugins is encapsulated in this method. The "context" is a python dictionary of data you want the plugin to use when rendering its template. You override it by copying the signature from the base class:
def get_context(self, queryset, **kwargs):
The queryset argument will be a Django Queryset
of the Sal Machine
table. Prior to this stage, the queryset will have been filtered to include only deployed computers from the viewing context's group, be it Business Unit, Machine Group, or "all". Also, all access permissions will have already been handled, so the plugin code itself does not have to worry about checking for authentication or access permissions.
The **kwargs
argument is a bit of Python black magic that collects any keyword arguments used in calling this method into a dictionary, named kwargs. (This is the **
operator). This is here for potential wizardry in the future, but at the moment will only contain the following data:
Key | Value |
---|---|
group_type |
`'all', 'business_unit', 'machine_group', 'machine' |
group_id |
int ID for this group's database record, or 0 for 'all' . |
To make your life easier, the first thing the get_context
method should do is call the base class' super_get_context
method. (Again, we do it this way due to how Yapsy finds and instantiates plugins; we cannot use the super
function like you would think.
context = self.super_get_context(queryset, **kwargs)
The context
in this case is a python dictionary that will be passed to the plugin's template. Django uses data from the context to populate any templatetags in the template file, to dynamically generate the output. The super_get_context
method doesn't do much; it just adds the group_type
, group_id
, and the plugin
instance itself, into your context
. Trust me, you want this.
Finally, ensure your get_context()
method ends by returning the context
dict. It's in the name!
def get_context(self, queryset, **kwargs):
context = self.super_get_context(queryset, **kwargs)
# Plugin processing goes here...
return context
Now you can begin processing the queryset
passed into the plugin. Your goal is to build a data structure of output values that you add to the context
variable for later display in the plugin's template.
If you are writing a Widget
or a ReportPlugin
and you expect to have different clickable regions in your output chart, or links, you will also need to override the filter(machines, title)
method. This definition can be copied in as below too, to speed up plugin setup.
The parameters to filter
are machines
, which is a Django Queryset
of the Machine
model, pre-filtered for Business Unit, Machine Group, etc (dependent on context of course) and deployment status (default is to remove undeployed machines).
data
is a string, and can be anything you want. Your plugin's template should use this data value in constructing the URL for the link to the Sal machine_list
view.
For an easy to follow example of this mechanism, take a look at the source for the Munki Version plugin. A more advanced example is the Encryption plugin.
Sal expects the filter
method to return a tuple of the filtered machines
, and any title you may want the machine_list
view to have (for example "Machines with version 0.44.07 of Dwarf Fortress").
Here is an example of all of the boilerplate, set up for the Encryption plugin. Once you have this all set up, you can begin writing the actual plugin code.
# other imports...
import sal.plugin
class Encryption(sal.plugin.Widget):
def get_context(self, queryset, **kwargs):
context = self.super_get_context(queryset, **kwargs)
# Plugin processing goes here...
return context
def filter(self, machines, data):
# Plugin machine filtering code goes here...
return machines, title
So now what? If you are new to Django, you may want to read some of the documentation https://docs.djangoproject.com/en/2.0/, and especially https://docs.djangoproject.com/en/2.0/ref/models/querysets/
You can certainly take a look at the builtin plugins to see how they accomplish things.
At any point, it helps to be able to experiment. A couple of ideas for how to go about doing this; but you will need a development server to do this on (Developer Installation).
- Add
import pdb; pdb.set_trace()
in your plugin and enable it on your testing server. Then, whenever that line is hit during plugin execution, you will be dropped into the python debugger. This is similar enough to the python interpreter that it should be pretty easy to grasp quickly. Google "python debugger" for any one of a number of great tutorials. Once execution pauses, you can experiment with database queries, processing, etc, all with the actual data your plugin has received, at the point you specified it stop. - Similar, you can run the Django
manage.py
subcommandshell
to get into a python interpreter set up to work with Sal. You still have to import things. So you will probably do$ ./manage.py shell
followed byfrom server.models import *
to get all of the main Sal database models (especiallyMachine
). You can then fool around with those at your leisure.
It may help to familiarize yourself with Django's templating system. The docs start here: https://docs.djangoproject.com/en/2.0/intro/tutorial03/
If you are already familiar with Jinja2 or some other templating system, you're in luck!
In brief, Sal's templates are html files that are used for the actual output in a web browser. Any time you see curly braces (e.g. {{ hostname }}
or {% if os_family != 'Darwin' %}
, that's a templatetag, a place where Django will replace data from your plugin's context
in the template's html. You can do pretty involved things using for loops and other tags, so please examine the builtin plugins and Django's documentation for more ideas.
By default, without specifying a template path, Sal will look for a template in your plugin's templates folder, with the same name as the python module and a .html
extension. If possible, it's best to use a single template file, unless the complexity of doing so results in heinously ugly and convoluted conditional template code.
If multiple template files are needed, you will need to override the get_template
method of the plugin, which is parameterized with the group_type
and group_id
values to allow you to conditionally select a template.
Sal uses Bootstrap for its CSS framework. bootstrap. Take a look at the builtin plugins' templates to get an idea of how to use Bootstrap to style your plugin.
Sal includes MorrisJS in all templates for easy charting. You can of course use any charting library of your choosing by making sure to add any <script>
includes for that library in your template file.
Other default Javascript includes are jQuery, metisMenu, and DataTables.
To have your plugin include links to the Sal machine_list
view (a DataTable of client machine), construct a URL using the url
templatetag:
<a href="{% url 'machine_list' plugin 'ok' group_type group_id %}">
If you have used the super_get_context
method to create your context object, the plugin,
group_type, and
group_idvalues will already be present in the
context`.
- Brute force protection
- LDAP integration
- Active Directory integration
- API
- Usage reporting
- License Management
- Maintenance
- Search
- Troubleshooting
- SAML
- IAM Authentication for AWS RDS Postgres
- Docker
- Ubuntu 14.04
- Ubuntu 16.04
- RHEL 7
- Kubernetes
- Heroku?