Beaker

Coding guidelines

This page summarizes some of the guidelines that should be helpful while adding new code to Beaker.

Model

From version 0.16 onwards, Object Relational Mapped classes should be defined declaratively (see Declarative). Previous versions used “classical mapping” for some classes (see Classical Mappings).

Some basic guidelines to follow when modifying model:

  • Commonly used queries should be encapsulated as class methods of the respective classes or using hybrid attributes.
  • Enumerated types should be defined as type DeclEnum and not be described in a database schema. This helps avoid over normalization, cuts down on unnecessary calls to the database, and reduces the likelihood of complex joins that confuse the query optimizer. This only applies though if it’s an enumeration that is static.
  • When writing queries, use ORM attributes over SQL expression language whenever possible, and never use sqlalchemy.sql.expression.text().
  • Write efficient queries. Do what you can to write the most reasonably efficient query. For various reasons, Beaker has few options of removing its historical data. Thus, dataset can only increase in size over time. As Beaker’s UI relies heavily on database calls, writing inefficient queries can quickly become a bottleneck and create a marked reduction in usability.
  • Beyond the basic relationship mapping, relationships should be defined keeping performance in mind. The sqlalchemy documentation provides some good ideas (see Working with Large Collections).
  • Remember to define relevant cascade options.

Database column defaults

For database columns with default values, always use a Python-level default: pass a scalar or Python callable as the default parameter for the Column() constructor. See Column Insert/Update Defaults for more details.

Note that this means the default value will not appear on the column definition in the database schema. Schema upgrade notes which add a new column with a default value should look like this (note no DEFAULT clause):

ALTER TABLE power ADD COLUMN power_quiescent_period INT NOT NULL;
UPDATE power SET power_quiescent_period = 5;

Do not use server-side column defaults (server_default parameter, or default with a SQL expression). That way we avoid defining the default value in two places (model code and the database schema); Beaker can skip the extra database round-trip to fetch the generated defaults, which might be expensive in some cases; and we have one consistent mechanism for defining column defaults throughout the application.

Database migrations

Starting with Beaker 0.18, the Beaker server uses Alembic to manage database migrations. The migrations in the alembic/versions contain the changes needed to migrate from older beaker releases to newer versions. A migration occurs by executing a script that details the changes needed to upgrade/downgrade the database. The migration scripts are ordered so that multiple scripts can run sequentially to update the database. The scripts are executed by beaker-init which uses the Alembic library to manage the migration.

A database migration script is required when you submit a change to beaker that alters the database model definition. The migration script is a special python file that includes code to update/downgrade the database to match the changes in the model definition. Alembic will execute these scripts in order to provide a linear migration path between revision:

$ PYTHONPATH=../Common:. python -c '__requires__ = ["CherryPy < 3.0"]; \
    import pkg_resources; from bkr.log import log_to_stream; import sys; \
    from bkr.server.util import load_config;from alembic.config import main; \
    import logging; log_to_stream(sys.stderr, level=logging.INFO); \
    load_config(); main()' -c alembic.ini \
    revision --autogenerate -m "description of revision"

This generates a prepopulated template with the changes needed to match the database state with the models(your working directory should be the Server subdirectory of your local clone of the main beaker project). You should inspect the autogenerated template to ensure that the proper models have been altered. Please see Alembic’s documentation for full detail including caveats and limitations.

In rare circumstances, you may want to start with an empty migration template and manually author the changes necessary for an upgrade/downgrade. You can create a blank file via:

$ PYTHONPATH=../Common:. python -c '__requires__ = ["CherryPy < 3.0"]; \
    import pkg_resources; from bkr.log import log_to_stream; import sys; \
    from bkr.server.util import load_config;from alembic.config import main; \
    import logging; log_to_stream(sys.stderr, level=logging.INFO); \
    load_config(); main()' -c alembic.ini \
    revision -m "description of revision"

To upgrade the database to the latest version:

$ beaker-init

To downgrade the database by a revision identifier:

$ beaker-init --downgrade <revision-identifier>

Controller methods

Starting with Beaker 0.15, the Beaker server uses Flask. The Flask application instance, app needs to be imported from bkr.server.app and then the view function can be exposed by decorating it with @app.route(). You can also specify the HTTP methods which the view can handle using the methods keyword argument. Example:

@app.route('/systems/<fqdn>/access-policy', methods=['POST','PUT'])

To learn more about Flask routing, see here.

CherryPy is embedded inside Flask to support the large number of legacy TurboGears controllers which still exist in Beaker. New code should not use TurboGears or CherryPy unless necessary.

In most controller methods, you may need to perform one or more of the following functions:

  • Authentication: If a view function requires authentication, it should be decorated using the bkr.server.flask_util.auth_required decorator (added in Beaker 0.15.2).
  • Returning data: Use Flask’s jsonify() function to return your response as JSON objects. To learn more, see here.
  • Aborting: If something is not right, raise an appropriate exception from one of the exception classes defined in bkr.server.flask_util (starting with Beaker 0.15.2). For example, raise NotFound404('System not found'). If an appropriate exception is not found, please add one in this module along with your patch.
  • Empty response: If the view function has nothing to return, return an empty string with a status code, like so: return '', 204.

API compatibility

To avoid unnecessary churn for our users, Beaker maintains API compatibility across all maintenance releases in a series (for example 19.0, 19.1, …). Any patches in a maintenance release must not break API compatibility.

APIs can be removed (if absolutely necessary) only after they have been through a deprecation period of at least one release. This entails updating all relevant documentation and code to mark the API as deprecated in version N, and then removing it no sooner than version N+1.

These guidelines apply specifically to (programmatic) HTTP interfaces, XML-RPC interfaces, and the bkr client.

Client–server compatibility

The bkr client must be backwards compatible with at least the previous version of the server (for example, client 20.x must be compatible with server 19.x). New commands are excluded from this requirement.

Also note that the bkr client itself is considered an API for scripting purposes, so it must also maintain API compatibility with older versions of itself as described above.

Logging Activities

If an activity needs to be logged, use the ActivityMixin methods to record it. For example:

system.record_activity(user=identity.current.user,
        service=u'HTTP',field=u'Access Policy Rule', action=u'Removed')

However, for this to be possible, the ORM class should inherit the ActivityMixin class and define an activity_type attribute set to the Activity subclass to use, like so:

class User(MappedObject, ActivityMixin):
    @property
    def activity_type(self):
        return UserActivity
# class definition

Writing tests

The unittest2 package adds a number of additional convenience methods and hence should be preferred for new tests. All existing and new tests should import it as : import unittest2 as unittest.

New selenium tests should use webdriver via WebDriverTestCase.