diff --git a/.gitignore b/.gitignore
index 7e94d84..1e5989b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,15 @@
 
 # Vagrant files
 .vagrant
+
+# Configuration files
+clouds.yaml
+ssh_config
+tobiko.conf
+
+# Cache files
+__pycache__
+.pytest_cache
+
+# Log files
+*.log
diff --git a/Vagrantfile b/Vagrantfile
index f781704..f689c40 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -16,7 +16,7 @@ MEMORY = ENV.fetch("VM_SIZE", "8192").to_i
 BOX = ENV.fetch("VM_BOX", "generic/ubuntu2004")
 
 # Machine host name
-HOSTNAME = "tobiko"
+HOSTNAME = "devstack"
 
 # Top vagrantfile dir
 VAGRANTFILE_DIR = File.dirname(__FILE__)
@@ -29,7 +29,8 @@ PROVISION_PLAYBOOK = ENV.fetch(
 PROVISION_DIR = File.dirname(PROVISION_PLAYBOOK)
 
 # Host IP address to be assigned to OpenStack in DevStack
-HOST_IP = "192.168.56.10"
+HOST_NETWORK = "192.168.56"
+TENANT_NETWORK = "192.168.57"
 
 # Local directory from where look for required projects files
 PROJECTS_DIR = File.dirname(ENV.fetch('PROJECTS_DIR', VAGRANTFILE_DIR))
@@ -40,30 +41,16 @@ TOX_ENVLIST = ENV.fetch('TOX_ENVLIST', '')
 TOX_EXTRA_ARGS = ENV.fetch('TOX_EXTRA_ARGS', '--notest')
 
 # Allow to switch configuration
-DEVSTACK_CONF_NAME = ENV.fetch('DEVSTACK_CONF_NAME', 'ovs')
+DEVSTACK_CONF_NAME = ENV.fetch('DEVSTACK_CONF_NAME', 'ovn')
 
 DEVSTACK_LOCAL_CONF_FILE = ENV.fetch(
   'DEVSTACK_LOCAL_CONF_FILE',
   "#{PROVISION_DIR}/#{DEVSTACK_CONF_NAME}/local.conf" )
 
-# Local project directories to be copied
-DEVSTACK_PROJECTS = {
-  # Local directory from where look for devstack project files
-  'devstack' => {
-    'src_dir' => ENV.fetch("DEVSTACK_DIR", "#{PROJECTS_DIR}/devstack"),
-  },
 
-  # Local directory from where look for devstack tobiko plugin project files
-  'devstack-plugin-tobiko' => {
-    'src_dir' => ENV.fetch("DEVSTACK_PLUGIN_TOBIKO_SRC_DIR", VAGRANTFILE_DIR),
-  },
-
-  # Local directory from where looking for Tobiko project files
-  'tobiko' => {
-    'src_dir' => ENV.fetch("TOBIKO_SRC_DIR", "#{PROJECTS_DIR}/tobiko"),
-  },
-
-}
+# Multi-node configuration is still not working. There are issues with setting up
+# networking to be handled.
+NODES_COUNT = 1
 
 
 # All Vagrant configuration is done below. The "2" in Vagrant.configure
@@ -99,7 +86,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
 
   # Create a private network, which allows host-only access to the machine
   # using a specific IP.
-  config.vm.network "private_network", ip: HOST_IP
+  # config.vm.network "private_network", ip: HOST_IP
 
   # Create a public network, which generally matched to bridged network.
   # Bridged networks make the machine appear as another physical device on
@@ -116,30 +103,46 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
   # backing providers for Vagrant. These expose provider-specific options.
   # Example for VirtualBox:
   #
+
+  node_memory = [4096, (MEMORY / NODES_COUNT).to_i].max
+  node_cpus = [4, (CPUS / NODES_COUNT).to_i].max
   config.vm.provider "virtualbox" do |vb|
     # Display the VirtualBox GUI when booting the machine
     vb.gui = false
-
-    vb.cpus = CPUS
-    vb.memory = MEMORY
+    vb.cpus = node_cpus
+    vb.memory = node_memory
   end
 
   config.vm.provider "libvirt" do |libvirt|
     libvirt.qemu_use_session = false
-    libvirt.cpus = CPUS
-    libvirt.memory =  MEMORY
+    libvirt.cpus = node_cpus
+    libvirt.memory = node_memory
   end
 
-  # Run provision playbook
-  config.vm.provision "ansible" do |ansible|
-    ansible.limit = 'all'
-    ansible.playbook = PROVISION_PLAYBOOK
-    ansible.extra_vars = ansible.extra_vars = {
-      'devstack_projects' => DEVSTACK_PROJECTS,
-      'devstack_local_conf_file' => DEVSTACK_LOCAL_CONF_FILE,
-      'tox_envlist' => TOX_ENVLIST,
-      'tox_extra_args' => TOX_EXTRA_ARGS,
-    }
+  for node_id in 0..(NODES_COUNT - 2)
+    secondary_hostname =  "#{HOSTNAME}-secondary-#{node_id}"
+    config.vm.define secondary_hostname, primary: false do |secondary|
+      secondary.vm.hostname = secondary_hostname
+      secondary.vm.network "private_network", ip: "#{HOST_NETWORK}.#{20 + node_id}"
+      secondary.vm.network "private_network", ip: "#{TENANT_NETWORK}.#{20 + node_id}"
+    end
   end
 
+  primary_hostname = "#{HOSTNAME}-primary"
+  config.vm.define "devstack-primary", primary: true do |primary|
+    primary.vm.hostname = primary_hostname
+    primary.vm.network "private_network", ip: "#{HOST_NETWORK}.10"
+    primary.vm.network "private_network", ip: "#{TENANT_NETWORK}.10"
+
+    # Run provision playbook
+    primary.vm.provision "ansible" do |ansible|
+      ansible.limit = 'all'
+      ansible.playbook = PROVISION_PLAYBOOK
+      ansible.extra_vars = ansible.extra_vars = {
+        'devstack_local_conf_file' => DEVSTACK_LOCAL_CONF_FILE,
+        'tox_envlist' => TOX_ENVLIST,
+        'tox_extra_args' => TOX_EXTRA_ARGS,
+      }
+    end
+  end
 end
diff --git a/playbooks/vagrant/ovn/local.conf b/playbooks/vagrant/ovn/local.conf
index 4dbbc9b..758d9fe 100644
--- a/playbooks/vagrant/ovn/local.conf
+++ b/playbooks/vagrant/ovn/local.conf
@@ -1,14 +1,55 @@
 [[local|localrc]]
 ADMIN_PASSWORD=secret
-DATABASE_PASSWORD=$ADMIN_PASSWORD
+DATABASE_PASSWORD=${ADMIN_PASSWORD}
 KEYSTONE_ADMIN_ENDPOINT=True
-RABBIT_PASSWORD=$ADMIN_PASSWORD
-SERVICE_PASSWORD=$ADMIN_PASSWORD
+RABBIT_PASSWORD=${ADMIN_PASSWORD}
+SERVICE_PASSWORD=${ADMIN_PASSWORD}
+
+DATABASE_TYPE=mysql
 
 LOGFILE=/opt/stack/devstack/stack.log
 VERBOSE=True
 LOG_COLOR=True
-MULTI_HOST="0"
+MULTI_HOST=1
+
+IP_VERSION=4
+HOST_IP={{ devstack_host_ip }}
+# FLOATING_RANGE=192.168.56.128/25
+SERVICE_HOST={{ devstack_primary_host_ip }}
+MYSQL_HOST=${SERVICE_HOST}
+RABBIT_HOST=${SERVICE_HOST}
+GLANCE_HOSTPORT=${SERVICE_HOST}:9292
+
+# ENABLE_CHASSIS_AS_GW=True
+ENABLE_TLS=False
+KEYSTONE_ADMIN_ENDPOINT=True
+ML2_L3_PLUGIN=ovn-router,trunk
+OVN_BUILD_MODULES=False
+OVN_DBS_LOG_LEVEL=dbg
+OVN_IGMP_SNOOPING_ENABLE=True
+OVN_L3_CREATE_PUBLIC_NETWORK=True
+# PHYSICAL_NETWORK=public
+# PUBLIC_INTERFACE={{ devstack_public_interface }}
+Q_AGENT=ovn
+Q_ML2_PLUGIN_MECHANISM_DRIVERS=ovn,logger
+Q_ML2_PLUGIN_TYPE_DRIVERS=local,flat,vlan,geneve
+Q_ML2_TENANT_NETWORK_TYPE=geneve
+Q_USE_PROVIDERNET_FOR_PUBLIC=True
+
+# Whether or not to build custom openvswitch kernel modules from the ovs git
+# tree. This is disabled by default.  This is required unless your distro kernel
+# includes ovs+conntrack support.  This support was first released in Linux 4.3,
+# and will likely be backported by some distros.
+# NOTE(mjozefcz): We need to compile the module for Ubuntu Bionic, because default
+# shipped kernel module doesn't openflow meter action support.
+OVN_BUILD_MODULES=False
+
+GLANCE_ENABLE_QUOTAS=False
+TOBIKO_NEUTRON_IPV4_DNS_NAMESERVERS=1.1.1.1,8.8.8.8
+
+
+{% if devstack_primary %}
+# Devstack primary node =======================================================
 
 # Enable required services ----------------------------------------------------
 enable_service key
@@ -57,41 +98,12 @@ disable_service q-l3
 disable_service q-dhcp
 disable_service q-meta
 
-HOST_IP=192.168.56.10
-IP_VERSION=4
-
-# ENABLE_CHASSIS_AS_GW=True
-ENABLE_TLS=False
-KEYSTONE_ADMIN_ENDPOINT=True
-ML2_L3_PLUGIN=ovn-router,trunk
-OVN_BUILD_MODULES=False
-OVN_DBS_LOG_LEVEL=dbg
-OVN_IGMP_SNOOPING_ENABLE=True
-OVN_L3_CREATE_PUBLIC_NETWORK=True
-# PHYSICAL_NETWORK=public
-# PUBLIC_INTERFACE=eth0
-Q_AGENT=ovn
-Q_ML2_PLUGIN_MECHANISM_DRIVERS=ovn,logger
-Q_ML2_PLUGIN_TYPE_DRIVERS=local,flat,vlan,geneve
-Q_ML2_TENANT_NETWORK_TYPE=geneve
-Q_USE_PROVIDERNET_FOR_PUBLIC=True
-
-# Whether or not to build custom openvswitch kernel modules from the ovs git
-# tree. This is disabled by default.  This is required unless your distro kernel
-# includes ovs+conntrack support.  This support was first released in Linux 4.3,
-# and will likely be backported by some distros.
-# NOTE(mjozefcz): We need to compile the module for Ubuntu Bionic, because default
-# shipped kernel module doesn't openflow meter action support.
-OVN_BUILD_MODULES=False
-
-
 # Configure Horizon -----------------------------------------------------------
 disable_service horizon
 
 # Configure Glance ------------------------------------------------------------
-enable_service g-api
 
-GLANCE_ENABLE_QUOTAS="False"
+enable_service g-api
 
 # Configure Cinder ------------------------------------------------------------
 enable_service c-api
@@ -106,4 +118,10 @@ enable_plugin heat https://opendev.org/openstack/heat.git
 # Configure Tobiko ------------------------------------------------------------
 enable_plugin devstack-plugin-tobiko https://opendev.org/x/devstack-plugin-tobiko.git
 
-TOBIKO_NEUTRON_IPV4_DNS_NAMESERVERS=1.1.1.1,8.8.8.8
+{% elif devstack_secondary %}
+# Devstack secondary node =====================================================
+
+enable_plugin neutron https://opendev.org/openstack/neutron.git
+ENABLED_SERVICES=n-cpu,c-vol,placement-client,ovn-controller,ovs-vswitchd,ovsdb-server,q-ovn-metadata-agent
+
+{% endif %}
diff --git a/playbooks/vagrant/provision.yaml b/playbooks/vagrant/provision.yaml
index cd01592..5a373e5 100644
--- a/playbooks/vagrant/provision.yaml
+++ b/playbooks/vagrant/provision.yaml
@@ -1,44 +1,23 @@
 ---
 
 - hosts: all
-  vars:
-    resolv_conf_file: /etc/resolv.conf
-    dest_dir: /opt/stack
+  roles:
+    - devstack-tobiko-vagrant
 
-  pre_tasks:
-
-    - name: copy '{{ resolv_conf_file}}' file
-      become: true
-      copy:
-        src: '{{ resolv_conf_file }}'
-        dest: /etc/resolv.conf
-        owner: root
-        group: root
-        mode: '0644'
-
-    - name: update APT database
-      apt:
-        update_cache: true
-        cache_valid_time: 3600
-      become: true
-      when:
-        - ansible_os_family == 'Debian'
-
-    - become: true
-      when:
-        - ansible_distribution == "CentOS"
-        - ansible_distribution_major_version == "8"
-      block:
-        - name: Switch from CentOS Linux repos to Centos Stream repos
-          command: dnf -y swap centos-linux-repos centos-stream-repos
-          args:
-            warn: false
-
-        - name: Switch packages from CentOS Linux to to Centos Stream
-          command: dnf -y distro-sync
-          args:
-            warn: false
 
+- hosts: devstack-primary
   roles:
     - devstack-tobiko-deploy
-    - devstack-tobiko-run-tests
+    - get-clouds-file
+    - get-vagrant-ssh-config
+  vars:
+    devstack_primary: true
+    devstack_plugin_tobiko_src_dir: >-
+      {{ playbook_dir | realpath | dirname | dirname  }}
+
+
+- hosts: devstack-secondary-*
+  roles:
+    - role: devstack-tobiko-deploy
+      vars:
+        devstack_primary: false
diff --git a/roles/devstack-tobiko-common/defaults/main.yaml b/roles/devstack-tobiko-common/defaults/main.yaml
index b8b7bb5..df35914 100644
--- a/roles/devstack-tobiko-common/defaults/main.yaml
+++ b/roles/devstack-tobiko-common/defaults/main.yaml
@@ -7,17 +7,14 @@ devstack_local_conf_file: '{{ playbook_dir }}/local.conf'
 
 devstack_projects_dir: '{{ playbook_dir | dirname }}'
 
-devstack_projects_base:
-  devstack:
-    git_repo: 'https://opendev.org/openstack/devstack.git'
-  devstack-plugin-tobiko:
-    git_repo: 'https://opendev.org/x/devstack-plugin-tobiko.git'
-  tobiko:
-    git_repo: 'https://opendev.org/x/tobiko.git'
-
-devstack_projects: {}
-
 devstack_dir: '{{ devstack_dest_dir }}/devstack'
+devstack_git_repo: 'https://opendev.org/openstack/devstack.git'
+
+devstack_plugin_tobiko_dir: '{{ devstack_dest_dir }}/devstack-plugin-tobiko'
+devstack_plugin_tobiko_src_dir: ''
+
+tobiko_config_path: '{{ devstack_plugin_tobiko_src_dir }}/tobiko.conf'
+tobiko_config_dir: '{{ tobiko_config_path | realpath | dirname }}'
 
 sudo_secure_path: ''
 
diff --git a/roles/devstack-tobiko-deploy/defaults/main.yaml b/roles/devstack-tobiko-deploy/defaults/main.yaml
index 2997e9f..de3a293 100644
--- a/roles/devstack-tobiko-deploy/defaults/main.yaml
+++ b/roles/devstack-tobiko-deploy/defaults/main.yaml
@@ -3,3 +3,8 @@
 force_restack: false
 stack_succeeded_file: '{{ devstack_dir }}/SUCCEEDED'
 swap_file_size: 8192
+devstack_primary: true
+devstack_secondary: '{{ not devstack_primary }}'
+devstack_host_interface: eth1
+devstack_primary_host_ip: 192.168.56.10
+devstack_public_interface: eth2
diff --git a/roles/devstack-tobiko-deploy/tasks/copy-devstack-plugin-tobiko.yaml b/roles/devstack-tobiko-deploy/tasks/copy-devstack-plugin-tobiko.yaml
new file mode 100644
index 0000000..3be6b96
--- /dev/null
+++ b/roles/devstack-tobiko-deploy/tasks/copy-devstack-plugin-tobiko.yaml
@@ -0,0 +1,33 @@
+---
+
+- name: consolidate file paths
+  set_fact:
+    devstack_plugin_tobiko_dir: >-
+      {{ devstack_plugin_tobiko_dir | realpath }}
+    devstack_plugin_tobiko_src_dir: >-
+      {{ devstack_plugin_tobiko_src_dir | realpath }}
+
+
+- name: ensure '{{ devstack_plugin_tobiko_dir }}' exists
+  become: true
+  become_user: root
+  file:
+    path: '{{ devstack_plugin_tobiko_dir }}'
+    state: directory
+    mode: '0755'
+    owner: stack
+    group: stack
+
+
+- name: copy '{{ devstack_plugin_tobiko_src_dir }}'
+  synchronize:
+    group: false
+    owner: false
+    src: '{{ devstack_plugin_tobiko_src_dir }}/.'
+    dest: '{{ devstack_plugin_tobiko_dir }}'
+    use_ssh_args: true
+    recursive: true
+    rsync_opts:
+      - '--exclude-from={{ devstack_plugin_tobiko_src_dir }}/.gitignore'
+  become: true
+  become_user: stack
diff --git a/roles/devstack-tobiko-deploy/tasks/deploy-local-conf.yaml b/roles/devstack-tobiko-deploy/tasks/deploy-local-conf.yaml
index 825debd..27c9f1f 100644
--- a/roles/devstack-tobiko-deploy/tasks/deploy-local-conf.yaml
+++ b/roles/devstack-tobiko-deploy/tasks/deploy-local-conf.yaml
@@ -1,8 +1,18 @@
 ---
 
+- name: get {{ devstack_host_interface }} host IP
+  shell: >-
+    ip addr list {{ devstack_host_interface }} | grep 'inet ' | awk '{print $2}'
+  register: get_devstack_host_ip
+
+- name: parse host IP
+  set_fact:
+    devstack_host_ip: >-
+      {{ get_devstack_host_ip.stdout.split('/', 1)[0] }}
+
 - name: copy local.conf file
   become: true
-  copy:
+  template:
     owner: stack
     group: stack
     src: '{{ devstack_local_conf_file }}'
diff --git a/roles/devstack-tobiko-deploy/tasks/deploy-project.yaml b/roles/devstack-tobiko-deploy/tasks/deploy-project.yaml
deleted file mode 100644
index 19a144c..0000000
--- a/roles/devstack-tobiko-deploy/tasks/deploy-project.yaml
+++ /dev/null
@@ -1,52 +0,0 @@
----
-
-- name: "ensure '{{ project_dest_dir }}' exists"
-  become: true
-  become_user: root
-  file:
-    path: '{{ project_dest_dir | realpath }}'
-    state: directory
-    mode: '0755'
-    owner: stack
-    group: stack
-  when: >-
-    ( project_src_dir | length) > 0 or
-    ( project_git_repo | length) > 0
-
-
-- name: "check '{{ project_src_dir }}' exists"
-  stat:
-    path: '{{ project_src_dir }}'
-  delegate_to: localhost
-  register: check_project_src_dir_exists
-  when: ( project_src_dir | length) > 0
-
-
-- become: true
-  become_user: stack
-  block:
-
-    - name: copy '{{ project_src_dir }}' to '{{ project_dest_dir }}'
-      synchronize:
-        group: false
-        owner: false
-        src: "{{ project_src_dir | realpath }}/."
-        dest: "{{ project_dest_dir | realpath }}"
-        use_ssh_args: true
-        recursive: true
-        rsync_opts:
-          - '--exclude-from={{ project_src_dir | realpath }}/.gitignore'
-      register: copy_project_src_dir
-      when: check_project_src_dir_exists.stat.isdir | default(False)
-
-    - name: >-
-        fetch project sources from '{{ project_git_repo }}' to
-        '{{ project_dest_dir }}'
-      git:
-        repo: '{{ project_git_repo }}'
-        dest: '{{ project_dest_dir }}'
-        version: '{{ project_git_version }}'
-        force: true
-      when:
-        - copy_project_src_dir is skipped
-        - ( project_git_repo | length) > 0
diff --git a/roles/devstack-tobiko-deploy/tasks/deploy-projects.yaml b/roles/devstack-tobiko-deploy/tasks/deploy-projects.yaml
deleted file mode 100644
index a1aec7a..0000000
--- a/roles/devstack-tobiko-deploy/tasks/deploy-projects.yaml
+++ /dev/null
@@ -1,21 +0,0 @@
----
-
-- name: combine DevStack projects
-  set_fact:
-    devstack_projects_combined: >-
-      {{ [devstack_projects_base, devstack_projects] |
-         combine(recursive=True) }}
-
-
-- name: show resulting DevStack projects
-  debug: var=devstack_projects_combined
-
-
-- name: deploy projects
-  include_tasks: deploy-project.yaml
-  with_dict: '{{ devstack_projects_combined }}'
-  vars:
-    project_dest_dir: '{{ devstack_dest_dir }}/{{ item.key }}'
-    project_src_dir: '{{ item.value.src_dir | default("") }}'
-    project_git_repo: '{{ item.value.git_repo | default("") }}'
-    project_git_version: '{{ item.value.git_version | default("HEAD") }}'
diff --git a/roles/devstack-tobiko-deploy/tasks/get-devstack.yaml b/roles/devstack-tobiko-deploy/tasks/get-devstack.yaml
new file mode 100644
index 0000000..c4606a0
--- /dev/null
+++ b/roles/devstack-tobiko-deploy/tasks/get-devstack.yaml
@@ -0,0 +1,20 @@
+---
+
+- name: ensure '{{ devstack_dir }}' exists
+  become: true
+  become_user: root
+  file:
+    path: '{{ devstack_dir | realpath }}'
+    state: directory
+    mode: '0755'
+    owner: stack
+    group: stack
+
+
+- name: checkout devstack files from '{{ devstack_git_repo }}'
+  git:
+    repo: '{{ devstack_git_repo }}'
+    dest: '{{ devstack_dir | realpath }}'
+    update: no
+  become: true
+  become_user: stack
diff --git a/roles/devstack-tobiko-deploy/tasks/install-bindeps.yaml b/roles/devstack-tobiko-deploy/tasks/install-bindeps.yaml
index 5e01808..9db8338 100644
--- a/roles/devstack-tobiko-deploy/tasks/install-bindeps.yaml
+++ b/roles/devstack-tobiko-deploy/tasks/install-bindeps.yaml
@@ -1,9 +1,10 @@
 ---
 
-- name: "ensure DevStack bindeps are installed"
+- name: "ensure required packages are installed"
   become: true
   package:
     name:
+      - acl
       - git
       - iptables
       - python3
diff --git a/roles/devstack-tobiko-deploy/tasks/main.yaml b/roles/devstack-tobiko-deploy/tasks/main.yaml
index 74f9ea3..35b3bd9 100644
--- a/roles/devstack-tobiko-deploy/tasks/main.yaml
+++ b/roles/devstack-tobiko-deploy/tasks/main.yaml
@@ -4,7 +4,10 @@
 - include_tasks: install-bindeps.yaml
 - include_tasks: ensure-stack-user.yaml
 - include_tasks: run-unstack.yaml
-- include_tasks: deploy-projects.yaml
+- include_tasks: get-devstack.yaml
+- include_tasks: copy-devstack-plugin-tobiko.yaml
+  when: devstack_plugin_tobiko_src_dir
 - include_tasks: deploy-local-conf.yaml
 - include_tasks: run-stack.yaml
 - include_tasks: setup-profile.yaml
+- include_tasks: register-compute-node.yaml
diff --git a/roles/devstack-tobiko-deploy/tasks/register-compute-node.yaml b/roles/devstack-tobiko-deploy/tasks/register-compute-node.yaml
new file mode 100644
index 0000000..9b89021
--- /dev/null
+++ b/roles/devstack-tobiko-deploy/tasks/register-compute-node.yaml
@@ -0,0 +1,31 @@
+---
+
+- name: wait for nova service registration
+  shell: |
+    export OS_CLOUD=devstack-admin
+    openstack compute service list -f value -c Host | grep $(hostname)
+  until: get_compute_service_list is not failed
+  retries: 30
+  delay: 5
+  register: get_compute_service_list
+  changed_when: false
+
+- name: run tools/discover_hosts.sh
+  shell:
+    cmd: >-
+      ./tools/discover_hosts.sh
+    chdir: '{{ devstack_dir }}'
+  become: true
+  become_user: stack
+  delegate_to: devstack-primary
+
+- name: wait for nova hypervisor registration
+  shell: |
+    export OS_CLOUD=devstack-admin
+    openstack hypervisor list -f value -c 'Hypervisor Hostname' |\
+      grep $(hostname)
+  until: get_compute_service_list is not failed
+  retries: 3
+  delay: 5
+  register: get_compute_service_list
+  changed_when: false
diff --git a/roles/devstack-tobiko-vagrant/defaults/main.yaml b/roles/devstack-tobiko-vagrant/defaults/main.yaml
new file mode 100644
index 0000000..44f293d
--- /dev/null
+++ b/roles/devstack-tobiko-vagrant/defaults/main.yaml
@@ -0,0 +1,3 @@
+---
+
+resolv_conf_file: /etc/resolv.conf
diff --git a/roles/devstack-tobiko-vagrant/tasks/main.yaml b/roles/devstack-tobiko-vagrant/tasks/main.yaml
new file mode 100644
index 0000000..a482a94
--- /dev/null
+++ b/roles/devstack-tobiko-vagrant/tasks/main.yaml
@@ -0,0 +1,33 @@
+---
+
+- name: copy '{{ resolv_conf_file}}' file
+  become: true
+  copy:
+    src: '{{ resolv_conf_file }}'
+    dest: /etc/resolv.conf
+    owner: root
+    group: root
+    mode: '0644'
+
+- name: update APT database
+  apt:
+    update_cache: true
+    cache_valid_time: 3600
+  become: true
+  when:
+    - ansible_os_family == 'Debian'
+
+- become: true
+  when:
+    - ansible_distribution == "CentOS"
+    - ansible_distribution_major_version == "8"
+  block:
+    - name: Switch from CentOS Linux repos to Centos Stream repos
+      command: dnf -y swap centos-linux-repos centos-stream-repos
+      args:
+        warn: false
+
+    - name: Switch packages from CentOS Linux to to Centos Stream
+      command: dnf -y distro-sync
+      args:
+        warn: false
diff --git a/roles/get-clouds-file/defaults/main.yaml b/roles/get-clouds-file/defaults/main.yaml
new file mode 100644
index 0000000..e7b901a
--- /dev/null
+++ b/roles/get-clouds-file/defaults/main.yaml
@@ -0,0 +1,4 @@
+---
+
+devstack_clouds_file_path: /etc/openstack/clouds.yaml
+tobiko_clouds_file_path: '{{ tobiko_config_dir }}/clouds.yaml'
diff --git a/roles/get-clouds-file/meta/main.yaml b/roles/get-clouds-file/meta/main.yaml
new file mode 100644
index 0000000..13a388c
--- /dev/null
+++ b/roles/get-clouds-file/meta/main.yaml
@@ -0,0 +1,4 @@
+---
+
+dependencies:
+  - devstack-tobiko-common
diff --git a/roles/get-clouds-file/tasks/get-clouds-file.yaml b/roles/get-clouds-file/tasks/get-clouds-file.yaml
new file mode 100644
index 0000000..9c47088
--- /dev/null
+++ b/roles/get-clouds-file/tasks/get-clouds-file.yaml
@@ -0,0 +1,40 @@
+---
+
+- name: Read {{ devstack_clouds_file_path }} file
+  slurp:
+    src: '{{ devstack_clouds_file_path }}'
+  register: read_devstack_clouds_file
+
+
+- name: Write clouds file to '{{ tobiko_clouds_file_path }}'
+  copy:
+    content: '{{ read_devstack_clouds_file.content | b64decode }}'
+    dest: '{{ tobiko_clouds_file_path }}'
+  delegate_to: localhost
+
+
+- name: Write clouds file dir to {{ tobiko_config_path }}
+  ini_file:
+    path: '{{ tobiko_config_path }}'
+    section: keystone
+    option: clouds_file_dirs
+    value: '{{ tobiko_clouds_file_path | dirname }}'
+  delegate_to: localhost
+
+
+- name: Write clouds file name to {{ tobiko_config_path }}
+  ini_file:
+    path: '{{ tobiko_config_path }}'
+    section: keystone
+    option: clouds_file_names
+    value: '{{ tobiko_clouds_file_path | basename }}'
+  delegate_to: localhost
+
+
+- name: Write cloud name dir to {{ tobiko_config_path }}
+  ini_file:
+    path: '{{ tobiko_config_path }}'
+    section: keystone
+    option: cloud_name
+    value: devstack-admin
+  delegate_to: localhost
diff --git a/roles/get-clouds-file/tasks/main.yaml b/roles/get-clouds-file/tasks/main.yaml
new file mode 100644
index 0000000..81f0e2a
--- /dev/null
+++ b/roles/get-clouds-file/tasks/main.yaml
@@ -0,0 +1,3 @@
+---
+
+- include_tasks: get-clouds-file.yaml
diff --git a/roles/get-vagrant-ssh-config/defaults/main.yaml b/roles/get-vagrant-ssh-config/defaults/main.yaml
new file mode 100644
index 0000000..c28f263
--- /dev/null
+++ b/roles/get-vagrant-ssh-config/defaults/main.yaml
@@ -0,0 +1,3 @@
+---
+
+tobiko_ssh_config_path: '{{ tobiko_config_dir }}/ssh_config'
diff --git a/roles/get-vagrant-ssh-config/meta/main.yaml b/roles/get-vagrant-ssh-config/meta/main.yaml
new file mode 100644
index 0000000..13a388c
--- /dev/null
+++ b/roles/get-vagrant-ssh-config/meta/main.yaml
@@ -0,0 +1,4 @@
+---
+
+dependencies:
+  - devstack-tobiko-common
diff --git a/roles/get-vagrant-ssh-config/tasks/get-ssh-config.yaml b/roles/get-vagrant-ssh-config/tasks/get-ssh-config.yaml
new file mode 100644
index 0000000..6496b40
--- /dev/null
+++ b/roles/get-vagrant-ssh-config/tasks/get-ssh-config.yaml
@@ -0,0 +1,38 @@
+---
+
+- delegate_to: localhost
+  block:
+
+  - name: ensure '{{ tobiko_ssh_config_path | dirname }}' exists
+    file:
+      path: '{{ tobiko_ssh_config_path | dirname }}'
+      state: directory
+
+  - name: get ssh_config content'
+    shell:
+      cmd: vagrant ssh-config
+      chdir: '{{ devstack_plugin_tobiko_src_dir }}'
+    register: get_ssh_config
+    changed_when: false
+
+  - debug: var=get_ssh_config
+
+  - name: write ssh_config to file '{{ tobiko_ssh_config_path }}'
+    copy:
+      content: |
+        {{ get_ssh_config.stdout }}
+      dest: '{{ tobiko_ssh_config_path }}'
+
+  - name: write ssh_config file path to {{ tobiko_config_path }}
+    ini_file:
+      path: '{{ tobiko_config_path }}'
+      section: ssh
+      option: config_files
+      value: '{{ tobiko_ssh_config_path }}'
+
+  - name: write SSH proxy jump host to {{ tobiko_config_path }}
+    ini_file:
+      path: '{{ tobiko_config_path }}'
+      section: ssh
+      option: proxy_jump
+      value: devstack-primary
diff --git a/roles/get-vagrant-ssh-config/tasks/main.yaml b/roles/get-vagrant-ssh-config/tasks/main.yaml
new file mode 100644
index 0000000..0eb8a49
--- /dev/null
+++ b/roles/get-vagrant-ssh-config/tasks/main.yaml
@@ -0,0 +1,3 @@
+---
+
+- include_tasks: get-ssh-config.yaml
diff --git a/test-requirements.txt b/test-requirements.txt
new file mode 100644
index 0000000..4ac0a7d
--- /dev/null
+++ b/test-requirements.txt
@@ -0,0 +1,14 @@
+ansi2html                      # LGPLv3+
+dpkt                           # BSD
+pandas                         # BSD
+podman                         # Apache-2.0
+
+pytest                         # MIT
+pytest-cov                     # MIT
+pytest-html                    # MPL-2.0
+pytest-reportportal            # Apache-2.0
+pytest-rerunfailures           # MPL-2.0
+pytest-timeout                 # MIT
+pytest-xdist[psutil]           # MIT
+tobiko                         # Apache-2.0
+varlink                        # Apache-2.0
diff --git a/tests/functional/test_server.py b/tests/functional/test_server.py
new file mode 100644
index 0000000..d1b2387
--- /dev/null
+++ b/tests/functional/test_server.py
@@ -0,0 +1,38 @@
+# Copyright (c) 2019 Red Hat
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+from __future__ import absolute_import
+
+import pytest
+
+import tobiko
+from tobiko.openstack import stacks
+from tobiko.shell import ping
+from tobiko.shell import sh
+
+
+@pytest.fixture
+def server_stack() -> stacks.ServerStackFixture:
+    return tobiko.setup_fixture(stacks.CirrosServerStackFixture)
+
+
+def test_ssh(server_stack: stacks.ServerStackFixture):
+    """Test SSH connectivity to floating IP address"""
+    hostname = sh.ssh_hostname(ssh_client=server_stack.ssh_client)
+    assert server_stack.server_name.lower() == hostname
+
+
+def test_ping(server_stack: stacks.ServerStackFixture):
+    """Test ICMP connectivity to floating IP address"""
+    ping.assert_reachable_hosts([server_stack.floating_ip_address])
diff --git a/tests/functional/test_topology.py b/tests/functional/test_topology.py
new file mode 100644
index 0000000..97d04f6
--- /dev/null
+++ b/tests/functional/test_topology.py
@@ -0,0 +1,32 @@
+# Copyright (c) 2019 Red Hat
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+from __future__ import absolute_import
+
+from tobiko.openstack import topology
+from tobiko.shell import ping
+from tobiko.shell import sh
+
+
+def test_ssh():
+    """Test SSH connectivity to devstack nodes"""
+    for node in topology.list_openstack_nodes():
+        assert node.name == sh.ssh_hostname(ssh_client=node.ssh_client)
+
+
+def test_ping():
+    """Test ICMP connectivity to devstack nodes"""
+    ips = [node.public_ip
+           for node in topology.list_openstack_nodes()]
+    ping.assert_reachable_hosts(ips)
diff --git a/tobiko.conf.example b/tobiko.conf.example
new file mode 100644
index 0000000..5af3f10
--- /dev/null
+++ b/tobiko.conf.example
@@ -0,0 +1,8 @@
+
+[keystone]
+clouds_file_dirs = .
+clouds_file_names = clouds.yaml
+
+[ssh]
+config_files = ssh_config
+proxy_jump = devstack-primary
diff --git a/tox.ini b/tox.ini
index 1262c60..a917a11 100644
--- a/tox.ini
+++ b/tox.ini
@@ -26,3 +26,12 @@ deps =
 changedir = doc/source
 commands =
     sphinx-build -W -b html . ../build/html
+
+
+[testenv:functional]
+deps =
+    -r test-requirements.txt
+setenv =
+    TESTS_DIR = {toxinidir}/tests/functional
+commands =
+    pytest {posargs:{env:TESTS_DIR}}