Copyright © 2005 Warp Networks S.L., DBS Servicios Informáticos S.L.
Copyright © 2006-2007 Warp Networks S.L.
Table of Contents
List of Figures
List of Examples
Object model definitionObject member model definitionJabber dispatcherconfiguration form data model
definitionRows dumped result__n function_regenConfig
implementation_stopService
implementationlogAdminDeferred methodEBox::ObjectsObserver subclass
implementationEBox::NetworkObserversummary sample implementationstatusSummary sample
implementation_groupAddOns implementation_includeLDAPSchemasEBox::NTP constructorEBox::CGI::NTP::Index
datetime.mas templatestatusSummary in
EBox::NTP/etc/ntp.conf configuration
file/etc/ntp.conf_regenConfig methodtools/runit/ntpd file_stopService method._regenConfig for web serviceports.conf.mas templatevhost.mas templatetools/runit/apache2 filetools/runit/apache2.finish file_stopService methodonInstall and onRemove
methods to configure web servicevirtualHosts method_exposedMethods method_exposedMethods usage by a
Perl scriptEBox::Event::Watcher::DiskFreeSpace
constructor
Table of Contents
This guide is written for developers interested in writing new modules with new features for eBox, and for those who need to change or extend the base eBox framework.
eBox is written in perl and is mostly object-oriented. It is assumed that readers know how to write object oriented programs in perl. Some features of the language or the libraries used in eBox may be discussed, but no attempt has been made to document those aspects throughly.
Some experience writing object oriented applications and using design patterns in any language should be useful, but you should be fine just with the basic concepts of object oriented programming.
An effort has been made to provide examples for all aspects of eBox development explained in this guide. Most of them come straight from existing and working modules. Furthermore, a complete module is developed step by step in Chapter 8. It's a real module so that chapter should cover all the parts of a complete and working module.
eBox is a platform for the development and deployment of security and work-group related services on a local network. It is configured through a web interface that integrates all services in a consistent and easy to use way. Its goal is to be usable by non-experts.
eBox is meant to be installed on a dedicated machine, all configuration tasks are performed through the eBox web interface. This means that the configuration of the underlying services is one-way: eBox modules generate configuration files, overwriting system files in some cases (although that tends to be avoided if possible) and manual changes to those files are not detected by eBox. This simplifies the implementation and usage of the package but has the disadvantage that developers need to be careful if they use their own system for testing purposes.
eBox design is modular, new modules providing new services and features can be developed independently from its core package. eBox simplifies the deployment of new modules and the updates of existing ones with a software management module, which is also independent from the eBox base package.
The system is based on Linux and has been developed on top of Debian, no support is provided for other Linux distributions as there are some "debianisms" in some of the modules. Porting to other Linux distributions should be quite easy, and porting to other Unix like operating systems such as OpenBSD would take a little more work but it should still be doable, and worth it.
eBox is based on a few software packages, which are used for several purposes:
eBox relies on some features provided by the kernel, some are available only on the 2.6 series. Among these features are netfilter (for the firewall), 802.1q (for VLANs) and ipsec.
All eBox modules are implemented in perl, the web interface runs under mod_perl for performance reasons.
mason is a templating system for perl, it is used to generate HTML for the web based user interface and to generate configuration files.
The web interface is served by apache, we usually use the apache-perl Debian package.
eBox stores its configuration using gconf2 and its perl library bindings.
Apache runs as an unprivileged user, all eBox modules use sudo to execute commands that need to be run as root.
The base eBox package provides a development framework for new modules. By using this framework modules automatically get features like configuration backups and reversion of changes in the configuration before they are saved. The features available to module developers will be explained in detail in later chapters.
A few conventions are used throughout this guide regarding several concepts:
Booleans. Whenever we need to talk about a boolean value passed
to a method or returned by it, we will refer to a true
value as true and to a false value as
undef or false.
Perl modules and eBox modules. There may be some confusion in some parts of the guide about the term module as the term may refer to an eBox module or a perl module. An eBox module is the complete set of classes (and perl modules) and other files that provide a self-contained functionality for eBox and that may be installed or uninstalled independently from the rest of eBox. A perl module is basically a perl source file.
Whenever there is no possibility of confusion, we'll just write module, otherwise we'll write explicitly perl module since this meaning will be the one used less frequently.
The typical eBox module handles the configuration of a daemon, possibly integrated with other eBox modules. The developer decides in what ways should the user be able to configure the daemon, this ways do not necessarily map directly to the daemon configuration options on a one-to-one relationship. The developer may pick a sane default value for most of the options and hide them from the user, showing him just the ones that he feels are important. Even further, an option changed by the user through the web interface may cause configuration changes in several real configuration options or even on several eBox modules. The main goal is to have an user interface as simple, easy to use, and integrated as possible, while providing the user with a rich set of features.
However, there may be modules that do not handle the configuration of a network service. An example is the 'sysinfo' module in the base system, it just gathers system information to be shown in the Summary page and provides a few menu entries for features that do not belong to any module in particular. The module parent class defines several abstract methods that real modules are free to leave unimplemented. Thus, a module may just provide info in the Summary, add new menu items, handle a network service or all of the above.
The normal, and most interesting, case is the module described in the first paragraph. Such a module has three parts.
It defines and implements an API that will let the GUI, other modules or plain perl scripts configure the daemon it is going to handle.
The second is the GUI, which is a set of CGIs that show the current configuration to the user and let him change it, these CGIs use the API defined earlier to fetch the configuration info and make changes to it.
The third part of the module is usually quite small, it translates all the configuration information stored in GConf into firewall rules, configuration files and commands that make the network service behave as the user expects. It also takes care of starting, stopping and restarting the service when needed.
This separation between the GUI and the backend opens the possibility for other means of changing the configuration. One such means is through perl scripts, these is useful when making packages for a distribution, the package maintainer can write a simple script to import the current system configuration into eBox, or set up some default values. Another use of the API is for other modules, the firewall module is the most used case, almost all modules need tell the firewall to open some port for them. In the future a wrapper may be written around these APIs to publish them through web-services, this would make eBox configurable programmatically over the network.
Besides these three parts, the module has some other minor parts, like its piece of the summary page in the web interface, menu entries, dependency declarations, backups of configuration pieces not stored in GConf, etc.
That's all there is to it, creating a module is as simple as following these steps:
Decide what daemon your module is going to handle and learn how it works and how to set it up.
Plan what options you are going to expose to the user through the web interface, and how they may interact with other eBox modules
Define and implement the API that will let the GUI manipulate
the necessary configuration options. The main class in your module
should inherit from EBox::GConfModule, this
class wraps the GConf API and transparently implements some useful
features that all eBox modules need to have.
Create the CGIs and HTML templates that will let the
user interact with the module. CGIs should inherit from the
EBox::CGI::Base class which, again, provides
some features transparently to all its children.
Write the code needed to make the daemon work, possibly
generating a configuration file and setting up a firewall rule using the
EBox::Firewall class. Configuration files are
generated almost trivially with mason, which is the template system
also used to generate the HTML pages for the GUI.
The size and complexity of a module depends directly on the complexity of the service involved and the amount of configuration items exposed to the user. The necessary work to make a small eBox module is minimal, take the DNSCache module as an example, its CGIs add up to 49 lines of code and the module itself is 134 lines long.
The directory structure of an eBox module may look quite complex for a newcomer. A usual eBox module directory should look like this:
AUTHORS configure.ac INSTALL Makefile.am README stubs/ autogen.sh COPYING NEWS schemas/ tools/ ChangeLog debian/ m4/ po/ src/ www/ migration/
The more important directories are src/,
schemas/, www/,
stubs/ and migration/.
The src/ directory contains the source
code for the module. Inside this directory two directories can be
found: EBox and templates.
The EBox directory contains the Perl source code
files, including a CGI subdirectory with the web
frontend for the module. The templates directory
is used to store the Mason templates, which will be used to generate
the HTML output.
The schemas/ directory contains gconf
schemas, used to define the configuration schemas for modules, and
optionally provide default values for some options.
The www/ directory contains images and
stylesheets that will be used in the web frontend.
The stubs/ is used to store Mason templates
to generate configuration files for the module services.
The migration/ stores the
migration perl scripts
to upgrade from an eBox module version to a newer one.
Bear Section 1.4 in mind, a new way to develop eBox modules have been developed. It relies on Model-View-Controller design pattern, which an architectural pattern used in large software projects to decouple highly data (model) and user interface (view) so that developer can change the data architecture wihout changing the user interface and vice versa. A new component is added, the controller, which manages data access and business logic acting as an intermediate between them.
Thus with our new architecture the eBox MVC conception will be the following:
Our model is defined as a perl module which is a subclass of
EBox::Model::DataTable or one of the subclasses.
The already defined Mason templates are used to show the information to the user.
The CGIs which are mapped from the Mason templates to do actions. There are some default actions already implemented.
In Chapter 7, there is a large explanation about how it works internally. Below we are only going to introduce the main components which an eBox developer requires to know to build up an eBox MVC's module.
A basic MVC based module should have, at least, these three main parts:
A series of data models which represent the information you want to store to configure your desired service.
Manage your service daemon (if any) accessing to the data model, add menu entry, summary and make the data contained in the model(s) available to the eBox system.
Expose the desired API to other eBox modules, perl scripts and SOAP interface.
A model is just a table concept, that is, an array of rows which have a defined fixed
definition, the table description. This table description contains a series of
EBox::Types which are instances of EBox::Types::Abstract,
these eBox types try to include the required representation for eBox configuration parameters,
a brief list could be:
Basic types include are: Boolean, Int,
Text, Password,
Select (an enumeration) or Link, which
describe a type that stores a simple link reference.
These comprise IPAddr which represents an IP address,
MACAddr which is a Ethernet address, Service
which matches with port and protocol such as /etc/services does
and PortRange which describes a single port number,
a port range or any port.
Currently, just a Union type has been developed which
represents a type is used from a defined list of options. If none of options are suitable,
a Union::Text may be used with this purpose.
The data storage is managed automatically by the model itself. A model stores its information
under a GConf directory which must set explicitly at the model creation. However, you can set
dynamically which becomes the model instance in a submodel. The submodels are model instances
whose GConf directory is set dynamically, not only at the runtime. Its usage is intimately with
EBox::Types::HasMany, which describes a relationship between a field from
a model with another model indicating that field contains the whole data from another model.
Apart from the table description, it is required to set several attributes such as the name, the printable name, if it is ordered, etc... Some model definition examples may help to understand the subject:
Example 1.1. Object model definition
sub _table
{
my @tableHead =
(
new EBox::Types::Text
(
'fieldName' => 'name',
'printableName' => __('Name'),
'size' => '12',
'unique' => 1,
'editable' => 1
),
new EBox::Types::HasMany
(
'fieldName' => 'members',
'printableName' => __('Members'),
'foreignModel' => 'MemberTable',
'view' => '/ebox/Object/View/MemberTable',
'backView' => '/ebox/Object/View/MemberTable',
'size' => '1',
)
);
my $dataTable =
{
'tableName' => 'ObjectTable',
'printableTableName' => __('Objects'),
'automaticRemove' => 1,
'modelDomain' => 'Objects',
'defaultActions' => ['add', 'del', 'editField' ],
'tableDescription' => \@tableHead,
'class' => 'dataTable',
'help' => __('Objects'),
'printableRowName' => __('object'),
};
return $dataTable;
}
As it is shown, a model is used to describe an eBox object, which has a name and a group of members which are described in Example 1.2:
Example 1.2. Object member model definition
sub _table
{
my @tableHead =
(
new EBox::Types::Text
(
'fieldName' => 'name',
'printableName' => __('Name'),
'size' => '12',
'unique' => 1,
'editable' => 1
),
new EBox::Types::IPAddr
(
'fieldName' => 'ipaddr',
'printableName' => __('IP Address'),
'editable' => 1,
),
new EBox::Types::MACAddr
(
'fieldName' => 'macaddr',
'printableName' => __('MAC Address'),
'editable' => 1,
'optional' => 1
),
);
my $dataTable =
{
'tableName' => 'MemberTable',
'printableTableName' => __('Members'),
'automaticRemove' => 1,
'defaultActions' => ['add', 'del', 'editField' ],
'modelDomain' => 'Objects',
'tableDescription' => \@tableHead,
'class' => 'dataTable',
'help' => __('Objects'),
'printableRowName' => __('member'),
};
return $dataTable;
}
As you may see, every field has some attributes as well as the model itself. Just note the actions are the allowed actions to apply to that model. For instance, you can add, delete rows, edit fields... Several attributes are allowed for every type and model, if they are not explicitly set at the model definition, they are treated as false value. That is, if it is a boolean type or empty string if the type is an string.
The best part of it comes from forms. Typical eBox scenarios contain a set of tables and forms. So the information that a form may show can be stored in a data model as described above. However, it has some significant differences. A form data model looks like a table data model with just one row which is always shown to be edited. A form model definition is similar to a table data model one as Example 1.3 depicts:
Example 1.3. Jabber dispatcherconfiguration form data model
definition
sub _table
{
my @tableDesc =
(
new EBox::Types::Text(
fieldName => 'server',
printableName => __('Jabber server name'),
size => 12,
editable => 1,
),
new EBox::Types::Int(
fieldName => 'port',
printableName => __('Port'),
size => 6,
editable => 1,
defaultValue => 5222,
),
new EBox::Types::Text(
fieldName => 'user',
printableName => __('Jabber user name'),
size => 12,
editable => 1,
),
new EBox::Types::Password(
fieldName => 'password',
printableName => __('User password'),
size => 12,
editable => 1,
minLength => 4,
maxLength => 25,
),
new EBox::Types::Boolean(
fieldName => 'subscribe',
printableName => __('Subscribe'),
editable => 1,
),
new EBox::Types::Text(
fieldName => 'adminJID',
printableName => __('Administrator Jabber Identifier'),
size => 12,
editable => 1,
),
);
my $dataForm = {
tableName => 'JabberDispatcherForm',
printableTableName => __('Configure Jabber dispatcher'),
modelDomain => 'Events',
tableDescription => \@tableDesc,
class => 'dataForm',
help => __('In order to configure the Jabber event dispatcher ' .
'is required to be registered at the chosen Jabber ' .
'server or check subscribe to do register. The administrator ' .
'identifier should follow the pattern: user@domain[/resource]'),
};
return $dataForm;
}
As you may notice, fields at a form can contain default values. This is done due to have default eBox configuration working. For example, you may want to enable a service to a specific port whose value does not modify the service if the default value is set.
Once the definition is set you may want to validate the user input. Types do some
validation, for instance, IPAddr checks if the parameters
contain a valid IP. However, there are cases where the developer requires to do
extra validation. To achieve so, there are two methods to override
validateRow and validateTypedRow.
Furthermore, actions have callbacks to called after an action have been finished.
Therefore add action has addedRowNotify,
edit updatedRowNotify and so on. This
allows you to manage the updated information to do something different from just
storing updating information at GConf database.
This section explains how the data model is shown at eBox user interface. If you just want to show a data model in eBox, a new menu entry is required. Its URL must have this pattern: ${modelDomain}/View/${tableName}. The modelDomain and tableName are attributes which have been already set at the data model definition. A table model will show up an empty table and a HTML form will appear if a form model is defined, they are named as their viewers.
You may wish to combine the several models in the same page. Here where the model composites play its role. The model composites are containers which stores two kinds of components: models and composites, thus you can create complex composites to find the more usable and adapted user interface for your needs. The current available layouts are:
The components are shown in order from top to the bottom.
The component viewers are displayed in a tabbed way. That is, each component is shown when the tab name is clicked
Define a model composite is even simpler than a model one as it depicts in this example:
Example 1.4. A sample model composite definition
sub _description
{
my $description =
{
components => [
'EnableForm',
'ConfigurationComposite',
],
layout => 'top-bottom',
name => 'GeneralComposite',
printableName => __('Events'),
compositeDomain => 'Events',
help => __('Events module may help you to make eBox ' .
'inform you about events that happen at eBox ' .
'in some different ways'),
};
return $description;
}
The components are referenced using its name, tableName at models and name at composites. In order to display a composite based page, it is required to change the menu entry. In this case, the URL to call must follow this pattern ${compositeDomain}/Composite/${name}. That's all you need to display your models together in a single page.
Just get the magic done, it is required to publish the models and composites.
To achieve so, the eBox modules, which are subclasses of
EBox::GConfModule, must inherit from
EBox::Model::ModelProvider and
EBox::Model::CompositeProvider interfaces, implementing
its respectively methods models and
composites, which returns an array reference of model and
composite instances that lives within this eBox module realm. An example could be
the next one:
Example 1.5. Model and composite publishing example
sub models {
my ($self) = @_;
return [
$self->configureLogModel(),
$self->forcePurgeModel(),
];
}
sub composites
{
my ($self) = @_;
return [
$self->_configureLogComposite(),
];
}
Once the models are published and ready to be displayed, they actually have to do something to manage its service. Current data model API is quite experimental in the way it is constantly evolving to satisfy the client needs. The main components that use data model are migration scripts and eBox module itself to obtain the required data to configure the service.
The main method to retrieve the data model is rows which
returns an array reference which contains the rows from that data model. You can filter
the result using a filter string or get the result from
a defined page, since data model allows filtering and paging. Each
array component contains the return value from row which is a
hash reference containing the following key - value pairs:
Each row has an identifier which allows distinguishing from the others
inside the data model. You can use it as well as indexer calling the
row method.
A data model can be ordered or not. If so, the rows are ordered in the same
order they have been added. Furthermore, you might tailor the order overriding
_tailoredOrder to give rows in the desired order.
Array reference which contains eBox types, which were explained previously.
Hash reference indexed by field name whose values are eBox types as values does.
For convenience, some methods have been added to help accessing the methods. This one returns a hash reference containing field name - value pairs.
Similar to the previous one, it returns a hash reference containing field name - printable value pairs.
The rows may be quite complex. An dumped from a result
could clarify the subject:
Example 1.6. Rows dumped result
[
{
'order' => undef,
'plainValueHash' => {
'members' => {
...
},
'name' => 'RRHH',
'id' => 'x6666'
},
'valueHash' => {
'members' => bless( {
...
}, 'EBox::Types::HasMany' ),
'name' => bless( {
'unique' => 1,
'value' => 'RRHH',
'printableName' => 'Name',
'model' => $VAR1->[0]{'valueHash'}{'members'}{'model'},
'HTMLSetter' => '/ajax/setter/textSetter.mas',
'optional' => 0,
'size' => '12',
'row' => $VAR1->[0],
'editable' => 1,
'type' => 'text',
'HTMLViewer' => '/ajax/viewer/textViewer.mas',
'fieldName' => 'name'
}, 'EBox::Types::Text' )
},
'values' => [
$VAR1->[0]{'valueHash'}{'name'},
$VAR1->[0]{'valueHash'}{'members'}
],
'id' => 'x6666',
'printableValueHash' => {
'members' => {
'model' => 'MemberTable',
'values' => [
{
'macaddr' => undef,
'name' => 'Claudia',
'id' => 'memb44',
'ipaddr' => '192.168.1.92/32'
}
],
'directory' => 'objectTable/keys/x6666/members'
},
'name' => 'RRHH',
'id' => 'x6666'
},
'readOnly' => 0
},
];
With the ease of use in mind, form data model allows accessing directly to the value,
printable value attributes or the whole type using as method name the field name. For instance,
at jabber dispatcher configuration form data model,
you can access to the value, printable value and the instanced type itself of
user property calling userValue,
userPrintableValue and userType respectively.
Nevertheless, you may search for rows which match a fixed criterion, this implies the
match is done against the field value. Four methods make the
work for you, find, findAll,
findValue and findValueAll. They are divided
in two groups, the ones which returns the valueHash and the ones
which returns the printableValueHash and has the 'Value' word in their names.
Moreover, if the method ends with 'All' words, returns not just the first row which matchs
but also the possible remainder rows which match the criterion as well. Taking the previous
data model a search with $model->find( 'name' => 'RHHH' ), the dumped return value
is:
{
'members' => {
'model' => 'MemberTable',
'values' => [
{
'macaddr' => undef,
'name' => 'Claudia',
'id' => 'memb44',
'ipaddr' => '192.168.1.92/32'
}
],
'directory' => 'objectTable/keys/x6666/members'
},
'name' => 'RRHH',
'id' => 'x6666'
}
Final part of developing with MVC framework is expose the desired API to other eBox modules, perl scripts and SOAP clients. Two main options are probable, just disclose the instanced data model allowing full access to the model using in the way we used above or integrate some helper methods to access to the most important features which clients use most. The latter is likely the chosen if the data model rises in its complexity. A module which consists of several data models connected with each other indicates that a simple API may help the module's clients.
As you may see, the MVC framework is powerful scheme to develop in eBox. However, it is not so impressive yet. It is still under development and some optimization as well as lack of some features. For instance, an eBox type to manage file uploading from the user, automatic getter and setter methods,...
Table of Contents
The EBox::Global class offers various
module management functions. The most commonly used functions are those
regarding module instantiation. EBox::Global
works as a module factory, you get an instance of it using the
getInstance static method and then you can use
that instance to create modules. The factory comes in two flavors, a
read-only flavor and a read-write one.
Calling getInstance without arguments
will yield a read-write factory, which creates modules that let
you make calls which change the configuration. A very important
detail of read-write modules is that they return their latest
configuration information, even if it has not been saved (which
means that it could be revoked later by the user). This idea is
important, configuration info reported by a read-write module
should not be treated as final, unless that module has no changes
waiting to be saved. You can see if a module has unsaved changes
by calling the modIsChanged method in the
EBox::Global class.
The idea behind the read-write modules behavior is to make it easy to build a configuration front-end. For example, if the user creates a new network object in the objects module, the new object will show up instantly in the firewall configuration, so he can create new firewall rules that use it. After all the desired changes have been made, the user saves the configuration. If he decides to cancel the changes he just made, both the new object and the firewall rules will be deleted.
There is one situation when you don't want to get information
that has not been saved yet. That's when you are generating the
configuration file for a daemon, setting up firewall rules, setting
the address for a network interface, or any other activity that
needs the real, final configuration info. This situation happens
in system scripts (the boot script, cron jobs, etc). In these
cases you need what we call read-only modules, they only report
saved information and they do not allow method calls that change
the configuration of the module. To obtain read-only modules you
create EBox::Global instance setting its
readonly parameter to true.
Module instances returned by a factory created in this way will be
read-only ones. This script shows how to get an instance of the squid
module and tell it to restart itself:
Example 2.1. Creating a read-only module instance
#!/usr/bin/perl
use strict;
use warnings;
use EBox;
use EBox::Global;
use Error qw(:try);
EBox::init();
my $global = EBox::Global->getInstance(1);
my $squid = $global->modInstance('squid');
try {
$squid->restartService();
} catch EBox::Exceptions::Base with {
print STDERR "Squid module failed to restart.\n";
};There are two functions that make it easy to perform two
common tasks: getting an instance of every module and getting
and instance of every module that implements some abstract
class. These are modInstances and
modInstancesOfType. Using them is straight
forward:
Example 2.2. Functions for instantiating more than one module
my $global = EBox::Global->getInstance(1);
# restart all modules
foreach my $mod (@{$global->modInstances()}) {
$mod->restartService();
}
# restart only modules that implement the NetworkObserver class
foreach my $mod (@{$global->modInstancesOfType('EBox::NetworkObserver')}) {
$mod->restartService();
}Restarting all the modules is even easier than
that, EBox::Global provides the
restartAllModules just for that. It's
one of the methods that perform an operation on all the installed
modules:
restartAllModulesCalls the restartService method
on all the modules. This has the effect of restarting all the
services handled by eBox. Config files are regenerated every time
a service is restarted.
stopAllModulesCalls the stopService method on
all the modules, this includes the web based administration
interface, so be careful.
revokeAllModulesCancels the configuration changes that have been made in all the modules since the last time the configuration was saved.
saveAllModulesSaves the config changes in all modules.
All error handling in eBox is implemented with
java-style exceptions. These are provided by the perl
module Error, you can check the perl
documentation for that module for a detailed description on how
to use exceptions in perl. eBox exceptions are defined in the
EBox::Exceptions namespace.
eBox provides a hierarchy of exceptions from which you can choose the most appropriate for each situation. All eBox exceptions inherit from one of these two exceptions:
EBox::Exceptions::External
EBox::Exceptions::Internal
The difference between these two kinds is what the user will see when an uncaught exceptions occurs. The user interface framework will show the message contained in the exception unaltered if the exception is an external one. You should use external exceptions for user induced errors, like a syntax error in an IP address given by the user interface to the module backend.
If the exception is internal it will log the error and assume something is wrong internally in eBox (a bug) or in the system in general. When an internal exception is caught by the GUI framework the user will see a generic error message, recommending him to check the system logs or seek technical support. Any exception, internal or external, may be used internally for other purposes, as long as it is caught before it goes all the way up to the GUI framework.
The internal exceptions developed are:
EBox::Exceptions::CommandException raised when there was an error
launching a command, that is, its returned value is different
from zero. EBox::Exceptions::Command::Sudo
does the same but with a sudo command.
EBox::Exceptions::DataInUseException raised when there is a data in eBox which it is about to be removed which it is being used by another part of eBox.
EBox::Exceptions::DeprecatedMethodException raised when a deprecated method has been called at runtime.
EBox::Exceptions::InvalidTypeException raised when the type of an argument is not the expected one.
EBox::Exceptions::LockException raised when a module cannot get the lock.
EBox::Exceptions::MissingArgumentException raised when a compulsory argument is missing in a method call
EBox::Exceptions::NotImplementedException raised when a method is not yet implemented. It could be used in an Abstract class in order to simulate its behaviour.
The external exceptions developed are:
EBox::Exceptions::DataExistsException raised when a user wants to add an element to eBox which already exists.
EBox::Exceptions::DataMissingException raised when a user ignores a compulsory element which has to be filled to apply the configuration change.
EBox::Exceptions::DataNotFoundException raised when a user searches for an element which does not exist in eBox.
EBox::Exceptions::InvalidDataException raised when a user enters a value for a data which is invalid. An advice to the user may be set.
If you can't find an exception for certain error condition,
you have two options: create a new exception class, inheriting
from either EBox::Exceptions::External or
EBox::Exceptions::Internal, or just use one
those two classes as generic exceptions:
Example 2.3. Throwing a generic internal exception
if ($foo_condition) {
throw EBox::Exceptions::Internal('Something happened!');
}As you can see, throwing exceptions follows a syntax very similar to Java's. Catching them is very similar too:
Example 2.4. Catching an exception
use Error qw(:try);
sub foo
{
my $bar = shift;
try {
$bar->doSomething();
} catch EBox::Exceptions::Base with {
# do nothing, just ignore the error
};
}You can do whatever you like inside the “catch” clause. You may also write several “catch” clauses if you need to do different things for different exception types. “otherwise” and “finally” clauses are also allowed, for a detailed explanation about them just run perldoc Error. A very important detail is not to forget the semicolon after the last clause (the “catch” in the example), failing to do so may produce very weird results, this is due to the black magic that is used in the implementation of the try-catch idiom.
It is very important that every function exposed by each module validates its input data correctly. All of the arguments need to be checked, if this wasn't done a module could save syntactically wrong values in its configuration, and the underlying service would behave in an unpredictable way with the configuration file generated by the module. This also helps find bugs in the GUI front-end, and any other code that uses the module.
The EBox::Validate perl module provides a
bunch of functions to validate different types of data. All of this
functions work the same way, their first argument is the value that's
going to be checked. They return true if the value
is correct, undef if it's not:
Example 2.5. Using data validation functions
use EBox::Validate qw(:all); my $ip = '192.168.0.1'; unless (checkIP($ip)) print STDERR "$ip is invalid.\n"; }
The usual thing to do if an argument is incorrect is to
throw an exception. And most of the time the incorrect value will
be an user-supplied one. For this reasons, all the functions in
EBox::Validate provide an easy mechanism for
throwing EBox::External exceptions. All you have
to do is pass one more argument to them, with a name or description for
the value you are trying to check. If you pass that argument and the
validation fails, an exception will be thrown with a message indicating
the name of the wrong field:
Example 2.6. Using data validation functions with automatic error handling
checkIP($ip, 'IP address');
If you don't want an exception to be thrown, or if you want to throw a different kind of exception, just do not pass the last argument and handle the error yourself.
eBox uses gettext, the GNU project's internationalization (i18n) platform. It makes it easy for translators to translate the eBox user interface into their own language.
This section describes i18n from the point of view of both the developer (what steps must be taken to build a translatable module) and the translator (how to translate eBox into a new language).
Every eBox module has it's own gettext domain for the text strings it owns, the domain name is derived from the module name. Modules must set their gettext domain both in their constructor and in each one of their CGIs as explained in Section 3.1.1 and Example 5.7 respectively.
All files that contain translatable strings, including mason
templates, must include the EBox::Gettext
perl module. Also, every translatable string must be marked as such,
using the __ function as the following example
shows:
print __("Hello world");
This will enable the eBox build system to detect and extract all the translatable strings.
If a string includes a variable you should not concatenate it:
print __("Edit ") . $group . __(" members"); # wrong
This would make it impossible to correctly translate the whole
sentence. The right way to do it is to use the
__x function, which is a variant of
__ which lets you include variables:
print __x("Edit {group} members", group => $group); # right
This way the translator can place the variable
group in the right position in the sentence.
If you want to mark a string as translable but determining not
to translate it, you should use __n function. An
example of usage is the following:
Example 2.7. Using __n function
my $options = [__n('Foo'), __n('Bar'), __n('Foobaz') ]; # As many as you want
print __($options[$index]);
The example showed above gives the chance to translator to translate only the option selected leaving the remainder (a high number normally) without a translation.
If you follow this rules correctly, the build system will automatically generate PO files with all translatable strings included. These PO files can be easily translated, as explained in the next section.
Using GNU gettext as an i18n platform means we have the most widely used format for translations: PO files. Every eBox module has its one PO file for every available language. A translator just needs to translate all strings listed in that file in order to have a completely translated module. In case you are starting a translation for a new language from scratch, all you need to do is ask for a PO file to be generated for your language.
A PO file contains the text strings in the original language (English) along with their translations. Each string may be in one for three possible states: translated (a translation exists for that string), fuzzy (a translation exists, but the original string has changed slightly) or untranslated:
# translated string msgid "Name" msgstr "Nombre" # fuzzy string #, fuzzy msgid "Interfaces" msgstr "Interface" # untranslated string msgid "External" msgstr ""
If the string includes the value of a variable when it is shown in the user interface you will see an entry like this one in the PO file:
msgid "Edit {group} members"
In this case you just have to rewrite the sentence placing the name of the variable (in curly braces) in its proper place, but without translating it. The correct translation for this string in Spanish would look like this:
msgstr "Editar miembros de {group}"
The preferred way to translate PO files is using our Web interface which is developed by Pootle, a Web application introduced in order to lower the barrier to start translating eBox.
Even though it's possible to translate a PO file using any UTF-8 enabled text editor or some of the created specifically for this task, such as KBabel (*nix) or poEdit (multi-platform). This is the unrecommended way to translate, however, it could be useful for translators which have to start its home localisation from scratch sending the po files to the eBox team after editing with these tools.
Table of Contents
The eBox framework offers modules several functionalities that are made available through a mostly object-oriented API. Not everything is object-oriented, procedural interfaces have been used where it made sense to do so.
Some of the features of the API work through inheritance, these usually provide a means for the module to implement standard functionalities like menus, the status page, configuration saving and revoking, etc. Some of these functionalities need to be implemented by the module, others are given by the framework for free, and the module may override or extend them.
All eBox modules inherit from the
EBox::Module class, this class defines abstract
methods that modules may override to implement certain functionality.
These methods are called by the framework when they are needed.
Besides those abstract methods, the class implements a few methods, providing some basic functionality. These methods often follow the template method design pattern, they perform some operation but delegate some part of it on some abstract method that may be implemented by child classes.
Finally, there are a few methods that implement some common
operations that get used in most of the modules, these are meant to be
called by children classes when they need them. They are located in the
EBox::Module class just for convenience.
All module instances are created and cached by the
EBox::Global class, module constructors must
not be called directly except by EBox::Global.
For this reason, they are named _create
instead of new.
The leading underscore in the method name is a naming
convention, methods with such a name are meant to be used
privately by the owner class and its ancestors, they should
not be called directly by an unrelated class. In the case of
the module constructor, the only class that should call it is
EBox::Global.
The _create method in
EBox::Module takes two named arguments
from the child classes. name is the name
of the module, it is required; domain is
the gettext domain for the module, it's
optional and “ebox” is its default value.
Example 3.1. Simple module constructor
sub _create
{
my $class = shift;
my $self = $class->SUPER::_create(name => 'dhcp',
domain => 'ebox-dhcp',
@_);
bless ($self, $class);
return $self;
}The apache server under which eBox runs, and any Perl script that uses the eBox API run under a dedicated user id, the user is typically called “ebox”. eBox modules need to execute certain commands and write certain files with root privileges, this is done using sudo
You can invoke any command using the
root()
function within the EBox::Sudo Perl module. If the
command fails, root() throws an
EBox::Exceptions::Sudo::Command exception, so make sure you catch it if it is OK for the command to
fail or if you want to inform the user in a different
way. Anyway, you can use
the output,
error and
exitValue exception methods
to gather more information about the command failure.
In the rare cases when the sudo program itself fails the
exception raised is one of the
EBox::Exceptions::Sudo::Wrapper kind
and the last methods are not available.
Example 3.2. Using EBox::Sudo::root()
try {
EBox::Sudo::root("/usr/bin/eject -t");
}
catch EBox::Exceptions::Sudo::Wrapper with {
throw EBox::Exceptions::Internal("sudo program failed");
}
catch EBox::Exceptions::Sudo::Command with {
my ($ex) = @_;
EBox::debug("/usr/bin/eject failed. Stderr was " . (join "\n", @{ $ex->error() })) ;
throw EBox::Exceptions::External("Cannot close CD-ROM tray. Please close it manually" );
}
EBox::Sudo also provides
wrappers for frequent used
commands executed with root
privileges. stat provides a
statistic wrapper, useful to get file system information
for any file. If you need a more concise file
information you can use
fileTest to check files for
simples queries like /usr/bin/test does.
eBox uses GConf to store its configuration, the development framework provides a wrapper around the original perl bindings. GConf gives us an easy API for storing and retrieving typed configuration values organized hierarchically. It also lets us define limited schemas for certain configuration keys, setting their type and default values.
The development framework defines a wrapper around the GConf API
that provides some extra features. The wrapper is implemented as child
class for EBox::Module, so all modules that want
to use GConf inherit from EBox::GConfModule. Its
children automatically get these features:
Automatic backups: the first time a key's value is changed,
a backup of the configuration tree for the whole module is made.
When the configuration is saved, the backup is automatically
deleted, when the configuration changes are revoked the backup is
automatically restored. EBox::GConfModule
implements the makeBackup,
restoreBackup and
revokeConfig methods
defined in EBox::Module, so
EBox::GConfModule children do not need to
implement them unless they have really special requirements or
store part of their configuration outside of GConf.
Error handling: GConf errors are handled by the wrapper class, it translates them into internal exceptions.
Read-only and read-write instances: when the module is instantiated in read-only mode, the wrapper class uses a copy of the configuration to avoid seeing unsaved changes. It also prevents calls to methods that write to GConf.
Namespace boundary checks: the wrapper checks all gconf keys used by the module to see if they are in its namespace. This ensures that the keys for a module are only directly read or written to by the module itself (as long as all modules use the wrapper class).
Relative pathnames: methods in the wrapper class take both absolute and relative pathnames for gconf keys. The root of a module's namespace changes depending on the type of instance (read-only of read-write) and the type of key (normal or status) being accessed. For this reason it is best to use relative pathnames when calling the wrapper class methods, they translate the relative path to an absolute one automatically.
Status namespace: the is certain information that's not
given by the user and needs to be written at any time, even by
read-only instances. This kind of information is not subject to
configuration saving and revoking. It is status information,
like the name servers or IP address given to the system by an
external DHCP server. A separate namespace is provided for this
kind of information, and again it is automatically kept out of the
backup/save/revoke operations mentioned earlier. The methods use to
access this namespace are identical to the usual ones, they just
have the prefix st_ in their names.
Recursive retrieval of directories: there are two
methods in EBox::GConfModule that
allow easy retrievals of whole directory structures.
hash_from_dir takes a directory as
an argument and returns a hash with all the keys under it.
array_from_dir takes a directory as
an argument and returns an array of hashes as returned by
hash_from_dir for each one of its
subdirectories.
This is a list of the most important methods in
EBox::GConfModule:
all_dirsGiven a key it returns all directories within.
all_dirs_baseGiven a key it returns all directories within, removing any leading directory component.
all_entriesGiven a key it returns all entries within. Entries are all those keys which are not directories, hence they contain a value.
all_entries_baseGiven a key it returns all entries within, removing any leading directory component. Entries are all those keys which are not directories, hence they contain a value.
array_from_dirGiven a key it returns an array using a hash reference to
contain in each element the directories under the key. Also, the
hash contains the key _dir which tells
you the directory's name.
dir_existsGiven a key referencing a directory it returns
true if it exists.
get_boolReturns the value of a boolean key.
get_intReturns the value of a integer key.
get_listReturns an array containing the list referenced by the key.
get_stringReturns the value of a string key.
get_unique_idIt generates a unique random identifier with a leading
prefix in the root of the module's
namespace, if directory is passed, it will
be added to the path. Note that it does not create the entry, it
just returns a unique identifier, so it is up to you to create
the proper entry.
hash_from_dirIt returns a hash containing all the entries in the directory referenced by the key.
isReadOnlyIt returns true if the current
EBox::GConfModule instance you are
accessing is read-only.
makeBackupIt dumps your current configuration to file.
restoreBackupIt restores the last backup.
revokeConfigAll changes done since your first write or delete will be dismissed.
set_boolIt sets a key with a boolean
value.
set_intIt sets a key with an integer
value.
set_listIt sets a list of
typevalues in
value.
set_stringIt sets values of
type in key.
Let's see some examples from the above functions.
Example 3.5. Getting and using a unique identifier
my $id = $self->get_unique_id('p', 'printers');
$self->set_string("printers/$id/name", $name);
$self->set_bool("printers/$id/configured", undef);
The need to keep certain information ordered is fairly common
across eBox modules. It is also common to provide reordering functions.
An example of this are firewall rules, which need to be applied in
a given order, and the user has to be able to change their order.
The EBox::Order class solves just this
problem.
The idea is to give a directory to each item you want to
keep ordered. Following the firewall example, we could have a
rules/ directory and, below it, one directory
per rule. Each rule would get a unique identifier, and that
would be its directory name below rules/.
A rule with id r3561 would be stored in
rules/r3561, and below that directory we would
store keys with each one of the rule's parameters. This is the most
natural way of organizing items such as firewall rules, and it is with
this organization that EBox::Order is designed
to work.
The ordering mechanism adds one field to the
items being ordered. Not surprisingly it is called
order. To use the ordering API you have
to create an EBox::Order instance. Its
constructor takes to arguments: the instance of the module that owns
the items being ordered, and the base directory where the items are
stored.
EBox::Order implements these
operations:
highestReturns the highest order key of
all the items.
lowestReturns the lowest order key of
all the items.
nextnGiven a number, returns the
order key for the next item.
prevnGiven a number, returns the
order key for the previous
item.
getReturns the identifier for the item whose
order key equals a given
number.
swapIt finds the items whose order
keys match two given numbers and swaps their values.
listReturns a reference to an array that holds the identifiers of all items, ordered from lowest to highest order.
Example 3.6. Ordering firewall rules
Let's see how the firewall module uses
EBox::Order to keep its forwarding rules
ordered. The _fwdRulesOrder returns the
EBox::Order instance for the firewall
rules:
sub _fwdRulesOrder
{
my $self = shift;
return new EBox::Order($self, "fwdrules");
}fwdrules is the directory that
holds all the rules. Another private helper function is
_fwdRuleNumer, it returns the order number
for a given rule identifier:
sub _fwdRuleNumber # (rule)
{
my ($self, $rule) = @_;
return $self->get_int("fwdrules/$rule/order");
}New rules are appended at the end of the list, so we find the
highest order number and add one to it, this code is part of the
addFwdRule method:
my $order = $self->_lastFwdRule() + 1;
$self->set_string("fwdrules/$id/name", $id);
$self->set_string("fwdrules/$id/action", $action);
$self->set_bool("fwdrules/$id/active", 1);
$self->set_int("fwdrules/$id/order", $order);_lastFwdRule is a trivial wrapper that
returns the highest ordered number:
sub _lastFwdRule
{
my $self = shift;
my $order = $self->_fwdRulesOrder();
defined($order) or return 0;
return $order->highest;
}Finally there are two methods to allow rule
reordering, they are FwdRuleUp and
FwdRuleDown (only the first one is shown
here since they are almost identical):
sub FwdRuleUp # (rule)
{
my ($self, $rule) = @_;
my $order = $self->_fwdRulesOrder();
defined($order) or return;
my $num = $self->_fwdRuleNumber($rule);
if ($num == 0) {
return;
}
my $prev = $order->prevn($num);
$order->swap($num, $prev);
}Besides exposing an API that allows reading and changing the configuration of a given service, a module's backend is in charge of making that service work. Typically, the service will be some sort of daemon that offers some functionality across the network after reading a configuration file. So your module needs to generate the configuration file and start/stop/restart the daemon at the right times.
The easiest way to generate configuration files is using the mason templating engine which is also used in the web front-end. Using mason templates is documented in Section 5.2 so we won't repeat it here.
Mason templates for configuration files are installed in the
stubs directory under the shared directory
for eBox. In the module source tree they are usually placed in
directory of their own called stubs too. The
Makefile.am in the stubs
directory for the dns-cache module looks like this:
Stubdir = @STUBSPATH@/dns-cache nobase_Stub_DATA = named.conf.mas named.conf.options.mas \ named.conf.local.mas EXTRA_DIST = $(nobase_Stub_DATA) MAINTAINERCLEANFILES = Makefile.in
The autoconf macro included in
ebox.m4 automatically exports the stubs
directory path as STUBSPATH, so you just need
to put create a directory for your module in it and put your mason
templates there.
There is a method in EBox::Module
that helps with file permissions and other details. It is called
writeConfFile and it takes three
arguments:
The path of the configuration file that's going to be generated.
The path of the mason template relative to the stubs directory.
A reference to the arguments that you want to pass to the mason template.
writeConfFile will generate the configuration
file in a temporary location and then copy it on top of the desired
destination, keeping its original permissions and ownership.
Example 3.7. Generating a configuration file
This is the code that generates the configuration file in the NTP module:
my $self = shift; my @array = (); my @servers = $self->servers; my $synch = 'no'; my $active = 'no'; ($self->synchronized) and $synch = 'yes'; ($self->service) and $active = 'yes'; push(@array, 'active' => $active); push(@array, 'synchronized' => $synch); push(@array, 'servers' => \@servers); $self->writeConfFile(NTPCONFFILE, "ntp/ntp.conf.mas", \@array);
And this is the template the generates the
ntp.conf file:
<%args>
$active
$synchronized
@servers
</%args>
# /etc/ntp.conf, configuration for ntpd
# Generated by EBox
driftfile /var/lib/ntp/ntp.drift
statsdir /var/log/ntpstats/
% if ($synchronized eq 'yes') {
% if ($servers[0]) {
server <% $servers[0] %>
% }
% if ($servers[1]) {
server <% $servers[1] %>
% }
% if ($servers[2]) {
server <% $servers[2] %>
% }
% }
% if ($active eq 'yes') {
server 127.127.1.0
% }
fudge 127.127.1.0 stratum 13
restrict default kod notrap nomodify nopeer noquery
restrict 127.0.0.1 nomodify
The first thing you need to know is when to start and
stop the daemon you are controlling. Services are started
by calling the restartService or
save methods on a module instance, however
those methods are implemented by EBox::Module
and should normally not be overridden, since they takes
care of logging and/or saving the configuration changes.
Both of these methods call an abstract method when they
need to actually start/restart the service. This method is
_regenConfig, and it's the one that you need
to implement.
_regenConfig should generate
the configuration files for the daemon and start or restart
it. A sample implementation of this method can be found in
Example 3.8. If you need to know whether the
call is caused by a service start/restart or by saving the
configuration changes, you can check the parameters passed to
_regenConfig. When it's called because
the configuration was saved (save())
the named argument save will be set
to 1, if it is just a regular service
restart (restartService()), the
restart argument will be set to
1. In most situations you won't need this, since
you can easily know whether the daemon is running, this is only
useful for special cases like the network module.
When _regenConfig gets called,
you'll probably need to know whether to start or restart the
daemon, because choosing the wrong operation may produce an
error. To make that decision you need to know if the daemon is
currently running. EBox::Module has two
methods to make this task easier. If you know the process ID of
the process you can use pidRunning,
it receives a process ID as an argument. If you just know the
name of the file where the daemon stored its process ID, you'll
want to call pidFileRunning, which
takes a file name, checks for a process ID in it and calls
pidRunning. Both methods return true if the
process is running and false if it's not.
Example 3.8. Sample _regenConfig
implementation
sub _regenConfig
{
my $self = shift;
$self->_setBindConf;
$self->_doDaemon();
}
sub _doDaemon
{
my $self = shift;
if ($self->service and $self->pidFileRunning(PIDFILE)) {
$self->_daemon('reload');
} elsif ($self->service) {
$self->_daemon('start');
} elsif ($self->pidFileRunning(PIDFILE)) {
$self->_daemon('stop');
}
}
sub _daemon # (action)
{
my ($self, $action) = @_;
my $command = BIND9INIT . " " . $action . " 2>&1";
if ( $action eq 'start') {
root($command);
} elsif ( $action eq 'stop') {
root($command);
} elsif ( $action eq 'reload') {
root($command);
} else {
throw EBox::Exceptions::Internal(
"Bad argument: $action");
}
}Stopping the service is similar, you just check if
it is running and if it is, then run the command that
stops it. As with restartService,
EBox::Module has a template implementation
of the stopService method, which calls an
abstract method when it's time to actually stop the service. The
abstract method, _stopService is the one you
need to implement.
Example 3.9. Sample _stopService
implementation
sub _stopService
{
my $self = shift;
if ($self->pidFileRunning(PIDFILE)) {
$self->_daemon('stop');
}
}A gconf schema is a special gconf key that sets the type and
default value for some other gconf key. They are stored in the gconf
database under the /schemas directory.
You will need to create a gconf schema for you module. This is needed at least to integrate your module within the framework. By means of this schema your module could be instanced. So the first thing you should do is to associate the given name for your module and its proper perl module. Let's look at an example to clarify this idea a little further.
Say we have a module called foobar,
and the implementation for this module lies in
EBox::Foobar. Your gconf schema would look
something like this:
Example 3.10. Minimal gconf schema
<gconfschemafile> <schemalist> <schema> <key>/schemas/ebox/modules/global/modules/foobar/class</key> <applyto>/ebox/modules/global/modules/foobar/class</applyto> <owner>ebox</owner> <type>string</type> <default>EBox::Foobar</default> <locale name="C"/> </schema> </schemalist> </gconfschemafile>
Once you have done this, anytime you instance
the module foobar using the
method modInstance from
EBox::Global, it will know that it has to load
and instance the class EBox::Foobar.
Example 3.11. Creating a module instance
my $foobar = EBox::Global->modInstance('foobar');
$foobar->some_method();Gconf schemas become useful to establish default values. These values will be used by the system when the user has not set a value for the corresponding key.
There is an obvious scenario in which you might be interested in using this feature. This is when your module is installed for the first time, it could come in handy to set default values for your initial configuration.
Let's illustrate this with a simple example. Imagine you are
providing a module to manage a HTTP proxy. One of the configurable
parameters is the listening port. You want the user to have the
capability to change it, but also you wish to provide 3128 as an
initial value. Gconf schema is the right place to do it. For
this example we would use the key
/schemas/ebox/modules/proxy/port.
Example 3.12. Setting a default value in gconf schemas
<schema> <key>/schemas/ebox/modules/proxy/port</key> <applyto>/ebox/modules/proxy/port</applyto> <owner>ebox</owner> <type>int</type> <default>3128</default> <locale name="C"/> </schema>
It is likely your module configuration depends on some other modules. If this occurs to your module, you will be interested in being notified anytime that there is a change in any of the modules you depend on.
Continuing with the example used earlier, consider you want to configure the proxy for listening solely on the internal interfaces. The network module is the one which deals with the network interfaces. So at the time of generating the configuration for the proxy we will ask the network module for the network interfaces configured as internal. This raises an obvious issue, your module should regenerate its configuration whenever the internal interfaces change, that is, when the network module's configuration is changed.
In order to express this relationship of dependence we will use your module's gconf schema as follows:
Example 3.13. Setting module dependencies
<schema> <key>/schemas/ebox/modules/global/modules/proxy/depends</key> <applyto>/ebox/modules/global/modules/proxy/depends</applyto> <owner>ebox</owner> <type>list</type> <list_type>string</list_type> <default>[network]</default> <locale name="C"/> </schema>
The above snippet tells the module global that
proxy depends on network. This way, global will run
the method _regenConfig from
EBox::Proxy when the network module's
configuration changes.
As we pointed out earlier, all changes made on the keys stored
in gconf are automatically backed up the first time a write or delete
operation is carried on. Consequently, all these changes could be
restored automatically if the user wishes to dismiss or restore them
from a file.This is actually true as long as you use the methods
provided by EBox::GConfModule to do so.
If your module has at least some part
independent from GConf, it must override methods
dumpConfig and
restoreConfig.
Additionally, apart from module configuration if
you want to add some other data concerned to the module
which comprise user files, log content and so on; it
must implement the methods
extendedBackup and
extendedRestore. Remember all
these information will just save and restore whether
user requests it explicitly. Let's show how with an
example:
Example 3.14. Overriding backup functions
sub dumpConfig
{
my ($self, $dir) = @_;
$self->{ldap}->dumpLdapData($dir);
}
sub restoreConfig
{
my ($self, $dir) = @_;
$self->{ldap}->loadLdapData($dir);
}
ebox-logs module is
intended to administer eBox logs. We wish to have the
chance to back up its contents. As module
configuration does not include these data, we are
going to use extendedBackup
and extendedRestore. Remember
that this kind of data is only stored if user chooses
it.
Example 3.15. Overriding functions for extended backups
sub extendedBackup
{
my ($self, %params) = @_;
my $dir = $params{dir};
my $dbengine = EBox::DBEngineFactory::DBEngine();
my $dumpFile = "$dir/eboxlogs.dump";
$dbengine->dumpDB($dumpFile);
}
sub extendedRestore
{
my ($self, %params) = @_;
my $dir = $params{dir};
my $dbengine = EBox::DBEngineFactory::DBEngine();
my $dumpFile = "$dir/eboxlogs.dump";
$dbengine->restoreDB($dumpFile);
}
eBox can log every change made through its administrative interface, which is useful to know when something was changed or who changed it. Every eBox module should report every log message to give a comprehensive administrative logging support.
In order to do so, you must include a new package from eBox core
EBox::LogAdmin in your module.
A new module attribute named title is used, you must add it to the create call. For instance:
Example 3.17. Creating an object with administrative logging support
my $self = $class->SUPER::_create(name => 'objects',
title => __n('Objects'),
domain => 'ebox-objects',
@_);
Furthermore, a hash is needed in the constructor with the actions your module
will report with a nicely formatted message.
Following the same example in EBox::Objects module:
Example 3.18. Adding actions to log
$self->{'actions'} = {};
$self->{'actions'}->{'addObject'} = __n('Added object {object}');
$self->{'actions'}->{'addToObject'} =
__n('Added {name} ({ip}/{mask} [{mac}]) to object {object}');
$self->{'actions'}->{'removeObject'} = __n('Removed object {object}');
For each action you want to log, which should be everything that can be done from web interface, you should follow these steps:
Add an action mark to the function, like:
sub addObject # (description)
{
#action: addObject
Call logAdminDeferred or
logAdminNow. logAdminDeferred
puts the action in the database with the field committed set to
false, and should be called for actions that don't take place
until the changes are saved, that is, anything that gets stored in gconf.
When you save changes, the actions are marked as committed, if you revert
changes the actions are removed.
logAdminNow puts the action in the database with the field
committed set to true, and should be used
for actions that take effect immediately like changing the password or adding a user.
Both functions have the same signature:
logAdminDeferred(module,action,params) logAdminNow(module,action,params)
You should call it only when you're sure that the action has taken place, for
example, for addObject:
Example 3.19. Calling logAdminDeferred method
$self->set_string("$id/description", $desc);
logAdminDeferred('objects',"addObject","object=$desc");
return $id;