2
0
mirror of https://gitlab.isc.org/isc-projects/kea synced 2025-08-28 20:47:48 +00:00

[master] Merge branch 'trac4489'

This commit is contained in:
Marcin Siodelski 2016-08-25 12:41:28 +02:00
commit 0d8dc7affb
34 changed files with 915 additions and 370 deletions

View File

@ -4,7 +4,7 @@ EXTRA_DIST = version.ent.in Doxyfile Doxyfile-xml
EXTRA_DIST += devel/config-backend.dox
EXTRA_DIST += devel/contribute.dox
EXTRA_DIST += devel/mainpage.dox
EXTRA_DIST += devel/qa.dox
EXTRA_DIST += devel/unit-tests.dox
nobase_dist_doc_DATA = examples/ddns/sample1.json
nobase_dist_doc_DATA += examples/ddns/template.json

View File

@ -94,7 +94,7 @@ written and observing the test fail, you can detect the situation
where a bug in the test code will cause it to pass regardless of
the code being tested.
See @ref qaUnitTests for instructions on how to run unit-tests.
See @ref unitTests for instructions on how to run unit-tests.
If you happen to add new files or have modified any \c Makefile.am
files, it is also a good idea to check if you haven't broken the

View File

@ -1,4 +1,4 @@
// Copyright (C) 2012-2015 Internet Systems Consortium, Inc. ("ISC")
// Copyright (C) 2012-2016 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
@ -34,6 +34,12 @@
* @section contrib Contributor's Guide
* - @subpage contributorGuide
*
* @section buildingKeaWithUnitTests Building Kea with Unit tests
* - @subpage unitTests
* - @subpage unitTestsIntroduction
* - @subpage unitTestsEnvironmentVariables
* - @subpage unitTestsDatabaseConfig
*
* @section hooksFramework Hooks Framework
* - @subpage hooksdgDevelopersGuide
* - @subpage dhcpv4Hooks
@ -107,9 +113,6 @@
* - @subpage configBackendAdding
* - @subpage perfdhcpInternals
*
* @section qualityAssurance Quality Assurance
* - @subpage qaUnitTests
*
* @section miscellaneousTopics Miscellaneous Topics
* - @subpage logKeaLogging
* - @subpage logBasicIdeas

View File

@ -1,68 +0,0 @@
// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
/**
@page qa Kea Quality Assurance processes
@section qaUnitTests Unit-tests
Kea uses the Google C++ Testing Framework (also called googletest or gtest) as a
base for our C++ unit-tests. See http://code.google.com/p/googletest/ for
details. We used to have Python unit-tests that were inherited from BIND10
days. Those tests are removed now, so please do not develop any new Python
tests in Kea. If you want to write DHCP tests in Python, we encourage you to
take a look at ISC Forge: http://kea.isc.org/wiki/IscForge. You must have \c
gtest installed or at least extracted in a directory before compiling Kea
unit-tests. To enable unit-tests in Kea, use:
@code
./configure --with-gtest=/path/to/your/gtest/dir
@endcode
or
@code
./configure --with-gtest-source=/path/to/your/gtest/dir
@endcode
Depending on how you compiled or installed \c gtest (e.g. from sources
or using some package management system) one of those two switches will
find \c gtest. After that you make run unit-tests:
@code
make check
@endcode
The following environment variable can affect unit-tests:
- KEA_LOCKFILE_DIR - Specifies a directory where the logging system should
create its lock file. If not specified, it is prefix/var/run/kea, where prefix
defaults to /usr/local. This variable must not end with a slash. There is one
special value: "none", which instructs Kea to not create lock file at
all. This may cause issues if several processes log to the same file.
Also see Kea User's Guide, section 15.3.
- KEA_LOGGER_DESTINATION - Specifies logging destination. If not set, logged
messages will not be recorded anywhere. There are 3 special values:
stdout, stderr and syslog. Any other value is interpreted as a filename.
Also see Kea User's Guide, section 15.3.
- KEA_PIDFILE_DIR - Specifies the directory which should be used for PID files
as used by dhcp::Daemon or its derivatives. If not specified, the default is
prefix/var/run/kea, where prefix defaults to /usr/local. This variable must
not end with a slash.
- KEA_SOCKET_TEST_DIR - if set, it specifies the directory where Unix
sockets are created. There's OS limitation on how long a Unix socket
path can be. It is typcially slightly over 100 characters. If you
happen to build and run unit-tests in deeply nested directories, this
may become a problem. KEA_SOCKET_TEST_DIR can be specified to instruct
unit-test to use a different directory. Must not end with slash (e.g.
/tmp).
*/

264
doc/devel/unit-tests.dox Normal file
View File

@ -0,0 +1,264 @@
// Copyright (C) 2015-2016 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
/**
@page unitTests Building Kea with Unit Tests
@section unitTestsIntroduction Introduction
Kea uses the Google C++ Testing Framework (also called googletest or gtest) as a
base for our C++ unit-tests. See http://code.google.com/p/googletest/ for
details. We used to have Python unit-tests inherited from BIND10
days but have been removed, so please do not write any new Kea unit
tests in Python. (If you want to write DHCP tests in Python, we encourage you to
take a look at ISC Forge: http://kea.isc.org/wiki/IscForge.)
You must have \c gtest installed or at least extracted in a directory
before compiling Kea unit-tests. To enable unit-tests in Kea, use:
@code
./configure --with-gtest=/path/to/your/gtest/dir
@endcode
or
@code
./configure --with-gtest-source=/path/to/your/gtest/dir
@endcode
Depending on how you compiled or installed \c gtest (e.g. from sources
or using some package management system) one of those two switches will
find \c gtest. After that you make and run the unit-tests with:
@code
make check
@endcode
@section unitTestsEnvironmentVariables Environment Variables
The following environment variable can affect the unit tests:
- KEA_LOCKFILE_DIR - Specifies a directory where the logging system should
create its lock file. If not specified, it is <i>prefix</i>/var/run/kea,
where <i>prefix</i> defaults to /usr/local. This variable must not end
with a slash. There is one special value, "none", which instructs Kea to
not create a lock file at all. This may cause issues if several processes
log to the same file. (Also see the Kea User's Guide, section 15.3.)
- KEA_LOGGER_DESTINATION - Specifies the logging destination. If not set, logged
messages will not be recorded anywhere. There are three special values:
stdout, stderr and syslog. Any other value is interpreted as a filename.
(Also see Kea User's Guide, section 15.3.)
- KEA_PIDFILE_DIR - Specifies the directory which should be used for PID files
as used by dhcp::Daemon or its derivatives. If not specified, the
default is <i>prefix</i>/var/run/kea, where <i>prefix</i> defaults to
/usr/local. This variable must not end with a slash.
- KEA_SOCKET_TEST_DIR - if set, it specifies the directory where Unix
sockets are created. There is an operating system limitation on how
long a Unix socket path can be, typically slightly over 100
characters. If you happen to build and run unit-tests in deeply nested
directories, this may become a problem. KEA_SOCKET_TEST_DIR can be
specified to instruct unit-test to use a different directory. It must
not end with slash.
@section unitTestsDatabaseConfig Databases Configuration for Unit Tests
With the use of databases requiring separate authorisation, there are
certain database-specific pre-requisites for successfully running the unit
tests. These are listed in the following sections.
@subsection unitTestsDatabaseUsers Database Users Required for Unit Tests
Unit tests validating database backends require that the <i>keatest</i>
database is created. This database should be empty. The unit tests
also require that the <i>keatest</i> user is created and that this user
is configured to access the database with a password of <i>keatest</i>.
Unit tests use these credentials to create database schema, run test cases
and drop the schema. Thus, the <i>keatest</i> user must have sufficiently
high privileges to create and drop tables, as well as insert and modify the
data within those tables.
The database backends which support read only access to the host
reservations databases (currently MySQL and PostgreSQL) include unit
tests verifying that a database user with read-only privileges can be
used to retrieve host reservations. Those tests require another user,
<i>keatest_readonly</i>, with SQL SELECT privilege to the <i>keatest</i>
database (i.e. without INSERT, UPDATE etc.), is also created.
<i>keatest_readonly</i> should also have the password <i>keatest</i>.
The following sections provide step-by-step guidelines how to setup the
databases for running unit tests.
@subsection mysqlUnitTestsPrerequisites MySQL Database
The steps to create the database and users are:
-# Log into MySQL as root:
@verbatim
% mysql -u root -p
Enter password:
:
mysql>@endverbatim\n
-# Create the test database. This must be called "keatest":
@verbatim
mysql> CREATE DATABASE keatest;
mysql>@endverbatim\n
-# Create the users under which the test client will connect to the database
(the apostrophes around the words <i>keatest</i>, <i>keatest_readonly</i>, and
<i>localhost</i> are required):
@verbatim
mysql> CREATE USER 'keatest'@'localhost' IDENTIFIED BY 'keatest';
mysql> CREATE USER 'keatest_readonly'@'localhost' IDENTIFIED BY 'keatest';
mysql>@endverbatim\n
-# Grant the created users permissions to access the <i>keatest</i> database
(again, the apostrophes around the user names and <i>localhost</i>
are required):
@verbatim
mysql> GRANT ALL ON keatest.* TO 'keatest'@'localhost';
mysql> GRANT SELECT ON keatest.* TO 'keatest_readonly'@'localhost';
mysql>@endverbatim\n
-# Exit MySQL:
@verbatim
mysql> quit
Bye
%@endverbatim
The unit tests are run automatically when "make check" is executed (providing
that Kea has been build with the \c --with-dhcp-mysql switch (see the installation
section in the <a href="http://kea.isc.org/docs/kea-guide.html">Kea Administrator
Reference Manual</a>).
@subsection pgsqlUnitTestsPrerequisites PostgreSQL Database
PostgreSQL set up differs from system to system. Please consult your
operating system-specific PostgreSQL documentation. The remainder of
that section uses Ubuntu 13.10 x64 (with PostgreSQL 9.0+) as an example.
On Ubuntu, PostgreSQL is installed (with <tt>sudo apt-get install
postgresql</tt>) under user <i>postgres</i>. To create new databases
or add new users, initial commands must be issued under this username:
@verbatim
$ sudo -u postgres psql postgres
[sudo] password for thomson:
psql (9.1.12)
Type "help" for help.
postgres=# CREATE USER keatest WITH PASSWORD 'keatest';
CREATE ROLE
postgres=# CREATE DATABASE keatest;
CREATE DATABASE
postgres=# GRANT ALL PRIVILEGES ON DATABASE keatest TO keatest;
GRANT
postgres=# \q
@endverbatim
PostgreSQL versions earlier than 9.0 don't provide an SQL statement for granting
privileges on all tables in a database. In newer PostgreSQL versions, it is
possible to grant specific privileges on all tables within a schema.
However, this only affects tables which exist when the privileges are granted.
To ensure that the user has specific privileges to tables dynamically created
by the unit tests, the default schema privileges must be altered.
The following example demonstrates how to create the user <i>keatest_readonly</i>,
which has SELECT privilege to the tables within the <i>keatest</i> database,
in Postgres 9.0+. For earlier versions of Postgres, it is recommended to
simply grant full privileges to <i>keatest_readonly</i>, using the
same steps as for the <i>keatest</i> user.
@verbatim
$ psql -U postgres
Password for user postgres:
psql (9.1.12)
Type "help" for help.
postgres=# CREATE USER keatest_readonly WITH PASSWORD 'keatest';
CREATE ROLE
postgres=# \q
$ psql -U keatest
Password for user keatest:
psql (9.1.12)
Type "help" for help.
keatest=> ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES to keatest_readonly;
ALTER DEFAULT PRIVILEGES
keatest=> \q
@endverbatim
Note that the <i>keatest</i> user (rather than <i>postgres</i>) is used to grant
privileges to the <i>keatest_readonly</i> user. This ensures that the SELECT
privilege is granted only on the tables that the <i>keatest</i> user can access
within the public schema.
Now we should be able to log into the newly created database using both user
names:
@verbatim
$ psql -d keatest -U keatest
Password for user keatest:
psql (9.1.12)
Type "help" for help.
keatest=> \q
$ psql -d keatest -U keatest_readonly
Password for user keatest_readonly:
psql (9.1.12)
Type "help" for help.
keatest=>
@endverbatim
If instead of seeing keatest=> prompt, your login is refused with an error
code about failed peer or indent authentication, it means that PostgreSQL is
configured to check unix username and reject login attempts if PostgreSQL names
are different. To alter that, the PostgreSQL configuration must be changed -
the <tt>/etc/postgresql/9.1/main/pg_hba.conf</tt> config file
has to be altered. (It may be in a different location in your system.) The following
lines:
@verbatim
local all all peer
host all all 127.0.0.1/32 md5
host all all ::1/128 md5
@endverbatim
need to be replaced with:
@verbatim
local all all password
host all all 127.0.0.1/32 password
host all all ::1/128 password
@endverbatim
Another possible problem is that you get no password prompt. This is
most probably because you have no <tt>pg_hba.conf</tt> config file
and everybody is by default trusted. As it has a very bad effect
on the security you should have been warned this is a highly unsafe
configuration. The solution is the same, i.e., require password or
md5 authentication method.
If you lose the postgres user access you can first add:
@verbatim
local all postgres trust
@endverbatim
to trust only the local postgres user. Note the postgres user can
be pgsql on some systems.
Please consult your PostgreSQL user manual before applying those changes as
those changes may expose your other databases that you run on the same system.
In general case, it is a poor idea to run anything of value on a system
that runs tests. Use caution!
The unit tests are run automatically when "make check" is executed (providing
that Kea has been build with the \c --with-dhcp-pgsql switch (see the installation
section in the <a href="http://kea.isc.org/docs/kea-guide.html">Kea Administrator
Reference Manual</a>).
*/

View File

@ -11,11 +11,13 @@
<title>Databases and Database Version Numbers</title>
<para>
Kea stores leases in one of several supported databases.
As future versions of Kea are released, the structure of those
databases will change. For example, Kea currently only stores
lease information: in the future, additional data - such as host
reservation details - will also be stored.
Kea supports storing leases and host reservations (i.e. static
assignments of addresses, prefixes and options) in one of
the several supported databases. As future versions of Kea
are released, the structure of those databases will change.
For example, Kea currently only stores lease information
and host reservations: in the future, additional
data - such as subnet definitions - will also be stored.
</para>
<para>
@ -710,6 +712,18 @@ $ <userinput>kea-admin lease-upgrade cql -n <replaceable>database-name</replacea
</section>
</section> <!-- end of CQL sections -->
<section>
<title>Using read only databases with host reservations</title>
<para>If read only database is used for storing host reservations,
Kea must be explicitly configured to operate on the database in
the read only mode.
Sections <xref linkend="read-only-database-configuration4"/> and
<xref linkend="read-only-database-configuration6"/> describe when
such configuration may be reqired and how to configure Kea to
operate using read only host database.
</para>
</section>
<section>
<title>Limitations related to the use of the SQL databases</title>
@ -726,6 +740,7 @@ $ <userinput>kea-admin lease-upgrade cql -n <replaceable>database-name</replacea
2147483647 seconds since the beginning of the epoch (around year 2038).
</para>
</section>
</section> <!-- End of Database sections -->
</chapter>

View File

@ -537,6 +537,38 @@ If a timeout is given though, it should be an integer greater than zero.
If there is no password to the account, set the password to the empty string
"". (This is also the default.)</para>
</section>
<section id="read-only-database-configuration4">
<title>Using Read-Only Databases for Host Reservations</title>
<para>
In some deployments the database user whose name is specified in the database backend
configuration may not have write privileges to the database. This is often
required by the policy within a given network to secure the data from being
unintentionally modified. In many cases administrators have inventory databases
deployed, which contain substantially more information about the hosts than
static reservations assigned to them. The inventory database can be used to create
a view of a Kea hosts database and such view is often read only.
</para>
<para>
Kea host database backends operate with an implicit configuration to both
read from and write to the database. If the database user does not have
write access to the host database, the backend will fail to start and the
server will refuse to start (or reconfigure). However, if access to a read
only host database is required for retrieving reservations for clients
and/or assign specific addresses and options, it is possible to explicitly
configure Kea to start in "read-only" mode. This is controlled by the
<command>readonly</command> boolean parameter as follows:
<screen>
"Dhcp4": { "hosts-database": { <userinput>"readonly": true</userinput>, ... }, ... }
</screen>
Setting this parameter to <userinput>false</userinput> would configure the
database backend to operate in "read-write" mode, which is also a default
configuration if the parameter is not specified.
</para>
<note><para>The <command>readonly</command> parameter is currently only supported
for MySQL and PostgreSQL databases.</para></note>
</section>
</section>
<section id="dhcp4-interface-configuration">

View File

@ -477,6 +477,7 @@ If a timeout is given though, it should be an integer greater than zero.
If there is no password to the account, set the password to the empty string
"". (This is also the default.)</para>
</section>
</section>
<section id="hosts6-storage">
@ -537,8 +538,40 @@ If a timeout is given though, it should be an integer greater than zero.
If there is no password to the account, set the password to the empty string
"". (This is also the default.)</para>
</section>
<section id="read-only-database-configuration6">
<title>Using Read-Only Databases for Host Reservations</title>
<para>
In some deployments the database user whose name is specified in the database backend
configuration may not have write privileges to the database. This is often
required by the policy within a given network to secure the data from being
unintentionally modified. In many cases administrators have inventory databases
deployed, which contain substantially more information about the hosts than
static reservations assigned to them. The inventory database can be used to create
a view of a Kea hosts database and such view is often read only.
</para>
<para>
Kea host database backends operate with an implicit configuration to both
read from and write to the database. If the database user does not have
write access to the host database, the backend will fail to start and the
server will refuse to start (or reconfigure). However, if access to a read
only host database is required for retrieving reservations for clients
and/or assign specific addresses and options, it is possible to explicitly
configure Kea to start in "read-only" mode. This is controlled by the
<command>readonly</command> boolean parameter as follows:
<screen>
"Dhcp6": { "hosts-database": { <userinput>"readonly": true</userinput>, ... }, ... }
</screen>
Setting this parameter to <userinput>false</userinput> would configure the
database backend to operate in "read-write" mode, which is also a default
configuration if the parameter is not specified.
</para>
<note><para>The <command>readonly</command> parameter is currently only supported
for MySQL and PostgreSQL databases.</para></note>
</section>
-->
</section>
<section id="dhcp6-interface-selection">
<title>Interface selection</title>

View File

@ -459,6 +459,17 @@ Debian and Ubuntu:
Kea is built. This section covers the building of Kea with MySQL and/or PostgreSQL
and the creation of the lease database.
</para>
<note>
<simpara>
When unit tests are built with Kea (--with-gtest configuration option is specified),
the databases must be manually pre-configured for the unit tests to run.
The details of this configuration can be found in the
<ulink url="http://git.kea.isc.org/~tester/kea/doxygen">Kea Developer's
Guide</ulink>.
</simpara>
</note>
<section>
<title>Building with MySQL Support</title>
<para>

View File

@ -275,6 +275,12 @@
"item_type": "integer",
"item_optional": true,
"item_default": 0
},
{
"item_name": "readonly",
"item_type": "boolean",
"item_optional": true,
"item_default": false
}
]
},

View File

@ -302,6 +302,12 @@
"item_type": "integer",
"item_optional": true,
"item_default": 0
},
{
"item_name": "readonly",
"item_type": "boolean",
"item_optional": true,
"item_default": false
}
]
},

View File

@ -83,137 +83,5 @@
- <b>user</b> - database user ID under which the database is accessed. If not
specified, no user ID is used - the database is assumed to be open.
@section dhcp-backend-unittest Running Unit Tests
With the use of databases requiring separate authorisation, there are
certain database-specific pre-requisites for successfully running the unit
tests. These are listed in the following sections.
@subsection dhcp-mysql-unittest MySQL Unit Tests
A database called <i>keatest</i> must be created. A database user, also called
<i>keatest</i> (and with a password <i>keatest</i>) must also be created and
be given full privileges in that database. The unit tests create the schema
in the database before each test and delete it afterwards.
In detail, the steps to create the database and user are:
-# Log into MySQL as root:
@verbatim
% mysql -u root -p
Enter password:
:
mysql>@endverbatim\n
-# Create the test database. This must be called "keatest":
@verbatim
mysql> CREATE DATABASE keatest;
mysql>@endverbatim\n
-# Create the user under which the test client will connect to the database
(the apostrophes around the words <i>keatest</i> and <i>localhost</i> are
required):
@verbatim
mysql> CREATE USER 'keatest'@'localhost' IDENTIFIED BY 'keatest';
mysql>@endverbatim\n
-# Grant the created user permissions to access the <i>keatest</i> database
(again, the apostrophes around the words <i>keatest</i> and <i>localhost</i>
are required):
@verbatim
mysql> GRANT ALL ON keatest.* TO 'keatest'@'localhost';
mysql>@endverbatim\n
-# Exit MySQL:
@verbatim
mysql> quit
Bye
%@endverbatim
The unit tests are run automatically when "make check" is executed (providing
that Kea has been build with the \--with-dhcp-mysql switch (see the installation
section in the <a href="http://kea.isc.org/docs/kea-guide.html">Kea Administrator
Reference Manual</a>).
@subsection dhcp-pgsql-unittest PostgreSQL Unit Tests
Conceptually, the steps required to run PostgreSQL unit-tests are the same as
in MySQL. First, a database called <i>keatest</i> must be created. A database
user, also called <i>keatest</i> (that will be allowed to log in using password
<i>keatest</i>) must be created and given full privileges in that database. The
unit tests create the schema in the database before each test and delete it
afterwards.
PostgreSQL set up differs from system to system. Please consult your OS-specific
PostgreSQL documentation. The remainder of that section uses Ubuntu 13.10 x64 as
example. On Ubuntu, after installing PostgreSQL (with <tt>sudo apt-get install
postgresql</tt>), it is installed as user <i>postgres</i>. To create new databases
or add new users, initial commands must be issued as user postgres:
@verbatim
$ sudo -u postgres psql postgres
[sudo] password for thomson:
psql (9.1.12)
Type "help" for help.
postgres=# CREATE USER keatest WITH PASSWORD 'keatest';
CREATE ROLE
postgres=# CREATE DATABASE keatest;
CREATE DATABASE
postgres=# GRANT ALL PRIVILEGES ON DATABASE keatest TO keatest;
GRANT
postgres=# \q
@endverbatim
Now we are back to our regular, unprivileged user. Try to log into the newly
created database using keatest credentials:
@verbatim
$ psql -d keatest -U keatest
Password for user keatest:
psql (9.1.12)
Type "help" for help.
keatest=>
@endverbatim
If instead of seeing keatest=> prompt, your login will be refused with error
code about failed peer or indent authentication, it means that PostgreSQL is
configured to check unix username and reject login attepts if PostgreSQL names
are different. To alter that, PostgreSQL configuration must be changed.
Alternatively, you may set up your environment, so the tests would be run from
unix account keatest. <tt>/etc/postgresql/9.1/main/pg_hba.conf</tt> config file
had to betweaked. It may be in a different location in your system. The following
lines:
@verbatim
local all all peer
host all all 127.0.0.1/32 md5
host all all ::1/128 md5
@endverbatim
were replaced with:
@verbatim
local all all password
host all all 127.0.0.1/32 password
host all all ::1/128 password
@endverbatim
Another possible problem is to get no password prompt, in general because
you have no <tt>pg_hba.conf</tt> config file and everybody is by default
trusted. As it has a very bad effect on the security you should have
been warned it is a highly unsafe config. The solution is the same,
i.e., require password or md5 authentication method. If you lose
the postgres user access you can add first:
@verbatim
local all postgres trust
@endverbatim
to trust only the local postgres user. Note the postgres user can
be pgsql on some systems.
Please consult your PostgreSQL user manual before applying those changes as
those changes may expose your other databases that you run on the same system.
In general case, it is a poor idea to run anything of value on a system
that runs tests. Use caution!
The unit tests are run automatically when "make check" is executed (providing
that Kea has been build with the \--with-dhcp-pgsql switch (see the installation
section in the <a href="http://kea.isc.org/docs/kea-guide.html">Kea Administrator
Reference Manual</a>).
*/

View File

@ -1,10 +1,11 @@
// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
// Copyright (C) 2015-2016 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#include <dhcpsrv/database_connection.h>
#include <dhcpsrv/db_exceptions.h>
#include <dhcpsrv/dhcpsrv_log.h>
#include <exceptions/exceptions.h>
@ -86,5 +87,24 @@ DatabaseConnection::redactedAccessString(const ParameterMap& parameters) {
return (access);
}
bool
DatabaseConnection::configuredReadOnly() const {
std::string readonly_value = "false";
try {
readonly_value = getParameter("readonly");
boost::algorithm::to_lower(readonly_value);
} catch (...) {
// Parameter "readonly" hasn't been specified so we simply use
// the default value of "false".
}
if ((readonly_value != "false") && (readonly_value != "true")) {
isc_throw(DbInvalidReadOnly, "invalid value '" << readonly_value
<< "' specified for boolean parameter 'readonly'");
}
return (readonly_value == "true");
}
};
};

View File

@ -54,6 +54,15 @@ public:
isc::Exception(file, line, what) {}
};
/// @brief Invalid 'readonly' value specification.
///
/// Thrown when the value of the 'readonly' boolean parameter is invalid.
class DbInvalidReadOnly : public Exception {
public:
DbInvalidReadOnly(const char* file, size_t line, const char* what) :
isc::Exception(file, line, what) {}
};
/// @brief Common database connection class.
///
@ -112,6 +121,14 @@ public:
/// @return Redacted database access string.
static std::string redactedAccessString(const ParameterMap& parameters);
/// @brief Convenience method checking if database should be opened with
/// read only access.
///
/// @return true if "readonly" parameter is specified and set to true;
/// false if "readonly" parameter is not specified or it is specified
/// and set to false.
bool configuredReadOnly() const;
private:
/// @brief List of parameters passed in dbconfig

View File

@ -1,4 +1,4 @@
// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
// Copyright (C) 2015-2016 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
@ -40,6 +40,13 @@ public:
isc::Exception(file, line, what) {}
};
/// @brief Attempt to modify data in read-only database.
class ReadOnlyDb : public Exception {
public:
ReadOnlyDb(const char* file, size_t line, const char* what) :
isc::Exception(file, line, what) {}
};
};
};

View File

@ -551,6 +551,12 @@ connection including database name and username needed to access it
A debug message issued when the server is about to obtain schema version
information from the MySQL hosts database.
% DHCPSRV_MYSQL_HOST_DB_READONLY MySQL host database opened for read access only
This informational message is issued when the user has configured the MySQL
database in read-only mode. Kea will not be able to insert or modify
host reservations but will be able to retrieve existing ones and
assign them to the clients communicating with the server.
% DHCPSRV_MYSQL_ROLLBACK rolling back MySQL database
The code has issued a rollback call. All outstanding transaction will
be rolled back and not committed to the database.
@ -696,6 +702,12 @@ V6) is about to open a PostgreSQL hosts database. The parameters of the
connection including database name and username needed to access it
(but not the password if any) are logged.
% DHCPSRV_PGSQL_HOST_DB_READONLY PostgreSQL host database opened for read access only
This informational message is issued when the user has configured the PostgreSQL
database in read-only mode. Kea will not be able to insert or modify
host reservations but will be able to retrieve existing ones and
assign them to the clients communicating with the server.
% DHCPSRV_PGSQL_ROLLBACK rolling back PostgreSQL database
The code has issued a rollback call. All outstanding transaction will
be rolled back and not committed to the database.

View File

@ -12,7 +12,6 @@
#include <boost/lexical_cast.hpp>
#include <algorithm>
#include <iterator>
#include <stdint.h>
#include <string>
#include <limits>
@ -213,22 +212,26 @@ MySqlConnection::prepareStatement(uint32_t index, const char* text) {
}
void
MySqlConnection::prepareStatements(const TaggedStatement tagged_statements[],
size_t num_statements) {
// Allocate space for all statements
statements_.clear();
statements_.resize(num_statements, NULL);
text_statements_.clear();
text_statements_.resize(num_statements, std::string(""));
MySqlConnection::prepareStatements(const TaggedStatement* start_statement,
const TaggedStatement* end_statement) {
// Created the MySQL prepared statements for each DML statement.
for (int i = 0; tagged_statements[i].text != NULL; ++i) {
prepareStatement(tagged_statements[i].index,
tagged_statements[i].text);
for (const TaggedStatement* tagged_statement = start_statement;
tagged_statement != end_statement; ++tagged_statement) {
if (tagged_statement->index >= statements_.size()) {
statements_.resize(tagged_statement->index + 1, NULL);
text_statements_.resize(tagged_statement->index + 1,
std::string(""));
}
prepareStatement(tagged_statement->index,
tagged_statement->text);
}
}
void MySqlConnection::clearStatements() {
statements_.clear();
text_statements_.clear();
}
/// @brief Destructor
MySqlConnection::~MySqlConnection() {
// Free up the prepared statements, ignoring errors. (What would we do

View File

@ -240,15 +240,21 @@ public:
///
/// Creates the prepared statements for all of the SQL statements used
/// by the MySQL backend.
/// @param tagged_statements an array of statements to be compiled
/// @param num_statements number of statements in tagged_statements
///
/// @param start_statement Pointer to the first statement in range of the
/// statements to be compiled.
/// @param end_statement Pointer to the statement marking end of the
/// range of statements to be compiled. This last statement is not compiled.
///
/// @throw isc::dhcp::DbOperationError An operation on the open database has
/// failed.
/// @throw isc::InvalidParameter 'index' is not valid for the vector. This
/// represents an internal error within the code.
void prepareStatements(const TaggedStatement tagged_statements[],
size_t num_statements);
void prepareStatements(const TaggedStatement* start_statement,
const TaggedStatement* end_statement);
/// @brief Clears prepared statements and text statements.
void clearStatements();
/// @brief Open Database
///

View File

@ -11,6 +11,7 @@
#include <dhcp/option_definition.h>
#include <dhcp/option_space.h>
#include <dhcpsrv/cfg_option.h>
#include <dhcpsrv/db_exceptions.h>
#include <dhcpsrv/dhcpsrv_log.h>
#include <dhcpsrv/mysql_host_data_source.h>
#include <dhcpsrv/db_exceptions.h>
@ -19,6 +20,7 @@
#include <boost/algorithm/string/split.hpp>
#include <boost/algorithm/string/classification.hpp>
#include <boost/array.hpp>
#include <boost/pointer_cast.hpp>
#include <boost/static_assert.hpp>
@ -846,7 +848,7 @@ private:
size_t start_column_;
/// @brief Option id.
uint64_t option_id_;
uint32_t option_id_;
/// @brief Option code.
uint16_t code_;
@ -916,7 +918,7 @@ private:
//@}
/// @brief Option id for last processed row.
uint64_t most_recent_option_id_;
uint32_t most_recent_option_id_;
};
/// @brief Pointer to the @ref OptionProcessor class.
@ -1113,7 +1115,7 @@ public:
/// @brief Returns last fetched reservation id.
///
/// @return Reservation id or 0 if no reservation data is fetched.
uint64_t getReservationId() const {
uint32_t getReservationId() const {
if (reserv_type_null_ == MLM_FALSE) {
return (reservation_id_);
}
@ -1251,7 +1253,7 @@ public:
private:
/// @brief IPv6 reservation id.
uint64_t reservation_id_;
uint32_t reservation_id_;
/// @brief IPv6 reservation type.
uint8_t reserv_type_;
@ -1272,7 +1274,7 @@ private:
uint8_t prefix_len_;
/// @brief IAID.
uint8_t iaid_;
uint32_t iaid_;
/// @name Indexes of columns holding information about IPv6 reservations.
//@{
@ -1294,7 +1296,7 @@ private:
//@}
/// @brief Reservation id for last processed row.
uint64_t most_recent_reservation_id_;
uint32_t most_recent_reservation_id_;
};
@ -1585,7 +1587,7 @@ private:
std::vector<uint8_t> value_;
/// @brief Option value length.
size_t value_len_;
unsigned long value_len_;
/// @brief Formatted option value length.
unsigned long formatted_value_len_;
@ -1594,7 +1596,7 @@ private:
std::string space_;
/// @brief Option space name length.
size_t space_len_;
unsigned long space_len_;
/// @brief Boolean flag indicating if the option is always returned to
/// a client or only when requested.
@ -1604,7 +1606,7 @@ private:
std::string client_class_;
/// @brief Length of the string holding client classes for the option.
size_t client_class_len_;
unsigned long client_class_len_;
/// @brief Subnet identifier.
uint32_t subnet_id_;
@ -1630,12 +1632,11 @@ public:
/// @brief Statement Tags
///
/// The contents of the enum are indexes into the list of SQL statements
/// The contents of the enum are indexes into the list of SQL statements.
/// It is assumed that the order is such that the indicies of statements
/// reading the database are less than those of statements modifying the
/// database.
enum StatementIndex {
INSERT_HOST, // Insert new host to collection
INSERT_V6_RESRV, // Insert v6 reservation
INSERT_V4_OPTION, // Insert DHCPv4 option
INSERT_V6_OPTION, // Insert DHCPv6 option
GET_HOST_DHCPID, // Gets hosts by host identifier
GET_HOST_ADDR, // Gets hosts by IPv4 address
GET_HOST_SUBID4_DHCPID, // Gets host by IPv4 SubnetID, HW address/DUID
@ -1643,9 +1644,20 @@ public:
GET_HOST_SUBID_ADDR, // Gets host by IPv4 SubnetID and IPv4 address
GET_HOST_PREFIX, // Gets host by IPv6 prefix
GET_VERSION, // Obtain version number
INSERT_HOST, // Insert new host to collection
INSERT_V6_RESRV, // Insert v6 reservation
INSERT_V4_OPTION, // Insert DHCPv4 option
INSERT_V6_OPTION, // Insert DHCPv6 option
NUM_STATEMENTS // Number of statements
};
/// @brief Index of first statement performing write to the database.
///
/// This value is used to mark border line between queries and other
/// statements and statements performing write operation on the database,
/// such as INSERT, DELETE, UPDATE.
static const StatementIndex WRITE_STMTS_BEGIN = INSERT_HOST;
/// @brief Constructor.
///
/// This constructor opens database connection and initializes prepared
@ -1751,6 +1763,15 @@ public:
StatementIndex stindex,
boost::shared_ptr<MySqlHostExchange> exchange) const;
/// @brief Throws exception if database is read only.
///
/// This method should be called by the methods which write to the
/// database. If the backend is operating in read-only mode this
/// method will throw exception.
///
/// @throw DbReadOnly if backend is operating in read only mode.
void checkReadOnly() const;
/// @brief Pointer to the object representing an exchange which
/// can be used to retrieve hosts and DHCPv4 options.
boost::shared_ptr<MySqlHostWithOptionsExchange> host_exchange_;
@ -1776,39 +1797,18 @@ public:
/// @brief MySQL connection
MySqlConnection conn_;
/// @brief Indicates if the database is opened in read only mode.
bool is_readonly_;
};
namespace {
/// @brief Array of tagged statements.
typedef boost::array<TaggedStatement, MySqlHostDataSourceImpl::NUM_STATEMENTS>
TaggedStatementArray;
/// @brief Prepared MySQL statements used by the backend to insert and
/// retrieve hosts from the database.
TaggedStatement tagged_statements[] = {
// Inserts a host into the 'hosts' table.
{MySqlHostDataSourceImpl::INSERT_HOST,
"INSERT INTO hosts(host_id, dhcp_identifier, dhcp_identifier_type, "
"dhcp4_subnet_id, dhcp6_subnet_id, ipv4_address, hostname, "
"dhcp4_client_classes, dhcp6_client_classes) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"},
// Inserts a single IPv6 reservation into 'reservations' table.
{MySqlHostDataSourceImpl::INSERT_V6_RESRV,
"INSERT INTO ipv6_reservations(address, prefix_len, type, "
"dhcp6_iaid, host_id) "
"VALUES (?,?,?,?,?)"},
// Inserts a single DHCPv4 option into 'dhcp4_options' table.
// Using fixed scope_id = 3, which associates an option with host.
{MySqlHostDataSourceImpl::INSERT_V4_OPTION,
"INSERT INTO dhcp4_options(option_id, code, value, formatted_value, space, "
"persistent, dhcp_client_class, dhcp4_subnet_id, host_id, scope_id) "
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 3)"},
// Inserts a single DHCPv6 option into 'dhcp6_options' table.
// Using fixed scope_id = 3, which associates an option with host.
{MySqlHostDataSourceImpl::INSERT_V6_OPTION,
"INSERT INTO dhcp6_options(option_id, code, value, formatted_value, space, "
"persistent, dhcp_client_class, dhcp6_subnet_id, host_id, scope_id) "
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 3)"},
TaggedStatementArray tagged_statements = { {
// Retrieves host information, IPv6 reservations and both DHCPv4 and
// DHCPv6 options associated with the host. The LEFT JOIN clause is used
// to retrieve information from 4 different tables using a single query.
@ -1930,11 +1930,33 @@ TaggedStatement tagged_statements[] = {
{MySqlHostDataSourceImpl::GET_VERSION,
"SELECT version, minor FROM schema_version"},
// Marks the end of the statements table.
{MySqlHostDataSourceImpl::NUM_STATEMENTS, NULL}
};
// Inserts a host into the 'hosts' table.
{MySqlHostDataSourceImpl::INSERT_HOST,
"INSERT INTO hosts(host_id, dhcp_identifier, dhcp_identifier_type, "
"dhcp4_subnet_id, dhcp6_subnet_id, ipv4_address, hostname, "
"dhcp4_client_classes, dhcp6_client_classes) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"},
}; // end anonymouse namespace
// Inserts a single IPv6 reservation into 'reservations' table.
{MySqlHostDataSourceImpl::INSERT_V6_RESRV,
"INSERT INTO ipv6_reservations(address, prefix_len, type, "
"dhcp6_iaid, host_id) "
"VALUES (?,?,?,?,?)"},
// Inserts a single DHCPv4 option into 'dhcp4_options' table.
// Using fixed scope_id = 3, which associates an option with host.
{MySqlHostDataSourceImpl::INSERT_V4_OPTION,
"INSERT INTO dhcp4_options(option_id, code, value, formatted_value, space, "
"persistent, dhcp_client_class, dhcp4_subnet_id, host_id, scope_id) "
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 3)"},
// Inserts a single DHCPv6 option into 'dhcp6_options' table.
// Using fixed scope_id = 3, which associates an option with host.
{MySqlHostDataSourceImpl::INSERT_V6_OPTION,
"INSERT INTO dhcp6_options(option_id, code, value, formatted_value, space, "
"persistent, dhcp_client_class, dhcp6_subnet_id, host_id, scope_id) "
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 3)"}}
};
MySqlHostDataSourceImpl::
MySqlHostDataSourceImpl(const MySqlConnection::ParameterMap& parameters)
@ -1944,23 +1966,43 @@ MySqlHostDataSourceImpl(const MySqlConnection::ParameterMap& parameters)
DHCP4_AND_DHCP6)),
host_ipv6_reservation_exchange_(new MySqlIPv6ReservationExchange()),
host_option_exchange_(new MySqlOptionExchange()),
conn_(parameters) {
conn_(parameters),
is_readonly_(false) {
// Open the database.
conn_.openDatabase();
// Disable autocommit. To avoid a flush to disk on every commit, the global
// parameter innodb_flush_log_at_trx_commit should be set to 2. This will
// cause the changes to be written to the log, but flushed to disk in the
// background every second. Setting the parameter to that value will speed
// up the system, but at the risk of losing data if the system crashes.
my_bool result = mysql_autocommit(conn_.mysql_, 0);
// Enable autocommit. In case transaction is explicitly used, this
// setting will be overwritten for the transaction. However, there are
// cases when lack of autocommit could cause transactions to hang
// until commit or rollback is explicitly called. This already
// caused issues for some unit tests which were unable to cleanup
// the database after the test because of pending transactions.
// Use of autocommit will eliminate this problem.
my_bool result = mysql_autocommit(conn_.mysql_, 1);
if (result != 0) {
isc_throw(DbOperationError, mysql_error(conn_.mysql_));
}
// Prepare all statements likely to be used.
conn_.prepareStatements(tagged_statements, NUM_STATEMENTS);
// Prepare query statements. Those are will be only used to retrieve
// information from the database, so they can be used even if the
// database is read only for the current user.
conn_.prepareStatements(tagged_statements.begin(),
tagged_statements.begin() + WRITE_STMTS_BEGIN);
// Check if the backend is explicitly configured to operate with
// read only access to the database.
is_readonly_ = conn_.configuredReadOnly();
// If we are using read-write mode for the database we also prepare
// statements for INSERTS etc.
if (!is_readonly_) {
// Prepare statements for writing to the database, e.g. INSERT.
conn_.prepareStatements(tagged_statements.begin() + WRITE_STMTS_BEGIN,
tagged_statements.end());
} else {
LOG_INFO(dhcpsrv_logger, DHCPSRV_MYSQL_HOST_DB_READONLY);
}
}
MySqlHostDataSourceImpl::~MySqlHostDataSourceImpl() {
@ -2159,6 +2201,14 @@ getHost(const SubnetID& subnet_id,
return (result);
}
void
MySqlHostDataSourceImpl::checkReadOnly() const {
if (is_readonly_) {
isc_throw(ReadOnlyDb, "MySQL host database backend is configured to"
" operate in read only mode");
}
}
MySqlHostDataSource::
MySqlHostDataSource(const MySqlConnection::ParameterMap& parameters)
@ -2171,6 +2221,9 @@ MySqlHostDataSource::~MySqlHostDataSource() {
void
MySqlHostDataSource::add(const HostPtr& host) {
// If operating in read-only mode, throw exception.
impl_->checkReadOnly();
// Initiate MySQL transaction as we will have to make multiple queries
// to insert host information into multiple tables. If that fails on
// any stage, the transaction will be rolled back by the destructor of
@ -2493,12 +2546,16 @@ std::pair<uint32_t, uint32_t> MySqlHostDataSource::getVersion() const {
void
MySqlHostDataSource::commit() {
// If operating in read-only mode, throw exception.
impl_->checkReadOnly();
impl_->conn_.commit();
}
void
MySqlHostDataSource::rollback() {
// If operating in read-only mode, throw exception.
impl_->checkReadOnly();
impl_->conn_.rollback();
}

View File

@ -8,6 +8,7 @@
#define MYSQL_HOST_DATA_SOURCE_H
#include <dhcpsrv/base_host_data_source.h>
#include <dhcpsrv/db_exceptions.h>
#include <dhcpsrv/mysql_connection.h>
namespace isc {
@ -255,7 +256,7 @@ public:
private:
/// @brief Pointer to the implementation of the @ref MySqlHostDataSource.
MySqlHostDataSourceImpl* impl_;
MySqlHostDataSourceImpl* impl_;
};
}

View File

@ -1,4 +1,4 @@
// Copyright (C) 2012-2015 Internet Systems Consortium, Inc. ("ISC")
// Copyright (C) 2012-2016 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
@ -13,6 +13,7 @@
#include <dhcpsrv/mysql_lease_mgr.h>
#include <dhcpsrv/mysql_connection.h>
#include <boost/array.hpp>
#include <boost/static_assert.hpp>
#include <mysqld_error.h>
@ -82,7 +83,8 @@ const size_t HOSTNAME_MAX_LEN = 255;
/// colon separators.
const size_t ADDRESS6_TEXT_MAX_LEN = 39;
TaggedStatement tagged_statements[] = {
boost::array<TaggedStatement, MySqlLeaseMgr::NUM_STATEMENTS>
tagged_statements = { {
{MySqlLeaseMgr::DELETE_LEASE4,
"DELETE FROM lease4 WHERE address = ?"},
{MySqlLeaseMgr::DELETE_LEASE4_STATE_EXPIRED,
@ -204,9 +206,8 @@ TaggedStatement tagged_statements[] = {
"prefix_len = ?, fqdn_fwd = ?, fqdn_rev = ?, "
"hostname = ?, hwaddr = ?, hwtype = ?, hwaddr_source = ?, "
"state = ? "
"WHERE address = ?"},
// End of list sentinel
{MySqlLeaseMgr::NUM_STATEMENTS, NULL}
"WHERE address = ?"}
}
};
};
@ -1236,7 +1237,7 @@ MySqlLeaseMgr::MySqlLeaseMgr(const MySqlConnection::ParameterMap& parameters)
}
// Prepare all statements likely to be used.
conn_.prepareStatements(tagged_statements, MySqlLeaseMgr::NUM_STATEMENTS);
conn_.prepareStatements(tagged_statements.begin(), tagged_statements.end());
// Create the exchange objects for use in exchanging data between the
// program and the database.

View File

@ -53,7 +53,7 @@ DbAccessParser::build(isc::data::ConstElementPtr config_value) {
// 2. Update the copy with the passed keywords.
BOOST_FOREACH(ConfigPair param, config_value->mapValue()) {
try {
if (param.first == "persist") {
if ((param.first == "persist") || (param.first == "readonly")) {
values_copy[param.first] = (param.second->boolValue() ?
"true" : "false");
@ -72,7 +72,8 @@ DbAccessParser::build(isc::data::ConstElementPtr config_value) {
}
} catch (const isc::data::TypeError& ex) {
// Append position of the element.
isc_throw(isc::data::TypeError, ex.what() << " ("
isc_throw(BadValue, "invalid value type specified for "
"parameter '" << param.first << "' ("
<< param.second->getPosition() << ")");
}
}

View File

@ -129,6 +129,16 @@ PgSqlConnection::prepareStatement(const PgSqlTaggedStatement& statement) {
}
}
void
PgSqlConnection::prepareStatements(const PgSqlTaggedStatement* start_statement,
const PgSqlTaggedStatement* end_statement) {
// Created the PostgreSQL prepared statements.
for (const PgSqlTaggedStatement* tagged_statement = start_statement;
tagged_statement != end_statement; ++tagged_statement) {
prepareStatement(*tagged_statement);
}
}
void
PgSqlConnection::openDatabase() {
string dbconnparameters;

View File

@ -313,6 +313,21 @@ public:
/// failed.
void prepareStatement(const PgSqlTaggedStatement& statement);
/// @brief Prepare statements
///
/// Creates the prepared statements for all of the SQL statements used
/// by the PostgreSQL backend.
///
/// @param start_statement Pointer to the first statement in range of the
/// statements to be compiled.
/// @param end_statement Pointer to the statement marking end of the
/// range of statements to be compiled. This last statement is not compiled.
///
/// @throw isc::dhcp::DbOperationError An operation on the open database has
/// failed.
void prepareStatements(const PgSqlTaggedStatement* start_statement,
const PgSqlTaggedStatement* end_statement);
/// @brief Open Database
///
/// Opens the database using the information supplied in the parameters

View File

@ -10,6 +10,7 @@
#include <dhcp/option.h>
#include <dhcp/option_definition.h>
#include <dhcp/option_space.h>
#include <dhcpsrv/db_exceptions.h>
#include <dhcpsrv/cfg_option.h>
#include <dhcpsrv/dhcpsrv_log.h>
#include <dhcpsrv/pgsql_host_data_source.h>
@ -19,6 +20,7 @@
#include <boost/algorithm/string/split.hpp>
#include <boost/algorithm/string/classification.hpp>
#include <boost/array.hpp>
#include <boost/pointer_cast.hpp>
#include <boost/static_assert.hpp>
@ -35,7 +37,7 @@ namespace {
/// @brief Maximum length of option value.
/// The maximum size of the raw option data that may be read from the
/// database.
/// database.
const size_t OPTION_VALUE_MAX_LEN = 4096;
/// @brief Numeric value representing last supported identifier.
@ -1103,12 +1105,11 @@ public:
/// @brief Statement Tags
///
/// The contents of the enum are indexes into the list of SQL statements
/// The contents of the enum are indexes into the list of SQL statements.
/// It is assumed that the order is such that the indicies of statements
/// reading the database are less than those of statements modifying the
/// database.
enum StatementIndex {
INSERT_HOST, // Insert new host to collection
INSERT_V6_RESRV, // Insert v6 reservation
INSERT_V4_HOST_OPTION, // Insert DHCPv4 option
INSERT_V6_HOST_OPTION, // Insert DHCPv6 option
GET_HOST_DHCPID, // Gets hosts by host identifier
GET_HOST_ADDR, // Gets hosts by IPv4 address
GET_HOST_SUBID4_DHCPID, // Gets host by IPv4 SubnetID, HW address/DUID
@ -1116,9 +1117,20 @@ public:
GET_HOST_SUBID_ADDR, // Gets host by IPv4 SubnetID and IPv4 address
GET_HOST_PREFIX, // Gets host by IPv6 prefix
GET_VERSION, // Obtain version number
INSERT_HOST, // Insert new host to collection
INSERT_V6_RESRV, // Insert v6 reservation
INSERT_V4_HOST_OPTION, // Insert DHCPv4 option
INSERT_V6_HOST_OPTION, // Insert DHCPv6 option
NUM_STATEMENTS // Number of statements
};
/// @brief Index of first statement performing write to the database.
///
/// This value is used to mark border line between queries and other
/// statements and statements performing write operation on the database,
/// such as INSERT, DELETE, UPDATE.
static const StatementIndex WRITE_STMTS_BEGIN = INSERT_HOST;
/// @brief Constructor.
///
/// This constructor opens database connection and initializes prepared
@ -1222,6 +1234,14 @@ public:
StatementIndex stindex,
boost::shared_ptr<PgSqlHostExchange> exchange) const;
/// @brief Throws exception if database is read only.
///
/// This method should be called by the methods which write to the
/// database. If the backend is operating in read-only mode this
/// method will throw exception.
///
/// @throw DbReadOnly if backend is operating in read only mode.
void checkReadOnly() const;
/// @brief Returns PostgreSQL schema version of the open database
///
@ -1258,66 +1278,25 @@ public:
/// @brief MySQL connection
PgSqlConnection conn_;
/// @brief Indicates if the database is opened in read only mode.
bool is_readonly_;
};
namespace {
/// @brief Array of tagged statements.
typedef boost::array<PgSqlTaggedStatement, PgSqlHostDataSourceImpl::NUM_STATEMENTS>
TaggedStatementArray;
/// @brief Prepared PosgreSQL statements used by the backend to insert and
/// retrieve reservation data from the database.
PgSqlTaggedStatement tagged_statements[] = {
// PgSqlHostDataSourceImpl::INSERT_HOST
// Inserts a host into the 'hosts' table. Returns the inserted host id.
{8,
{ OID_BYTEA, OID_INT2,
OID_INT4, OID_INT4, OID_INT8, OID_VARCHAR,
OID_VARCHAR, OID_VARCHAR },
"insert_host",
"INSERT INTO hosts(dhcp_identifier, dhcp_identifier_type, "
" dhcp4_subnet_id, dhcp6_subnet_id, ipv4_address, hostname, "
" dhcp4_client_classes, dhcp6_client_classes) "
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING host_id"
},
//PgSqlHostDataSourceImpl::INSERT_V6_RESRV
// Inserts a single IPv6 reservation into 'reservations' table.
{5,
{ OID_VARCHAR, OID_INT2, OID_INT4, OID_INT4, OID_INT4 },
"insert_v6_resrv",
"INSERT INTO ipv6_reservations(address, prefix_len, type, "
" dhcp6_iaid, host_id) "
"VALUES ($1, $2, $3, $4, $5)"
},
// PgSqlHostDataSourceImpl::INSERT_V4_HOST_OPTION
// Inserts a single DHCPv4 option into 'dhcp4_options' table.
// Using fixed scope_id = 3, which associates an option with host.
{6,
{ OID_INT2, OID_BYTEA, OID_TEXT,
OID_VARCHAR, OID_BOOL, OID_INT8},
"insert_v4_host_option",
"INSERT INTO dhcp4_options(code, value, formatted_value, space, "
" persistent, host_id, scope_id) "
"VALUES ($1, $2, $3, $4, $5, $6, 3)"
},
// PgSqlHostDataSourceImpl::INSERT_V6_HOST_OPTION
// Inserts a single DHCPv6 option into 'dhcp6_options' table.
// Using fixed scope_id = 3, which associates an option with host.
{6,
{ OID_INT2, OID_BYTEA, OID_TEXT,
OID_VARCHAR, OID_BOOL, OID_INT8},
"insert_v6_host_option",
"INSERT INTO dhcp6_options(code, value, formatted_value, space, "
" persistent, host_id, scope_id) "
"VALUES ($1, $2, $3, $4, $5, $6, 3)"
},
TaggedStatementArray tagged_statements = { {
// PgSqlHostDataSourceImpl::GET_HOST_DHCPID
// Retrieves host information, IPv6 reservations and both DHCPv4 and
// DHCPv6 options associated with the host. The LEFT JOIN clause is used
// to retrieve information from 4 different tables using a single query.
// Hence, this query returns multiple rows for a single host.
{2,
{2,
{ OID_BYTEA, OID_INT2 },
"get_host_dhcpid",
"SELECT h.host_id, h.dhcp_identifier, h.dhcp_identifier_type, "
@ -1417,7 +1396,7 @@ PgSqlTaggedStatement tagged_statements[] = {
// are returned due to left joining IPv6 reservations and DHCPv6 options.
// The number of rows returned is multiplication of number of existing
// IPv6 reservations and DHCPv6 options.
{2,
{2,
{ OID_VARCHAR, OID_INT2 },
"get_host_prefix",
"SELECT h.host_id, h.dhcp_identifier, "
@ -1439,14 +1418,59 @@ PgSqlTaggedStatement tagged_statements[] = {
//PgSqlHostDataSourceImpl::GET_VERSION
// Retrieves MySQL schema version.
{0,
{0,
{ OID_NONE },
"get_version",
"SELECT version, minor FROM schema_version"
},
// Marks the end of the statements table.
{0, { 0 }, NULL, NULL}
// PgSqlHostDataSourceImpl::INSERT_HOST
// Inserts a host into the 'hosts' table. Returns the inserted host id.
{8,
{ OID_BYTEA, OID_INT2,
OID_INT4, OID_INT4, OID_INT8, OID_VARCHAR,
OID_VARCHAR, OID_VARCHAR },
"insert_host",
"INSERT INTO hosts(dhcp_identifier, dhcp_identifier_type, "
" dhcp4_subnet_id, dhcp6_subnet_id, ipv4_address, hostname, "
" dhcp4_client_classes, dhcp6_client_classes) "
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING host_id"
},
//PgSqlHostDataSourceImpl::INSERT_V6_RESRV
// Inserts a single IPv6 reservation into 'reservations' table.
{5,
{ OID_VARCHAR, OID_INT2, OID_INT4, OID_INT4, OID_INT4 },
"insert_v6_resrv",
"INSERT INTO ipv6_reservations(address, prefix_len, type, "
" dhcp6_iaid, host_id) "
"VALUES ($1, $2, $3, $4, $5)"
},
// PgSqlHostDataSourceImpl::INSERT_V4_HOST_OPTION
// Inserts a single DHCPv4 option into 'dhcp4_options' table.
// Using fixed scope_id = 3, which associates an option with host.
{6,
{ OID_INT2, OID_BYTEA, OID_TEXT,
OID_VARCHAR, OID_BOOL, OID_INT8},
"insert_v4_host_option",
"INSERT INTO dhcp4_options(code, value, formatted_value, space, "
" persistent, host_id, scope_id) "
"VALUES ($1, $2, $3, $4, $5, $6, 3)"
},
// PgSqlHostDataSourceImpl::INSERT_V6_HOST_OPTION
// Inserts a single DHCPv6 option into 'dhcp6_options' table.
// Using fixed scope_id = 3, which associates an option with host.
{6,
{ OID_INT2, OID_BYTEA, OID_TEXT,
OID_VARCHAR, OID_BOOL, OID_INT8},
"insert_v6_host_option",
"INSERT INTO dhcp6_options(code, value, formatted_value, space, "
" persistent, host_id, scope_id) "
"VALUES ($1, $2, $3, $4, $5, $6, 3)"
}
}
};
}; // end anonymous namespace
@ -1459,20 +1483,27 @@ PgSqlHostDataSourceImpl(const PgSqlConnection::ParameterMap& parameters)
DHCP4_AND_DHCP6)),
host_ipv6_reservation_exchange_(new PgSqlIPv6ReservationExchange()),
host_option_exchange_(new PgSqlOptionExchange()),
conn_(parameters) {
conn_(parameters),
is_readonly_(false) {
// Open the database.
conn_.openDatabase();
int i = 0;
for( ; tagged_statements[i].text != NULL ; ++i) {
conn_.prepareStatement(tagged_statements[i]);
}
conn_.prepareStatements(tagged_statements.begin(),
tagged_statements.begin() + WRITE_STMTS_BEGIN);
// Just in case somebody foo-barred things
if (i != NUM_STATEMENTS) {
isc_throw(DbOpenError, "Number of statements prepared: " << i
<< " does not match expected count:" << NUM_STATEMENTS);
// Check if the backend is explicitly configured to operate with
// read only access to the database.
is_readonly_ = conn_.configuredReadOnly();
// If we are using read-write mode for the database we also prepare
// statements for INSERTS etc.
if (!is_readonly_) {
conn_.prepareStatements(tagged_statements.begin() + WRITE_STMTS_BEGIN,
tagged_statements.end());
} else {
LOG_INFO(dhcpsrv_logger, DHCPSRV_PGSQL_HOST_DB_READONLY);
}
}
@ -1632,6 +1663,15 @@ std::pair<uint32_t, uint32_t> PgSqlHostDataSourceImpl::getVersion() const {
return (std::make_pair<uint32_t, uint32_t>(version, minor));
}
void
PgSqlHostDataSourceImpl::checkReadOnly() const {
if (is_readonly_) {
isc_throw(ReadOnlyDb, "PostgreSQL host database backend is configured"
" to operate in read only mode");
}
}
/*********** PgSqlHostDataSource *********************/
@ -1646,6 +1686,9 @@ PgSqlHostDataSource::~PgSqlHostDataSource() {
void
PgSqlHostDataSource::add(const HostPtr& host) {
// If operating in read-only mode, throw exception.
impl_->checkReadOnly();
// Initiate PostgreSQL transaction as we will have to make multiple queries
// to insert host information into multiple tables. If that fails on
// any stage, the transaction will be rolled back by the destructor of
@ -1894,5 +1937,20 @@ std::pair<uint32_t, uint32_t> PgSqlHostDataSource::getVersion() const {
return(impl_->getVersion());
}
void
PgSqlHostDataSource::commit() {
// If operating in read-only mode, throw exception.
impl_->checkReadOnly();
impl_->conn_.commit();
}
void
PgSqlHostDataSource::rollback() {
// If operating in read-only mode, throw exception.
impl_->checkReadOnly();
impl_->conn_.rollback();
}
}; // end of isc::dhcp namespace
}; // end of isc namespace

View File

@ -273,6 +273,16 @@ public:
/// has failed.
virtual std::pair<uint32_t, uint32_t> getVersion() const;
/// @brief Commit Transactions
///
/// Commits all pending database operations.
virtual void commit();
/// @brief Rollback Transactions
///
/// Rolls back all pending database operations.
virtual void rollback();
private:
/// @brief Pointer to the implementation of the @ref PgSqlHostDataSource.

View File

@ -88,7 +88,7 @@ public:
}
// Add the keyword and value - make sure that they are quoted.
// The parameters which are not quoted are persist and
// The parameters which are not quoted are persist, readonly and
// lfc-interval as they are boolean and integer respectively.
result += quote + keyval[i] + quote + colon + space;
if (!quoteValue(std::string(keyval[i]))) {
@ -176,7 +176,8 @@ private:
/// @return true if the value of the parameter should be quoted.
bool quoteValue(const std::string& parameter) const {
return ((parameter != "persist") && (parameter != "lfc-interval") &&
(parameter != "connect-timeout"));
(parameter != "connect-timeout") &&
(parameter != "readonly"));
}
};
@ -560,4 +561,45 @@ TEST_F(DbAccessParserTest, getDbAccessString) {
EXPECT_EQ(dbaccess, "name=keatest type=mysql");
}
// Check that the configuration is accepted for the valid value
// of "readonly".
TEST_F(DbAccessParserTest, validReadOnly) {
const char* config[] = {"type", "mysql",
"user", "keatest",
"password", "keatest",
"name", "keatest",
"readonly", "true",
NULL};
string json_config = toJson(config);
ConstElementPtr json_elements = Element::fromJSON(json_config);
EXPECT_TRUE(json_elements);
TestDbAccessParser parser("lease-database", DbAccessParser::LEASE_DB);
EXPECT_NO_THROW(parser.build(json_elements));
checkAccessString("Valid readonly parameter",
parser.getDbAccessParameters(),
config);
}
// Check that for the invalid value of the "readonly" parameter
// an exception is thrown.
TEST_F(DbAccessParserTest, invalidReadOnly) {
const char* config[] = {"type", "mysql",
"user", "keatest",
"password", "keatest",
"name", "keatest",
"readonly", "1",
NULL};
string json_config = toJson(config);
ConstElementPtr json_elements = Element::fromJSON(json_config);
EXPECT_TRUE(json_elements);
TestDbAccessParser parser("lease-database", DbAccessParser::LEASE_DB);
EXPECT_THROW(parser.build(json_elements), BadValue);
}
}; // Anonymous namespace

View File

@ -12,8 +12,10 @@
#include <dhcp/option_string.h>
#include <dhcp/option_int.h>
#include <dhcp/option_vendor.h>
#include <dhcpsrv/host_data_source_factory.h>
#include <dhcpsrv/tests/generic_host_data_source_unittest.h>
#include <dhcpsrv/tests/test_utils.h>
#include <dhcpsrv/testutils/schema.h>
#include <dhcpsrv/database_connection.h>
#include <asiolink/io_address.h>
#include <util/buffer.h>
@ -504,6 +506,55 @@ GenericHostDataSourceTest::addTestOptions(const HostPtr& host,
LibDHCP::setRuntimeOptionDefs(defs);
}
void
GenericHostDataSourceTest::testReadOnlyDatabase(const char* valid_db_type) {
ASSERT_TRUE(hdsptr_);
// The database is initially opened in "read-write" mode. We can
// insert some data to the databse.
HostPtr host = initializeHost6("2001:db8::1", Host::IDENT_DUID, false);
ASSERT_TRUE(host);
ASSERT_NO_THROW(hdsptr_->add(host));
// Subnet id will be used in queries to the database.
SubnetID subnet_id = host->getIPv6SubnetID();
// Make sure that the host has been inserted and that the data can be
// retrieved.
ConstHostPtr host_by_id = hdsptr_->get6(subnet_id, host->getIdentifierType(),
&host->getIdentifier()[0],
host->getIdentifier().size());
ASSERT_TRUE(host_by_id);
ASSERT_NO_FATAL_FAILURE(compareHosts(host, host_by_id));
// Close the database connection and reopen in "read-only" mode as
// specified by the "VALID_READONLY_DB" parameter.
HostDataSourceFactory::destroy();
HostDataSourceFactory::create(connectionString(valid_db_type,
VALID_NAME,
VALID_HOST,
VALID_READONLY_USER,
VALID_PASSWORD,
VALID_READONLY_DB));
hdsptr_ = HostDataSourceFactory::getHostDataSourcePtr();
// Check that an attempt to insert new host would result in
// exception.
HostPtr host2 = initializeHost6("2001:db8::2", Host::IDENT_DUID, false);
ASSERT_TRUE(host2);
ASSERT_THROW(hdsptr_->add(host2), ReadOnlyDb);
ASSERT_THROW(hdsptr_->commit(), ReadOnlyDb);
ASSERT_THROW(hdsptr_->rollback(), ReadOnlyDb);
// Reading from the database should still be possible, though.
host_by_id = hdsptr_->get6(subnet_id, host->getIdentifierType(),
&host->getIdentifier()[0],
host->getIdentifier().size());
ASSERT_TRUE(host_by_id);
ASSERT_NO_FATAL_FAILURE(compareHosts(host, host_by_id));
}
void GenericHostDataSourceTest::testBasic4(const Host::IdentifierType& id) {
// Make sure we have the pointer to the host data source.
ASSERT_TRUE(hdsptr_);

View File

@ -337,6 +337,23 @@ public:
/// @brief Pointer to the host data source
HostDataSourcePtr hdsptr_;
/// @brief Test that backend can be started in read-only mode.
///
/// Some backends can operate when the database is read only, e.g.
/// host reservation tables are read only, or the database user has
/// read only privileges on the entire database. In such cases, the
/// Kea server administrator can specify in the backend configuration
/// that the database should be opened in read only mode, i.e.
/// INSERT, UPDATE, DELETE statements can't be issued. If any of the
/// functions updating the database is called for the backend, the
/// error is reported. The database running in read only mode can
/// be merely used to retrieve existing host reservations from the
/// database. This test verifies that this is the case.
///
/// @param valid_db_type Parameter specifying type of backend to
/// be used, e.g. type=mysql.
void testReadOnlyDatabase(const char* valid_db_type);
/// @brief Test that checks that simple host with IPv4 reservation
/// can be inserted and later retrieved.
///

View File

@ -63,8 +63,13 @@ public:
/// Rolls back all pending transactions. The deletion of myhdsptr_ will close
/// the database. Then reopen it and delete everything created by the test.
virtual ~MySqlHostDataSourceTest() {
hdsptr_->rollback();
try {
hdsptr_->rollback();
} catch (...) {
// Rollback may fail if backend is in read only mode. That's ok.
}
HostDataSourceFactory::destroy();
hdsptr_.reset();
destroyMySQLSchema();
}
@ -157,6 +162,9 @@ TEST(MySqlHostDataSource, OpenDatabase) {
EXPECT_THROW(HostDataSourceFactory::create(connectionString(
MYSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD, INVALID_TIMEOUT_2)),
DbInvalidTimeout);
EXPECT_THROW(HostDataSourceFactory::create(connectionString(
MYSQL_VALID_TYPE, VALID_NAME, VALID_HOST, VALID_USER, VALID_PASSWORD,
VALID_TIMEOUT, INVALID_READONLY_DB)), DbInvalidReadOnly);
// Check for missing parameters
EXPECT_THROW(HostDataSourceFactory::create(connectionString(
@ -167,6 +175,8 @@ TEST(MySqlHostDataSource, OpenDatabase) {
destroyMySQLSchema();
}
/// @brief Check conversion functions
///
/// The server works using cltt and valid_filetime. In the database, the
@ -208,6 +218,10 @@ TEST(MySqlConnection, checkTimeConversion) {
EXPECT_EQ(cltt, converted_cltt);
}
TEST_F(MySqlHostDataSourceTest, testReadOnlyDatabase) {
testReadOnlyDatabase(MYSQL_VALID_TYPE);
}
// Test verifies if a host reservation can be added and later retrieved by IPv4
// address. Host uses hw address as identifier.
TEST_F(MySqlHostDataSourceTest, basic4HWAddr) {

View File

@ -64,8 +64,13 @@ public:
/// close the database. Then reopen it and delete everything created by
/// the test.
virtual ~PgSqlHostDataSourceTest() {
hdsptr_->rollback();
try {
hdsptr_->rollback();
} catch (...) {
// Rollback may fail if backend is in read only mode. That's ok.
}
HostDataSourceFactory::destroy();
hdsptr_.reset();
destroyPgSQLSchema();
}
@ -168,6 +173,12 @@ TEST(PgSqlHostDataSource, OpenDatabase) {
destroyPgSQLSchema();
}
// This test verifies that database backend can operate in Read-Only mode.
TEST_F(PgSqlHostDataSourceTest, testReadOnlyDatabase) {
testReadOnlyDatabase(PGSQL_VALID_TYPE);
}
// Test verifies if a host reservation can be added and later retrieved by IPv4
// address. Host uses hw address as identifier.
TEST_F(PgSqlHostDataSourceTest, basic4HWAddr) {

View File

@ -25,15 +25,19 @@ const char* INVALID_NAME = "name=invalidname";
const char* VALID_HOST = "host=localhost";
const char* INVALID_HOST = "host=invalidhost";
const char* VALID_USER = "user=keatest";
const char* VALID_READONLY_USER = "user=keatest_readonly";
const char* INVALID_USER = "user=invaliduser";
const char* VALID_PASSWORD = "password=keatest";
const char* INVALID_PASSWORD = "password=invalid";
const char* VALID_TIMEOUT = "connect-timeout=10";
const char* INVALID_TIMEOUT_1 = "connect-timeout=foo";
const char* INVALID_TIMEOUT_2 = "connect-timeout=-17";
const char* VALID_READONLY_DB = "readonly=true";
const char* INVALID_READONLY_DB = "readonly=5";
string connectionString(const char* type, const char* name, const char* host,
const char* user, const char* password, const char* timeout) {
const char* user, const char* password, const char* timeout,
const char* readonly_db = NULL) {
const string space = " ";
string result = "";
@ -75,6 +79,13 @@ string connectionString(const char* type, const char* name, const char* host,
result += string(timeout);
}
if (readonly_db != NULL) {
if (! result.empty()) {
result += space;
}
result += string(readonly_db);
}
return (result);
}

View File

@ -21,12 +21,16 @@ extern const char* INVALID_NAME;
extern const char* VALID_HOST;
extern const char* INVALID_HOST;
extern const char* VALID_USER;
extern const char* VALID_READONLY_USER;
extern const char* INVALID_USER;
extern const char* VALID_PASSWORD;
extern const char* INVALID_PASSWORD;
extern const char* VALID_TIMEOUT;
extern const char* INVALID_TIMEOUT_1;
extern const char* INVALID_TIMEOUT_2;
extern const char* VALID_READONLY_DB;
extern const char* INVALID_READONLY_DB;
/// @brief Given a combination of strings above, produce a connection string.
///
/// @param type type of the database
@ -35,10 +39,12 @@ extern const char* INVALID_TIMEOUT_2;
/// @param user username used to authenticate during connection attempt
/// @param password password used to authenticate during connection attempt
/// @param timeout timeout used during connection attempt
/// @param readonly_db specifies if database is read only
/// @return string containing all specified parameters
std::string connectionString(const char* type, const char* name = NULL,
const char* host = NULL, const char* user = NULL,
const char* password = NULL, const char* timeout = NULL);
const char* password = NULL, const char* timeout = NULL,
const char* readonly_db = NULL);
};
};
};

View File

@ -465,6 +465,11 @@ ALTER TABLE dhcp6_options
ADD CONSTRAINT fk_dhcp6_option_scope FOREIGN KEY (scope_id)
REFERENCES dhcp_option_scope (scope_id);
# Add UNSIGNED to reservation_id
ALTER TABLE ipv6_reservations
MODIFY reservation_id INT UNSIGNED NOT NULL AUTO_INCREMENT;
# Update the schema version number
UPDATE schema_version
SET version = '4', minor = '2';