Skip to content

How to Write a Plugin

adam.stokes edited this page Aug 7, 2014 · 35 revisions

Where do they go?

Plugins live in the sos.plugins package. This is the first place that a proper python package exists on the python path (e.g. ./sos/plugins)

The bare minimum

Essentially you need to create a class that inherits from Plugin and at least one of the provided tagging superclasses.

from sos.plugins import Plugin, RedHatPlugin

class MyPlugin(Plugin, RedHatPlugin):
    pass

Let's copy some stuff

In order for a plugin to actually do any work one of the hook methods must be defined. The setup hook is called during normal collection on every enabled plugin.

from sos.plugins import Plugin, RedHatPlugin

class CopyTest(Plugin, RedHatPlugin):
    """This plugin copies some stuff"""

    def setup(self):
        self.add_copy_specs([
            "/path/to/something/interesting",
            "/path/to/something/else/interesting",
        ])

The above will copy the two files specified in the add_copy_specs method to the final archive.

If you only need to add a single file you can call the add_copy_spec method instead:

    def setup(self):
        self.add_copy_spec("/path/to/something/interesting")

A final version allows optional size-limiting and 'tailing' of files to limit the final report size. Currently this method should only be used for single files or globs that expand to a list of files; size-limiting is not applied to directories added recursively via this method. The tailit option should only be used with line formatted text data; attempting to use this option with binary files will lead to unpredictable results.

    def add_copy_spec_limit(self, copyspec, sizelimit=None, tailit=True):
        """Add a file or glob but limit it to sizelimit megabytes. If fname is
        a single file the file will be tailed to meet sizelimit. If the first
        file in a glob is too large it will be tailed to meet the sizelimit.
        """

I want to run a program too

If you wish to collect the output of a program as part of your collection process call the add_cmd_output method:

from sos.plugins import Plugin, RedHatPlugin

class CopyTest(Plugin, RedHatPlugin):
    """This plugin copies some stuff"""

    def setup(self):
        self.add_cmd_output("bin -v")

The add_cmd_output method will execute it's argument without a shell using the PATH specified by the active policy. There is normally no need to specify an absolute path. If you need to use shell syntax this can be done by calling sh -c "<command string>".

The output of the command will be added to the report archive under sos_commands/plugin_name/mangled_command_name. Mangling converts spaces to underscores and removes other characters that are illegal or problematic in path names.

Additionally, the command will be added to the report index automatically.

Attempting to run a command that isn't installed is not treated as an error (but errors produced by commands that are found are logged) - it's fine to go ahead and speculatively try commands in a plugin without explicitly checking for their presence.

The add_cmd_output method also accepts several keyword parameters to control the collection process:

    def add_cmd_output(self, exe, suggest_filename=None,
                       root_symlink=None, timeout=300,
                       runat=None):
        """Run a program and collect the output"""

Setting suggest_filename allows a plugin to override the default choice of file name in the report archive.

A symbolic link to the collected file from the report's root directory can be requested using the root_symlink parameter.

The timeout parameter sets a maximum time (in seconds) to wait for the child process to exit. After this time sos will abandon the child and continue with report generation.

A directory may be specified via the runat program. The child will switch to this directory before executing the command.

A simpler interface to allow several commands to be run with the same timeout and path also exists:

    def add_cmd_outputs(self, cmds, timeout=300, runat=None):
        """Run a list of programs and collect the output"""

Dependencies

You can inform sosreport that your plugin should only run when certain conditions are met. The default behavior checks for the presence of files or packages specified in the plugin class. More complex checks can be implemented by overriding the check_enabled method of the base Plugin class.

from sos.plugins import Plugin, RedHatPlugin

class DepTest(Plugin, RedHatPlugin):
    """This plugin depends on a file or package"""

    files = ('/path/to/file/I/need',)
    packages = ('name-of-package',)

    def setup(self):
        self.add_copy_specs([
            "/path/to/something",
            "/path/to/something/else",
        ])

Note: if you use a tuple for files or packages be sure that you place the trailing comma in the case of a 1-tuple. ('some string') does not create a 1-tuple, but ('some string',) does.

Be aware that if any of the files or packages are found then the plugin will attempt to run. If you need to ensure that multiple files are in place or multiple packages are in place then you will want to implement your own check_enabled method.

from sos.plugins import Plugin, RedHatPlugin
from os.path import exists

class DepTest(Plugin, RedHatPlugin):
    """This plugin depends on something"""

    def check_enabled(self):
        files = [
            '/path/to/thing/i/need',
            '/path/to/other/thing/i/need'
        ]
        return all(map(exists,files))

    def setup(self):
        self.add_copy_specs([
            "/path/to/something",
            "/path/to/something/else",
        ])

Run as a Regular User

Most plugins require super user access to perform their tasks. However, certain plugins may not have this requirement. If your plugin does not need root access then you can set a class-level variable requires_root to False.

from sos.plugins import Plugin, IndependentPlugin

class MyPlugin(Plugin, IndependentPlugin):

    requires_root = False

    def setup(self):
        pass

Cross Platform Plugins

If your plugin needs to run on multiple platforms you can do that a couple ways. If the rules for collection are the same on every platform (like for say JBOSS) then subclassing IndependentPlugin is all you really need to do:

from sos.plugins import Plugin, IndependentPlugin

class MyPlugin(Plugin, IndependentPlugin):

    def setup(self):
        pass # do stuff

However, if you need to do different things on different platforms you need to define one plugin per platform, like so:

from sos.plugins import Plugin, RedHatPlugin, DebianPlugin

class MyRedHatPlugin(Plugin, RedHatPlugin):

    name = "myplugin"

    def setup(self):
       pass # do red hat specific stuff


class MyDebianPlugin(Plugin, DebianPlugin):
  
    name = "myplugin"

    def setup(self):
        pass # do debian specific stuff

Notice how both plugins have a class-level name attribute. This should be the same for all platform-specific implementations of your plugin. The name attribute determines the name presented to the user for plugin selection as well as option definition.

In some cases you may wish to share certain bits between platform-specific plugins, in this case you can make a common shared superclass:

from sos.plugins import Plugin, RedHatPlugin, DebianPlugin

class MyPlugin(Plugin):

    name = "myplugin"

    def setup(self):
        pass # do common things here


class MyRedHatPlugin(MyPlugin, RedHatPlugin):

    def setup(self):
       super(MyRedHatPlugin, self).setup()
       pass # do red hat specific stuff


class MyDebianPlugin(MyPlugin, DebianPlugin):

    def setup(self):
        super(MyDebianPlugin, self).setup()
        pass # do debian specific stuff

Note how the leaf classes are still the only ones that subclass things like RedHatPlugin and DebianPlugin. This ensures that your shared plugin class does not get run as a plugin on its own. Note that for this scheme to work correctly it's important for the leaf classes to use appropriate super(MySuperClass, self).method() calls in order to properly inherit the generic plugin's behavior (unless intentionally overriding an entire method).

Common gotcha's

If unicode characters are included in the source of your plugin commonly seen with a person's name in the copyright. Python 2 will require adding the following to the code somewhere near the top:

# -*- coding: utf8 -*-