vom: Add cross connect support 04/13304/3
authorMohsin Kazmi <sykazmi@cisco.com>
Fri, 29 Jun 2018 15:04:23 +0000 (17:04 +0200)
committerNeale Ranns <nranns@cisco.com>
Tue, 3 Jul 2018 11:26:04 +0000 (11:26 +0000)
Change-Id: Ia316730d8f9fe9836200aa96e0b5fd827dc71c98
Signed-off-by: Mohsin Kazmi <sykazmi@cisco.com>
extras/vom/vom/Makefile.am
extras/vom/vom/l2_xconnect.cpp [new file with mode: 0644]
extras/vom/vom/l2_xconnect.hpp [new file with mode: 0644]
extras/vom/vom/l2_xconnect_cmds.cpp [new file with mode: 0644]
extras/vom/vom/l2_xconnect_cmds.hpp [new file with mode: 0644]
test/ext/vom_test.cpp

index 2abf346..6c24d01 100644 (file)
@@ -114,6 +114,8 @@ libvom_la_SOURCES =                         \
        ip_unnumbered.cpp               \
        l2_binding_cmds.cpp             \
        l2_binding.cpp                  \
+       l2_xconnect_cmds.cpp            \
+       l2_xconnect.cpp                 \
        l3_binding_cmds.cpp             \
        l3_binding.cpp                  \
        lldp_binding_cmds.cpp           \
@@ -204,6 +206,7 @@ vominclude_HEADERS =                        \
        interface_span.hpp              \
        ip_unnumbered.hpp               \
        l2_binding.hpp                  \
+       l2_xconnect.hpp                 \
        l3_binding.hpp                  \
        lldp_binding.hpp                \
        lldp_global.hpp                 \
diff --git a/extras/vom/vom/l2_xconnect.cpp b/extras/vom/vom/l2_xconnect.cpp
new file mode 100644 (file)
index 0000000..83d6541
--- /dev/null
@@ -0,0 +1,210 @@
+/*
+ * Copyright (c) 2018 Cisco and/or its affiliates.
+ * 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.
+ */
+
+#include "vom/l2_xconnect.hpp"
+#include "vom/l2_xconnect_cmds.hpp"
+#include "vom/singular_db_funcs.hpp"
+
+namespace VOM {
+/**
+ * A DB of all the L2 x-connect Configs
+ */
+singular_db<l2_xconnect::key_t, l2_xconnect> l2_xconnect::m_db;
+
+l2_xconnect::event_handler l2_xconnect::m_evh;
+
+/**
+ * Construct a new object matching the desried state
+ */
+l2_xconnect::l2_xconnect(const interface& east_itf, const interface& west_itf)
+  : m_east_itf(east_itf.singular())
+  , m_west_itf(west_itf.singular())
+  , m_xconnect_east(0)
+  , m_xconnect_west(0)
+{
+}
+
+l2_xconnect::l2_xconnect(const l2_xconnect& o)
+  : m_east_itf(o.m_east_itf)
+  , m_west_itf(o.m_west_itf)
+  , m_xconnect_east(o.m_xconnect_east)
+  , m_xconnect_west(o.m_xconnect_west)
+{
+}
+
+const l2_xconnect::key_t
+l2_xconnect::key() const
+{
+  if (m_east_itf->name() < m_west_itf->name())
+    return (std::make_pair(m_east_itf->key(), m_west_itf->key()));
+  return (std::make_pair(m_west_itf->key(), m_east_itf->key()));
+}
+
+bool
+l2_xconnect::operator==(const l2_xconnect& l) const
+{
+  return ((*m_east_itf == *l.m_east_itf) && (*m_west_itf == *l.m_west_itf));
+}
+
+std::shared_ptr<l2_xconnect>
+l2_xconnect::find(const key_t& key)
+{
+  return (m_db.find(key));
+}
+
+void
+l2_xconnect::sweep()
+{
+  if (m_xconnect_east && m_xconnect_west &&
+      handle_t::INVALID != m_east_itf->handle() &&
+      handle_t::INVALID != m_west_itf->handle()) {
+    HW::enqueue(new l2_xconnect_cmds::unbind_cmd(
+      m_xconnect_east, m_east_itf->handle(), m_west_itf->handle()));
+    HW::enqueue(new l2_xconnect_cmds::unbind_cmd(
+      m_xconnect_west, m_west_itf->handle(), m_east_itf->handle()));
+  }
+
+  HW::write();
+}
+
+void
+l2_xconnect::replay()
+{
+  if (m_xconnect_east && m_xconnect_west &&
+      handle_t::INVALID != m_east_itf->handle() &&
+      handle_t::INVALID != m_west_itf->handle()) {
+    HW::enqueue(new l2_xconnect_cmds::bind_cmd(
+      m_xconnect_east, m_east_itf->handle(), m_west_itf->handle()));
+    HW::enqueue(new l2_xconnect_cmds::bind_cmd(
+      m_xconnect_west, m_west_itf->handle(), m_east_itf->handle()));
+  }
+}
+
+l2_xconnect::~l2_xconnect()
+{
+  sweep();
+
+  // not in the DB anymore.
+  m_db.release(key(), this);
+}
+
+std::string
+l2_xconnect::to_string() const
+{
+  std::ostringstream s;
+  s << "L2-xconnect:[" << m_east_itf->to_string() << " "
+    << m_west_itf->to_string() << " " << m_xconnect_east.to_string() << " "
+    << m_xconnect_west.to_string() << "]";
+
+  return (s.str());
+}
+
+void
+l2_xconnect::update(const l2_xconnect& desired)
+{
+  /*
+   * the desired state is always that the interface should be created
+   */
+  if (rc_t::OK != m_xconnect_east.rc() && rc_t::OK != m_xconnect_west.rc()) {
+    HW::enqueue(new l2_xconnect_cmds::bind_cmd(
+      m_xconnect_east, m_east_itf->handle(), m_west_itf->handle()));
+    HW::enqueue(new l2_xconnect_cmds::bind_cmd(
+      m_xconnect_west, m_west_itf->handle(), m_east_itf->handle()));
+  }
+}
+
+std::shared_ptr<l2_xconnect>
+l2_xconnect::find_or_add(const l2_xconnect& temp)
+{
+  return (m_db.find_or_add(temp.key(), temp));
+}
+
+std::shared_ptr<l2_xconnect>
+l2_xconnect::singular() const
+{
+  return find_or_add(*this);
+}
+
+void
+l2_xconnect::dump(std::ostream& os)
+{
+  db_dump(m_db, os);
+}
+
+l2_xconnect::event_handler::event_handler()
+{
+  OM::register_listener(this);
+  inspect::register_handler({ "l2-xconnect" }, "L2 xconnects", this);
+}
+
+void
+l2_xconnect::event_handler::handle_replay()
+{
+  m_db.replay();
+}
+
+void
+l2_xconnect::event_handler::handle_populate(const client_db::key_t& key)
+{
+  /**
+   * This needs to be done here
+   */
+  std::shared_ptr<l2_xconnect_cmds::dump_cmd> cmd =
+    std::make_shared<l2_xconnect_cmds::dump_cmd>();
+
+  HW::enqueue(cmd);
+  HW::write();
+
+  for (auto& x_record : *cmd) {
+    auto& payload = x_record.get_payload();
+
+    VOM_LOG(log_level_t::DEBUG) << "l2-xconnect dump: "
+                                << " east-itf: " << payload.rx_sw_if_index
+                                << " west-itf: " << payload.tx_sw_if_index;
+
+    std::shared_ptr<interface> east_itf =
+      interface::find(payload.rx_sw_if_index);
+    std::shared_ptr<interface> west_itf =
+      interface::find(payload.tx_sw_if_index);
+
+    if (east_itf && west_itf) {
+      if (east_itf->name() > west_itf->name())
+        continue;
+      l2_xconnect l2_xc(*east_itf, *west_itf);
+      OM::commit(key, l2_xc);
+    }
+  }
+}
+
+dependency_t
+l2_xconnect::event_handler::order() const
+{
+  return (dependency_t::BINDING);
+}
+
+void
+l2_xconnect::event_handler::show(std::ostream& os)
+{
+  db_dump(m_db, os);
+}
+}
+
+/*
+ * fd.io coding-style-patch-verification: ON
+ *
+ * Local Variables:
+ * eval: (c-set-style "mozilla")
+ * End:
+ */
diff --git a/extras/vom/vom/l2_xconnect.hpp b/extras/vom/vom/l2_xconnect.hpp
new file mode 100644 (file)
index 0000000..0699869
--- /dev/null
@@ -0,0 +1,194 @@
+/*
+ * Copyright (c) 2018 Cisco and/or its affiliates.
+ * 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.
+ */
+
+#ifndef __VOM_L2_XCONNECT_H__
+#define __VOM_L2_XCONNECT_H__
+
+#include "vom/hw.hpp"
+#include "vom/inspect.hpp"
+#include "vom/interface.hpp"
+#include "vom/object_base.hpp"
+#include "vom/om.hpp"
+#include "vom/singular_db.hpp"
+
+namespace VOM {
+/**
+ * A Class representing the cross connnect of an L2 interface with another
+ * l2 interface
+ */
+class l2_xconnect : public object_base
+{
+public:
+  /**
+   * Key type for an L2 xconnect in the singular DB
+   */
+  typedef std::pair<interface::key_t, interface::key_t> key_t;
+
+  /**
+   * Construct a new object matching the desried state
+   */
+  l2_xconnect(const interface& east_itf, const interface& west_itf);
+
+  /**
+   * Copy Constructor
+   */
+  l2_xconnect(const l2_xconnect& o);
+
+  /**
+   * Destructor
+   */
+  ~l2_xconnect();
+
+  /**
+   * Return the xconnect's key
+   */
+  const key_t key() const;
+
+  /**
+   * Comparison operator - for UT
+   */
+  bool operator==(const l2_xconnect& l) const;
+
+  /**
+   * Return the 'singular instance' of the L2 config that matches this
+   * object
+   */
+  std::shared_ptr<l2_xconnect> singular() const;
+
+  /**
+   * convert to string format for debug purposes
+   */
+  std::string to_string() const;
+
+  /**
+   * Dump all l2_xconnects into the stream provided
+   */
+  static void dump(std::ostream& os);
+
+  /**
+   * Static function to find the bridge_domain in the model
+   */
+  static std::shared_ptr<l2_xconnect> find(const key_t& key);
+
+private:
+  /**
+   * Class definition for listeners to OM events
+   */
+  class event_handler : public OM::listener, public inspect::command_handler
+  {
+  public:
+    event_handler();
+    virtual ~event_handler() = default;
+
+    /**
+     * Handle a populate event
+     */
+    void handle_populate(const client_db::key_t& key);
+
+    /**
+     * Handle a replay event
+     */
+    void handle_replay();
+
+    /**
+     * Show the object in the Singular DB
+     */
+    void show(std::ostream& os);
+
+    /**
+     * Get the sortable Id of the listener
+     */
+    dependency_t order() const;
+  };
+
+  /**
+   * event_handler to register with OM
+   */
+  static event_handler m_evh;
+
+  /**
+   * Enque commands to the VPP command Q for the update
+   */
+  void update(const l2_xconnect& obj);
+
+  /**
+   * Find or Add the singular instance in the DB
+   */
+  static std::shared_ptr<l2_xconnect> find_or_add(const l2_xconnect& temp);
+
+  /*
+   * It's the OM class that calls singular()
+   */
+  friend class OM;
+
+  /**
+   * It's the singular_db class that calls replay()
+   */
+  friend class singular_db<key_t, l2_xconnect>;
+
+  /**
+   * Sweep/reap the object if still stale
+   */
+  void sweep(void);
+
+  /**
+   * replay the object to create it in hardware
+   */
+  void replay(void);
+
+  /**
+   * A reference counting pointer the interface that this L2 layer
+   * represents. By holding the reference here, we can guarantee that
+   * this object will outlive the interface
+   */
+  const std::shared_ptr<interface> m_east_itf;
+
+  /**
+   * A reference counting pointer the Bridge-Domain that this L2
+   * interface is bound to. By holding the reference here, we can
+   * guarantee that this object will outlive the BD.
+   */
+  const std::shared_ptr<interface> m_west_itf;
+
+  /**
+   * HW configuration for the xconnect. The bool representing the
+   * do/don't bind.
+   */
+  HW::item<bool> m_xconnect_east;
+
+  /**
+   * HW configuration for the xconnect. The bool representing the
+   * do/don't bind.
+   */
+  HW::item<bool> m_xconnect_west;
+
+  /**
+   * A map of all L2 interfaces key against the interface's handle_t
+   */
+  static singular_db<key_t, l2_xconnect> m_db;
+};
+
+std::ostream& operator<<(std::ostream& os, const l2_xconnect::key_t& key);
+};
+
+/*
+ * fd.io coding-style-patch-verification: ON
+ *
+ * Local Variables:
+ * eval: (c-set-style "mozilla")
+ * End:
+ */
+
+#endif
diff --git a/extras/vom/vom/l2_xconnect_cmds.cpp b/extras/vom/vom/l2_xconnect_cmds.cpp
new file mode 100644 (file)
index 0000000..dc2c40c
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2018 Cisco and/or its affiliates.
+ * 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.
+ */
+
+#include "vom/l2_xconnect_cmds.hpp"
+
+namespace VOM {
+namespace l2_xconnect_cmds {
+bind_cmd::bind_cmd(HW::item<bool>& item,
+                   const handle_t& east_itf,
+                   const handle_t& west_itf)
+  : rpc_cmd(item)
+  , m_east_itf(east_itf)
+  , m_west_itf(west_itf)
+{
+}
+
+bool
+bind_cmd::operator==(const bind_cmd& other) const
+{
+  return ((m_east_itf == other.m_east_itf) && (m_west_itf == other.m_west_itf));
+}
+
+rc_t
+bind_cmd::issue(connection& con)
+{
+  msg_t req(con.ctx(), std::ref(*this));
+
+  auto& payload = req.get_request().get_payload();
+  payload.rx_sw_if_index = m_east_itf.value();
+  payload.tx_sw_if_index = m_west_itf.value();
+  payload.enable = 1;
+
+  VAPI_CALL(req.execute());
+
+  m_hw_item.set(wait());
+
+  return (rc_t::OK);
+}
+
+std::string
+bind_cmd::to_string() const
+{
+  std::ostringstream s;
+  s << "L2-bind: " << m_hw_item.to_string()
+    << " east-itf:" << m_east_itf.to_string()
+    << " west-itf:" << m_west_itf.to_string();
+
+  return (s.str());
+}
+
+unbind_cmd::unbind_cmd(HW::item<bool>& item,
+                       const handle_t& east_itf,
+                       const handle_t& west_itf)
+  : rpc_cmd(item)
+  , m_east_itf(east_itf)
+  , m_west_itf(west_itf)
+{
+}
+
+bool
+unbind_cmd::operator==(const unbind_cmd& other) const
+{
+  return ((m_east_itf == other.m_east_itf) && (m_west_itf == other.m_west_itf));
+}
+
+rc_t
+unbind_cmd::issue(connection& con)
+{
+  msg_t req(con.ctx(), std::ref(*this));
+
+  auto& payload = req.get_request().get_payload();
+  payload.rx_sw_if_index = m_east_itf.value();
+  payload.tx_sw_if_index = m_west_itf.value();
+  payload.enable = 0;
+
+  VAPI_CALL(req.execute());
+
+  wait();
+  m_hw_item.set(rc_t::NOOP);
+
+  return (rc_t::OK);
+}
+
+std::string
+unbind_cmd::to_string() const
+{
+  std::ostringstream s;
+  s << "L2-unbind: " << m_hw_item.to_string()
+    << " east-itf:" << m_east_itf.to_string()
+    << " west-itf:" << m_west_itf.to_string();
+
+  return (s.str());
+}
+
+dump_cmd::dump_cmd()
+{
+}
+
+bool
+dump_cmd::operator==(const dump_cmd& other) const
+{
+  return (true);
+}
+
+rc_t
+dump_cmd::issue(connection& con)
+{
+  m_dump.reset(new msg_t(con.ctx(), std::ref(*this)));
+
+  VAPI_CALL(m_dump->execute());
+
+  wait();
+
+  return rc_t::OK;
+}
+
+std::string
+dump_cmd::to_string() const
+{
+  return ("l2-xconnect-dump");
+}
+
+}; // namespace l2_xconnect_cmds
+}; // namespace VOM
+
+/*
+ * fd.io coding-style-patch-verification: ON
+ *
+ * Local Variables:
+ * eval: (c-set-style "mozilla")
+ * End:
+ */
diff --git a/extras/vom/vom/l2_xconnect_cmds.hpp b/extras/vom/vom/l2_xconnect_cmds.hpp
new file mode 100644 (file)
index 0000000..fbb869f
--- /dev/null
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2018 Cisco and/or its affiliates.
+ * 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.
+ */
+
+#ifndef __VOM_L2_XCONNECT_CMDS_H__
+#define __VOM_L2_XCONNECT_CMDS_H__
+
+#include "vom/dump_cmd.hpp"
+#include "vom/l2_xconnect.hpp"
+#include "vom/rpc_cmd.hpp"
+
+#include <vapi/l2.api.vapi.hpp>
+#include <vapi/vpe.api.vapi.hpp>
+
+namespace VOM {
+namespace l2_xconnect_cmds {
+
+/**
+ * A functor class that binds L2 configuration to an interface
+ */
+class bind_cmd
+  : public rpc_cmd<HW::item<bool>, rc_t, vapi::Sw_interface_set_l2_xconnect>
+{
+public:
+  /**
+   * Constructor
+   */
+  bind_cmd(HW::item<bool>& item,
+           const handle_t& east_itf,
+           const handle_t& west_itf);
+
+  /**
+   * Issue the command to VPP/HW
+   */
+  rc_t issue(connection& con);
+  /**
+   * convert to string format for debug purposes
+   */
+  std::string to_string() const;
+
+  /**
+   * Comparison operator - only used for UT
+   */
+  bool operator==(const bind_cmd& i) const;
+
+private:
+  /**
+   * The east interface for cross_connect
+   */
+  const handle_t m_east_itf;
+
+  /**
+   * The west interface for x-connect
+   */
+  const handle_t m_west_itf;
+};
+
+/**
+ * A cmd class that Unbinds L2 configuration from an interface
+ */
+class unbind_cmd
+  : public rpc_cmd<HW::item<bool>, rc_t, vapi::Sw_interface_set_l2_xconnect>
+{
+public:
+  /**
+   * Constructor
+   */
+  unbind_cmd(HW::item<bool>& item,
+             const handle_t& east_itf,
+             const handle_t& west_itf);
+
+  /**
+   * Issue the command to VPP/HW
+   */
+  rc_t issue(connection& con);
+
+  /**
+   * convert to string format for debug purposes
+   */
+  std::string to_string() const;
+
+  /**
+   * Comparison operator - only used for UT
+   */
+  bool operator==(const unbind_cmd& i) const;
+
+private:
+  /**
+   * The east interface for x-connect
+   */
+  const handle_t m_east_itf;
+
+  /**
+   * The west interface for x-connect
+   */
+  const handle_t m_west_itf;
+};
+
+/**
+ * A cmd class that Dumps all the bridge domains
+ */
+class dump_cmd : public VOM::dump_cmd<vapi::L2_xconnect_dump>
+{
+public:
+  /**
+   * Constructor
+   */
+  dump_cmd();
+
+  /**
+   * Issue the command to VPP/HW
+   */
+  rc_t issue(connection& con);
+  /**
+   * convert to string format for debug purposes
+   */
+  std::string to_string() const;
+
+  /**
+   * Comparison operator - only used for UT
+   */
+  bool operator==(const dump_cmd& i) const;
+};
+
+}; // namespace l2_xconnect_cmds
+}; // namespace VOM
+
+/*
+ * fd.io coding-style-patch-verification: ON
+ *
+ * Local Variables:
+ * eval: (c-set-style "mozilla")
+ * End:
+ */
+
+#endif
index 44df243..0213c42 100644 (file)
@@ -25,6 +25,8 @@
 #include "vom/bond_group_binding_cmds.hpp"
 #include "vom/l2_binding.hpp"
 #include "vom/l2_binding_cmds.hpp"
+#include "vom/l2_xconnect.hpp"
+#include "vom/l2_xconnect_cmds.hpp"
 #include "vom/l3_binding.hpp"
 #include "vom/l3_binding_cmds.hpp"
 #include "vom/bridge_domain.hpp"
@@ -293,6 +295,14 @@ public:
                     {
                         rc = handle_derived<l2_binding_cmds::set_vtr_op_cmd>(f_exp, f_act);
                     }
+                    else if (typeid(*f_exp) == typeid(l2_xconnect_cmds::bind_cmd))
+                    {
+                        rc = handle_derived<l2_xconnect_cmds::bind_cmd>(f_exp, f_act);
+                    }
+                    else if (typeid(*f_exp) == typeid(l2_xconnect_cmds::unbind_cmd))
+                    {
+                        rc = handle_derived<l2_xconnect_cmds::unbind_cmd>(f_exp, f_act);
+                    }
                     else if (typeid(*f_exp) == typeid(vxlan_tunnel_cmds::create_cmd))
                     {
                         rc = handle_derived<vxlan_tunnel_cmds::create_cmd>(f_exp, f_act);
@@ -1017,6 +1027,60 @@ BOOST_AUTO_TEST_CASE(test_bridge) {
     TRY_CHECK(OM::remove(jkr));
 }
 
+BOOST_AUTO_TEST_CASE(test_l2_xconnect) {
+    VppInit vi;
+    const std::string nicholas = "NicholasAbercrombie";
+    rc_t rc = rc_t::OK;
+
+    /*
+     * Interface 1
+     */
+    std::string itf1_name = "host1";
+    interface itf1(itf1_name,
+                   interface::type_t::AFPACKET,
+                   interface::admin_state_t::UP);
+    HW::item<handle_t> hw_ifh(2, rc_t::OK);
+    HW::item<interface::admin_state_t> hw_as_up(interface::admin_state_t::UP, rc_t::OK);
+    ADD_EXPECT(interface_cmds::af_packet_create_cmd(hw_ifh, itf1_name));
+    ADD_EXPECT(interface_cmds::state_change_cmd(hw_as_up, hw_ifh));
+    TRY_CHECK_RC(OM::write(nicholas, itf1));
+
+    /*
+     * Interface 2
+     */
+    std::string itf2_name = "host2";
+    interface itf2(itf2_name,
+                   interface::type_t::AFPACKET,
+                   interface::admin_state_t::UP);
+
+    HW::item<handle_t> hw_ifh2(4, rc_t::OK);
+    ADD_EXPECT(interface_cmds::af_packet_create_cmd(hw_ifh2, itf2_name));
+    ADD_EXPECT(interface_cmds::state_change_cmd(hw_as_up, hw_ifh2));
+    TRY_CHECK_RC(OM::write(nicholas, itf2));
+
+    l2_xconnect *l2_xconn = new l2_xconnect(itf1, itf2);
+    HW::item<bool> xconnect_east(true, rc_t::OK);
+    HW::item<bool> xconnect_west(true, rc_t::OK);
+    HW::item<bool> xconnect_east_unbind(false, rc_t::OK);
+    HW::item<bool> xconnect_west_unbind(false, rc_t::OK);
+    ADD_EXPECT(l2_xconnect_cmds::bind_cmd(xconnect_east, hw_ifh.data(), hw_ifh2.data()));
+    ADD_EXPECT(l2_xconnect_cmds::bind_cmd(xconnect_west, hw_ifh2.data(), hw_ifh.data()));
+    TRY_CHECK_RC(OM::write(nicholas, *l2_xconn));
+
+    delete l2_xconn;
+
+    HW::item<interface::admin_state_t> hw_as_down(interface::admin_state_t::DOWN, rc_t::OK);
+    STRICT_ORDER_OFF();
+    ADD_EXPECT(l2_xconnect_cmds::unbind_cmd(xconnect_east_unbind, hw_ifh.data(), hw_ifh2.data()));
+    ADD_EXPECT(l2_xconnect_cmds::unbind_cmd(xconnect_west_unbind, hw_ifh2.data(), hw_ifh.data()));
+    ADD_EXPECT(interface_cmds::state_change_cmd(hw_as_down, hw_ifh2));
+    ADD_EXPECT(interface_cmds::af_packet_delete_cmd(hw_ifh2, itf2_name));
+    ADD_EXPECT(interface_cmds::state_change_cmd(hw_as_down, hw_ifh));
+    ADD_EXPECT(interface_cmds::af_packet_delete_cmd(hw_ifh, itf1_name));
+
+    TRY_CHECK(OM::remove(nicholas));
+}
+
 BOOST_AUTO_TEST_CASE(test_vxlan) {
     VppInit vi;
     const std::string franz = "FranzKafka";