Add possibility to use MySQL backend
Just for the information:
The base code for row_present state was mainly taken from sqlite3
SaltStack state.
MySQL database schema was taken from:
https://doc.powerdns.com/authoritative/backends/generic-mysql.html
Related-PROD: PROD-21596 (PROD:21596)
Change-Id: If58216b058c352c692ea4997019193dba287038a
diff --git a/README.rst b/README.rst
index f16ed70..8563d18 100644
--- a/README.rst
+++ b/README.rst
@@ -1,63 +1,102 @@
+=====
+Usage
+=====
-========
-PowerDNS
-========
+PowerDNS, founded in the late 1990s, is a premier supplier
+of open source DNS software, services and support. Deployed
+throughout the world with some of the most demanding users
+of DNS, we pride ourselves on providing quality software
+and the very best support available.
-Sample pillar:
+Sample Pillars
+==============
PowerDNS server with MySQL backend
.. code-block:: yaml
- powedns:
- server:
- enabled: true
- backend:
- engine: mysql
- host: localhost
- port: 3306
- name: pdns
- user: pdns
- password: password
- bind:
- address: 0.0.0.0
- port: 53
- axfr_ips:
- - 10.11.0.0/16
- - 127.0.0.1
- api:
- enabled: true
- key: SecurePass
- webserver:
- enabled: true
- password: SuperSecurePass
- address: 0.0.0.0
- port: 8081
+ powerdns:
+ server:
+ api:
+ enabled: True
+ key: VxK9cMlFL5Ae
+ axfr_ips:
+ - 172.16.10.103
+ - 172.16.10.104
+ - 172.16.10.102
+ - 127.0.0.1
+ backend:
+ engine : mysql
+ host : localhost
+ port : 3306
+ dbname : powerdns
+ user : powerdns
+ password : powerdns
+ timeout': 10
+ dnssec : on
+ bind:
+ address: 172.16.10.103
+ overwrite_supermasters: True
+ supermasters:
+ - account: master
+ ip: 172.16.10.103
+ nameserver: ns1.example.org
+ - account: master
+ ip: 172.16.10.104
+ nameserver: ns2.example.org
+ - account: master
+ ip: 172.16.10.102
+ nameserver: ns3.example.org
+ webserver:
+ address: 172.16.10.103
+ enabled: True
+ password: gJ6n3gVaYP8eS
+ port: 8081
-PowerDNS server with sqlite backend
+.. note:: If you use one MySQL database across several
+ PowerDNS instances, be sure to pass *-b1* parameter
+ to *salt* command to avoid race condition.
+
+PowerDNS server with SQLite backend
.. code-block:: yaml
- powerdns:
- server:
- enabled: true
- backend:
- engine: sqlite
- dbname: pdns.sqlite
- dbpath: /var/lib/powerdns
- bind:
- address: 127.0.0.1
- port: 55
- default-soa-name: ns1.domain.tld
- soa-minimum-ttl: 3600
- api:
- enabled: true
- key: SecurePass
- webserver:
- enabled: true
- password: SuperSecurePass
- address: 0.0.0.0
- port: 8081
+
+ powerdns:
+ server:
+ api:
+ enabled: True
+ key: VxK9cMlFL5Ae
+ axfr_ips:
+ - 172.16.10.103
+ - 172.16.10.104
+ - 172.16.10.102
+ - 127.0.0.1
+ backend:
+ engine: sqlite
+ dbname: pdns.sqlite
+ dbpath: /var/lib/powerdns
+ bind:
+ address: 172.16.10.103
+ overwrite_supermasters: True
+ supermasters:
+ - account: master
+ ip: 172.16.10.103
+ nameserver: ns1.example.org
+ - account: master
+ ip: 172.16.10.104
+ nameserver: ns2.example.org
+ - account: master
+ ip: 172.16.10.102
+ nameserver: ns3.example.org
+ webserver:
+ address: 172.16.10.103
+ enabled: True
+ password: gJ6n3gVaYP8eS
+ port: 8081
-Read more
-=========
+Documentation
+=============
+
+* https://doc.powerdns.com/
+
diff --git a/_states/powerdns_mysql.py b/_states/powerdns_mysql.py
new file mode 100644
index 0000000..2023ab0
--- /dev/null
+++ b/_states/powerdns_mysql.py
@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+'''
+PowerDNS MySQL interections
+==========================
+
+:depends: - MySQLdb Python module
+:configuration: See :py:mod:`salt.modules.mysql` for setup instructions.
+
+The mysql_query module is used to execute queries on MySQL databases.
+Its output may be stored in a file or in a grain.
+
+Sometimes you would need to store information about PowerDNS supermasters
+in tables to work with.
+Here is an example of making sure rows with specific data exist:
+
+ .. code-block:: yaml
+
+ use_supermaster_127.0.0.1:
+ powerdns_mysql.row_present:
+ - table: supermasters
+ - where_sql: ip="127.0.0.1"
+ - database: powerdns
+ - data:
+ ip: 127.0.0.1
+ nameserver: ns.localhost.local
+ account: master
+
+Comprehensive example could be found in file:
+powerdns/server/backends/mysql.sls
+'''
+
+import sys
+import salt.utils
+import salt.ext.six as six
+
+
+def __virtual__():
+ '''
+ Only load if the mysql module is available in __salt__
+ '''
+ return 'mysql.query' in __salt__
+
+
+def _get_mysql_error():
+ '''
+ Look in module context for a MySQL error. Eventually we should make a less
+ ugly way of doing this.
+ '''
+ return sys.modules[
+ __salt__['test.ping'].__module__
+ ].__context__.pop('mysql.error', None)
+
+
+def row_present(name,
+ database,
+ table,
+ data,
+ where_sql,
+ update=False,
+ **connection_args):
+ '''
+ Checks to make sure the given row exists. If row exists and update is True
+ then row will be updated with data. Otherwise it will leave existing
+ row unmodified and check it against data. If the existing data
+ doesn't match data check the state will fail. If the row doesn't
+ exist then it will insert data into the table. If more than one
+ row matches, then the state will fail.
+
+ name
+ Used only as an ID
+
+ database
+ The name of the database to execute the query on
+
+ table
+ The table name to check the data
+
+ data
+ The dictionary of key/value pairs to check against if
+ row exists, insert into the table if it doesn't
+
+ where_sql
+ The SQL to select the row to check
+
+ update
+ True will replace the existing row with data
+ When False and the row exists and data does not equal
+ the row data then the state will fail
+
+ connection_args
+ MySQL connection arguments to connect to MySQL server with
+ '''
+
+ ret = {'name': name,
+ 'changes': {},
+ 'result': True,
+ 'comment': 'Database {0} is already present'.format(database)}
+
+ # check if database exists
+ if not __salt__['mysql.db_exists'](database, **connection_args):
+ err = _get_mysql_error()
+ if err is not None:
+ ret['comment'] = err
+ ret['result'] = False
+ return ret
+
+ ret['result'] = None
+ ret['comment'] = ('Database {0} is not present'
+ ).format(name)
+ return ret
+
+ try:
+ query = "SELECT * FROM `" + table + "` WHERE " + where_sql
+ select_result = __salt__['mysql.query'](database, query, **connection_args)
+
+ if select_result['rows returned'] > 1:
+ ret['result'] = False
+ ret['comment'] = 'More than one row matched the specified query'
+ elif select_result['rows returned'] == 1:
+ # create ordered dict of values returned by mysql.query function
+ old_data = salt.utils.odict.OrderedDict()
+ for num in xrange(0, len(select_result['columns'])):
+ old_data[select_result['columns'][num]] = select_result['results'][0][num]
+
+ for key, value in six.iteritems(data):
+ if key in old_data and old_data[key] != value:
+ if update:
+ if __opts__['test']:
+ ret['result'] = True
+ ret['comment'] = "Row will be update in " + table
+ else:
+ columns = []
+ for key, value in six.iteritems(data):
+ columns.append('`' + key + '`="' + value + '"')
+
+ query = "UPDATE `" + table + "` SET "
+ query += ",".join(columns)
+ query += " WHERE "
+ query += where_sql
+ update_result = __salt__['mysql.query'](database, query, **connection_args)
+
+ if update_result['rows affected'] == 1:
+ ret['result'] = True
+ ret['comment'] = "Row updated"
+ ret['changes']['old'] = old_data
+ ret['changes']['new'] = data
+ else:
+ ret['result'] = None
+ ret['comment'] = "Row update failed"
+ else:
+ ret['result'] = False
+ ret['comment'] = "Existing data does" + \
+ "not match desired state"
+ break
+
+ if ret['result'] is None:
+ ret['result'] = True
+ ret['comment'] = "Row exists"
+ else:
+ if __opts__['test']:
+ ret['result'] = True
+ ret['changes']['new'] = data
+ ret['comment'] = "Row will be inserted into " + table
+ else:
+ columns = []
+ values = []
+ for key, value in six.iteritems(data):
+ values.append('"' + value + '"')
+ columns.append("`" + key + "`")
+
+ query = "INSERT INTO `" + table + "` ("
+ query += ",".join(columns)
+ query += ") VALUES ("
+ query += ",".join(values)
+ query += ")"
+ insert_result = __salt__['mysql.query'](database, query, **connection_args)
+
+ if insert_result['rows affected'] == 1:
+ ret['result'] = True
+ ret['changes']['new'] = data
+ ret['comment'] = 'Inserted row'
+ else:
+ ret['result'] = False
+ ret['comment'] = "Unable to insert data"
+
+ except Exception as e:
+ ret['result'] = False
+ ret['comment'] = str(e)
+
+ return ret
diff --git a/powerdns/files/backends/mysql.conf b/powerdns/files/backends/mysql.conf
index 9ff090e..f064469 100644
--- a/powerdns/files/backends/mysql.conf
+++ b/powerdns/files/backends/mysql.conf
@@ -11,7 +11,7 @@
gmysql-host={{ server.backend.host }}
gmysql-port={{ server.backend.port }}
{%- endif %}
-gmysql-dbname={{ server.backend.name }}
+gmysql-dbname={{ server.backend.dbname }}
gmysql-user={{ server.backend.user }}
gmysql-password={{ server.backend.password }}
gmysql-dnssec={{ server.backend.dnssec }}
diff --git a/powerdns/files/mysql.sql b/powerdns/files/mysql.sql
new file mode 100644
index 0000000..1c6eee5
--- /dev/null
+++ b/powerdns/files/mysql.sql
@@ -0,0 +1,89 @@
+CREATE TABLE domains (
+ id INT AUTO_INCREMENT,
+ name VARCHAR(255) NOT NULL,
+ master VARCHAR(128) DEFAULT NULL,
+ last_check INT DEFAULT NULL,
+ type VARCHAR(6) NOT NULL,
+ notified_serial INT UNSIGNED DEFAULT NULL,
+ account VARCHAR(40) CHARACTER SET 'utf8' DEFAULT NULL,
+ PRIMARY KEY (id)
+) Engine=InnoDB CHARACTER SET 'latin1';
+
+CREATE UNIQUE INDEX name_index ON domains(name);
+
+
+CREATE TABLE records (
+ id BIGINT AUTO_INCREMENT,
+ domain_id INT DEFAULT NULL,
+ name VARCHAR(255) DEFAULT NULL,
+ type VARCHAR(10) DEFAULT NULL,
+ content VARCHAR(64000) DEFAULT NULL,
+ ttl INT DEFAULT NULL,
+ prio INT DEFAULT NULL,
+ change_date INT DEFAULT NULL,
+ disabled TINYINT(1) DEFAULT 0,
+ ordername VARCHAR(255) BINARY DEFAULT NULL,
+ auth TINYINT(1) DEFAULT 1,
+ PRIMARY KEY (id)
+) Engine=InnoDB CHARACTER SET 'latin1';
+
+CREATE INDEX nametype_index ON records(name,type);
+CREATE INDEX domain_id ON records(domain_id);
+CREATE INDEX ordername ON records (ordername);
+
+
+CREATE TABLE supermasters (
+ ip VARCHAR(64) NOT NULL,
+ nameserver VARCHAR(255) NOT NULL,
+ account VARCHAR(40) CHARACTER SET 'utf8' NOT NULL,
+ PRIMARY KEY (ip, nameserver)
+) Engine=InnoDB CHARACTER SET 'latin1';
+
+
+CREATE TABLE comments (
+ id INT AUTO_INCREMENT,
+ domain_id INT NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ type VARCHAR(10) NOT NULL,
+ modified_at INT NOT NULL,
+ account VARCHAR(40) CHARACTER SET 'utf8' DEFAULT NULL,
+ comment TEXT CHARACTER SET 'utf8' NOT NULL,
+ PRIMARY KEY (id)
+) Engine=InnoDB CHARACTER SET 'latin1';
+
+CREATE INDEX comments_name_type_idx ON comments (name, type);
+CREATE INDEX comments_order_idx ON comments (domain_id, modified_at);
+
+
+CREATE TABLE domainmetadata (
+ id INT AUTO_INCREMENT,
+ domain_id INT NOT NULL,
+ kind VARCHAR(32),
+ content TEXT,
+ PRIMARY KEY (id)
+) Engine=InnoDB CHARACTER SET 'latin1';
+
+CREATE INDEX domainmetadata_idx ON domainmetadata (domain_id, kind);
+
+
+CREATE TABLE cryptokeys (
+ id INT AUTO_INCREMENT,
+ domain_id INT NOT NULL,
+ flags INT NOT NULL,
+ active BOOL,
+ content TEXT,
+ PRIMARY KEY(id)
+) Engine=InnoDB CHARACTER SET 'latin1';
+
+CREATE INDEX domainidindex ON cryptokeys(domain_id);
+
+
+CREATE TABLE tsigkeys (
+ id INT AUTO_INCREMENT,
+ name VARCHAR(255),
+ algorithm VARCHAR(50),
+ secret VARCHAR(255),
+ PRIMARY KEY (id)
+) Engine=InnoDB CHARACTER SET 'latin1';
+
+CREATE UNIQUE INDEX namealgoindex ON tsigkeys(name, algorithm);
diff --git a/powerdns/map.jinja b/powerdns/map.jinja
index 3ebd104..3930df4 100644
--- a/powerdns/map.jinja
+++ b/powerdns/map.jinja
@@ -1,4 +1,3 @@
-
{%- set server = salt['grains.filter_by']({
'Debian': {
'service': 'pdns',
@@ -8,9 +7,11 @@
'engine': 'mysql',
'host': 'localhost',
'port': '3306',
+ 'use_socket': False,
+ 'socket': '/var/run/mysqld/mysqld.sock',
'dbname': 'powerdns',
- 'user': 'mysql',
- 'password': 'mysql',
+ 'user': 'powerdns',
+ 'password': 'powerdns',
'timeout': 10,
'dnssec': 'on'
},
diff --git a/powerdns/server/backends/mysql.sls b/powerdns/server/backends/mysql.sls
index 755930a..6445a60 100644
--- a/powerdns/server/backends/mysql.sls
+++ b/powerdns/server/backends/mysql.sls
@@ -3,10 +3,94 @@
include:
- powerdns.server.service
+{%- if server.backend.get('use_socket', False) == True %}
+ {%- set mysql_connection_args = {
+ 'use_socket': True,
+ 'connection_charset': server.backend.get('dbcharset', 'utf8'),
+ 'connection_db': server.backend.dbname,
+ 'connection_default_file': server.backend.get('dbdefault_file', ''),
+ 'connection_unix_socket': server.backend.socket,
+ 'connection_user': server.backend.user,
+ 'connection_pass': server.backend.password,
+ } %}
+{%- else %}
+ {%- set mysql_connection_args = {
+ 'use_socket': False,
+ 'connection_charset': server.backend.get('dbcharset', 'utf8'),
+ 'connection_db': server.backend.dbname,
+ 'connection_default_file': server.backend.get('dbdefault_file', ''),
+ 'connection_host': server.backend.host,
+ 'connection_port': server.backend.port,
+ 'connection_user': server.backend.user,
+ 'connection_pass': server.backend.password,
+ } %}
+{%- endif %}
+
powerdns_mysql_packages:
pkg.installed:
- names: {{ packages.backends.mysql }}
+/etc/powerdns/dbtemplate.sql:
+ file.managed:
+ - source: salt://powerdns/files/mysql.sql
+ - require:
+ - pkg: powerdns_mysql_packages
+
+{%- set powerdns_db_tables = salt['mysql.db_tables'](server.backend.dbname, **mysql_connection_args) %}
+{%- if not powerdns_db_tables %}
+powerdns_init_mysql_db:
+ mysql_query.run_file:
+ - database: {{ server.backend.dbname }}
+ - query_file: /etc/powerdns/dbtemplate.sql
+ - connection_charset: {{ mysql_connection_args.connection_charset }}
+{%- if mysql_connection_args.connection_default_file %}
+ - connection_default_file: {{ mysql_connection_args.connection_default_file }}
+{%- endif %}
+{%- if mysql_connection_args.use_socket == True %}
+ - connection_unix_socket: {{ mysql_connection_args.connection_unix_socket }}
+{%- else %}
+ - connection_host: {{ mysql_connection_args.connection_host }}
+ - connection_port: {{ mysql_connection_args.connection_port }}
+{%- endif %}
+ - connection_user: {{ mysql_connection_args.connection_user }}
+ - connection_pass: {{ mysql_connection_args.connection_pass }}
+{%- endif %}
+ - require:
+ - file: /etc/powerdns/dbtemplate.sql
+{%- if server.supermasters is defined %}
+{% for supermaster in server.supermasters %}
+use_supermaster_{{ supermaster.ip }}:
+ powerdns_mysql.row_present:
+ - table: supermasters
+ - where_sql: ip="{{ supermaster.ip }}"
+ - database: {{ server.backend.dbname }}
+ - data:
+ ip: {{ supermaster.ip }}
+ nameserver: {{ supermaster.nameserver }}
+ account: {{supermaster.account }}
+ {%- if server.overwrite_supermasters is defined %}
+ - update: {{ server.overwrite_supermasters }}
+ {%- endif %}
+ - connection_charset: {{ mysql_connection_args.connection_charset }}
+{%- if mysql_connection_args.connection_default_file %}
+ - connection_default_file: {{ mysql_connection_args.connection_default_file }}
+{%- endif %}
+{%- if mysql_connection_args.use_socket == True %}
+ - connection_unix_socket: {{ mysql_connection_args.connection_unix_socket }}
+{%- else %}
+ - connection_host: {{ mysql_connection_args.connection_host }}
+ - connection_port: {{ mysql_connection_args.connection_port }}
+{%- endif %}
+ - connection_user: {{ mysql_connection_args.connection_user }}
+ - connection_pass: {{ mysql_connection_args.connection_pass }}
+{%- if not powerdns_db_tables %}
+ - require:
+ - powerdns_init_mysql_db
+{%- endif %}
+ - watch_in:
+ - service: powerdns_service
+{% endfor %}
+{%- endif %}
/etc/powerdns/pdns.d/pdns.local.gmysql.conf:
file.managed:
- source: salt://powerdns/files/backends/mysql.conf
diff --git a/powerdns/server/packages.jinja b/powerdns/server/packages.jinja
index ea93df0..0655491 100644
--- a/powerdns/server/packages.jinja
+++ b/powerdns/server/packages.jinja
@@ -24,4 +24,4 @@
'pgsql': ['pdns-backend-postgresql'],
},
},
-}, grain='os_family') %}
+}, base='default', grain='os_family') %}
diff --git a/powerdns/server/service.sls b/powerdns/server/service.sls
index 9ff3a70..ef25891 100644
--- a/powerdns/server/service.sls
+++ b/powerdns/server/service.sls
@@ -1,6 +1,6 @@
{%- from "powerdns/map.jinja" import server with context %}
{%- from "powerdns/server/packages.jinja" import packages with context %}
-{%- if server.enabled %}
+{%- if server.get('enabled', False) %}
powerdns_packages:
pkg.installed: