diff --git a/devstack/discovery_exercise.sh b/devstack/discovery_exercise.sh
new file mode 100755
index 000000000..b368c55ba
--- /dev/null
+++ b/devstack/discovery_exercise.sh
@@ -0,0 +1,84 @@
+#!/bin/bash
+
+set -eux
+
+# Import help functions
+IRONIC_INSPECTOR_DEVSTACK_PLUGIN_DIR=$(cd $(dirname "${BASH_SOURCE:-$0}") && pwd)
+source ${IRONIC_INSPECTOR_DEVSTACK_PLUGIN_DIR}/exercise_common.sh
+
+# this exercise destroys BM nodes
+# precaution measures
+assert_sudo
+hook=$(get_ini $IRONIC_INSPECTOR_CONF_FILE processing node_not_found_hook) || {
+ echo "Please, enable node_not_found_hook in processing section of inspector.conf"
+ exit 1
+}
+
+if [ -z "$hook" ] ; then
+ echo "Please, provide a value for node_not_found_hook in processing section of inspector.conf"
+ exit 1
+fi
+
+nodes=$(node_list)
+if [ -z "$nodes" ]; then
+ echo "No nodes found in Ironic"
+ exit 1
+fi
+
+# Choose one ironic node for discover
+discover_uuid=
+for uuid in $nodes; do
+ provision_state=$(node_attribute $uuid provision_state)
+ if [[ $provision_state = "available" ]] || [[ $provision_state = "enroll" ]] ; then
+ discover_uuid=$uuid
+ break
+ fi
+done
+
+if [ -z "$discover_uuid" ] ; then
+ echo "No nodes in available provisioning state"
+ exit 1
+fi
+
+# Get node details before delete it
+node_name=$(node_attribute $discover_uuid name)
+node_driver=$(node_attribute $discover_uuid driver)
+node_mac=$(node_mac $discover_uuid)
+declare -A driver_info
+node_driver_info $discover_uuid driver_info
+
+# create temporary discovery rule
+discovery_rule=$(mktemp)
+node_discovery_rule $node_name $node_driver driver_info > "$discovery_rule"
+
+echo "Purging introspection rules; importing custom rules"
+openstack baremetal introspection rule purge
+openstack baremetal introspection rule import "$discovery_rule"
+
+# get virsh node uuid
+virsh_uuid=$(node_to_virsh_uuid $discover_uuid)
+
+# delete&rediscover node
+echo "Delete Ironic node $discover_uuid (and ports) for discovery"
+ironic node-delete $discover_uuid
+wait_for 120 ! assert_mac_blacklisted $node_mac
+
+# Start vm's for discover
+echo "booting virsh $virsh_uuid domain to be discovered"
+sudo virsh start $virsh_uuid
+
+echo "waiting for discovered node to appear"
+discovered_node=
+wait_for 900 node_exists $node_name discovered_node
+
+echo "waiting for introspection to finish"
+wait_for 900 assert_node_introspection_status $discovered_node
+
+# validate discovery result
+validate_node_flavor $discovered_node baremetal
+assert_equal $node_driver $(node_attribute $discovered_node driver)
+validate_node_driver_info $discovered_node driver_info
+
+rm -f $discovery_rule
+
+echo "Validation passed"
diff --git a/devstack/exercise_common.sh b/devstack/exercise_common.sh
new file mode 100644
index 000000000..21790e373
--- /dev/null
+++ b/devstack/exercise_common.sh
@@ -0,0 +1,312 @@
+#!/bin/bash
+
+IRONIC_INSPECTOR_CONF_FILE="/etc/ironic-inspector/inspector.conf"
+
+function assert_sudo {
+ # make sure sudo works in non-interactive mode
+ if ! sudo -n true ; then
+ echo "ERROR: sudo doesn't work"
+ return 1
+ fi
+}
+
+function token_issue {
+ openstack token issue -f value -c id
+}
+
+function endpoint_url {
+ local endpoint=${1:?endpoint not specified}
+ openstack endpoint show ${endpoint} -f value -c adminurl
+}
+
+function auth_curl {
+ local url=${1:?url not specified} ; shift
+ local method=${1:-GET} ; shift
+ local token=$(token_issue)
+
+ curl -H "X-Auth-Token: $token" -X ${method} ${url} ${@}
+}
+
+function curl_ironic {
+ local url=${1:?url not specified} ; shift
+ local method=${1:-GET} ; shift
+
+ auth_curl "$(endpoint_url baremetal)/${url#/}" ${method} ${@}
+}
+
+function node_list {
+ openstack baremetal list -f value -c UUID
+}
+
+function node_attribute {
+ local uuid=${1:?uuid not specified}
+ local attribute=${2:?attribute not specified}
+
+ openstack baremetal show ${uuid} -f value -c ${attribute}
+}
+
+function json_query {
+ local data_name=${1:?data variable name not specified}; shift
+ local var_name=${1:?variable name not specified}; shift
+ local key=${1:?key not specified}; shift
+ local query=$@
+
+ local tmp=$(jq ${query} <<<${!data_name})
+ eval ${var_name}[${key}]=${tmp}
+}
+
+function virsh_domains {
+
+ # Id Name State
+ #----------------------------------------------------
+ # - baremetalbrbm_0 shut off
+ #
+ sudo -n virsh list --all | tail -n+3 | awk '{print $2;}' | head -n-1
+}
+
+function virsh_domain_mac {
+ local domain=${1:?domain not specified}
+
+ # ....
+ #
+ # ....
+ sudo -n virsh dumpxml $domain | grep 'mac address' | cut -d \' -f2
+}
+
+function node_mac {
+ local uuid=${1:?uuid not specified}
+
+ # +--------------------------------------+-------------------+
+ # | UUID | Address |
+ # +--------------------------------------+-------------------+
+ # | 4d734b98-bae9-43a7-ba27-8dbdce2b0bf1 | 52:54:00:df:96:0c |
+ # +--------------------------------------+-------------------+
+ ironic node-port-list $uuid | tail -n+4 | head -n+1 | head -1 | tr -d \| | awk '{print $2;}'
+}
+
+function node_to_virsh_uuid {
+ local uuid=${1:?uuid not specified}
+ local node_mac=$(node_attribute $uuid mac_address)
+ local map
+ local node
+ local domain
+
+ declare -A map
+
+ for node in $(node_list) ; do
+ map[$(node_mac $node)]=$node
+ done
+
+ for domain in $(virsh_domains) ; do
+ if [[ ${map[$(virsh_domain_mac $domain)]} = $uuid ]] ; then
+ echo $domain
+ return
+ fi
+ done
+ return 1
+}
+
+function node_exists {
+ local query=${1:?query not specified}
+ local result_name=${2}
+
+ for node in $(node_list) ; do
+ if [ "${node}" == "${query}" ] || [ $(node_attribute $node name) == "${query}" ] ; then
+ if [ -n "$result_name" ] ; then
+ eval $result_name=$node
+ fi
+ return
+ fi
+ done
+ return 1
+}
+
+function flavor_expand {
+ local flavor=${1:?flavor not specified}
+ local var_name=${2:?variable name not specified}
+
+ eval $var_name[vcpus]=$(openstack flavor show ${flavor} -f value -c vcpus)
+ eval $var_name[ram]=$(openstack flavor show ${flavor} -f value -c ram)
+ eval $var_name[cpu_arch]=$(openstack flavor show ${flavor} -f value -c properties | sed "s/.*cpu_arch='\([^']*\)'.*/\1/")
+ eval $var_name[disk]=$(openstack flavor show ${flavor} -f value -c disk)
+ eval $var_name[ephemeral]=$(openstack flavor show ${flavor} -f value -c "OS-FLV-EXT-DATA:ephemeral")
+ eval $var_name[local_gb]=$(($var_name[disk] + $var_name[ephemeral]))
+
+}
+
+function assert_last {
+ local code=${1:?code not specified}
+ local expected=${2:-0}
+ local message=${3:-}
+
+ if [ ${code} -ne ${expected} ] ; then
+ if [ -n "${message}" ] ; then
+ echo "${message}"
+ fi
+ return 1
+ fi
+}
+
+function assert_equal {
+ local lvalue=${1:?lvalue not specified}
+ local rvalue=${2:?rvalue not specified}
+ local message=${3:-}
+
+ [ "${lvalue}" == "${rvalue}" ] || assert_last ${?} 0 "${message}" || return ${?}
+}
+
+function assert_equal_arrays {
+ local lvalue_name=${1:?lvalue name not specified}
+ local rvalue_name=${2:?rvalue name not specified}
+ local lvalue
+ local rvalue
+ local keys
+ local key
+
+ eval keys=\${!${lvalue_name}[@]}
+ for key in ${keys} ; do
+ eval lvalue=\${$lvalue_name[$key]}
+ eval rvalue=\${$rvalue_name[$key]}
+ assert_equal $lvalue $rvalue "$key: $lvalue != $rvalue" || return ${?}
+ done
+ eval keys=\${!${rvalue_name}[@]}
+ for key in ${keys} ; do
+ eval lvalue=\${$lvalue_name[$key]}
+ eval rvalue=\${$rvalue_name[$key]}
+ assert_equal $lvalue $rvalue "$key: $lvalue != $rvalue" || return ${?}
+ done
+}
+
+function assert_mac_blacklisted {
+ local mac=${1:?mac not specified}
+
+ sudo -n iptables -L ironic-inspector | grep -iq "${mac}" && return
+ return 1
+}
+
+function assert_node_introspection_status {
+ local node=${1:?uuid not specified}
+ local finished_query=${2:-True}
+ local error_query=${3:-None}
+ local finished=$(openstack baremetal introspection status $node -f value -c finished)
+ local error=$(openstack baremetal introspection status $node -f value -c error)
+
+ assert_equal ${finished_query} ${finished} || return ${?}
+ assert_equal ${error_query} ${error} || return ${?}
+}
+
+function node_discovery_rule {
+ local node_name=${1:?node name not specified}
+ local node_driver=${2:?driver not specified}
+ local driver_info_name=${3:?driver info name not specified}
+ local keys
+ local key
+ local value
+
+ cat <