Build Service/osc plugins

From openSUSE

This document shows how to write osc plugins, and how to use the osc module from external python scripts.


It is probably best shown by example.

Let's say, you want to write a script which looks in the Education:desktop project, and shows metadata of all packages inside this project.

You could do it like this:

#!/usr/bin/python

import os

project = 'Education:desktop'

def run(command):
    input, output = os.popen2(command)
    lines = output.readlines()
    return lines

lines = run('osc ls ' + project)
for line in lines:
    package = line.rstrip('\n')
    meta = run('osc meta pkg ' + project + ' ' + package)
    for line in meta:
        print line


But this way, you only call osc as external process. This means that osc is initializing each time, and that you cannot access internal capabilities of osc.

Instead, you could write a script which imports the osc module, and uses that:

#!/usr/bin/python

import os
import osc.conf
import osc.core

project = 'Education:desktop'

# initialize osc configuration
osc.conf.get_config()
packages = osc.core.meta_get_packagelist(osc.conf.config['apiurl'], 
                                       project)
for package in packages:

    m = osc.core.show_package_meta(osc.conf.config['apiurl'], project, package)
    print .join(m)

This is easier.

Now, let's say you wrote something which could be more generally useful. Could it make sense to integrate the functionality as new osc command? If so, it is easy to do so. Custom osc commands can be thrown into ~/.osc-plugins or /var/lib/osc-plugins, and will be loaded by osc from there. The snippet from above could look like this:

def do_show_packages(self, subcmd, opts, project):
    """${cmd_name}: Show metadata of all packages in an project

    This command shows metadata of all packages in a given project.
    
    ${cmd_usage}
    ${cmd_option_list}
    """

    packages = meta_get_packagelist(conf.config['apiurl'], 
                                        project)

    for package in packages:
        m = show_package_meta(conf.config['apiurl'], project, package)
        print .join(m)

The file's name is important; it must end in .py. Other than that, no restrictions on how it's named, although it is of course wise for its name to match the command name it implements.

The method implemented must start with "do_". There is some other magic going on. A python module named cmdln (see pydoc osc.cmdln for more info) automatically takes care of documenting, argument checking, and more. After installing the plugin, if you call 'osc help', you will notice a new command in the long help output:

   show_packages     Show metadata of all packages in an project

The command is automatically documented:

 % osc help show_packages     
show_packages: Show metadata of all packages in an project

This command shows metadata of all packages in a given project.

usage:
    osc show_packages PROJECT

and it can be called as "osc show_packages <prj>".

There's more to it. It is extremely easy to add command line options.

Here is a more complex example, which also shows command line stuff:

@cmdln.option('-q', '--quiet', action='store_true',
              help='do not show downloading progress')
@cmdln.option('--package', metavar='PACKAGE',
              help='only binaries of this package')
@cmdln.option('-d', '--destdir', default='.', metavar='DIR',
              help='destination directory')
def do_mirror_binaries(self, subcmd, opts, project, repository, architecture):
    """${cmd_name}: Mirror binaries of a project to local directory

    It does download directly from the api server. 
    
    Packages don't need to be "published" to be downloaded.

    ${cmd_usage}
    ${cmd_option_list}
    """

    # Get package list
    filenames = get_binarylist(conf.config['apiurl'],
                               project, repository, architecture,
                               package=opts.package)

    if not os.path.isdir(opts.destdir):
        print "Creating %s" % opts.destdir
        os.makedirs(opts.destdir, 0755)

    progress_meter = True
    if opts.quiet:
        progress_meter = False

    for filename in filenames:

        if os.path.exists('%s/%s' % (opts.destdir, filename)):
            continue

        targetfilename = '%s/%s' % (opts.destdir, filename)

        get_binary_file(conf.config['apiurl'],
                        project, repository, architecture,
                        filename,
                        targetfilename=targetfilename,
                        package=opts.package,
                        progress_meter=progress_meter)

Help output will look like this:

 % osc help mirror_binaries
mirror_binaries: Mirror binaries of a project to local directory

It does download directly from the api server.

Packages don't need to be "published" to be downloaded.

usage:
    osc mirror_binaries PROJECT REPOSITORY ARCHITECTURE 

options:
    -h, --help          show this help message and exit
    -d DIR, --destdir=DIR
                        destination directory
    --package=PACKAGE   only binaries of this package
    -q, --quiet         do not show downloading progress

Plenty other examples can be found by looking inside <python-libdir>/osc/command.py.

This method is very well suited to override, or improve, any existing osc command. If you are not happy with the ls command, just write your own. Or if you have an idea how to improve it or add a new feature, copy and paste the command from osc.command.py to your osc plugin directory and start hacking on it.

There is one difference to note between a command in osc.command.py and a plugged-in command. As indentation matters to Python, you need to indent stuff 4 spaces if you copy it into osc.command.py, or remove 4 spaces the other way round. Other than that, simple copy&paste should do.

And if you implemented a cool command, which you would like to see integrated in the upstream osc, please write to the buildservice-mailinglist now!