Table of Contents
Once you've seen the eBox internals, we are going to apply all the knowledge explained in the previous chapters creating a module from scratch. The first thing to do is choose a network service that you want to integrate into eBox. In our case we've chosen the Network Time Protocol server because it is one of the most simple service that exist in eBox. Our module will provide eBox these features:
Date and time synchronisation with an external server.
Let clients synchronise their time and date with eBox.
And these are the steps we are going to follow in the development for the module:
Study the NTP server and its features.
Create a new module from the module template.
Define and implement an API to manage its configuration.
Develop CGIs and mason templates.
Make our module show up in the eBox menu and in the summary page.
Generate the configuration files and control the daemon execution.
Establish custom rules in the firewall that let our module work.
We've decided to develop this module using the ntp server from www.ntp.org. Debian includes packages for the server so we are going to use two of them: ntp-server to provide the time synchronisation service to clients and ntpdate to make synchronisation queries to external servers.
After choosing the software we have to study how it works, its configuration files and the configurable parameters within them. With all this information we will be able to choose what degree of control the user of our module will have. We'll try to strike a balance between flexibility in the configuration and ease of use.
For the module we are developing we only need to tune some of the
parameters contained in the /etc/ntp.conf
file,
which holds the configuration for the ntp daemon. Of all the possible
configuration options contained in that file, we are only interested in
two:
The list of ntp servers we are going to use to synchronise our date and time. This can be achieved by adding lines like the following as many times as necessary to the configuration file:
server ntp_server_ip
The ability to act as an NTP server for clients in our network.
This is done using the server
attribute, as seen
above, with an special SIP address:
server 127.127.1.0
The NTP server has more configuration options, but we have decided to leave them with sane default values that are transparent to the user. We will concentrate on those features that will be generally most interesting to our users.
We will also add the possibility for the user to manually change the system time and date and the time zone, in case there is no possibility to synchronise the time with external NTP servers.
Time and date modification is easily done using the /bin/date command.
Time zone configuration is easy too.
/etc/localtime
is a symbolic link that points
to a file named after the location we are in. All possible timezones
are stored under the /usr/share/zoneinfo/
directory. Each location is stored inside its continent,
so if our time zone is Madrid/Europe,
then /etc/localtime
must point to
/usr/share/zoneinfo/Europe/Madrid
. The
operation of modifying the time zone is as simple as changing the
/etc/localtime
symbolic link.
As we have seen above, eBox modules have a complex directory
structure that can be tedious to set up. Because of that, a module
template is provided inside the tools directory in the base ebox
module. This template provides a basic skeleton for an eBox module and
can be cloned to create a new one. Note that you should replace the
string modulename
with your module name.
eBox uses autoconf and
automake for module configuration and
installation. The files autogen.sh
,
configure.ac
and Makefile.am
contain the basic autotools configuration for a standard module and are
also included in the template. The template also provides a
m4/ebox.m4
file, used by modules to detect the
current eBox installation paths.
The Autoconf and Automake manuals are the best autotools reference.
At this point we should have gained a good degree of familiarity with the network service we are going to work with. We should also know what features will be exposed to our future users. In addition we have a template to use as a base for our module. The next step is to define the API for our module's backend. We must define which methods will be needed to let the rest of the modules read and write all the configuration options and to manage the daemon: start, stop, etc...
The backend of our module is going to be in the
EBox::NTP
class. It will inherit from
EBox::GConfModule
and it will contain all the
methods that conform the NTP API. This is its constructor:
Example 8.1. EBox::NTP
constructor
sub _create { my $class = shift; my $self = $class->SUPER::_create(name => 'ntp', domain => 'ebox-ntp', @_); bless($self, $class); return $self; }
After the analysis performed in the previous
section we can define the following methods in
EBox::NTP
(remember that we use a leading
underscore for private method names):
setService
service
setSynchronized
synchronized
setServers
servers
setNewData
setNewTimeZone
_restartAllServices
setService
This method gets a boolean argument that enables or disables the NTP service, which will allows clients in the local network synchronise their time and date with eBox.
Here is its implementation:
Example 8.2. Enabling the NTP server
sub setService { my ($self, $active) = @_; if ($active xor $self->service) { $self->set_bool('active', $active); } }
First we read the active
argument
and then we use the service
method
to find out if the server is currently enabled. Only if the
new value is different than the old one we go on to set
the active
gconf key, by calling
set_bool
.
service
This method returns whether the NTP server is currently
enabled or not. Its implementation is trivial, all we need is
to fetch the active
gconf key and return
it:
Example 8.3. Reading the state of the NTP server
sub service { my $self = shift; return $self->get_bool('active'); }
setSynchronized
This method receives a boolean parameter that decides
whether eBox will synchronise its date and time with external
NTP servers. As you can see, its implementation is similar
to setService
, we just use the
synchronized
gconf key instead:
Example 8.4. Enabling the external NTP synchronisation
sub setSynchronized # (synchronized) { my ($self, $synchronized) = @_; if ($synchronized xor $self->synchronized) { $self->set_bool('synchronized', $synchronized); } }
synchronized
This method returns the value of the
synchronized
gconf key.
Its implementation is quite simple and similar to
service
.
Example 8.5. Fetching the configuration for external synchronization
sub synchronized { my $self = shift; return $self->getbool('synchronized'); }
setServers
This allows us to store the names of the ntp servers we
will use to synchronise our time and date in the gconf database.
Its implementation checks whether an SIP address or a domain name
were introduced and checks the syntax of the value accordingly.
If the syntax is correct the servers will be stored in the
gconf database calling the set_string
method. In addition to that, we won't let the user set a
secondary server if no primary server has been set, and the
same for the third server. Let's see part [1]of its implementation:
Example 8.6. Setting the external NTP servers
sub setServers # (server1, server2, server3) { my ($self, $s1, $s2, $s3) = @_; if ($s1 =~ /^(\d{1,3}\.){3}\d{1,3}$/) { checkIP($s1, __("primary server IP address")); $self->set_string('server1', $s1); } else { checkDomainName($s1, __("primary server name ")); $self->set_string('server1', $s1); } if (defined($s2) and ($s2 ne "")) { if ($s2 =~ /^(\d{1,3}\.){3}\d{1,3}$/) { checkIP($s2, __("secondary server IP address")); $self->set_string('server2', $s2); } else { ...
servers
It returns an array that contains the NTP external servers stored in gconf:
Example 8.7. Getting the list of external NTP servers
sub servers { my $self = shift; my @servers; @servers = ($self->get_string('server1'), $self->get_string('server2'), $self->get_string('server3')); return @servers; }
setNewDate
This method changes the system's date and time. This is its implementation:
Example 8.8. Setting a new system time and date
sub setNewDate # (day, month, year, hour, minute, second) { my ($self, $day, $month, $year, $hour, $min, $sec) = @_; my $newdate = "$year-$month-$day $hour:$min:$sec"; my $command = "/bin/date --set \"$newdate\""; root($command); $self->_restartAllServices; }
We receive as arguments every piece of data necessary to set the time and date in the system: day, month, year, hour, minutes and seconds.
We build the complete command in the
command
variable and a call is made to the
root
function that executes it. This
command must be run as root using sudo, that's
why we don't run it directly.
When the system time changes, some system services and eBox
modules need to be restarted. This is implemented in the
_restartAllServices
method seen in Example 8.10.
setNewTimeZone
Along with the time and date, the time zone may be changed too. For this purpose we implemented this method:
Example 8.9. Setting a new time zone
sub setNewTimeZone # (continent, country) { my ($self, $continent, $country) = @_; my $command = "ln -s /usr/share/zoneinfo/$continent/$country" . " /etc/localtime"; $self->set_string('continent', $continent); $self->set_string('country', $country); root("rm /etc/localtime"); root($command); $self->_restartAllServices; }
It gets two arguments: continent
and country
. We use them to redo
the symbolic link in /etc/localtime
so that it points to the new time zone. For example,
if the two arguments are Africa
and
Africa
, /etc/localtime
will be changed so that it points to
/usr/share/zoneinfo/Africa/Dakar
.
We store both arguments in gconf too, making to calls to
set_string
. When the time zone has
been changed some eBox modules and the system log services are
restarted so that they don't run with the wrong time.
_restartAllServices
Operations that change the system's time and date may leave some parts of the system in an inconsistent state. To avoid having eBox modules and system log services with a time shift we'll write a method that will restart all those services so that they get the new time and date. Here it goes:
Example 8.10. Restarting eBox modules and system services
sub _restartAllServices { my $self = shift; my $global = EBox::Global->getInstance(); my @names = grep(!/^network$/, @{$global->modNames}); @names = grep(!/^firewall$/, @names); my $log = $global->logger; my $failed = ""; $log->info("Restarting all modules"); foreach my $name (@names) { my $mod = $global->modInstance($name); try { $mod->restartService(); } catch EBox::Exceptions::Internal with { $failed .= "$name "; }; } if ($failed ne "") { throw EBox::Exceptions::Internal("The following modules ". "failed while being restarted, their state is ". "unknown: $failed"); } $log->info("Restarting system logs"); try { root("/etc/init.d/sysklogd restart"); root("/etc/init.d/klogd restart"); root("/etc/init.d/cron restart"); } catch EBox::Exceptions::Internal with { }; }
First we get an EBox::Global
instance that will build instances of every eBox module.
We restart all modules except network
and firewall, catching any exception
that may be thrown while restarting them. Then we manually
restart the system daemons: sysklogd,
klogd and crond.
Doing this requires root privileges so we invoke the
root
function.
After designing and implementing the API, it is time to create the layer that will interact with it: CGIs and mason templates. As you saw in Section 8.1.1, this module is going to answer NTP queries for clients in the local network. The API that enables and disables this service was implemented in Section 8.1.3. We will now create two CGIs and a mason template that will use these methods to give the user an interface for this feature:
The two CGIs are EBox::CGI::NTP::Index
and
EBox::CGI::NTP::Enable
, the template is called
ntp/index.mas
.
Our first CGI is EBox::CGI::NTP::Index
.
It inherits from EBox::CGI::ClientBase
and
implements a constructor that sets the title for our page and the name
of the template associated to this CGI. This is the constructor:
Example 8.11. Constructor for EBox::CGI::NTP::Index
package EBox::CGI::NTP::Index; use strict; use warnings; use base 'EBox::CGI::ClientBase'; use EBox::Global; use EBox::Gettext; sub new { my $class = shift; my $self = $class->SUPER::new('title' => NTP, 'template' => 'ntp/index.mas', @_); $self->{domain} = "ebox-ntp"; bless($self, $class); return $self; }
A noteworthy detail is the fact that the title string in this CGI is not translatable, and thus we haven't followed the instructions in Section 2.4.1 for i18n. If the title was translatable we would have followed those instructions.
We are now ready to implement the
_process
method, which reads the current
configuration for the NTP server and feeds it to the mason
template.
Example 8.12. Feeding the configuration of the NTP server to the mason template
sub _process { my $self = shift; my $ntp = EBox::Global->modInstance('ntp'); my @array = (); my $active = 'no'; if ($ntp->service()) { $active = 'yes'; } push (@array, 'active' => $active); $self->{params} = \@array; }
You can see that the first thing we do is create an instance of
our module (ntp) using EBox::Global
. We use this
instance to invoke the service
method, which
returns the configuration of the server and we pass it to the mason
template placing it in the param
attribute
of the CGI. If the mason template needed more arguments we would just add
them to the array
variable.
This CGI we just implemented, along with its mason template, shows the current configuration for the NTP server. We will now implement the CGI that will receive a new configuration from the user and will tell the backend to change it.
Example 8.13. CGI to enable and disable the NTP server
package EBox::CGI::NTP::Enable; use strict; use warnings; use base 'EBox::CGI::ClientBase'; use EBox::Global; use EBox::Gettext; sub new { my $class = shift; my $self = $class->SUPER::new('title' => 'NTP', @_); $self->{redirect} = "NTP/Index"; $self->{domain} = "ebox-ntp"; bless($self, $class); return $self; } sub _process { my $self = shift; my $ntp= EBox::Global->modInstance('ntp'); $self->_requireParam('active', __('module status')); $ntp->setService(($self->param('active') eq 'yes')); } 1;
Its implementation is quite simple, only a couple of details are noteworthy.
First, the constructor sets the
redirect
attribute to
NTP/Index
. This will make the browser invoke that
CGI after setting the configuration so that the value shown to the user
is refreshed.
Finally, we use the setService
method,
and we pass it a boolean argument that results from the reading of the
active
parameter from the HTTP request. HTTP
parameters are fetched by invoking the param
method from the parent class.
Next we are going to create the mason template that will
display the configuration and the form to change it. This is an
special case, since most eBox modules will need to enable/disable
network services. A common way of doing this is provided by the
enable.mas
mason template, which is part of the
basic framework.
It is very easy to use it. We create a template that receives an argument with the current configuration of the server. As explained in Section 5.2 we can include an external template by writing:
<& template.mas, @arguments &>
.
We are going to include enable.mas
and we give it two
arguments, the title and the current configuration of the NTP server. It
looks like this:
Example 8.14. Mason template for enabling the NTP server
<%args> $active </%args> <%init> use EBox::Gettext; </%init> <div class='ntpnew'> <br /> <& enable.mas, title => __('Enable the local NTP server'), active => $active &> </div>
Using enable.mas
requires that
the CGI that enables or disables the service be called
Enable
, since that is hard-coded into
enable.mas
. If you take another look at the CGI
we implemented for this purpose, you'll see that its classname is
EBox::CGI::NTP::Enable
, just as required.
We are now missing the CGIs that change the time zone, the time and date, and set the external NTP servers. These CGIs and templates are quite simple, it you want to see their source code you can check the subversion repository, their code will not be shown here, we'll limit ourselves to a quick overview of the files involved and their relationships.
Two CGIs handle the time zone changing feature:
Timezone
uses a mason template
(timezone.mas
) to display the current timezone
configuration.
ChangeTimeZone
receives
the new timezone from the web browser and invokes the
SetNewTimeZone
.
There is one more detail about the
TimeZone
CGI. It sends the current country and
continent to the mason template, but it also sends a list with all
possible continents and a hash that links each continents with the
list of countries it contains. All this information is read from the
/usr/share/zoneinfo/zone.tab
file.
Finally, our module provides two ways to establish the system's
time and date: manually and synchronizing with external NTP servers.
Each of these two methods excludes the other one (the user can only
use one of the two methods). The Datetime
CGI
displays the information about the current time and date and the
configuration of the external NTP servers stored in gconf. Two CGIs let
the user change the settings shown in Datetime
:
Synch
enables the synchronization against
external NTP servers and ChangeDate
changes the
time and date manually.
We just said that only one of the two methods may be used at
the same time. This is enforced by the mason templates, and we are
going to see how it's done. The datetime.mas
template gets its arguments from the Datetime
CGI, it includes the template synch.mas
which lets the user choose whether he wants to set the time
manually or use external NTP servers. Then, depending on the
current configuration it loads the NTP server selection template
(servers.mas
) or the manual time and date
configuration template (date.mas
).
Example 8.15. datetime.mas
template
<& /ntp/synch.mas, title => __('Synchronize with external NTP servers'), synchronized => $synchronized &> % if ($synchronized eq 'yes') { <& /ntp/servers.mas, title => __('External NTP servers'), servers => \@servers &> % } % if ($synchronized eq 'no') { <& /ntp/date.mas, title => __('Change Date and Time'), date => \@date &> % }
synch.mas
, server.mas
and date.mas
just display the information that
datetime.mas
sends them as arguments.
We are already in the final steps in the development of our module. Now we are going to add a new section to the eBox menu that will let the user access the user interface of our module and a section to the summary page with information about the ntp module.
Adding a new section to the menu is as simple as
implementing the menu
method in our
EBox::NTP
class. This method gets an instance
of EBox::Menu::Root
to which we will add
a new NTP section with several items: “NTP Server”,
“Date/Time” and “Time zone”. Here is the
menu
method:
Example 8.16. Adding entries to the eBox menu
sub menu { my ($self, $root) = @_; my $folder = new EBox::Menu::Folder('name' => 'NTP', 'text' => __('NTP')); $folder->add(new EBox::Menu::Item('url' => 'NTP/Index', 'text' => __('NTP server'))); $folder->add(new EBox::Menu::Item('url' => 'NTP/Datetime', 'text' => __('Date/time'))); $folder->add(new EBox::Menu::Item('url' => 'NTP/Timezone', 'text' => __('Time zone'))); $root->add($folder); }
As you saw in Section 5.3, a new section is
created with an instance of EBox::Menu::Folder
,
which needs to be given a name.
The we add to it instances of
EBox::Menu::Item
which needs a name and a URL
(as we explained in Section 5.1, we only need to specify
“NTP/Index”, not the whole path).
The NTP module is not going to have its own section in the
summary page as there is not much information to display. We'll just
add an entry in the status table at top of that page. For that we are going
to implement the statusSummary
method in
EBox::NTP
:
Example 8.17. statusSummary
in
EBox::NTP
sub statusSummary { my $self = shift; return new EBox::Summary::Status('ntp', __('NTP local server'), $self->isRunning, $self->service); }
Let's see how you can use mason templates to generate the config
file for the NTP server. In Section 8.1.1
we saw that the NTP server reads all its configuration from the
/etc/ntp.conf
file. We'll use the same system we
use to generate HTML for this file.
This part of the module belongs in the backend, so
all the methods needed to implemented will be placed in
EBox::NTP
.
We have created a private method called
_setNTPConf
which will be invoked every time
we need to generate the configuration file. Here it is:
Example 8.18. Generating the /etc/ntp.conf
configuration
file
sub _setNTPConf { 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); }
It is very simple, we just add all the arguments for the mason
template to the array
variable. Our arguments
are:
active
It tells the template whether we are going to offer the NTP service for clients in our network or not.
synchronized
It tells the template whether we are going to synchronize our system's time with external NTP servers.
servers
It's an array with the list of external NTP servers.
After building the array with the arguments we call
writeConfFile
, which generates the configuration
file with proper permissions and needs these arguments:
The absolute path to the configuration file. In our case it is the
NTPCONFFILE
constant, which is defined in the
beginning of our module:
use constant NTPCONFFILE => "/etc/ntp.conf";
The path to the mason template that generates the file.
An array with the arguments for the template.
And now let's see the template. We decided to leave some of the values in the configuration file with fixed values, other values are dynamic and are generated based on the arguments received:
Example 8.19. Template to generate
/etc/ntp.conf
<%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 only thing left is the code to manage the ntp
daemon. We have to implement several methods, the first is an
abstract method defined in EBox::Module
:
_regenConfig
. It is invoked when services
are restarted or when a new configuration for a module is saved.
It has to generate the configuration file, using the already seen
_setNTPConf
method. Let's see it:
Example 8.20. _regenConfig
method
sub _regenConfig { my $self = shift; $self->_setNTPConf; $self->_doDaemon(); }
Besides invoking _setNTPConf
,
it needs to restart the daemon, it does so by calling
a private method: _doDaemon
.
Together with methods _stopService
, and
isRunning
, _doDaemon
performs the management of the daemon. Let's see them one by
one:
Example 8.21. NTP daemon management method
sub _doDaemon { my $self = shift; my $logger = EBox::Global->logger; if (($self->service or $self->synchronized) and $self->isRunning) { EBox::Service::manage('ntpd','stop'); sleep 2; if ($self->synchronized) { my $exserver = $self->get_string('server1'); try { root("/usr/sbin/ntpdate $exserver"); } catch EBox::Exceptions::Internal with { $logger->info("Error, ntpdate could" . " not be started."); }; } EBox::Service::manage('ntpd','start'); } elsif ($self->service or $self->synchronized) { if ($self->synchronized) { my $exserver = $self->get_string('server1'); try { root("/usr/sbin/ntpdate $exserver"); } catch EBox::Exceptions::Internal with { $logger->info("Error ntpdate could" . " not be started."); }; } EBox::Service::manage('ntpd','start'); } elsif ($self->isRunning) { EBox::Service::manage('ntpd','stop'); if ($self->synchronized) { EBox::Service::manage('ntpd','start'); } } }
This method is invoked:
To launch the server if it was stopped.
To restart the server when it is running.
To stop the server.
Depending on which case we are in it calls
EBox::Service::manage
telling it which action we want to
perform: start or stop. If the system date is to be synchronised
with external servers we should try to make a manual query with the
/usr/sbin/ntpdate before starting the daemon. This
is a recommended practice before launching the ntp daemon.
Let's see how EBox::Service::manage
works,
it starts, stops or restarts a service directly depending on its
argument. It uses
runit application to manage automatically
daemon by its supervision. This allows us to check the daemon status,
for how long and restart the service every time is goes down.
In order to configure a runit service, it is required to edit a shell script
with the service name, in our case ntpd
, indicating
how the service is run in foreground mode. This file must place in
tools/runit/
directory.
There only two methods left for you to see.
_stopService
is defined as an abstract method by
EBox::Module
, it just stops the service.
isRunning
tells us whether the ntp daemon is
currently running, it does so by checking its process id (PID).
Example 8.23. _stopService
method.
sub _stopService { my $self = shift; if ($self->isRunning) { $self->_daemon('stop'); } }
Example 8.24. Telling whether the NTP daemon is running or not.
sub isRunning { my $self = shift; return $self->pidFileRunning(PIDFILE); }
The last step in creating this module is to let the firewall know
about our needs, so that the ntp service works fine. If our date is set
by querying external servers we will need to make UDP connections on
port 123 to them. We also need to let clients connect to our 123 UDP
port if we are going to be an NTP server. We created a private method
called _configureFirewall
that takes care of
all this stuff. Here it is:
Example 8.25. Firewall configuration
sub _configureFirewall { my $self = shift; my $fw = EBox::Global->modInstance('firewall'); if ($self->synchronized) { $fw->addOutputRule('udp', 123); } else { $fw->removeOutputRule('udp', 123); } if ($self->service and (!defined($fw->service('ntp')))) { $fw->addService('ntp', 'udp', 123, 0); $fw->setObjectService('_global', 'ntp', 'allow'); } elsif ( !($self->service) and defined($fw->service('ntp')) ) { $fw->removeService('ntp'); } }
The eBox firewall module simplifies this job providing
methods that let us add new rules to the firewall. First of all
we need an instance of the firewall module, as usual we use
EBox::Global
to get it. Then we add the
output rule if we need to connect to external servers or remove
it if we do not need to and it had been previously added. The
methods for this are addOutputRule
and
removeOutputRule
.
For our NTP server, we need to register our service with
the firewall by calling addService
and then we'll allow it by default by calling
setObjectService
. If the NTP server feature is
disabled we remove the ntp service from the firewall by calling
removeService
.
We are done, we just created an eBox module form scratch. We've gone through every step needed and seen what issues to watch out for. Now it is time to try it out and do some tests to check that it works as we expect. We hope this guide has been helpful and encourages you to contribute to making it a better platform with new modules and features.