diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 023ed7829d63387ddd5eb8496b3faa93e99028ea..8b3e07cf816742ca4995d6b1668db6b573103be5 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -18,13 +18,6 @@ python3.5:
     - pip install tox
     - tox -e py35 pep8
 
-python2.7:
-    image: python:2.7
-    stage: test
-    script:
-    - pip install tox
-    - tox -e py27 pep8
-
 python3.6:
     image: python:3.6
     stage: test
diff --git a/.travis.yml b/.travis.yml
index 0a80ccbca166e2ca4a7bb83673e318d741622a55..1ea9d8e4bbb82c614b973e0a33c174e49fe87987 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,6 @@
 sudo: false
 language: python
 python:
-      - "2.7"
       - "3.5"
       - "3.6"
 install: pip install tox-travis
diff --git a/docs/apidoc/infra.rst b/docs/apidoc/infra.rst
index 7f6c29bf3a4817b33c888f6b42922cc7ba9167ed..d751ba55f1c3c58e5438e43f639f19d2b3616bd0 100644
--- a/docs/apidoc/infra.rst
+++ b/docs/apidoc/infra.rst
@@ -9,52 +9,106 @@ Base Provider Class
     :undoc-members:
     :show-inheritance:
 
+
+Vagrant
+-------
+
 Vagrant Provider Class
-----------------------
+^^^^^^^^^^^^^^^^^^^^^^
 
 .. automodule:: enoslib.infra.enos_vagrant.provider
     :members:
     :undoc-members:
     :show-inheritance:
 
-    .. autodata:: FLAVORS
+Vagrant Schema
+^^^^^^^^^^^^^^
+
+.. literalinclude:: ../../enoslib/infra/enos_vagrant/schema.py
 
-Grid5000 Provider Class
------------------------
+
+Grid5000 (G5k)
+--------------
+
+G5k Provider Class
+^^^^^^^^^^^^^^^^^^
 
 .. automodule:: enoslib.infra.enos_g5k.provider
     :members:
     :undoc-members:
     :show-inheritance:
 
-Grid5000 API
-^^^^^^^^^^^^
+G5k Schema
+^^^^^^^^^^
+
+.. literalinclude:: ../../enoslib/infra/enos_g5k/schema.py
+
+G5k API
+^^^^^^^
 
 .. automodule:: enoslib.infra.enos_g5k.api
     :members:
     :undoc-members:
     :show-inheritance:
 
+Static
+------
+
+Static Provider Class
+^^^^^^^^^^^^^^^^^^^^^
+
+.. automodule:: enoslib.infra.enos_static.provider
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
+Static Schema
+^^^^^^^^^^^^^
+
+.. literalinclude:: ../../enoslib/infra/enos_static/schema.py
+
+Openstack
+---------
+
 Openstack Provider Class
-------------------------
+^^^^^^^^^^^^^^^^^^^^^^^^
 
 .. automodule:: enoslib.infra.enos_openstack.provider
     :members:
     :undoc-members:
     :show-inheritance:
 
+Openstack Schema
+^^^^^^^^^^^^^^^^
+
+.. literalinclude:: ../../enoslib/infra/enos_openstack/schema.py
+
+Chameleon
+---------
+
 Chameleon(kvm) Provider Class
------------------------------
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 .. automodule:: enoslib.infra.enos_chameleonkvm.provider
     :members:
     :undoc-members:
     :show-inheritance:
 
+Chameleon(kvm) Schema
+^^^^^^^^^^^^^^^^^^^^^
+
+.. literalinclude:: ../../enoslib/infra/enos_chameleonkvm/schema.py
+
 Chameleon(bare metal) Provider Class
-------------------------------------
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 .. automodule:: enoslib.infra.enos_chameleonbaremetal.provider
     :members:
     :undoc-members:
     :show-inheritance:
+
+Chameleon(bare metal) schema
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. literalinclude:: ../../enoslib/infra/enos_chameleonbaremetal/schema.py
+
diff --git a/docs/tutorials/chameleon.rst b/docs/tutorials/chameleon.rst
index 763fc1127b76d693473ed508bf149614f4c54c39..b9ca0ed032cbc64fe8be7046095a2973d2d8dddb 100644
--- a/docs/tutorials/chameleon.rst
+++ b/docs/tutorials/chameleon.rst
@@ -15,10 +15,29 @@ Installation
 
   It's a good practice to use a virtualenv or a python version manager like `pyenv`.
 
-Alternatively you can also install from sources: 
+
+
+Basic example
+-------------
+
+The following reserve 2 nodes on the chameleon baremetal infrastructure.
+Prior to the execution you must source your openrc file:
 
 .. code-block:: bash
 
-    $ pip install -U git+https://github.com/BeyondTheClouds/enoslib/enoslib#egg=enoslib[chameleon]
-    
-..
+   $ source CH-XXXXX.sh
+
+
+You must also configure an access key in you project and replace with its name
+in the following.
+
+
+.. literalinclude:: chameleon/tuto_chameleonbaremetal.py
+   :language: python
+   :linenos:
+
+
+.. note::
+
+   Similarly to other provider the configuration can be generated
+   programmatically instead of using a dict.
diff --git a/docs/tutorials/chameleon/tuto_chameleonbaremetal.py b/docs/tutorials/chameleon/tuto_chameleonbaremetal.py
new file mode 100644
index 0000000000000000000000000000000000000000..7b92021b99b810df96a936c1ccf017817b70aaf2
--- /dev/null
+++ b/docs/tutorials/chameleon/tuto_chameleonbaremetal.py
@@ -0,0 +1,39 @@
+from enoslib.api import generate_inventory, emulate_network, validate_network
+from enoslib.infra.enos_chameleonbaremetal.provider import Chameleonbaremetal
+from enoslib.infra.enos_chameleonbaremetal.configuration import Configuration
+
+import logging
+import os
+
+logging.basicConfig(level=logging.INFO)
+
+provider_conf = {
+    "key_name": "enos_matt",
+    "resources": {
+        "machines": [{
+            "roles": ["control"],
+            "flavour": "compute_skylake",
+            "number": 1,
+        },{
+            "roles": ["compute"],
+            "flavour": "compute_skylake",
+            "number": 1,
+        }],
+        "networks": ["network_interface"]
+    }
+}
+
+tc = {
+    "enable": True,
+    "default_delay": "20ms",
+    "default_rate": "1gbit",
+}
+inventory = os.path.join(os.getcwd(), "hosts")
+conf = Configuration.from_dictionnary(provider_conf)
+provider = Chameleonbaremetal(conf)
+# provider.destroy()
+roles, networks = provider.init()
+generate_inventory(roles, networks, inventory, check_networks=True)
+emulate_network(roles, inventory, tc)
+validate_network(roles, inventory)
+provider.destroy()
diff --git a/docs/tutorials/grid5000.rst b/docs/tutorials/grid5000.rst
index d2d54d402c6bf354d4edc5d8e71accf133f96924..f9d7506d1ed3c06afeacb7b4d2e44884a8bbc56c 100644
--- a/docs/tutorials/grid5000.rst
+++ b/docs/tutorials/grid5000.rst
@@ -1,7 +1,7 @@
 Tutorial 2 - Working with Grid'5000
 ===================================
 
-This tutorial illustrate the use of EnOSlib to interact with Grid'5000. For a
+This tutorial illustrates the use of EnOSlib to interact with Grid'5000. For a
 full description of the API and the available options, please refer to the API
 documentation of the Grid'5000 provider.
 
@@ -21,8 +21,13 @@ On Grid'5000, you can go with a virtualenv :
 Basic example
 -------------
 
-The following ``tuto_grid5000.py`` implements a basic workflow where 2 nodes are
-reserved and 2 roles are described. Nodes are here put in a dedicated vlan.
+We'll implement a basic workflow where 2 nodes are reserved and 2 roles are
+described. Nodes are here put in a dedicated vlan.
+
+
+Build the configuration from a dictionnary
+******************************************
+
 
 .. literalinclude:: grid5000/tuto_grid5000.py
    :language: python
@@ -45,31 +50,65 @@ reserved and 2 roles are described. Nodes are here put in a dedicated vlan.
     Compared to the vagrant provider, Grid'5000 requires a more precise
     network description that should map what is available on the platform.
 
+Build the configuration programmatically
+****************************************
+
+The above script can be rewritten using the programmatc API.
+
+.. literalinclude:: grid5000/tuto_grid5000_p.py
+   :language: python
+   :linenos:
+
+.. note::
+
+   Here we first create a network and pass its reference to each group of
+   machine to configure the first interface of the Grid'5000 nodes.
+
 
 Subnet reservation
 ------------------
 
-The following ``tuto_grid5000_subnet.py`` shows how to deal with a subnet reservation.
+This shows how to deal with a subnet reservation
+
+Build the configuration from a dictionnary
+******************************************
 
 .. literalinclude:: grid5000/tuto_grid5000_subnet.py
     :language: python
     :linenos:
 
+Build the configuration programmatically
+****************************************
+
+.. literalinclude:: grid5000/tuto_grid5000_subnet_p.py
+    :language: python
+    :linenos:
 
 Non deploy reservation
 ----------------------
 
-The following ``tuto_grid5000_non_deploy.py`` shows how to deal with a non
-deploy reservation. Root ssh access will be granted to the nodes. For this
-purpose you must have a ``~/.ssh/id_rsa.pub`` file available. All the
-connections will be done as root user allowing to mimic the behaviour of deploy
-jobs (without the kadeploy3 step). This is particularly interesting if your
-deployment does't require more than one network interface.
+The following shows how to deal with a non deploy reservation. Root ssh access
+will be granted to the nodes. For this purpose you must have a
+``~/.ssh/id_rsa.pub`` file available. All the connections will be done as root
+user allowing to mimic the behaviour of deploy jobs (without the kadeploy3
+step). This is particularly interesting if your deployment does't require more
+than one network interface.
+
+Build the configuration from a dictionnary
+******************************************
 
 .. literalinclude:: grid5000/tuto_grid5000_non_deploy.py
     :language: python
     :linenos:
 
+Build the configuration programmatically
+****************************************
+
+.. literalinclude:: grid5000/tuto_grid5000_non_deploy_p.py
+    :language: python
+    :linenos:
+
+
 More complete example
 ---------------------
 
diff --git a/docs/tutorials/grid5000/hosts b/docs/tutorials/grid5000/hosts
index 077d3335714d4cdd73c42db400836886f0e80a61..1a08f3ed2ae2425f333f1e29a3bfe93296d04a2e 100644
--- a/docs/tutorials/grid5000/hosts
+++ b/docs/tutorials/grid5000/hosts
@@ -1,5 +1,5 @@
 [control]
-paravance-9-kavlan-4.rennes.grid5000.fr ansible_host=paravance-9-kavlan-4.rennes.grid5000.fr ansible_ssh_user=root ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' enos_devices="['eth0']" my_network=eth0
-parasilo-7-kavlan-4.rennes.grid5000.fr ansible_host=parasilo-7-kavlan-4.rennes.grid5000.fr ansible_ssh_user=root ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' enos_devices="['eth0']" my_network=eth0
+paravance-49-kavlan-8.rennes.grid5000.fr ansible_host=paravance-49-kavlan-8.rennes.grid5000.fr ansible_ssh_user=root ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' enos_devices="['eno1']" my_network=eno1
+paravance-51-kavlan-8.rennes.grid5000.fr ansible_host=paravance-51-kavlan-8.rennes.grid5000.fr ansible_ssh_user=root ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' enos_devices="['eno1']" my_network=eno1
 [compute]
-parasilo-7-kavlan-4.rennes.grid5000.fr ansible_host=parasilo-7-kavlan-4.rennes.grid5000.fr ansible_ssh_user=root ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' enos_devices="['eth0']" my_network=eth0%
+paravance-51-kavlan-8.rennes.grid5000.fr ansible_host=paravance-51-kavlan-8.rennes.grid5000.fr ansible_ssh_user=root ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' enos_devices="['eno1']" my_network=eno1
\ No newline at end of file
diff --git a/docs/tutorials/grid5000/tuto_grid5000.py b/docs/tutorials/grid5000/tuto_grid5000.py
index 04fb3fd983a4fe487d93fbddea4b22a3b79202c4..30f096ca946b2d268d371857504cd14e62cef41d 100644
--- a/docs/tutorials/grid5000/tuto_grid5000.py
+++ b/docs/tutorials/grid5000/tuto_grid5000.py
@@ -1,5 +1,6 @@
 from enoslib.api import generate_inventory, emulate_network, validate_network
 from enoslib.infra.enos_g5k.provider import G5k
+from enoslib.infra.enos_g5k.configuration import Configuration
 
 import logging
 import os
@@ -9,23 +10,28 @@ logging.basicConfig(level=logging.INFO)
 provider_conf = {
     "resources": {
         "machines": [{
-            "role": "control",
+            "roles": ["control"],
             "cluster": "paravance",
             "nodes": 1,
             "primary_network": "n1",
-            "secondary_networks": []
+            "secondary_networks": ["n2"]
         },{
 
             "roles": ["control", "compute"],
             "cluster": "parasilo",
             "nodes": 1,
             "primary_network": "n1",
-            "secondary_networks": []
+            "secondary_networks": ["n2"]
         }],
         "networks": [{
             "id": "n1",
             "type": "kavlan",
-            "role": "my_network",
+            "roles": ["my_network"],
+            "site": "rennes",
+         }, {
+            "id": "n2",
+            "type": "kavlan",
+            "roles": ["my_second_network"],
             "site": "rennes",
          }]
     }
@@ -35,11 +41,11 @@ provider_conf = {
 inventory = os.path.join(os.getcwd(), "hosts")
 
 # claim the resources
-provider = G5k(provider_conf)
+conf = Configuration.from_dictionnary(provider_conf)
+provider = G5k(conf)
 roles, networks = provider.init()
 
 # generate an inventory compatible with ansible
 generate_inventory(roles, networks, inventory, check_networks=True)
 
 # destroy the reservation
-provider.destroy()
diff --git a/docs/tutorials/grid5000/tuto_grid5000_non_deploy.py b/docs/tutorials/grid5000/tuto_grid5000_non_deploy.py
index 2127800c6807137025a9e820b7774e2877eeddaa..0a9aaa59fc0b0d097aadf9bffd50dc8ec393a3a5 100644
--- a/docs/tutorials/grid5000/tuto_grid5000_non_deploy.py
+++ b/docs/tutorials/grid5000/tuto_grid5000_non_deploy.py
@@ -1,5 +1,6 @@
 from enoslib.api import generate_inventory
 from enoslib.infra.enos_g5k.provider import G5k
+from enoslib.infra.enos_g5k.configuration import Configuration
 
 import logging
 import os
@@ -11,7 +12,7 @@ provider_conf = {
     "job_name": "test-non-deploy",
     "resources": {
         "machines": [{
-            "role": "control",
+            "roles": ["control"],
             "cluster": "parapluie",
             "nodes": 1,
             "primary_network": "n1",
@@ -27,7 +28,7 @@ provider_conf = {
         "networks": [{
             "id": "n1",
             "type": "prod",
-            "role": "my_network",
+            "roles": ["my_network"],
             "site": "rennes",
          }]
     }
@@ -37,7 +38,9 @@ provider_conf = {
 inventory = os.path.join(os.getcwd(), "hosts")
 
 # claim the resources
-provider = G5k(provider_conf)
+conf = Configuration.from_dictionnary(provider_conf)
+provider = G5k(conf)
+
 roles, networks = provider.init()
 
 # generate an inventory compatible with ansible
diff --git a/docs/tutorials/grid5000/tuto_grid5000_non_deploy_p.py b/docs/tutorials/grid5000/tuto_grid5000_non_deploy_p.py
new file mode 100644
index 0000000000000000000000000000000000000000..f4b77f523aeef9a0bea5b8b66bc0637c5282e700
--- /dev/null
+++ b/docs/tutorials/grid5000/tuto_grid5000_non_deploy_p.py
@@ -0,0 +1,39 @@
+from enoslib.api import generate_inventory
+from enoslib.infra.enos_g5k.provider import G5k
+from enoslib.infra.enos_g5k.configuration import (Configuration,
+                                                  NetworkConfiguration)
+
+import logging
+import os
+
+logging.basicConfig(level=logging.INFO)
+
+# path to the inventory
+inventory = os.path.join(os.getcwd(), "hosts")
+
+# claim the resources
+conf = Configuration.from_settings(job_type="allow_classic_ssh",
+                                   job_name="test-non-deploy")
+network = NetworkConfiguration(id="n1",
+                               type="prod",
+                               roles=["my_network"],
+                               site="rennes")
+conf.add_network_conf(network)\
+    .add_machine(roles=["control"],
+                 cluster="parapluie",
+                 nodes=1,
+                 primary_network=network)\
+    .add_machine(roles=["control", "network"],
+                 cluster="parapluie",
+                 nodes=1,
+                 primary_network=network)\
+    .finalize()
+provider = G5k(conf)
+
+roles, networks = provider.init()
+
+# generate an inventory compatible with ansible
+generate_inventory(roles, networks, inventory, check_networks=True)
+
+# destroy the reservation
+provider.destroy()
diff --git a/docs/tutorials/grid5000/tuto_grid5000_p.py b/docs/tutorials/grid5000/tuto_grid5000_p.py
new file mode 100644
index 0000000000000000000000000000000000000000..a1da7686eb0eb2d7b197dcacaeaeadcc5046bf37
--- /dev/null
+++ b/docs/tutorials/grid5000/tuto_grid5000_p.py
@@ -0,0 +1,38 @@
+from enoslib.api import generate_inventory, emulate_network, validate_network
+from enoslib.infra.enos_g5k.provider import G5k
+from enoslib.infra.enos_g5k.configuration import Configuration, NetworkConfiguration
+
+import logging
+import os
+
+logging.basicConfig(level=logging.INFO)
+
+# path to the inventory
+inventory = os.path.join(os.getcwd(), "hosts")
+
+# claim the resources
+network = NetworkConfiguration(id="n1",
+                               type="kavlan",
+                               roles=["my_network"],
+                               site="rennes")
+
+conf = Configuration.from_settings(job_name="test-enoslib")\
+    .add_network_conf(network)\
+    .add_machine(roles=["control"],
+                 cluster="paravance",
+                 nodes=1,
+                 primary_network=network)\
+    .add_machine(roles=["control", "compute"],
+                 cluster="paravance",
+                 nodes=1,
+                 primary_network=network)\
+    .finalize()
+
+provider = G5k(conf)
+roles, networks = provider.init()
+
+# generate an inventory compatible with ansible
+generate_inventory(roles, networks, inventory, check_networks=True)
+
+# destroy the reservation
+provider.destroy()
diff --git a/docs/tutorials/grid5000/tuto_grid5000_subnet.py b/docs/tutorials/grid5000/tuto_grid5000_subnet.py
index ea7fe434800c2abdae08227b7c0d058761a7d2d8..eb3ee66479beae575ae1e017e12a89b0beedec68 100644
--- a/docs/tutorials/grid5000/tuto_grid5000_subnet.py
+++ b/docs/tutorials/grid5000/tuto_grid5000_subnet.py
@@ -1,5 +1,6 @@
 from enoslib.api import generate_inventory, emulate_network, validate_network
 from enoslib.infra.enos_g5k.provider import G5k
+from enoslib.infra.enos_g5k.configuration import Configuration
 
 import logging
 import os
@@ -9,7 +10,7 @@ logging.basicConfig(level=logging.INFO)
 provider_conf = {
     "resources": {
         "machines": [{
-            "role": "control",
+            "roles": ["control"],
             "cluster": "parapluie",
             "nodes": 1,
             "primary_network": "n1",
@@ -19,13 +20,13 @@ provider_conf = {
           {
             "id": "n1",
             "type": "prod",
-            "role": "my_network",
+            "roles": ["my_network"],
             "site": "rennes"
           },
           {
             "id": "not_linked_to_any_machine",
             "type": "slash_22",
-            "role": "my_subnet",
+            "roles": ["my_subnet"],
             "site": "rennes",
          }]
     }
@@ -35,7 +36,8 @@ provider_conf = {
 inventory = os.path.join(os.getcwd(), "hosts")
 
 # claim the resources
-provider = G5k(provider_conf)
+conf = Configuratin.from_dictionnary(provider_conf)
+provider = G5k(conf)
 roles, networks = provider.init()
 
 # Retrieving subnet
diff --git a/docs/tutorials/grid5000/tuto_grid5000_subnet_p.py b/docs/tutorials/grid5000/tuto_grid5000_subnet_p.py
new file mode 100644
index 0000000000000000000000000000000000000000..7a5e0b5fa37a0e09c9e8ce332eb1cc25bd121476
--- /dev/null
+++ b/docs/tutorials/grid5000/tuto_grid5000_subnet_p.py
@@ -0,0 +1,53 @@
+from enoslib.api import generate_inventory
+from enoslib.infra.enos_g5k.provider import G5k
+from enoslib.infra.enos_g5k.configuration import (Configuration, 
+                                                  NetworkConfiguration)
+
+import logging
+import os
+
+logging.basicConfig(level=logging.INFO)
+
+# path to the inventory
+inventory = os.path.join(os.getcwd(), "hosts")
+
+# claim the resources
+conf = Configuration.from_settings(job_type="allow_classic_ssh")
+prod_network = NetworkConfiguration(id="n1",
+                                    type="prod",
+                                    roles=["my_network"],
+                                    site="rennes")
+conf.add_network_conf(prod_network)\
+    .add_network(id="not_linked_to_any_machine",
+                 type="slash_22",
+                 roles=["my_subnet"],
+                 site="rennes")\
+    .add_machine(roles=["control"],
+                 cluster="parapluie",
+                 nodes=1,
+                 primary_network=prod_network)\
+    .finalize()
+
+provider = G5k(conf)
+roles, networks = provider.init()
+
+# Retrieving subnet
+subnet = [n for n in networks if "my_subnet" in n["roles"]]
+logging.info(subnet)
+# This returns the subnet information
+# {
+#    'roles': ['my_subnet'],
+#    'start': '10.158.0.1',
+#    'dns': '131.254.203.235',
+#    'end': '10.158.3.254',
+#    'cidr': '10.158.0.0/22',
+#    'gateway': '10.159.255.254'
+#    'mac_end': '00:16:3E:9E:03:FE',
+#    'mac_start': '00:16:3E:9E:00:01',
+# } 
+
+# generate an inventory compatible with ansible
+generate_inventory(roles, networks, inventory, check_networks=True)
+
+# destroy the reservation
+provider.destroy()
diff --git a/docs/tutorials/grid5000/virt/tuto_grid5000_virt.py b/docs/tutorials/grid5000/virt/tuto_grid5000_virt.py
index cfce471ce7309906d530d7317c3a968519ffcea9..6f26728b1da832f3bd802bbb7ced0b7a2e18462b 100644
--- a/docs/tutorials/grid5000/virt/tuto_grid5000_virt.py
+++ b/docs/tutorials/grid5000/virt/tuto_grid5000_virt.py
@@ -1,5 +1,8 @@
 from enoslib.api import generate_inventory, run_ansible
 from enoslib.infra.enos_g5k.provider import G5k
+from enoslib.infra.enos_g5k.configuration import (Configuration,
+                                                  NetworkConfiguration)
+
 import logging
 from netaddr import EUI
 import os
@@ -19,39 +22,31 @@ def range_mac(mac_start, mac_end, step=1):
         ip = ['10'] + [str(int(i, 2)) for i in mac.bits().split('-')[-3:]]
         yield str(mac).replace('-', ':'), '.'.join(ip)
 
-provider_conf = {
-    "job_type": "allow_classic_ssh",
-    "job_name": "test-non-deploy",
-    "walltime": "1:00:00",
-    "resources": {
-        "machines": [{
-            "role": "compute",
-            "cluster": "parasilo",
-            "nodes": PMS,
-            "primary_network": "n1",
-            "secondary_networks": []
-        }],
-        "networks": [{
-            "id": "n1",
-            "type": "prod",
-            "role": "my_network",
-            "site": "rennes",
-         }, {
-            "id": "not_linked_to_any_machine",
-            "type": "slash_22",
-            "role": "my_subnet",
-            "site": "rennes",
-         }]
-    }
-}
+# claim the resources
+prod = NetworkConfiguration(id="n1",
+                            type="prod",
+                            roles=["my_network"],
+                            site="rennes")
+conf = Configuration.from_settings(job_type="allow_classic_ssh",
+                                   job_name="enoslib-virt",
+                                   walltime="01:00:00")\
+                    .add_network_conf(prod)\
+                    .add_network(id="_subnet_network",
+                                 type="slash_22",
+                                 roles=["my_subnet"],
+                                 site="rennes")\
+                    .add_machine(roles=["compute"],
+                                 cluster="parasilo",
+                                 nodes=PMS,
+                                 primary_network=prod)\
+                    .finalize()
+
+provider = G5k(conf)
+roles, networks = provider.init()
 
 # path to the inventory
 inventory = os.path.join(os.getcwd(), "hosts")
 
-# claim the resources
-provider = G5k(provider_conf)
-roles, networks = provider.init()
-
 # generate an inventory compatible with ansible
 generate_inventory(roles, networks, inventory, check_networks=True)
 
diff --git a/docs/tutorials/using-tasks/_tmp_enos_/enos-0.out b/docs/tutorials/using-tasks/_tmp_enos_/enos-0.out
index cd944071f9a5e9b270acd16044e3d5187f9b6bff..89cbfe172b1c29541e9dca4475dbbaff89db1ea4 100644
--- a/docs/tutorials/using-tasks/_tmp_enos_/enos-0.out
+++ b/docs/tutorials/using-tasks/_tmp_enos_/enos-0.out
@@ -1,5 +1,5 @@
-192.168.142.245 : 0.03 0.04 0.04 0.04 0.04
-192.168.142.244 : 41.50 41.55 41.63 42.50 41.43
+192.168.142.244 : 40.48 40.40 40.42 40.43 40.35 40.36 40.36 40.44 40.27 40.34
+192.168.142.245 : 0.02 0.02 0.03 0.02 0.03 0.03 0.03 0.02 0.03 0.02
 
        2 targets
        2 alive
@@ -7,12 +7,12 @@
        0 unknown addresses
 
        0 timeouts (waiting for response)
-      10 ICMP Echos sent
-      10 ICMP Echo Replies received
+      20 ICMP Echos sent
+      20 ICMP Echo Replies received
        0 other ICMP received
 
- 0.03 ms (min round trip time)
- 20.8 ms (avg round trip time)
- 42.5 ms (max round trip time)
-        4.075 sec (elapsed real time)
+ 0.02 ms (min round trip time)
+ 20.2 ms (avg round trip time)
+ 40.4 ms (max round trip time)
+        9.042 sec (elapsed real time)
 
diff --git a/docs/tutorials/using-tasks/hosts b/docs/tutorials/using-tasks/hosts
index bc084a6663c4ef89ed243c7d7649c8aa4d3fecfb..aabdad89ba43dda4dbfea6e9d21668a60759984f 100644
--- a/docs/tutorials/using-tasks/hosts
+++ b/docs/tutorials/using-tasks/hosts
@@ -1,4 +1,4 @@
 [control]
-enos-0 ansible_host=127.0.0.1 ansible_ssh_user=root ansible_port=2222 ansible_ssh_private_key_file=/Users/msimonin/sed/discovery/openstack/enoslib/docs/tutorials/using-tasks/.vagrant/machines/enos-0/virtualbox/private_key ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' n1=eth1 enos_devices="['eth1']"
+enos-0 ansible_host=192.168.121.70 ansible_ssh_user=root ansible_port=22 ansible_ssh_private_key_file=/home/msimonin/workspace/repos/enoslib/docs/tutorials/using-tasks/.vagrant/machines/enos-0/libvirt/private_key ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' n3=eth1 enos_devices="['eth1']"
 [compute]
-enos-1 ansible_host=127.0.0.1 ansible_ssh_user=root ansible_port=2201 ansible_ssh_private_key_file=/Users/msimonin/sed/discovery/openstack/enoslib/docs/tutorials/using-tasks/.vagrant/machines/enos-1/virtualbox/private_key ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' n1=eth1 enos_devices="['eth1']"
\ No newline at end of file
+enos-1 ansible_host=192.168.121.170 ansible_ssh_user=root ansible_port=22 ansible_ssh_private_key_file=/home/msimonin/workspace/repos/enoslib/docs/tutorials/using-tasks/.vagrant/machines/enos-1/libvirt/private_key ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' n3=eth1 enos_devices="['eth1']"
\ No newline at end of file
diff --git a/docs/tutorials/using-tasks/step1.py b/docs/tutorials/using-tasks/step1.py
index 1f304e3c8239388eb6c263da5def56604ff154cd..9009661bc16eedd391862cbd1e6b0f96aadb82e7 100644
--- a/docs/tutorials/using-tasks/step1.py
+++ b/docs/tutorials/using-tasks/step1.py
@@ -1,6 +1,7 @@
 from enoslib.api import generate_inventory, emulate_network,\
     validate_network, reset_network
 from enoslib.infra.enos_vagrant.provider import Enos_vagrant
+from enoslib.infra.enos_vagrant.configuration import Configuration
 
 import logging
 import os
@@ -10,12 +11,12 @@ logging.basicConfig(level=logging.INFO)
 provider_conf = {
     "resources": {
         "machines": [{
-            "role": "control",
+            "roles": ["control"],
             "flavor": "tiny",
             "number": 1,
             "networks": ["n1"]
         },{
-            "role": "compute",
+            "roles": ["compute"],
             "flavor": "tiny",
             "number": 1,
             "networks": ["n1"]
@@ -33,7 +34,9 @@ tc = {
 inventory = os.path.join(os.getcwd(), "hosts")
 
 # claim the resources
-provider = Enos_vagrant(provider_conf)
+conf = Configuration.from_dictionnary(provider_conf)
+
+provider = Enos_vagrant(conf)
 roles, networks = provider.init()
 generate_inventory(roles, networks, inventory, check_networks=True)
 
diff --git a/docs/tutorials/using-tasks/step2.py b/docs/tutorials/using-tasks/step2.py
index c2879f08f0f781ddec8d571876d7620df9ef9fcc..364a223a0fd88618d79ebc133db959e9b25fc690 100644
--- a/docs/tutorials/using-tasks/step2.py
+++ b/docs/tutorials/using-tasks/step2.py
@@ -1,21 +1,19 @@
 from enoslib.api import generate_inventory, emulate_network, validate_network
 from enoslib.task import enostask
 from enoslib.infra.enos_vagrant.provider import Enos_vagrant
+from enoslib.infra.enos_vagrant.configuration import Configuration
 
-import click
 import os
 
 provider_conf = {
-    "backend": "virtualbox",
-    "user": "root",
     "resources": {
         "machines": [{
-            "role": "control",
+            "roles": ["control"],
             "flavor": "tiny",
             "number": 1,
             "networks": ["n1"]
         },{
-            "role": "compute",
+            "roles": ["compute"],
             "flavor": "tiny",
             "number": 1,
             "networks": ["n1"]
@@ -34,7 +32,8 @@ tc = {
 def up(force=True, env=None, **kwargs):
     "Starts a new experiment"
     inventory = os.path.join(os.getcwd(), "hosts")
-    provider = Enos_vagrant(provider_conf)
+    conf = Configuration.from_dictionnary(provider_conf)
+    provider = Enos_vagrant(conf)
     roles, networks = provider.init()
     generate_inventory(roles, networks, inventory, check_networks=True)
     env["roles"] = roles
diff --git a/docs/tutorials/using-tasks/step3.py b/docs/tutorials/using-tasks/step3.py
index 64ea1bd502e87e84edeadf6203bf8db47ec8d843..c990e2786ed117c7881370162260c095b5d53169 100644
--- a/docs/tutorials/using-tasks/step3.py
+++ b/docs/tutorials/using-tasks/step3.py
@@ -1,23 +1,21 @@
 from enoslib.api import generate_inventory, emulate_network, validate_network
 from enoslib.task import enostask
 from enoslib.infra.enos_vagrant.provider import Enos_vagrant
+from enoslib.infra.enos_vagrant.configuration import Configuration
 
 import os
 import logging
 logging.basicConfig(level=logging.INFO)
 
 provider_conf = {
-    "backend": "virtualbox",
-    "user": "root",
-    "box": "debian/jessie64",
     "resources": {
         "machines": [{
-            "role": "control",
+            "roles": ["control"],
             "flavor": "tiny",
             "number": 1,
             "networks": ["n1", "n2"]
         },{
-            "role": "compute",
+            "roles": ["compute"],
             "flavor": "tiny",
             "number": 1,
             "networks": ["n1", "n3"]
@@ -44,7 +42,8 @@ def cli():
 def up(force, env=None, **kwargs):
     """Starts a new experiment using vagrant"""
     inventory = os.path.join(os.getcwd(), "hosts")
-    provider = Enos_vagrant(provider_conf)
+    conf = Configuration.from_dictionnary(provider_conf)
+    provider = Enos_vagrant(conf)
     roles, networks = provider.init(force_deploy=force)
     generate_inventory(roles, networks, inventory, check_networks=True)
     env["roles"] = roles
diff --git a/docs/tutorials/vagrant.rst b/docs/tutorials/vagrant.rst
index a0f339141e834378a3d65317cd64de2b4f54c938..e6817aa2d800dff476841b2256996175f1577204 100644
--- a/docs/tutorials/vagrant.rst
+++ b/docs/tutorials/vagrant.rst
@@ -20,6 +20,9 @@ Installation
 Using the API
 -------------
 
+From a dictionnary
+******************
+
 The following ``tuto_vagrant.py`` implements the desired workflow.
 
 .. literalinclude:: vagrant/tuto_vagrant.py
@@ -54,4 +57,12 @@ The following ``tuto_vagrant.py`` implements the desired workflow.
         Note the extra variables concerning the network. They can be use in
         your ansible playbooks to refer to a specific network.
 
+Programmatic way
+****************
+
+
+.. literalinclude:: vagrant/tuto_vagrant_p.py
+   :language: python
+   :linenos:
+
 .. _pyenv: https://github.com/pyenv/pyenv
diff --git a/docs/tutorials/vagrant/hosts b/docs/tutorials/vagrant/hosts
index eb75bd9919bf6e64db4fea52e1ab548940f89eed..fffe7a056672d59d7b787f2e57605bbf5394af68 100644
--- a/docs/tutorials/vagrant/hosts
+++ b/docs/tutorials/vagrant/hosts
@@ -1,5 +1,5 @@
-[control]
-enos-0 ansible_host=127.0.0.1 ansible_ssh_user=root ansible_port=2201 ansible_ssh_private_key_file=/Users/msimonin/sed/discovery/openstack/enoslib/docs/tutorials/vagrant/.vagrant/machines/enos-0/virtualbox/private_key ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' n1=eth1 enos_devices="['eth1']"
-enos-1 ansible_host=127.0.0.1 ansible_ssh_user=root ansible_port=2202 ansible_ssh_private_key_file=/Users/msimonin/sed/discovery/openstack/enoslib/docs/tutorials/vagrant/.vagrant/machines/enos-1/virtualbox/private_key ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' n1=eth1 enos_devices="['eth1']"
 [compute]
-enos-1 ansible_host=127.0.0.1 ansible_ssh_user=root ansible_port=2202 ansible_ssh_private_key_file=/Users/msimonin/sed/discovery/openstack/enoslib/docs/tutorials/vagrant/.vagrant/machines/enos-1/virtualbox/private_key ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' n1=eth1 enos_devices="['eth1']"
\ No newline at end of file
+enos-1 ansible_host=192.168.121.181 ansible_ssh_user=root ansible_port=22 ansible_ssh_private_key_file=/home/msimonin/workspace/repos/enoslib/docs/tutorials/vagrant/.vagrant/machines/enos-1/libvirt/private_key ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' mynetwork=eth1 enos_devices="['eth1']"
+[control]
+enos-0 ansible_host=192.168.121.234 ansible_ssh_user=root ansible_port=22 ansible_ssh_private_key_file=/home/msimonin/workspace/repos/enoslib/docs/tutorials/vagrant/.vagrant/machines/enos-0/libvirt/private_key ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' mynetwork=eth1 enos_devices="['eth1']"
+enos-1 ansible_host=192.168.121.181 ansible_ssh_user=root ansible_port=22 ansible_ssh_private_key_file=/home/msimonin/workspace/repos/enoslib/docs/tutorials/vagrant/.vagrant/machines/enos-1/libvirt/private_key ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' mynetwork=eth1 enos_devices="['eth1']"
\ No newline at end of file
diff --git a/docs/tutorials/vagrant/tuto_vagrant.py b/docs/tutorials/vagrant/tuto_vagrant.py
index 59cf70fde226a8f0d9bc78f601352e0acb6a2f99..14fef6b6e08921fdac5cb2306d43c6b8a8c0d88b 100644
--- a/docs/tutorials/vagrant/tuto_vagrant.py
+++ b/docs/tutorials/vagrant/tuto_vagrant.py
@@ -1,5 +1,6 @@
 from enoslib.api import generate_inventory, emulate_network, validate_network
 from enoslib.infra.enos_vagrant.provider import Enos_vagrant
+from enoslib.infra.enos_vagrant.configuration import Configuration
 
 import logging
 import os
@@ -7,20 +8,17 @@ import os
 logging.basicConfig(level=logging.INFO)
 
 provider_conf = {
-    "backend": "virtualbox",
-    "user": "root",
     "resources": {
         "machines": [{
-            "role": "control",
-            "flavor": "tiny",
+            "roles": ["control"],
+            "flavour": "tiny",
             "number": 1,
-            "networks": ["n1"]
         },{
             "roles": ["control", "compute"],
-            "flavor": "tiny",
+            "flavour": "tiny",
             "number": 1,
-            "networks": ["n1"]
-        }]
+        }],
+        "networks": [{"roles": ["r1"], "cidr": "172.16.42.0/16"}]
     }
 }
 
@@ -28,11 +26,12 @@ provider_conf = {
 inventory = os.path.join(os.getcwd(), "hosts")
 
 # claim the resources
-provider = Enos_vagrant(provider_conf)
+conf = Configuration.from_dictionnary(provider_conf)
+provider = Enos_vagrant(conf)
 roles, networks = provider.init()
 
 # generate an inventory compatible with ansible
 generate_inventory(roles, networks, inventory, check_networks=True)
 
 # destroy the boxes
-provider.destroy()
+# provider.destroy()
diff --git a/docs/tutorials/vagrant/tuto_vagrant_p.py b/docs/tutorials/vagrant/tuto_vagrant_p.py
new file mode 100644
index 0000000000000000000000000000000000000000..025e53325e7fee7ebaaef7703355cd6c5b9e35cb
--- /dev/null
+++ b/docs/tutorials/vagrant/tuto_vagrant_p.py
@@ -0,0 +1,33 @@
+from enoslib.api import generate_inventory
+from enoslib.infra.enos_vagrant.provider import Enos_vagrant
+from enoslib.infra.enos_vagrant.configuration import Configuration
+
+import logging
+import os
+
+logging.basicConfig(level=logging.INFO)
+
+conf = Configuration()\
+       .add_machine(roles=["control"],
+                    flavour="tiny",
+                    number=1)\
+       .add_machine(roles=["control", "compute"],
+                    flavour="tiny",
+                    number=1)\
+        .add_network(roles=["mynetwork"],
+                      cidr="192.168.42.0/24")\
+       .finalize()
+
+# claim the resources
+provider = Enos_vagrant(conf)
+roles, networks = provider.init()
+
+
+# path to the inventory
+inventory = os.path.join(os.getcwd(), "hosts")
+
+# generate an inventory compatible with ansible
+generate_inventory(roles, networks, inventory, check_networks=True)
+
+# destroy the boxes
+provider.destroy()
diff --git a/enoslib/api.py b/enoslib/api.py
index eb48102b59e570c75a417d4a895cc5df26837653..fdb5c5a544fbfaf979113a56be3dea7b33e56592 100644
--- a/enoslib/api.py
+++ b/enoslib/api.py
@@ -619,7 +619,7 @@ def expand_groups(grp):
         * grp[1-3] will be expanded to [grp1, grp2, grp3]
         * grp1 will be expanded to [grp1]
     """
-    p = re.compile("(?P<name>.+)\[(?P<start>\d+)-(?P<end>\d+)\]")
+    p = re.compile(r"(?P<name>.+)\[(?P<start>\d+)-(?P<end>\d+)\]")
     m = p.match(grp)
     if m is not None:
         s = int(m.group('start'))
@@ -770,9 +770,9 @@ def _generate_default_grp_constraints(roles, network_constraints):
             'rate': default_rate,
             'loss': default_loss
         } for grp1 in grps for grp2 in grps
-        if (grp1 != grp2 or
-            _src_equals_dst_in_constraints(network_constraints, grp1)) and
-            grp1 not in except_groups and grp2 not in except_groups]
+        if (grp1 != grp2
+            or _src_equals_dst_in_constraints(network_constraints, grp1))
+            and grp1 not in except_groups and grp2 not in except_groups]
 
 
 def _generate_actual_grp_constraints(network_constraints):
diff --git a/enoslib/infra/configuration.py b/enoslib/infra/configuration.py
new file mode 100644
index 0000000000000000000000000000000000000000..5e562b15ce33f3fbecd3b25904fa7910807d5d12
--- /dev/null
+++ b/enoslib/infra/configuration.py
@@ -0,0 +1,68 @@
+import jsonschema
+
+
+class BaseConfiguration:
+    """Base class for all the provider configuration object.
+
+    This should be used as it is.
+    """
+
+    # Setting this is defered to the inherited classes
+    _SCHEMA = None
+
+    def __init__(self):
+        # A configuration has a least these two
+        self.machines = []
+        self.networks = []
+
+        # Filling up with the right machine and network
+        # constructor is deferred to the sub classes.
+        self._machine_cls = str
+        self._network_cls = str
+
+    @classmethod
+    def from_dictionnary(cls, dictionnary, validate=True):
+        """Alternative constructor. Build the configuration from a
+        dictionnary."""
+        pass
+
+    @classmethod
+    def from_settings(cls, **kwargs):
+        """Alternative constructor. Build the configuration from a
+        the kwargs."""
+        self = cls()
+        self.set(**kwargs)
+        return self
+
+    @classmethod
+    def validate(cls, dictionnary):
+        jsonschema.validate(dictionnary, cls._SCHEMA)
+
+    def to_dict(self):
+        return {}
+
+    def finalize(self):
+        d = self.to_dict()
+        self.validate(d)
+        return self
+
+    def set(self, **kwargs):
+        for k, v in kwargs.items():
+            setattr(self, k, v)
+        return self
+
+    def add_machine_conf(self, machine):
+        self.machines.append(machine)
+        return self
+
+    def add_machine(self, *args, **kwargs):
+        self.machines.append(self._machine_cls(*args, **kwargs))
+        return self
+
+    def add_network_conf(self, network):
+        self.networks.append(network)
+        return self
+
+    def add_network(self, *args, **kwargs):
+        self.networks.append(self._network_cls(*args, **kwargs))
+        return self
diff --git a/enoslib/infra/enos_chameleonbaremetal/configuration.py b/enoslib/infra/enos_chameleonbaremetal/configuration.py
new file mode 100644
index 0000000000000000000000000000000000000000..fc059a29052c5a2a4f0ba4ce64b746e1ea98a6e4
--- /dev/null
+++ b/enoslib/infra/enos_chameleonbaremetal/configuration.py
@@ -0,0 +1,32 @@
+import enoslib.infra.enos_openstack.configuration as OSConfiguration
+
+from .constants import (DEFAULT_CONFIGURE_NETWORK, DEFAULT_IMAGE,
+                        DEFAULT_NAMESERVERS, DEFAULT_NETWORK, DEFAULT_SUBNET,
+                        DEFAULT_USER)
+from ..enos_chameleonkvm.schema import SCHEMA
+
+
+class Configuration(OSConfiguration.Configuration):
+
+    _SCHEMA = SCHEMA
+
+    def __init__(self):
+        super().__init__()
+        # new keys
+        self.lease_name = "enos-lease"
+        self.walltime = "02:00:00"
+        self.extra_ips = 0
+
+        # overrident keys
+        self.image = DEFAULT_IMAGE
+        self.user = DEFAULT_USER
+        self.configure_network = DEFAULT_CONFIGURE_NETWORK
+        self.network = DEFAULT_NETWORK
+        self.subnet = DEFAULT_SUBNET
+        self.dns_nameservers = DEFAULT_NAMESERVERS
+
+        self.gateway_user = self.user
+
+
+class MachineConfiguration(OSConfiguration.MachineConfiguration):
+    pass
diff --git a/enoslib/infra/enos_chameleonbaremetal/constants.py b/enoslib/infra/enos_chameleonbaremetal/constants.py
new file mode 100644
index 0000000000000000000000000000000000000000..b93157601e91a69d5579de2dffd6a54f1fc135fe
--- /dev/null
+++ b/enoslib/infra/enos_chameleonbaremetal/constants.py
@@ -0,0 +1,6 @@
+DEFAULT_IMAGE = "CC-Ubuntu16.04"
+DEFAULT_USER = "cc"
+DEFAULT_CONFIGURE_NETWORK = False
+DEFAULT_NETWORK = {"name": "sharednet1"}
+DEFAULT_SUBNET = {"name": "sharednet1-subnet"}
+DEFAULT_NAMESERVERS = ['130.202.101.6', '130.202.101.37']
diff --git a/enoslib/infra/enos_chameleonbaremetal/provider.py b/enoslib/infra/enos_chameleonbaremetal/provider.py
index 36f92b2da0ba7446a22763ead27348aac121e7c3..5b75b1c67e268efd7cb6504e29e471a6ac93317d 100644
--- a/enoslib/infra/enos_chameleonbaremetal/provider.py
+++ b/enoslib/infra/enos_chameleonbaremetal/provider.py
@@ -1,13 +1,13 @@
 # -*- coding: utf-8 -*-
 
-from blazarclient import client as blazar_client
-from neutronclient.neutron import client as neutron
-from itertools import groupby
-
-import enoslib.infra.enos_chameleonkvm.provider as cc
 import enoslib.infra.enos_openstack.provider as openstack
+import enoslib.infra.enos_chameleonkvm.provider as cc
+
+from blazarclient import client as blazar_client
 import datetime
+from itertools import groupby
 import logging
+from neutronclient.neutron import client as neutron
 import os
 import time
 
@@ -18,25 +18,27 @@ PORT_NAME = "enos-port"
 
 
 def lease_is_reusable(lease):
-    return lease['action'] == 'START' or lease['action'] == 'CREATE'
+    statuses = ["CREATING", "STARTING, UPDATING", "ACTIVE", "PENDING"]
+    return lease["status"] in statuses
 
 
 def lease_is_running(lease):
-    return lease['action'] == 'START' and lease['status'] == 'COMPLETE'
+    statuses = ["ACTIVE"]
+    return lease["status"] in statuses
 
 
 def lease_is_terminated(lease):
-    return lease['action'] == 'STOP'
+    statuses = ["TERMINATED"]
+    return lease["status"] in statuses
 
 
 def lease_to_s(lease):
-    return "[id=%s, name=%s, start=%s, end=%s, action=%s, status=%s]" % (
-        lease['id'],
-        lease['name'],
-        lease['start_date'],
-        lease['end_date'],
-        lease['action'],
-        lease['status'])
+    return "[id=%s, name=%s, start=%s, end=%s, status=%s]" % (
+        lease["id"],
+        lease["name"],
+        lease["start_date"],
+        lease["end_date"],
+        lease["status"])
 
 
 def create_blazar_client(config, session):
@@ -48,7 +50,7 @@ def create_blazar_client(config, session):
 
 def get_reservation(bclient, provider_conf):
     leases = bclient.lease.list()
-    leases = [l for l in leases if l["name"] == provider_conf['lease_name']]
+    leases = [l for l in leases if l["name"] == provider_conf.lease_name]
     if len(leases) >= 1:
         lease = leases[0]
         if lease_is_reusable(lease):
@@ -64,46 +66,45 @@ def get_reservation(bclient, provider_conf):
         return None
 
 
-def by_flavor(x):
-    return x['flavor']
+def by_flavor(machine):
+    return machine.flavour
 
 
 def create_reservation(bclient, provider_config):
     # NOTE(msimonin): This implies that
     #  * UTC is used
-    #  * we don't support yet in advance reservation
-    resources = provider_config['resources']
+    #  * we don"t support yet in advance reservation
     start_datetime = datetime.datetime.utcnow()
-    w = provider_config['walltime'].split(':')
+    w = provider_config.walltime.split(":")
     delta = datetime.timedelta(hours=int(w[0]),
                                minutes=int(w[1]),
                                seconds=int(w[2]))
-    # Make sure we're not reserving in the past by adding 1 minute
+    # Make sure we"re not reserving in the past by adding 1 minute
     # This should be rare
     start_datetime = start_datetime + datetime.timedelta(minutes=1)
     end_datetime = start_datetime + delta
-    start_date = start_datetime.strftime('%Y-%m-%d %H:%M')
-    end_date = end_datetime.strftime('%Y-%m-%d %H:%M')
+    start_date = start_datetime.strftime("%Y-%m-%d %H:%M")
+    end_date = end_datetime.strftime("%Y-%m-%d %H:%M")
     logger.info("[blazar]: Claiming a lease start_date=%s, end_date=%s",
                  start_date,
                  end_date)
 
     reservations = []
-    for flavor, descs in groupby(resources['machines'], key=by_flavor):
+    for flavor, machines in groupby(provider_config.machines, key=by_flavor):
         # NOTE(msimonin): We create one reservation per flavor
-        total = sum([desc['number'] for desc in descs])
+        total = sum([machine.number for machine in machines])
         resource_properties = "[\"=\", \"$node_type\", \"%s\"]" % flavor
 
         reservations.append({
-            'min': total,
-            'max': total,
-            'resource_properties': resource_properties,
-            'hypervisor_properties': '',
-            'resource_type': 'physical:host'
+            "min": total,
+            "max": total,
+            "resource_properties": resource_properties,
+            "hypervisor_properties": "",
+            "resource_type": "physical:host"
             })
 
     lease = bclient.lease.create(
-        provider_config['lease_name'],
+        provider_config.lease_name,
         start_date,
         end_date,
         reservations,
@@ -113,10 +114,10 @@ def create_reservation(bclient, provider_config):
 
 def wait_reservation(bclient, lease):
     logger.info("[blazar]: Waiting for %s to start" % lease_to_s(lease))
-    lease = bclient.lease.get(lease['id'])
+    lease = bclient.lease.get(lease["id"])
     while(not lease_is_running(lease)):
         time.sleep(10)
-        lease = bclient.lease.get(lease['id'])
+        lease = bclient.lease.get(lease["id"])
         logger.info("[blazar]: Waiting for %s to start" % lease_to_s(lease))
     return lease
 
@@ -133,25 +134,25 @@ def check_reservation(config, session):
 
 
 def check_extra_ports(session, network, total):
-    nclient = neutron.Client('2', session=session,
-                             region_name=os.environ['OS_REGION_NAME'])
-    ports = nclient.list_ports()['ports']
+    nclient = neutron.Client("2", session=session,
+                             region_name=os.environ["OS_REGION_NAME"])
+    ports = nclient.list_ports()["ports"]
     logger.debug("Found %s ports" % ports)
     port_name = PORT_NAME
-    ports_with_name = list(filter(lambda p: p['name'] == port_name, ports))
+    ports_with_name = list(filter(lambda p: p["name"] == port_name, ports))
     logger.info("[neutron]: Reusing %s ports" % len(ports_with_name))
     # create missing ports
     for _ in range(0, total - len(ports_with_name)):
-        port = {'admin_state_up': True,
-                'name': PORT_NAME,
-                'network_id': network['id']}
+        port = {"admin_state_up": True,
+                "name": PORT_NAME,
+                "network_id": network["id"]}
         # Checking port with PORT_NAME
-        nclient.create_port({'port': port})
-    ports = nclient.list_ports()['ports']
-    ports_with_name = list(filter(lambda p: p['name'] == port_name, ports))
+        nclient.create_port({"port": port})
+    ports = nclient.list_ports()["ports"]
+    ports_with_name = list(filter(lambda p: p["name"] == port_name, ports))
     ip_addresses = []
     for port in ports_with_name:
-        ip_addresses.append(port['fixed_ips'][0]['ip_address'])
+        ip_addresses.append(port["fixed_ips"][0]["ip_address"])
     logger.info("[neutron]: Returning %s free ip addresses" % ip_addresses)
     return ip_addresses
 
@@ -162,43 +163,46 @@ class Chameleonbaremetal(cc.Chameleonkvm):
 
         conf = self.provider_conf
         env = openstack.check_environment(conf)
-        lease = check_reservation(conf, env['session'])
-        extra_ips = check_extra_ports(env['session'],
-                                      env['network'],
-                                      conf['extra_ips'])
-        reservations = lease['reservations']
-        machines = self.provider_conf["resources"]["machines"]
+        lease = check_reservation(conf, env["session"])
+        extra_ips = check_extra_ports(env["session"],
+                                      env["network"],
+                                      conf.extra_ips)
+        reservations = lease["reservations"]
+        machines = self.provider_conf.machines
         machines = sorted(machines, key=by_flavor)
         servers = []
         for flavor, descs in groupby(machines, key=by_flavor):
-            machines = list(descs)
+            _machines = list(descs)
             # NOTE(msimonin): There should be only one reservation per flavor
-            hints = [{'reservation': r['id']} for r in reservations
-                             if flavor in r['resource_properties']]
+            hints = [{"reservation": r["id"]} for r in reservations
+                             if flavor in r["resource_properties"]]
+            # It's still a bit tricky here
             os_servers = openstack.check_servers(
-                env['session'],
-                {"machines": machines},
-                extra_prefix="{}__{}__".format(conf['prefix'], flavor),
+                env["session"],
+                _machines,
+                # NOTE(msimonin): we should be able to deduce the flavour from
+                # the name
+                extra_prefix="-o-{}-o-".format(flavor),
                 force_deploy=force_deploy,
-                key_name=conf.get('key_name'),
-                image_id=env['image_id'],
+                key_name=conf.key_name,
+                image_id=env["image_id"],
                 flavors="baremetal",
-                network=env['network'],
-                ext_net=env['ext_net'],
+                network=env["network"],
+                ext_net=env["ext_net"],
                 scheduler_hints=hints)
             servers.extend(os_servers)
 
         deployed, _ = openstack.wait_for_servers(
-            env['session'],
+            env["session"],
             servers)
 
         gateway_ip, _ = openstack.check_gateway(
             env,
-            conf.get('gateway', False),
+            conf.gateway,
             deployed)
 
         # NOTE(msimonin) build the roles and networks This is a bit tricky here
-        # since flavor (e.g compute_haswell) doesn't correspond to a flavor
+        # since flavor (e.g compute_haswell) doesn"t correspond to a flavor
         # attribute of the nova server object. We have to encode the flavor
         # name (e.g compute_haswell) in the server name. Decoding the flavor
         # name from the server name helps then to form the roles.
@@ -207,7 +211,7 @@ class Chameleonbaremetal(cc.Chameleonkvm):
             conf,
             gateway_ip,
             deployed,
-            lambda s: s.name.split('__')[1],
+            lambda s: s.name.split("-o-")[1],
             extra_ips=extra_ips)
 
     def destroy(self):
@@ -215,32 +219,8 @@ class Chameleonbaremetal(cc.Chameleonkvm):
         session = openstack.get_session()
         bclient = create_blazar_client(self.provider_conf, session)
         lease = get_reservation(bclient, self.provider_conf)
-        bclient.lease.delete(lease['id'])
+        if lease is None:
+            logger.info("No lease to destroy")
+            return
+        bclient.lease.delete(lease["id"])
         logger.info("Destroyed %s" % lease_to_s(lease))
-
-    def default_config(self):
-        default_config = super(Chameleonbaremetal, self).default_config()
-        default_config.update({
-            'type': 'chameleonbaremetal',
-            # Name os the lease to use
-            'lease_name': 'enos-lease',
-            # Glance image to use
-            'image_name': 'CC-Ubuntu16.04',
-            # User to use to connect to the machines
-            # (sudo will be used to configure them)
-            'user': 'cc',
-            # True iff Enos must configure a network stack for you
-            'configure_network': False,
-            # Name of the network to use or to create
-            'network': {'name': 'sharednet1'},
-            # Name of the subnet to use or to create
-            'subnet': {'name': 'sharednet1-subnet'},
-            # DNS server to use when creating network
-            'dns_nameservers': ['130.202.101.6', '130.202.101.37'],
-            # Experiment duration
-            "walltime": "02:00:00",
-            # extra ips to reserve
-            "extra_ips": 0
-        })
-
-        return default_config
diff --git a/enoslib/infra/enos_chameleonkvm/configuration.py b/enoslib/infra/enos_chameleonkvm/configuration.py
new file mode 100644
index 0000000000000000000000000000000000000000..13b12d40cf88e7e7d4a245da00599b3c976eba15
--- /dev/null
+++ b/enoslib/infra/enos_chameleonkvm/configuration.py
@@ -0,0 +1,19 @@
+import enoslib.infra.enos_openstack.configuration as OSConfiguration
+from ..enos_chameleonkvm.schema import SCHEMA
+from .constants import (DEFAULT_IMAGE, DEFAUT_NAMESERVERS, DEFAULT_USER)
+
+
+class Configuration(OSConfiguration.Configuration):
+
+    _SCHEMA = SCHEMA
+
+    def __init__(self):
+        super().__init__()
+        self.image = DEFAULT_IMAGE
+        self.dns_nameservers = DEFAUT_NAMESERVERS
+        self.user = DEFAULT_USER
+        self.gateway_user = self.user
+
+
+class MachineConfiguration(OSConfiguration.MachineConfiguration):
+    pass
diff --git a/enoslib/infra/enos_chameleonkvm/constants.py b/enoslib/infra/enos_chameleonkvm/constants.py
new file mode 100644
index 0000000000000000000000000000000000000000..44e4e33bb1e70ae7be60ad132634e2cf6ed57b87
--- /dev/null
+++ b/enoslib/infra/enos_chameleonkvm/constants.py
@@ -0,0 +1,5 @@
+DEFAULT_IMAGE = "CC-Ubuntu16.04"
+DEFAULT_USER = "cc"
+DEFAUT_NAMESERVERS = ["129.114.97.1",
+                      "129.114.97.2",
+                      "129.116.84.203"]
diff --git a/enoslib/infra/enos_chameleonkvm/provider.py b/enoslib/infra/enos_chameleonkvm/provider.py
index 7f167f1f594a63f692023ec12f494e1f41b6cdb3..3634d795b4048df512cf3772dceed54bcaf4e0d8 100644
--- a/enoslib/infra/enos_chameleonkvm/provider.py
+++ b/enoslib/infra/enos_chameleonkvm/provider.py
@@ -10,16 +10,3 @@ class Chameleonkvm(openstack.Openstack):
 
     def destroy(self):
         super(Chameleonkvm, self).destroy()
-
-    def default_config(self):
-        default_config = super(Chameleonkvm, self).default_config()
-        default_config.update({
-            'type': 'chameleonkvm',
-            'image': 'CC-Ubuntu16.04',
-            'user': 'cc',
-            'dns_nameservers': ['129.114.97.1',
-                                '129.114.97.2',
-                                '129.116.84.203'],
-        })
-
-        return default_config
diff --git a/enoslib/infra/enos_chameleonkvm/schema.py b/enoslib/infra/enos_chameleonkvm/schema.py
new file mode 100644
index 0000000000000000000000000000000000000000..2fb8f7c221e94e5942710dc6545b870104b27cc4
--- /dev/null
+++ b/enoslib/infra/enos_chameleonkvm/schema.py
@@ -0,0 +1,79 @@
+SCHEMA = {
+    "type": "object",
+    "properties": {
+        "resources": {"$ref": "#/resources"},
+        "key_name": {"type": "string"},
+        # everything is optionnal
+        "image": {"type": "string"},
+        "user": {"type": "string"},
+        "allocation_pool": {"$ref": "#/os_allocation_pool"},
+        "configure_network": {"type": "boolean"},
+        "dns_nameservers": {"type": "array", "items": {"type": "string"}},
+        "gateway": {"type": "boolean"},
+        "gateway_user": {"type": "string"},
+        "network": {"$ref": "#/os_network"},
+        "subnet": {"$ref": "#/os_subnet"},
+        "prefix": {"type": "string"}
+    },
+    "additionalProperties": False,
+    "required": ["resources", "key_name"],
+
+    "os_allocation_pool": {
+        "title": "OSallocationPool",
+        "type": "object",
+        "properties": {
+            "start": {"type": "string"},
+            "end": {"type": "string"}
+        },
+        "required": ["start", "end"],
+        "additionalProperties": False
+    },
+
+    "os_network": {
+        "title": "OSNetwork",
+        "type": "object",
+        "properties": {
+            "name": {"type": "string"}
+        },
+        "required": ["name"],
+        "additionalProperties": False
+    },
+
+    "os_subnet": {
+        "title": "OSSubnet",
+        "type": "object",
+        "properties": {
+            "name": {"type": "string"},
+            "cidr": {"type": "string"}
+        },
+        "required": ["name", "cidr"],
+        "additionalProperties": False
+    },
+
+    "resources": {
+        "title": "Resource",
+
+        "type": "object",
+        "properties": {
+            "machines": {"type": "array", "items": {"$ref": "#/machine"}},
+            "networks": {"type": "array", "items": {"type": "string"}}
+        },
+        "additionalProperties": False,
+        "required": ["machines", "networks"]
+    },
+
+    "machine": {
+        "title": "Compute",
+        "type": "object",
+        "properties": {
+            "roles": {"type": "array", "items": {"type": "string"}},
+            "flavour": {"type": "string"},
+            "number": {"type": "number"}
+        },
+        "required": [
+            "roles",
+            "number",
+            "flavour"
+        ]
+    }
+}
diff --git a/enoslib/infra/enos_g5k/api.py b/enoslib/infra/enos_g5k/api.py
index 181deb6224460cf86905236593e1744e1d2cbc72..49506286248b954ee39f4d68585a2ec893ff547e 100644
--- a/enoslib/infra/enos_g5k/api.py
+++ b/enoslib/infra/enos_g5k/api.py
@@ -8,7 +8,7 @@ from operator import itemgetter
 from enoslib.infra.enos_g5k import remote
 from enoslib.infra.enos_g5k import utils
 from enoslib.infra.enos_g5k.driver import get_driver
-from enoslib.infra.enos_g5k.constants import ENV_NAME, JOB_TYPE_DEPLOY
+from enoslib.infra.enos_g5k.constants import DEFAULT_ENV_NAME, JOB_TYPE_DEPLOY
 
 
 def get_clusters_sites(clusters):
@@ -137,7 +137,7 @@ class Resources:
             vlan_id = net["_c_network"]["vlan_id"]
             return [translate(node, vlan_id) for node in nodes]
 
-        env_name = self.configuration.get("env_name", ENV_NAME)
+        env_name = self.configuration.get("env_name", DEFAULT_ENV_NAME)
         force_deploy = self.configuration.get("force_deploy", False)
 
         machines = self.c_resources["machines"]
diff --git a/enoslib/infra/enos_g5k/configuration.py b/enoslib/infra/enos_g5k/configuration.py
new file mode 100644
index 0000000000000000000000000000000000000000..676c71cb93a32154d9cb8d26c2c6339dcc5d0081
--- /dev/null
+++ b/enoslib/infra/enos_g5k/configuration.py
@@ -0,0 +1,158 @@
+from ..configuration import BaseConfiguration
+from .constants import (DEFAULT_ENV_NAME, DEFAULT_JOB_NAME, DEFAULT_JOB_TYPE,
+                        DEFAULT_NUMBER, DEFAULT_QUEUE, DEFAULT_WALLTIME)
+from .schema import SCHEMA
+
+
+class Configuration(BaseConfiguration):
+
+    _SCHEMA = SCHEMA
+
+    def __init__(self):
+        super().__init__()
+        self.dhcp = True
+        self.force_deploy = False
+        self.env_name = DEFAULT_ENV_NAME
+        self.job_name = DEFAULT_JOB_NAME
+        self.job_type = DEFAULT_JOB_TYPE
+        self.oargrid_jobid = None
+        self.oar_jobid = None
+        self.oar_site = None
+        self.queue = DEFAULT_QUEUE
+        self.walltime = DEFAULT_WALLTIME
+
+        self._machine_cls = MachineConfiguration
+        self._network_cls = NetworkConfiguration
+
+    @classmethod
+    def from_dictionnary(cls, dictionnary, validate=True):
+        if validate:
+            cls.validate(dictionnary)
+
+        self = cls()
+
+        for k in self.__dict__.keys():
+            v = dictionnary.get(k)
+            if v is not None:
+                setattr(self, k, v)
+
+        _resources = dictionnary["resources"]
+        _machines = _resources["machines"]
+        _networks = _resources["networks"]
+        self.networks = [NetworkConfiguration.from_dictionnary(n) for n in
+                         _networks]
+        self.machines = [MachineConfiguration.from_dictionnary(m,
+                         self.networks) for m in _machines]
+
+        self.finalize()
+        return self
+
+    def to_dict(self):
+        d = {}
+        for k, v in self.__dict__.items():
+            if v is None or k in ["machines", "networks", "_machine_cls",
+                                  "_network_cls"]:
+                continue
+            d.update({k: v})
+
+        d.update(resources={
+            "machines": [m.to_dict() for m in self.machines],
+            "networks": [n.to_dict() for n in self.networks]
+        })
+        return d
+
+
+class MachineConfiguration:
+
+    def __init__(self, *,
+                 roles=None,
+                 cluster=None,
+                 primary_network=None,
+                 nodes=DEFAULT_NUMBER,
+                 secondary_networks=None
+    ):
+        # NOTE(msimonin): mandatory keys will be captured by the finalize
+        # function of the configuration.
+        self.roles = roles
+        self.cluster = cluster
+        self.primary_network = primary_network
+        self.nodes = nodes
+        self.secondary_networks = []
+        if secondary_networks is not None:
+            self.secondary_networks = secondary_networks
+
+    @classmethod
+    def from_dictionnary(cls, dictionnary, networks=None):
+        if networks is None or networks == []:
+            raise ValueError("At least one network must be set")
+
+        roles = dictionnary["roles"]
+        cluster = dictionnary["cluster"]
+        primary_network_id = dictionnary["primary_network"]
+
+        secondary_networks_ids = dictionnary.get("secondary_networks", [])
+
+        primary_network = [n for n in networks if n.id == primary_network_id]
+        if len(primary_network) < 1:
+            raise ValueError("Primary network with id={id} not found".format(
+                id=primary_network_id))
+
+        secondary_networks = [n for n in networks if n.id in
+                              secondary_networks_ids]
+        if len(secondary_networks_ids) != len(secondary_networks):
+            raise ValueError("Secondary network resolution fails")
+
+        kwargs = {}
+        nodes = dictionnary.get("nodes")
+        if nodes is not None:
+            kwargs.update(nodes=nodes)
+
+        return cls(roles=roles,
+                   cluster=cluster,
+                   primary_network=primary_network[0],
+                   secondary_networks=secondary_networks,
+                   **kwargs)
+
+    def to_dict(self):
+        d = {}
+        d.update(
+            roles=self.roles,
+            cluster=self.cluster,
+            nodes=self.nodes,
+            primary_network=self.primary_network.id,
+            secondary_networks=[n.id for n in self.secondary_networks]
+        )
+        return d
+
+
+class NetworkConfiguration:
+
+    def __init__(self, *,
+                 id=None,
+                 roles=None,
+                 type=None,
+                 site=None):
+        # NOTE(msimonin): mandatory keys will be captured by the finalize
+        # function of the configuration.
+        self.roles = roles
+        self.id = id
+        self.roles = roles
+        self.type = type
+        self.site = site
+
+    @classmethod
+    def from_dictionnary(cls, dictionnary):
+        id = dictionnary["id"]
+        type = dictionnary["type"]
+        roles = dictionnary["roles"]
+        site = dictionnary["site"]
+
+        return cls(id=id, roles=roles, type=type, site=site)
+
+    def to_dict(self):
+        d = {}
+        d.update(id=self.id,
+                 type=self.type,
+                 roles=self.roles,
+                 site=self.site)
+        return d
diff --git a/enoslib/infra/enos_g5k/constants.py b/enoslib/infra/enos_g5k/constants.py
index 43e8c9dd4cc9fa3050492a843d0422581fb741e8..01854b061e00411e8ba1807d91b428f8049daa6e 100644
--- a/enoslib/infra/enos_g5k/constants.py
+++ b/enoslib/infra/enos_g5k/constants.py
@@ -1,6 +1,9 @@
 # -*- coding: utf-8 -*-
 
-ENV_NAME = "debian9-x64-nfs"
-JOB_NAME = "EnOSlib"
-WALLTIME = "02:00:00"
 JOB_TYPE_DEPLOY = "deploy"
+DEFAULT_ENV_NAME = "debian9-x64-nfs"
+DEFAULT_JOB_NAME = "EnOSlib"
+DEFAULT_JOB_TYPE = JOB_TYPE_DEPLOY
+DEFAULT_QUEUE = "default"
+DEFAULT_WALLTIME = "02:00:00"
+DEFAULT_NUMBER = 1
diff --git a/enoslib/infra/enos_g5k/driver.py b/enoslib/infra/enos_g5k/driver.py
index c8f69f0d69a04be71874a286ab75a87ba4e49d31..3be1a40b7b9965d6ae9a5950af2ecd5ad562477a 100644
--- a/enoslib/infra/enos_g5k/driver.py
+++ b/enoslib/infra/enos_g5k/driver.py
@@ -6,7 +6,8 @@ from enoslib.infra.enos_g5k.utils import (grid_get_or_create_job,
                                           grid_destroy_from_id,
                                           oar_reload_from_id,
                                           oar_destroy_from_id)
-from enoslib.infra.enos_g5k.constants import (JOB_NAME, WALLTIME,
+from enoslib.infra.enos_g5k.constants import (DEFAULT_JOB_NAME,
+                                              DEFAULT_WALLTIME,
                                               JOB_TYPE_DEPLOY)
 import logging
 
@@ -29,8 +30,8 @@ def get_driver(configuration):
         logger.debug("Loading the OarStaticDriver")
         return OarStaticDriver(oar_jobid, oar_site)
     else:
-        job_name = configuration.get("job_name", JOB_NAME)
-        walltime = configuration.get("walltime", WALLTIME)
+        job_name = configuration.get("job_name", DEFAULT_JOB_NAME)
+        walltime = configuration.get("walltime", DEFAULT_WALLTIME)
         job_type = configuration.get("job_type", JOB_TYPE_DEPLOY)
         reservation_date = configuration.get("reservation", False)
         # NOTE(msimonin): some time ago asimonet proposes to auto-detect
diff --git a/enoslib/infra/enos_g5k/provider.py b/enoslib/infra/enos_g5k/provider.py
index d7dd2c52dd48dcf8b1e7476e5f063f91820b4264..c01224bfcef4a083423f92827baec6734d2e71e2 100644
--- a/enoslib/infra/enos_g5k/provider.py
+++ b/enoslib/infra/enos_g5k/provider.py
@@ -1,9 +1,7 @@
 # -*- coding: utf-8 -*-
 
 import enoslib.infra.enos_g5k.api as api
-from enoslib.infra.enos_g5k.constants import (JOB_NAME, WALLTIME, ENV_NAME,
-                                              JOB_TYPE_DEPLOY)
-from enoslib.infra.enos_g5k.schema import SCHEMA, KAVLAN_TYPE, SUBNET_TYPE
+from enoslib.infra.enos_g5k.schema import KAVLAN_TYPE, SUBNET_TYPE
 from enoslib.host import Host
 from enoslib.infra.provider import Provider
 from enoslib.utils import get_roles_as_list
@@ -12,15 +10,6 @@ from netaddr import IPAddress, IPNetwork, IPSet
 import logging
 logger = logging.getLogger(__name__)
 
-#: The default configuration of the Grid5000 provider
-DEFAULT_CONFIG = {
-    'job_name': JOB_NAME,
-    'walltime': WALLTIME,
-    'env_name': ENV_NAME,
-    'reservation': False,
-    'job_type': JOB_TYPE_DEPLOY
-}
-
 
 def _to_enos_roles(roles):
     """Transform the roles to use enoslib.host.Host hosts.
@@ -111,71 +100,7 @@ def _to_enos_networks(networks):
 
 
 class G5k(Provider):
-    """The provider to use when deploying on Grid'5000
-
-        Examples:
-
-        .. code-block:: yaml
-
-            # provider_conf in yaml
-            ---
-            job_name: enoslib
-            walltime: 01:00:00
-            # will give all configured interfaces an IP
-            dhcp: True
-            # force_deploy: True
-            resources:
-                machines:
-                - roles: [telegraf]
-                    cluster: griffon
-                    nodes: 1
-                    primary_network: n1
-                    secondary_networks: [n2]
-                - roles:
-                    - control
-                        registry
-                        prometheus
-                        grafana
-                        telegraf
-                    cluster: griffon
-                    nodes: 1
-                    min: 1
-                    primary_network: n1
-                    secondary_networks: [n2]
-                networks:
-                - id: n1
-                    roles: [control_network]
-                    type: prod
-                    site: nancy
-                - id: n2
-                    roles: [internal_network]
-                    type: kavlan-local
-                    site: nancy
-
-
-        Supported network types are
-
-            - kavlan
-            - kavlan-local
-            - kavlan-global
-            - prod
-            - slash_22 (subnet reservation)
-            - slash_18 (subnet reservation)
-
-    Machines must use at least one network of type prod or kavlan*. Subnets are
-    optional and must not be linked to any interfaces as they are a way to
-    claim extra ips and corresponding macs. In this case the returned network
-    attributes `start` and `end` corresponds to the first and last mapping of
-    (ip, mac).
-
-    If a key ``oargrid_jobid`` is found, the resources will be reloaded from
-    the corresponding oargrid job. In this case what is described under the
-    ``resources`` key mut be compatible with the job content.
-
-    If the keys ``oar_jobid`` and ``oar_site`` are found, the resources will be
-    reloaded from the corresponding oar job. In this case what is described
-    under the ``resources`` key mut be compatible with the job content.
-    """
+    """The provider to use when deploying on Grid'5000."""
 
     def init(self, force_deploy=False):
         """Reserve and deploys the nodes according to the resources section
@@ -191,10 +116,11 @@ class G5k(Provider):
             NotEnoughNodesError: If the `min` constraints can't be met.
 
            """
+        self.provider_conf.force_deploy = force_deploy
 
-        self.provider_conf.setdefault("force_deploy", force_deploy)
-        r = api.Resources(self.provider_conf)
-        # insert force_deploy
+        # TODO remove the use of dict
+        self._provider_conf = self.provider_conf.to_dict()
+        r = api.Resources(self._provider_conf)
         r.launch()
         roles = r.get_roles()
         networks = r.get_networks()
@@ -204,17 +130,9 @@ class G5k(Provider):
 
     def destroy(self):
         """Destroys the jobs."""
-        r = api.Resources(self.provider_conf)
+        r = api.Resources(self._provider_conf)
         # insert force_deploy
         r.destroy()
 
-    def default_config(self):
-        """Default config."""
-        return DEFAULT_CONFIG
-
-    def schema(self):
-        """Returns the schema of the provider config"""
-        return SCHEMA
-
     def __str__(self):
         return 'G5k'
diff --git a/enoslib/infra/enos_g5k/schema.py b/enoslib/infra/enos_g5k/schema.py
index 1aaae6f7579aeaed2cf4034225b4ab3858baf68f..869f8c6e12d08b85e3507a34e86fc2340da66a06 100644
--- a/enoslib/infra/enos_g5k/schema.py
+++ b/enoslib/infra/enos_g5k/schema.py
@@ -1,7 +1,5 @@
 # -*- coding: utf-8 -*-
 
-from jsonschema import validate
-
 KAVLAN = "kavlan"
 KAVLAN_LOCAL = "kavlan-local"
 KAVLAN_GLOBAL = "kavlan-global"
@@ -14,25 +12,39 @@ SUBNET_TYPE = [SLASH_18, SLASH_22]
 PROD = "prod"
 
 NETWORK_TYPES = [PROD] + KAVLAN_TYPE + SUBNET_TYPE
+JOB_TYPES = ["deploy", "allow_classic_ssh"]
+QUEUE_TYPES = ["default", "production", "testing"]
 
-# This is the schema for the abstract description of the resources
 SCHEMA = {
     "type": "object",
     "properties": {
+        "dhcp": {"type": "boolean"},
+        "force_deploy": {"type": "boolean"},
+        "env_name": {"type": "string"},
+        "job_name": {"type": "string"},
+        "job_type": {"type": "string", "enum": JOB_TYPES},
+        "oargrid_jobid": {"type": "string"},
+        "oar_jobid": {"type": "string"},
+        "queue": {"type": "string", "enum": QUEUE_TYPES},
+        "walltime": {"type": "string"},
         "resources": {"$ref": "#/resources"}
     },
-    "additionalProperties": True,
+    "additionalProperties": False,
     "required": ["resources"],
     "resources": {
         "title": "Resource",
 
         "type": "object",
         "properties": {
-            "machines": {"type": "array", "items": {"$ref": "#/machine"}},
+            "machines": {
+                "type": "array",
+                "items": {"$ref": "#/machine"}
+            },
             "networks": {
                 "type": "array",
                 "items": {"$ref": "#/network"},
-                "uniqueItems": True},
+                "uniqueItems": True
+            },
         },
         "additionalProperties": False,
         "required": ["machines", "networks"],
@@ -42,10 +54,7 @@ SCHEMA = {
         "title": "Compute",
         "type": "object",
         "properties": {
-            "anyOf": [
-                {"roles": {"type": "array", "items": {"type": "string"}}},
-                {"role": {"type": "string"}}
-            ],
+            "roles": {"type": "array", "items": {"type": "string"}},
             "cluster": {"type": "string"},
             "nodes": {"type": "number"},
             "min": {"type": "number"},
@@ -56,10 +65,9 @@ SCHEMA = {
                 "uniqueItems": True}
         },
         "required": [
-            "nodes",
+            "roles",
             "cluster",
-            "primary_network",
-            "secondary_networks"
+            "primary_network"
         ]
     },
     "network": {
@@ -67,37 +75,36 @@ SCHEMA = {
         "properties": {
             "id": {"type": "string"},
             "type": {"enum": NETWORK_TYPES},
-            "anyOf": [
-                {"roles": {"type": "array", "items": {"type": "string"}}},
-                {"role": {"type": "string"}}
-            ],
+            "roles": {"type": "array", "items": {"type": "string"}},
             "site": {"type": "string"}
         },
-        "required": ["id", "type", "site"]
+        "required": ["id", "type", "roles", "site"]
     }
 }
+"""
+Additionnal notes
 
+Supported network types are
 
-def validate_schema(provider_conf):
-    """Validate the schema of the configuration.
-
-    Args:
-        provider_conf (dict): The provider configuration
-    """
-    # First, validate the syntax
-    validate(provider_conf, SCHEMA)
-    # Second, validate the network names
-    # _validate_network_names(d)
-    # Third validate the network number on each nodes
+    - kavlan
+    - kavlan-local
+    - kavlan-global
+    - prod
+    - slash_22 (subnet reservation)
+    - slash_18 (subnet reservation)
 
+Machines must use at least one network of type prod or kavlan*. Subnets are
+optional and must not be linked to any interfaces as they are a way to
+claim extra ips and corresponding macs. In this case the returned network
+attributes `start` and `end` corresponds to the first and last mapping of
+(ip, mac).
 
-def _validate_network_names(resources):
-    # every network role used in the machine descriptions should fit with one
-    # in network
-    pass
+If a key ``oargrid_jobid`` is found, the resources will be reloaded from
+the corresponding oargrid job. In this case what is described under the
+``resources`` key mut be compatible with the job content.
 
+If the keys ``oar_jobid`` and ``oar_site`` are found, the resources will be
+reloaded from the corresponding oar job. In this case what is described
+under the ``resources`` key mut be compatible with the job content.
 
-def _validate_network_number(resources):
-    # The number of network wanted for each node should fit the number of NIC
-    # available on g5k
-    pass
+"""
diff --git a/enoslib/infra/enos_g5k/utils.py b/enoslib/infra/enos_g5k/utils.py
index 6fc946d51070ff755381009ad2ca4938a725b97a..b22f4fe2f2328da77a3e4bd331bf2a16c32e70b3 100644
--- a/enoslib/infra/enos_g5k/utils.py
+++ b/enoslib/infra/enos_g5k/utils.py
@@ -16,6 +16,43 @@ from enoslib.infra.utils import pick_things, mk_pools
 logger = logging.getLogger(__name__)
 
 
+# Note(msimonin): Monkey patching execo
+# This is a pending patch we need to support python3
+# Let's remove this when this will be merged on
+# execo side
+def __set_nodes_vlan(site, hosts, interface, vlan_id):
+    """Set the interface of a list of hosts in a given vlan
+
+    :param site: Site name
+
+    :param hosts: List of hosts
+
+    :param interface: The interface to put in the vlan
+
+    :param vlan_id: Id of the vlan to use
+    """
+
+    def _to_network_address(host):
+        """Translate a host to a network address
+
+        e.g:
+        paranoia-20.rennes.grid5000.fr -> paranoia-20-eth2.rennes.grid5000.fr
+        """
+        splitted = host.address.split('.')
+        splitted[0] = splitted[0] + "-" + interface
+        return ".".join(splitted)
+
+    network_addresses = [_to_network_address(h) for h in hosts]
+    logger.info("Setting %s in vlan %s of site %s" % (network_addresses,
+                                                      vlan_id, site))
+    return api._get_g5k_api().post('/sites/%s/vlans/%s' % (site, str(vlan_id)),
+                                   {"nodes": network_addresses})
+
+
+api.set_nodes_vlan = __set_nodes_vlan
+# End of execo monkey patching
+
+
 def dhcp_interfaces(c_resources):
     # TODO(msimonin) add a filter
     machines = c_resources["machines"]
@@ -188,9 +225,10 @@ def get_cluster_interfaces(cluster, extra_cond=lambda nic: True):
     # https://intranet.grid5000.fr/bugzilla/show_bug.cgi?id=9272
     # When its fixed we should be able to only use the new predictable name.
     nics = [(nic['device'], nic['name']) for nic in nics
-            if nic['mountable'] and
-            nic['interface'] == 'Ethernet' and
-            not nic['management'] and extra_cond(nic)]
+            if nic['mountable']
+            and nic['interface'] == 'Ethernet'
+            and not nic['management']
+            and extra_cond(nic)]
     nics = sorted(nics)
     return nics
 
diff --git a/enoslib/infra/enos_openstack/configuration.py b/enoslib/infra/enos_openstack/configuration.py
new file mode 100644
index 0000000000000000000000000000000000000000..5711d94fb198cf841df0dc4ac86cd3addb2b9af4
--- /dev/null
+++ b/enoslib/infra/enos_openstack/configuration.py
@@ -0,0 +1,84 @@
+from ..configuration import BaseConfiguration
+
+from .schema import SCHEMA
+from .constants import (DEFAULT_ALLOCATION_POOL, DEFAULT_CONFIGURE_NETWORK,
+                        DEFAULT_DNS_NAMESERVERS, DEFAULT_GATEWAY,
+                        DEFAULT_NETWORK, DEFAULT_PREFIX, DEFAULT_SUBNET)
+
+
+class Configuration(BaseConfiguration):
+
+    _SCHEMA = SCHEMA
+
+    def __init__(self):
+        super().__init__()
+        self.key_name = None
+        self.image = None
+        self.user = None
+
+        self.allocation_pool = DEFAULT_ALLOCATION_POOL
+        self.configure_network = DEFAULT_CONFIGURE_NETWORK
+        self.dns_nameservers = DEFAULT_DNS_NAMESERVERS
+        self.gateway = DEFAULT_GATEWAY
+        self.gateway_user = self.user
+        self.network = DEFAULT_NETWORK
+        self.subnet = DEFAULT_SUBNET
+        self.prefix = DEFAULT_PREFIX
+
+        self._machine_cls = MachineConfiguration
+        self._network_cls = str
+
+    @classmethod
+    def from_dictionnary(cls, dictionnary, validate=True):
+        if validate:
+            cls.validate(dictionnary)
+
+        self = cls()
+        for k in self.__dict__.keys():
+            v = dictionnary.get(k)
+            if v is not None:
+                setattr(self, k, v)
+        _machines = dictionnary["resources"]["machines"]
+        _networks = dictionnary["resources"]["networks"]
+        self.machines = [MachineConfiguration.from_dictionnary(m) for m in
+                         _machines]
+        self.networks = _networks
+
+        self.finalize()
+        return self
+
+    def to_dict(self):
+        d = {}
+        d.update(key_name=self.key_name,
+                 image=self.image,
+                 user=self.user,
+                 resources={
+                     "machines": [m.to_dict() for m in self.machines],
+                     "networks": self.networks
+                 })
+        return d
+
+
+class MachineConfiguration:
+
+    def __init__(self, *,
+                 roles=None,
+                 flavour=None,
+                 number=None):
+        self.roles = roles
+        self.flavour = flavour
+        self.number = number
+
+    @classmethod
+    def from_dictionnary(cls, dictionnary):
+        roles = dictionnary["roles"]
+        flavour = dictionnary["flavour"]
+        number = dictionnary["number"]
+        return cls(roles=roles, flavour=flavour, number=number)
+
+    def to_dict(self):
+        d = {}
+        d.update(roles=self.roles,
+                 flavour=self.flavour,
+                 number=self.number)
+        return d
diff --git a/enoslib/infra/enos_openstack/constants.py b/enoslib/infra/enos_openstack/constants.py
new file mode 100644
index 0000000000000000000000000000000000000000..356cc5f1198da39f0ece601051500867afb3bd10
--- /dev/null
+++ b/enoslib/infra/enos_openstack/constants.py
@@ -0,0 +1,23 @@
+# NOTE(msimonin): build the subnet following the good rules
+# https://www.chameleoncloud.org/docs/bare-metal-user-guide/network-isolation-bare-metal/
+# Some defaults
+SUBNET_CIDR = '10.87.23.0/24'
+
+GLANCE_VERSION = '2'
+NEUTRON_VERSION = '2'
+NOVA_VERSION = '2.1'
+
+DEFAULT_PREFIX = "enos"
+# These are private resources
+NETWORK_NAME = "%s-network" % DEFAULT_PREFIX
+ROUTER_NAME = "%s-router" % DEFAULT_PREFIX
+SECGROUP_NAME = "%s-secgroup" % DEFAULT_PREFIX
+SUBNET_NAME = "%s-subnet" % DEFAULT_PREFIX
+
+
+DEFAULT_CONFIGURE_NETWORK = True
+DEFAULT_NETWORK = {"name": NETWORK_NAME}
+DEFAULT_SUBNET = {"name": SUBNET_NAME, "cidr": SUBNET_CIDR}
+DEFAULT_DNS_NAMESERVERS = ["8.8.8.8", "8.8.4.4"]
+DEFAULT_ALLOCATION_POOL = {'start': '10.87.23.10', 'end': '10.87.23.100'}
+DEFAULT_GATEWAY = True
diff --git a/enoslib/infra/enos_openstack/provider.py b/enoslib/infra/enos_openstack/provider.py
index 5ed479816f28cd4775e04d790d4c5f3c0623e586..ca2adf9644b3ab62608862294b71ec147c4b6efa 100644
--- a/enoslib/infra/enos_openstack/provider.py
+++ b/enoslib/infra/enos_openstack/provider.py
@@ -1,9 +1,11 @@
 # -*- coding: utf-8 -*-
 
+from .constants import (NOVA_VERSION, GLANCE_VERSION, DEFAULT_PREFIX,
+                        SECGROUP_NAME, ROUTER_NAME)
 from enoslib.host import Host
 from enoslib.infra.utils import pick_things, mk_pools
 from enoslib.infra.provider import Provider
-from enoslib.utils import get_roles_as_list
+
 from glanceclient import client as glance
 from keystoneauth1.identity import v2, v3
 from keystoneauth1 import session
@@ -20,74 +22,8 @@ import time
 
 logger = logging.getLogger(__name__)
 
-PREFIX = 'enos'
-CONFIGURE_NETWORK = True
-NETWORK_NAME = "%s-network" % PREFIX
-SUBNET_NAME = "%s-subnet" % PREFIX
-
-# NOTE(msimonin): build the subnet following the good rules
-# https://www.chameleoncloud.org/docs/bare-metal-user-guide/network-isolation-bare-metal/
-# Some defaults
-SUBNET_CIDR = '10.87.23.0/24'
-ALLOCATION_POOL = {'start': '10.87.23.10', 'end': '10.87.23.100'}
-DNS_NAMESERVERS = ['8.8.8.8', '8.8.4.4']
-
-# These are private resources
-ROUTER_NAME = "%s-router" % PREFIX
-SECGROUP_NAME = "%s-secgroup" % PREFIX
-GLANCE_VERSION = '2'
-NEUTRON_VERSION = '2'
-NOVA_VERSION = '2.1'
-
 CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))
 
-SCHEMA = {
-    "type": "object",
-    "properties": {
-        "resources": {"$ref": "#/resources"},
-        # Mandatory keys
-        "key_name": {"type": "string"},
-        "image": {"type": "string"},
-        "user": {"type": "string"}
-    },
-    "additionalProperties": True,
-    "required": ["resources", "key_name", "image", "user"],
-
-    "resources": {
-        "title": "Resource",
-
-        "type": "object",
-        "properties": {
-            "machines": {"type": "array", "items": {"$ref": "#/machine"}},
-            "networks": {
-                "type": "array",
-                "items": {"type": "string"},
-                "minItems": 1,
-#                "maxItems": 1
-            }
-        },
-        "additionalProperties": False,
-        "required": ["machines", "networks"]
-    },
-
-    "machine": {
-        "title": "Compute",
-        "type": "object",
-        "properties": {
-            "anyOf": [
-                {"roles": {"type": "array", "items": {"type": "string"}}},
-                {"role": {"type": "string"}}
-            ],
-            "flavor": {"type": "string"},
-            "number": {"type": "number"}
-        },
-        "required": [
-            "number",
-            "flavor"
-        ]
-    }
-}
-
 
 def get_session():
     """Build the session object."""
@@ -134,7 +70,7 @@ def check_glance(session, image_name):
     return image_id
 
 
-def check_flavors(session, resources):
+def check_flavors(session):
     """Build the flavors mapping
 
     returns the mappings id <-> flavor
@@ -202,8 +138,8 @@ def check_network(session, configure_network, network, subnet,
 
     subnets = nclient.list_subnets()['subnets']
     subnet_name = subnet['name']
-    if (subnet_name not in list(map(itemgetter('name'), subnets)) and
-            configure_network):
+    if (subnet_name not in list(map(itemgetter('name'), subnets))
+        and configure_network):
         subnet = {'name': subnet['name'],
         'network_id': network['id'],
         # NOTE(msimonin): using the dns of chameleon
@@ -243,7 +179,7 @@ def check_network(session, configure_network, network, subnet,
         interface = {
             'subnet_id': subnet['id'].encode('UTF-8')
         }
-        nclient.add_interface_router(r['router']['id'], interface)
+        nclient.add_interface_router(str(r['router']['id']), interface)
 
     return (ext_net, network, subnet)
 
@@ -295,12 +231,12 @@ def wait_for_servers(session, servers):
     return deployed, undeployed
 
 
-def _get_total_wanted_machines(resources):
-    total = sum([machine["number"] for machine in resources["machines"]])
+def _get_total_wanted_machines(machines):
+    total = sum([machine.number for machine in machines])
     return total
 
 
-def check_servers(session, resources, extra_prefix="",
+def check_servers(session, machines, extra_prefix="",
         force_deploy=False, key_name=None, image_id=None,
         flavors='m1.medium', network=None, ext_net=None, scheduler_hints=None):
     """Checks the servers status for the deployment.
@@ -308,12 +244,13 @@ def check_servers(session, resources, extra_prefix="",
     If needed, it creates new servers and add a floating ip to one of them.
     This server can be used as a gateway to the others.
     """
+
     scheduler_hints = scheduler_hints or []
     nclient = nova.Client(NOVA_VERSION, session=session,
                           region_name=os.environ['OS_REGION_NAME'])
     servers = nclient.servers.list(
-        search_opts={'name': '-'.join([PREFIX, extra_prefix])})
-    wanted = _get_total_wanted_machines(resources)
+        search_opts={'name': '-'.join([DEFAULT_PREFIX, extra_prefix])})
+    wanted = _get_total_wanted_machines(machines)
     if force_deploy:
         for server in servers:
             server.delete()
@@ -327,14 +264,14 @@ def check_servers(session, resources, extra_prefix="",
 
     # starting the servers
     total = 0
-    for machine in resources["machines"]:
-        number = machine["number"]
-        roles = get_roles_as_list(machine)
+    for machine in machines:
+        number = machine.number
+        roles = machine.roles
         logger.info("[nova]: Starting %s servers" % number)
         logger.info("[nova]: for roles %s" % roles)
         logger.info("[nova]: with extra hints %s" % scheduler_hints)
         for _ in range(number):
-            flavor = machine["flavor"]
+            flavor = machine.flavour
             if isinstance(flavors, str):
                 flavor = flavors
             else:
@@ -348,7 +285,7 @@ def check_servers(session, resources, extra_prefix="",
                 _scheduler_hints = []
 
             server = nclient.servers.create(
-                name='-'.join([PREFIX, extra_prefix, str(total)]),
+                name='-'.join([DEFAULT_PREFIX, extra_prefix, str(total)]),
                 image=image_id,
                 flavor=flavor,
                 nics=[{'net-id': network['id']}],
@@ -385,7 +322,7 @@ def is_in_current_deployment(server, extra_prefix=""):
     """Check if an existing server in the system take part to
     the current deployment
     """
-    return re.match(r"^%s" % '-'.join([PREFIX, extra_prefix]),
+    return re.match(r"^%s" % '-'.join([DEFAULT_PREFIX, extra_prefix]),
             server.name) is not None
 
 
@@ -422,15 +359,15 @@ def allow_address_pairs(session, network, subnet):
 def check_environment(provider_conf):
     """Check all ressources needed by Enos."""
     session = get_session()
-    image_id = check_glance(session, provider_conf['image'])
-    flavor_to_id, id_to_flavor = check_flavors(session,
-                                               provider_conf['resources'])
-    ext_net, network, subnet = check_network(session,
-        provider_conf['configure_network'],
-        provider_conf['network'],
-        subnet=provider_conf['subnet'],
-        dns_nameservers=provider_conf['dns_nameservers'],
-        allocation_pool=provider_conf['allocation_pool'])
+    image_id = check_glance(session, provider_conf.image)
+    flavor_to_id, id_to_flavor = check_flavors(session)
+    ext_net, network, subnet = check_network(
+        session,
+        provider_conf.configure_network,
+        provider_conf.network,
+        subnet=provider_conf.subnet,
+        dns_nameservers=provider_conf.dns_nameservers,
+        allocation_pool=provider_conf.allocation_pool)
 
     return {
         'session': session,
@@ -444,14 +381,14 @@ def check_environment(provider_conf):
 
 
 def finalize(env, provider_conf, gateway_ip, servers, keyfnc, extra_ips=None):
-    def build_roles(resources, env, servers, keyfnc):
+    def build_roles(provider_conf, env, servers, keyfnc):
         result = {}
         pools = mk_pools(servers, keyfnc)
-        machines = resources["machines"]
+        machines = provider_conf.machines
         for desc in machines:
-            flavor = desc["flavor"]
-            nb = desc["number"]
-            roles = get_roles_as_list(desc)
+            flavor = desc.flavour
+            nb = desc.number
+            roles = desc.roles
             nodes = pick_things(pools, flavor, nb)
             for role in roles:
                 result.setdefault(role, []).extend(nodes)
@@ -460,16 +397,14 @@ def finalize(env, provider_conf, gateway_ip, servers, keyfnc, extra_ips=None):
 
     # Distribute the machines according to the resource/topology
     # specifications
-    os_roles = build_roles(provider_conf["resources"], env, servers, keyfnc)
+    os_roles = build_roles(provider_conf, env, servers, keyfnc)
 
     extra = {}
-    network_name = provider_conf['network']['name']
-    if provider_conf['gateway']:
-        user = provider_conf.get('user')
-        gw_user = provider_conf.get('gateway_user', user)
+    network_name = provider_conf.network['name']
+    if provider_conf.gateway:
         extra.update({
             'gateway': gateway_ip,
-            'gateway_user': gw_user,
+            'gateway_user': provider_conf.gateway_user,
             'forward_agent': True
             })
     extra.update({'ansible_become': 'yes'})
@@ -483,7 +418,7 @@ def finalize(env, provider_conf, gateway_ip, servers, keyfnc, extra_ips=None):
                 # NOTE(msimonin): the alias is used by ansible and thus
                 # must be an ascii hostname
                 alias=str(server.name),
-                user=provider_conf['user'],
+                user=provider_conf.user,
                 extra=extra))
 
     # build the network
@@ -496,10 +431,9 @@ def finalize(env, provider_conf, gateway_ip, servers, keyfnc, extra_ips=None):
         'extra_ips': extra_ips,
         'gateway': env['subnet']["gateway_ip"],
         'dns': '8.8.8.8',
-        'roles': provider_conf["resources"].get(
-            "networks", ["default_network"]
-        )
+        'roles': provider_conf.networks
     }
+
     return roles, [network]
 
 
@@ -507,13 +441,14 @@ class Openstack(Provider):
 
     def init(self, force_deploy=False):
         logger.info("Checking the existing environment")
+
         env = check_environment(self.provider_conf)
         servers = check_servers(
             env['session'],
-            self.provider_conf['resources'],
-            self.provider_conf['prefix'],
+            self.provider_conf.machines,
+            extra_prefix=self.provider_conf.prefix,
             force_deploy=force_deploy,
-            key_name=self.provider_conf.get('key_name'),
+            key_name=self.provider_conf.key_name,
             image_id=env['image_id'],
             flavors=(env['flavor_to_id'], env['id_to_flavor']),
             network=env['network'],
@@ -527,12 +462,12 @@ class Openstack(Provider):
 
         gateway_ip, _ = check_gateway(
             env,
-            self.provider_conf.get('gateway', False),
+            self.provider_conf.gateway,
             deployed)
 
         allow_address_pairs(env['session'],
             env['network'],
-            self.provider_conf['subnet']['cidr'])
+            self.provider_conf.subnet['cidr'])
 
         # NOTE(msimonin): polling is missing
         # we aren't sure that machines are ssh-reachable
@@ -543,20 +478,6 @@ class Openstack(Provider):
                         servers,
                         lambda s: env['id_to_flavor'][s.flavor['id']])
 
-    def default_config(self):
-        return {
-            'configure_network': CONFIGURE_NETWORK,
-            'network': {'name': NETWORK_NAME},
-            'subnet': {'name': SUBNET_NAME, 'cidr': SUBNET_CIDR},
-            'dns_nameservers': DNS_NAMESERVERS,
-            'allocation_pool': ALLOCATION_POOL,
-            'prefix': PREFIX,
-            'gateway': True,
-            }
-
-    def schema(self):
-        return SCHEMA
-
     def destroy(self):
         session = get_session()
         nclient = nova.Client(NOVA_VERSION, session=session,
diff --git a/enoslib/infra/enos_openstack/schema.py b/enoslib/infra/enos_openstack/schema.py
new file mode 100644
index 0000000000000000000000000000000000000000..a729b79c6429ce1610c858186cae1d8b1d5b62d5
--- /dev/null
+++ b/enoslib/infra/enos_openstack/schema.py
@@ -0,0 +1,80 @@
+SCHEMA = {
+    "type": "object",
+    "properties": {
+        "resources": {"$ref": "#/resources"},
+        # Mandatory keys
+        "key_name": {"type": "string"},
+        "image": {"type": "string"},
+        "user": {"type": "string"},
+        # optional keys
+        "allocation_pool": {"$ref": "#/os_allocation_pool"},
+        "configure_network": {"type": "boolean"},
+        "dns_nameservers": {"type": "array", "items": {"type": "string"}},
+        "gateway": {"type": "boolean"},
+        "gateway_user": {"type": "string"},
+        "network": {"$ref": "#/os_network"},
+        "subnet": {"$ref": "#/os_subnet"},
+        "prefix": {"type": "string"}
+    },
+    "additionalProperties": False,
+    "required": ["resources", "key_name", "image", "user"],
+
+    "os_allocation_pool": {
+        "title": "OSallocationPool",
+        "type": "object",
+        "properties": {
+            "start": {"type": "string"},
+            "end": {"type": "string"}
+        },
+        "required": ["start", "end"],
+        "additionalProperties": False
+    },
+
+    "os_network": {
+        "title": "OSNetwork",
+        "type": "object",
+        "properties": {
+            "name": {"type": "string"}
+        },
+        "required": ["name"],
+        "additionalProperties": False
+    },
+
+    "os_subnet": {
+        "title": "OSSubnet",
+        "type": "object",
+        "properties": {
+            "name": {"type": "string"},
+            "cidr": {"type": "string"}
+        },
+        "required": ["name", "cidr"],
+        "additionalProperties": False
+    },
+
+    "resources": {
+        "title": "Resource",
+
+        "type": "object",
+        "properties": {
+            "machines": {"type": "array", "items": {"$ref": "#/machine"}},
+            "networks": {"type": "array", "items": {"type": "string"}}
+        },
+        "additionalProperties": False,
+        "required": ["machines", "networks"]
+    },
+
+    "machine": {
+        "title": "Compute",
+        "type": "object",
+        "properties": {
+            "roles": {"type": "array", "items": {"type": "string"}},
+            "flavour": {"type": "string"},
+            "number": {"type": "number"}
+        },
+        "required": [
+            "roles",
+            "number",
+            "flavour"
+        ]
+    }
+}
diff --git a/enoslib/infra/enos_static/configuration.py b/enoslib/infra/enos_static/configuration.py
new file mode 100644
index 0000000000000000000000000000000000000000..8d51a850f4cac2b3ff3a3b486a38d3502712d745
--- /dev/null
+++ b/enoslib/infra/enos_static/configuration.py
@@ -0,0 +1,120 @@
+from ..configuration import BaseConfiguration
+from .schema import SCHEMA
+
+
+class Configuration(BaseConfiguration):
+
+    _SCHEMA = SCHEMA
+
+    def __init__(self):
+        super().__init__()
+        self._network_cls = NetworkConfiguration
+        self._machine_cls = MachineConfiguration
+
+    @classmethod
+    def from_dictionnary(cls, dictionnary, validate=True):
+        if validate:
+            cls.validate(dictionnary)
+        self = cls()
+        _resources = dictionnary["resources"]
+        _machines = _resources["machines"]
+        _networks = _resources["networks"]
+        self.machines = [MachineConfiguration.from_dictionnary(m) for m in
+                         _machines]
+        self.networks = [NetworkConfiguration.from_dictionnary(n) for n in
+                         _networks]
+
+        self.finalize()
+        return self
+
+    def to_dict(self):
+        return {
+            "resources": {
+                "machines": [m.to_dict() for m in self.machines],
+                "networks": [n.to_dict() for n in self.networks]
+            }
+        }
+
+
+class MachineConfiguration:
+
+    def __init__(self, *,
+                 address=None,
+                 roles=None,
+                 alias=None,
+                 user=None,
+                 keyfile=None,
+                 port=None,
+                 extra=None):
+        self.address = address
+        self.roles = roles
+        self.alias = alias
+        self.user = user
+        self.keyfile = keyfile
+        self.port = port
+        self.extra = extra if extra is not None else {}
+
+    @classmethod
+    def from_dictionnary(cls, dictionnary):
+
+        return cls(address=dictionnary["address"],
+                   roles=dictionnary["roles"],
+                   alias=dictionnary.get("alias"),
+                   user=dictionnary.get("user"),
+                   keyfile=dictionnary.get("keyfile"),
+                   port=dictionnary.get("port"),
+                   extra=dictionnary.get("extra")
+        )
+
+    def to_dict(self):
+        d = {}
+        d.update(address=self.address, roles=self.roles)
+        if self.alias:
+            d.update(alias=self.alias)
+        if self.user:
+            d.update(user=self.user)
+        if self.keyfile:
+            d.update(keyfile=self.keyfile)
+        if self.port:
+            d.update(port=self.port)
+        if self.extra:
+            d.update(extra=self.extra)
+
+        return d
+
+
+class NetworkConfiguration:
+
+    def __init__(self, *,
+                 roles=None,
+                 start=None,
+                 end=None,
+                 cidr=None,
+                 gateway=None,
+                 dns=None):
+        self.roles = roles
+        self.cidr = cidr
+        self.gateway = gateway
+        self.start = start
+        self.end = end
+        self.dns = dns
+
+    @classmethod
+    def from_dictionnary(cls, dictionnary):
+
+        return cls(roles=dictionnary["roles"],
+                   start=dictionnary["start"],
+                   end=dictionnary["end"],
+                   cidr=dictionnary["cidr"],
+                   gateway=dictionnary["gateway"],
+                   dns=dictionnary["dns"])
+
+    def to_dict(self):
+        d = {}
+        d.update(roles=self.roles,
+                 start=self.start,
+                 end=self.end,
+                 cidr=self.cidr,
+                 gateway=self.gateway,
+                 dns=self.dns)
+        return d
diff --git a/enoslib/infra/enos_static/provider.py b/enoslib/infra/enos_static/provider.py
index b3f4737db2f322e22d63cc69d6396331b2d9f6cb..1a826b96cb79b495a9d980a6f21683bacfa4121c 100644
--- a/enoslib/infra/enos_static/provider.py
+++ b/enoslib/infra/enos_static/provider.py
@@ -2,100 +2,31 @@
 
 from enoslib.host import Host
 from enoslib.infra.provider import Provider
-from enoslib.utils import get_roles_as_list
-
-SCHEMA = {
-    "type": "object",
-    "properties": {
-        "resources": {"$ref": "#/resources"},
-        # Mandatory keys
-        "key_name": {"type": "string"},
-        "image": {"type": "string"},
-        "user": {"type": "string"}
-    },
-    "additionalProperties": True,
-    "required": ["resources"],
-
-    "resources": {
-        "title": "Resource",
-
-        "type": "object",
-        "properties": {
-            "machines": {"type": "array", "items": {"$ref": "#/machine"}},
-            "networks": {
-                "type": "array",
-                "items": {"$ref": "#/network"},
-                "uniqueItems": True},
-            },
-        "additionalProperties": False,
-        "required": ["machines", "networks"]
-    },
-
-    "machine": {
-        "title": "Compute",
-        "type": "object",
-        "properties": {
-            "anyOf": [
-                {"roles": {"type": "array", "items": {"type": "string"}}},
-                {"role": {"type": "string"}}
-            ],
-            "address": {"type": "string"},
-            "alias": {"type": "string"},
-            "user": {"type":  "string"},
-            "keyfile": {"type": "string"},
-            "port": {"type": "number"},
-            "extra": {"type": "object"}
-            # This may contain the network mapping
-            # network role -> interfaces
-            # if auto-discovery network feature isn't used
-        },
-        "required": [
-            "address"
-        ]
-    },
-
-    "network": {
-        "type": "object",
-        "properties": {
-            "anyOf": [
-                {"roles": {"type": "array", "items": {"type": "string"}}},
-                {"role": {"type": "string"}}
-            ],
-            # TODO(msimonin): validate the ip schema
-            "start": {"type": "string"},
-            "end": {"type": "string"},
-            "cidr": {"type": "string"},
-            "gateway": {"type": "string"},
-            "dns": {"type": "string"}
-        },
-        "required": ["cidr"]
-    }
-}
 
 
 class Static(Provider):
+    """The Static provider class.
+
+    This class is used when one has already machines and network configured.
+    This is usefull when there si no provider class corresponding the user
+    testbed. To use it the user must build a configuration object that reflects
+    the exact settings of his machines and networks.
+    """
 
     def init(self, force_deploy=False):
-        resources = self.provider_conf["resources"]
-        machines = resources["machines"]
+        machines = self.provider_conf.machines
         roles = {}
         for machine in machines:
-            rs = get_roles_as_list(machine)
-            for r in rs:
+            for r in machine.roles:
                 roles.setdefault(r, []).append(
-                    Host(machine["address"],
-                         alias=machine.get("alias"),
-                         user=machine.get("user"),
-                         keyfile=machine.get("keyfile"),
-                         port=machine.get("port"),
-                         extra=machine.get("extra")))
-        return roles, resources["networks"]
+                    Host(machine.address,
+                         alias=machine.alias,
+                         user=machine.user,
+                         keyfile=machine.keyfile,
+                         port=machine.port,
+                         extra=machine.extra))
+        return roles, [n.to_dict() for n in
+                       self.provider_conf.networks]
 
     def destroy(self):
         pass
-
-    def default_config(self):
-        return {}
-
-    def schema(self):
-        return SCHEMA
diff --git a/enoslib/infra/enos_static/schema.py b/enoslib/infra/enos_static/schema.py
new file mode 100644
index 0000000000000000000000000000000000000000..5a775869c2f7a6612f4b88c144d790e8546de35a
--- /dev/null
+++ b/enoslib/infra/enos_static/schema.py
@@ -0,0 +1,55 @@
+SCHEMA = {
+    "type": "object",
+    "properties": {
+        "resources": {"$ref": "#/resources"},
+    },
+    "additionalProperties": True,
+    "required": ["resources"],
+
+    "resources": {
+        "title": "Resource",
+
+        "type": "object",
+        "properties": {
+            "machines": {"type": "array", "items": {"$ref": "#/machine"}},
+            "networks": {
+                "type": "array",
+                "items": {"$ref": "#/network"},
+                "uniqueItems": True},
+            },
+        "additionalProperties": False,
+        "required": ["machines", "networks"]
+    },
+
+    "machine": {
+        "title": "Compute",
+        "type": "object",
+        "properties": {
+            "roles": {"type": "array", "items": {"type": "string"}},
+            "address": {"type": "string"},
+            "alias": {"type": "string"},
+            "user": {"type":  "string"},
+            "keyfile": {"type": "string"},
+            "port": {"type": "number"},
+            "extra": {"type": "object"}
+            # This may contain the network mapping
+            # network role -> interfaces
+            # if auto-discovery network feature isn't used
+        },
+        "required": ["roles", "address"]
+    },
+
+    "network": {
+        "type": "object",
+        "properties": {
+            "roles": {"type": "array", "items": {"type": "string"}},
+            # TODO(msimonin): validate the ip schema
+            "start": {"type": "string"},
+            "end": {"type": "string"},
+            "cidr": {"type": "string"},
+            "gateway": {"type": "string"},
+            "dns": {"type": "string"}
+        },
+        "required": ["roles", "start", "end", "cidr", "gateway", "dns"]
+    }
+}
diff --git a/enoslib/infra/enos_vagrant/configuration.py b/enoslib/infra/enos_vagrant/configuration.py
new file mode 100644
index 0000000000000000000000000000000000000000..43cbb3a6d7aac9782a66fe369e4f46749c6c83e9
--- /dev/null
+++ b/enoslib/infra/enos_vagrant/configuration.py
@@ -0,0 +1,118 @@
+from ..configuration import BaseConfiguration
+from .constants import (DEFAULT_BACKEND, DEFAULT_BOX, DEFAULT_FLAVOUR,
+                        DEFAULT_USER, FLAVOURS)
+from .schema import SCHEMA
+
+
+class Configuration(BaseConfiguration):
+
+    _SCHEMA = SCHEMA
+
+    def __init__(self):
+        super().__init__()
+        # top level atttributes
+        self.resources = None
+        self.backend = DEFAULT_BACKEND
+        self.user = DEFAULT_USER
+        self.box = DEFAULT_BOX
+
+        self._machine_cls = MachineConfiguration
+        self._network_cls = NetworkConfiguration
+
+    @classmethod
+    def from_dictionnary(cls, dictionnary, validate=True):
+        if validate:
+            cls.validate(dictionnary)
+
+        self = cls()
+        _resources = dictionnary["resources"]
+        _machines = _resources["machines"]
+        _networks = _resources["networks"]
+        self.machines = [MachineConfiguration.from_dictionnary(m) for m in
+                         _machines]
+        self.networks = [NetworkConfiguration.from_dictionnary(n) for n in
+                         _networks]
+        for key in ["backend", "user", "box"]:
+            value = dictionnary.get(key)
+            if value is not None:
+                setattr(self, key, value)
+
+        self.finalize()
+        return self
+
+    def to_dict(self):
+        d = {}
+        d.update(backend=self.backend,
+                 user=self.user,
+                 box=self.box,
+                 resources={
+                     "machines": [m.to_dict() for m in self.machines],
+                     "networks": [n.to_dict() for n in self.networks]
+                 })
+        return d
+
+
+class MachineConfiguration:
+
+    def __init__(self, *,
+                 roles=None,
+                 flavour=None,
+                 number=1):
+
+        self.roles = roles
+
+        if flavour is None:
+            self.flavour = DEFAULT_FLAVOUR
+        if isinstance(flavour, dict):
+            self.flavour = flavour
+        elif isinstance(flavour, str):
+            self.flavour = FLAVOURS[flavour]
+        else:
+            self.flavour = DEFAULT_FLAVOUR
+
+        self.number = number
+
+    @classmethod
+    def from_dictionnary(cls, dictionnary):
+        kwargs = {}
+        roles = dictionnary["roles"]
+        kwargs.update(roles=roles)
+        flavour = dictionnary.get("flavour")
+        if flavour is not None:
+            # The flavour name is used in the dictionnary
+            # This makes a diff with the constructor where
+            # A dict describing the flavour is given
+            kwargs.update(flavour=FLAVOURS[flavour])
+        number = dictionnary.get("number")
+        if number is not None:
+            kwargs.update(number=number)
+
+        return cls(**kwargs)
+
+    def to_dict(self):
+        d = {}
+        d.update(roles=self.roles, flavour=self.flavour, number=self.number)
+        return d
+
+
+class NetworkConfiguration:
+
+    def __init__(self, *,
+                 roles=None,
+                 cidr=""):
+        self.roles = roles
+        self.cidr = cidr
+
+    @classmethod
+    def from_dictionnary(cls, dictionnary):
+        kwargs = {}
+        roles = dictionnary["roles"]
+        cidr = dictionnary["cidr"]
+        kwargs.update(roles=roles, cidr=cidr)
+
+        return cls(**kwargs)
+
+    def to_dict(self):
+        d = {}
+        d.update(roles=self.roles, cidr=self.cidr)
+        return d
diff --git a/enoslib/infra/enos_vagrant/constants.py b/enoslib/infra/enos_vagrant/constants.py
new file mode 100644
index 0000000000000000000000000000000000000000..c42cfc8b7bf2b71efa301760e3b9a6d25ad5d20d
--- /dev/null
+++ b/enoslib/infra/enos_vagrant/constants.py
@@ -0,0 +1,56 @@
+
+# Box
+DEFAULT_BOX = "generic/debian9"
+
+# Backends
+BACKEND_VIRTUALBOX = "virtualbox"
+BACKEND_LIBVIRT = "libvirt"
+BACKENDS = [BACKEND_LIBVIRT, BACKEND_VIRTUALBOX]
+
+DEFAULT_BACKEND = BACKEND_LIBVIRT
+
+# User
+DEFAULT_USER = "root"
+
+#: The default configuration of the vagrant provider
+DEFAULT_CONFIG = {
+    "backend": DEFAULT_BACKEND,
+    "box": DEFAULT_BOX,
+    "user": DEFAULT_USER,
+}
+
+#: Sizes of the machines available for the configuration
+FLAVOURS = {
+    "tiny": {
+        "cpu": 1,
+        "mem": 512
+    },
+    "small": {
+        "cpu": 1,
+        "mem": 1024
+    },
+    "medium": {
+        "cpu": 2,
+        "mem": 2048
+    },
+    "big": {
+        "cpu": 3,
+        "mem": 3072,
+    },
+    "large": {
+        "cpu": 4,
+        "mem": 4096
+    },
+    "extra-large": {
+        "cpu": 6,
+        "mem": 6144
+    }
+}
+
+DEFAULT_FLAVOUR = FLAVOURS["tiny"]
+
+
+DEFAULT_NETWORKS = ["enos_network"]
+
+
+DEFAULT_NUMBER = 1
diff --git a/enoslib/infra/enos_vagrant/provider.py b/enoslib/infra/enos_vagrant/provider.py
index 6edc0bbc0e9179994031e3e90164b1a0260d9153..bf73004d6cd8336af30cb08aa1fe7eb07b2e113a 100644
--- a/enoslib/infra/enos_vagrant/provider.py
+++ b/enoslib/infra/enos_vagrant/provider.py
@@ -1,5 +1,4 @@
 from enoslib.host import Host
-from enoslib.utils import get_roles_as_list
 from netaddr import IPNetwork
 from jinja2 import Environment, FileSystemLoader
 from enoslib.infra.provider import Provider
@@ -8,127 +7,15 @@ import logging
 import os
 import vagrant
 
-logger = logging.getLogger(__name__)
 
-#: The default configuration of the vagrant provider
-DEFAULT_CONFIG = {
-    "backend": "virtualbox",
-    "box": "bento/debian-9",
-    "user": "root",
-}
+logger = logging.getLogger(__name__)
 
-#: Sizes of the machines available for the configuration
-FLAVORS = {
-    "tiny": {
-        "cpu": 1,
-        "mem": 512
-    },
-    "small": {
-        "cpu": 1,
-        "mem": 1024
-    },
-    "medium": {
-        "cpu": 2,
-        "mem": 2048
-    },
-    "big": {
-        "cpu": 3,
-        "mem": 3072,
-    },
-    "large": {
-        "cpu": 4,
-        "mem": 4096
-    },
-    "extra-large": {
-        "cpu": 6,
-        "mem": 6144
-    }
-}
 
 TEMPLATE_DIR = os.path.dirname(os.path.realpath(__file__))
 
-# This is the schema for the abstract description of the resources
-SCHEMA = {
-    "type": "object",
-    "properties": {
-        "resources": {"$ref": "#/resources"}
-    },
-    "additionalProperties": True,
-    "required": ["resources"],
-
-    "resources": {
-        "title": "Resource",
-
-        "type": "object",
-        "properties": {
-            "machines": {"type": "array", "items": {"$ref": "#/machine"}},
-            "networks": {
-                "type": "array",
-                "items": {"$ref": "#/network"},
-                "uniqueItems": True},
-        },
-        "additionalProperties": False,
-        "required": ["machines"]
-    },
-
-    "machine": {
-        "title": "Compute",
-        "type": "object",
-        "properties": {
-            "anyOf": [
-                {"roles": {"type": "array", "items": {"type": "string"}}},
-                {"role": {"type": "string"}}
-            ],
-            "flavor": {"type": "string"},
-            "number": {"type": "number"},
-            "networks": {"type": "array", "items": {"type": "string"}}
-        },
-        "required": [
-            "number",
-            "flavor"
-        ]
-    }
-}
 
-
-# NOTE(msimonin) add provider config validation
-# for now :
-# provider_conf:
-#   backend:
-#   box:
-#   machines:
-#     - size: (enum)
-#       number:
-#       role:
-#       (or roles:[...])
-#       networks: [network_names]
 class Enos_vagrant(Provider):
     """The provider to use when working with vagrant (local machine).
-
-        Examples:
-
-            .. code-block:: yaml
-
-                # provider_conf in yaml
-                ---
-                backend: virtualbox
-                user: root
-                box: debian/jessie64
-                resources:
-                    machines:
-                    - roles: [telegraf]
-                        flavor: tiny
-                        number: 1
-                        networks: [control_network, internal_network]
-                    - roles:
-                        - control
-                        - registry
-                        - prometheus
-                        - grafana
-                        - telegraf
-                        flavor: medium
-                        number: 1
-                        networks: [control_network]
     """
 
     def init(self, force_deploy=False):
@@ -136,76 +23,33 @@ class Enos_vagrant(Provider):
 
         Args:
             force_deploy (bool): True iff new machines should be started
-
-        The above ``provider_conf`` will return a tuple (roles, networks)
-        where:
-
-            .. code-block:: yaml
-
-                roles:
-                  telegraf:
-                    - !!python/object:enoslib.host.Host
-                      address: 127.0.0.1
-                      alias: enos-1
-                      extra:
-                        enos_devices: [eth1, eth2]
-                        control_network: eth1
-                        internal_network: eth2
-                      keyfile: ...
-                      port: '2205'
-                      user: root
-                    - !!python/object:enoslib.host.Host
-                      address: 127.0.0.1
-                      alias: enos-0
-                      extra:
-                        enos_devices: [eth1, eth2]
-                        control_network: eth1
-                        internal_network: eth2
-                      keyfile: ...
-                      port: '2204'
-                      user: root
-                  control:
-                    # machine with role control
-
-               networks:
-                 - cidr: 192.168.142.0/24
-                   dns: 8.8.8.8
-                   end: 192.168.142.243
-                   gateway: 192.168.142.1
-                   roles: [control_network]
-                   start: 192.168.142.10
-                 - cidr: 192.168.143.0/24
-                   dns: 8.8.8.8
-                   end: 192.168.143.244
-                   gateway: 192.168.143.1
-                   roles: [internal_network]
-                   start: 192.168.143.10
         """
-        # Arbitrary net pool size
-        slash_24 = [142 + x for x in range(0, 100)]
-        slash_24 = [IPNetwork("192.168.%s.1/24" % x) for x in slash_24]
-        net_pool = [list(x)[10:-10] for x in slash_24]
+        machines = self.provider_conf.machines
+        networks = self.provider_conf.networks
+        _networks = []
+        for network in networks:
+            ipnet = IPNetwork(network.cidr)
+            _networks.append({
+                "netpool": list(ipnet)[10:-10],
+                "cidr": network.cidr,
+                "roles": network.roles,
+                "gateway": ipnet.ip
+            })
 
-        machines = self.provider_conf["resources"]["machines"]
-        # build the mapping network_name -> pool
-        networks = [machine.get("networks", []) for machine in machines]
-        # flatten and build the set of networks
-        networks = set([item for sublist in networks for item in sublist])
-        networks = dict(zip(networks, net_pool))
         vagrant_machines = []
         vagrant_roles = {}
         j = 0
         for machine in machines:
-            for _ in range(machine["number"]):
+            for _ in range(machine.number):
                 vagrant_machine = {
                     "name": "enos-%s" % j,
-                    "cpu": FLAVORS[machine["flavor"]]["cpu"],
-                    "mem": FLAVORS[machine["flavor"]]["mem"],
-                    "ips": [networks[n].pop() for n in machine["networks"]],
+                    "cpu": machine.flavour["cpu"],
+                    "mem": machine.flavour["mem"],
+                    "ips": [n["netpool"].pop() for n in _networks],
                 }
                 vagrant_machines.append(vagrant_machine)
                 # Assign the machines to the right roles
-                for role in get_roles_as_list(machine):
+                for role in machine.roles:
                     vagrant_roles.setdefault(role, []).append(vagrant_machine)
                 j = j + 1
 
@@ -223,7 +67,7 @@ class Enos_vagrant(Provider):
         # Build env for Vagrant with a copy of env variables (needed by
         # subprocess opened by vagrant
         v_env = dict(os.environ)
-        v_env['VAGRANT_DEFAULT_PROVIDER'] = self.provider_conf['backend']
+        v_env['VAGRANT_DEFAULT_PROVIDER'] = self.provider_conf.backend
 
         v = vagrant.Vagrant(root=os.getcwd(),
                             quiet_stdout=False,
@@ -242,22 +86,21 @@ class Enos_vagrant(Provider):
                 roles.setdefault(role, []).append(
                     Host(address,
                          alias=machine['name'],
-                         user=self.provider_conf['user'],
+                         user=self.provider_conf.user,
                          port=port,
                          keyfile=keyfile))
 
         networks = [{
-            'cidr': str(ipnet.cidr),
-            'start': str(pool[0]),
-            'end': str(pool[-1]),
+            'cidr': str(n["cidr"]),
+            'start': str(n["netpool"][0]),
+            'end': str(n["netpool"][-1]),
             'dns': '8.8.8.8',
-            'gateway': str(ipnet.ip),
-            'roles': [net]
-            } for ipnet, pool, net in zip(
-                slash_24,
-                net_pool[0: len(networks.keys())], networks.keys())]
+            'gateway': n["gateway"],
+            'roles': n["roles"]
+            } for n in _networks]
         logger.debug(roles)
         logger.debug(networks)
+
         return (roles, networks)
 
     def destroy(self):
@@ -266,13 +109,3 @@ class Enos_vagrant(Provider):
                             quiet_stdout=False,
                             quiet_stderr=True)
         v.destroy()
-
-    def default_config(self):
-        """
-        Default configuration
-        """
-        return DEFAULT_CONFIG
-
-    def schema(self):
-        """Returns the schema of the provider config."""
-        return SCHEMA
diff --git a/enoslib/infra/enos_vagrant/schema.py b/enoslib/infra/enos_vagrant/schema.py
new file mode 100644
index 0000000000000000000000000000000000000000..7f10de6ece8114f757bc422c82f4eef0b38f3034
--- /dev/null
+++ b/enoslib/infra/enos_vagrant/schema.py
@@ -0,0 +1,68 @@
+from ..schema import JSON_SCHEMA
+from .constants import FLAVOURS, BACKENDS
+
+
+SCHEMA = {
+    "type": "object",
+    "$schema": JSON_SCHEMA,
+    "properties": {
+        "backend": {"type": "string", "enum": BACKENDS},
+        "box": {"type": "string"},
+        "user": {"type": "string"},
+        "resources": {"$ref": "#/resources"}
+    },
+    "additionalProperties": False,
+    "required": ["resources"],
+
+    "resources": {
+        "title": "Resource",
+
+        "type": "object",
+        "properties": {
+            "machines": {"type": "array", "items": {"$ref": "#/machine"}},
+            "networks": {
+                "type": "array",
+                "items": {"$ref": "#/network"},
+                "uniqueItems": True},
+        },
+        "additionalProperties": False,
+        "required": ["machines", "networks"]
+    },
+
+    "machine": {
+        "title": "Compute",
+        "type": "object",
+        "properties": {
+            "roles": {"type": "array", "items": {"type": "string"}},
+            "oneOf": [
+                {"flavour": {"type": "string", "enum": list(FLAVOURS.keys())}},
+                {"flavour_desc": {"$ref": "#/flavour_desc"}}
+            ],
+            "number": {"type": "number"}
+        },
+        "required": [
+            "roles",
+
+        ]
+    },
+
+    "network": {
+        "type": "object",
+        "properties": {
+            "cidr": {"type": "string"},
+            "roles": {"type": "array", "items": {"type": "string"}},
+        },
+        "required": ["cidr", "roles"]
+    },
+
+    "flavour_desc": {
+        "title": "Flavour",
+        "type": "object",
+        "properties": {
+            "core": {"type": "number"},
+            "mem": {"type": "number"}
+        },
+        "required": ["core", "mem"],
+        "additionalProperties": False
+    }
+}
diff --git a/enoslib/infra/provider.py b/enoslib/infra/provider.py
index d073eba5ae2b3bf6cba24d036d944dbdcd9dd4d4..ab5c3ade8cf334836f13003fafc39fffff694c3e 100644
--- a/enoslib/infra/provider.py
+++ b/enoslib/infra/provider.py
@@ -1,6 +1,5 @@
 # -*- coding: utf-8 -*-
 from abc import ABCMeta, abstractmethod
-import jsonschema
 
 
 class Provider:
@@ -8,21 +7,14 @@ class Provider:
     __metaclass__ = ABCMeta
 
     def __init__(self, provider_conf):
-        """
-        The constructor takes care of loading the configuration and applying
-        the default parameters given by
-        :py:meth:`~enoslib.infra.provider.Provider.default_config`.
-        The resulting configuration is then validated using the
-        :py:meth:`~enoslib.infra.provider.validate` method.
+        """The constructor takes care of loading the configuration.
 
         Args:
-            provider_conf (dict): config of the provider. Specific to the
-                underlying provider.
+            provider_conf (BaseConfiguration): configuration of the provider.
+        The configuration object is specific to each provider and must follow
+        the provider's schema
         """
         self.provider_conf = provider_conf
-        self.provider_conf = self.default_config()
-        self.provider_conf.update(provider_conf)
-        self.validate()
 
     @abstractmethod
     def init(self, force_deploy=False):
@@ -46,24 +38,3 @@ class Provider:
     def destroy(self):
         "Abstract. Destroy the resources used for the deployment."
         pass
-
-    @abstractmethod
-    def default_config(self):
-        """Abstract. Default config for the provider config.
-
-        Returns a dict with all keys used to initialize the provider
-        (section `provider` of reservation.yaml file). Keys should be
-        provided with a default value.
-        """
-        pass
-
-    @abstractmethod
-    def schema(self):
-        """Abstract. Returns the schema of the provider config"""
-
-    def validate(self):
-        """Validates the configuration.
-
-        By default validate the jsonschema.
-        """
-        jsonschema.validate(self.provider_conf, self.schema())
diff --git a/enoslib/infra/schema.py b/enoslib/infra/schema.py
new file mode 100644
index 0000000000000000000000000000000000000000..e2de43d49fda9fa878b1b12d1e3778450f1429a4
--- /dev/null
+++ b/enoslib/infra/schema.py
@@ -0,0 +1 @@
+JSON_SCHEMA = "http://json-schema.org/draft-04/schema"
diff --git a/enoslib/tests/functionnal/test_openstack.py b/enoslib/tests/functionnal/test_openstack.py
new file mode 100644
index 0000000000000000000000000000000000000000..29cc835f453fa8d1b8942005ab838b75a529801f
--- /dev/null
+++ b/enoslib/tests/functionnal/test_openstack.py
@@ -0,0 +1,43 @@
+from enoslib.api import generate_inventory, emulate_network, validate_network, wait_ssh
+from enoslib.infra.enos_openstack.provider import Openstack
+from enoslib.infra.enos_openstack.configuration import Configuration
+
+import logging
+import os
+
+logging.basicConfig(level=logging.INFO)
+
+provider_conf = {
+    "key_name": "enos_matt",
+    "user": "cc",
+    "image":"CC-Ubuntu16.04",
+    "prefix": "plop",
+    "resources": {
+        "machines": [{
+            "roles": ["control"],
+            "flavour": "m1.medium",
+            "number": 1,
+        },{
+            "roles": ["compute"],
+            "flavour": "m1.medium",
+            "number": 5,
+        }],
+        "networks": ["network_interface"]
+    }
+}
+
+tc = {
+    "enable": True,
+    "default_delay": "20ms",
+    "default_rate": "1gbit",
+}
+inventory = os.path.join(os.getcwd(), "hosts")
+conf = Configuration.from_dictionnary(provider_conf)
+provider = Openstack(conf)
+provider.destroy()
+roles, networks = provider.init()
+generate_inventory(roles, networks, inventory)
+wait_ssh(inventory)
+generate_inventory(roles, networks, inventory, check_networks=True)
+emulate_network(roles, inventory, tc)
+validate_network(roles, inventory)
diff --git a/enoslib/tests/functionnal/test_static.py b/enoslib/tests/functionnal/test_static.py
new file mode 100644
index 0000000000000000000000000000000000000000..50eb14f89824921fb331a90bd23541cba8efbce5
--- /dev/null
+++ b/enoslib/tests/functionnal/test_static.py
@@ -0,0 +1,57 @@
+from enoslib.api import generate_inventory, emulate_network, validate_network
+from enoslib.infra.enos_vagrant.provider import Enos_vagrant
+from enoslib.infra.enos_vagrant.configuration import Configuration as VagrantConf
+from enoslib.infra.enos_static.provider import Static
+from enoslib.infra.enos_static.configuration import Configuration as StaticConf
+
+import os
+
+provider_conf = {
+    "resources": {
+        "machines": [{
+            "roles": ["control"],
+            "flavor": "tiny",
+            "number": 1,
+            "networks": ["n1", "n2"]
+        },{
+            "roles": ["compute"],
+            "flavor": "tiny",
+            "number": 1,
+            "networks": ["n1", "n3"]
+        }]
+    }
+}
+
+tc = {
+    "enable": True,
+    "default_delay": "20ms",
+    "default_rate": "1gbit",
+}
+inventory = os.path.join(os.getcwd(), "hosts")
+print("Starting ressources with the provider vagrant")
+provider = Enos_vagrant(VagrantConf.from_dictionnary(provider_conf))
+roles, networks = provider.init()
+import ipdb; ipdb.set_trace()
+print("Building the machine list")
+resources = {"machines": [], "networks": []}
+
+for role, machines in roles.items():
+    for machine in machines:
+        resources["machines"].append({
+            "address": machine.address,
+            "alias": machine.alias,
+            "user": machine.user,
+            "port": int(machine.port),
+            "keyfile": machine.keyfile,
+            "roles": [role]
+        })
+
+resources["networks"] = networks
+
+
+provider = Static(StaticConf.from_dictionnary({"resources": resources}))
+roles, _ = provider.init()
+generate_inventory(roles, networks, inventory, check_networks=True)
+emulate_network(roles, inventory, tc)
+validate_network(roles, inventory)
+
diff --git a/enoslib/tests/functionnal/test_static_run_ansible.py b/enoslib/tests/functionnal/test_static_run_ansible.py
index 8422f0f941ad3daeba522ea4eba3ee0586677304..84c697f9a792da35129f4e396d423bcc7291f2a3 100644
--- a/enoslib/tests/functionnal/test_static_run_ansible.py
+++ b/enoslib/tests/functionnal/test_static_run_ansible.py
@@ -1,5 +1,6 @@
 from enoslib.api import generate_inventory, run_command
 from enoslib.infra.enos_static.provider import Static
+from enoslib.infra.enos_static.configuration import Configuration
 
 import json
 import os
@@ -9,14 +10,14 @@ import os
 provider_conf = {
     "resources": {
         "machines": [{
-            "role": "control",
+            "roles": ["control"],
             "address": "localhost",
             "extra": {
                 "ansible_connection": "local"
             }
         }],
         "networks": [{
-            "role": "local",
+            "roles": ["local"],
             "start": "172.17.0.0",
             "end": "172.17.255.255",
             "cidr": "172.17.0.0/16",
@@ -27,7 +28,7 @@ provider_conf = {
 }
 
 inventory = os.path.join(os.getcwd(), "hosts")
-provider = Static(provider_conf)
+provider = Static(Configuration.from_dictionnary(provider_conf))
 roles, networks = provider.init()
 generate_inventory(roles, networks, inventory, check_networks=True)
 result = run_command("control", "date", inventory)
diff --git a/enoslib/tests/functionnal/test_static_run_command.py b/enoslib/tests/functionnal/test_static_run_command.py
index 8422f0f941ad3daeba522ea4eba3ee0586677304..5f057e74a422b388c4f49eb544a43388b899ea47 100644
--- a/enoslib/tests/functionnal/test_static_run_command.py
+++ b/enoslib/tests/functionnal/test_static_run_command.py
@@ -1,5 +1,6 @@
 from enoslib.api import generate_inventory, run_command
 from enoslib.infra.enos_static.provider import Static
+from enoslib.infra.enos_static.configuration import Configuration
 
 import json
 import os
@@ -9,14 +10,14 @@ import os
 provider_conf = {
     "resources": {
         "machines": [{
-            "role": "control",
+            "roles": ["control"],
             "address": "localhost",
             "extra": {
                 "ansible_connection": "local"
             }
         }],
         "networks": [{
-            "role": "local",
+            "roles": ["local"],
             "start": "172.17.0.0",
             "end": "172.17.255.255",
             "cidr": "172.17.0.0/16",
@@ -27,7 +28,8 @@ provider_conf = {
 }
 
 inventory = os.path.join(os.getcwd(), "hosts")
-provider = Static(provider_conf)
+conf = Configuration.from_dictionnary(provider_conf)
+provider = Static(conf)
 roles, networks = provider.init()
 generate_inventory(roles, networks, inventory, check_networks=True)
 result = run_command("control", "date", inventory)
diff --git a/enoslib/tests/unit/infra/__init__.py b/enoslib/tests/unit/infra/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e966b61791df0d0d0424f7beb20944e223bcb1fb
--- /dev/null
+++ b/enoslib/tests/unit/infra/__init__.py
@@ -0,0 +1 @@
+# __init__
diff --git a/enoslib/tests/unit/infra/enos_chameleonbaremetal/__init__.py b/enoslib/tests/unit/infra/enos_chameleonbaremetal/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..a6131c10e6a0636dfad21d2d40d5fe7db6cd9f9f
--- /dev/null
+++ b/enoslib/tests/unit/infra/enos_chameleonbaremetal/__init__.py
@@ -0,0 +1 @@
+# init
diff --git a/enoslib/tests/unit/infra/enos_chameleonbaremetal/test_configuration.py b/enoslib/tests/unit/infra/enos_chameleonbaremetal/test_configuration.py
new file mode 100644
index 0000000000000000000000000000000000000000..f38207a840f1310703b6989892023d1af97361c7
--- /dev/null
+++ b/enoslib/tests/unit/infra/enos_chameleonbaremetal/test_configuration.py
@@ -0,0 +1,44 @@
+from enoslib.infra.enos_chameleonbaremetal.configuration import Configuration, MachineConfiguration
+import enoslib.infra.enos_chameleonbaremetal.constants as constants
+
+from ... import EnosTest
+
+
+class TestConfiguration(EnosTest):
+
+    def test_from_dictionnary_minimal(self):
+        d = {
+            "key_name": "test-key",
+            "resources": {
+                "machines": [],
+                "networks": []
+            }
+        }
+        conf = Configuration.from_dictionnary(d)
+        self.assertEqual(constants.DEFAULT_IMAGE, conf.image)
+        self.assertEqual(constants.DEFAULT_USER, conf.user)
+        self.assertEqual(constants.DEFAULT_NAMESERVERS, conf.dns_nameservers)
+
+
+    def test_programmatic_conf(self):
+        conf = Configuration.from_settings(key_name="test-key")
+        conf.add_machine_conf(MachineConfiguration(roles=["test-role"],
+                                                   flavour="m1.tiny",
+                                                   number=1))
+        conf.add_network("api_network")
+        conf.finalize()
+
+        self.assertEqual(1, len(conf.machines))
+        self.assertEqual(1, len(conf.networks))
+
+    def test_programmatic(self):
+        conf = Configuration.from_settings(key_name="test-key")
+        conf.add_machine(roles=["test-role"],
+                         flavour="m1.tiny",
+                         number=1)\
+            .add_network("api_network")
+
+        conf.finalize()
+        self.assertEqual(1, len(conf.machines))
+        self.assertEqual(1, len(conf.networks))
+
diff --git a/enoslib/tests/unit/infra/enos_chameleonkvm/__init__.py b/enoslib/tests/unit/infra/enos_chameleonkvm/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e2cbd538230af689d968efd75629c6edee0c5673
--- /dev/null
+++ b/enoslib/tests/unit/infra/enos_chameleonkvm/__init__.py
@@ -0,0 +1 @@
+#init
diff --git a/enoslib/tests/unit/infra/enos_chameleonkvm/test_configuration.py b/enoslib/tests/unit/infra/enos_chameleonkvm/test_configuration.py
new file mode 100644
index 0000000000000000000000000000000000000000..c42e1cf9ef7170737c5dc3c9c397824d257e23b2
--- /dev/null
+++ b/enoslib/tests/unit/infra/enos_chameleonkvm/test_configuration.py
@@ -0,0 +1,51 @@
+from enoslib.infra.enos_chameleonkvm.configuration import Configuration, MachineConfiguration
+import enoslib.infra.enos_chameleonkvm.constants as constants
+
+from ... import EnosTest
+
+
+class TestConfiguration(EnosTest):
+
+    def test_from_dictionnary_minimal(self):
+        d = {
+            "key_name": "test-key",
+            "resources": {
+                "machines": [],
+                "networks": []
+            }
+        }
+        conf = Configuration.from_dictionnary(d)
+        self.assertEqual(constants.DEFAULT_IMAGE, conf.image)
+        self.assertEqual(constants.DEFAULT_USER, conf.user)
+        self.assertEqual(constants.DEFAUT_NAMESERVERS, conf.dns_nameservers)
+
+
+    def test_from_settings(self):
+        conf = Configuration.from_settings(key_name="test-key", image="image")
+        self.assertEqual("image", conf.image)
+        self.assertEqual(constants.DEFAULT_USER, conf.user)
+        self.assertEqual(constants.DEFAUT_NAMESERVERS, conf.dns_nameservers)
+
+
+    def test_programmatic_con(self):
+        conf = Configuration.from_settings(key_name="test-key")
+        conf.add_machine_conf(MachineConfiguration(roles=["test-role"],
+                                                   flavour="m1.tiny",
+                                                   number=1))
+        conf.add_network_conf("api_network")
+        conf.finalize()
+        self.assertEqual(1, len(conf.machines))
+        self.assertEqual(1, len(conf.networks))
+
+    def test_programmatic(self):
+        conf = Configuration.from_settings(key_name="test-key")
+        conf.add_machine(roles=["test-role"],
+                         flavour="m1.tiny",
+                         number=1)\
+            .add_network("api_network")
+
+        conf.finalize()
+        self.assertEqual(1, len(conf.machines))
+        self.assertEqual(1, len(conf.networks))
+
+
diff --git a/enoslib/tests/unit/infra/enos_g5k/test_api.py b/enoslib/tests/unit/infra/enos_g5k/test_api.py
index 3218eb95ee37f46b311ccf583e9351aca7da7517..9c32cc94e9f0d675e984438950252110020f00e6 100644
--- a/enoslib/tests/unit/infra/enos_g5k/test_api.py
+++ b/enoslib/tests/unit/infra/enos_g5k/test_api.py
@@ -1,6 +1,5 @@
-from enoslib.infra.enos_g5k.api import Resources, ENV_NAME
+from enoslib.infra.enos_g5k.api import Resources, DEFAULT_ENV_NAME
 from enoslib.infra.enos_g5k import utils
-from enoslib.infra.enos_g5k import schema
 from enoslib.infra.enos_g5k.schema import PROD, KAVLAN
 import mock
 
@@ -10,7 +9,6 @@ class TestGetNetwork(EnosTest):
 
     def test_no_concrete_network_yet(self):
         expected_networks = [{"type": PROD, "id": "network1"}]
-        schema.validate = mock.Mock()
         r = Resources({"resources": {"networks": expected_networks, "machines": []}})
         networks = r.get_networks()
         self.assertCountEqual(expected_networks, networks)
@@ -18,7 +16,6 @@ class TestGetNetwork(EnosTest):
     def test_concrete_network(self):
         networks = [{"type": KAVLAN, "id": "network1", "_c_network": {"site": "nancy", "vlan_id": 1}}]
         expected_networks = [{"type": KAVLAN, "id": "network1", "site": "nancy", "vlan_id": 1}]
-        schema.validate = mock.Mock()
         r = Resources({"resources": {"networks": networks, "machines": []}})
         networks = r.get_networks()
         self.assertCountEqual(expected_networks, networks)
@@ -28,7 +25,6 @@ class TestDeploy(EnosTest):
 
     def test_prod(self):
         nodes = ["foocluster-1", "foocluster-2"]
-        schema.validate = mock.Mock()
         r = Resources({
             "resources":{
                 "machines": [{
@@ -42,13 +38,12 @@ class TestDeploy(EnosTest):
         undeployed = set()
         utils._deploy = mock.Mock(return_value=(deployed, undeployed))
         r.deploy()
-        utils._deploy.assert_called_with(nodes, False, {"env_name": ENV_NAME})
+        utils._deploy.assert_called_with(nodes, False, {"env_name": DEFAULT_ENV_NAME})
         self.assertCountEqual(deployed, r.c_resources["machines"][0]["_c_deployed"])
         self.assertCountEqual(undeployed, r.c_resources["machines"][0]["_c_undeployed"])
 
     def test_vlan(self):
         nodes = ["foocluster-1", "foocluster-2"]
-        schema.validate = mock.Mock()
         r = Resources({
             "resources": {
                 "machines": [{
@@ -62,14 +57,13 @@ class TestDeploy(EnosTest):
         undeployed = set()
         utils._deploy = mock.Mock(return_value=(deployed, undeployed))
         r.deploy()
-        utils._deploy.assert_called_with(nodes, False, {"env_name": ENV_NAME, "vlan": 4})
+        utils._deploy.assert_called_with(nodes, False, {"env_name": DEFAULT_ENV_NAME, "vlan": 4})
         self.assertCountEqual(deployed, r.c_resources["machines"][0]["_c_deployed"])
         self.assertCountEqual(undeployed, r.c_resources["machines"][0]["_c_undeployed"])
 
     def test_2_deployements_with_undeployed(self):
         nodes_foo = ["foocluster-1", "foocluster-2"]
         nodes_bar = ["barcluster-1", "barcluster-2"]
-        schema.validate = mock.Mock()
         r = Resources({
             "resources": {
                 "machines": [{
@@ -91,7 +85,7 @@ class TestDeploy(EnosTest):
         u_bar = set(nodes_bar) - d_bar
         utils._deploy = mock.Mock(side_effect=[(d_foo, u_foo), (d_bar, u_bar)])
         r.deploy()
-        self.assertEquals(2, utils._deploy.call_count)
+        self.assertEqual(2, utils._deploy.call_count)
         self.assertCountEqual(d_foo, r.c_resources["machines"][0]["_c_deployed"])
         self.assertCountEqual(u_foo, r.c_resources["machines"][0]["_c_undeployed"])
         self.assertCountEqual(d_bar, r.c_resources["machines"][1]["_c_deployed"])
diff --git a/enoslib/tests/unit/infra/enos_g5k/test_configuration.py b/enoslib/tests/unit/infra/enos_g5k/test_configuration.py
new file mode 100644
index 0000000000000000000000000000000000000000..b7a56e14728b3a2f6a693decde36a5fd90dcfb03
--- /dev/null
+++ b/enoslib/tests/unit/infra/enos_g5k/test_configuration.py
@@ -0,0 +1,238 @@
+from enoslib.infra.enos_g5k.configuration import Configuration, NetworkConfiguration, MachineConfiguration
+import enoslib.infra.enos_g5k.constants as constants
+
+from ... import EnosTest
+
+class TestConfiguration(EnosTest):
+
+    def test_from_dictionnary_minimal(self):
+        d = {
+            "resources": {
+                "machines": [],
+                "networks": []
+            }
+        }
+        conf = Configuration.from_dictionnary(d)
+        self.assertEqual(constants.DEFAULT_ENV_NAME, conf.env_name)
+        self.assertEqual(constants.DEFAULT_JOB_NAME, conf.job_name)
+        self.assertEqual(constants.DEFAULT_JOB_TYPE, conf.job_type)
+        self.assertEqual(constants.DEFAULT_QUEUE, conf.queue)
+        self.assertEqual(constants.DEFAULT_WALLTIME, conf.walltime)
+        self.assertEqual([], conf.machines)
+        self.assertEqual([], conf.networks)
+
+
+    def test_from_dictionnary_some_metadatas(self):
+        d = {
+            "job_name": "test",
+            "queue": "production",
+            "resources": {
+                "machines": [],
+                "networks": []
+            }
+        }
+        conf = Configuration.from_dictionnary(d)
+        self.assertEqual(constants.DEFAULT_ENV_NAME, conf.env_name)
+        self.assertEqual("test", conf.job_name)
+        self.assertEqual(constants.DEFAULT_JOB_TYPE, conf.job_type)
+        self.assertEqual("production", conf.queue)
+        self.assertEqual(constants.DEFAULT_WALLTIME, conf.walltime)
+
+    def test_from_dictionnary_with_machines(self):
+        d = {
+            "resources": {
+                "machines": [{
+                    "roles": ["r1"],
+                    "nodes": 2,
+                    "cluster": "cluste1",
+                    "primary_network": "n1"
+                }],
+                "networks": [{
+                    "id": "n1",
+                    "roles": ["rn1"],
+                    "site": "siteA",
+                    "type": "prod"
+                }]
+            }
+        }
+
+        conf = Configuration.from_dictionnary(d)
+        self.assertTrue(len(conf.machines) == 1)
+        self.assertTrue(len(conf.networks) == 1)
+
+        machine_group = conf.machines[0]
+        network = conf.networks[0]
+
+        self.assertEqual(2, machine_group.nodes)
+
+        # check the network ref
+        self.assertEqual(network, machine_group.primary_network)
+
+    def test_from_dictionnary_with_machines_and_secondary_networks(self):
+        d = {
+            "resources": {
+                "machines": [{
+                    "roles": ["r1"],
+                    "nodes": 2,
+                    "cluster": "cluste1",
+                    "primary_network": "n1",
+                    "secondary_networks": ["n2"]
+                }],
+                "networks": [{
+                    "id": "n1",
+                    "roles": ["rn1"],
+                    "site": "siteA",
+                    "type": "prod"
+                }, {
+                    "id": "n2",
+                    "roles": ["rn2"],
+                    "site": "siteA",
+                    "type": "kavlan"
+                }]
+            }
+        }
+
+        conf = Configuration.from_dictionnary(d)
+        self.assertTrue(len(conf.machines) == 1)
+        self.assertTrue(len(conf.networks) == 2)
+
+        machine_group = conf.machines[0]
+        networks = conf.networks
+
+        self.assertEqual(2, machine_group.nodes)
+
+        # check the network ref
+        self.assertTrue(machine_group.primary_network in networks)
+        self.assertEqual("n1", machine_group.primary_network.id)
+        self.assertEqual(1, len(machine_group.secondary_networks))
+        self.assertTrue(machine_group.secondary_networks[0] in networks)
+        self.assertEqual("n2", machine_group.secondary_networks[0].id)
+
+
+    def test_from_dictionnary_no_network(self):
+        d = {
+            "resources": {
+                "machines": [{
+                    "roles": ["r1"],
+                    "nodes": 2,
+                    "cluster": "cluste1",
+                    "primary_network": "n1"
+                }],
+                "networks": []
+            }
+        }
+
+        with self.assertRaises(ValueError) as _:
+            Configuration.from_dictionnary(d)
+
+    def test_from_dictionnary_unbound_network(self):
+        d = {
+            "resources": {
+                "machines": [{
+                    "roles": ["r1"],
+                    "nodes": 2,
+                    "cluster": "cluste1",
+                    "primary_network": "n1"
+                }],
+                "networks": [{
+                    "id": "unbound_network",
+                    "roles": ["rn1"],
+                    "site": "siteA",
+                    "type": "prod"
+                }]
+            }
+        }
+
+        with self.assertRaises(ValueError) as _:
+            Configuration.from_dictionnary(d)
+
+
+    def test_from_dictionnary_no_secondary_networks(self):
+        d = {
+            "resources": {
+                "machines": [{
+                    "roles": ["r1"],
+                    "nodes": 2,
+                    "cluster": "cluste1",
+                    "primary_network": "n1",
+                    "secondary_networks": ["n2"]
+                }],
+                "networks": [{
+                    "id": "n1",
+                    "roles": ["rn1"],
+                    "site": "siteA",
+                    "type": "prod"
+                }]
+            }
+        }
+
+        with self.assertRaises(ValueError) as ctx:
+            conf = Configuration.from_dictionnary(d)
+
+    def test_from_dictionnary_unbound_secondary_networks(self):
+        d = {
+            "resources": {
+                "machines": [{
+                    "roles": ["r1"],
+                    "nodes": 2,
+                    "cluster": "cluste1",
+                    "primary_network": "n1",
+                    "secondary_networks": ["n2"]
+                }],
+                "networks": [{
+                    "id": "network",
+                    "roles": ["n1"],
+                    "site": "siteA",
+                    "type": "prod"
+                }, {
+                    "id": "unbound_network",
+                    "roles": ["nr2"],
+                    "site": "siteA",
+                    "type": "kavlan"
+                }]
+            }
+        }
+
+        with self.assertRaises(ValueError) as ctx:
+            conf = Configuration.from_dictionnary(d)
+
+
+    def test_programmatic(self):
+        conf = Configuration()
+        network = NetworkConfiguration(id="id",
+                                       roles=["my_network"],
+                                       type="prod",
+                                       site="rennes")
+
+        conf.add_network_conf(network)\
+            .add_machine_conf(MachineConfiguration(roles=["r1"],
+                                              cluster="paravance",
+                                              primary_network=network))\
+            .add_machine_conf(MachineConfiguration(roles=["r2"],
+                                              cluster="parapluie",
+                                              primary_network=network,
+                                              nodes=10))
+
+        conf.finalize()
+
+        self.assertEqual(2, len(conf.machines))
+        self.assertEqual(1, len(conf.networks))
+
+
+class TestNetworkConfiguration(EnosTest):
+
+    def test_network_minimal(self):
+        n = {
+            "id": "n1",
+            "roles": ["r1"],
+            "site": "siteA",
+            "type": "prod"
+        }
+
+        network = NetworkConfiguration.from_dictionnary(n)
+        self.assertEqual("siteA", network.site)
+        self.assertEqual(["r1"], network.roles)
+        self.assertEqual("prod", network.type)
+        self.assertEqual("n1", network.id)
+
+
diff --git a/enoslib/tests/unit/infra/enos_g5k/test_utils.py b/enoslib/tests/unit/infra/enos_g5k/test_utils.py
index a18d58a1671c46c6fe56fb90d58174d233a9a724..c2398ccaf8b56752cdb86c08682bc1966ed048a3 100644
--- a/enoslib/tests/unit/infra/enos_g5k/test_utils.py
+++ b/enoslib/tests/unit/infra/enos_g5k/test_utils.py
@@ -101,8 +101,8 @@ class TestConcretizeNetwork(EnosTest):
         ]
         subnets = []
         utils.concretize_networks(self.resources, networks, subnets)
-        self.assertEquals(networks[0], self.resources["networks"][0]["_c_network"])
-        self.assertEquals(networks[1], self.resources["networks"][1]["_c_network"])
+        self.assertEqual(networks[0], self.resources["networks"][0]["_c_network"])
+        self.assertEqual(networks[1], self.resources["networks"][1]["_c_network"])
 
 
     def test_act_subnets(self):
@@ -112,8 +112,8 @@ class TestConcretizeNetwork(EnosTest):
             { "site": "rennes", "ip_prefix": "10.156.0.0/22" },
         ]
         utils.concretize_networks(self.resources_subnet, networks, subnets)
-        self.assertEquals(subnets[1], self.resources_subnet["networks"][0]["_c_network"])
-        self.assertEquals(subnets[0], self.resources_subnet["networks"][1]["_c_network"])
+        self.assertEqual(subnets[1], self.resources_subnet["networks"][0]["_c_network"])
+        self.assertEqual(subnets[0], self.resources_subnet["networks"][1]["_c_network"])
 
 
     def test_prod(self):
@@ -123,9 +123,9 @@ class TestConcretizeNetwork(EnosTest):
         ]
         subnets = []
         utils.concretize_networks(self.resources, networks, subnets)
-        # self.assertEquals(networks[0], self.resources["networks"][0]["_c_network"])
-        self.assertEquals(None, self.resources["networks"][0]["_c_network"]["vlan_id"])
-        self.assertEquals(networks[0], self.resources["networks"][1]["_c_network"])
+        # self.assertEqual(networks[0], self.resources["networks"][0]["_c_network"])
+        self.assertEqual(None, self.resources["networks"][0]["_c_network"]["vlan_id"])
+        self.assertEqual(networks[0], self.resources["networks"][1]["_c_network"])
 
     def test_one_missing(self):
         networks = [
diff --git a/enoslib/tests/unit/infra/enos_openstack/__init__.py b/enoslib/tests/unit/infra/enos_openstack/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..a6131c10e6a0636dfad21d2d40d5fe7db6cd9f9f
--- /dev/null
+++ b/enoslib/tests/unit/infra/enos_openstack/__init__.py
@@ -0,0 +1 @@
+# init
diff --git a/enoslib/tests/unit/infra/enos_openstack/test_configuration.py b/enoslib/tests/unit/infra/enos_openstack/test_configuration.py
new file mode 100644
index 0000000000000000000000000000000000000000..aba41d11ded8d39843a53a7ffe5019eab52de6a4
--- /dev/null
+++ b/enoslib/tests/unit/infra/enos_openstack/test_configuration.py
@@ -0,0 +1,47 @@
+from enoslib.infra.enos_openstack.configuration import Configuration
+import enoslib.infra.enos_openstack.constants as constants
+
+from ... import EnosTest
+
+import jsonschema
+
+class TestConfiguration(EnosTest):
+
+    def test_from_dictionnary_minimal(self):
+        d = {
+            "key_name": "test_key",
+            "user": "test_user",
+            "image": "test_image",
+            "resources": {
+                "machines": [],
+                "networks": []
+            }
+        }
+        conf = Configuration.from_dictionnary(d)
+        self.assertEqual(constants.DEFAULT_ALLOCATION_POOL, conf.allocation_pool)
+        self.assertEqual(constants.DEFAULT_CONFIGURE_NETWORK, conf.configure_network)
+
+    def test_from_dictionnary_missing_keys(self):
+        d = {
+            "resources": {
+                "machines": [],
+                "networks": []
+            }
+        }
+        with self.assertRaises(jsonschema.exceptions.ValidationError) as _:
+            Configuration.from_dictionnary(d)
+
+
+    def test_from_dictionnary(self):
+        d = {
+            "key_name": "test_key",
+            "user": "test_user",
+            "image": "test_image",
+            "resources": {
+                "machines": [{"roles": ["r1"], "flavour": "m1.tiny", "number": 1}],
+                "networks": ["api"]
+            }
+        }
+        conf = Configuration.from_dictionnary(d)
+        self.assertEqual("test_key", conf.key_name)
+        self.assertEqual("test_image", conf.image)
diff --git a/enoslib/tests/unit/infra/enos_static/test_configuration.py b/enoslib/tests/unit/infra/enos_static/test_configuration.py
new file mode 100644
index 0000000000000000000000000000000000000000..d15bdd54e06333b3f4811cb3be5cbe6ded53a116
--- /dev/null
+++ b/enoslib/tests/unit/infra/enos_static/test_configuration.py
@@ -0,0 +1,43 @@
+from enoslib.infra.enos_static.configuration import Configuration, NetworkConfiguration, MachineConfiguration
+from ... import EnosTest
+
+import jsonschema
+
+
+class TestConfiguration(EnosTest):
+
+    def test_from_dictionnary_minimal(self):
+        d = {
+            "resources": {
+                "machines": [],
+                "networks": []
+            }
+        }
+        conf = Configuration.from_dictionnary(d)
+        self.assertEqual([], conf.machines)
+        self.assertEqual([], conf.networks)
+
+    def test_programmatic_missing_keys(self):
+        conf = Configuration()
+        conf.add_machine_conf(MachineConfiguration())
+        with self.assertRaises(jsonschema.exceptions.ValidationError) as _:
+            conf.finalize()
+
+    def test_programmatic_missing_address(self):
+        conf = Configuration()
+        conf.add_machine_conf(MachineConfiguration(roles=["plop"]))
+        with self.assertRaises(jsonschema.exceptions.ValidationError) as _:
+            conf.finalize()
+
+    def test_programmatic(self):
+        conf = Configuration()
+        conf.add_machine_conf(MachineConfiguration(roles=["role"],
+                                                   address="1.2.3.4"))
+        conf.add_network_conf(NetworkConfiguration(roles=["nrole"],
+                                                   start="1.2.3.10",
+                                                   end="1.2.3.15",
+                                                   cidr="1.2.3.4/24",
+                                                   gateway="1.2.3.254",
+                                                   dns="1.2.3.253"))
+        conf.finalize()
+        self.assertEqual(1, len(conf.machines))
diff --git a/enoslib/tests/unit/infra/enos_static/test_static.py b/enoslib/tests/unit/infra/enos_static/test_static.py
index 595a4dcc2efcf855f0371d0b967ccdd49b992a5c..74ff48f6e0018b7f89a5bd4c20655550925e2439 100644
--- a/enoslib/tests/unit/infra/enos_static/test_static.py
+++ b/enoslib/tests/unit/infra/enos_static/test_static.py
@@ -1,4 +1,5 @@
 from enoslib.infra.enos_static.provider import Static
+from enoslib.infra.enos_static.configuration import Configuration
 from enoslib.tests.unit import EnosTest
 
 class TestBuildResources(EnosTest):
@@ -11,19 +12,30 @@ class TestBuildResources(EnosTest):
                 "roles": ["role1", "role2"]
             },{
                 "address": "ip2",
-                "role": "role1"
+                "roles": ["role1"]
             }],
             "networks": [{
-                "cidr": "cidr1",
-                "roles": ["net1", "net2"]
+                "cidr": "",
+                "roles": ["net2"],
+                "start": "2.2.3.100",
+                "end": "2.2.3.252",
+                "gateway": "2.2.3.254",
+                "dns": "2.2.3.253"
             },{
-                "cidr": "cidr2",
-                "role": "net1"
+                "cidr": "1.2.3.4/24",
+                "roles": ["net1"],
+                "start": "1.2.3.100",
+                "end": "1.2.3.252",
+                "gateway": "1.2.3.254",
+                "dns": "1.2.3.253"
             }]
         }
-
-        s = Static({"resources": resources})
-        roles, _ = s.init()
+        conf = Configuration.from_dictionnary({"resources": resources})
+        s = Static(conf)
+        roles, networks = s.init()
         self.assertCountEqual(["role1", "role2"], roles.keys())
-        self.assertEquals(2, len(roles["role1"]))
-        self.assertEquals(1, len(roles["role2"]))
+        self.assertEqual(2, len(roles["role1"]))
+        self.assertEqual(1, len(roles["role2"]))
+        self.assertEqual(2, len(networks))
+        self.assertTrue(networks[0]["cidr"] in ["1.2.3.4/24", ""])
+
diff --git a/enoslib/tests/unit/infra/enos_vagrant/__init__.py b/enoslib/tests/unit/infra/enos_vagrant/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e966b61791df0d0d0424f7beb20944e223bcb1fb
--- /dev/null
+++ b/enoslib/tests/unit/infra/enos_vagrant/__init__.py
@@ -0,0 +1 @@
+# __init__
diff --git a/enoslib/tests/unit/infra/enos_vagrant/test_configuration.py b/enoslib/tests/unit/infra/enos_vagrant/test_configuration.py
new file mode 100644
index 0000000000000000000000000000000000000000..ea0d89e2daead8d7608c8ae26e84c6fb0b70635e
--- /dev/null
+++ b/enoslib/tests/unit/infra/enos_vagrant/test_configuration.py
@@ -0,0 +1,73 @@
+from enoslib.infra.enos_vagrant.configuration import (Configuration,
+                                                      MachineConfiguration,
+                                                      NetworkConfiguration)
+from enoslib.infra.enos_vagrant.constants import FLAVOURS
+import enoslib.infra.enos_vagrant.constants as constants
+
+from ... import EnosTest
+
+import jsonschema
+
+
+class TestConfiguration(EnosTest):
+
+    def test_from_dictionnary_minimal(self):
+        d = {
+            "resources": {
+                "machines": [],
+                "networks": []
+            }
+        }
+        conf = Configuration.from_dictionnary(d)
+        self.assertEqual(constants.DEFAULT_BACKEND, conf.backend)
+        self.assertEqual(constants.DEFAULT_BOX, conf.box)
+        self.assertEqual(constants.DEFAULT_USER, conf.user)
+
+    def test_from_dictionnary_custom_backend(self):
+        d = {
+            "backend": "virtualbox",
+            "resources": {
+                "machines": [],
+                "networks": []
+            }
+        }
+        conf = Configuration.from_dictionnary(d)
+        self.assertEqual("virtualbox", conf.backend)
+
+    def test_programmatic(self):
+        conf = Configuration()
+        conf.add_machine_conf(MachineConfiguration(roles=["r1"],
+                                                   flavour=FLAVOURS["large"],
+                                                   number=10))\
+            .add_network_conf(NetworkConfiguration(roles=["net1"], cidr="192.168.2.1/24"))
+
+        conf.finalize()
+        self.assertEqual(1, len(conf.machines))
+
+    def test_programmatic_missing_keys(self):
+        conf = Configuration()
+        conf.add_machine_conf(MachineConfiguration())
+        with self.assertRaises(jsonschema.exceptions.ValidationError) as _:
+            conf.finalize()
+
+
+class TestMachineConfiguration(EnosTest):
+
+    def test_from_dictionnary_minimal(self):
+        d = {
+            "roles": ["r1"]
+        }
+        conf = MachineConfiguration.from_dictionnary(d)
+        self.assertEqual(constants.DEFAULT_FLAVOUR, conf.flavour)
+        self.assertEqual(constants.DEFAULT_NUMBER, conf.number)
+
+    def test_from_dictionnary(self):
+        d = {
+            "roles": ["r1"],
+            "flavour": "large",
+            "number": 2,
+            "networks": ["n1"]
+        }
+        conf = MachineConfiguration.from_dictionnary(d)
+        self.assertEqual(constants.FLAVOURS["large"], conf.flavour)
+        self.assertEqual(2, conf.number)
diff --git a/enoslib/tests/unit/test_network_utils.py b/enoslib/tests/unit/test_network_utils.py
index 00aff0e008fe4d42cc8f84f9401b1197d679a57a..5110861d0a8d22b0130e87639933277530369c8b 100644
--- a/enoslib/tests/unit/test_network_utils.py
+++ b/enoslib/tests/unit/test_network_utils.py
@@ -18,7 +18,7 @@ class TestExpandDescription(EnosTest):
             'symetric': True
         }
         descs = _expand_description(desc)
-        self.assertEquals(1, len(descs))
+        self.assertEqual(1, len(descs))
         self.assertDictEqual(desc, descs[0])
 
     def test_src_expansion(self):
@@ -31,11 +31,11 @@ class TestExpandDescription(EnosTest):
         }
         # checking cardinality : the cartesian product
         descs = _expand_description(desc)
-        self.assertEquals(3, len(descs))
+        self.assertEqual(3, len(descs))
 
         # checking that expansion has been generated
         srcs = map(lambda d: d.pop('src'), descs)
-        self.assertEquals(set(srcs), {'grp1', 'grp2', 'grp3'})
+        self.assertEqual(set(srcs), {'grp1', 'grp2', 'grp3'})
 
         # checking that the remaining is untouched
         desc.pop('src')
@@ -53,11 +53,11 @@ class TestExpandDescription(EnosTest):
         }
         # checking cardinality : the cartesian product
         descs = _expand_description(desc)
-        self.assertEquals(3, len(descs))
+        self.assertEqual(3, len(descs))
 
         # checking that expansion has been generated
         dsts = map(lambda d: d.pop('dst'), descs)
-        self.assertEquals(set(dsts), {'grp1', 'grp2', 'grp3'})
+        self.assertEqual(set(dsts), {'grp1', 'grp2', 'grp3'})
 
         # checking that the remaining is untouched
         desc.pop('dst')
@@ -75,14 +75,14 @@ class TestExpandDescription(EnosTest):
         }
         # checking cardinality : the cartesian product
         descs = _expand_description(desc)
-        self.assertEquals(9, len(descs))
+        self.assertEqual(9, len(descs))
 
         # checking that expansion has been generated
         dsts = map(lambda d: d.pop('dst'), descs)
-        self.assertEquals(set(dsts), {'grp4', 'grp5', 'grp6'})
+        self.assertEqual(set(dsts), {'grp4', 'grp5', 'grp6'})
         # checking that expansion has been generated
         srcs = map(lambda d: d.pop('src'), descs)
-        self.assertEquals(set(srcs), {'grp1', 'grp2', 'grp3'})
+        self.assertEqual(set(srcs), {'grp1', 'grp2', 'grp3'})
 
         # checking that the remaining is untouched
         desc.pop('dst')
@@ -104,16 +104,16 @@ class TestGenerateDefaultGrpConstraints(EnosTest):
         descs = _generate_default_grp_constraints(roles, network_constraints)
 
         # Cartesian product is applied
-        self.assertEquals(2, len(descs))
+        self.assertEqual(2, len(descs))
 
         # defaults are applied
         for d in descs:
-            self.assertEquals('10mbit', d['rate'])
-            self.assertEquals('10ms', d['delay'])
+            self.assertEqual('10mbit', d['rate'])
+            self.assertEqual('10ms', d['delay'])
 
         # descs are symetrics
-        self.assertEquals(descs[0]['src'], descs[1]['dst'])
-        self.assertEquals(descs[0]['dst'], descs[1]['src'])
+        self.assertEqual(descs[0]['src'], descs[1]['dst'])
+        self.assertEqual(descs[0]['dst'], descs[1]['src'])
 
 
     def test_except_one_group(self):
@@ -129,7 +129,7 @@ class TestGenerateDefaultGrpConstraints(EnosTest):
         }
         descs = _generate_default_grp_constraints(roles, network_constraints)
         # Cartesian product is applied but grp1 isn't taken
-        self.assertEquals(2, len(descs))
+        self.assertEqual(2, len(descs))
 
         for d in descs:
             self.assertTrue('grp1' != d['src'])
@@ -150,7 +150,7 @@ class TestGenerateDefaultGrpConstraints(EnosTest):
         descs = _generate_default_grp_constraints(roles, network_constraints)
 
         # Cartesian product is applied but grp1 isn't taken
-        self.assertEquals(2, len(descs))
+        self.assertEqual(2, len(descs))
 
         for d in descs:
             self.assertTrue('grp1' != d['src'])
@@ -172,7 +172,7 @@ class TestGenerateActualGrpConstraints(EnosTest):
         }
         descs = _generate_actual_grp_constraints(network_constraints)
 
-        self.assertEquals(1, len(descs))
+        self.assertEqual(1, len(descs))
         self.assertDictEqual(constraints[0], descs[0])
 
 
@@ -191,16 +191,16 @@ class TestGenerateActualGrpConstraints(EnosTest):
         }
         descs = _generate_actual_grp_constraints(network_constraints)
 
-        self.assertEquals(2, len(descs))
+        self.assertEqual(2, len(descs))
 
         # bw/rate are applied
         for d in descs:
-            self.assertEquals('20mbit', d['rate'])
-            self.assertEquals('20ms', d['delay'])
+            self.assertEqual('20mbit', d['rate'])
+            self.assertEqual('20ms', d['delay'])
 
         # descs are symetrics
-        self.assertEquals(descs[0]['src'], descs[1]['dst'])
-        self.assertEquals(descs[0]['dst'], descs[1]['src'])
+        self.assertEqual(descs[0]['src'], descs[1]['dst'])
+        self.assertEqual(descs[0]['dst'], descs[1]['src'])
 
     def test_expansion_symetric(self):
         constraints = [{
@@ -217,12 +217,12 @@ class TestGenerateActualGrpConstraints(EnosTest):
         }
         descs = _generate_actual_grp_constraints(network_constraints)
 
-        self.assertEquals(3*3*2, len(descs))
+        self.assertEqual(3*3*2, len(descs))
 
         # bw/rate are applied
         for d in descs:
-            self.assertEquals('20mbit', d['rate'])
-            self.assertEquals('20ms', d['delay'])
+            self.assertEqual('20mbit', d['rate'])
+            self.assertEqual('20ms', d['delay'])
 
     def test_expansion_no_symetric(self):
         constraints = [{
@@ -238,12 +238,12 @@ class TestGenerateActualGrpConstraints(EnosTest):
         }
         descs = _generate_actual_grp_constraints(network_constraints)
 
-        self.assertEquals(3*3, len(descs))
+        self.assertEqual(3*3, len(descs))
 
         # bw/rate are applied
         for d in descs:
-            self.assertEquals('20mbit', d['rate'])
-            self.assertEquals('20ms', d['delay'])
+            self.assertEqual('20mbit', d['rate'])
+            self.assertEqual('20ms', d['delay'])
 
     def test_same_src_and_dest_defaults_embedded(self):
         constraints = [{
@@ -259,7 +259,7 @@ class TestGenerateActualGrpConstraints(EnosTest):
         }
         descs = _generate_actual_grp_constraints(network_constraints)
 
-        self.assertEquals(1, len(descs))
+        self.assertEqual(1, len(descs))
         self.assertDictEqual(constraints[0], descs[0])
         for d in descs:
             self.assertTrue('grp1' == d['src'])
@@ -280,15 +280,15 @@ class TestGenerateActualGrpConstraints(EnosTest):
             'constraints': constraints
         }
         descs = _build_grp_constraints(roles, network_constraints)
-        self.assertEquals(3, len(descs))
+        self.assertEqual(3, len(descs))
         # bw/rate are applied
         count_src_equals_dst = 0
         for d in descs:
-            self.assertEquals('10mbit', d['rate'])
-            self.assertEquals('10ms', d['delay'])
+            self.assertEqual('10mbit', d['rate'])
+            self.assertEqual('10ms', d['delay'])
             if d['src'] == d['dst'] == 'grp1':
                 count_src_equals_dst += 1
-        self.assertEquals(1,count_src_equals_dst)
+        self.assertEqual(1,count_src_equals_dst)
 
 
 class TestMergeConstraints(EnosTest):
@@ -365,7 +365,7 @@ class TestBuildIpConstraints(EnosTest):
         self.assertTrue('tc' in ips_with_tc['node1'])
         tcs = ips_with_tc['node1']['tc']
         # one rule per dest ip and source device
-        self.assertEquals(2*2, len(tcs))
+        self.assertEqual(2*2, len(tcs))
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tox.ini b/tox.ini
index e3d3332962657ba169a5584cf48bde800747889e..65f150c5d6d57fa0528b26a323a72c78b3f36af3 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,7 +1,7 @@
 # tox -epy27
 [tox]
 skipsdist = True
-envlist = py27, py35, py36, pep8
+envlist = py35, py36, pep8
 
 [testenv]
 whitelist_externals = make
@@ -32,7 +32,7 @@ commands =
 # https://pep8.readthedocs.io/en/latest/intro.html#error-codes
 # http://flake8.pycqa.org/en/latest/user/error-codes.html
 # F821 : F821 undefined name 'basestring' (python3)
-ignore = E121,E122,E123,E124,E125,E127,E128,E129,E131,E241,H405,F821
+ignore = E121,E122,E123,E124,E125,E127,E128,E129,E131,E241,H405,F821, W503
 show-source = true
 exclude = venv,.git,.tox,dist,*egg,ansible,tests
 max-complexity = 12
@@ -40,6 +40,5 @@ max-complexity = 12
 # Instruct travis which envs to run
 [travis]
 python =
-  2.7: py27, pep8
   3.5: py35, pep8
   3.6: py36, pep8