Browse Source

[master] Merge branch 'trac4489'

Marcin Siodelski 8 years ago
parent
commit
0d8dc7affb
34 changed files with 915 additions and 370 deletions
  1. 1 1
      doc/Makefile.am
  2. 1 1
      doc/devel/contribute.dox
  3. 7 4
      doc/devel/mainpage.dox
  4. 0 68
      doc/devel/qa.dox
  5. 264 0
      doc/devel/unit-tests.dox
  6. 20 5
      doc/guide/admin.xml
  7. 32 0
      doc/guide/dhcp4-srv.xml
  8. 34 1
      doc/guide/dhcp6-srv.xml
  9. 11 0
      doc/guide/install.xml
  10. 6 0
      src/bin/dhcp4/dhcp4.spec
  11. 6 0
      src/bin/dhcp6/dhcp6.spec
  12. 0 132
      src/lib/dhcpsrv/database_backends.dox
  13. 21 1
      src/lib/dhcpsrv/database_connection.cc
  14. 17 0
      src/lib/dhcpsrv/database_connection.h
  15. 8 1
      src/lib/dhcpsrv/db_exceptions.h
  16. 12 0
      src/lib/dhcpsrv/dhcpsrv_messages.mes
  17. 16 13
      src/lib/dhcpsrv/mysql_connection.cc
  18. 10 4
      src/lib/dhcpsrv/mysql_connection.h
  19. 113 56
      src/lib/dhcpsrv/mysql_host_data_source.cc
  20. 2 1
      src/lib/dhcpsrv/mysql_host_data_source.h
  21. 7 6
      src/lib/dhcpsrv/mysql_lease_mgr.cc
  22. 3 2
      src/lib/dhcpsrv/parsers/dbaccess_parser.cc
  23. 10 0
      src/lib/dhcpsrv/pgsql_connection.cc
  24. 15 0
      src/lib/dhcpsrv/pgsql_connection.h
  25. 126 68
      src/lib/dhcpsrv/pgsql_host_data_source.cc
  26. 10 0
      src/lib/dhcpsrv/pgsql_host_data_source.h
  27. 44 2
      src/lib/dhcpsrv/tests/dbaccess_parser_unittest.cc
  28. 51 0
      src/lib/dhcpsrv/tests/generic_host_data_source_unittest.cc
  29. 17 0
      src/lib/dhcpsrv/tests/generic_host_data_source_unittest.h
  30. 15 1
      src/lib/dhcpsrv/tests/mysql_host_data_source_unittest.cc
  31. 12 1
      src/lib/dhcpsrv/tests/pgsql_host_data_source_unittest.cc
  32. 12 1
      src/lib/dhcpsrv/testutils/schema.cc
  33. 7 1
      src/lib/dhcpsrv/testutils/schema.h
  34. 5 0
      src/share/database/scripts/mysql/dhcpdb_create.mysql

+ 1 - 1
doc/Makefile.am

@@ -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

+ 1 - 1
doc/devel/contribute.dox

@@ -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

+ 7 - 4
doc/devel/mainpage.dox

@@ -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

+ 0 - 68
doc/devel/qa.dox

@@ -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 - 0
doc/devel/unit-tests.dox

@@ -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>).
+
+
+ */

+ 20 - 5
doc/guide/admin.xml

@@ -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>
@@ -711,6 +713,18 @@ $ <userinput>kea-admin lease-upgrade cql -n <replaceable>database-name</replacea
   </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>
 
       <para>
@@ -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>

+ 32 - 0
doc/guide/dhcp4-srv.xml

@@ -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">

+ 34 - 1
doc/guide/dhcp6-srv.xml

@@ -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>

+ 11 - 0
doc/guide/install.xml

@@ -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>

+ 6 - 0
src/bin/dhcp4/dhcp4.spec

@@ -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
             }
         ]
       },

+ 6 - 0
src/bin/dhcp6/dhcp6.spec

@@ -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
             }
         ]
       },

+ 0 - 132
src/lib/dhcpsrv/database_backends.dox

@@ -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>).
 
   */

+ 21 - 1
src/lib/dhcpsrv/database_connection.cc

@@ -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");
+}
+
 };
 };

+ 17 - 0
src/lib/dhcpsrv/database_connection.h

@@ -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

+ 8 - 1
src/lib/dhcpsrv/db_exceptions.h

@@ -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) {}
+};
+
 };
 };
 

+ 12 - 0
src/lib/dhcpsrv/dhcpsrv_messages.mes

@@ -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.

+ 16 - 13
src/lib/dhcpsrv/mysql_connection.cc

@@ -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

+ 10 - 4
src/lib/dhcpsrv/mysql_connection.h

@@ -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
     ///

+ 113 - 56
src/lib/dhcpsrv/mysql_host_data_source.cc

@@ -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 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)"},
+/// @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.
+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 (?, ?, ?, ?, ?, ?, ?, ?, ?)"},
+
+    // 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 (?,?,?,?,?)"},
 
-}; // end anonymouse namespace
+    // 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();
 }
 

+ 2 - 1
src/lib/dhcpsrv/mysql_host_data_source.h

@@ -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_;
 };
 
 }

+ 7 - 6
src/lib/dhcpsrv/mysql_lease_mgr.cc

@@ -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.

+ 3 - 2
src/lib/dhcpsrv/parsers/dbaccess_parser.cc

@@ -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() << ")");
         }
     }

+ 10 - 0
src/lib/dhcpsrv/pgsql_connection.cc

@@ -130,6 +130,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;
     string shost = "localhost";

+ 15 - 0
src/lib/dhcpsrv/pgsql_connection.h

@@ -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

+ 126 - 68
src/lib/dhcpsrv/pgsql_host_data_source.cc

@@ -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

+ 10 - 0
src/lib/dhcpsrv/pgsql_host_data_source.h

@@ -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.

+ 44 - 2
src/lib/dhcpsrv/tests/dbaccess_parser_unittest.cc

@@ -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

+ 51 - 0
src/lib/dhcpsrv/tests/generic_host_data_source_unittest.cc

@@ -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_);

+ 17 - 0
src/lib/dhcpsrv/tests/generic_host_data_source_unittest.h

@@ -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.
     ///

+ 15 - 1
src/lib/dhcpsrv/tests/mysql_host_data_source_unittest.cc

@@ -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) {

+ 12 - 1
src/lib/dhcpsrv/tests/pgsql_host_data_source_unittest.cc

@@ -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) {

+ 12 - 1
src/lib/dhcpsrv/testutils/schema.cc

@@ -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);
 }
 

+ 7 - 1
src/lib/dhcpsrv/testutils/schema.h

@@ -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);
 };
 };
 };

+ 5 - 0
src/share/database/scripts/mysql/dhcpdb_create.mysql

@@ -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';