How to Write a Policy

Bryn M. Reeves edited this page Jun 10, 2014 · 11 revisions

A policy is a bit of code that Sosreport delegates to to make decisions about how to behave on a particular distribution. Each supported distribution requires a policy class. Policies are responsible for providing validation on whether or not a plugin can run on the system they understand as well as provide a simple API into the package manager (if applicable) among other distribution-level details.

What's required?

Sosreport loads policies dynamically in the same way that it loads plugins. All policies live in $SOSROOT/policies/ and are plain python source files. The name of the python source file does not matter, but it makes the most sense to name them after the distribution that they support. Inside the file there must be at least one class that subclasses the Policy superclass and implements a few methods.

The class below represents the minimum set of things that a policy is required to do to be useful to Sosreport.

import os
from sos.policies import Policy
from sos.plugins import MyPluginSuperClass

class MyPolicy(Policy):

    distro = "MyDistro"
    vendor = "MyVendor"
    vendor_url = "http://www.example.com/"
    vendor_text = ""
    valid_subclasses = [MyPluginSuperClass]

    @classmethod
    def check(class_):
        return os.path.isfile('/path/to/my/distribution/identity')

The distro, vendor, vendor_url and vendor_text class variables are used as as substitutions in the preamble message displayed before collection starts. The base Policy class provides a default generic message but policy implementers are free to override this with their own custom text (using the tags listed above to substitute policy-defined values). The current default policy text is:

    msg = _("""\
This command will collect system configuration and diagnostic information \
from this %(distro)s system. An archive containing the collected information \
will be generated in %(tmpdir)s.

For more information on %(vendor)s visit:

  %(vendor_url)s

The generated archive may contain data considered sensitive and its content \
should be reviewed by the originating organization before being passed to \
any third party.

No changes will be made to system configuration.
%(vendor_text)s
""")

The Red Hat Enterprise Linux policy provides an alternate set of tag definitions and message:

class RHELPolicy(RedHatPolicy):
    distro = "Red Hat Enterprise Linux"
    vendor = "Red Hat"
    vendor_url = "https://access.redhat.com/support/"
    msg = _("""\
This command will collect diagnostic and configuration \
information from this %(distro)s system and installed \
applications.

An archive containing the collected information will be \
generated in %(tmpdir)s and may be provided to a %(vendor)s \
support representative.

Any information provided to %(vendor)s will be treated in \
accordance with the published support policies at:\n
  %(vendor_url)s

The generated archive may contain data considered sensitive \
and its content should be reviewed by the originating \
organization before being passed to any third party.

No changes will be made to system configuration.
%(vendor_text)s
""")
[...]

The check() method is called once per Policy subclass and is responsible for letting Sosreport know if it thinks that the underlying platform is the distribution it understands. The check made by this method should be designed to be mutually exclusive with every other Policy subclass as the first check that returns True wins.

Extras

There are numerous other methods that can be implemented by your Policy class. Many of these methods have usable defaults in the Superclass.

def is_root(self)

This method is responsible for determining if the process owner is a superuser. The default checks the user's uid (on unix). This method must be overriden for systems where testing os.getuid for zero is not sufficient.

def get_preferred_archive(self)

This method is responsible for returning a subclass of the Archive class. There are two concrete implementations provided in sos.archive; TarFileArchive and ZipFileArchive. The default is TarFileArchive. An abstract FileCacheArchive provides a facility for building a temporary cache at a policy defined location in the file system. Subclasses should override the finalize() method to create a final archive file from the temporary tree. This class is used by the current TarFileArchive implementation.

def validate_plugin(self, plugin_class)

The validate_plugin() method is called once for each plugin that is dynamically loaded by Sosreport to determine its whether it should run on the current distribution. This is usually done by checking the superclass of the plugin. It is expected for plugins to tell sos which distribution(s) they support by tagging themselves with the proper superclasses. See How-to-Write-a-Plugin for further information. You should only override this method if you need to do something more complex than checking the plugin's class hierarchy.

Linux Policies

Be aware that if you wish to add a policy for a Linux-based operating system that you may want to start with the LinuxPolicy superclass. This policy contains many helpful defaults that should save you a lot of work.

Package Managers

NOTE: See Issue #80 for discussion of the PackageManager API.

Generally, any platform with a package manager can be supported simply by using the PackageManager superclass and providing a command string that will return a full list of packages on the system in the following format:

FIXME should use name-version-release

package name|major.minor.bugfix version\n

Here is an example from the RedHatPolicy:

self.package_manager = PackageManager('rpm -qa --queryformat "%{NAME}|%{VERSION}\\n"')

If the above doesn't work for you then you will need to implement a PackageManager class that plugins can interact with. Generally all you must do is define how to get the full list of packages:

from sos.policies import PackageManager

class MyPackageManager(PackageManager):

    def all_pkgs(self):
        return {
            'package_name': {
                'name': 'package_name',
                'version': ('1', '0', '0'), # version 1.0.0
                # so on and so on...
            }
        }

The default PackageManager class should work if you lack a real package manager on the system.

class PackageManager(object):

    def all_pkgs_by_name(self, name):
        """
        Return a list of packages that match name.
        """
        return fnmatch.filter(self.allPkgs().keys(), name)

    def all_pkgs_by_name_regex(self, regex_name, flags=None):
        """
        Return a list of packages that match regex_name.
        """
        reg = re.compile(regex_name, flags)
        return [pkg for pkg in self.allPkgs().keys() if reg.match(pkg)]

    def pkg_by_name(self, name):
        """
        Return a single package that matches name.
        """
        try:
            self.all_pkgs_by_name(name)[-1]
        except Exception:
            return None

    def all_pkgs(self):
        """
        Return a list of all packages.
        """
        return []

    def pkg_nvra(self, pkg):
        fields = pkg.split("-")
        version, release, arch = fields[-3:]
        name = "-".join(fields[:-3])
        return (name, version, release, arch)