Pulp v2 Preview: Client Extensions
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.