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: