The Pulp team is gearing up for the first community release of the v2 code base. Over the next few weeks I’ll be highlighting some of the new features in the v2 release. As with any blog entry, this is scoped to a point in time and the specific details are subject to some change before release. The overall concepts, however, should remain consistent despite any tweaks we make.

Introduction

While the separation of content types makes for a significantly more flexible Pulp installation, it also makes the user experience trickier to handle. Without a priori knowledge of the types supported by a Pulp server, any client provided with the Pulp platform will inevitably lack the usability a custom client would provide. At the same time, it’s cumbersome to require an entirely new client customized for each content type.

The v2 client supports an extension mechanism for adding new functionality to the client. Extensions benefit from the setup and configuration performed by the client, allowing them to focus directly on the UI functionality. Extensions are normal Python code and benefit from all of the abilities of the language.

A side effect of this architecture is that it allows users to augment the client for their specific needs. These additions can be as simple as how data are rendered or as complex as scripting multiple REST API calls into a single client command.

Before I go on, let me start with some simple terminology. A “command” is the actual executable portion of a client call. Commands are organized into “sections”, where sections can be nested for more fine grained organization. Commands can accept “options” from the user to drive its execution. So given the following command:

pulp-admin repo sync run --repo-id demo

The “repo” and “sync” portions represent sections; sync is a subsection of repo. The “run” component is the actual command that is invoked. The execution method for that command will be handed a dictionary mapping “repo-id” to “demo” to indicate the user’s intentions.

The remainder of this article describes a simple extension. More information will be avaliable in a separate extension development guide as v2 gets closer to release.

CSV Support Demo

One of the simplest extension examples revolves around new display formats for data in the Pulp server. While the built in extensions attempt to format data returned from queries against the server in a reasonable fashion, inevitably there are desired views that aren’t supported out of the box. One of the driving use cases around the extension mechanism is to provide a simple way for the end user to provide such views.

Creating the Extension

Extensions for the admin client are found under /usr/lib/pulp/admin/extensions. Each subdirectory within there is a Python package (the simplest way to achieve this is to add an empty file named __init__.py in the directory).

There is a single entry point into the extension, a file named pulp_cli.py (evntually a different entry point will be provided when we provide an interactive shell, but I’m getting ahead of myself). The client will load that module and call the method with the following signature:

def initialize(context):
   # Extension implementation here

The Context

A copy of the client context is provided to each extension when it is initialized. The context is meant to provide the extension with everything it needs to function. For example, below are a few of the things available in the context:

  • Server Bindings – Bindings, pre-configured from the client configuration, used to make calls against the server.
  • CLI – The CLI framework itself, allowing new sections/commands to be added or existing ones to be removed, allowing user-written extensions to appear and act in the same fashion as Pulp provided functionality.
  • Prompt – A utility for reading user input and formatting data with Pulp’s common look and feel.

Adding Commands to an Existing Section

While entirely new sections and subsections can be added to the CLI, the simplest example is adding a command to an existing section. This demo will add a “csv” command to the existing search section of commands (found at “repo units”). The command will require a single option to indicate the repository being searched. The relevant code is below:

repo_section = context.cli.find_section('repo')
units_section = repo_section.find_subsection('units')
 
command = units_section.create_command('csv', 'repo contents in csv format', display_csv)
command.create_option('--repo-id', 'repository to search', required=True)

In the above example, the “display_csv” argument refers to the method that will be invoked when the command is run. For now, that method is stubbed out:

def display_csv(**kwargs):
    pass

The kwargs is used to capture the user specified options; in this case, “repo-id”.

At this point, the command is installed and accessible to the client. The pulp-admin --map command can be used to display the full structure of sections and commands available and should reflect the csv command:

...
  units: list/search for RPM-related content in a repository
    rpm:          search for RPMs in a repository
    srpm:         search for SRPMs in a repository
    drpm:         search for DRPMs in a repository
    errata:       search errata in a repository
    distribution: list distributions in a repository
    csv:          repo contents in csv format
    all:          search for all content in a repository
...

The client framework will take care of rendering an appropriate usage based on the command’s configuration (for instance, the --repo-id argument is described and flagged as required):

$ pulp-admin repo units csv --help
Command: csv
Description: repo contents in csv format
 
Available Arguments:
 
  --repo-id - (required) repository to search

Server Calls

The display_csv method will need access to the context (more specfically, the server bindings) to retrieve the server data. There are a number of ways to do this; the command base class could be subclassed and handed the context on instantiation for a cleaner approach. For simplicity in this demo, the context is stored in a global variable in the module:

CONTEXT = None
 
def initialize(context):
    global CONTEXT
    CONTEXT = context

This call will use the unit associations API. To be honest, I’m not terribly happy with how these calls look currently, but even with some API tweaks the concept will remain the same.

repo_id = kwargs['repo-id']
units = CONTEXT.server.repo_search.search(repo_id, {}).response_body

The empty dictionary passed to this call is the query criteria; for this demo, all units in the repository will be returned.

Displaying the Data

At this point, it’s up to the extension writer’s imagination. The data have been retrieved from the server and the extension is normal Python code, so anything is possible. Below is a simple CSV implementation that will contain the RPM’s name and version:

for u in units:
    name = u['metadata']['name']
    version = u['metadata']['version']
    CONTEXT.prompt.write('%s,%s' % (name, version))

Putting It All Together

Below is the full body of the pulp_cli.py module written during this demo:

CONTEXT = None
 
def initialize(context):
    global CONTEXT
    CONTEXT = context
 
    repo_section = context.cli.find_section('repo')
    units_section = repo_section.find_subsection('units')
 
    command = units_section.create_command('csv', 'repo contents in csv format', display_csv)
    command.create_option('--repo-id', 'repository to search', required=True)
 
def display_csv(**kwargs):
    repo_id = kwargs['repo-id']
    units = CONTEXT.server.repo_search.search(repo_id, {}).response_body
 
    for u in units:
        name = u['metadata']['name']
        version = u['metadata']['version']
        CONTEXT.prompt.write('%s,%s' % (name, version))

To test this, the Pulp server has a repository named “pulp” which contains the RPMs found in Pulp’s Fedora 17 v2 testing repository. Sample usage of the CSV command for this repository is as follows:

$ pulp-admin repo units csv --repo-id pulp
python-pulp-common,0.0.305
gofer,0.70
python-pulp-rpm-common,0.0.305
pulp-builtins-admin-extensions,0.0.305
pulp-rpm-yumplugins,0.0.305
mod_wsgi-debuginfo,3.3
grinder,0.1.4
pulp-rpm-consumer-client,0.0.305
pulp-server,0.0.305
python-isodate,0.4.4
python-qpid,0.7.946106
python-pulp-client-lib,0.0.305
mod_wsgi,3.3
python-pulp-agent-lib,0.0.305
pulp-admin-client,0.0.305
python-oauth2,1.5.170
pulp-builtins-consumer-extensions,0.0.305
pulp-consumer-client,0.0.305
python-webpy,0.32
pulp-rpm-handlers,0.0.305
gofer-package,0.70
pulp-rpm-admin-client,0.0.305
pulp-rpm-server,0.0.305
python-okaara,1.0.18
m2crypto-debuginfo,0.21.1.pulp
pulp-rpm-consumer-extensions,0.0.305
python-gofer,0.70
python-pulp-bindings,0.0.305
pulp-rpm-admin-extensions,0.0.305
python-rhsm,0.96.4
pulp-agent,0.0.305
pulp-rpm-plugins,0.0.305
m2crypto,0.21.1.pulp
pulp-rpm-agent,0.0.305

Summary

This demonstration is a small subset of the possibilities of the client’s extension framework. All of the functionality in the v2 client is based on the extension framework and uses the features available in the client context. Both the admin and consumer clients use this framework, allowing the consumer client experience to be customized in the same way.

By release, the goal is to provide a more complete extensions guide, including extensions accessing the client’s configuration, a description of the client’s exception middleware for handling common server errors, and API documentation for all of the server bindings. In the meantime, feel free to ping me (jdob in #pulp on Freenode) with any questions.