diff --git a/action_plugins/moinmoin_page.py b/action_plugins/moinmoin_page.py
new file mode 100755
index 0000000000000000000000000000000000000000..30bd8c4975eb1d95a18b825767fdc544d3127221
--- /dev/null
+++ b/action_plugins/moinmoin_page.py
@@ -0,0 +1,177 @@
+#!/usr/bin/env python3
+
+# Copyright: (c) 2019, Alexandre Iooss <erdnaxe@crans.org>
+#
+# GNU General Public License v3.0+
+
+import re
+import urllib.error
+import urllib.parse
+import urllib.request
+import difflib
+
+
+from ansible.errors import AnsibleError
+from ansible.plugins.action import ActionBase
+from ansible.utils.display import Display
+from ansible.module_utils._text import to_native
+
+display = Display()
+
+
+class ActionModule(ActionBase):
+
+    TRANSFERS_FILES = False
+    _VALID_ARGS = frozenset(('url', 'user', 'password', 'content', 'revision_comment'))
+
+    def login(self, url, user, password):
+        """
+        Log in and return session cookie or None if failed
+
+        :param url: random wiki url (not root page)
+        :param user: wiki user
+        :param password: user's password
+        :return: session cookie
+        """
+        # Send a HTTP POST request
+        data = urllib.parse.urlencode({
+            'action': 'login',
+            'login': 'Connexion',
+            'name': user,
+            'password': password
+        }).encode()
+        req = urllib.request.Request(url, data)
+        try:
+            response = urllib.request.urlopen(req)
+            cookie = response.getheader('set-cookie')
+        except urllib.error.HTTPError as e:
+            # If 404, then also return header
+            cookie = e.getheader('set-cookie')
+
+        # Check that authentication worked
+        if not cookie:
+            raise AnsibleError(to_native('server did not return a session cookie'))
+        return cookie
+
+    def craft_request(self, suffix):
+        """
+        Crafts a function that takes an url and a cookie,
+        and returns the content of the requested page with given action suffix.
+        """
+        def f(url, cookie):
+            req = urllib.request.Request(url + suffix)
+            req.add_header("Cookie", cookie)
+            content = urllib.request.urlopen(req).read().decode('utf-8')
+            return content
+        return f
+
+
+    def edit_ticket(self, url, cookie):
+        """
+        Return edition ticket of url
+
+        :param url: page to edit
+        :param cookie: session cookie
+        :return: edit ticket
+        """
+        # Send request with session cookie
+        content = self.craft_request("?action=edit&editor=text")(url, cookie)
+
+        # Search for ticket
+        search = re.search('name=\"ticket\" value=\"([^\"]*)\"', content)
+        if not search:
+            raise AnsibleError(to_native('no edit ticket was found'))
+
+        return search.group(1)
+
+
+    def edit(self, url, user, password, content, revision_comment, cookie):
+        """
+        Edit a MoinMoin wiki page
+
+        :param url: page to edit
+        :param user: wiki user
+        :param password: user's password
+        :param content: content to place on this page
+        :param revision_comment: revision comment
+        """
+        # Connect and get edit ticket
+        ticket = self.edit_ticket(url, cookie)
+
+        # Create request and send
+        data = {
+            'button_save': 'Enregistrer les modifications',
+            'category': '',
+            'comment': revision_comment.encode("utf-8"),
+            'savetext': content.encode("utf-8"),
+            'action': 'edit',
+            'ticket': ticket
+        }
+        req = urllib.request.Request(url, urllib.parse.urlencode(data).encode())
+        req.add_header("Cookie", cookie)
+        urllib.request.urlopen(req)
+
+
+    def run(self, tmp=None, task_vars=None):
+        """
+        The run method is the main Action Plugin driver. All work is done from within this 	method.
+
+        tmp: Temporary directory. Sometimes an action plugin sets up
+             a temporary directory and then calls another module. This parameter
+             allows us to reuse the same directory for both.
+
+        task_vars: The variables (host vars, group vars, config vars, etc) associated with this task.
+                   Note that while this will contain Ansible facts from the host, they should be used
+                   with caution as a user running Ansible can disable their collection. If you want
+                   make sure that your Action Plugin always has access to the ones it needs, you may
+                   want to consider running the setup module directly in the run the method and getting
+                   the Ansible facts that way.
+                   The strategy plugin which manages running tasks on instances uses an ansible.vars.manager
+                   VariableManager instance to retrieve this context specific dict of variables.
+        """
+        if task_vars is None:
+            task_vars = dict()
+
+
+        result = super(ActionModule, self).run(tmp, task_vars)
+        del tmp
+
+
+        url = self._task.args.get("url")
+        user = self._task.args.get("user")
+        password = self._task.args.get("password")
+        content = self._task.args.get("content")
+        revision_comment = self._task.args.get("revision_comment")
+
+        cookie = self.login(url, user, password)
+
+        try:
+            raw = self.craft_request("?action=raw")(url, cookie)
+        except urllib.error.HTTPError:  # We will create the page.
+            raw = ""
+
+        diff = difflib.unified_diff(raw.splitlines(), content.splitlines(), fromfile="old", tofile="new", lineterm="")
+        i=0
+
+        # Display any change
+        for line in diff:
+            i+=1
+            if line.startswith("-"):
+                display.display(line, "red")
+            elif line.startswith("+"):
+                display.display(line, "green")
+            elif line.startswith("@"):
+                display.display(line, "yellow")
+            else:
+                display.display(line)
+
+        # Do apply the change if not in check mode
+        if not self._play_context.check_mode:
+            self.edit(url, user, password, content, revision_comment, cookie)
+
+        result['changed']=i>0
+
+        self._supports_check_mode = True
+        self._supports_async = False
+
+        return result
diff --git a/all.yml b/all.yml
index 709ba117d65e758e97df9ec12d746853ab7c4512..67830dafb264ce0a3c8275690f81603d93cdd8c9 100755
--- a/all.yml
+++ b/all.yml
@@ -5,13 +5,13 @@
 
 - import_playbook: plays/mail.yml
 - import_playbook: plays/nfs.yml
-#- import_playbook: plays/logs.yml
-#- import_playbook: plays/backup.yml
-- import_playbook: plays/network-interfaces.yml
+#- import_playbook: plays/logs.yml  TODO: rsyncd
+- import_playbook: plays/backup.yml
+# - import_playbook: plays/network-interfaces.yml  TODO: check this paybook
 - import_playbook: plays/monitoring.yml
 
 # Services that only apply to a subset of server
-- import_playbook: plays/cas.yml
+# - import_playbook: plays/cas.yml
 - import_playbook: plays/dhcp.yml
 - import_playbook: plays/dns.yml
 - import_playbook: plays/etherpad.yml
diff --git a/hosts b/hosts
index ed6d9d842be8afcfaef893f464f2298c5b8914ca..a87d64d941832666d8b2f336040a0c12ec8d1f67 100644
--- a/hosts
+++ b/hosts
@@ -1,70 +1,59 @@
 # Crans servers inventory
 
-# How to name your server ?
-# > We name servers according to location, then type.
-# > Then we regroup everything in global geographic and type groups.
-
-
-# [framadate]
-# voyager.adm.crans.org
-#
-# [dhcp]
-# dhcp.adm.crans.org
-# odlyd.adm.crans.org
-#
-# [keepalived]
-# gulp.adm.crans.org
-# odlyd.adm.crans.org
-# eap.adm.crans.org
-# radius.adm.crans.org
-# frontdaur.adm.crans.org
-# bakdaur.adm.crans.org
-#
-# [test_vm]
-# re2o-test.adm.crans.org
-[dovecot]
-owl.adm.crans.org
-
 [backups]
 zephir.adm.crans.org
 
-[certbot]
-gitzly.adm.crans.org
+[baie]
+cameron.adm.crans.org
+tealc.adm.crans.org
+
+[bdd]
+tealc.adm.crans.org
 
 [certbot:children]
+dovecot
+git
 radius  # We use certbot to manage LE certificates
 reverseproxy
-dovecot
 
-[nginx_rtmp]
-fluxx.adm.crans.org
+[dhcp:children]
+routeurs_vm
 
-[reverseproxy]
-hodaur.adm.crans.org
+[dns_auth_master]
+silice.adm.crans.org
 
-[roundcube]
-roundcube-srv.adm.crans.org
+[dns_authoritative:children]
+dns_auth_master
+freebox
+ovh_physical
+
+[dns_recursive:children]
+routeurs_vm
+
+[dovecot]
+owl.adm.crans.org
 
 [ethercalc]
 ethercalc-srv.adm.crans.org
 
-[horde]
-horde.adm.crans.org
+[framadate]
+voyager.adm.crans.org
 
-[radius]
-routeur-sam.adm.crans.org
+[freebox]
+boeing.adm.crans.org
+titanic.adm.crans.org
 
-[re2o]
-re2o-newinfra.adm.crans.org
-routeur-sam.adm.crans.org
+[git]
+gitzly.adm.crans.org
 
-[bdd]
-tealc.adm.crans.org
+[horde]
+horde.adm.crans.org
 
-[virtu]
-sam.adm.crans.org
-daniel.adm.crans.org
-jack.adm.crans.org
+[irc]
+irc.adm.crans.org
+
+[keepalived:children]
+routeurs_vm
 
 [ldap_server]
 tealc.adm.crans.org
@@ -72,55 +61,98 @@ sam.adm.crans.org
 daniel.adm.crans.org
 jack.adm.crans.org
 
-[keepalived]
-routeur-sam.adm.crans.org
-#routeur-daniel.adm.crans.org
+[monitoring]
+monitoring.adm.crans.org
+
+[nginx]
+charybde.adm.crans.org
+
+[nginx_rtmp]
+fluxx.adm.crans.org
+
+[nginx:children]
+reverseproxy
+
+[postfix]
+mailman.adm.crans.org
+redisdead.adm.crans.org
+zamok.adm.crans.org
+
+[postfix:children]
+freebox
+ovh_physical
+
+[radius:children]
+routeurs_vm
+
+[re2o]
+re2o-newinfra.adm.crans.org
+
+[re2o:children]
+radius
 
-[dhcp]
+[reverseproxy]
+hodaur.adm.crans.org
+
+[roundcube]
+roundcube-srv.adm.crans.org
+
+[routeurs_vm]
+routeur-daniel.adm.crans.org
+routeur-jack.adm.crans.org
 routeur-sam.adm.crans.org
-#routeur-daniel.adm.crans.org
+
+[virtu]
+daniel.adm.crans.org
+jack.adm.crans.org
+sam.adm.crans.org
 
 [crans_routeurs:children]
-dhcp
-keepalived
+# dhcp  TODO: Really needed ?
+# keepalived
+routeurs_vm
 
 [crans_physical]
-cameron.adm.crans.org
-tealc.adm.crans.org
-sam.adm.crans.org
-daniel.adm.crans.org
-jack.adm.crans.org
-#gulp.adm.crans.org
-zephir.adm.crans.org
+charybde.adm.crans.org
+omnomnom.adm.crans.org
+zamok.adm.crans.org
+
+[crans_physical:children]
+backups
+baie
+virtu
 
 [crans_vm]
-owl.adm.crans.org
-codichotomie.adm.crans.org
-voyager.adm.crans.org
-#silice.adm.crans.org
-routeur-sam.adm.crans.org
-#routeur-daniel.adm.crans.org
 #belenios.adm.crans.org
-#re2o-ldap.adm.crans.org
+bigbluebutton.adm.crans.org
+#boeing.adm.crans.org
+#casouley.adm.crans.org
+codichotomie.adm.crans.org
+#ethercalc-srv.adm.crans.org
+fluxx.adm.crans.org
 gitlab-ci.adm.crans.org
 gitzly.adm.crans.org
 hodaur.adm.crans.org
-monitoring.adm.crans.org
-#boeing.adm.crans.org
-fluxx.adm.crans.org
-#unifi.adm.crans.org
-#pastemoisa.adm.crans.org
-#casouley.adm.crans.org
-kiwi.adm.crans.org
-kiwijuice.adm.crans.org
-tracker.adm.crans.org
+horde.adm.crans.org
+irc.adm.crans.org
 jitsi.adm.crans.org
-#ethercalc-srv.adm.crans.org
 kenobi.adm.crans.org
-roundcube.adm.crans.org
-horde.adm.crans.org
-bigbluebutton.adm.crans.org
+kiwi.adm.crans.org
+kiwijuice.adm.crans.org
+monitoring.adm.crans.org
+owl.adm.crans.org
 owncloud.adm.crans.org
+#re2o-ldap.adm.crans.org
+redisdead.adm.crans.org
+roundcube.adm.crans.org
+#silice.adm.crans.org
+titanic.adm.crans.org
+tracker.adm.crans.org
+voyager.adm.crans.org
+#unifi.adm.crans.org
+
+[crans_vm:children]
+routeurs_vm
 
 [ovh_physical]
 sputnik.adm.crans.org
@@ -129,7 +161,6 @@ sputnik.adm.crans.org
 [crans_server:children]
 crans_physical
 crans_vm
-crans_routeurs
 
 # everything at crans
 [crans:children]
@@ -147,7 +178,6 @@ ovh_physical
 # every virtual machine
 [vm:children]
 crans_vm
-crans_routeurs
 
 # every server
 [server:children]
diff --git a/plays/dns.yml b/plays/dns.yml
index b261acaa7293ba4dc0ace4eb27b24e0231c9acce..4e61330fbcfc8e2174ab732e058e89602b78189f 100755
--- a/plays/dns.yml
+++ b/plays/dns.yml
@@ -1,12 +1,12 @@
 #!/usr/bin/env ansible-playbook
 ---
 # Deploy recursive DNS cache server
-- hosts: routeur-sam.adm.crans.org,routeur-daniel.adm.crans.org
+- hosts: dns_recursive
   roles:
     - bind-recursive
 
 # Deploy authoritative DNS server
-- hosts: silice.adm.crans.org,sputnik.adm.crans.org,boeing.adm.crans.org
+- hosts: dns_authoritative
   vars:
     certbot_dns_secret: "{{ vault_certbot_dns_secret }}"
     certbot_adm_dns_secret: "{{ vault_certbot_adm_dns_secret }}"
@@ -18,7 +18,7 @@
   roles:
     - bind-authoritative
 
-- hosts: silice.adm.crans.org
+- hosts: dns_auth_master
   vars:
     re2o:
       server: re2o.adm.crans.org
diff --git a/plays/mail.yml b/plays/mail.yml
index ea4cc641b3234fbc1711fba625c6f4544873f4a2..536a2b6866fb3a4cfb4b639d6c21b6377fe56b1a 100755
--- a/plays/mail.yml
+++ b/plays/mail.yml
@@ -6,7 +6,7 @@
 # All other servers uses nullmailer to send local mail to Crans SMTP.
 
 # Redirect local mail to mailserver
-- hosts: crans_server,!redisdead.adm.crans.org,!soyouz.adm.crans.org,!titanic.adm.crans.org,!boeing.adm.crans.org,!sputnik.adm.crans.org,!zamok.adm.crans.org,!mailman.adm.crans.org
+- hosts: crans_server,!postfix
   vars:
     mail_root: root@crans.org
     mail_smtp_server: smtp.adm.crans.org
diff --git a/plays/monitoring.yml b/plays/monitoring.yml
index ca8fc85a006bf8beff366f0fcbb71d43b438247f..2a6c6bcd8d03d78d714c34a6162948d2ed8822f2 100755
--- a/plays/monitoring.yml
+++ b/plays/monitoring.yml
@@ -1,7 +1,7 @@
 #!/usr/bin/env ansible-playbook
 ---
 # Deploy Prometheus and Grafana on monitoring server
-- hosts: monitoring.adm.crans.org
+- hosts: monitoring
   vars:
     # Prometheus targets.json
     prometheus:
@@ -64,13 +64,13 @@
 
 
 # Monitor all hosts
-- hosts: server,test_vm
+- hosts: server
   vars:
     adm_ipv4: "{{ query('ldap', 'ip', ansible_hostname, 'adm') | ipv4 | first }}"
   roles: ["prometheus-node-exporter"]
 
 # Export nginx metrics
-- hosts: charybde.adm.crans.org,hodaur.adm.crans.org
+- hosts: nginx
   vars:
     adm_ipv4: "{{ query('ldap', 'ip', ansible_hostname, 'adm') | ipv4 | first }}"
   roles: ["prometheus-nginx-exporter"]
diff --git a/plays/root.yml b/plays/root.yml
index a6b8f7906abeb8b1a4f469aa8071b684b41b6e8c..73e17b54114ec3cd52ae129ef8fd1e61db2db5a0 100755
--- a/plays/root.yml
+++ b/plays/root.yml
@@ -20,9 +20,7 @@
         insertafter: '127.0.0.1 localhost'
       when: check_mirror.found == 0
 
-
-
-- hosts: tealc.adm.crans.org
+- hosts: baie
   roles:
     - baie
 
@@ -75,7 +73,7 @@
   roles:
     - openssh
 
-- hosts: server
+- hosts: crans_vm
   tasks:
     - name: Remove cloud-init
       apt:
diff --git a/roles/moinmoin-gendoc/library/dmidecode.py b/roles/moinmoin-gendoc/library/dmidecode_facts.py
similarity index 70%
rename from roles/moinmoin-gendoc/library/dmidecode.py
rename to roles/moinmoin-gendoc/library/dmidecode_facts.py
index 6e01acc36f5a78424bdb93a610dc48bf86913054..765713d6149841c5c68a3321abfd315379113fa9 100644
--- a/roles/moinmoin-gendoc/library/dmidecode.py
+++ b/roles/moinmoin-gendoc/library/dmidecode_facts.py
@@ -32,7 +32,6 @@ EXAMPLES = '''
 
 '''
 
-import dmidecode
 import json
 
 from ansible.module_utils.basic import AnsibleModule
@@ -48,16 +47,30 @@ def decode_dict(data):
 
 def run_module():
     module = AnsibleModule(
-        argument_spec = {}
+        argument_spec = {},
+        supports_check_mode=True,
     )
-    dmi_data = decode_dict({
-        'bios': dmidecode.bios(),
-        'processor': dmidecode.processor(),
-        'system': dmidecode.system(),
-        'memory': dmidecode.memory(),
-        'slot': dmidecode.slot(),
-    })
-    module.exit_json(changed=True, ansible_facts=dmi_data)
+
+    try:
+        import dmidecode
+        dmi_data = decode_dict({
+            'bios': dmidecode.bios(),
+            'processor': dmidecode.processor(),
+            'system': dmidecode.system(),
+            'memory': dmidecode.memory(),
+            'slot': dmidecode.slot(),
+        })
+        
+    except ImportError:
+        dmi_data = {
+            'bios': dict(),
+            'processor': dict(),
+            'system': dict(),
+            'memory': dict(),
+            'slot': dict(),
+        }
+
+    module.exit_json(changed=False, ansible_facts=dmi_data)
 
 
 def main():
diff --git a/roles/moinmoin-gendoc/library/moinmoin_page.py b/roles/moinmoin-gendoc/library/moinmoin_page.py
deleted file mode 100644
index b6f6ee91a23af165afd08f7ac01099d4f59c3563..0000000000000000000000000000000000000000
--- a/roles/moinmoin-gendoc/library/moinmoin_page.py
+++ /dev/null
@@ -1,139 +0,0 @@
-#!/usr/bin/env python3
-
-# Copyright: (c) 2019, Alexandre Iooss <erdnaxe@crans.org>
-# GNU General Public License v3.0+
-
-"""
-This module simulate the edition of a MoinMoin wiki page
-
-Example:
-  moinmoin_page:
-    url: https://wiki.crans.org/WikiErdnaxe
-    user: WikiErdnaxe
-    password: HoTuNeMeConnaisPas
-    content: "{{ lookup('template', 'mapage.j2') }}"
-    revision_comment: Bip bip
-"""
-
-import re
-import urllib.error
-import urllib.parse
-import urllib.request
-
-from ansible.module_utils.basic import AnsibleModule
-
-
-def login(url, user, password):
-    """
-    Log in and return session cookie or None if failed
-
-    :param url: random wiki url (not root page)
-    :param user: wiki user
-    :param password: user's password
-    :return: session cookie
-    """
-    # Send a HTTP POST request
-    data = urllib.parse.urlencode({
-        'action': 'login',
-        'login': 'Connexion',
-        'name': user,
-        'password': password
-    }).encode()
-    req = urllib.request.Request(url, data)
-    try:
-        response = urllib.request.urlopen(req)
-        cookie = response.getheader('set-cookie')
-    except urllib.error.HTTPError as e:
-        # If 404, then also return header
-        cookie = e.getheader('set-cookie')
-
-    # Check that authentication worked
-    assert cookie, 'server did not return a session cookie'
-    return cookie
-
-
-def edit_ticket(url, cookie):
-    """
-    Return edition ticket of url
-
-    :param url: page to edit
-    :param cookie: session cookie
-    :return: edit ticket
-    """
-    # Send request with session cookie
-    suffix = "?action=edit&editor=text"
-    req = urllib.request.Request(url + suffix)
-    req.add_header("Cookie", cookie)
-    content = urllib.request.urlopen(req).read().decode('utf-8')
-
-    # Search for ticket
-    search = re.search('name=\"ticket\" value=\"([^\"]*)\"', content)
-    assert search, 'no edit ticket was found'
-    return search.group(1)
-
-
-def edit(url, user, password, content, revision_comment):
-    """
-    Edit a MoinMoin wiki page
-
-    :param url: page to edit
-    :param user: wiki user
-    :param password: user's password
-    :param content: content to place on this page
-    :param revision_comment: revision comment
-    """
-    # Connect and get edit ticket
-    cookie = login(url, user, password)
-    ticket = edit_ticket(url, cookie)
-
-    # Create request and send
-    data = {
-        'button_save': 'Enregistrer les modifications',
-        'category': '',
-        'comment': revision_comment.encode("utf-8"),
-        'savetext': content.encode("utf-8"),
-        'action': 'edit',
-        'ticket': ticket
-    }
-    req = urllib.request.Request(url, urllib.parse.urlencode(data).encode())
-    req.add_header("Cookie", cookie)
-    urllib.request.urlopen(req)
-
-
-def run_module():
-    # Define arguments that should be passed
-    module_args = {
-        'url': {'type': 'str', 'required': True},
-        'user': {'type': 'str', 'required': True},
-        'password': {'type': 'str', 'required': True},
-        'content': {'type': 'str', 'required': True},
-        'revision_comment': {'type': 'str', 'required': True},
-    }
-
-    # Define arguments that are returned
-    result = {
-        'changed': False,
-    }
-
-    # Our AnsibleModule
-    module = AnsibleModule(
-        argument_spec=module_args,
-        supports_check_mode=True
-    )
-
-    # TODO: get current wiki page and compare
-    result['changed'] = True
-
-    # If not is check mode and page need to change, then update page
-    if not module.check_mode and result['changed']:
-        edit(**module.params)
-
-    module.exit_json(**result)
-
-
-def main():
-    run_module()
-
-
-if __name__ == '__main__':
-    main()
diff --git a/roles/moinmoin-gendoc/tasks/main.yml b/roles/moinmoin-gendoc/tasks/main.yml
index bcc819b5d1e403cdfcbc5ef56f7de166300d215d..a821e24791dc731454bda34376ecde627503de13 100644
--- a/roles/moinmoin-gendoc/tasks/main.yml
+++ b/roles/moinmoin-gendoc/tasks/main.yml
@@ -8,7 +8,7 @@
   until: apt_result is succeeded
 
 - name: get dmidecode facts
-  dmidecode: {}
+  dmidecode_facts: {}
 
 - name: get ssh fingerprints
   sshfp: {}
diff --git a/roles/moinmoin-gendoc/templates/server.j2 b/roles/moinmoin-gendoc/templates/server.j2
index e287e258f246eef8d6943296fcb24346694d0f9b..c49a503996995abfecc3f67cd972bf8580508106 100644
--- a/roles/moinmoin-gendoc/templates/server.j2
+++ b/roles/moinmoin-gendoc/templates/server.j2
@@ -4,7 +4,7 @@
 == Caractéristiques matérielles ==
 
 {% if ansible_form_factor != 'Other' and ansible_form_factor != 'Unknown' %}
-'''Forme du serveur''' : 
+'''Forme du serveur''' :
 {{ ansible_form_factor }}
 
 {% endif %}
@@ -56,7 +56,7 @@ et {{ (ansible_memory_mb.swap.total/1024)|round(1) }} GiB de SWAP.
 == Caractéristiques logicielles ==
 
 '''Système d'exploitation''' :
-{{ ansible_lsb.description }}
+{{ ansible_distribution }} {{ ansible_distribution_major_version }} ({{ ansible_distribution_release }})
 
 '''Noyau''' :
 {{ ansible_kernel }}