added auth to ntp formula

Change-Id: Ida54379a126ff1c43517bbe489f3fb591e89c004
diff --git a/.kitchen.yml b/.kitchen.yml
index 48077ac..01dce9d 100644
--- a/.kitchen.yml
+++ b/.kitchen.yml
@@ -45,4 +45,14 @@
     provisioner:
       pillars-from-files:
         ntp.sls: tests/pillar/server.sls
+
+  - name: client_auth
+    provisioner:
+      pillars-from-files:
+        ntp.sls: tests/pillar/client_auth.sls
+
+  - name: server_auth
+    provisioner:
+      pillars-from-files:
+        ntp.sls: tests/pillar/server_auth.sls
 # vim: ft=yaml sw=2 ts=2 sts=2 tw=125
diff --git a/.travis.yml b/.travis.yml
index 4c09641..4898a3f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -21,6 +21,12 @@
   - PLATFORM=epcim/salt-formulas:saltstack-ubuntu-xenial-salt-2016.3 SUITE=server
   - PLATFORM=epcim/salt-formulas:saltstack-ubuntu-xenial-salt-2017.7 SUITE=client
   - PLATFORM=epcim/salt-formulas:saltstack-ubuntu-xenial-salt-2017.7 SUITE=server
+#
+  - PLATFORM=epcim/salt-formulas:saltstack-ubuntu-xenial-salt-2016.3 SUITE=client_auth
+  - PLATFORM=epcim/salt-formulas:saltstack-ubuntu-xenial-salt-2016.3 SUITE=server_auth
+  - PLATFORM=epcim/salt-formulas:saltstack-ubuntu-xenial-salt-2017.7 SUITE=client_auth
+  - PLATFORM=epcim/salt-formulas:saltstack-ubuntu-xenial-salt-2017.7 SUITE=server_auth
+
   # - PLATFORM=epcim/salt-formulas:saltstack-ubuntu-bionic-salt-2017.7 SUITE=client
   # - PLATFORM=epcim/salt-formulas:saltstack-ubuntu-bionic-salt-2017.7 SUITE=server
 
diff --git a/README.rst b/README.rst
index 6831a7e..800e869 100644
--- a/README.rst
+++ b/README.rst
@@ -1,5 +1,5 @@
 ===
-NTP 
+NTP
 ===
 
 Network time synchronisation services.
@@ -7,7 +7,7 @@
 Sample pillars
 ==============
 
-NTP client
+NTP client (old version), not used when stratum parameter exists
 
 .. code-block:: yaml
 
@@ -18,6 +18,115 @@
         - ntp.cesnet.cz
         - ntp.nic.cz
 
+NTP client (extended definitions with auth)
+
+.. code-block:: yaml
+
+    ntp:
+      client:
+        enabled: true
+        stratum:
+          primary:
+            server: ntp.cesnet.cz
+            key_id: 1
+          secondary:
+            server: ntp.nic.cz
+            key_id: 2
+
+NTP with MD5 auth
+Requires extended definitions
+
+.. code-block:: yaml
+
+    ntp:
+      client:
+        enabled: true
+        auth:
+          enabled: true
+          secrets:
+            1:
+              secret_type: 'M'
+              secret: 'Runrabbitrundigthath'
+              trustedkey: true
+            2:
+              secret_type: 'M'
+              secret: 'Howiwishyouwereherew'
+              trustedkey: true
+        stratum:
+          primary:
+            server: ntp.cesnet.cz
+            key_id: 1
+          secondary:
+            server: ntp.nic.cz
+            key_id: 2
+
+.. code-block:: yaml
+
+    ntp:
+      client:
+        enabled: false
+      server:
+        enabled: true
+        auth:
+          enabled: true
+          secrets:
+            1:
+              secret_type: 'M'
+              secret: 'Runrabbitrundigthath'
+              trustedkey: true
+            2:
+              secret_type: 'M'
+              secret: 'Howiwishyouwereherew'
+              trustedkey: true
+        stratum:
+          primary:
+            server: ntp.cesnet.cz
+            key_id: 1
+          secondary:
+            server: ntp.nic.cz
+            key_id: 2
+
+Peering (simple):
+
+.. code-block:: yaml
+
+    ntp:
+      server:
+        peers:
+        - 192.168.0.241
+        - 192.168.0.242
+
+Peering (extended definitions):
+
+.. code-block:: yaml
+
+    ntp:
+      server:
+        peers:
+          1:
+            host: 192.168.31.1
+          2:
+            host: 192.168.31.2
+          3:
+            host: 192.168.31.3
+
+Enable listen/ignote on specific addresses
+
+.. code-block:: yaml
+
+    ntp:
+      server:
+          1:
+            value: wildcard
+            action: ignore
+          2:
+            value: ::1
+            action: listen
+          3:
+            value: 192.168.31.1
+            action: listen
+
+
 Read more
 =========
 
diff --git a/metadata/service/client/init.yml b/metadata/service/client/init.yml
index 24e6ead..156fc36 100644
--- a/metadata/service/client/init.yml
+++ b/metadata/service/client/init.yml
@@ -1,5 +1,5 @@
 applications:
-- ntp
+  - ntp
 classes:
   - service.ntp.support
 parameters:
diff --git a/metadata/service/client/single.yml b/metadata/service/client/single.yml
new file mode 100644
index 0000000..f0d44e1
--- /dev/null
+++ b/metadata/service/client/single.yml
@@ -0,0 +1,18 @@
+applications:
+  - ntp
+classes:
+  - service.ntp.support
+parameters:
+  _param:
+    ntp_strata_host1: ntp.cesnet.cz
+    ntp_strata_key1: ~
+  ntp:
+    client:
+      enabled: true
+      stratum:
+        primary:
+          server: ${_param:ntp_strata_host1}
+          key_id: ${_param:ntp_strata_key1}
+    server:
+      enabled: false
+      mode7: false
diff --git a/ntp/client.sls b/ntp/client.sls
index 2f6084d..cf5fc59 100644
--- a/ntp/client.sls
+++ b/ntp/client.sls
@@ -37,6 +37,23 @@
 
 {%- endif %}
 
+{%- if client.get('auth', {}).get('enabled', False) %}
+
+ntp_keys_client:
+  file.managed:
+  - name: /etc/ntp.keys
+  - source: salt://ntp/files/ntp.keys
+  - user: root
+  - group: root
+  - mode: 600
+  - template: jinja
+  - require:
+    - pkg: ntp_packages
+  - watch_in:
+    - ntp_service
+
+{%- endif %}
+
 /etc/ntp.conf:
   file.managed:
   - source: salt://ntp/files/ntp.conf
diff --git a/ntp/files/ntp.conf b/ntp/files/ntp.conf
index 1987879..df8db2c 100644
--- a/ntp/files/ntp.conf
+++ b/ntp/files/ntp.conf
@@ -10,22 +10,59 @@
 
 # Associate to cloud NTP pool servers
 {%- if client.get('enabled', False) %}
-{%- for stratum in client.strata %}
-server {{ stratum }}{% if loop.first %} iburst{% endif %}
+
+{%- if client.stratum is defined %}
+{%- for stratum_name, stratum in client.stratum.items() %}
+server {{ stratum.server }} {%- if stratum.get('key_id') %} key {{ stratum.key_id }} {%- endif %} {%- if loop.first %} iburst{%- endif %}
 {%- endfor %}
-{%- endif %}
+{%- else %}
+{%- for stratum in client.strata %}
+server {{ stratum }}{%- if loop.first %} iburst{%- endif %}
+{%- endfor %}
+{%- endif -%}
+
+{%- endif -%}
 
 {%- if server.get('enabled', False) %}
-{%- for stratum in server.strata %}
-server {{ stratum }}{% if loop.first %} iburst{% endif %}
-{%- endfor %}
 
-{%- if server.get('peers') %}
-# Set of peer servers
+{%- if server.interface is defined and server.interface != None %}
+{%- for _, iface in server.interface.items() %}
+interface {{ iface.action }} {{ iface.value }}
+{%- endfor -%}
+{%- endif -%}
+
+{%- if server.stratum is defined %}
+{%- for stratum_name, stratum in server.stratum.items() %}
+server {{ stratum.server }} {%- if stratum.get('key_id') %} key {{ stratum.key_id }} {%- endif %} {%- if loop.first %} iburst{%- endif %}
+{%- endfor %}
+{%- else %}
+{%- for stratum in server.strata %}
+server {{ stratum }}{%- if loop.first %} iburst{%- endif %}
+{%- endfor %}
+{%- endif -%}
+
+{# Drop myown peer,unless ntpd will try reach ownself #}
+{% set _drop_list=[] %}
+{%- if ( server.interface is defined and server.interface != None ) and ( server.peers is defined and server.peers != None ) %} {#12#}
+{%- for _, zz in server.interface.items() %} {% if zz.action == "listen" %} {% do _drop_list.append(zz.value) %} {% endif %} {%- endfor -%}
+#SALT: Those interfaces were removed from peers list:
+#{%- for i in _drop_list %} {{ i }} {%- endfor -%}
+{%- endif %}
+
+{%- if server.peers is defined and server.peers != None %}
+{%- if server.peers is mapping %}
+{%- for _, peer in server.peers.items() %}
+{%- if peer.host not in _drop_list %}
+peer {{ peer.host }} {% if peer.key_id is defined -%} key {{ peer.key_id }} {% endif %}
+{%- endif -%}
+{%- endfor -%}
+{%- else %}                         {# follback support for old schema via list. For follback we don't care about removing peers #}
 {%- for peer in server.peers %}
 peer {{ peer }}
-{%- endfor %}
-{%- endif %}
+{%- endfor -%}
+{%- endif -%}
+{%- endif -%}
+
 
 {%- if server.get('orphan') %}
 # Orphan mode stratum level
@@ -63,3 +100,22 @@
 # Location of drift file
 driftfile /var/lib/ntp/ntp.drift
 logfile /var/log/ntp.log
+
+{%- if client.get('auth', {}).get('enabled', False) or server.get('auth', {}).get('enabled', False) %}
+
+# Auth keys
+keys /etc/ntp.keys
+
+{# TODO: simplify once salt filter unique is available -#}
+
+{%- set secrets = {} %}
+{%- if client.get('auth', {}).get('enabled', False) %}
+{%- do secrets.update(client.auth.secrets) %}
+{%- endif %}
+{%- if server.get('auth', {}).get('enabled', False) %}
+{%- do secrets.update(server.auth.secrets) %}
+{%- endif -%}
+
+trustedkey {%- for key_name, key in secrets.items() %} {%- if key.trustedkey %} {{ key_name }} {%- endif %} {%- endfor -%}
+
+{%- endif -%}
diff --git a/ntp/files/ntp.keys b/ntp/files/ntp.keys
new file mode 100644
index 0000000..4b87357
--- /dev/null
+++ b/ntp/files/ntp.keys
@@ -0,0 +1,15 @@
+{%- from "ntp/map.jinja" import client, server with context %}
+
+{%- set secrets = {} %}
+
+{%- if client.get('auth', {}).get('enabled', False) %}
+{%- do secrets.update(client.auth.secrets) %}
+{%- endif %}
+
+{%- if server.get('auth', {}).get('enabled', False) %}
+{%- do secrets.update(server.auth.secrets) %}
+{%- endif %}
+
+{%- for key_name, key in secrets.items() %}
+{{ key_name }} {{ key.secret_type }} {{ key.secret }}
+{%- endfor %}
diff --git a/ntp/meta/collectd.yml b/ntp/meta/collectd.yml
index a686ceb..9e3f898 100644
--- a/ntp/meta/collectd.yml
+++ b/ntp/meta/collectd.yml
@@ -1,6 +1,6 @@
 {% from "ntp/map.jinja" import client with context %}
 
-{%- if client.get('enabled', False) and client.mode7 %}
+{%- if client.get('enabled', False) and client.get('mode7', False) %}
 local_plugin:
   ntp_server_status:
     plugin: ntpd
diff --git a/ntp/meta/meta.yml b/ntp/meta/meta.yml
index 097f691..f63956d 100644
--- a/ntp/meta/meta.yml
+++ b/ntp/meta/meta.yml
@@ -5,11 +5,20 @@
   service: ntp.client
   type: software-system
   relations:
-  {%- for stratum in client.strata %}
+  {%- if client.stratum is defined %}
+  {%- for _, stratum in client.stratum.items() %}
   - service: other-service
+    host_external: udp://{{ stratum.server }}
+    direction: source
+    type: udp
+  {%- endfor %}
+  {%- else %}
+  {%- for stratum in client.strata %}
+    - service: other-service
     host_external: udp://{{ stratum }}
     direction: source
     type: udp
   {%- endfor %}
+  {%- endif %}
 {%- endif %}
 
diff --git a/ntp/schemas/client.yaml b/ntp/schemas/client.yaml
index becc91a..21033b4 100644
--- a/ntp/schemas/client.yaml
+++ b/ntp/schemas/client.yaml
@@ -1,6 +1,4 @@
-%YAML 1.1
----
-"$schema": "http://json-schema.org/draft-06/schema#"
+$schema: "http://json-schema.org/draft-06/schema#"
 title: NTP client role
 description: |
   NTP service, client role.
@@ -15,16 +13,74 @@
     description: |
       Enables NTP client service.
     type: boolean
+  mode7:
+    description: |
+      Enables mode7 for the NTP server.
+    type: boolean
   strata:
     description: |
       List of NTP stratums to keep the time in sync.
     type: array
+    minProperties: 1
     items:
-      $ref: "#/definitions/stratum"
+      $ref: "#/definitions/_ntp:common:strata"
+  stratum:
+    description: |
+      List of NTP stratums to keep the time in sync. If define used instead of strata
+    type: object
+    patternProperties:
+      "^[0-9]*$":
+        $ref: "#/definitions/_ntp:common:stratum"
+  auth:
+    description: |
+     Support of ntp auth.
+    type: object
+    additionalProperties: false
+    required: [enabled, secrets]
+    properties:
+      enabled:
+        description: |
+          Enables NTP auth.
+        type: boolean
+      secrets:
+        description: |
+          Dict with secrets
+        type: object
+        additionalProperties: false
+        minProperties: 1
+        patternProperties:
+          "^[0-9]*$":
+            $ref: '#/definitions/_ntp:common:secret'
 
 definitions:
-  stratum:
+  _ntp:common:strata:
     description: |
       Hostname or IP address of the stratum server.
     type: string
     format: hostname-ip
+  _ntp:common:stratum:
+    description: |
+      Define exactly one ntp stratum server with more parameters.
+    type: object
+    additionalProperties: false
+    minProperties: 1
+    required: [server]
+    properties:
+      server:
+        type: string
+      key_id:
+        type: integer
+  _ntp:common:secret:
+    description: |
+     Define exactly one ntp auth secret
+    type: object
+    additionalProperties: false
+    minProperties: 1
+    required: [secret_type, secret, trustedkey ]
+    properties:
+      secret_type:
+        type: string
+      secret:
+        type: string
+      trustedkey:
+        type: boolean
diff --git a/ntp/schemas/server.yaml b/ntp/schemas/server.yaml
index b578323..44ccbdf 100644
--- a/ntp/schemas/server.yaml
+++ b/ntp/schemas/server.yaml
@@ -30,27 +30,52 @@
       List of subnets that servers gives time to.
     type: array
     items:
-      $ref: "#/definitions/restrict"
+      $ref: "#/definitions/_ntp:server:restrict"
   peers:
     description: |
       List of peered NTP stratum services.
     type: array
     items:
-      $ref: "#/definitions/peer"
+      $ref: "#/definitions/_ntp:server:peer"
   strata:
     description: |
       List of NTP stratums to keep the time in sync.
     type: array
     items:
-      $ref: "#/definitions/stratum"
-
+      $ref: "#/definitions/_ntp:common:strata"
+  stratum:
+    description: |
+      List of NTP stratums to keep the time in sync. If define used instead of strata
+    type: object
+    patternProperties:
+      "^[0-9]*$":
+        $ref: "#/definitions/_ntp:common:stratum"
+  auth:
+    description: |
+     Support of ntp auth.
+    type: object
+    additionalProperties: false
+    required: [enabled, secrets]
+    properties:
+      enabled:
+        description: |
+          Enables NTP auth.
+        type: boolean
+      secrets:
+        description: |
+          Dict with secrets
+        type: object
+        additionalProperties: false
+        minProperties: 1
+        patternProperties:
+          "^[0-9]*$":
+            $ref: '#/definitions/_ntp:common:secret'
 definitions:
-  restrict:
+  _ntp:server:restrict:
     description: |
       Restrict the service to given networks.
     type: object
-    flowStyle: block
-    propertyOrder: [subnet, mask,options]
+    additionalProperties: false
     required: [subnet, mask]
     properties:
       subnet:
@@ -63,19 +88,75 @@
           Subnet mask of the network
         type: string
         style: inline
+        example: 255.255.255.0
       options:
         description: |
           Additional options passed to the net [notrap nomodify]
         type: string
         style: inline
-    additionalProperties: false
-  peer:
+  _ntp:server:peer:
     description: |
-      Hostname or IP address of the peer server.
-    type: string
-    format: hostname-ip
-  stratum:
+      Configuration one  peer server address.
+    type: object
+    additionalProperties: false
+    minProperties: 1
+    required: [host]
+    properties:
+      host:
+        type: string
+      key_id:
+        type: integer
+  _ntp:common:strata:
     description: |
       Hostname or IP address of the stratum server.
     type: string
     format: hostname-ip
+  _ntp:common:stratum:
+    description: |
+      Define exactly one ntp stratum server with more parameters.
+    type: object
+    additionalProperties: false
+    minProperties: 1
+    required: [server] #, key_id ]
+    properties:
+      server:
+        type: string
+      key_id:
+        type: integer
+  _ntp:common:interface:
+    description: |
+      Define exactly one ntp interface to configure.
+    type: object
+    additionalProperties: false
+    minProperties: 1
+    required: [action,value]
+    properties:
+      action:
+        description: |
+          Determines the action for addresses which match
+        type: string
+        eval: [listen, ignore, drop ]
+      value:
+        description: |
+          That parameter specifies a class of addresses, or a specific
+          interface name, or an address. In the address case, prefixlen
+          determines how many bits must match for this rule to apply.
+          ignore prevents opening matching addresses, drop causes ntpd to
+          open the address and drop all received packets without examination.
+        type: string
+        example: "all | ipv4 | ipv6 | wildcard | name | address[/prefixlen]"
+  _ntp:common:secret:
+    description: |
+     Define exactly one ntp auth secret
+    type: object
+    additionalProperties: false
+    minProperties: 1
+    required: [secret_type, secret, trustedkey ]
+    properties:
+      secret_type:
+        type: string
+      secret:
+        type: string
+      trustedkey:
+        type: boolean
+
diff --git a/ntp/server.sls b/ntp/server.sls
index 81d2d80..6c3a8bc 100644
--- a/ntp/server.sls
+++ b/ntp/server.sls
@@ -37,6 +37,23 @@
 
 {%- endif %}
 
+{%- if server.get('auth', {}).get('enabled', False) %}
+
+ntp_keys_server:
+  file.managed:
+  - name: /etc/ntp.keys
+  - source: salt://ntp/files/ntp.keys
+  - user: root
+  - group: root
+  - mode: 600
+  - template: jinja
+  - require:
+    - pkg: ntp_packages
+  - watch_in:
+    - ntp_service
+
+{%- endif %}
+
 /etc/ntp.conf:
   file.managed:
   - source: salt://ntp/files/ntp.conf
@@ -51,4 +68,4 @@
   - watch:
     - file: /etc/ntp.conf
 
-{%- endif %}
\ No newline at end of file
+{%- endif %}
diff --git a/tests/pillar/client_auth.sls b/tests/pillar/client_auth.sls
new file mode 100644
index 0000000..ebe515e
--- /dev/null
+++ b/tests/pillar/client_auth.sls
@@ -0,0 +1,25 @@
+ntp:
+  client:
+    enabled: true
+    auth:
+      enabled: true
+      secrets:
+        # Jsonschemavalidator expect pattern keys in str format
+        "1":
+          secret_type: 'M'
+          secret: 'Runrabbitrundigthath'
+          trustedkey: true
+        "2":
+          secret_type: 'M'
+          secret: 'Howiwishyouwereherew'
+          trustedkey: false
+    strata:
+    - ntp.cesnet.cz
+    - pool.ntp.org
+    stratum:
+      primary:
+        server: ntp.cesnet.cz
+        key_id: 1
+      secondary:
+        server: ntp.nic.cz
+        key_id: 2
\ No newline at end of file
diff --git a/tests/pillar/server.sls b/tests/pillar/server.sls
index 0f2e2f2..5287e83 100644
--- a/tests/pillar/server.sls
+++ b/tests/pillar/server.sls
@@ -1,18 +1,18 @@
 ntp:
   server:
     enabled: true
+    mode7: true
+    orphan: 5
+    peers:
+    - host: 192.168.31.1
+    - host: 192.168.31.2
+    - host: 192.168.31.3
+    restrict:
+    - mask: 255.255.255.0
+      subnet: 192.168.0.1
+    - mask: 255.255.0.0
+      options: notrap nomodify
+      subnet: 172.16.1.1
     strata:
     - ntp.cesnet.cz
-    - pool.ntp.org
-    restrict:
-      - subnet: 192.168.0.1
-        mask: 255.255.255.0
-      - subnet: 172.16.1.1
-        mask: 255.255.0.0
-        options: notrap nomodify
-    mode7: true
-    peers:
-      - 192.168.31.1
-      - 192.168.31.2
-      - 192.168.31.3
-    orphan: 5
+    - pool.ntp.org
\ No newline at end of file
diff --git a/tests/pillar/server_auth.sls b/tests/pillar/server_auth.sls
new file mode 100644
index 0000000..9c351c9
--- /dev/null
+++ b/tests/pillar/server_auth.sls
@@ -0,0 +1,37 @@
+ntp:
+  server:
+    enabled: true
+    auth:
+      enabled: true
+      secrets:
+        # Jsonschemavalidator expect pattern keys in str format
+        "1":
+          secret: Runrabbitrundigthath
+          secret_type: M
+          trustedkey: true
+        "2":
+          secret: Howiwishyouwereherew
+          secret_type: M
+          trustedkey: false
+    mode7: true
+    orphan: 5
+    peers:
+    - host: 192.168.31.1
+    - host: 192.168.31.2
+    - host: 192.168.31.3
+    restrict:
+    - mask: 255.255.255.0
+      subnet: 192.168.0.1
+    - mask: 255.255.0.0
+      options: notrap nomodify
+      subnet: 172.16.1.1
+    strata:
+    - ntp.cesnet.cz
+    - pool.ntp.org
+    stratum:
+      primary:
+        server: ntp.cesnet.cz
+        key_id: 1
+      secondary:
+        server: ntp.nic.cz
+        key_id: 2
diff --git a/tests/run_tests.sh b/tests/run_tests.sh
index 35929b5..7093161 100755
--- a/tests/run_tests.sh
+++ b/tests/run_tests.sh
@@ -2,7 +2,8 @@
 
 ###
 # Script requirments:
-#  apt-get install -y python-yaml virtualenv git
+#apt-get install -y python-yaml virtualenv git
+
 set -e
 [ -n "$DEBUG" ] && set -x
 
@@ -42,12 +43,12 @@
 
 setup_virtualenv() {
     log_info "Setting up Python virtualenv"
+    dependency_check virtualenv
     virtualenv $VENV_DIR
     source ${VENV_DIR}/bin/activate
     python -m pip install salt${PIP_SALT_VERSION}
-    python -m pip install reno
-    if [[ -f ${CURDIR}/pip_requirements.txt ]]; then
-       python -m pip install -r ${CURDIR}/pip_requirements.txt
+    if [[ -f ${CURDIR}/test-requirements.txt ]]; then
+       python -m pip install -r ${CURDIR}/test-requirements.txt
     fi
 }
 
@@ -94,7 +95,6 @@
   base:
   - ${SALT_FILE_DIR}
   - ${CURDIR}/..
-  - /usr/share/salt-formulas/env
 
 pillar_roots:
   base:
@@ -110,7 +110,7 @@
     dep_root="${DEPSDIR}/$(basename $dep_source .git)"
     dep_metadata="${dep_root}/metadata.yml"
 
-    [ -d /usr/share/salt-formulas/env/${dep_name} ] && { log_info "Dependency $dep_name already present in system-wide salt env"; return 0; }
+    dependency_check git
     [ -d $dep_root ] && { log_info "Dependency $dep_name already fetched"; return 0; }
 
     log_info "Fetching dependency $dep_name"
@@ -161,10 +161,12 @@
     setup_pillar
     setup_salt
     install_dependencies
+    link_modules
 }
 
 lint_releasenotes() {
     [[ ! -f "${VENV_DIR}/bin/activate" ]] && setup_virtualenv
+    source ${VENV_DIR}/bin/activate
     reno lint ${CURDIR}/../
 }
 
@@ -201,19 +203,38 @@
 }
 
 run_model_validate(){
-    [[ -d ${SCHEMARDIR} ]] || { log_err "${SCHEMARDIR} not found!"; return 1; }
+  # Run modelschema.model_validate validation.
+  # TEST iterateble, run for `each formula ROLE against each ROLE_PILLARNAME`
+  # Pillars should be named in conviend ROLE_XXX.sls or ROLE.sls
+  # Example:
+  # client.sls  client_auth.sls  server.sls  server_auth.sls
+  if [ -d ${SCHEMARDIR} ]; then
     # model validator require py modules
     fetch_dependency "salt:https://github.com/salt-formulas/salt-formula-salt"
     link_modules
-    # Rendered Example:
-    # salt-call --local -c /test1/maas/tests/build/salt --id=maas_cluster modelschema.model_validate maas cluster
+    salt_run saltutil.clear_cache; salt_run saltutil.refresh_pillar; salt_run saltutil.sync_all;
     for role in ${SCHEMARDIR}/*.yaml; do
-        state_name=$(basename "${role%*.yaml}")
-        minion_id="${state_name}"
-        # in case debug-reruns, usefull to make cleanup
-        [ -n "$DEBUG" ] && { salt_run saltutil.clear_cache; salt_run saltutil.refresh_pillar; salt_run saltutil.sync_all; }
-        salt_run --id=${minion_id} modelschema.model_validate ${FORMULA_NAME} ${state_name} || { log_err "Execution of ${FORMULA_NAME}.${state_name} failed"; exit 1 ; }
+      role_name=$(basename "${role%*.yaml}")
+      for pillar in pillar/${role_name}*.sls; do
+        pillar_name=$(basename "${pillar%*.sls}")
+        local _message="FORMULA:${FORMULA_NAME} ROLE:${role_name} against PILLAR:${pillar_name}"
+        log_info "model_validate ${_message}"
+        # Rendered Example:
+        # python $(which salt-call) --local -c /test1/maas/tests/build/salt --id=maas_cluster modelschema.model_validate maas cluster
+        salt_run -m ${DEPSDIR}/salt-formula-salt --id=${pillar_name} modelschema.model_validate ${FORMULA_NAME} ${role_name} || { log_err "Execution of model_validate ${_message} failed"; exit 1 ; }
+      done
     done
+  else
+    log_info "${SCHEMARDIR} not found!";
+  fi
+}
+
+dependency_check() {
+  local DEPENDENCY_COMMANDS=$*
+
+  for DEPENDENCY_COMMAND in $DEPENDENCY_COMMANDS; do
+    which $DEPENDENCY_COMMAND > /dev/null || ( log_err "Command \"$DEPENDENCY_COMMAND\" can not be found in default path."; exit 1; )
+  done
 }
 
 _atexit() {
diff --git a/tests/pip_requirements.txt b/tests/test-requirements.txt
similarity index 68%
rename from tests/pip_requirements.txt
rename to tests/test-requirements.txt
index d89304b..a0f561a 100644
--- a/tests/pip_requirements.txt
+++ b/tests/test-requirements.txt
@@ -1 +1,2 @@
 jsonschema
+reno