[ONOS-7732] Automating switch workflow: ofoverlay(Openflow overlay) workflow

Change-Id: I162a93465267547b3056f9096743b699aa4416b9
diff --git a/apps/workflow/BUILD b/apps/workflow/BUILD
index 118458e..e8b6a91 100644
--- a/apps/workflow/BUILD
+++ b/apps/workflow/BUILD
@@ -1,5 +1,6 @@
 BUNDLES = [
     "//apps/workflow/api:onos-apps-workflow-api",
+    "//apps/workflow/model:onos-apps-workflow-model",
     "//apps/workflow/app:onos-apps-workflow-app",
 ]
 
diff --git a/apps/workflow/app/BUILD b/apps/workflow/app/BUILD
index 87df2e1..d5af5a2 100644
--- a/apps/workflow/app/BUILD
+++ b/apps/workflow/app/BUILD
@@ -1,5 +1,6 @@
 COMPILE_DEPS = CORE_DEPS + KRYO + JACKSON + CLI + [
     "//core/store/serializers:onos-core-serializers",
+    "//core/store/primitives:onos-core-primitives",
     "//apps/workflow/api:onos-apps-workflow-api",
 ]
 
diff --git a/apps/workflow/model/BUILD b/apps/workflow/model/BUILD
new file mode 100644
index 0000000..016e14a
--- /dev/null
+++ b/apps/workflow/model/BUILD
@@ -0,0 +1,8 @@
+COMPILE_DEPS = CORE_DEPS + KRYO + JACKSON + [
+    "//core/store/serializers:onos-core-serializers",
+    "//apps/workflow/api:onos-apps-workflow-api",
+]
+
+osgi_jar_with_tests(
+    deps = COMPILE_DEPS,
+)
diff --git a/apps/workflow/model/src/main/java/org/onosproject/workflow/model/accessinfo/NetconfAccessInfo.java b/apps/workflow/model/src/main/java/org/onosproject/workflow/model/accessinfo/NetconfAccessInfo.java
new file mode 100644
index 0000000..8e428e3
--- /dev/null
+++ b/apps/workflow/model/src/main/java/org/onosproject/workflow/model/accessinfo/NetconfAccessInfo.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.
+ */
+package org.onosproject.workflow.model.accessinfo;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.NumericNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.google.common.base.MoreObjects;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.TpPort;
+import org.onosproject.workflow.api.WorkflowException;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Class for NETCONF access information.
+ */
+public final class NetconfAccessInfo {
+
+    private static final String REMOTE_IP = "remoteIp";
+    private static final String PORT = "port";
+    private static final String USER = "user";
+    private static final String PASSWORD = "password";
+
+    private final IpAddress remoteIp;
+    private final TpPort port;
+    private final String user;
+    private final String password;
+
+    /**
+     * Constructor for a given NETCONF access information.
+     *
+     * @param remoteIp remote ip address
+     * @param port port number
+     * @param user user name
+     * @param password password
+     */
+    public NetconfAccessInfo(IpAddress remoteIp,
+                             TpPort port,
+                             String user,
+                             String password) {
+        this.remoteIp = checkNotNull(remoteIp);
+        this.port = checkNotNull(port);
+        this.user = checkNotNull(user);
+        this.password = checkNotNull(password);
+    }
+
+    /**
+     * Builds NetconfAccessInfo from json.
+     * @param root json root node for NetconfAccessinfo
+     * @return NETCONF access information
+     * @throws WorkflowException workflow exception
+     */
+    public static NetconfAccessInfo valueOf(JsonNode root) throws WorkflowException {
+
+        JsonNode node = root.at(ptr(REMOTE_IP));
+        if (node == null || !(node instanceof TextNode)) {
+            throw new WorkflowException("invalid remoteIp for " + root);
+        }
+        IpAddress remoteIp = IpAddress.valueOf(node.asText());
+
+        node = root.at(ptr(PORT));
+        if (node == null || !(node instanceof NumericNode)) {
+            throw new WorkflowException("invalid port for " + root);
+        }
+        TpPort tpPort = TpPort.tpPort(node.asInt());
+
+        node = root.at(ptr(USER));
+        if (node == null || !(node instanceof TextNode)) {
+            throw new WorkflowException("invalid user for " + root);
+        }
+        String strUser = node.asText();
+
+        node = root.at(ptr(PASSWORD));
+        if (node == null || !(node instanceof TextNode)) {
+            throw new WorkflowException("invalid password for " + root);
+        }
+        String strPassword = node.asText();
+
+        return new NetconfAccessInfo(remoteIp, tpPort, strUser, strPassword);
+    }
+
+    private static String ptr(String field) {
+        return "/" + field;
+    }
+
+    /**
+     * Returns the remote IP address.
+     *
+     * @return ip address
+     */
+    public IpAddress remoteIp() {
+        return this.remoteIp;
+    }
+
+    /**
+     * Returns the port number.
+     *
+     * @return port
+     */
+    public TpPort port() {
+        return this.port;
+    }
+
+    /**
+     * Returns the user name.
+     *
+     * @return user name
+     */
+    public String user() {
+        return this.user;
+    }
+
+    /**
+     * Returns the password.
+     * @return password
+     */
+    public String password() {
+        return this.password;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj instanceof NetconfAccessInfo) {
+            NetconfAccessInfo that = (NetconfAccessInfo) obj;
+            return Objects.equals(remoteIp, that.remoteIp) &&
+                    Objects.equals(port, that.port) &&
+                    Objects.equals(user, that.user);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(remoteIp, port, user);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("remoteIp", remoteIp)
+                .add("port", port)
+                .add("user", user)
+                .toString();
+    }
+}
\ No newline at end of file
diff --git a/apps/workflow/model/src/main/java/org/onosproject/workflow/model/accessinfo/RestAccessInfo.java b/apps/workflow/model/src/main/java/org/onosproject/workflow/model/accessinfo/RestAccessInfo.java
new file mode 100644
index 0000000..c13270d
--- /dev/null
+++ b/apps/workflow/model/src/main/java/org/onosproject/workflow/model/accessinfo/RestAccessInfo.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.
+ */
+package org.onosproject.workflow.model.accessinfo;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.NumericNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.google.common.base.MoreObjects;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.TpPort;
+import org.onosproject.workflow.api.WorkflowException;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Class for REST access information.
+ */
+public final class RestAccessInfo {
+
+    private static final String REMOTE_IP = "remoteIp";
+    private static final String PORT = "port";
+    private static final String USER = "user";
+    private static final String PASSWORD = "password";
+
+    private final IpAddress remoteIp;
+    private final TpPort port;
+    private final String user;
+    private String password;
+
+    /**
+     * Creates a new REST access information.
+     *
+     * @param remoteIp remote ip address
+     * @param port port number
+     * @param user user name
+     * @param password password
+     */
+    private RestAccessInfo(IpAddress remoteIp,
+                          TpPort port,
+                          String user,
+                          String password) {
+        this.remoteIp = checkNotNull(remoteIp);
+        this.port = checkNotNull(port);
+        this.user = checkNotNull(user);
+        this.password = checkNotNull(password);
+    }
+
+    /**
+     * Builds RestAccessInfo from json.
+     * @param root json root node for RestAccessinfo
+     * @return REST access information
+     * @throws WorkflowException workflow exception
+     */
+    public static RestAccessInfo valueOf(JsonNode root) throws WorkflowException {
+
+        JsonNode node = root.at(ptr(REMOTE_IP));
+        if (node == null || !(node instanceof TextNode)) {
+            throw new WorkflowException("invalid remoteIp for " + root);
+        }
+        IpAddress remoteIp = IpAddress.valueOf(node.asText());
+
+        node = root.at(ptr(PORT));
+        if (node == null || !(node instanceof NumericNode)) {
+            throw new WorkflowException("invalid port for " + root);
+        }
+        TpPort tpPort = TpPort.tpPort(node.asInt());
+
+        node = root.at(ptr(USER));
+        if (node == null || !(node instanceof TextNode)) {
+            throw new WorkflowException("invalid user for " + root);
+        }
+        String strUser = node.asText();
+
+        node = root.at(ptr(PASSWORD));
+        if (node == null || !(node instanceof TextNode)) {
+            throw new WorkflowException("invalid password for " + root);
+        }
+        String strPassword = node.asText();
+
+        return new RestAccessInfo(remoteIp, tpPort, strUser, strPassword);
+    }
+
+    private static String ptr(String field) {
+        return "/" + field;
+    }
+
+    /**
+     * Returns the remote IP address.
+     *
+     * @return ip address
+     */
+    public IpAddress remoteIp() {
+        return this.remoteIp;
+    }
+
+    /**
+     * Returns the port number.
+     *
+     * @return REST port
+     */
+    public TpPort port() {
+        return this.port;
+    }
+
+    /**
+     * Returns the user name.
+     *
+     * @return user name
+     */
+    public String user() {
+        return this.user;
+    }
+
+    /**
+     * Returns the password.
+     *
+     * @return password
+     */
+    public String password() {
+        return this.password;
+    }
+
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj instanceof RestAccessInfo) {
+            RestAccessInfo that = (RestAccessInfo) obj;
+            return Objects.equals(remoteIp, that.remoteIp) &&
+                    Objects.equals(port, that.port) &&
+                    Objects.equals(user, that.user);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(remoteIp, port, user);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("remoteIp", remoteIp)
+                .add("port", port)
+                .add("user", user)
+                .toString();
+    }
+}
\ No newline at end of file
diff --git a/apps/workflow/model/src/main/java/org/onosproject/workflow/model/accessinfo/SshAccessInfo.java b/apps/workflow/model/src/main/java/org/onosproject/workflow/model/accessinfo/SshAccessInfo.java
new file mode 100644
index 0000000..aec224a
--- /dev/null
+++ b/apps/workflow/model/src/main/java/org/onosproject/workflow/model/accessinfo/SshAccessInfo.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.
+ */
+package org.onosproject.workflow.model.accessinfo;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.NumericNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.google.common.base.MoreObjects;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.TpPort;
+import org.onosproject.workflow.api.WorkflowException;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Class for SSH access information.
+ */
+public final class SshAccessInfo {
+
+    private static final String REMOTE_IP = "remoteIp";
+    private static final String PORT = "port";
+    private static final String USER = "user";
+    private static final String PASSWORD = "password";
+    private static final String KEYFILE = "keyfile";
+
+    private final IpAddress remoteIp;
+    private final TpPort port;
+    private final String user;
+    private String password;
+    private final String privateKey;
+
+    /**
+     * Constructor for SSH access information.
+     *
+     * @param remoteIp ssh remote ip address
+     * @param port ssh port number
+     * @param user user name
+     * @param password password
+     * @param privateKey path of ssh private key
+     */
+    private SshAccessInfo(IpAddress remoteIp, TpPort port, String user, String password, String privateKey) {
+        this.remoteIp = checkNotNull(remoteIp);
+        this.port = checkNotNull(port);
+        this.user = checkNotNull(user);
+        this.password = password;
+        this.privateKey = checkNotNull(privateKey);
+    }
+
+    /**
+     * Builds SshAccessInfo from json.
+     * @param root json root node for SshAccessinfo
+     * @return SSH access information
+     * @throws WorkflowException workflow exception
+     */
+    public static SshAccessInfo valueOf(JsonNode root) throws WorkflowException {
+
+        JsonNode node = root.at(ptr(REMOTE_IP));
+        if (node == null || !(node instanceof TextNode)) {
+            throw new WorkflowException("invalid remoteIp for " + root);
+        }
+        IpAddress sshIp = IpAddress.valueOf(node.asText());
+
+        node = root.at(ptr(PORT));
+        if (node == null || !(node instanceof NumericNode)) {
+            throw new WorkflowException("invalid port for " + root);
+        }
+        TpPort sshPort = TpPort.tpPort(node.asInt());
+
+        node = root.at(ptr(USER));
+        if (node == null || !(node instanceof TextNode)) {
+            throw new WorkflowException("invalid user for " + root);
+        }
+        String sshUser = node.asText();
+
+        node = root.at(ptr(PASSWORD));
+        if (node == null || !(node instanceof TextNode)) {
+            throw new WorkflowException("invalid password for " + root);
+        }
+        String sshPassword = node.asText();
+
+        node = root.at(ptr(KEYFILE));
+        if (node == null || !(node instanceof TextNode)) {
+            throw new WorkflowException("invalid keyfile for " + root);
+        }
+        String sshKeyfile = node.asText();
+
+        return new SshAccessInfo(sshIp, sshPort, sshUser, sshPassword, sshKeyfile);
+    }
+
+    private static String ptr(String field) {
+        return "/" + field;
+    }
+
+    /**
+     * Returns the remote IP address.
+     *
+     * @return ip address
+     */
+    public IpAddress remoteIp() {
+        return this.remoteIp;
+    }
+
+    /**
+     * Returns the port number.
+     *
+     * @return ssh port
+     */
+    public TpPort port() {
+        return this.port;
+    }
+
+    /**
+     * Returns the user name.
+     *
+     * @return user name
+     */
+    public String user() {
+        return this.user;
+    }
+
+    /**
+     * Returns the password.
+     * @return password
+     */
+    public String password() {
+        return this.password;
+    }
+
+    /**
+     * Returns the private key path.
+     *
+     * @return privateKey
+     */
+    public String privateKey() {
+        return privateKey;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj instanceof SshAccessInfo) {
+            SshAccessInfo that = (SshAccessInfo) obj;
+            return Objects.equals(remoteIp, that.remoteIp) &&
+                    Objects.equals(port, that.port) &&
+                    Objects.equals(user, that.user) &&
+                    Objects.equals(privateKey, that.privateKey);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(remoteIp, port, user, privateKey);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("remoteIp", remoteIp)
+                .add("port", port)
+                .add("user", user)
+                .add("privateKey", privateKey)
+                .toString();
+    }
+}
diff --git a/apps/workflow/model/src/main/java/org/onosproject/workflow/model/accessinfo/package-info.java b/apps/workflow/model/src/main/java/org/onosproject/workflow/model/accessinfo/package-info.java
new file mode 100644
index 0000000..bc88e3b
--- /dev/null
+++ b/apps/workflow/model/src/main/java/org/onosproject/workflow/model/accessinfo/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Package for workflow common data model - access info.
+ */
+package org.onosproject.workflow.model.accessinfo;
diff --git a/apps/workflow/ofoverlay/BUILD b/apps/workflow/ofoverlay/BUILD
new file mode 100644
index 0000000..cc75d82
--- /dev/null
+++ b/apps/workflow/ofoverlay/BUILD
@@ -0,0 +1,21 @@
+BUNDLES = [
+    "@jsch//jar",
+    "//protocols/ovsdb/api:onos-protocols-ovsdb-api",
+    "//protocols/ovsdb/rfc:onos-protocols-ovsdb-rfc",
+    "//apps/workflow/ofoverlay/app:onos-apps-workflow-ofoverlay-app",
+]
+
+onos_app(
+    app_name = "org.onosproject.workflow.ofoverlay",
+    category = "Utility",
+    description = "Openflow overaly application",
+    included_bundles = BUNDLES,
+    required_apps = [
+        "org.onosproject.openflow",
+        "org.onosproject.ovsdb",
+        "org.onosproject.drivers.ovsdb",
+        "org.onosproject.workflow",
+    ],
+    title = "Openflow overlay",
+    url = "http://onosproject.org",
+)
diff --git a/apps/workflow/ofoverlay/app/BUILD b/apps/workflow/ofoverlay/app/BUILD
new file mode 100644
index 0000000..d8a5fa0
--- /dev/null
+++ b/apps/workflow/ofoverlay/app/BUILD
@@ -0,0 +1,13 @@
+COMPILE_DEPS = CORE_DEPS + KRYO + JACKSON + CLI + [
+    "//core/store/serializers:onos-core-serializers",
+    "//core/store/primitives:onos-core-primitives",
+    "//apps/workflow/api:onos-apps-workflow-api",
+    "//apps/workflow/model:onos-apps-workflow-model",
+    "@jsch//jar",
+    "//protocols/ovsdb/api:onos-protocols-ovsdb-api",
+    "//protocols/ovsdb/rfc:onos-protocols-ovsdb-rfc",
+]
+
+osgi_jar_with_tests(
+    deps = COMPILE_DEPS,
+)
diff --git a/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/OfOverlayWorkflow.java b/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/OfOverlayWorkflow.java
new file mode 100644
index 0000000..5fb8cd2
--- /dev/null
+++ b/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/OfOverlayWorkflow.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.
+ */
+package org.onosproject.ofoverlay.impl;
+
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.workflow.api.ImmutableListWorkflow;
+import org.onosproject.workflow.api.Workflow;
+import org.onosproject.workflow.api.WorkflowExecutionService;
+import org.onosproject.workflow.api.WorkflowStore;
+import org.onosproject.workflow.api.WorkplaceStore;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.URI;
+import java.util.concurrent.ScheduledExecutorService;
+
+import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
+import static org.onlab.util.Tools.groupedThreads;
+
+/**
+ * Class for Open-flow overlay configuration workflow.
+ */
+@Component(immediate = true)
+public class OfOverlayWorkflow {
+
+    private static final Logger log = LoggerFactory.getLogger(OfOverlayWorkflow.class);
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected WorkflowStore workflowStore;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected WorkplaceStore workplaceStore;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected WorkflowExecutionService workflowExecutionService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected DeviceService deviceService;
+
+    private ScheduledExecutorService eventMapTriggerExecutor;
+
+    @Activate
+    public void activate() {
+        log.info("Activated");
+
+        eventMapTriggerExecutor = newSingleThreadScheduledExecutor(
+                groupedThreads("onos/of-overlay", "eventmap-trigger-executor"));
+
+        registerWorkflows();
+
+    }
+
+    @Deactivate
+    public void deactivate() {
+        log.info("Deactivated");
+    }
+
+    /**
+     * Registers workflows.
+     */
+    private void registerWorkflows() {
+        // registering class-loader
+        workflowStore.registerLocal(this.getClass().getClassLoader());
+
+        // registering new workflow definition
+        URI uri = URI.create("of-overlay.workflow-nova");
+        Workflow workflow = ImmutableListWorkflow.builder()
+                .id(uri)
+                //.attribute(WorkflowAttribute.REMOVE_AFTER_COMPLETE)
+                .chain(Ovs.CreateOvsdbDevice.class.getName())
+                .chain(Ovs.UpdateOvsVersion.class.getName())
+                .chain(Ovs.UpdateOverlayBridgeId.class.getName())
+                .chain(Ovs.CreateOverlayBridge.class.getName())
+                .chain(Ovs.UpdateUnderlayBridgeId.class.getName())
+                .chain(Ovs.CreateUnderlayBridge.class.getName())
+                .chain(Ovs.CreateOverlayBridgeVxlanPort.class.getName())
+                .chain(Ovs.AddPhysicalPortsOnUnderlayBridge.class.getName())
+                .chain(Ovs.ConfigureUnderlayBridgeLocalIp.class.getName())
+                .build();
+        workflowStore.register(workflow);
+
+        uri = URI.create("of-overlay.clean-workflow-nova");
+        workflow = ImmutableListWorkflow.builder()
+                .id(uri)
+                //.attribute(WorkflowAttribute.REMOVE_AFTER_COMPLETE)
+                .chain(Ovs.DeleteOverlayBridgeConfig.class.getName())
+                .chain(Ovs.RemoveOverlayBridgeOfDevice.class.getName())
+                .chain(Ovs.DeleteUnderlayBridgeConfig.class.getName())
+                .chain(Ovs.RemoveUnderlayBridgeOfDevice.class.getName())
+                .chain(Ovs.RemoveOvsdbDevice.class.getName())
+                .build();
+        workflowStore.register(workflow);
+
+        uri = URI.create("of-overlay.workflow-ovs-leaf");
+        workflow = ImmutableListWorkflow.builder()
+                .id(uri)
+                .chain(Ovs.CreateOvsdbDevice.class.getName())
+                .chain(Ovs.UpdateOvsVersion.class.getName())
+                .chain(Ovs.UpdateUnderlayBridgeId.class.getName())
+                .chain(Ovs.CreateUnderlayBridge.class.getName())
+                .chain(Ovs.AddPhysicalPortsOnUnderlayBridge.class.getName())
+                .build();
+        workflowStore.register(workflow);
+
+        uri = URI.create("of-overlay.workflow-ovs-spine");
+        workflow = ImmutableListWorkflow.builder()
+                .id(uri)
+                .chain(Ovs.CreateOvsdbDevice.class.getName())
+                .chain(Ovs.UpdateOvsVersion.class.getName())
+                .chain(Ovs.UpdateUnderlayBridgeId.class.getName())
+                .chain(Ovs.CreateUnderlayBridge.class.getName())
+                .chain(Ovs.AddPhysicalPortsOnUnderlayBridge.class.getName())
+                .build();
+        workflowStore.register(workflow);
+
+        deviceService.addListener(
+                event -> {
+                    // trigger EventTask for DeviceEvent
+                    eventMapTriggerExecutor.submit(
+                            () -> workflowExecutionService.eventMapTrigger(
+                                    event,
+                                    // event hint supplier
+                                    (ev) -> {
+                                        if (ev == null || ev.subject() == null) {
+                                            return null;
+                                        }
+                                        String hint = event.subject().id().toString();
+                                        log.debug("hint: {}", hint);
+                                        return hint;
+                                    }
+                            )
+                    );
+                }
+        );
+
+    }
+
+}
diff --git a/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/Ovs.java b/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/Ovs.java
new file mode 100644
index 0000000..428688b
--- /dev/null
+++ b/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/Ovs.java
@@ -0,0 +1,1220 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.
+ */
+package org.onosproject.ofoverlay.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.TpPort;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.event.Event;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Port;
+import org.onosproject.net.behaviour.BridgeConfig;
+import org.onosproject.net.behaviour.BridgeDescription;
+import org.onosproject.net.behaviour.BridgeName;
+import org.onosproject.net.behaviour.ControlProtocolVersion;
+import org.onosproject.net.behaviour.ControllerInfo;
+import org.onosproject.net.behaviour.DefaultBridgeDescription;
+import org.onosproject.net.behaviour.DefaultTunnelDescription;
+import org.onosproject.net.behaviour.InterfaceConfig;
+import org.onosproject.net.behaviour.TunnelDescription;
+import org.onosproject.net.behaviour.TunnelEndPoints;
+import org.onosproject.net.behaviour.TunnelKeys;
+import org.onosproject.net.device.DeviceAdminService;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.driver.Behaviour;
+import org.onosproject.net.driver.DriverHandler;
+import org.onosproject.net.driver.DriverService;
+import org.onosproject.ofoverlay.impl.util.NetworkAddress;
+import org.onosproject.ofoverlay.impl.util.OvsDatapathType;
+import org.onosproject.ofoverlay.impl.util.OvsVersion;
+import org.onosproject.ofoverlay.impl.util.SshUtil;
+import org.onosproject.ovsdb.controller.OvsdbClientService;
+import org.onosproject.ovsdb.controller.OvsdbController;
+import org.onosproject.ovsdb.controller.OvsdbNodeId;
+import org.onosproject.workflow.api.AbstractWorklet;
+import org.onosproject.workflow.api.JsonDataModel;
+import org.onosproject.workflow.api.WorkflowContext;
+import org.onosproject.workflow.api.WorkflowException;
+import org.onosproject.workflow.model.accessinfo.SshAccessInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import static org.onosproject.net.AnnotationKeys.PORT_NAME;
+import static org.onosproject.workflow.api.CheckCondition.check;
+
+/**
+ * Class for defining OVS workflows.
+ */
+public final class Ovs {
+
+    private static final Logger log = LoggerFactory.getLogger(Ovs.class);
+
+    private static final String MODEL_MGMT_IP = "/mgmtIp";
+    private static final String MODEL_OVSDB_PORT = "/ovsdbPort";
+    private static final String MODEL_OVS_VERSION = "/ovsVersion";
+    private static final String MODEL_OVS_DATAPATH_TYPE = "/ovsDatapathType";
+    private static final String MODEL_SSH_ACCESSINFO = "/sshAccessInfo";
+    private static final String MODEL_OF_DEVID_OVERLAY_BRIDGE = "/ofDevIdBrInt";
+    private static final String MODEL_OF_DEVID_UNDERLAY_BRIDGE = "/ofDevIdBrPhy";
+    private static final String MODEL_PHY_PORTS = "/physicalPorts";
+    private static final String MODEL_VTEP_IP = "/vtepIp";
+
+    private static final String BRIDGE_OVERLAY = "br-int";
+    private static final String BRIDGE_UNDERLAY = "br-phy";
+
+    private static final int DEVID_IDX_BRIDGE_OVERLAY = 0;
+    private static final int DEVID_IDX_BRIDGE_UNDERLAY_NOVA = 1;
+
+    private static final ControlProtocolVersion BRIDGE_DEFAULT_OF_VERSION = ControlProtocolVersion.OF_1_3;
+    private static final int    OPENFLOW_PORT = 6653;
+    private static final String OPENFLOW_CHANNEL_PROTO = "tcp";
+    private static final String OVSDB_DEVICE_PREFIX = "ovsdb:";
+
+    private static final long TIMEOUT_DEVICE_CREATION_MS = 60000L;
+    private static final long TIMEOUT_PORT_ADDITION_MS = 120000L;
+
+
+    /**
+     * Utility class for OVS workflow.
+     */
+    public static final class OvsUtil {
+
+        private OvsUtil() {
+
+        }
+
+        private static final String OPENFLOW_DEVID_FORMAT = "of:%08x%08x";
+
+        /**
+         * Builds Open-flow device id with ip address, and index.
+         * @param addr ip address
+         * @param index index
+         * @return created device id
+         */
+        public static DeviceId buildOfDeviceId(IpAddress addr, int index) {
+            if (addr.isIp4()) {
+                Ip4Address v4Addr = addr.getIp4Address();
+                return DeviceId.deviceId(String.format(OPENFLOW_DEVID_FORMAT, v4Addr.toInt(), index));
+            } else if (addr.isIp6()) {
+                Ip6Address v6Addr = addr.getIp6Address();
+                return DeviceId.deviceId(String.format(OPENFLOW_DEVID_FORMAT, v6Addr.hashCode(), index));
+            } else {
+                return DeviceId.deviceId(String.format(OPENFLOW_DEVID_FORMAT, addr.hashCode(), index));
+            }
+        }
+
+        /**
+         * Builds OVS data path type.
+         * @param strOvsDatapathType string ovs data path type
+         * @return ovs data path type
+         * @throws WorkflowException workflow exception
+         */
+        public static final OvsDatapathType buildOvsDatapathType(String strOvsDatapathType) throws WorkflowException {
+            try {
+                return OvsDatapathType.valueOf(strOvsDatapathType.toUpperCase());
+            } catch (IllegalArgumentException e) {
+                throw new WorkflowException(e);
+            }
+        }
+
+        /**
+         * Gets OVSDB behavior.
+         * @param context workflow context
+         * @param mgmtIp management ip
+         * @param behaviourClass behavior class
+         * @param <T> behavior class
+         * @return OVSDB behavior
+         * @throws WorkflowException workflow exception
+         */
+        public static final <T extends Behaviour> T getOvsdbBehaviour(WorkflowContext context, String mgmtIp,
+                                                                    Class<T> behaviourClass) throws WorkflowException {
+
+            DriverService driverService = context.getService(DriverService.class);
+
+            DeviceId devId = ovsdbDeviceId(mgmtIp);
+            DriverHandler handler = driverService.createHandler(devId);
+            if (Objects.isNull(handler)) {
+                throw new WorkflowException("Failed to get DriverHandler for " + devId);
+            }
+            T behaviour;
+            try {
+                behaviour =  handler.behaviour(behaviourClass);
+                if (Objects.isNull(behaviour)) {
+                    throw new WorkflowException("Failed to get " + behaviourClass + " for " + devId + "-" + handler);
+                }
+            } catch (IllegalArgumentException e) {
+                throw new WorkflowException("Failed to get " + behaviourClass + " for " + devId + "-" + handler);
+            }
+            return behaviour;
+        }
+
+        /**
+         * Gets bridge description.
+         * @param bridgeConfig bridge config
+         * @param bridgeName bridge name
+         * @return bridge description optional
+         */
+        public static final Optional<BridgeDescription> getBridgeDescription(BridgeConfig bridgeConfig,
+                                                                             String bridgeName) {
+            try {
+                Collection<BridgeDescription> bridges = bridgeConfig.getBridges();
+                for (BridgeDescription br: bridges) {
+                    if (Objects.equals(bridgeName, br.name())) {
+                        return Optional.of(br);
+                    }
+                }
+            } catch (Exception e) {
+                log.error("Exception : ", e);
+            }
+            return Optional.empty();
+        }
+
+        /**
+         * Builds OVSDB device id.
+         * @param mgmtIp management ip address string
+         * @return OVSDB device id
+         */
+        public static final DeviceId ovsdbDeviceId(String mgmtIp) {
+            return DeviceId.deviceId(OVSDB_DEVICE_PREFIX.concat(mgmtIp));
+        }
+
+        /**
+         * Returns {@code true} if this bridge is available;
+         * returns {@code false} otherwise.
+         * @param context workflow context
+         * @param devId device id
+         * @return {@code true} if this bridge is available; {@code false} otherwise.
+         * @throws WorkflowException workflow exception
+         */
+        public static final boolean isAvailableBridge(WorkflowContext context, DeviceId devId)
+                throws WorkflowException {
+
+            if (Objects.isNull(devId)) {
+                throw new WorkflowException("Invalid device id in data model");
+            }
+
+            DeviceService deviceService = context.getService(DeviceService.class);
+            Device dev = deviceService.getDevice(devId);
+            if (Objects.isNull(dev)) {
+                return false;
+            }
+
+            return deviceService.isAvailable(devId);
+        }
+
+        /**
+         * Gets openflow controller information list.
+         * @param context workflow context
+         * @return openflow controller information list
+         * @throws WorkflowException workflow exception
+         */
+        public static final List<ControllerInfo> getOpenflowControllerInfoList(WorkflowContext context)
+                throws WorkflowException {
+            ClusterService clusterService = context.getService(ClusterService.class);
+            java.util.List<org.onosproject.net.behaviour.ControllerInfo> controllers = new ArrayList<>();
+            Sets.newHashSet(clusterService.getNodes()).forEach(
+                    controller -> {
+                        org.onosproject.net.behaviour.ControllerInfo ctrlInfo =
+                                new org.onosproject.net.behaviour.ControllerInfo(controller.ip(),
+                                        OPENFLOW_PORT,
+                                        OPENFLOW_CHANNEL_PROTO);
+                        controllers.add(ctrlInfo);
+                    }
+            );
+            return controllers;
+        }
+
+        /**
+         * Creates bridge.
+         * @param bridgeConfig bridge config
+         * @param name bridge name to create
+         * @param dpid openflow data path id of bridge to create
+         * @param ofControllers openflow controller information list
+         * @param datapathType OVS data path type
+         */
+        public static final void createBridge(BridgeConfig bridgeConfig, String name, String dpid,
+                                              List<ControllerInfo> ofControllers, OvsDatapathType datapathType) {
+            BridgeDescription.Builder bridgeDescBuilder = DefaultBridgeDescription.builder()
+                    .name(name)
+                    .failMode(BridgeDescription.FailMode.SECURE)
+                    .datapathId(dpid)
+                    .disableInBand()
+                    .controlProtocols(Collections.singletonList(BRIDGE_DEFAULT_OF_VERSION))
+                    .controllers(ofControllers);
+
+            if (datapathType != null && !(datapathType.equals(OvsDatapathType.EMPTY))) {
+                bridgeDescBuilder.datapathType(datapathType.toString());
+                log.info("create {} with dataPathType {}", name, datapathType);
+            }
+
+            BridgeDescription bridgeDesc = bridgeDescBuilder.build();
+            bridgeConfig.addBridge(bridgeDesc);
+        }
+
+        /**
+         * Index of data path id in openflow device id.
+         */
+        private static final int DPID_BEGIN_INDEX = 3;
+
+        /**
+         * Gets bridge data path id.
+         * @param devId device id
+         * @return bridge data path id
+         */
+        public static final String bridgeDatapathId(DeviceId devId) {
+            return devId.toString().substring(DPID_BEGIN_INDEX);
+        }
+
+        /**
+         * Gets OVSDB client.
+         * @param context workflow context
+         * @param strMgmtIp management ip address
+         * @param intOvsdbPort OVSDB port
+         * @return ovsdb client
+         * @throws WorkflowException workflow exception
+         */
+        public static final OvsdbClientService getOvsdbClient(
+                WorkflowContext context, String strMgmtIp, int intOvsdbPort) throws WorkflowException {
+            IpAddress mgmtIp = IpAddress.valueOf(strMgmtIp);
+            TpPort ovsdbPort = TpPort.tpPort(intOvsdbPort);
+            OvsdbController ovsdbController = context.getService(OvsdbController.class);
+            return ovsdbController.getOvsdbClient(new OvsdbNodeId(mgmtIp, ovsdbPort.toInt()));
+        }
+
+        /**
+         * Checks whether 2 controller informations include same controller information.
+         * @param a controller information list
+         * @param b controller information list
+         * @return {@code true} if 2 controller informations include same controller information
+         */
+        public static boolean isEqual(List<ControllerInfo> a, List<ControllerInfo> b) {
+            if (a == b) {
+                return true;
+            } else if (a == null) {
+                // equivalent to (a == null && b != null)
+                return false;
+            } else if (b == null) {
+                // equivalent to (a != null && b == null)
+                return false;
+            } else if (a.size() != b.size()) {
+                return false;
+            }
+
+            return a.containsAll(b);
+        }
+
+        /**
+         * Gets the name of the port.
+         * @param port port
+         * @return the name of the port
+         */
+        public static final String portName(Port port) {
+            return port.annotations().value(PORT_NAME);
+        }
+    }
+
+    /**
+     * Work-let class for creating OVSDB device.
+     */
+    public static class CreateOvsdbDevice extends AbstractWorklet {
+
+        @JsonDataModel(path = MODEL_MGMT_IP)
+        String strMgmtIp;
+
+        @JsonDataModel(path = MODEL_OVSDB_PORT)
+        Integer intOvsdbPort;
+
+        @Override
+        public boolean isNext(WorkflowContext context) throws WorkflowException {
+
+            OvsdbClientService ovsdbClient = OvsUtil.getOvsdbClient(context, strMgmtIp, intOvsdbPort);
+            return ovsdbClient == null || !ovsdbClient.isConnected();
+        }
+
+        @Override
+        public void process(WorkflowContext context) throws WorkflowException {
+            IpAddress mgmtIp = IpAddress.valueOf(strMgmtIp);
+            TpPort ovsdbPort = TpPort.tpPort(intOvsdbPort);
+            OvsdbController ovsdbController = context.getService(OvsdbController.class);
+            context.waitCompletion(DeviceEvent.class, OVSDB_DEVICE_PREFIX.concat(strMgmtIp),
+                    () -> ovsdbController.connect(mgmtIp, ovsdbPort),
+                    TIMEOUT_DEVICE_CREATION_MS
+            );
+        }
+
+        @Override
+        public boolean isCompleted(WorkflowContext context, Event event)throws WorkflowException {
+            if (!(event instanceof DeviceEvent)) {
+                return false;
+            }
+            DeviceEvent deviceEvent = (DeviceEvent) event;
+            Device device = deviceEvent.subject();
+            switch (deviceEvent.type()) {
+                case DEVICE_ADDED:
+                case DEVICE_AVAILABILITY_CHANGED:
+                case DEVICE_UPDATED:
+                    return context.getService(DeviceService.class).isAvailable(device.id());
+                default:
+                    return false;
+            }
+        }
+
+        @Override
+        public void timeout(WorkflowContext context) throws WorkflowException {
+            if (!isNext(context)) {
+                context.completed(); //Complete the job of worklet by timeout
+            } else {
+                super.timeout(context);
+            }
+        }
+    }
+
+
+    /**
+     * Work-let class for removing OVSDB device.
+     */
+    public static class RemoveOvsdbDevice extends AbstractWorklet {
+
+        @JsonDataModel(path = MODEL_MGMT_IP)
+        String strMgmtIp;
+
+        @Override
+        public boolean isNext(WorkflowContext context) throws WorkflowException {
+
+            DeviceId devId = DeviceId.deviceId(OVSDB_DEVICE_PREFIX.concat(strMgmtIp));
+
+            Device dev = context.getService(DeviceService.class).getDevice(devId);
+            return dev != null;
+        }
+
+        @Override
+        public void process(WorkflowContext context) throws WorkflowException {
+            IpAddress mgmtIp = IpAddress.valueOf(strMgmtIp);
+            check(mgmtIp != null, "mgmt ip is invalid");
+            DeviceId devId = DeviceId.deviceId(OVSDB_DEVICE_PREFIX.concat(strMgmtIp));
+            DeviceAdminService adminService = context.getService(DeviceAdminService.class);
+
+            context.waitCompletion(DeviceEvent.class, devId.toString(),
+                    () -> adminService.removeDevice(devId),
+                    TIMEOUT_DEVICE_CREATION_MS
+            );
+        }
+
+        @Override
+        public boolean isCompleted(WorkflowContext context, Event event)throws WorkflowException {
+            if (!(event instanceof DeviceEvent)) {
+                return false;
+            }
+            DeviceEvent deviceEvent = (DeviceEvent) event;
+            switch (deviceEvent.type()) {
+                case DEVICE_REMOVED:
+                    return !isNext(context);
+                default:
+                    return false;
+            }
+        }
+
+        @Override
+        public void timeout(WorkflowContext context) throws WorkflowException {
+            if (!isNext(context)) {
+                context.completed(); //Complete worklet by timeout
+            } else {
+                super.timeout(context);
+            }
+        }
+    }
+
+    /**
+     * Work-let class for updating OVS version.
+     */
+    public static class UpdateOvsVersion extends AbstractWorklet {
+
+        @JsonDataModel(path = MODEL_OVS_VERSION, optional = true)
+        String strOvsVersion;
+
+        @JsonDataModel(path = MODEL_SSH_ACCESSINFO)
+        JsonNode strSshAccessInfo;
+
+        @Override
+        public boolean isNext(WorkflowContext context) throws WorkflowException {
+
+            return strOvsVersion == null;
+        }
+
+        @Override
+        public void process(WorkflowContext context) throws WorkflowException {
+
+            SshAccessInfo sshAccessInfo = SshAccessInfo.valueOf(strSshAccessInfo);
+            check(Objects.nonNull(sshAccessInfo), "Invalid ssh access info " + context.data());
+
+            OvsVersion ovsVersion = SshUtil.exec(sshAccessInfo,
+                    session -> SshUtil.fetchOvsVersion(session));
+
+            check(Objects.nonNull(ovsVersion), "Failed to fetch ovs version " + context.data());
+            strOvsVersion = ovsVersion.toString();
+
+            context.completed();
+        }
+    }
+
+    /**
+     * Work-let class for updating overlay bridge device id.
+     */
+    public static class UpdateOverlayBridgeId extends AbstractWorklet {
+
+        @JsonDataModel(path = MODEL_MGMT_IP)
+        String strMgmtIp;
+
+        @JsonDataModel(path = MODEL_OF_DEVID_OVERLAY_BRIDGE, optional = true)
+        String strOfDevIdOverlay;
+
+        @Override
+        public boolean isNext(WorkflowContext context) throws WorkflowException {
+
+            return strOfDevIdOverlay == null;
+        }
+
+        @Override
+        public void process(WorkflowContext context) throws WorkflowException {
+
+            BridgeConfig bridgeConfig = OvsUtil.getOvsdbBehaviour(context, strMgmtIp, BridgeConfig.class);
+            Optional<BridgeDescription> optBd = OvsUtil.getBridgeDescription(bridgeConfig, BRIDGE_OVERLAY);
+            if (optBd.isPresent()) {
+                Optional<DeviceId> optDevId = optBd.get().deviceId();
+                if (optDevId.isPresent()) {
+                    log.info("Updates {} of device id with existing device id {}", BRIDGE_OVERLAY, optDevId.get());
+                    strOfDevIdOverlay = optDevId.get().toString();
+                } else {
+                    DeviceId newDevId = OvsUtil.buildOfDeviceId(IpAddress.valueOf(strMgmtIp), DEVID_IDX_BRIDGE_OVERLAY);
+                    log.info("Failed to find devId. Updates {} of device id with new device id {}",
+                            BRIDGE_OVERLAY, newDevId);
+                    strOfDevIdOverlay = newDevId.toString();
+                }
+            } else {
+                DeviceId newDevId = OvsUtil.buildOfDeviceId(IpAddress.valueOf(strMgmtIp), DEVID_IDX_BRIDGE_OVERLAY);
+                log.info("Failed to find description. Updates {} of device id with new device id {}",
+                        BRIDGE_OVERLAY, newDevId);
+                strOfDevIdOverlay = newDevId.toString();
+            }
+
+            context.completed();
+        }
+    }
+
+    /**
+     * Work-let class for creating overlay openflow bridge.
+     */
+    public static class CreateOverlayBridge extends AbstractWorklet {
+
+        @JsonDataModel(path = MODEL_MGMT_IP)
+        String strMgmtIp;
+
+        @JsonDataModel(path = MODEL_OVSDB_PORT)
+        Integer intOvsdbPort;
+
+        @JsonDataModel(path = MODEL_OVS_DATAPATH_TYPE)
+        String strOvsDatapath;
+
+        @JsonDataModel(path = MODEL_OF_DEVID_OVERLAY_BRIDGE, optional = true)
+        String strOfDevIdOverlay;
+
+        @Override
+        public boolean isNext(WorkflowContext context) throws WorkflowException {
+
+            check(strOfDevIdOverlay != null, "invalid strOfDevIdOverlay");
+            return !OvsUtil.isAvailableBridge(context, DeviceId.deviceId(strOfDevIdOverlay));
+        }
+
+        @Override
+        public void process(WorkflowContext context) throws WorkflowException {
+
+            check(strOfDevIdOverlay != null, "invalid strOfDevIdOverlay");
+            BridgeConfig bridgeConfig = OvsUtil.getOvsdbBehaviour(context, strMgmtIp, BridgeConfig.class);
+            List<ControllerInfo> ofControllers = OvsUtil.getOpenflowControllerInfoList(context);
+            DeviceId ofDeviceId = DeviceId.deviceId(strOfDevIdOverlay);
+
+            if (ofControllers == null || ofControllers.size() == 0) {
+                throw new WorkflowException("Invalid of controllers");
+            }
+
+            Optional<BridgeDescription> optBd = OvsUtil.getBridgeDescription(bridgeConfig, BRIDGE_OVERLAY);
+            if (!optBd.isPresent()) {
+
+                // If bridge does not exist, just creates a new bridge.
+                context.waitCompletion(DeviceEvent.class, ofDeviceId.toString(),
+                        () -> OvsUtil.createBridge(bridgeConfig,
+                                BRIDGE_OVERLAY,
+                                OvsUtil.bridgeDatapathId(ofDeviceId),
+                                ofControllers,
+                                OvsUtil.buildOvsDatapathType(strOvsDatapath)),
+                        TIMEOUT_DEVICE_CREATION_MS
+                );
+                return;
+
+            } else {
+                BridgeDescription bd = optBd.get();
+                if (OvsUtil.isEqual(ofControllers, bd.controllers())) {
+                    log.error("{} has valid controller setting({})", BRIDGE_OVERLAY, bd.controllers());
+                    context.completed();
+                    return;
+                }
+
+                OvsdbClientService ovsdbClient = OvsUtil.getOvsdbClient(context, strMgmtIp, intOvsdbPort);
+                if (ovsdbClient == null || !ovsdbClient.isConnected()) {
+                    throw new WorkflowException("Invalid ovsdb client for " + strMgmtIp);
+                }
+
+                // If controller settings are not matched, set controller with valid controller information.
+                context.waitCompletion(DeviceEvent.class, ofDeviceId.toString(),
+                        () -> ovsdbClient.setControllersWithDeviceId(bd.deviceId().get(), ofControllers),
+                        TIMEOUT_DEVICE_CREATION_MS
+                );
+                return;
+            }
+        }
+
+        @Override
+        public boolean isCompleted(WorkflowContext context, Event event)throws WorkflowException {
+            if (!(event instanceof DeviceEvent)) {
+                return false;
+            }
+            DeviceEvent deviceEvent = (DeviceEvent) event;
+            Device device = deviceEvent.subject();
+            switch (deviceEvent.type()) {
+                case DEVICE_ADDED:
+                case DEVICE_AVAILABILITY_CHANGED:
+                case DEVICE_UPDATED:
+                    return context.getService(DeviceService.class).isAvailable(device.id());
+                default:
+                    return false;
+            }
+        }
+
+        @Override
+        public void timeout(WorkflowContext context) throws WorkflowException {
+            if (!isNext(context)) {
+                context.completed(); //Complete the job of worklet by timeout
+            } else {
+                super.timeout(context);
+            }
+        }
+    }
+
+    /**
+     * Work-let class for updating underlay bridge device id.
+     */
+    public static class UpdateUnderlayBridgeId extends AbstractWorklet {
+
+        @JsonDataModel(path = MODEL_MGMT_IP)
+        String strMgmtIp;
+
+        @JsonDataModel(path = MODEL_OF_DEVID_UNDERLAY_BRIDGE, optional = true)
+        String strOfDevIdUnderlay;
+
+        @Override
+        public boolean isNext(WorkflowContext context) throws WorkflowException {
+
+            return strOfDevIdUnderlay == null;
+        }
+
+        @Override
+        public void process(WorkflowContext context) throws WorkflowException {
+
+            BridgeConfig bridgeConfig = OvsUtil.getOvsdbBehaviour(context, strMgmtIp, BridgeConfig.class);
+            Optional<BridgeDescription> optBd = OvsUtil.getBridgeDescription(bridgeConfig, BRIDGE_UNDERLAY);
+            if (optBd.isPresent()) {
+                Optional<DeviceId> optDevId = optBd.get().deviceId();
+                if (optDevId.isPresent()) {
+                    log.info("Updates {} of device id with existing device id {}", BRIDGE_UNDERLAY, optDevId.get());
+                    strOfDevIdUnderlay = optDevId.get().toString();
+                } else {
+                    DeviceId devId = OvsUtil.buildOfDeviceId(IpAddress.valueOf(strMgmtIp),
+                            DEVID_IDX_BRIDGE_UNDERLAY_NOVA);
+                    log.info("Failed to find devId. Updates {} of device id with new device id {}",
+                            BRIDGE_UNDERLAY, devId);
+                    strOfDevIdUnderlay = devId.toString();
+                }
+            } else {
+                DeviceId devId = OvsUtil.buildOfDeviceId(IpAddress.valueOf(strMgmtIp), DEVID_IDX_BRIDGE_UNDERLAY_NOVA);
+                log.info("Failed to find description. Updates {} of device id with new device id {}",
+                        BRIDGE_UNDERLAY, devId);
+                strOfDevIdUnderlay = devId.toString();
+            }
+
+            context.completed();
+        }
+    }
+
+    /**
+     * Work-let class for creating underlay openflow bridge.
+     */
+    public static class CreateUnderlayBridge extends AbstractWorklet {
+
+        @JsonDataModel(path = MODEL_MGMT_IP)
+        String strMgmtIp;
+
+        @JsonDataModel(path = MODEL_OVSDB_PORT)
+        Integer intOvsdbPort;
+
+        @JsonDataModel(path = MODEL_OVS_DATAPATH_TYPE)
+        String strOvsDatapath;
+
+        @JsonDataModel(path = MODEL_OF_DEVID_UNDERLAY_BRIDGE)
+        String strOfDevIdUnderlay;
+
+        @Override
+        public boolean isNext(WorkflowContext context) throws WorkflowException {
+
+            check(strOfDevIdUnderlay != null, "invalid strOfDevIdUnderlay");
+            return !OvsUtil.isAvailableBridge(context, DeviceId.deviceId(strOfDevIdUnderlay));
+        }
+
+        @Override
+        public void process(WorkflowContext context) throws WorkflowException {
+
+            check(strOfDevIdUnderlay != null, "invalid strOfDevIdUnderlay");
+            BridgeConfig bridgeConfig = OvsUtil.getOvsdbBehaviour(context, strMgmtIp, BridgeConfig.class);
+            List<ControllerInfo> ofControllers = OvsUtil.getOpenflowControllerInfoList(context);
+            DeviceId ofDeviceId = DeviceId.deviceId(strOfDevIdUnderlay);
+
+            if (ofControllers == null || ofControllers.size() == 0) {
+                throw new WorkflowException("Invalid of controllers");
+            }
+
+            Optional<BridgeDescription> optBd = OvsUtil.getBridgeDescription(bridgeConfig, BRIDGE_UNDERLAY);
+            if (!optBd.isPresent()) {
+
+                // If bridge does not exist, just creates a new bridge.
+                context.waitCompletion(DeviceEvent.class, ofDeviceId.toString(),
+                        () -> OvsUtil.createBridge(bridgeConfig,
+                                BRIDGE_UNDERLAY,
+                                OvsUtil.bridgeDatapathId(ofDeviceId),
+                                ofControllers,
+                                OvsUtil.buildOvsDatapathType(strOvsDatapath)),
+                        TIMEOUT_DEVICE_CREATION_MS
+                );
+                return;
+
+            } else {
+                BridgeDescription bd = optBd.get();
+                if (OvsUtil.isEqual(ofControllers, bd.controllers())) {
+                    log.error("{} has valid controller setting({})", BRIDGE_UNDERLAY, bd.controllers());
+                    context.completed();
+                    return;
+                }
+
+                OvsdbClientService ovsdbClient = OvsUtil.getOvsdbClient(context, strMgmtIp, intOvsdbPort);
+                if (ovsdbClient == null || !ovsdbClient.isConnected()) {
+                    throw new WorkflowException("Invalid ovsdb client for " + strMgmtIp);
+                }
+
+                // If controller settings are not matched, set controller with valid controller information.
+                context.waitCompletion(DeviceEvent.class, ofDeviceId.toString(),
+                        () -> ovsdbClient.setControllersWithDeviceId(bd.deviceId().get(), ofControllers),
+                        TIMEOUT_DEVICE_CREATION_MS
+                );
+                return;
+            }
+        }
+
+        @Override
+        public boolean isCompleted(WorkflowContext context, Event event)throws WorkflowException {
+            if (!(event instanceof DeviceEvent)) {
+                return false;
+            }
+            DeviceEvent deviceEvent = (DeviceEvent) event;
+            Device device = deviceEvent.subject();
+            switch (deviceEvent.type()) {
+                case DEVICE_ADDED:
+                case DEVICE_AVAILABILITY_CHANGED:
+                case DEVICE_UPDATED:
+                    return context.getService(DeviceService.class).isAvailable(device.id());
+                default:
+                    return false;
+            }
+        }
+
+        @Override
+        public void timeout(WorkflowContext context) throws WorkflowException {
+            if (!isNext(context)) {
+                context.completed(); //Complete the job of worklet by timeout
+            } else {
+                super.timeout(context);
+            }
+        }
+
+    }
+
+    /**
+     * Work-let class for creating vxlan port on the overlay bridge.
+     */
+    public static class CreateOverlayBridgeVxlanPort extends AbstractWorklet {
+
+        @JsonDataModel(path = MODEL_MGMT_IP)
+        String strMgmtIp;
+
+        @JsonDataModel(path = MODEL_OF_DEVID_OVERLAY_BRIDGE, optional = true)
+        String strOfDevIdOverlay;
+
+        private static final String OVS_VXLAN_PORTNAME = "vxlan";
+
+        @Override
+        public boolean isNext(WorkflowContext context) throws WorkflowException {
+
+            check(strOfDevIdOverlay != null, "invalid strOfDevIdOverlay");
+            DeviceId deviceId = DeviceId.deviceId(strOfDevIdOverlay);
+            if (Objects.isNull(deviceId)) {
+                throw new WorkflowException("Invalid br-int bridge, before creating VXLAN port");
+            }
+
+            DeviceService deviceService = context.getService(DeviceService.class);
+            return !deviceService.getPorts(deviceId)
+                    .stream()
+                    .filter(port -> OvsUtil.portName(port).contains(OVS_VXLAN_PORTNAME) && port.isEnabled())
+                    .findAny().isPresent();
+        }
+
+
+        @Override
+        public void process(WorkflowContext context) throws WorkflowException {
+
+            check(strOfDevIdOverlay != null, "invalid strOfDevIdOverlay");
+            TunnelDescription description = DefaultTunnelDescription.builder()
+                    .deviceId(BRIDGE_OVERLAY)
+                    .ifaceName(OVS_VXLAN_PORTNAME)
+                    .type(TunnelDescription.Type.VXLAN)
+                    .remote(TunnelEndPoints.flowTunnelEndpoint())
+                    .key(TunnelKeys.flowTunnelKey())
+                    .build();
+
+            DeviceId ofDeviceId = DeviceId.deviceId(strOfDevIdOverlay);
+            InterfaceConfig interfaceConfig = OvsUtil.getOvsdbBehaviour(context, strMgmtIp, InterfaceConfig.class);
+
+            context.waitCompletion(DeviceEvent.class, ofDeviceId.toString(),
+                    () -> interfaceConfig.addTunnelMode(BRIDGE_OVERLAY, description),
+                    TIMEOUT_DEVICE_CREATION_MS
+            );
+        }
+
+        @Override
+        public boolean isCompleted(WorkflowContext context, Event event)throws WorkflowException {
+            if (!(event instanceof DeviceEvent)) {
+                return false;
+            }
+            DeviceEvent deviceEvent = (DeviceEvent) event;
+            switch (deviceEvent.type()) {
+                case PORT_ADDED:
+                    return !isNext(context);
+                default:
+                    return false;
+            }
+        }
+
+        @Override
+        public void timeout(WorkflowContext context) throws WorkflowException {
+            if (!isNext(context)) {
+                context.completed(); //Complete the job of worklet by timeout
+            } else {
+                super.timeout(context);
+            }
+        }
+    }
+
+    /**
+     * Work-let class for adding physical ports on the underlay openflow bridge.
+     */
+    public static class AddPhysicalPortsOnUnderlayBridge extends AbstractWorklet {
+
+        @JsonDataModel(path = MODEL_MGMT_IP)
+        String strMgmtIp;
+
+        @JsonDataModel(path = MODEL_OVSDB_PORT)
+        Integer intOvsdbPort;
+
+        @JsonDataModel(path = MODEL_OF_DEVID_UNDERLAY_BRIDGE, optional = true)
+        String strOfDevIdUnderlay;
+
+        @JsonDataModel(path = MODEL_OVS_DATAPATH_TYPE)
+        String strOvsDatapath;
+
+        @JsonDataModel(path = MODEL_PHY_PORTS)
+        ArrayNode arrNodePhysicalPorts;
+
+        @Override
+        public boolean isNext(WorkflowContext context) throws WorkflowException {
+            check(strOfDevIdUnderlay != null, "invalid strOfDevIdUnderlay");
+            DeviceId brphyDevId = DeviceId.deviceId(strOfDevIdUnderlay);
+            return !hasAllPhysicalPorts(context, brphyDevId);
+        }
+
+        @Override
+        public void process(WorkflowContext context) throws WorkflowException {
+            check(strOfDevIdUnderlay != null, "invalid strOfDevIdUnderlay");
+            DeviceId brphyDevId = DeviceId.deviceId(strOfDevIdUnderlay);
+
+            context.waitCompletion(DeviceEvent.class, brphyDevId.toString(),
+                    () -> addPhysicalPorts(context, brphyDevId, BRIDGE_UNDERLAY, strOvsDatapath),
+                    TIMEOUT_PORT_ADDITION_MS
+            );
+        }
+
+        @Override
+        public boolean isCompleted(WorkflowContext context, Event event)throws WorkflowException {
+            if (!(event instanceof DeviceEvent)) {
+                return false;
+            }
+            DeviceEvent deviceEvent = (DeviceEvent) event;
+            switch (deviceEvent.type()) {
+                case PORT_ADDED:
+                    return !isNext(context);
+                default:
+                    return false;
+            }
+        }
+
+        @Override
+        public void timeout(WorkflowContext context) throws WorkflowException {
+            if (!isNext(context)) {
+                context.completed(); //Complete the job of worklet by timeout
+            } else {
+                super.timeout(context);
+            }
+        }
+
+        private final List<String> getPhysicalPorts(WorkflowContext context) throws WorkflowException {
+            List<String> ports = Lists.newArrayList();
+            for (JsonNode jsonNode : arrNodePhysicalPorts) {
+                check(jsonNode instanceof TextNode, "Invalid physical ports " + arrNodePhysicalPorts);
+                ports.add(jsonNode.asText());
+            }
+            return ports;
+        }
+
+        private final boolean hasAllPhysicalPorts(WorkflowContext context, DeviceId devId) throws WorkflowException {
+
+            List<Port> devPorts = context.getService(DeviceService.class).getPorts(devId);
+            check(devPorts != null, "Invalid device ports for " + devId);
+            List<String> physicalPorts = getPhysicalPorts(context);
+            check(physicalPorts != null, "Invalid physical ports" + context);
+
+            log.info("physicalPorts: {} for {}", physicalPorts, devId);
+            for (String port: physicalPorts) {
+                if (devPorts.stream().noneMatch(p -> OvsUtil.portName(p).contains(port))) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        private final boolean hasPort(WorkflowContext context, DeviceId devId, String portName)
+                throws WorkflowException {
+
+            List<Port> devPorts = context.getService(DeviceService.class).getPorts(devId);
+            check(devPorts != null, "Invalid device ports for " + devId);
+            return devPorts.stream().anyMatch(p -> OvsUtil.portName(p).contains(portName));
+        }
+
+        private final void addPhysicalPorts(WorkflowContext context, DeviceId devId, String bridgeName,
+                                            String strOvsDatapathType)
+                throws WorkflowException {
+            OvsdbClientService ovsdbClient = OvsUtil.getOvsdbClient(context, strMgmtIp, intOvsdbPort);
+            check(ovsdbClient != null, "Invalid ovsdb client");
+
+            List<String> physicalPorts = getPhysicalPorts(context);
+            check(physicalPorts != null, "Invalid physical ports");
+
+            OvsDatapathType datapathType = OvsUtil.buildOvsDatapathType(strOvsDatapathType);
+            check(datapathType != null, "Invalid data path type");
+
+            List<String> sortedPhyPorts = physicalPorts.stream().sorted().collect(Collectors.toList());
+
+            for (String port: sortedPhyPorts) {
+                if (hasPort(context, devId, port)) {
+                    continue;
+                }
+                log.info("adding port {} on {}", port, devId);
+                switch (datapathType) {
+                    case NETDEV:
+                        throw new WorkflowException("NETDEV datapathType are not supported");
+                        //break;
+                    case SYSTEM:
+                    default:
+                        ovsdbClient.createPort(BridgeName.bridgeName(bridgeName).name(), port);
+                }
+            }
+        }
+    }
+
+    /**
+     * Work-let class for configure local ip of underlay openflow bridge.
+     */
+    public static class ConfigureUnderlayBridgeLocalIp extends AbstractWorklet {
+
+        @JsonDataModel(path = MODEL_SSH_ACCESSINFO)
+        JsonNode strSshAccessInfo;
+
+        @JsonDataModel(path = MODEL_VTEP_IP)
+        String strVtepIp;
+
+        @Override
+        public boolean isNext(WorkflowContext context) throws WorkflowException {
+
+            SshAccessInfo sshAccessInfo = SshAccessInfo.valueOf(strSshAccessInfo);
+            check(Objects.nonNull(sshAccessInfo), "Invalid ssh access info " + context.data());
+
+            NetworkAddress vtepIp = NetworkAddress.valueOf(strVtepIp);
+            check(Objects.nonNull(vtepIp), "Invalid vtep ip " + context.data());
+
+            return !SshUtil.exec(sshAccessInfo,
+                    session ->
+                            SshUtil.hasIpAddrOnInterface(session, BRIDGE_UNDERLAY, vtepIp)
+                                    && SshUtil.isIpLinkUpOnInterface(session, BRIDGE_UNDERLAY)
+            );
+        }
+
+        @Override
+        public void process(WorkflowContext context) throws WorkflowException {
+
+            SshAccessInfo sshAccessInfo = SshAccessInfo.valueOf(strSshAccessInfo);
+            check(Objects.nonNull(sshAccessInfo), "Invalid ssh access info " + context.data());
+
+            NetworkAddress vtepIp = NetworkAddress.valueOf(strVtepIp);
+            check(Objects.nonNull(vtepIp), "Invalid vtep ip " + context.data());
+
+            SshUtil.exec(sshAccessInfo,
+                    session -> {
+                        SshUtil.addIpAddrOnInterface(session, BRIDGE_UNDERLAY, vtepIp);
+                        SshUtil.setIpLinkUpOnInterface(session, BRIDGE_UNDERLAY);
+                        return "";
+                    });
+
+            context.completed();
+        }
+    }
+
+    /**
+     * Work-let class for deleting overlay bridge config.
+     */
+    public static class DeleteOverlayBridgeConfig extends AbstractWorklet {
+
+        @JsonDataModel(path = MODEL_MGMT_IP)
+        String strMgmtIp;
+
+        @Override
+        public boolean isNext(WorkflowContext context) throws WorkflowException {
+
+            BridgeConfig bridgeConfig = OvsUtil.getOvsdbBehaviour(context, strMgmtIp, BridgeConfig.class);
+
+            Collection<BridgeDescription> bridges = bridgeConfig.getBridges();
+            return bridges.stream()
+                    .anyMatch(bd -> Objects.equals(bd.name(), BRIDGE_OVERLAY));
+        }
+
+        @Override
+        public void process(WorkflowContext context) throws WorkflowException {
+
+            BridgeConfig bridgeConfig = OvsUtil.getOvsdbBehaviour(context, strMgmtIp, BridgeConfig.class);
+
+            bridgeConfig.deleteBridge(BridgeName.bridgeName(BRIDGE_OVERLAY));
+
+            for (int i = 0; i < 10; i++) {
+                if (!isNext(context)) {
+                    context.completed();
+                    return;
+                }
+                sleep(50);
+            }
+            throw new WorkflowException("Timeout happened for removing config");
+        }
+
+        protected void sleep(long ms) {
+            try {
+                Thread.sleep(ms);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * Work-let class for removing overlay bridge openflow device.
+     */
+    public static class RemoveOverlayBridgeOfDevice extends AbstractWorklet {
+
+        @JsonDataModel(path = MODEL_MGMT_IP)
+        String strMgmtIp;
+
+        @Override
+        public boolean isNext(WorkflowContext context) throws WorkflowException {
+
+            DeviceId devId = OvsUtil.buildOfDeviceId(IpAddress.valueOf(strMgmtIp), DEVID_IDX_BRIDGE_OVERLAY);
+
+            Device dev = context.getService(DeviceService.class).getDevice(devId);
+            return dev != null;
+        }
+
+        @Override
+        public void process(WorkflowContext context) throws WorkflowException {
+
+            DeviceId devId = OvsUtil.buildOfDeviceId(IpAddress.valueOf(strMgmtIp), DEVID_IDX_BRIDGE_OVERLAY);
+
+            DeviceAdminService adminService = context.getService(DeviceAdminService.class);
+
+            context.waitCompletion(DeviceEvent.class, devId.toString(),
+                    () -> adminService.removeDevice(devId),
+                    TIMEOUT_DEVICE_CREATION_MS
+            );
+        }
+
+        @Override
+        public boolean isCompleted(WorkflowContext context, Event event)throws WorkflowException {
+            if (!(event instanceof DeviceEvent)) {
+                return false;
+            }
+            DeviceEvent deviceEvent = (DeviceEvent) event;
+            switch (deviceEvent.type()) {
+                case DEVICE_REMOVED:
+                    return !isNext(context);
+                default:
+                    return false;
+            }
+        }
+
+        @Override
+        public void timeout(WorkflowContext context) throws WorkflowException {
+            if (!isNext(context)) {
+                context.completed(); //Complete the job of worklet by timeout
+            } else {
+                super.timeout(context);
+            }
+        }
+    }
+
+    /**
+     * Work-let class for deleting underlay bridge config.
+     */
+    public static class DeleteUnderlayBridgeConfig extends AbstractWorklet {
+
+        @JsonDataModel(path = MODEL_MGMT_IP)
+        String strMgmtIp;
+
+        @Override
+        public boolean isNext(WorkflowContext context) throws WorkflowException {
+
+            BridgeConfig bridgeConfig = OvsUtil.getOvsdbBehaviour(context, strMgmtIp, BridgeConfig.class);
+
+            Collection<BridgeDescription> bridges = bridgeConfig.getBridges();
+            return bridges.stream()
+                    .anyMatch(bd -> Objects.equals(bd.name(), BRIDGE_UNDERLAY));
+        }
+
+        @Override
+        public void process(WorkflowContext context) throws WorkflowException {
+
+            BridgeConfig bridgeConfig = OvsUtil.getOvsdbBehaviour(context, strMgmtIp, BridgeConfig.class);
+
+            bridgeConfig.deleteBridge(BridgeName.bridgeName(BRIDGE_UNDERLAY));
+
+            for (int i = 0; i < 10; i++) {
+                if (!isNext(context)) {
+                    context.completed();
+                    return;
+                }
+                sleep(50);
+            }
+            throw new WorkflowException("Timeout happened for removing config");
+        }
+
+        protected void sleep(long ms) {
+            try {
+                Thread.sleep(ms);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * Work-let class for removing underlay bridge openflow device.
+     */
+    public static class RemoveUnderlayBridgeOfDevice extends AbstractWorklet {
+
+        @JsonDataModel(path = MODEL_MGMT_IP)
+        String strMgmtIp;
+
+        @Override
+        public boolean isNext(WorkflowContext context) throws WorkflowException {
+
+            DeviceId devId = OvsUtil.buildOfDeviceId(IpAddress.valueOf(strMgmtIp), DEVID_IDX_BRIDGE_UNDERLAY_NOVA);
+
+            Device dev = context.getService(DeviceService.class).getDevice(devId);
+            return dev != null;
+        }
+
+        @Override
+        public void process(WorkflowContext context) throws WorkflowException {
+
+            DeviceId devId = OvsUtil.buildOfDeviceId(IpAddress.valueOf(strMgmtIp), DEVID_IDX_BRIDGE_UNDERLAY_NOVA);
+
+            DeviceAdminService adminService = context.getService(DeviceAdminService.class);
+
+            context.waitCompletion(DeviceEvent.class, devId.toString(),
+                    () -> adminService.removeDevice(devId),
+                    TIMEOUT_DEVICE_CREATION_MS
+            );
+        }
+
+        @Override
+        public boolean isCompleted(WorkflowContext context, Event event)throws WorkflowException {
+            if (!(event instanceof DeviceEvent)) {
+                return false;
+            }
+            DeviceEvent deviceEvent = (DeviceEvent) event;
+            switch (deviceEvent.type()) {
+                case DEVICE_REMOVED:
+                    return !isNext(context);
+                default:
+                    return false;
+            }
+        }
+
+        @Override
+        public void timeout(WorkflowContext context) throws WorkflowException {
+            if (!isNext(context)) {
+                context.completed(); //Complete the job of worklet by timeout
+            } else {
+                super.timeout(context);
+            }
+        }
+    }
+}
diff --git a/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/package-info.java b/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/package-info.java
new file mode 100644
index 0000000..42214bc
--- /dev/null
+++ b/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Open-flow overlay implementation package.
+ */
+package org.onosproject.ofoverlay.impl;
diff --git a/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/util/NetworkAddress.java b/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/util/NetworkAddress.java
new file mode 100644
index 0000000..c8a8829
--- /dev/null
+++ b/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/util/NetworkAddress.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.
+ */
+package org.onosproject.ofoverlay.impl.util;
+
+import com.google.common.base.MoreObjects;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Representation of a network address, which consists of IP address and prefix.
+ */
+public final class NetworkAddress {
+    private final IpAddress ip;
+    private final IpPrefix prefix;
+
+    /**
+     * Constructor for a given IP address and prefix.
+     *
+     * @param ip ip address
+     * @param prefix ip prefix
+     */
+    private NetworkAddress(IpAddress ip, IpPrefix prefix) {
+        this.ip = ip;
+        this.prefix = prefix;
+    }
+
+    /**
+     * Converts a CIDR notation string into a network address.
+     *
+     * @param cidr cidr
+     * @return network address
+     * @throws IllegalArgumentException if the cidr is not valid
+     */
+    public static NetworkAddress valueOf(String cidr) {
+        checkArgument(cidr.contains("/"));
+
+        IpAddress ipAddress = IpAddress.valueOf(cidr.split("/")[0]);
+        IpPrefix ipPrefix = IpPrefix.valueOf(cidr);
+
+        return new NetworkAddress(ipAddress, ipPrefix);
+    }
+
+    /**
+     * Returns the IP address value of the network address.
+     *
+     * @return ip address
+     */
+    public IpAddress ip() {
+        return this.ip;
+    }
+
+    /**
+     * Returns the IP prefix value of the network address.
+     *
+     * @return ip prefix
+     */
+    public IpPrefix prefix() {
+        return this.prefix;
+    }
+
+    /**
+     * Converts a network address to a CIDR notation.
+     *
+     * @return cidr notation string
+     */
+    public String cidr() {
+        return ip.toString() + "/" + prefix.prefixLength();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj instanceof NetworkAddress) {
+            NetworkAddress that = (NetworkAddress) obj;
+            return Objects.equals(ip, that.ip) && Objects.equals(prefix, that.prefix);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(ip, prefix);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("IpAddress", ip)
+                .add("IpPrefix", prefix)
+                .toString();
+    }
+}
\ No newline at end of file
diff --git a/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/util/OvsDatapathType.java b/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/util/OvsDatapathType.java
new file mode 100644
index 0000000..0a64e93
--- /dev/null
+++ b/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/util/OvsDatapathType.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.
+ */
+package org.onosproject.ofoverlay.impl.util;
+
+/**
+ * Enumeration for OVS data path type.
+ */
+public enum OvsDatapathType {
+    EMPTY(""),
+    NETDEV("netdev"),
+    SYSTEM("system");
+
+    private final String value;
+
+    /**
+     * Constructor for OvsDatapathType enumeration.
+     *
+     * @param value string OvsDatapathType
+     */
+    OvsDatapathType(String value) {
+        this.value = value;
+    }
+
+    @Override
+    public String toString() {
+        return value;
+    }
+}
diff --git a/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/util/OvsVersion.java b/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/util/OvsVersion.java
new file mode 100644
index 0000000..015ac73
--- /dev/null
+++ b/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/util/OvsVersion.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.
+ */
+package org.onosproject.ofoverlay.impl.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Class for OVS Version.
+ */
+public final class OvsVersion {
+    protected static final Logger log = LoggerFactory.getLogger(OvsVersion.class);
+
+    private int[] versionElements = new int[] {0, 0, 0, Integer.MAX_VALUE};
+    private int depth = 0;
+
+    /**
+     * Constructor for OvsVersion.
+     * @param ovsVerStr OVS version string
+     */
+    private OvsVersion(String ovsVerStr) {
+
+        // Supporting
+        // 1.2.3 (depth = 3, public release)
+        // 1.2.3.4 (depth = 4, beta release)
+        Matcher m = Pattern.compile("(\\d+)\\.(\\d+)\\.(\\d+)(\\.(\\d+))?").matcher(ovsVerStr);
+
+        if (!m.matches()) {
+            throw new IllegalArgumentException("Malformed OVS version");
+        }
+
+        versionElements[0] = Integer.parseInt(m.group(1));
+        versionElements[1] = Integer.parseInt(m.group(2));
+        versionElements[2] = Integer.parseInt(m.group(3));
+
+        if (m.group(4) == null) {
+            depth = 3;
+            return;
+        }
+
+        versionElements[3] = Integer.parseInt(m.group(5));
+        depth = 4;
+    }
+
+    /**
+     * Builder for OvsVersion.
+     * @param ovsVersionStr OVS version string
+     * @return ovs version
+     */
+    public static OvsVersion build(String ovsVersionStr) {
+        try {
+            return new OvsVersion(ovsVersionStr);
+        } catch (IllegalArgumentException e) {
+            log.error("Exception Occurred {}", e);
+            return null;
+        }
+    }
+
+    private int get(int level) {
+        return versionElements[level];
+    }
+
+    private int compare(OvsVersion tgt) {
+        //Comparison example
+        // 2.7.0 < 2.7.2
+        // 2.7.0 > 2.6.9.12
+        // 2.7.0 > 2.7.0.0 (because 2.7.0 is public release)
+        for (int i = 0; i < versionElements.length; i++) {
+            if (versionElements[i] == tgt.get(i)) {
+                continue;
+            } else if (versionElements[i] < tgt.get(i)) {
+                return (i + 1) * -1;
+            } else {
+                return (i + 1);
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * Returns whether this OVS version is equal to the target OVS version.
+     * @param tgt taret OVS version
+     * @return whether this OVS version is equal to the target OVS version
+     */
+    public boolean isEqOf(OvsVersion tgt) {
+        return (compare(tgt) == 0);
+    }
+
+    /**
+     * Returns whether this OVS version is prior to the target OVS version.
+     * @param tgt taret OVS version
+     * @return whether this OVS version is prior to the target OVS version
+     */
+    public boolean isPriorOf(OvsVersion tgt) {
+        return (compare(tgt) < 0);
+    }
+
+    /**
+     * Returns whether this OVS version is prior or equal to the target OVS version.
+     * @param tgt taret OVS version
+     * @return whether this OVS version is prior or equal to the target OVS version
+     */
+    public boolean isPriorOrEqOf(OvsVersion tgt) {
+        return (compare(tgt) <= 0);
+    }
+
+    /**
+     * Returns whether this OVS version is later to the target OVS version.
+     * @param tgt taret OVS version
+     * @return whether this OVS version is later to the target OVS version
+     */
+    public boolean isLaterOf(OvsVersion tgt) {
+        return (compare(tgt) > 0);
+    }
+
+    /**
+     * Returns whether this OVS version is later or equal to the target OVS version.
+     * @param tgt taret OVS version
+     * @return whether this OVS version is later or equal to the target OVS version
+     */
+    public boolean isLaterOrEqOf(OvsVersion tgt) {
+        return (compare(tgt) >= 0);
+    }
+
+    @Override
+    public String toString() {
+
+        StringBuilder strbuild = new StringBuilder();
+        strbuild.append(versionElements[0]);
+
+        for (int i = 1; i < depth; i++) {
+            strbuild.append(".");
+            strbuild.append(versionElements[i]);
+        }
+        return strbuild.toString();
+    }
+
+}
\ No newline at end of file
diff --git a/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/util/SshBehavior.java b/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/util/SshBehavior.java
new file mode 100644
index 0000000..6189837
--- /dev/null
+++ b/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/util/SshBehavior.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.
+ */
+package org.onosproject.ofoverlay.impl.util;
+
+import com.jcraft.jsch.Session;
+import org.onosproject.workflow.api.WorkflowException;
+
+/**
+ * Functional interface for ssh behavior.
+ * @param <R> return value of ssh behavior
+ */
+@FunctionalInterface
+public interface SshBehavior<R> {
+
+    R apply(Session session) throws WorkflowException;
+
+}
diff --git a/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/util/SshUtil.java b/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/util/SshUtil.java
new file mode 100644
index 0000000..0c6caff
--- /dev/null
+++ b/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/util/SshUtil.java
@@ -0,0 +1,366 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.
+ */
+package org.onosproject.ofoverlay.impl.util;
+
+import com.google.common.io.CharStreams;
+import com.jcraft.jsch.Channel;
+import com.jcraft.jsch.ChannelExec;
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.Session;
+import org.onlab.packet.IpAddress;
+import org.onosproject.workflow.api.WorkflowException;
+import org.onosproject.workflow.model.accessinfo.SshAccessInfo;
+import org.slf4j.Logger;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import static org.onosproject.workflow.api.CheckCondition.check;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Class for SSH utilities.
+ */
+public final class SshUtil {
+
+    protected static final Logger log = getLogger(SshUtil.class);
+
+    private static final String STRICT_HOST_CHECKING = "StrictHostKeyChecking";
+    private static final String DEFAULT_STRICT_HOST_CHECKING = "no";
+    private static final int DEFAULT_SESSION_TIMEOUT = 30000; // milliseconds
+
+    private static final String SPACESEPERATOR = " ";
+
+    /**
+     * Default constructor.
+     */
+    private SshUtil() {
+    }
+
+    /**
+     * Creates a new session with a given ssh access information.
+     *
+     * @param sshInfo information to ssh to the remote server
+     * @return ssh session, or null
+     */
+    public static Session connect(SshAccessInfo sshInfo) {
+        Session session;
+
+        try {
+            JSch jsch = new JSch();
+            jsch.addIdentity(sshInfo.privateKey());
+
+            session = jsch.getSession(sshInfo.user(),
+                    sshInfo.remoteIp().toString(),
+                    sshInfo.port().toInt());
+            session.setConfig(STRICT_HOST_CHECKING, DEFAULT_STRICT_HOST_CHECKING);
+            session.connect(DEFAULT_SESSION_TIMEOUT);
+
+        } catch (JSchException e) {
+            log.warn("Failed to connect to {}", sshInfo.toString(), e);
+            session = authUserPwd(sshInfo);
+        }
+        return session;
+    }
+
+    /**
+     * Creates a new session with ssh access info.
+     *
+     * @param sshInfo information to ssh to the remote server
+     * @return ssh session, or null
+     */
+    public static Session authUserPwd(SshAccessInfo sshInfo) {
+        log.info("Retrying Session with {}", sshInfo);
+        try {
+            JSch jsch = new JSch();
+
+            Session session = jsch.getSession(sshInfo.user(),
+                    sshInfo.remoteIp().toString(),
+                    sshInfo.port().toInt());
+            session.setPassword(sshInfo.password());
+            session.setConfig(STRICT_HOST_CHECKING, DEFAULT_STRICT_HOST_CHECKING);
+            session.connect(DEFAULT_SESSION_TIMEOUT);
+
+            return session;
+        } catch (JSchException e) {
+            log.warn("Failed to connect to {} due to {}", sshInfo.toString(), e);
+            return null;
+        }
+    }
+
+    /**
+     * Closes a connection.
+     *
+     * @param session session ssh session
+     */
+    public static void disconnect(Session session) {
+        if (session.isConnected()) {
+            session.disconnect();
+        }
+    }
+
+    /**
+     * Fetches last term after executing command.
+     * @param session  ssh session
+     * @param command command to execute
+     * @return last term, or null
+     */
+    public static String fetchLastTerm(Session session, String command) {
+         if (session == null || !session.isConnected()) {
+             log.error("Invalid session({})", session);
+             return null;
+         }
+
+         log.info("fetchLastTerm: ssh command {} to {}", command, session.getHost());
+
+         try {
+             Channel channel = session.openChannel("exec");
+             if (channel == null) {
+                 log.error("Invalid channel of session({}) for command({})", session, command);
+                 return null;
+             }
+
+             ((ChannelExec) channel).setCommand(command);
+             channel.setInputStream(null);
+             InputStream output = channel.getInputStream();
+             channel.connect();
+             String[] lineList = null;
+
+             try (BufferedReader reader = new BufferedReader(new InputStreamReader(output, StandardCharsets.UTF_8))) {
+                 lineList = reader.lines().findFirst().get().split(SPACESEPERATOR);
+             } catch (IOException e) {
+                 log.error("Exception in fetchLastTerm", e);
+             } finally {
+                 channel.disconnect();
+                 output.close();
+             }
+
+             if (lineList.length > 0) {
+                 return lineList[lineList.length - 1];
+             } else {
+                 return null;
+             }
+
+         } catch (JSchException | IOException e) {
+             log.error("Exception in fetchLastTerm", e);
+             return null;
+         }
+    }
+
+    /**
+     * Executes a given command. It opens exec channel for the command and closes
+     * the channel when it's done.
+     *
+     * @param session ssh connection to a remote server
+     * @param command command to execute
+     * @return command output string if the command succeeds, or null
+     */
+    public static String executeCommand(Session session, String command) {
+        if (session == null || !session.isConnected()) {
+            log.error("Invalid session({})", session);
+            return null;
+        }
+
+        log.info("executeCommand: ssh command {} to {}", command, session.getHost());
+
+        try {
+            Channel channel = session.openChannel("exec");
+
+            if (channel == null) {
+                log.debug("Invalid channel of session({}) for command({})", session, command);
+                return null;
+            }
+
+            ((ChannelExec) channel).setCommand(command);
+            channel.setInputStream(null);
+            InputStream output = channel.getInputStream();
+
+            channel.connect();
+            String result = CharStreams.toString(new InputStreamReader(output, StandardCharsets.UTF_8));
+            log.trace("SSH result(on {}): {}", session.getHost(), result);
+            channel.disconnect();
+
+            return result;
+        } catch (JSchException | IOException e) {
+            log.debug("Failed to execute command {} due to {}", command, e);
+            return null;
+        }
+    }
+
+    /**
+     * Fetches OVS version information.
+     * @param session Jsch session
+     * @return OVS version
+     * @throws WorkflowException workflow exception
+     */
+    public static OvsVersion fetchOvsVersion(Session session) throws WorkflowException {
+
+        OvsVersion devOvsVersion;
+
+        String ovsVersionStr = fetchLastTerm(session, "ovs-vswitchd --version");
+        if (ovsVersionStr == null) {
+            log.error("Failed to get ovs Version String for ssh session:{}", session);
+            throw new WorkflowException("Failed to get ovs Version String");
+        }
+
+        devOvsVersion = OvsVersion.build(ovsVersionStr);
+        if (devOvsVersion == null) {
+            log.error("Failed to build OVS version for {}", ovsVersionStr);
+            throw new WorkflowException("Failed to build OVS version");
+        }
+
+        return devOvsVersion;
+    }
+
+    private static final String IP_PATTERN = "^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
+            "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
+            "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
+            "([01]?\\d\\d?|2[0-4]\\d|25[0-5])$";
+
+    private static boolean isIPv6(String address) {
+        boolean isCorrect = true;
+        try {
+            IpAddress.valueOf(address);
+        } catch (IllegalArgumentException e) {
+            log.debug("Exception Occurred {}", e.toString());
+            isCorrect = false;
+        }
+        return isCorrect;
+    }
+
+    private static boolean isCidr(String s) {
+        String[] splits = s.split("/");
+        return splits.length == 2 &&
+                (splits[0].matches(IP_PATTERN) || isIPv6(splits[0]));
+    }
+
+    /**
+     * Adds IP address on the interface.
+     * @param session SSH session
+     * @param ifname interface name
+     * @param address network address
+     * @throws WorkflowException workflow exception
+     */
+    public static void addIpAddrOnInterface(Session session, String ifname, NetworkAddress address)
+            throws WorkflowException {
+
+        executeCommand(session, String.format("ip addr add %s dev %s", address.cidr(), ifname));
+
+        Set<NetworkAddress> result = getIpAddrOfInterface(session, ifname);
+        if (!result.contains(address)) {
+            throw new WorkflowException("Failed to set ip(" + address + ") on " + ifname + ",  result: " + result);
+        }
+    }
+
+    /**
+     * Gets IP addresses of interface.
+     * @param session SSH session
+     * @param ifname interface name
+     * @return IP addresses of interface
+     */
+    public static Set<NetworkAddress> getIpAddrOfInterface(Session session, String ifname) {
+
+        String output = executeCommand(session, String.format("ip addr show %s", ifname));
+
+        if (output == null) {
+            return Collections.emptySet();
+        }
+
+        Set<NetworkAddress> result = Pattern.compile(" ")
+                .splitAsStream(output)
+                .filter(SshUtil::isCidr)
+                .map(NetworkAddress::valueOf)
+                .collect(Collectors.toSet());
+        return result;
+    }
+
+    /**
+     * Returns whether the interface has IP address.
+     * @param session SSH session
+     * @param ifname interface name
+     * @param addr network address
+     * @return whether the interface has IP address
+     */
+    public static boolean hasIpAddrOnInterface(Session session, String ifname, NetworkAddress addr) {
+
+        Set<NetworkAddress> phyBrIps = getIpAddrOfInterface(session, ifname);
+
+        return phyBrIps.stream()
+                .anyMatch(ip -> addr.ip().equals(ip.ip()));
+    }
+
+    /**
+     * Sets IP link UP on the interface.
+     * @param session SSH session
+     * @param ifname interface name
+     * @throws WorkflowException workflow exception
+     */
+    public static void setIpLinkUpOnInterface(Session session, String ifname)
+            throws WorkflowException {
+
+        executeCommand(session, String.format("ip link set %s up", ifname));
+
+        if (!isIpLinkUpOnInterface(session, ifname)) {
+            throw new WorkflowException("Failed to set UP on " + ifname);
+        }
+    }
+
+    /**
+     * Returns whether the link of the interface is up.
+     * @param session SSH session
+     * @param ifname interface name
+     * @return whether the link of the interface is up
+     */
+    public static boolean isIpLinkUpOnInterface(Session session, String ifname) {
+        String output = executeCommand(session, String.format("ip link show %s", ifname));
+
+        return output != null && output.contains("UP");
+    }
+
+    /**
+     * Executes SSH behavior.
+     * @param sshAccessInfo SSH Access information
+     * @param behavior SSH behavior
+     * @param <R> Return type of SSH behavior
+     * @return return of SSH behavior
+     * @throws WorkflowException workflow exception
+     */
+    public static <R> R exec(SshAccessInfo sshAccessInfo, SshBehavior<R> behavior)
+            throws WorkflowException {
+
+        check(sshAccessInfo != null, "Invalid sshAccessInfo");
+        Session session = connect(sshAccessInfo);
+        if (session == null || !session.isConnected()) {
+            log.error("Failed to get session for ssh:{}", sshAccessInfo);
+            throw new WorkflowException("Failed to get session for ssh:" + sshAccessInfo);
+        }
+
+        try {
+            return behavior.apply(session);
+        } finally {
+            disconnect(session);
+        }
+    }
+
+}
diff --git a/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/util/SshkeyExchange.java b/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/util/SshkeyExchange.java
new file mode 100644
index 0000000..5615112
--- /dev/null
+++ b/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/util/SshkeyExchange.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.
+ */
+package org.onosproject.ofoverlay.impl.util;
+
+import com.jcraft.jsch.Channel;
+import com.jcraft.jsch.ChannelSftp;
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.KeyPair;
+import com.jcraft.jsch.Session;
+import com.jcraft.jsch.SftpATTRS;
+import com.jcraft.jsch.SftpException;
+import org.onlab.packet.IpAddress;
+import org.slf4j.Logger;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Class for SSH key exchange.
+ */
+public class SshkeyExchange {
+
+    protected static final Logger log = getLogger(SshkeyExchange.class);
+    private static final String HOME_ENV = System.getProperty("user.home");
+    private static final String USER_ENV = System.getProperty("user.name");
+    private static final String PUBLIC_KEY = "/.ssh/id_rsa.pub";
+    private static final String PRIVATE_KEY = "/.ssh/id_rsa";
+    private static final String SFTP_CHANNEL = "sftp";
+    private static final String SSH_HOME = "/.ssh/";
+    private static final String SSH_AUTH_KEY = "/.ssh/authorized_keys";
+    private static final int SFTPPORT = 22;
+    private static final int DIR_PER = 448;
+    private static final int FILE_PER = 384;
+    private static final int KEY_SIZE = 2048;
+    private static final int DEFAULT_SESSION_TIMEOUT = 30000; // milliseconds
+
+    private Session getJschSession(String user, String host, String password) {
+        java.util.Properties config = new java.util.Properties();
+        config.put("StrictHostKeyChecking", "no");
+        Session session;
+        try {
+            session = new JSch().getSession(user, host, SFTPPORT);
+            session.setPassword(password);
+            session.setConfig(config);
+        } catch (JSchException e) {
+            log.error("Exception in getJschSession", e);
+            return null;
+        }
+
+        return session;
+    }
+
+    private boolean generateKeyPair() {
+        KeyPair kpair;
+        StringBuilder command = new StringBuilder()
+                .append("chmod 600 ")
+                .append(HOME_ENV)
+                .append(PRIVATE_KEY);
+        try {
+            kpair = KeyPair.genKeyPair(new JSch(), KeyPair.RSA, KEY_SIZE);
+            kpair.writePrivateKey(HOME_ENV + PRIVATE_KEY);
+            kpair.writePublicKey(HOME_ENV + PUBLIC_KEY, USER_ENV);
+            Runtime.getRuntime().exec(command.toString());
+            kpair.dispose();
+        } catch (JSchException | IOException e) {
+            log.error("Exception in generateKeyPair", e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Exchanges SSH key.
+     * @param host SSH server host
+     * @param user user
+     * @param password password
+     * @return SSH key exchange success or not
+     */
+    public boolean sshAutoKeyExchange(IpAddress host, String user, String password) {
+
+        Session session = getJschSession(user, host.toString(), password);
+        boolean returnFlag;
+        File publickeyPath = new File(HOME_ENV + PUBLIC_KEY);
+        if (session == null) {
+            log.error("Error While establishing SFTP connection with {}", host.toString());
+            return false;
+        }
+        Channel channel;
+        String remoteHome;
+        ChannelSftp sftp;
+        FileInputStream fis = null;
+        SftpATTRS attrs = null;
+        try {
+            session.connect(DEFAULT_SESSION_TIMEOUT);
+            channel = session.openChannel(SFTP_CHANNEL);
+            if (channel == null) {
+                log.error("SFTP channel open failed for {}", host.toString());
+                return false;
+            }
+            channel.connect();
+            sftp = (ChannelSftp) channel;
+            remoteHome = sftp.getHome();
+            // checking key pair existance
+
+            if (!publickeyPath.exists()) {
+                File dirs = new File(HOME_ENV + SSH_HOME);
+                if (!dirs.exists() && !dirs.mkdirs()) {
+                    log.error("{} not exists and unable to create ", dirs.getPath());
+                    return false;
+                } else if (!generateKeyPair()) {
+                    log.error("SSH Key pair generation failed");
+                    return false;
+                }
+            }
+
+            // checking for authenticate_keys file existance
+            fis = new FileInputStream(publickeyPath);
+            try {
+                sftp.lstat(remoteHome + SSH_HOME);
+            } catch (SftpException e) {
+                sftp.mkdir(remoteHome + SSH_HOME);
+                sftp.chmod(700, remoteHome + SSH_HOME);
+            }
+            try {
+                attrs = sftp.lstat(remoteHome + SSH_AUTH_KEY);
+            } catch (SftpException e) {
+                log.info("authorized_keys file does not exist at remote device ,"
+                        + "a new file will be created");
+            }
+
+            if (attrs != null) {
+                sftp.get(remoteHome + SSH_AUTH_KEY, HOME_ENV + "/tempauthorized_keys");
+
+                String pubKey;
+                try (Stream<String> st = Files.lines(Paths.get(HOME_ENV + PUBLIC_KEY))) {
+                    pubKey = st.collect(Collectors.joining());
+                }
+
+                String authKey;
+                try (Stream<String> st = Files.lines(Paths.get(HOME_ENV + "/tempauthorized_keys"))) {
+                    authKey = st.collect(Collectors.joining());
+                }
+
+                if (authKey.contains(pubKey)) {
+                    log.info("Skipping key append to server as Key is already added");
+                } else {
+                    sftp.put(fis, remoteHome + SSH_AUTH_KEY, ChannelSftp.APPEND);
+                    log.info("Public key appended to server");
+                }
+            } else {
+
+                sftp.put(fis, remoteHome + SSH_AUTH_KEY, ChannelSftp.APPEND);
+                // Give proper permission to file and directory.
+                sftp.chmod(DIR_PER, remoteHome + SSH_HOME);
+                sftp.chmod(FILE_PER, remoteHome + SSH_AUTH_KEY);
+                log.info("Public key appended to server");
+            }
+
+            sftp.exit();
+            session.disconnect();
+            returnFlag = true;
+        } catch (JSchException | SftpException | IOException e) {
+            log.error("Exception occured because of {} ", e);
+            returnFlag = false;
+        } finally {
+            if (fis != null) {
+                try {
+                    fis.close();
+                } catch (IOException e) {
+                    log.info("Error closing public key file");
+                }
+            }
+        }
+        return returnFlag;
+
+    }
+}
diff --git a/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/util/package-info.java b/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/util/package-info.java
new file mode 100644
index 0000000..c76ba58
--- /dev/null
+++ b/apps/workflow/ofoverlay/app/src/main/java/org/onosproject/ofoverlay/impl/util/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Utility classes of ofoverlay.
+ */
+package org.onosproject.ofoverlay.impl.util;
\ No newline at end of file
diff --git a/apps/workflow/ofoverlay/test-cfg/empty.json b/apps/workflow/ofoverlay/test-cfg/empty.json
new file mode 100755
index 0000000..2691a31
--- /dev/null
+++ b/apps/workflow/ofoverlay/test-cfg/empty.json
@@ -0,0 +1,12 @@
+{
+    "apps": {
+        "org.onosproject.workflow": {
+          "workflow" : {
+            "rpc" : [
+            ]
+
+          }
+
+        }
+    }
+}
diff --git a/apps/workflow/ofoverlay/test-cfg/network-cfg-cr-wf.json b/apps/workflow/ofoverlay/test-cfg/network-cfg-cr-wf.json
new file mode 100755
index 0000000..19e2669
--- /dev/null
+++ b/apps/workflow/ofoverlay/test-cfg/network-cfg-cr-wf.json
@@ -0,0 +1,317 @@
+{
+    "apps": {
+        "org.onosproject.workflow": {
+          "workflow" : {
+            "rpc" : [
+              {
+                "op"   : "workflow.invoke",
+                "params" : {
+                  "workplace" : "Nova-000",
+                  "id"        : "of-overlay.workflow-nova",
+                  "data"      : {
+
+                    "mgmtIp" : "192.168.10.8",
+                    "ovsdbPort" : 6641,
+
+                    "sshAccessInfo" : {
+                      "remoteIp" : "192.168.10.8",
+                      "port"     : 22,
+                      "user"     : "root",
+                      "password" : "iloveyou",
+                      "keyfile"  : "~/.ssh/id_rsa"
+                    },
+
+                    "ovsDatapathType" : "system",
+                    "physicalPorts" : [ "nova0_1" ],
+                    "vtepIp" : "120.0.0.200/24",
+
+                    "annotations" : {
+                      "rackId" : 1,
+                      "rackPosition" : 3
+                    }
+
+                  }
+                },
+                "id" : "00001@10.0.0.1"
+              },
+              {
+                "op"   : "workflow.invoke",
+                "params" : {
+                  "workplace" : "Nova-001",
+                  "id"        : "of-overlay.workflow-nova",
+                  "data"      : {
+
+                    "mgmtIp" : "192.168.10.9",
+                    "ovsdbPort" : 6641,
+
+                    "sshAccessInfo" : {
+                      "remoteIp" : "192.168.10.9",
+                      "port"     : 22,
+                      "user"     : "root",
+                      "password" : "iloveyou",
+                      "keyfile"  : "~/.ssh/id_rsa"
+                    },
+
+                    "ovsDatapathType" : "system",
+                    "physicalPorts" : [ "nova1_1" ],
+                    "vtepIp" : "120.0.0.201/24",
+
+                    "annotations" : {
+                      "rackId" : 1,
+                      "rackPosition" : 3
+                    }
+
+                  }
+                },
+                "id" : "00001@10.0.0.1"
+              },
+              {
+                "op"   : "workflow.invoke",
+                "params" : {
+                  "workplace" : "Nova-002",
+                  "id"        : "of-overlay.workflow-nova",
+                  "data"      : {
+
+                    "mgmtIp" : "192.168.10.10",
+                    "ovsdbPort" : 6641,
+
+                    "sshAccessInfo" : {
+                      "remoteIp" : "192.168.10.10",
+                      "port"     : 22,
+                      "user"     : "root",
+                      "password" : "iloveyou",
+                      "keyfile"  : "~/.ssh/id_rsa"
+                    },
+
+                    "ovsDatapathType" : "system",
+                    "physicalPorts" : [ "nova2_1" ],
+                    "vtepIp" : "120.0.0.202/24",
+
+                    "annotations" : {
+                      "rackId" : 1,
+                      "rackPosition" : 3
+                    }
+
+                  }
+                },
+                "id" : "00001@10.0.0.1"
+              },
+              {
+                "op"   : "workflow.invoke",
+                "params" : {
+                  "workplace" : "Nova-003",
+                  "id"        : "of-overlay.workflow-nova",
+                  "data"      : {
+
+                    "mgmtIp" : "192.168.10.11",
+                    "ovsdbPort" : 6641,
+
+                    "sshAccessInfo" : {
+                      "remoteIp" : "192.168.10.11",
+                      "port"     : 22,
+                      "user"     : "root",
+                      "password" : "iloveyou",
+                      "keyfile"  : "~/.ssh/id_rsa"
+                    },
+
+                    "ovsDatapathType" : "system",
+                    "physicalPorts" : [ "nova3_1" ],
+                    "vtepIp" : "120.0.0.203/24",
+
+                    "annotations" : {
+                      "rackId" : 1,
+                      "rackPosition" : 3
+                    }
+
+                  }
+                },
+                "id" : "00001@10.0.0.1"
+              },
+
+              {
+                "op"   : "workflow.invoke",
+                "params" : {
+                  "workplace" : "Leaf-000",
+                  "id"        : "of-overlay.workflow-ovs-leaf",
+                  "data"      : {
+
+                    "mgmtIp" : "192.168.10.4",
+                    "ovsdbPort" : 6641,
+
+                    "sshAccessInfo" : {
+                      "remoteIp" : "192.168.10.4",
+                      "port"     : 22,
+                      "user"     : "root",
+                      "password" : "iloveyou",
+                      "keyfile"  : "~/.ssh/id_rsa"
+                    },
+
+                    "ovsDatapathType" : "system",
+                    "physicalPorts" : [ "leaf0_0", "leaf0_1", "leaf0_nova00" ],
+
+                    "annotations" : {
+                      "rackId" : 1,
+                      "rackPosition" : 3
+                    }
+
+                  }
+                },
+                "id" : "00001@10.0.0.1"
+              },
+              {
+                "op"   : "workflow.invoke",
+                "params" : {
+                  "workplace" : "Leaf-001",
+                  "id"        : "of-overlay.workflow-ovs-leaf",
+                  "data"      : {
+
+                    "mgmtIp" : "192.168.10.5",
+                    "ovsdbPort" : 6641,
+
+                    "sshAccessInfo" : {
+                      "remoteIp" : "192.168.10.5",
+                      "port"     : 22,
+                      "user"     : "root",
+                      "password" : "iloveyou",
+                      "keyfile"  : "~/.ssh/id_rsa"
+                    },
+
+                    "ovsDatapathType" : "system",
+                    "physicalPorts" : [ "leaf1_0", "leaf1_1", "leaf1_nova10" ],
+
+                    "annotations" : {
+                      "rackId" : 1,
+                      "rackPosition" : 3
+                    }
+
+                  }
+                },
+                "id" : "00001@10.0.0.1"
+              },
+              {
+                "op"   : "workflow.invoke",
+                "params" : {
+                  "workplace" : "Leaf-002",
+                  "id"        : "of-overlay.workflow-ovs-leaf",
+                  "data"      : {
+
+                    "mgmtIp" : "192.168.10.6",
+                    "ovsdbPort" : 6641,
+
+                    "sshAccessInfo" : {
+                      "remoteIp" : "192.168.10.6",
+                      "port"     : 22,
+                      "user"     : "root",
+                      "password" : "iloveyou",
+                      "keyfile"  : "~/.ssh/id_rsa"
+                    },
+
+                    "ovsDatapathType" : "system",
+                    "physicalPorts" : [ "leaf2_0", "leaf2_1", "leaf2_nova20" ],
+
+                    "annotations" : {
+                      "rackId" : 1,
+                      "rackPosition" : 3
+                    }
+
+                  }
+                },
+                "id" : "00001@10.0.0.1"
+              },
+
+              {
+                "op"   : "workflow.invoke",
+                "params" : {
+                  "workplace" : "Leaf-003",
+                  "id"        : "of-overlay.workflow-ovs-leaf",
+                  "data"      : {
+
+                    "mgmtIp" : "192.168.10.7",
+                    "ovsdbPort" : 6641,
+
+                    "sshAccessInfo" : {
+                      "remoteIp" : "192.168.10.7",
+                      "port"     : 22,
+                      "user"     : "root",
+                      "password" : "iloveyou",
+                      "keyfile"  : "~/.ssh/id_rsa"
+                    },
+
+                    "ovsDatapathType" : "system",
+                    "physicalPorts" : [ "leaf3_0", "leaf3_1", "leaf3_nova30" ],
+
+                    "annotations" : {
+                      "rackId" : 1,
+                      "rackPosition" : 3
+                    }
+
+                  }
+                },
+                "id" : "00001@10.0.0.1"
+              },
+              {
+                "op"   : "workflow.invoke",
+                "params" : {
+                  "workplace" : "Spine-000",
+                  "id"        : "of-overlay.workflow-ovs-spine",
+                  "data"      : {
+
+                    "mgmtIp" : "192.168.10.2",
+                    "ovsdbPort" : 6641,
+
+                    "sshAccessInfo" : {
+                      "remoteIp" : "192.168.10.2",
+                      "port"     : 22,
+                      "user"     : "root",
+                      "password" : "iloveyou",
+                      "keyfile"  : "~/.ssh/id_rsa"
+                    },
+
+                    "ovsDatapathType" : "system",
+                    "physicalPorts" : [ "spine0_0", "spine0_1", "spine0_2", "spine0_3" ],
+
+                    "annotations" : {
+                      "rackId" : 1,
+                      "rackPosition" : 3
+                    }
+
+                  }
+                },
+                "id" : "00001@10.0.0.1"
+              },
+              {
+                "op"   : "workflow.invoke",
+                "params" : {
+                  "workplace" : "Spine-001",
+                  "id"        : "of-overlay.workflow-ovs-spine",
+                  "data"      : {
+
+                    "mgmtIp" : "192.168.10.3",
+                    "ovsdbPort" : 6641,
+
+                    "sshAccessInfo" : {
+                      "remoteIp" : "192.168.10.3",
+                      "port"     : 22,
+                      "user"     : "root",
+                      "password" : "iloveyou",
+                      "keyfile"  : "~/.ssh/id_rsa"
+                    },
+
+                    "ovsDatapathType" : "system",
+                    "physicalPorts" : [ "spine1_0", "spine1_1", "spine1_2", "spine1_3" ],
+
+                    "annotations" : {
+                      "rackId" : 1,
+                      "rackPosition" : 3
+                    }
+
+                  }
+                },
+                "id" : "00001@10.0.0.1"
+              }
+
+            ]
+          }
+        }
+    }
+}
diff --git a/apps/workflow/ofoverlay/test-cfg/network-cfg-rm-wf.json b/apps/workflow/ofoverlay/test-cfg/network-cfg-rm-wf.json
new file mode 100755
index 0000000..be554a9
--- /dev/null
+++ b/apps/workflow/ofoverlay/test-cfg/network-cfg-rm-wf.json
@@ -0,0 +1,98 @@
+{
+    "apps": {
+        "org.onosproject.workflow": {
+          "workflow" : {
+            "rpc" : [
+              {
+                "op"   : "workflow.invoke",
+                "params" : {
+                  "workplace" : "Nova-000",
+                  "id"        : "of-overlay.clean-workflow-nova",
+                  "data"      : {
+
+                    "mgmtIp" : "192.168.10.8",
+                    "ovsdbPort" : 6641,
+
+                    "sshAccessInfo" : {
+                      "remoteIp" : "192.168.10.8",
+                      "port"     : 22,
+                      "user"     : "root",
+                      "password" : "iloveyou",
+                      "keyfile"  : "~/.ssh/id_rsa"
+                    }
+                  }
+                },
+                "id" : "00001@10.0.0.1"
+              },
+              {
+                "op"   : "workflow.invoke",
+                "params" : {
+                  "workplace" : "Nova-001",
+                  "id"        : "of-overlay.clean-workflow-nova",
+                  "data"      : {
+
+                    "mgmtIp" : "192.168.10.9",
+                    "ovsdbPort" : 6641,
+
+                    "sshAccessInfo" : {
+                      "remoteIp" : "192.168.10.9",
+                      "port"     : 22,
+                      "user"     : "root",
+                      "password" : "iloveyou",
+                      "keyfile"  : "~/.ssh/id_rsa"
+                    }
+                  }
+                },
+                "id" : "00001@10.0.0.1"
+              },
+              {
+                "op"   : "workflow.invoke",
+                "params" : {
+                  "workplace" : "Nova-002",
+                  "id"        : "of-overlay.clean-workflow-nova",
+                  "data"      : {
+
+                    "mgmtIp" : "192.168.10.10",
+                    "ovsdbPort" : 6641,
+
+                    "sshAccessInfo" : {
+                      "remoteIp" : "192.168.10.10",
+                      "port"     : 22,
+                      "user"     : "root",
+                      "password" : "iloveyou",
+                      "keyfile"  : "~/.ssh/id_rsa"
+                    }
+                  }
+                },
+                "id" : "00001@10.0.0.1"
+              },
+              {
+                "op"   : "workflow.invoke",
+                "params" : {
+                  "workplace" : "Nova-003",
+                  "id"        : "of-overlay.clean-workflow-nova",
+                  "data"      : {
+
+                    "mgmtIp" : "192.168.10.11",
+                    "ovsdbPort" : 6641,
+
+                    "sshAccessInfo" : {
+                      "remoteIp" : "192.168.10.11",
+                      "port"     : 22,
+                      "user"     : "root",
+                      "password" : "iloveyou",
+                      "keyfile"  : "~/.ssh/id_rsa"
+                    }
+                  }
+                },
+                "id" : "00001@10.0.0.1"
+              }
+
+
+            ]
+
+          }
+
+        }
+    }
+}
diff --git a/tools/build/bazel/modules.bzl b/tools/build/bazel/modules.bzl
index 1872d2b..ca81933 100644
--- a/tools/build/bazel/modules.bzl
+++ b/tools/build/bazel/modules.bzl
@@ -248,6 +248,7 @@
     "//apps/inbandtelemetry:onos-apps-inbandtelemetry-oar",
     # "//web/gui2:onos-web-gui2-oar",
     "//apps/workflow:onos-apps-workflow-oar",
+    "//apps/workflow/ofoverlay:onos-apps-workflow-ofoverlay-oar",
 ]
 
 PROTOCOL_APPS = [