diff --git a/docs/apidoc/infra.rst b/docs/apidoc/infra.rst
index d751ba55f1c3c58e5438e43f639f19d2b3616bd0..18cb2bc56a5b4d5f39f3f30ada0eee79352dfb4d 100644
--- a/docs/apidoc/infra.rst
+++ b/docs/apidoc/infra.rst
@@ -51,6 +51,22 @@ G5k API
     :undoc-members:
     :show-inheritance:
 
+Virtual Machines on Grid5000 (VMonG5k)
+--------------------------------------
+
+VMonG5k Provider Class
+^^^^^^^^^^^^^^^^^^^^^^
+
+.. automodule:: enoslib.infra.enos_vmong5k.provider
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
+VMonG5k Schema
+^^^^^^^^^^^^^^^
+
+.. literalinclude:: ../../enoslib/infra/enos_vmong5k/schema.py
+
 Static
 ------
 
diff --git a/docs/tutorials/grid5000.rst b/docs/tutorials/grid5000.rst
index f9d7506d1ed3c06afeacb7b4d2e44884a8bbc56c..0aa966ba10455f86df91f43d7cbf902f44f23d69 100644
--- a/docs/tutorials/grid5000.rst
+++ b/docs/tutorials/grid5000.rst
@@ -117,4 +117,5 @@ You can found it `here
 It starts reserve non-deploy nodes and a subnet and start virtual machines on
 them.
 
-
+.. hint::
+  Note that it is now possible to use the ``VMonG5k`` provider directly.
diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst
index 42e39751a9400eb833d7a101f36f2e469d58f338..c2f42108ed149c6859101c5cbd2c5632524ff15f 100644
--- a/docs/tutorials/index.rst
+++ b/docs/tutorials/index.rst
@@ -16,3 +16,4 @@ Tutorials
     using-tasks
     ansible-integration
     chameleon
+    vmong5k
diff --git a/docs/tutorials/vmong5k.rst b/docs/tutorials/vmong5k.rst
new file mode 100644
index 0000000000000000000000000000000000000000..c62a4b188ea8d63a36d801ec51f05ed75909f3d5
--- /dev/null
+++ b/docs/tutorials/vmong5k.rst
@@ -0,0 +1,49 @@
+Tutorial 6 - Working with virtual machines on Grid'5000
+=======================================================
+
+This tutorial leverages the ``VmonG5k`` provider: a provider that provisions
+virtual machines for you on Grid'5000. 
+
+Installation
+------------
+
+
+On Grid'5000, you can go with a virtualenv :
+
+.. code-block:: bash
+
+    $ virtualenv venv
+    $ source venv/bin/activate
+    $ pip install -U pip
+
+    $ pip install enoslib
+
+Basic example
+-------------
+
+We'll imagine a system that requires 100 compute machines and 3 controller machines.
+We express this using the ~VmonG5K~ provider:
+
+.. literalinclude:: vmong5k/tuto_vmong5k.py
+   :language: python
+   :linenos:
+
+
+- You can launch the script using :
+
+    .. code-block:: bash
+
+        $ python tuto_vmg5k.py
+
+- The raw data structures of EnOSlib will be displayed and you should be able to
+  connect to any machine using SSH and the root account.
+
+Notes
+-----
+
+* The ``VmonG5K`` provider internally uses the ``G5k`` provider. In particular
+  it sets the ``job_type`` to ``allow_classic_ssh`` and claim an extra
+  ``slash_22`` subnet.
+
+* SSH access will be granted to the VMs using the ``~/.ssh/id_rsa | ~/.ssh/id_rsa.pub`` keypair.
+  So these files should be present in your home directory.
diff --git a/docs/tutorials/vmong5k/tuto_vmong5k.py b/docs/tutorials/vmong5k/tuto_vmong5k.py
new file mode 100644
index 0000000000000000000000000000000000000000..335c10a969516c383355d79ccd7f0d890a56858d
--- /dev/null
+++ b/docs/tutorials/vmong5k/tuto_vmong5k.py
@@ -0,0 +1,27 @@
+from enoslib.infra.enos_vmong5k.provider import VMonG5k
+from enoslib.infra.enos_vmong5k.configuration import Configuration
+
+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_name="tuto-vmong5k")\
+    .add_machine(roles=["control"],
+                 cluster="parapluie",
+                 number=3,
+                 flavour="large")\
+    .add_machine(roles=["compute"],
+                 cluster="parapluie",
+                 number=100,
+                 flavour="tiny")\
+    .finalize()
+provider = VMonG5k(conf)
+
+roles, networks = provider.init()
+print(roles)
+print(networks)
diff --git a/enoslib/host.py b/enoslib/host.py
index 601e3fbbc63937208c03dcde38d9037f49f5e512..8e4378ce158982734b08f59000efc432e7c29bcd 100644
--- a/enoslib/host.py
+++ b/enoslib/host.py
@@ -1,11 +1,12 @@
 # -*- coding: utf-8 -*-
+import copy
 
 
 class Host(object):
 
     def __init__(
             self,
-            address,
+            address, *,
             alias=None,
             user=None,
             keyfile=None,
@@ -20,6 +21,9 @@ class Host(object):
         self.port = port
         self.extra = extra or {}
 
+    def to_dict(self):
+        return copy.deepcopy(self.__dict__)
+
     def __repr__(self):
         args = [self.alias, "address=%s" % self.address]
         return "Host(%s)" % ", ".join(args)
diff --git a/enoslib/infra/enos_vmong5k/__init__.py b/enoslib/infra/enos_vmong5k/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..a6131c10e6a0636dfad21d2d40d5fe7db6cd9f9f
--- /dev/null
+++ b/enoslib/infra/enos_vmong5k/__init__.py
@@ -0,0 +1 @@
+# init
diff --git a/enoslib/infra/enos_vmong5k/ansible/cloud-init-data/meta-data b/enoslib/infra/enos_vmong5k/ansible/cloud-init-data/meta-data
new file mode 100644
index 0000000000000000000000000000000000000000..91ad25895f9088a49570cc44033239fc27faf7fe
--- /dev/null
+++ b/enoslib/infra/enos_vmong5k/ansible/cloud-init-data/meta-data
@@ -0,0 +1,4 @@
+#instance-id: iid-local01
+#local-hostname: example-vm
+public-keys:
+  - "{{ pubkey }}" 
diff --git a/enoslib/infra/enos_vmong5k/ansible/cloud-init-data/user-data b/enoslib/infra/enos_vmong5k/ansible/cloud-init-data/user-data
new file mode 100644
index 0000000000000000000000000000000000000000..270e063c042d51411947c3728e9e46c296dcd92e
--- /dev/null
+++ b/enoslib/infra/enos_vmong5k/ansible/cloud-init-data/user-data
@@ -0,0 +1,10 @@
+#cloud-config
+disable_root: false
+bootcmd:
+  - sed -i "/127.0.0.1/d" /etc/hosts
+  - echo "127.0.0.1 localhost" >> /etc/hosts
+  - echo "10.158.0.2   vm_control-0  vm_control-0.grid5000.fr" >> /etc/hosts
+  - echo "10.158.0.3   vm_control-1  vm_control-1.grid5000.fr" >> /etc/hosts
+  - echo "10.158.0.4   vm_control-2  vm_control-2.grid5000.fr" >> /etc/hosts
+  - echo "10.158.0.5   vm_network-0  vm_network-0.grid5000.fr" >> /etc/hosts
+  - echo "10.158.0.6   vm_compute-0  vm_compute-0.grid5000.fr" >> /etc/hosts
diff --git a/enoslib/infra/enos_vmong5k/ansible/domain.xml.j2 b/enoslib/infra/enos_vmong5k/ansible/domain.xml.j2
new file mode 100644
index 0000000000000000000000000000000000000000..3e701c65a3fa368b6717009c388ddec8b7e86119
--- /dev/null
+++ b/enoslib/infra/enos_vmong5k/ansible/domain.xml.j2
@@ -0,0 +1,41 @@
+<domain type='kvm'>
+ <cpu mode='host-passthrough'></cpu>
+ <name>{{ item.alias }}</name>
+ <memory>{{ item.mem }}</memory>
+ <vcpu>{{ item.core }}</vcpu>
+ <features>
+   <acpi/>
+ </features>
+ <os>
+   <type arch="x86_64">hvm</type>
+ </os>
+ <clock offset="localtime"/>
+ <on_poweroff>destroy</on_poweroff>
+ <on_reboot>restart</on_reboot>
+ <on_crash>destroy</on_crash>
+ <devices>
+   <emulator>/usr/bin/kvm</emulator>
+   <disk type='file' device='disk'>
+     <driver name='qemu' type='qcow2'/>
+     <source file='/tmp/disks/{{ item.alias}}'/>
+     <target dev='vda' bus='virtio'/>
+   </disk>
+   <disk type='file' device='cdrom'>
+     <source file='/tmp/cloud-init-data-{{ item.alias }}.iso'/>
+     <target dev='vdb' bus='virtio'/>
+     <readonly/>
+   </disk>
+   <interface type='bridge'>
+     <source bridge='br0'/>
+     <mac address='{{ item.eui }}'/>
+   </interface>
+   <serial type='pty'>
+     <source path='/dev/ttyS0'/>
+     <target port='0'/>
+   </serial>
+   <console type='pty'>
+     <source path='/dev/ttyS0'/>
+     <target port='0'/>
+   </console>
+ </devices>
+</domain>
diff --git a/enoslib/infra/enos_vmong5k/ansible/meta-data.j2 b/enoslib/infra/enos_vmong5k/ansible/meta-data.j2
new file mode 100644
index 0000000000000000000000000000000000000000..ed97d539c095cf1413af30cc23dea272095b97dd
--- /dev/null
+++ b/enoslib/infra/enos_vmong5k/ansible/meta-data.j2
@@ -0,0 +1 @@
+---
diff --git a/enoslib/infra/enos_vmong5k/ansible/site.yaml b/enoslib/infra/enos_vmong5k/ansible/site.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..4e962f8b79be58b48647ebb82eceecccfdeb8174
--- /dev/null
+++ b/enoslib/infra/enos_vmong5k/ansible/site.yaml
@@ -0,0 +1,119 @@
+---
+- name: This is a play for virtual machines on Grid'5000
+  hosts: all
+  vars:
+    pubkey: "{{lookup('file', '~/.ssh/id_rsa.pub')}}"
+  tasks:
+
+    - name: Destroy running virtual machines (vm -1 / 1)
+      virt:
+        name: "{{ item.alias }}"
+        state: destroyed
+      ignore_errors: yes
+      with_items: "{{ vms[inventory_hostname] }}"
+
+    - name: Unregister existing virtual machines (vm -0 / 1)
+      virt:
+        name: "{{ item.alias }}"
+        command: undefine
+      ignore_errors: yes
+      with_items: "{{ vms[inventory_hostname] }}"
+
+    - name: Enable nested virtualization
+      shell: |
+        modprobe -r kvm_intel
+        modprobe kvm_intel nested=1
+
+    - name: Unmount the tmpfs
+      mount:
+        path: /tmp/disks
+        state: unmounted
+      when:
+        - tmpfs is defined
+        - tmpfs
+
+    - name: Remove a tmpfs for the vms
+      file:
+        path: /tmp/disks
+        state: absent
+      when:
+        - tmpfs is defined
+        - tmpfs
+
+    - name: Create a directory for hosting the virtual disks
+      file:
+        path: /tmp/disks
+        state: directory
+        mode: 777
+
+    - name: Mount the tmpfs
+      shell: "mount -t tmpfs -o size={{ tmpfs }} tmpfs /tmp/disks"
+      when:
+        - tmpfs is defined
+        - tmpfs
+
+    - name: Removing previous cloud init data
+      file:
+        path: "/tmp/cloud-init-data-{{ item.alias }}"
+        state: absent
+      loop: "{{ vms[inventory_hostname] }}"
+
+    - name: Removing previous cloud init data iso
+      file:
+        path: "/tmp/cloud-init-data-{{ item.alias }}.iso"
+        state: absent
+      loop: "{{ vms[inventory_hostname] }}"
+
+    - name: Creating cloud init data directory
+      file:
+        path: "/tmp/cloud-init-data-{{ item.alias }}"
+        state: directory
+      loop: "{{ vms[inventory_hostname] }}"
+
+    - name: Generate meta-data for cloud-init
+      template:
+        src: meta-data.j2
+        dest: "/tmp/cloud-init-data-{{ item.alias }}/meta-data"
+      loop: "{{ vms[inventory_hostname] }}"
+
+    - name: Generate user data for cloud-init
+      template:
+        src: user-data.j2
+        dest: "/tmp/cloud-init-data-{{ item.alias }}/user-data"
+      loop: "{{ vms[inventory_hostname] }}"
+
+    #  Create one iso per vm
+    - name: Create the iso for cloud-init
+      shell: "cd /tmp &&  genisoimage -output cloud-init-data-{{ item.alias }}.iso -volid cidata -joliet -rock cloud-init-data-{{ item.alias }}/user-data cloud-init-data-{{ item.alias }}/meta-data"
+      loop: "{{ vms[inventory_hostname] }}"
+
+    - name: Check base image
+      stat:
+        path: "{{ base_image }}"
+      register: p
+
+    - name: Verify base image accessibility
+      fail:
+        msg: "Base image does not exist. Verify this path is valid: {{ base_image }}"
+      when: p.stat.exists == False
+
+      # NOTE(msimonin): We don't copy in the ramfs in a first iteration
+    - name: Copy base image
+      shell: "cp {{ base_image }} /tmp/kenan-base-image.qcow2"
+
+    - name: Link virtual image to base image
+      shell: "qemu-img create -f qcow2 -o backing_file=/tmp/kenan-base-image.qcow2 /tmp/disks/{{ item.alias }}"
+      with_items: "{{ vms[inventory_hostname] }}"
+
+    - name: Define virtual machines (vm 0 / 1)
+      virt:
+        name: "{{ item.alias }}"
+        command: define
+        xml: "{{ lookup('template', 'domain.xml.j2') }}"
+      with_items: "{{ vms[inventory_hostname] }}"
+
+    - name: Launch virtual machines (vm 1 / 1)
+      virt:
+        name: "{{ item.alias }}"
+        state: running
+      with_items: "{{ vms[inventory_hostname] }}"
diff --git a/enoslib/infra/enos_vmong5k/ansible/user-data.j2 b/enoslib/infra/enos_vmong5k/ansible/user-data.j2
new file mode 100644
index 0000000000000000000000000000000000000000..30671766016e12d576f532fec6c20e09a416224e
--- /dev/null
+++ b/enoslib/infra/enos_vmong5k/ansible/user-data.j2
@@ -0,0 +1,15 @@
+#cloud-config
+hostname: {{ item.alias }}
+fqdn: {{ item.alias }}.grid5000.fr
+
+disable_root: false
+bootcmd:
+  - mkdir -p /root/.ssh
+  - echo "{{ pubkey }}" > /root/.ssh/authorized_keys
+  - sed -i "/127.0.0.1/d" /etc/hosts
+  - echo "127.0.0.1 localhost" >> /etc/hosts
+{% for _vms in vms.values() %}
+{% for vm in _vms %}
+  - echo "{{ vm.address }}   {{ vm.alias }}  {{ vm.alias }}.grid5000.fr" >> /etc/hosts
+{% endfor %}
+{% endfor %}
diff --git a/enoslib/infra/enos_vmong5k/configuration.py b/enoslib/infra/enos_vmong5k/configuration.py
new file mode 100644
index 0000000000000000000000000000000000000000..8673614baeaf18d989d32a080ea2401288c06d4f
--- /dev/null
+++ b/enoslib/infra/enos_vmong5k/configuration.py
@@ -0,0 +1,114 @@
+from ..configuration import BaseConfiguration
+from .constants import (DEFAULT_FLAVOUR, DEFAULT_IMAGE, DEFAULT_JOB_NAME,
+                        DEFAULT_NETWORKS, DEFAULT_NUMBER, DEFAULT_QUEUE,
+                        DEFAULT_WALLTIME, FLAVOURS)
+from .schema import SCHEMA
+
+import uuid
+
+
+class Configuration(BaseConfiguration):
+
+    _SCHEMA = SCHEMA
+
+    def __init__(self):
+        super().__init__()
+        self.job_name = DEFAULT_JOB_NAME
+        self.queue = DEFAULT_QUEUE
+        self.walltime = DEFAULT_WALLTIME
+        self.image = DEFAULT_IMAGE
+
+        self._machine_cls = MachineConfiguration
+        self._network_cls = str
+
+        self.networks = DEFAULT_NETWORKS
+
+    @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) 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": self.networks
+        })
+        return d
+
+
+class MachineConfiguration:
+
+    def __init__(self, *,
+               roles=None,
+               cluster=None,
+               flavour=None,
+               number=DEFAULT_NUMBER):
+        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
+        self.cluster = cluster
+
+        # a cookie to identify uniquely the group of machine this is used when
+        # redistributing the vms to pms in the provider. I've the feeling that
+        # this could be used to express some affinity between vms
+        self.cookie = uuid.uuid4().hex
+
+    @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)
+
+        cluster = dictionnary["cluster"]
+        if cluster is not None:
+            kwargs.update(cluster=cluster)
+
+        return cls(**kwargs)
+
+    def to_dict(self):
+        d = {}
+        d.update(roles=self.roles, flavour=self.flavour, number=self.number,
+                 cluster=self.cluster)
+        return d
diff --git a/enoslib/infra/enos_vmong5k/constants.py b/enoslib/infra/enos_vmong5k/constants.py
new file mode 100644
index 0000000000000000000000000000000000000000..cd0d4eb1145fa295271378f0a21f8a02e8fba8f9
--- /dev/null
+++ b/enoslib/infra/enos_vmong5k/constants.py
@@ -0,0 +1,43 @@
+import os
+
+DEFAULT_JOB_NAME = "EnOslib-vmong5k"
+DEFAULT_QUEUE = "default"
+DEFAULT_WALLTIME = "02:00:00"
+DEFAULT_IMAGE = "/grid5000/virt-images/debian9-x64-base.qcow2"
+PROVIDER_PATH = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
+PLAYBOOK_PATH = os.path.join(PROVIDER_PATH, "ansible", "site.yaml")
+
+
+#: Sizes of the machines available for the configuration
+FLAVOURS = {
+    "tiny": {
+        "core": 1,
+        "mem": 512
+    },
+    "small": {
+        "core": 1,
+        "mem": 1024
+    },
+    "medium": {
+        "core": 2,
+        "mem": 2048
+    },
+    "big": {
+        "core": 3,
+        "mem": 3072,
+    },
+    "large": {
+        "core": 4,
+        "mem": 4096
+    },
+    "extra-large": {
+        "core": 6,
+        "mem": 6144
+    }
+}
+
+DEFAULT_FLAVOUR = FLAVOURS["tiny"]
+
+DEFAULT_NETWORKS = ["enos_network"]
+
+DEFAULT_NUMBER = 1
diff --git a/enoslib/infra/enos_vmong5k/provider.py b/enoslib/infra/enos_vmong5k/provider.py
new file mode 100644
index 0000000000000000000000000000000000000000..1371e11664b4edf4ea0981fbea3e088784e9f7b4
--- /dev/null
+++ b/enoslib/infra/enos_vmong5k/provider.py
@@ -0,0 +1,189 @@
+# -*- coding: utf-8 -*-
+from .constants import PLAYBOOK_PATH
+from ..provider import Provider
+
+
+from enoslib.api import generate_inventory, run_ansible
+from enoslib.host import Host
+import enoslib.infra.enos_g5k.api as enoslib
+import enoslib.infra.enos_g5k.configuration as g5kconf
+import enoslib.infra.enos_g5k.provider as g5kprovider
+
+
+from collections import defaultdict
+import execo_g5k.api_utils as utils
+from ipaddress import IPv4Address
+import itertools
+import logging
+from netaddr import EUI, mac_unix_expanded
+import os
+
+
+logger = logging.getLogger(__name__)
+
+
+def _get_clusters_site(clusters):
+    sites = enoslib.get_clusters_sites(clusters)
+    site_names = set(sites.values())
+    if len(site_names) > 1:
+        raise Exception("Multi-site deployment is not supported yet")
+
+    return site_names.pop()
+
+
+def get_subnet_ip(mac):
+    # This is the format allowed on G5K for subnets
+    address = ['10'] + [str(int(i, 2)) for i in mac.bits().split('-')[-3:]]
+    return IPv4Address('.'.join(address))
+
+
+def mac_range(start, stop, step=1):
+    for item in range(int(start) + 1, int(stop), step):
+        yield EUI(item, dialect=mac_unix_expanded)
+
+
+def get_host_cores(cluster):
+    hosts = utils.get_cluster_hosts(cluster)
+    # (suppose) all hosts are the same in a cluster
+    attributes = utils.get_host_attributes(hosts[-1])
+    processors = attributes['architecture']['nb_procs']
+    cores = attributes['architecture']['nb_cores']
+
+    # number of cores as reported in the Website
+    return cores * processors
+
+
+def find_nodes_number(machine):
+    cores = get_host_cores(machine.cluster)
+    return - ((-1 * machine.number * machine.flavour["core"]) // cores)
+
+
+def _do_build_g5k_conf(vmong5k_conf, site):
+    g5k_conf = g5kconf.Configuration.from_settings(
+        job_name=vmong5k_conf.job_name,
+        walltime=vmong5k_conf.walltime,
+        queue=vmong5k_conf.queue,
+        job_type="allow_classic_ssh")
+    prod_network = g5kconf.NetworkConfiguration(roles=["prod"],
+                                           id="prod",
+                                           type="prod",
+                                           site=site)
+    subnet_roles = vmong5k_conf.networks
+    subnet_roles.append("__subnet__")
+    subnet = g5kconf.NetworkConfiguration(roles=subnet_roles,
+                                          id="subnet",
+                                          type="slash_22",
+                                          site=site)
+    # let's start by adding the networks
+    g5k_conf.add_network_conf(prod_network)\
+           .add_network_conf(subnet)
+
+    for _, machine in enumerate(vmong5k_conf.machines):
+        # we hide a descriptor of group in the original machines
+        roles = machine.roles
+        roles.append(machine.cookie)
+        g5k_conf.add_machine(roles=roles,
+                            cluster=machine.cluster,
+                            nodes=find_nodes_number(machine),
+                            primary_network=prod_network)
+    return g5k_conf
+
+
+def _build_g5k_conf(vmong5k_conf):
+    """Build the conf of the g5k provider from the vmong5k conf."""
+    clusters = [m.cluster for m in vmong5k_conf.machines]
+    site = _get_clusters_site(clusters)
+
+    return _do_build_g5k_conf(vmong5k_conf, site)
+
+
+def _distribute(machines, g5k_roles, g5k_subnet):
+    vmong5k_roles = defaultdict(list)
+    euis = mac_range(EUI(g5k_subnet["mac_start"]), EUI(g5k_subnet["mac_end"]))
+    for machine in machines:
+        pms = g5k_roles[machine.cookie]
+        pms_it = itertools.cycle(pms)
+        for idx in range(machine.number):
+            name = "vm-{}".format(idx)
+            pm = next(pms_it)
+            vm = VirtualMachine(name, next(euis), machine.flavour, pm)
+
+            for role in machine.roles:
+                vmong5k_roles[role].append(vm)
+    return dict(vmong5k_roles)
+
+
+def _index_by_host(roles):
+    virtual_machines_by_host = defaultdict(set)
+    for vms in roles.values():
+        for virtual_machine in vms:
+            host = virtual_machine.pm
+            # Two vms are equal if they have the same euis
+            virtual_machines_by_host[host.alias].add(
+                virtual_machine)
+    # now serialize all the thing
+    vms_by_host = defaultdict(list)
+    for host, vms in virtual_machines_by_host.items():
+        for virtual_machine in vms:
+            vms_by_host[host].append(virtual_machine.to_dict())
+
+    return dict(vms_by_host)
+
+
+def start_virtualmachines(provider_conf, g5k_init, vmong5k_roles):
+    vms_by_host = _index_by_host(vmong5k_roles)
+
+    extra_vars = {'vms': vms_by_host,
+                  'base_image': provider_conf.image}
+    pm_inventory_path = os.path.join(os.getcwd(), "pm_hosts")
+    generate_inventory(*g5k_init, pm_inventory_path)
+    # deploy virtual machines with ansible playbook
+    run_ansible([PLAYBOOK_PATH], pm_inventory_path, extra_vars)
+
+
+class VirtualMachine(Host):
+
+    def __init__(self, name, eui, flavour, pm):
+        super().__init__(str(get_subnet_ip(eui)), alias=name)
+        self.core = flavour["core"]
+        # libvirt uses kiB by default
+        self.mem = int(flavour["mem"]) * 1024
+        self.eui = eui
+        self.pm = pm
+
+    def to_dict(self):
+        d = super().to_dict()
+        d.update(core=self.core, mem=self.mem, eui=str(self.eui),
+                 pm=self.pm.to_dict())
+        return d
+
+    def __hash__(self):
+        return int(self.eui)
+
+    def __eq__(self, other):
+        return int(self.eui) == int(other.eui)
+
+
+class VMonG5k(Provider):
+    """The provider to use when deploying virtual machines on Grid'5000."""
+
+    def init(self, force_deploy=False):
+        g5k_conf = _build_g5k_conf(self.provider_conf)
+        g5k_provider = g5kprovider.G5k(g5k_conf)
+        g5k_roles, g5k_networks = g5k_provider.init()
+        g5k_subnet = [n for n in g5k_networks if "__subnet__" in n["roles"]][0]
+        vmong5k_roles = _distribute(self.provider_conf.machines,
+                                    g5k_roles,
+                                    g5k_subnet)
+
+        start_virtualmachines(self.provider_conf,
+                              (g5k_roles, g5k_networks),
+                              vmong5k_roles)
+
+        return vmong5k_roles, [g5k_subnet]
+
+    def destroy(self):
+        pass
+
+    def __str__(self):
+        return 'VMonG5k'
diff --git a/enoslib/infra/enos_vmong5k/schema.py b/enoslib/infra/enos_vmong5k/schema.py
new file mode 100644
index 0000000000000000000000000000000000000000..c759c547bd4dfe4e1b2378995a97a15864f2d108
--- /dev/null
+++ b/enoslib/infra/enos_vmong5k/schema.py
@@ -0,0 +1,46 @@
+from .constants import FLAVOURS
+
+QUEUE_TYPES = ["default", "testing", "production"]
+
+SCHEMA = {
+    "type": "object",
+    "properties": {
+        "resources": {"$ref": "#/resources"},
+        "job_name": {"type": "string"},
+        "queue": {"type": "string", "enum": QUEUE_TYPES},
+        "walltime": {"type": "string"},
+        "image": {"type": "string"}
+    },
+    "additionalProperties": False,
+    "required": ["resources"],
+    "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"}},
+            "cluster": {"type": "string"},
+            "number": {"type": "number"},
+            "oneOf": [
+                {"flavour": {"type": "string", "enum": list(FLAVOURS.keys())}},
+                {"flavour_desc": {"$ref": "#/flavour_desc"}}
+            ],
+
+        },
+        "required": ["roles", "cluster"]
+    },
+}
diff --git a/enoslib/tests/unit/infra/enos_vmong5k/__init__.py b/enoslib/tests/unit/infra/enos_vmong5k/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..a6131c10e6a0636dfad21d2d40d5fe7db6cd9f9f
--- /dev/null
+++ b/enoslib/tests/unit/infra/enos_vmong5k/__init__.py
@@ -0,0 +1 @@
+# init
diff --git a/enoslib/tests/unit/infra/enos_vmong5k/test_configuration.py b/enoslib/tests/unit/infra/enos_vmong5k/test_configuration.py
new file mode 100644
index 0000000000000000000000000000000000000000..c69c63abc9b90e82a774dc87642fb07d082d03ca
--- /dev/null
+++ b/enoslib/tests/unit/infra/enos_vmong5k/test_configuration.py
@@ -0,0 +1,74 @@
+from enoslib.infra.enos_vmong5k.configuration import Configuration, MachineConfiguration
+import enoslib.infra.enos_vmong5k.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_JOB_NAME, conf.job_name)
+        self.assertEqual([], conf.machines)
+        self.assertEqual([], conf.machines)
+
+    def test_from_dictionnary_custom_backend(self):
+        d = {
+            "job_name": "test-job",
+            "walltime": "12:34:56",
+            "resources": {
+                "machines": [],
+                "networks": []
+            }
+        }
+        conf = Configuration.from_dictionnary(d)
+        self.assertEqual("test-job", conf.job_name)
+        self.assertEqual("12:34:56", conf.walltime)
+
+    def test_programmatic(self):
+        conf = Configuration()
+        conf.add_machine_conf(MachineConfiguration(roles=["r1"],
+                                                   flavour=constants.FLAVOURS["large"],
+                                                   number=10,
+                                                   cluster="test-cluster"
+                                                   ))
+        conf.finalize()
+        self.assertEqual(1, len(conf.machines))
+        # default networks
+        self.assertEqual(constants.DEFAULT_NETWORKS, conf.networks)
+
+    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"],
+            "cluster": "test-cluster"
+        }
+        conf = MachineConfiguration.from_dictionnary(d)
+        self.assertEqual(constants.DEFAULT_FLAVOUR, conf.flavour)
+
+    def test_from_dictionnary(self):
+        d = {
+            "roles": ["r1"],
+            "flavour": "large",
+            "number": 2,
+            "cluster": "test-cluster"
+        }
+        conf = MachineConfiguration.from_dictionnary(d)
+        self.assertEqual(constants.FLAVOURS["large"], conf.flavour)
+        self.assertEqual(2, conf.number)
diff --git a/enoslib/tests/unit/infra/enos_vmong5k/test_provider.py b/enoslib/tests/unit/infra/enos_vmong5k/test_provider.py
new file mode 100644
index 0000000000000000000000000000000000000000..b28f49149f3009674756ccff5d716668bd922ea5
--- /dev/null
+++ b/enoslib/tests/unit/infra/enos_vmong5k/test_provider.py
@@ -0,0 +1,141 @@
+from enoslib.host import Host
+from enoslib.infra.enos_vmong5k.configuration import Configuration, MachineConfiguration
+from enoslib.infra.enos_vmong5k.provider import (_do_build_g5k_conf,
+                                                 _distribute, _index_by_host,
+                                                 VirtualMachine)
+from enoslib.tests.unit import EnosTest
+
+from netaddr import EUI
+
+import mock
+
+
+class TestBuildG5kConf(EnosTest):
+
+    @mock.patch("enoslib.infra.enos_vmong5k.provider.find_nodes_number", return_value=2)
+    def test_do_build_g5k_conf(self, mock_find_node_number):
+        conf = Configuration()
+        conf.add_machine(roles=["r1"],
+                         cluster="cluster1",
+                         number=10,
+                         flavour="tiny")
+        conf.finalize()
+        g5k_conf = _do_build_g5k_conf(conf, "rennes")
+        # it's valid
+        g5k_conf.finalize()
+
+
+        # machines
+        self.assertEqual(1, len(g5k_conf.machines))
+        machine = g5k_conf.machines[0]
+        self.assertEqual("cluster1", machine.cluster)
+        self.assertEqual(2, machine.nodes)
+        # role have been expanded with the unique cookie
+        self.assertEqual(2, len(machine.roles))
+
+        # networks
+        self.assertEqual(2, len(g5k_conf.networks))
+        self.assertTrue(g5k_conf.networks[0].type in ["prod", "slash_22"])
+        self.assertTrue(g5k_conf.networks[1].type in ["prod", "slash_22"])
+
+
+class TestDistribute(EnosTest):
+
+    def test_distribute_minimal(self):
+        machine = MachineConfiguration(roles=["r1"],
+                                         flavour="tiny",
+                                         cluster="paravance",
+                                         number=1)
+        machines = [machine]
+        host = Host("paravance-1")
+
+        g5k_roles = {
+            "r1": [host],
+            machine.cookie: [host]
+        }
+
+        g5k_subnet = {
+            "mac_start": "00:16:3E:9E:44:00",
+            "mac_end": "00:16:3E:9E:47:FE"
+        }
+
+        vmong5k_roles = _distribute(machines, g5k_roles, g5k_subnet)
+        self.assertEqual(1, len(vmong5k_roles["r1"]))
+        vm = vmong5k_roles["r1"][0]
+        # we skip the first mac 
+        self.assertEqual(EUI(int(EUI(g5k_subnet['mac_start'])) + 1), vm.eui)
+        self.assertEqual(host, vm.pm)
+
+
+    def test_distribute_2_vms_1_host(self):
+        machine = MachineConfiguration(roles=["r1"],
+                                         flavour="tiny",
+                                         cluster="paravance",
+                                         number=2)
+        machines = [machine]
+        host = Host("paravance-1")
+
+        g5k_roles = {
+            "r1": [host],
+            machine.cookie: [host]
+        }
+
+        g5k_subnet = {
+            "mac_start": "00:16:3E:9E:44:00",
+            "mac_end": "00:16:3E:9E:47:FE"
+        }
+
+        vmong5k_roles = _distribute(machines, g5k_roles, g5k_subnet)
+        self.assertEqual(2, len(vmong5k_roles["r1"]))
+        vm = vmong5k_roles["r1"][0]
+        # we skip the first mac 
+        self.assertEqual(EUI(int(EUI(g5k_subnet['mac_start'])) + 1), vm.eui)
+        self.assertEqual(host, vm.pm)
+
+        vm = vmong5k_roles["r1"][1]
+        self.assertEqual(EUI(int(EUI(g5k_subnet['mac_start'])) + 2), vm.eui)
+        self.assertEqual(host, vm.pm)
+
+    def test_distribute_2_vms_2_hosts(self):
+        machine = MachineConfiguration(roles=["r1"],
+                                         flavour="tiny",
+                                         cluster="paravance",
+                                         number=2)
+        machines = [machine]
+        host0 = Host("paravance-1")
+        host1 = Host("paravance-2")
+
+        g5k_roles = {
+            "r1": [host0, host1],
+            machine.cookie: [host0, host1]
+        }
+
+        g5k_subnet = {
+            "mac_start": EUI("00:16:3E:9E:44:00"),
+            "mac_end": EUI("00:16:3E:9E:47:FE")
+        }
+
+        vmong5k_roles = _distribute(machines, g5k_roles, g5k_subnet)
+        self.assertEqual(2, len(vmong5k_roles["r1"]))
+        vm = vmong5k_roles["r1"][0]
+        # we skip the first mac
+        self.assertEqual(EUI(int(g5k_subnet['mac_start']) + 1), vm.eui)
+        self.assertEqual(host0, vm.pm)
+
+        vm = vmong5k_roles["r1"][1]
+        self.assertEqual(EUI(int(g5k_subnet['mac_start']) + 2), vm.eui)
+        self.assertEqual(host1, vm.pm)
+
+
+class TestIndexByHost(EnosTest):
+
+    def test_index_by_host(self):
+        host = Host("paravance-1")
+        machine = VirtualMachine("vm-test",
+                                 EUI("00:16:3E:9E:44:01"),
+                                 {"core": 1, "mem": 512},
+                                 host)
+        roles = {"r1": [machine]}
+        vms_by_host = _index_by_host(roles)
+        self.assertTrue(host.alias in vms_by_host)
+        self.assertEqual(1, len(vms_by_host[host.alias]))