Change in osmo-bsc[master]: inter-BSC HO: add neighbor_ident API to manage neighbor-BSS-cells

This is merely a historical archive of years 2008-2021, before the migration to mailman3.

A maintained and still updated list archive can be found at https://lists.osmocom.org/hyperkitty/list/gerrit-log@lists.osmocom.org/.

Neels Hofmeyr gerrit-no-reply at lists.osmocom.org
Mon Jun 18 07:13:16 UTC 2018


Neels Hofmeyr has uploaded this change for review. ( https://gerrit.osmocom.org/9666


Change subject: inter-BSC HO: add neighbor_ident API to manage neighbor-BSS-cells
......................................................................

inter-BSC HO: add neighbor_ident API to manage neighbor-BSS-cells

Change-Id: I0153d7069817fba9146ddc11214de2757d7d37bf
---
M include/osmocom/bsc/Makefile.am
M include/osmocom/bsc/gsm_data.h
M include/osmocom/bsc/handover.h
A include/osmocom/bsc/neighbor_ident.h
M src/osmo-bsc/Makefile.am
M src/osmo-bsc/bsc_init.c
M src/osmo-bsc/bsc_vty.c
M src/osmo-bsc/gsm_data.c
M src/osmo-bsc/handover_logic.c
A src/osmo-bsc/neighbor_ident.c
A src/osmo-bsc/neighbor_ident_vty.c
M src/osmo-bsc/net_init.c
M src/osmo-bsc/system_information.c
M tests/bsc/Makefile.am
M tests/gsm0408/Makefile.am
M tests/handover/Makefile.am
A tests/handover/neighbor_ident_test.c
A tests/handover/neighbor_ident_test.err
A tests/handover/neighbor_ident_test.ok
A tests/neighbor_ident.vty
M tests/testsuite.at
21 files changed, 1,958 insertions(+), 5 deletions(-)



  git pull ssh://gerrit.osmocom.org:29418/osmo-bsc refs/changes/66/9666/1

diff --git a/include/osmocom/bsc/Makefile.am b/include/osmocom/bsc/Makefile.am
index 5fa39eb..18737a3 100644
--- a/include/osmocom/bsc/Makefile.am
+++ b/include/osmocom/bsc/Makefile.am
@@ -27,6 +27,7 @@
 	meas_feed.h \
 	meas_rep.h \
 	misdn.h \
+	neighbor_ident.h \
 	network_listen.h \
 	openbscdefines.h \
 	osmo_bsc.h \
diff --git a/include/osmocom/bsc/gsm_data.h b/include/osmocom/bsc/gsm_data.h
index 5794617..76a1b30 100644
--- a/include/osmocom/bsc/gsm_data.h
+++ b/include/osmocom/bsc/gsm_data.h
@@ -35,6 +35,7 @@
 struct mgcp_client_conf;
 struct mgcp_client;
 struct mgcp_ctx;
+struct gsm0808_cell_id;
 
 /** annotations for msgb ownership */
 #define __uses
@@ -750,6 +751,12 @@
 	unsigned int used;
 };
 
+/* Useful to track N-N relations between BTS, for example neighbors. */
+struct gsm_bts_ref {
+	struct llist_head entry;
+	struct gsm_bts *bts;
+};
+
 /* One BTS */
 struct gsm_bts {
 	/* list header in net->bts_list */
@@ -984,6 +991,11 @@
 
 	struct handover_cfg *ho;
 
+	/* A list of struct gsm_bts_ref, indicating neighbors of this BTS.
+	 * When the si_common neigh_list is in automatic mode, it is populated from this list as well as
+	 * gsm_network->neighbor_bss_cells. */
+	struct llist_head local_neighbors;
+
 	/* BTS-specific overrides for timer values from struct gsm_network. */
 	uint8_t T3122;	/* ASSIGMENT REJECT wait indication */
 
@@ -998,6 +1010,10 @@
 
 struct gsm_bts *gsm_bts_alloc(struct gsm_network *net, uint8_t bts_num);
 struct gsm_bts *gsm_bts_num(struct gsm_network *net, int num);
+bool gsm_bts_matches_cell_id(struct gsm_bts *bts, const struct gsm0808_cell_id *ci);
+struct gsm_bts *gsm_bts_by_cell_id(struct gsm_network *net, const struct gsm0808_cell_id *ci);
+int gsm_bts_local_neighbor_add(struct gsm_bts *bts, struct gsm_bts *neighbor);
+int gsm_bts_local_neighbor_del(struct gsm_bts *bts, const struct gsm_bts *neighbor);
 
 struct gsm_bts_trx *gsm_bts_trx_alloc(struct gsm_bts *bts);
 struct gsm_bts_trx *gsm_bts_trx_num(const struct gsm_bts *bts, int num);
@@ -1281,6 +1297,9 @@
 		struct mgcp_client_conf *conf;
 		struct mgcp_client *client;
 	} mgw;
+
+	/* Remote BSS Cell Identifier Lists */
+	struct neighbor_ident_list *neighbor_bss_cells;
 };
 
 static inline const struct osmo_location_area_id *bts_lai(struct gsm_bts *bts)
diff --git a/include/osmocom/bsc/handover.h b/include/osmocom/bsc/handover.h
index eb03f6a..847d985 100644
--- a/include/osmocom/bsc/handover.h
+++ b/include/osmocom/bsc/handover.h
@@ -6,6 +6,8 @@
 #include <osmocom/core/timer.h>
 #include <osmocom/gsm/gsm_utils.h>
 
+#include <osmocom/bsc/neighbor_ident.h>
+
 struct gsm_lchan;
 struct gsm_bts;
 struct gsm_subscriber_connection;
@@ -93,3 +95,7 @@
 
 void handover_decision_callbacks_register(struct handover_decision_callbacks *hdc);
 struct handover_decision_callbacks *handover_decision_callbacks_get(int hodec_id);
+
+struct gsm_bts *bts_by_neighbor_ident(const struct gsm_network *net,
+				      const struct neighbor_ident_key *search_for);
+struct neighbor_ident_key *bts_ident_key(const struct gsm_bts *bts);
diff --git a/include/osmocom/bsc/neighbor_ident.h b/include/osmocom/bsc/neighbor_ident.h
new file mode 100644
index 0000000..86e062a
--- /dev/null
+++ b/include/osmocom/bsc/neighbor_ident.h
@@ -0,0 +1,60 @@
+/* Manage identity of neighboring BSS cells for inter-BSC handover */
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#include <osmocom/core/linuxlist.h>
+
+struct vty;
+struct gsm_network;
+struct gsm_bts;
+struct neighbor_ident_list;
+struct gsm0808_cell_id_list2;
+
+enum bsic_kind {
+	BSIC_NONE,
+	BSIC_6BIT,
+	BSIC_9BIT,
+};
+
+#define NEIGHBOR_IDENT_KEY_ANY_BTS -1
+
+struct neighbor_ident_key {
+	int from_bts; /*< BTS nr 0..255 or NEIGHBOR_IDENT_KEY_ANY_BTS */
+	uint16_t arfcn;
+	enum bsic_kind bsic_kind;
+	uint16_t bsic;
+};
+
+const char *neighbor_ident_key_name(const struct neighbor_ident_key *ni_key);
+
+struct neighbor_ident_list *neighbor_ident_init(void *talloc_ctx);
+void neighbor_ident_free(struct neighbor_ident_list *nil);
+
+bool neighbor_ident_key_match(const struct neighbor_ident_key *entry,
+			      const struct neighbor_ident_key *search_for,
+			      bool exact_match);
+
+int neighbor_ident_add(struct neighbor_ident_list *nil, const struct neighbor_ident_key *key,
+		       const struct gsm0808_cell_id_list2 *val);
+const struct gsm0808_cell_id_list2 *neighbor_ident_get(const struct neighbor_ident_list *nil,
+						       const struct neighbor_ident_key *key);
+bool neighbor_ident_del(struct neighbor_ident_list *nil, const struct neighbor_ident_key *key);
+void neighbor_ident_clear(struct neighbor_ident_list *nil);
+
+void neighbor_ident_iter(const struct neighbor_ident_list *nil,
+			 bool (* iter_cb )(const struct neighbor_ident_key *key,
+					   const struct gsm0808_cell_id_list2 *val,
+					   void *cb_data),
+			 void *cb_data);
+
+void neighbor_ident_vty_init(struct gsm_network *net, struct neighbor_ident_list *nil);
+void neighbor_ident_vty_write(struct vty *vty, const char *indent, struct gsm_bts *bts);
+
+#define NEIGHBOR_IDENT_VTY_KEY_PARAMS "arfcn <0-1023> (bsic|bsic9) (<0-511>|any)"
+#define NEIGHBOR_IDENT_VTY_KEY_DOC \
+	"ARFCN of neighbor cell\n" "ARFCN value\n" \
+	"BSIC of neighbor cell\n" "9-bit BSIC of neighbor cell\n" "BSIC value\n" \
+	"for all BSICs / use any BSIC in this ARFCN\n"
+bool neighbor_ident_vty_parse_key_params(struct vty *vty, const char **argv, struct neighbor_ident_key *key);
diff --git a/src/osmo-bsc/Makefile.am b/src/osmo-bsc/Makefile.am
index a459a92..afae0b6 100644
--- a/src/osmo-bsc/Makefile.am
+++ b/src/osmo-bsc/Makefile.am
@@ -64,6 +64,8 @@
 	handover_vty.c \
 	meas_feed.c \
 	meas_rep.c \
+	neighbor_ident.c \
+	neighbor_ident_vty.c \
 	net_init.c \
 	osmo_bsc_api.c \
 	osmo_bsc_audio.c \
diff --git a/src/osmo-bsc/bsc_init.c b/src/osmo-bsc/bsc_init.c
index b6bd410..1fe4847 100644
--- a/src/osmo-bsc/bsc_init.c
+++ b/src/osmo-bsc/bsc_init.c
@@ -247,6 +247,7 @@
 
 	net->ho = ho_cfg_init(net, NULL);
 	net->hodec2.congestion_check_interval_s = HO_CFG_CONGESTION_CHECK_DEFAULT;
+	net->neighbor_bss_cells = neighbor_ident_init(net);
 
 	/* init statistics */
 	net->bsc_ctrs = rate_ctr_group_alloc(net, &bsc_ctrg_desc, 0);
diff --git a/src/osmo-bsc/bsc_vty.c b/src/osmo-bsc/bsc_vty.c
index 57c5363..f25f731 100644
--- a/src/osmo-bsc/bsc_vty.c
+++ b/src/osmo-bsc/bsc_vty.c
@@ -62,6 +62,8 @@
 #include <osmocom/bsc/gsm_04_08_utils.h>
 #include <osmocom/bsc/acc_ramp.h>
 #include <osmocom/bsc/meas_feed.h>
+#include <osmocom/bsc/neighbor_ident.h>
+#include <osmocom/bsc/handover.h>
 
 #include <inttypes.h>
 
@@ -909,6 +911,8 @@
 			VTY_NEWLINE);
 	}
 
+	neighbor_ident_vty_write(vty, "  ", bts);
+
 	vty_out(vty, "  codec-support fr");
 	if (bts->codec.hr)
 		vty_out(vty, " hr");
@@ -4967,6 +4971,7 @@
 	install_element(BTS_NODE, &cfg_bts_no_acc_ramping_cmd);
 	install_element(BTS_NODE, &cfg_bts_acc_ramping_step_interval_cmd);
 	install_element(BTS_NODE, &cfg_bts_acc_ramping_step_size_cmd);
+	neighbor_ident_vty_init(network, network->neighbor_bss_cells);
 	/* See also handover commands added on bts level from handover_vty.c */
 
 	install_element(BTS_NODE, &cfg_trx_cmd);
diff --git a/src/osmo-bsc/gsm_data.c b/src/osmo-bsc/gsm_data.c
index 0f062d2..734e2fb 100644
--- a/src/osmo-bsc/gsm_data.c
+++ b/src/osmo-bsc/gsm_data.c
@@ -33,6 +33,7 @@
 #include <osmocom/core/statistics.h>
 #include <osmocom/gsm/protocol/gsm_04_08.h>
 #include <osmocom/gsm/gsm48.h>
+#include <osmocom/gsm/gsm0808_utils.h>
 
 #include <osmocom/bsc/gsm_data.h>
 #include <osmocom/bsc/bsc_msc_data.h>
@@ -563,6 +564,117 @@
 	return NULL;
 }
 
+bool gsm_bts_matches_cell_id(struct gsm_bts *bts, const struct gsm0808_cell_id *ci)
+{
+	if (!bts || !ci)
+		return false;
+	switch (ci->id_discr) {
+	case CELL_IDENT_WHOLE_GLOBAL:
+		if (osmo_plmn_cmp(&bts->network->plmn, &ci->id.global.lai.plmn))
+			return false;
+		if (bts->location_area_code != ci->id.global.lai.lac)
+			return false;
+		if (bts->cell_identity != ci->id.global.cell_identity)
+			return false;
+		return true;
+	case CELL_IDENT_LAC_AND_CI:
+		if (bts->location_area_code != ci->id.lac_and_ci.lac)
+			return false;
+		if (bts->cell_identity != ci->id.lac_and_ci.ci)
+			return false;
+		return true;
+	case CELL_IDENT_CI:
+		if (bts->cell_identity != ci->id.ci)
+			return false;
+		return true;
+	case CELL_IDENT_NO_CELL:
+		return false;
+	case CELL_IDENT_LAI_AND_LAC:
+		if (osmo_plmn_cmp(&bts->network->plmn, &ci->id.lai_and_lac.plmn))
+			return false;
+		if (bts->location_area_code != ci->id.lai_and_lac.lac)
+			return false;
+		return true;
+	case CELL_IDENT_LAC:
+		if (bts->location_area_code != ci->id.lac)
+			return false;
+		return true;
+	case CELL_IDENT_BSS:
+		return true;
+	case CELL_IDENT_UTRAN_PLMN_LAC_RNC:
+	case CELL_IDENT_UTRAN_RNC:
+	case CELL_IDENT_UTRAN_LAC_RNC:
+		/* Not implemented */
+	default:
+		return false;
+	}
+}
+
+struct gsm_bts *gsm_bts_by_cell_id(struct gsm_network *net, const struct gsm0808_cell_id *ci)
+{
+	struct gsm_bts *bts;
+
+	llist_for_each_entry(bts, &net->bts_list, list) {
+		if (gsm_bts_matches_cell_id(bts, ci))
+			return bts;
+	}
+
+	return NULL;
+}
+
+struct gsm_bts_ref *gsm_bts_ref_find(const struct llist_head *list, const struct gsm_bts *bts)
+{
+	struct gsm_bts_ref *ref;
+	if (!bts)
+		return NULL;
+	llist_for_each_entry(ref, list, entry) {
+		if (ref->bts == bts)
+			return ref;
+	}
+	return NULL;
+}
+
+/* Add a BTS reference to the local_neighbors list.
+ * Return 1 if added, 0 if such an entry already existed, and negative on errors. */
+int gsm_bts_local_neighbor_add(struct gsm_bts *bts, struct gsm_bts *neighbor)
+{
+	struct gsm_bts_ref *ref;
+	if (!bts || !neighbor)
+		return -ENOMEM;
+
+	if (bts == neighbor)
+		return -EINVAL;
+
+	/* Already got this entry? */
+	ref = gsm_bts_ref_find(&bts->local_neighbors, neighbor);
+	if (ref)
+		return 0;
+
+	ref = talloc_zero(bts, struct gsm_bts_ref);
+	if (!ref)
+		return -ENOMEM;
+	ref->bts = neighbor;
+	llist_add_tail(&ref->entry, &bts->local_neighbors);
+	return 1;
+}
+
+/* Remove a BTS reference from the local_neighbors list.
+ * Return 1 if removed, 0 if no such entry existed, and negative on errors. */
+int gsm_bts_local_neighbor_del(struct gsm_bts *bts, const struct gsm_bts *neighbor)
+{
+	struct gsm_bts_ref *ref;
+	if (!bts || !neighbor)
+		return -ENOMEM;
+
+	ref = gsm_bts_ref_find(&bts->local_neighbors, neighbor);
+	if (!ref)
+		return 0;
+
+	llist_del(&ref->entry);
+	talloc_free(ref);
+	return 1;
+}
+
 struct gsm_bts_trx *gsm_bts_trx_alloc(struct gsm_bts *bts)
 {
 	struct gsm_bts_trx *trx = talloc_zero(bts, struct gsm_bts_trx);
@@ -756,6 +868,7 @@
 
 	INIT_LLIST_HEAD(&bts->abis_queue);
 	INIT_LLIST_HEAD(&bts->loc_list);
+	INIT_LLIST_HEAD(&bts->local_neighbors);
 
 	return bts;
 }
diff --git a/src/osmo-bsc/handover_logic.c b/src/osmo-bsc/handover_logic.c
index 960bf69..55af0ed 100644
--- a/src/osmo-bsc/handover_logic.c
+++ b/src/osmo-bsc/handover_logic.c
@@ -392,6 +392,50 @@
 	return 0;
 }
 
+struct gsm_bts *bts_by_neighbor_ident(const struct gsm_network *net,
+				      const struct neighbor_ident_key *search_for)
+{
+	struct gsm_bts *found = NULL;
+	struct gsm_bts *bts;
+	struct gsm_bts *wildcard_match = NULL;
+
+	llist_for_each_entry(bts, &net->bts_list, list) {
+		struct neighbor_ident_key entry = {
+			.from_bts = NEIGHBOR_IDENT_KEY_ANY_BTS,
+			.arfcn = bts->c0->arfcn,
+			.bsic_kind = BSIC_6BIT,
+			.bsic = bts->bsic,
+		};
+		if (neighbor_ident_key_match(&entry, search_for, true)) {
+			if (found) {
+				LOGP(DHO, LOGL_ERROR, "CONFIG ERROR: Multiple BTS match %s: %d and %d\n",
+				     neighbor_ident_key_name(search_for),
+				     found->nr, bts->nr);
+				return found;
+			}
+			found = bts;
+		}
+		if (neighbor_ident_key_match(&entry, search_for, false))
+			wildcard_match = bts;
+	}
+
+	if (found)
+		return found;
+
+	return wildcard_match;
+}
+
+struct neighbor_ident_key *bts_ident_key(const struct gsm_bts *bts)
+{
+	static struct neighbor_ident_key key;
+	key = (struct neighbor_ident_key){
+		.arfcn = bts->c0->arfcn,
+		.bsic_kind = BSIC_6BIT,
+		.bsic = bts->bsic,
+	};
+	return &key;
+}
+
 static int ho_logic_sig_cb(unsigned int subsys, unsigned int signal,
 			   void *handler_data, void *signal_data)
 {
diff --git a/src/osmo-bsc/neighbor_ident.c b/src/osmo-bsc/neighbor_ident.c
new file mode 100644
index 0000000..8a7c580
--- /dev/null
+++ b/src/osmo-bsc/neighbor_ident.c
@@ -0,0 +1,296 @@
+/* Manage identity of neighboring BSS cells for inter-BSC handover.
+ *
+ * Measurement reports tell us about neighbor ARFCN and BSIC. If that ARFCN and BSIC is not managed by
+ * this local BSS, we need to tell the MSC a cell identity, like CGI, LAC+CI, etc. -- hence we need a
+ * mapping from ARFCN+BSIC to Cell Identifier List, which needs to be configured by the user.
+ */
+/* (C) 2018 by sysmocom - s.f.m.c. GmbH <info at sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * Author: Neels Hofmeyr <nhofmeyr at sysmocom.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <errno.h>
+
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/gsm/gsm0808.h>
+
+#include <osmocom/bsc/neighbor_ident.h>
+
+struct neighbor_ident_list {
+	struct llist_head list;
+};
+
+struct neighbor_ident {
+	struct llist_head entry;
+
+	struct neighbor_ident_key key;
+	struct gsm0808_cell_id_list2 val;
+};
+
+#define APPEND_THING(func, args...) do { \
+		int remain = buflen - (pos - buf); \
+		int l = func(pos, remain, ##args); \
+		if (l < 0 || l > remain) \
+			pos = buf + buflen; \
+		else \
+			pos += l; \
+	} while(0)
+#define APPEND_STR(fmt, args...) APPEND_THING(snprintf, fmt, ##args)
+
+const char *_neighbor_ident_key_name(char *buf, size_t buflen, const struct neighbor_ident_key *ni_key)
+{
+	char *pos = buf;
+
+	APPEND_STR("BTS ");
+	if (ni_key->from_bts == NEIGHBOR_IDENT_KEY_ANY_BTS)
+		APPEND_STR("*");
+	else if (ni_key->from_bts >= 0 && ni_key->from_bts <= 255)
+		APPEND_STR("%d", ni_key->from_bts);
+	else
+		APPEND_STR("invalid(%d)", ni_key->from_bts);
+
+	APPEND_STR(" to ");
+	switch (ni_key->bsic_kind) {
+	default:
+	case BSIC_NONE:
+		APPEND_STR("ARFCN %u (any BSIC)", ni_key->arfcn);
+		break;
+	case BSIC_6BIT:
+		APPEND_STR("ARFCN %u BSIC %u", ni_key->arfcn, ni_key->bsic & 0x3f);
+		break;
+	case BSIC_9BIT:
+		APPEND_STR("ARFCN %u BSIC %u(9bit)", ni_key->arfcn, ni_key->bsic & 0x1ff);
+		break;
+	}
+	return buf;
+}
+
+const char *neighbor_ident_key_name(const struct neighbor_ident_key *ni_key)
+{
+	static char buf[64];
+	return _neighbor_ident_key_name(buf, sizeof(buf), ni_key);
+}
+
+struct neighbor_ident_list *neighbor_ident_init(void *talloc_ctx)
+{
+	struct neighbor_ident_list *nil = talloc_zero(talloc_ctx, struct neighbor_ident_list);
+	OSMO_ASSERT(nil);
+	INIT_LLIST_HEAD(&nil->list);
+	return nil;
+}
+
+void neighbor_ident_free(struct neighbor_ident_list *nil)
+{
+	if (!nil)
+		return;
+	talloc_free(nil);
+}
+
+/* Return true when the entry matches the search_for requirements.
+ * If exact_match is false, a BSIC_NONE entry acts as wildcard to match any search_for on that ARFCN,
+ * and a BSIC_NONE in search_for likewise returns any one entry that matches the ARFCN;
+ * also a from_bts == NEIGHBOR_IDENT_KEY_ANY_BTS in either entry or search_for will match.
+ * If exact_match is true, only identical bsic_kind values and identical from_bts values return a match.
+ * Note, typically wildcard BSICs are only in entry, e.g. the user configured list, and search_for
+ * contains a specific BSIC, e.g. as received from a Measurement Report. */
+bool neighbor_ident_key_match(const struct neighbor_ident_key *entry,
+			      const struct neighbor_ident_key *search_for,
+			      bool exact_match)
+{
+	uint16_t bsic_mask;
+
+	if (exact_match
+	    && entry->from_bts != search_for->from_bts)
+		return false;
+
+	if (search_for->from_bts != NEIGHBOR_IDENT_KEY_ANY_BTS
+	    && entry->from_bts != NEIGHBOR_IDENT_KEY_ANY_BTS
+	    && entry->from_bts != search_for->from_bts)
+		return false;
+
+	if (entry->arfcn != search_for->arfcn)
+		return false;
+
+	switch (entry->bsic_kind) {
+	default:
+		return false;
+	case BSIC_NONE:
+		if (!exact_match) {
+			/* The neighbor identifier list entry matches any BSIC for this ARFCN. */
+			return true;
+		}
+		/* Match exact entry */
+		bsic_mask = 0;
+		break;
+	case BSIC_6BIT:
+		bsic_mask = 0x3f;
+		break;
+	case BSIC_9BIT:
+		bsic_mask = 0x1ff;
+		break;
+	}
+	if (!exact_match && search_for->bsic_kind == BSIC_NONE) {
+		/* The search is looking only for an ARFCN with any BSIC */
+		return true;
+	}
+	if (search_for->bsic_kind == entry->bsic_kind
+	    && (search_for->bsic & bsic_mask) == (entry->bsic & bsic_mask))
+		return true;
+	return false;
+}
+
+static struct neighbor_ident *_neighbor_ident_get(const struct neighbor_ident_list *nil,
+						  const struct neighbor_ident_key *key,
+						  bool exact_match)
+{
+	struct neighbor_ident *ni;
+	struct neighbor_ident *wildcard_match = NULL;
+
+	/* Do both exact-bsic and wildcard matching in the same iteration:
+	 * Any exact match returns immediately, while for a wildcard match we still go through all
+	 * remaining items in case an exact match exists. */
+	llist_for_each_entry(ni, &nil->list, entry) {
+		if (neighbor_ident_key_match(&ni->key, key, true))
+			return ni;
+		if (!exact_match) {
+			if (neighbor_ident_key_match(&ni->key, key, false))
+				wildcard_match = ni;
+		}
+	}
+	return wildcard_match;
+}
+
+static void _neighbor_ident_free(struct neighbor_ident *ni)
+{
+	llist_del(&ni->entry);
+	talloc_free(ni);
+}
+
+bool neighbor_ident_key_valid(const struct neighbor_ident_key *key)
+{
+	if (key->from_bts != NEIGHBOR_IDENT_KEY_ANY_BTS
+	    && (key->from_bts < 0 || key->from_bts > 255))
+		return false;
+
+	switch (key->bsic_kind) {
+	case BSIC_6BIT:
+		if (key->bsic > 0x3f)
+			return false;
+		break;
+	case BSIC_9BIT:
+		if (key->bsic > 0x1ff)
+			return false;
+		break;
+	case BSIC_NONE:
+		break;
+	default:
+		return false;
+	}
+	return true;
+}
+
+/*! Add Cell Identifiers to an ARFCN+BSIC entry.
+ * Exactly one kind of identifier is allowed per ARFCN+BSIC entry, and any number of entries of that kind
+ * may be added up to the capacity of gsm0808_cell_id_list2, by one or more calls to this function. To
+ * replace an existing entry, first call neighbor_ident_del(nil, key).
+ * \returns number of entries in the resulting identifier list, or negative on error:
+ *   see gsm0808_cell_id_list_add() for the meaning of returned error codes;
+ *   return -ENOMEM when the list is not initialized, -ERANGE when the BSIC value is too large. */
+int neighbor_ident_add(struct neighbor_ident_list *nil, const struct neighbor_ident_key *key,
+		       const struct gsm0808_cell_id_list2 *val)
+{
+	struct neighbor_ident *ni;
+	int rc;
+
+	if (!nil)
+		return -ENOMEM;
+
+	if (!neighbor_ident_key_valid(key))
+		return -ERANGE;
+
+	ni = _neighbor_ident_get(nil, key, true);
+	if (!ni) {
+		ni = talloc_zero(nil, struct neighbor_ident);
+		OSMO_ASSERT(ni);
+		*ni = (struct neighbor_ident){
+			.key = *key,
+			.val = *val,
+		};
+		llist_add_tail(&ni->entry, &nil->list);
+		return ni->val.id_list_len;
+	}
+
+	rc = gsm0808_cell_id_list_add(&ni->val, val);
+
+	if (rc < 0)
+		return rc;
+
+	return ni->val.id_list_len;
+}
+
+/*! Find cell identity for given BTS, ARFCN and BSIC, as previously added by neighbor_ident_add().
+ */
+const struct gsm0808_cell_id_list2 *neighbor_ident_get(const struct neighbor_ident_list *nil,
+						       const struct neighbor_ident_key *key)
+{
+	struct neighbor_ident *ni;
+	if (!nil)
+		return NULL;
+	ni = _neighbor_ident_get(nil, key, false);
+	if (!ni)
+		return NULL;
+	return &ni->val;
+}
+
+bool neighbor_ident_del(struct neighbor_ident_list *nil, const struct neighbor_ident_key *key)
+{
+	struct neighbor_ident *ni;
+	if (!nil)
+		return false;
+	ni = _neighbor_ident_get(nil, key, true);
+	if (!ni)
+		return false;
+	_neighbor_ident_free(ni);
+	return true;
+}
+
+void neighbor_ident_clear(struct neighbor_ident_list *nil)
+{
+	struct neighbor_ident *ni;
+	while ((ni = llist_first_entry_or_null(&nil->list, struct neighbor_ident, entry)))
+		_neighbor_ident_free(ni);
+}
+
+/*! Iterate all neighbor_ident_list entries and call iter_cb for each.
+ * If iter_cb returns false, the iteration is stopped. */
+void neighbor_ident_iter(const struct neighbor_ident_list *nil,
+			 bool (* iter_cb )(const struct neighbor_ident_key *key,
+					   const struct gsm0808_cell_id_list2 *val,
+					   void *cb_data),
+			 void *cb_data)
+{
+	struct neighbor_ident *ni, *ni_next;
+	if (!nil)
+		return;
+	llist_for_each_entry_safe(ni, ni_next, &nil->list, entry) {
+		if (!iter_cb(&ni->key, &ni->val, cb_data))
+			return;
+	}
+}
diff --git a/src/osmo-bsc/neighbor_ident_vty.c b/src/osmo-bsc/neighbor_ident_vty.c
new file mode 100644
index 0000000..5b659fd
--- /dev/null
+++ b/src/osmo-bsc/neighbor_ident_vty.c
@@ -0,0 +1,561 @@
+/* Quagga VTY implementation to manage identity of neighboring BSS cells for inter-BSC handover. */
+/* (C) 2018 by sysmocom - s.f.m.c. GmbH <info at sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * Author: Neels Hofmeyr <nhofmeyr at sysmocom.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+
+#include <osmocom/vty/command.h>
+#include <osmocom/gsm/gsm0808.h>
+
+#include <osmocom/bsc/vty.h>
+#include <osmocom/bsc/neighbor_ident.h>
+#include <osmocom/bsc/gsm_data.h>
+
+static struct gsm_network *g_net = NULL;
+static struct neighbor_ident_list *g_neighbor_cells = NULL;
+
+/* Parse VTY parameters matching NEIGHBOR_IDENT_VTY_KEY_PARAMS. Pass a pointer so that argv[0] is the
+ * ARFCN value followed by the BSIC keyword and value. vty *must* reference a BTS_NODE. */
+bool neighbor_ident_vty_parse_key_params(struct vty *vty, const char **argv, struct neighbor_ident_key *key)
+{
+	struct gsm_bts *bts = vty->index;
+	const char *arfcn_str = argv[0];
+	const char *bsic_kind = argv[1];
+	const char *bsic_str = argv[2];
+
+	OSMO_ASSERT(vty->node == BTS_NODE && bts);
+
+	*key = (struct neighbor_ident_key){
+		.from_bts = bts->nr,
+		.arfcn = atoi(arfcn_str),
+	};
+
+	if (!strcmp(bsic_str, "any"))
+		key->bsic_kind = BSIC_NONE;
+	else {
+		key->bsic_kind = (!strcmp(bsic_kind, "bsic9")) ? BSIC_9BIT : BSIC_6BIT;
+		key->bsic = atoi(bsic_str);
+		if (key->bsic_kind == BSIC_6BIT && key->bsic > 0x3f) {
+			vty_out(vty, "%% Error: BSIC value surpasses 6-bit range: %u, use 'bsic9' instead%s",
+				key->bsic, VTY_NEWLINE);
+			return false;
+		}
+	}
+	return true;
+}
+
+#define NEIGHBOR_ADD_CMD "neighbor add "
+#define NEIGHBOR_DEL_CMD "neighbor del "
+#define NEIGHBOR_DOC "Neighbor cell list\n"
+#define NEIGHBOR_ADD_DOC NEIGHBOR_DOC "Add local or remote-BSS neighbor cell\n"
+#define NEIGHBOR_DEL_DOC NEIGHBOR_DOC "Remove local or remote-BSS neighbor cell\n"
+
+static struct gsm_bts *neighbor_ident_vty_parse_bts_nr(struct vty *vty, const char **argv)
+{
+	const char *bts_nr_str = argv[0];
+	struct gsm_bts *bts = gsm_bts_num(g_net, atoi(bts_nr_str));
+	if (!bts)
+		vty_out(vty, "%% No such BTS: nr = %s%s\n", bts_nr_str, VTY_NEWLINE);
+	return bts;
+}
+
+static struct gsm_bts *bts_by_cell_id(struct vty *vty, struct gsm0808_cell_id *cell_id)
+{
+	struct gsm_bts *bts = gsm_bts_by_cell_id(g_net, cell_id);
+	if (!bts)
+		vty_out(vty, "%% No such BTS: %s%s\n", gsm0808_cell_id_name(cell_id), VTY_NEWLINE);
+	return bts;
+}
+
+static struct gsm0808_cell_id *neighbor_ident_vty_parse_lac(struct vty *vty, const char **argv)
+{
+	static struct gsm0808_cell_id cell_id;
+	cell_id = (struct gsm0808_cell_id){
+		.id_discr = CELL_IDENT_LAC,
+		.id.lac = atoi(argv[0]),
+	};
+	return &cell_id;
+}
+
+static struct gsm0808_cell_id *neighbor_ident_vty_parse_lac_ci(struct vty *vty, const char **argv)
+{
+	static struct gsm0808_cell_id cell_id;
+	cell_id = (struct gsm0808_cell_id){
+		.id_discr = CELL_IDENT_LAC_AND_CI,
+		.id.lac_and_ci = {
+			.lac = atoi(argv[0]),
+			.ci = atoi(argv[1]),
+		},
+	};
+	return &cell_id;
+}
+
+static struct gsm0808_cell_id *neighbor_ident_vty_parse_cgi(struct vty *vty, const char **argv)
+{
+	static struct gsm0808_cell_id cell_id;
+	cell_id = (struct gsm0808_cell_id){
+		.id_discr = CELL_IDENT_WHOLE_GLOBAL,
+	};
+	struct osmo_cell_global_id *cgi = &cell_id.id.global;
+	const char *mcc = argv[0];
+	const char *mnc = argv[1];
+	const char *lac = argv[2];
+	const char *ci = argv[3];
+
+	if (osmo_mcc_from_str(mcc, &cgi->lai.plmn.mcc)) {
+		vty_out(vty, "%% Error decoding MCC: %s%s", mcc, VTY_NEWLINE);
+		return NULL;
+	}
+
+	if (osmo_mnc_from_str(mnc, &cgi->lai.plmn.mnc, &cgi->lai.plmn.mnc_3_digits)) {
+		vty_out(vty, "%% Error decoding MNC: %s%s", mnc, VTY_NEWLINE);
+		return NULL;
+	}
+
+	cgi->lai.lac = atoi(lac);
+	cgi->cell_identity = atoi(ci);
+	return &cell_id;
+}
+
+static int add_local_bts(struct vty *vty, struct gsm_bts *neigh)
+{
+	int rc;
+	struct gsm_bts *bts = vty->index;
+	if (vty->node != BTS_NODE) {
+		vty_out(vty, "%% Error: cannot add local BTS neighbor, not on BTS node%s",
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	if (!bts) {
+		vty_out(vty, "%% Error: cannot add local BTS neighbor, no BTS on this node%s",
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	if (!neigh) {
+		vty_out(vty, "%% Error: cannot add local BTS neighbor to BTS %u, no such neighbor BTS%s"
+			"%% (To add remote-BSS neighbors, pass full ARFCN and BSIC as well)%s",
+			bts->nr, VTY_NEWLINE, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	rc = gsm_bts_local_neighbor_add(bts, neigh);
+	if (rc < 0) {
+		vty_out(vty, "%% Error: cannot add local BTS %u as neighbor to BTS %u: %s%s",
+			neigh->nr, bts->nr, strerror(-rc), VTY_NEWLINE);
+		return CMD_WARNING;
+	} else
+		vty_out(vty, "%% BTS %u %s local neighbor BTS %u with LAC %u CI %u and ARFCN %u BSIC %u%s",
+			bts->nr, rc? "now has" : "already had",
+			neigh->nr, neigh->location_area_code, neigh->cell_identity,
+			neigh->c0->arfcn, neigh->bsic, VTY_NEWLINE);
+	return CMD_SUCCESS;
+}
+
+static int del_local_bts(struct vty *vty, struct gsm_bts *neigh)
+{
+	int rc;
+	struct gsm_bts *bts = vty->index;
+	if (vty->node != BTS_NODE) {
+		vty_out(vty, "%% Error: cannot remove local BTS neighbor, not on BTS node%s",
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	if (!bts) {
+		vty_out(vty, "%% Error: cannot remove local BTS neighbor, no BTS on this node%s",
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	if (!neigh) {
+		vty_out(vty, "%% Error: cannot remove local BTS neighbor from BTS %u, no such neighbor BTS%s",
+			bts->nr, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	rc = gsm_bts_local_neighbor_del(bts, neigh);
+	if (rc < 0) {
+		vty_out(vty, "%% Error: cannot remove local BTS %u neighbor from BTS %u: %s%s",
+			neigh->nr, bts->nr, strerror(-rc), VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	if (rc == 0)
+		vty_out(vty, "%% BTS %u is no neighbor of BTS %u%s",
+			neigh->nr, bts->nr, VTY_NEWLINE);
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_neighbor_add_bts_nr, cfg_neighbor_add_bts_nr_cmd,
+	NEIGHBOR_ADD_CMD "bts <0-255>",
+	NEIGHBOR_ADD_DOC "Neighbor cell by local BTS number\n" "BTS number\n")
+{
+	return add_local_bts(vty, neighbor_ident_vty_parse_bts_nr(vty, argv));
+}
+
+DEFUN(cfg_neighbor_add_lac, cfg_neighbor_add_lac_cmd,
+	NEIGHBOR_ADD_CMD "lac <0-65535>",
+	NEIGHBOR_ADD_DOC "Neighbor cell by LAC\n" "LAC\n")
+{
+	return add_local_bts(vty, bts_by_cell_id(vty, neighbor_ident_vty_parse_lac(vty, argv)));
+}
+
+DEFUN(cfg_neighbor_add_lac_ci, cfg_neighbor_add_lac_ci_cmd,
+	NEIGHBOR_ADD_CMD "lac-ci <0-65535> <0-255>",
+	NEIGHBOR_ADD_DOC "Neighbor cell by LAC and CI\n" "LAC\n" "CI\n")
+{
+	return add_local_bts(vty, bts_by_cell_id(vty, neighbor_ident_vty_parse_lac_ci(vty, argv)));
+}
+
+bool neighbor_ident_key_matches_bts(const struct neighbor_ident_key *key, struct gsm_bts *bts)
+{
+	if (!bts || !key)
+		return false;
+	return key->arfcn == bts->c0->arfcn
+		&& (key->bsic_kind == BSIC_NONE || key->bsic == bts->bsic);
+}
+
+static int add_remote_or_local_bts(struct vty *vty, const struct gsm0808_cell_id *cell_id,
+				   const struct neighbor_ident_key *key)
+{
+	int rc;
+	struct gsm_bts *local_neigh;
+	struct gsm0808_cell_id_list2 cil;
+	struct gsm_bts *bts = vty->index;
+
+	if (vty->node != BTS_NODE) {
+		vty_out(vty, "%% Error: cannot add BTS neighbor, not on BTS node%s",
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	if (!bts) {
+		vty_out(vty, "%% Error: cannot add BTS neighbor, no BTS on this node%s",
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	/* Is there a local BTS that matches the cell_id? */
+	local_neigh = gsm_bts_by_cell_id(g_net, cell_id);
+	if (local_neigh) {
+		/* But do the advertised ARFCN and BSIC match as intended?
+		 * The user may omit ARFCN and BSIC for local cells, but if they are provided,
+		 * they need to match. */
+		if (!neighbor_ident_key_matches_bts(key, local_neigh)) {
+			vty_out(vty, "%% Error: bts %u: neighbor cell id %s indicates local BTS %u,"
+				" but it does not match ARFCN+BSIC %s%s",
+				bts->nr, gsm0808_cell_id_name(cell_id), local_neigh->nr,
+				neighbor_ident_key_name(key), VTY_NEWLINE);
+			/* TODO: error out fatally for non-interactive VTY? */
+			return CMD_WARNING;
+		}
+		return add_local_bts(vty, local_neigh);
+	}
+
+	/* The cell_id is not known in this BSS, so it must be a remote cell. */
+	gsm0808_cell_id_to_list(&cil, cell_id);
+	rc = neighbor_ident_add(g_neighbor_cells, key, &cil);
+
+	if (rc < 0) {
+		const char *reason;
+		switch (rc) {
+		case -EINVAL:
+			reason = ": mismatching type between current and newly added cell identifier";
+			break;
+		case -ENOSPC:
+			reason = ": list is full";
+			break;
+		default:
+			reason = "";
+			break;
+		}
+
+		vty_out(vty, "%% Error adding neighbor-BSS Cell Identifier %s%s%s",
+			gsm0808_cell_id_name(cell_id), reason, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	vty_out(vty, "%% %s now has %d remote BSS Cell Identifier List %s%s",
+		neighbor_ident_key_name(key), rc, rc == 1? "entry" : "entries", VTY_NEWLINE);
+	return CMD_SUCCESS;
+}
+									
+static int del_by_key(struct vty *vty, const struct neighbor_ident_key *key)
+{
+	int removed = 0;
+	int rc;
+	struct gsm_bts *bts = vty->index;
+	struct gsm_bts_ref *neigh, *safe;
+
+	if (vty->node != BTS_NODE) {
+		vty_out(vty, "%% Error: cannot remove BTS neighbor, not on BTS node%s",
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	if (!bts) {
+		vty_out(vty, "%% Error: cannot remove BTS neighbor, no BTS on this node%s",
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	/* Is there a local BTS that matches the key? */
+	llist_for_each_entry_safe(neigh, safe, &bts->local_neighbors, entry) {
+		struct gsm_bts *neigh_bts = neigh->bts;
+		if (!neighbor_ident_key_matches_bts(key, neigh->bts))
+			continue;
+		rc = gsm_bts_local_neighbor_del(bts, neigh->bts);
+		if (rc > 0) {
+			vty_out(vty, "%% Removed local neighbor bts %u to bts %u%s",
+				bts->nr, neigh_bts->nr, VTY_NEWLINE);
+			removed += rc;
+		}
+	}
+
+	if (neighbor_ident_del(g_neighbor_cells, key)) {
+		vty_out(vty, "%% Removed remote BSS neighbor %s%s",
+			neighbor_ident_key_name(key), VTY_NEWLINE);
+		removed ++;
+	}
+
+	if (!removed) {
+		vty_out(vty, "%% Cannot remove, no such neighbor: %s%s",
+			neighbor_ident_key_name(key), VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	return CMD_SUCCESS;
+}
+									
+DEFUN(cfg_neighbor_add_lac_arfcn_bsic, cfg_neighbor_add_lac_arfcn_bsic_cmd,
+	NEIGHBOR_ADD_CMD "lac <0-65535> " NEIGHBOR_IDENT_VTY_KEY_PARAMS,
+	NEIGHBOR_ADD_DOC "Neighbor cell by lac\n" "lac\n" NEIGHBOR_IDENT_VTY_KEY_DOC)
+{
+	struct neighbor_ident_key nik;
+	struct gsm0808_cell_id *cell_id = neighbor_ident_vty_parse_lac(vty, argv);
+	if (!cell_id)
+		return CMD_WARNING;
+	if (!neighbor_ident_vty_parse_key_params(vty, argv + 1, &nik))
+		return CMD_WARNING;
+	return add_remote_or_local_bts(vty, cell_id, &nik);
+}
+
+DEFUN(cfg_neighbor_add_lac_ci_arfcn_bsic, cfg_neighbor_add_lac_ci_arfcn_bsic_cmd,
+	NEIGHBOR_ADD_CMD "lac-ci <0-65535> <0-255> " NEIGHBOR_IDENT_VTY_KEY_PARAMS,
+	NEIGHBOR_ADD_DOC "Neighbor cell by LAC and CI\n" "LAC\n" "CI\n" NEIGHBOR_IDENT_VTY_KEY_DOC)
+{
+	struct neighbor_ident_key nik;
+	struct gsm0808_cell_id *cell_id = neighbor_ident_vty_parse_lac_ci(vty, argv);
+	if (!cell_id)
+		return CMD_WARNING;
+	if (!neighbor_ident_vty_parse_key_params(vty, argv + 2, &nik))
+		return CMD_WARNING;
+	return add_remote_or_local_bts(vty, cell_id, &nik);
+}
+
+DEFUN(cfg_neighbor_add_cgi_arfcn_bsic, cfg_neighbor_add_cgi_arfcn_bsic_cmd,
+	NEIGHBOR_ADD_CMD "cgi <0-999> <0-999> <0-65535> <0-255> " NEIGHBOR_IDENT_VTY_KEY_PARAMS,
+	NEIGHBOR_ADD_DOC "Neighbor cell by cgi\n" "MCC\n" "MNC\n" "LAC\n" "CI\n" NEIGHBOR_IDENT_VTY_KEY_DOC)
+{
+	struct neighbor_ident_key nik;
+	struct gsm0808_cell_id *cell_id = neighbor_ident_vty_parse_cgi(vty, argv);
+	if (!cell_id)
+		return CMD_WARNING;
+	if (!neighbor_ident_vty_parse_key_params(vty, argv + 4, &nik))
+		return CMD_WARNING;
+	return add_remote_or_local_bts(vty, cell_id, &nik);
+}
+
+DEFUN(cfg_neighbor_del_bts_nr, cfg_neighbor_del_bts_nr_cmd,
+	NEIGHBOR_DEL_CMD "bts <0-255>",
+	NEIGHBOR_DEL_DOC "Neighbor cell by local BTS number\n" "BTS number\n")
+{
+	return del_local_bts(vty, neighbor_ident_vty_parse_bts_nr(vty, argv));
+}
+
+DEFUN(cfg_neighbor_del_arfcn_bsic, cfg_neighbor_del_arfcn_bsic_cmd,
+	NEIGHBOR_DEL_CMD NEIGHBOR_IDENT_VTY_KEY_PARAMS,
+	NEIGHBOR_DEL_DOC NEIGHBOR_IDENT_VTY_KEY_DOC)
+{
+	struct neighbor_ident_key key;
+
+	if (!neighbor_ident_vty_parse_key_params(vty, argv, &key))
+		return CMD_WARNING;
+
+	return del_by_key(vty, &key);
+}
+
+struct write_neighbor_ident_entry_data {
+	struct vty *vty;
+	const char *indent;
+	struct gsm_bts *bts;
+};
+
+static bool write_neighbor_ident_list(const struct neighbor_ident_key *key,
+				      const struct gsm0808_cell_id_list2 *val,
+				      void *cb_data)
+{
+	struct write_neighbor_ident_entry_data *d = cb_data;
+	struct vty *vty = d->vty;
+	int i;
+
+	if (d->bts) {
+		if (d->bts->nr != key->from_bts)
+			return true;
+	} else if (key->from_bts != NEIGHBOR_IDENT_KEY_ANY_BTS)
+			return true;
+
+#define NEIGH_BSS_WRITE(fmt, args...) do { \
+		vty_out(vty, "%sneighbor add " fmt " arfcn %u ", d->indent, ## args, key->arfcn); \
+		switch (key->bsic_kind) { \
+		default: \
+		case BSIC_NONE: \
+			vty_out(vty, "bsic any"); \
+			break; \
+		case BSIC_6BIT: \
+			vty_out(vty, "bsic %u", key->bsic & 0x3f); \
+			break; \
+		case BSIC_9BIT: \
+			vty_out(vty, "bsic9 %u", key->bsic & 0x1ff); \
+			break; \
+		} \
+		vty_out(vty, "%s", VTY_NEWLINE); \
+	} while(0)
+
+	switch (val->id_discr) {
+	case CELL_IDENT_LAC:
+		for (i = 0; i < val->id_list_len; i++) {
+			NEIGH_BSS_WRITE("lac %u", val->id_list[i].lac);
+		}
+		break;
+	case CELL_IDENT_LAC_AND_CI:
+		for (i = 0; i < val->id_list_len; i++) {
+			NEIGH_BSS_WRITE("lac-ci %u %u",
+					val->id_list[i].lac_and_ci.lac,
+					val->id_list[i].lac_and_ci.ci);
+		}
+		break;
+	case CELL_IDENT_WHOLE_GLOBAL:
+		for (i = 0; i < val->id_list_len; i++) {
+			const struct osmo_cell_global_id *cgi = &val->id_list[i].global;
+			NEIGH_BSS_WRITE("cgi %s %s %u %u",
+					osmo_mcc_name(cgi->lai.plmn.mcc),
+					osmo_mnc_name(cgi->lai.plmn.mnc, cgi->lai.plmn.mnc_3_digits),
+					cgi->lai.lac, cgi->cell_identity);
+		}
+		break;
+	default:
+		vty_out(vty, "%% Unsupported Cell Identity%s", VTY_NEWLINE);
+	}
+#undef NEIGH_BSS_WRITE
+
+	return true;
+}
+
+void neighbor_ident_vty_write_remote_bss(struct vty *vty, const char *indent, struct gsm_bts *bts)
+{
+	struct write_neighbor_ident_entry_data d = {
+		.vty = vty,
+		.indent = indent,
+		.bts = bts,
+	};
+
+	neighbor_ident_iter(g_neighbor_cells, write_neighbor_ident_list, &d);
+}
+
+void neighbor_ident_vty_write_local_neighbors(struct vty *vty, const char *indent, struct gsm_bts *bts)
+{
+	struct gsm_bts_ref *neigh;
+
+	llist_for_each_entry(neigh, &bts->local_neighbors, entry) {
+		vty_out(vty, "%sneighbor add lac-ci %u %u%s",
+			indent, neigh->bts->location_area_code, neigh->bts->cell_identity,
+			VTY_NEWLINE);
+	}
+}
+
+void neighbor_ident_vty_write(struct vty *vty, const char *indent, struct gsm_bts *bts)
+{
+	neighbor_ident_vty_write_local_neighbors(vty, indent, bts);
+	neighbor_ident_vty_write_remote_bss(vty, indent, bts);
+}
+
+DEFUN(cfg_neighbor_resolve, cfg_neighbor_resolve_cmd,
+      "neighbor resolve " NEIGHBOR_IDENT_VTY_KEY_PARAMS,
+      NEIGHBOR_DOC
+      "Query which cell would be the target for this neighbor ARFCN+BSIC\n"
+      NEIGHBOR_IDENT_VTY_KEY_DOC)
+{
+	int found = 0;
+	struct neighbor_ident_key key;
+	struct gsm_bts_ref *neigh;
+	const struct gsm0808_cell_id_list2 *res;
+	struct gsm_bts *bts = vty->index;
+	struct write_neighbor_ident_entry_data d = {
+		.vty = vty,
+		.indent = "% ",
+		.bts = bts,
+	};
+
+	if (vty->node != BTS_NODE) {
+		vty_out(vty, "%% Error: cannot query BTS neighbor, not on BTS node%s",
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	if (!bts) {
+		vty_out(vty, "%% Error: cannot query BTS neighbor, no BTS on this node%s",
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	if (!neighbor_ident_vty_parse_key_params(vty, argv, &key))
+		return CMD_WARNING;
+
+	/* Is there a local BTS that matches the key? */
+	llist_for_each_entry(neigh, &bts->local_neighbors, entry) {
+		if (!neighbor_ident_key_matches_bts(&key, neigh->bts))
+			continue;
+		vty_out(vty, "%% %s resolves to local BTS %u lac-ci %u %u%s",
+			neighbor_ident_key_name(&key), neigh->bts->nr, neigh->bts->location_area_code,
+			neigh->bts->cell_identity, VTY_NEWLINE);
+		found++;
+	}
+
+	res = neighbor_ident_get(g_neighbor_cells, &key);
+	if (res) {
+		write_neighbor_ident_list(&key, res, &d);
+		found++;
+	}
+
+	if (!found)
+		vty_out(vty, "%% No entry for %s%s", neighbor_ident_key_name(&key), VTY_NEWLINE);
+
+	return CMD_SUCCESS;
+}
+
+void neighbor_ident_vty_init(struct gsm_network *net, struct neighbor_ident_list *nil)
+{
+	g_net = net;
+	g_neighbor_cells = nil;
+	install_element(BTS_NODE, &cfg_neighbor_add_bts_nr_cmd);
+	install_element(BTS_NODE, &cfg_neighbor_add_lac_cmd);
+	install_element(BTS_NODE, &cfg_neighbor_add_lac_ci_cmd);
+	install_element(BTS_NODE, &cfg_neighbor_add_lac_arfcn_bsic_cmd);
+	install_element(BTS_NODE, &cfg_neighbor_add_lac_ci_arfcn_bsic_cmd);
+	install_element(BTS_NODE, &cfg_neighbor_add_cgi_arfcn_bsic_cmd);
+	install_element(BTS_NODE, &cfg_neighbor_del_bts_nr_cmd);
+	install_element(BTS_NODE, &cfg_neighbor_del_arfcn_bsic_cmd);
+	install_element(BTS_NODE, &cfg_neighbor_resolve_cmd);
+}
diff --git a/src/osmo-bsc/net_init.c b/src/osmo-bsc/net_init.c
index 3ee35fe..db3d01c 100644
--- a/src/osmo-bsc/net_init.c
+++ b/src/osmo-bsc/net_init.c
@@ -22,6 +22,7 @@
 #include <osmocom/bsc/gsm_04_08_utils.h>
 #include <osmocom/bsc/handover_cfg.h>
 #include <osmocom/bsc/chan_alloc.h>
+#include <osmocom/bsc/neighbor_ident.h>
 
 /* Initialize the bare minimum of struct gsm_network, minimizing required dependencies.
  * This part is shared among the thin programs in osmo-bsc/src/utils/.
diff --git a/src/osmo-bsc/system_information.c b/src/osmo-bsc/system_information.c
index d99153f..071baba 100644
--- a/src/osmo-bsc/system_information.c
+++ b/src/osmo-bsc/system_information.c
@@ -40,6 +40,9 @@
 #include <osmocom/bsc/arfcn_range_encode.h>
 #include <osmocom/bsc/gsm_04_08_utils.h>
 #include <osmocom/bsc/acc_ramp.h>
+#include <osmocom/bsc/neighbor_ident.h>
+
+struct gsm0808_cell_id_list2;
 
 /*
  * DCS1800 and PCS1900 have overlapping ARFCNs. We would need to set the
@@ -588,6 +591,25 @@
 	return bitvec2freq_list(chan_list, bv, bts, false, false);
 }
 
+struct generate_bcch_chan_list__ni_iter_data {
+	struct gsm_bts *bts;
+	struct bitvec *bv;
+};
+
+static bool generate_bcch_chan_list__ni_iter_cb(const struct neighbor_ident_key *key,
+						const struct gsm0808_cell_id_list2 *val,
+						void *cb_data)
+{
+	struct generate_bcch_chan_list__ni_iter_data *data = cb_data;
+
+	if (key->from_bts != NEIGHBOR_IDENT_KEY_ANY_BTS
+	    && key->from_bts != data->bts->nr)
+		return true;
+
+	bitvec_set_bit_pos(data->bv, key->arfcn, 1);
+	return true;
+}
+
 /*! generate a cell channel list as per Section 10.5.2.22 of 04.08
  *  \param[out] chan_list caller-provided output buffer
  *  \param[in] bts BTS descriptor used for input data
@@ -602,6 +624,7 @@
 	struct bitvec *bv;
 	int rc;
 
+	/* first we generate a bitvec of the BCCH ARFCN's in our BSC */
 	if (si5 && bts->neigh_list_manual_mode == NL_MODE_MANUAL_SI5SEP)
 		bv = &bts->si_common.si5_neigh_list;
 	else
@@ -612,11 +635,29 @@
 		/* Zero-initialize the bit-vector */
 		memset(bv->data, 0, bv->data_len);
 
-		/* first we generate a bitvec of the BCCH ARFCN's in our BSC */
-		llist_for_each_entry(cur_bts, &bts->network->bts_list, list) {
-			if (cur_bts == bts)
-				continue;
-			bitvec_set_bit_pos(bv, cur_bts->c0->arfcn, 1);
+		if (llist_empty(&bts->local_neighbors)) {
+			/* There are no explicit neighbors, assume all BTS are. */
+			llist_for_each_entry(cur_bts, &bts->network->bts_list, list) {
+				if (cur_bts == bts)
+					continue;
+				bitvec_set_bit_pos(bv, cur_bts->c0->arfcn, 1);
+			}
+		} else {
+			/* Only add explicit neighbor cells */
+			struct gsm_bts_ref *neigh;
+			llist_for_each_entry(neigh, &bts->local_neighbors, entry) {
+				bitvec_set_bit_pos(bv, neigh->bts->c0->arfcn, 1);
+			}
+		}
+
+		/* Also add neighboring BSS cells' ARFCNs */
+		{
+			struct generate_bcch_chan_list__ni_iter_data data = {
+				.bv = bv,
+				.bts = bts,
+			};
+			neighbor_ident_iter(bts->network->neighbor_bss_cells,
+					    generate_bcch_chan_list__ni_iter_cb, &data);
 		}
 	}
 
diff --git a/tests/bsc/Makefile.am b/tests/bsc/Makefile.am
index a930629..2e34d79 100644
--- a/tests/bsc/Makefile.am
+++ b/tests/bsc/Makefile.am
@@ -45,6 +45,7 @@
 	$(top_builddir)/src/osmo-bsc/gsm_data.o \
 	$(top_builddir)/src/osmo-bsc/handover_cfg.o \
 	$(top_builddir)/src/osmo-bsc/handover_logic.o \
+	$(top_builddir)/src/osmo-bsc/neighbor_ident.o \
 	$(top_builddir)/src/osmo-bsc/net_init.o \
 	$(top_builddir)/src/osmo-bsc/paging.o \
 	$(top_builddir)/src/osmo-bsc/pcu_sock.o \
diff --git a/tests/gsm0408/Makefile.am b/tests/gsm0408/Makefile.am
index 6d10b9f..3eb47f6 100644
--- a/tests/gsm0408/Makefile.am
+++ b/tests/gsm0408/Makefile.am
@@ -28,6 +28,7 @@
 	$(top_builddir)/src/osmo-bsc/net_init.o \
 	$(top_builddir)/src/osmo-bsc/rest_octets.o \
 	$(top_builddir)/src/osmo-bsc/system_information.o \
+	$(top_builddir)/src/osmo-bsc/neighbor_ident.o \
 	$(LIBOSMOCORE_LIBS) \
 	$(LIBOSMOGSM_LIBS) \
 	$(LIBOSMOABIS_LIBS) \
diff --git a/tests/handover/Makefile.am b/tests/handover/Makefile.am
index 07491d5..2f84d7a 100644
--- a/tests/handover/Makefile.am
+++ b/tests/handover/Makefile.am
@@ -23,6 +23,7 @@
 
 noinst_PROGRAMS = \
 	handover_test \
+	neighbor_ident_test \
 	$(NULL)
 
 handover_test_SOURCES = \
@@ -56,6 +57,7 @@
 	$(top_builddir)/src/osmo-bsc/handover_decision_2.o \
 	$(top_builddir)/src/osmo-bsc/handover_logic.o \
 	$(top_builddir)/src/osmo-bsc/meas_rep.o \
+	$(top_builddir)/src/osmo-bsc/neighbor_ident.o \
 	$(top_builddir)/src/osmo-bsc/osmo_bsc_lcls.o \
 	$(top_builddir)/src/osmo-bsc/net_init.o \
 	$(top_builddir)/src/osmo-bsc/paging.o \
@@ -69,3 +71,17 @@
 	$(LIBOSMOSIGTRAN_LIBS) \
 	$(LIBOSMOMGCPCLIENT_LIBS) \
 	$(NULL)
+
+neighbor_ident_test_SOURCES = \
+	neighbor_ident_test.c \
+	$(NULL)
+
+neighbor_ident_test_LDADD = \
+	$(top_builddir)/src/osmo-bsc/neighbor_ident.o \
+	$(LIBOSMOCORE_LIBS) \
+	$(LIBOSMOGSM_LIBS) \
+	$(NULL)
+
+.PHONY: update_exp
+update_exp:
+	$(builddir)/neighbor_ident_test >$(srcdir)/neighbor_ident_test.ok 2>$(srcdir)/neighbor_ident_test.err
diff --git a/tests/handover/neighbor_ident_test.c b/tests/handover/neighbor_ident_test.c
new file mode 100644
index 0000000..b67219c
--- /dev/null
+++ b/tests/handover/neighbor_ident_test.c
@@ -0,0 +1,278 @@
+/* Test the neighbor_ident.h API */
+/*
+ * (C) 2018 by sysmocom - s.f.m.c. GmbH <info at sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Neels Hofmeyr <nhofmeyr at sysmocom.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <talloc.h>
+#include <stdio.h>
+#include <errno.h>
+
+#include <osmocom/gsm/gsm0808.h>
+
+#include <osmocom/bsc/neighbor_ident.h>
+
+struct neighbor_ident_list *nil;
+
+static const struct neighbor_ident_key *k(int from_bts, uint16_t arfcn, enum bsic_kind kind, uint16_t bsic)
+{
+	static struct neighbor_ident_key key;
+	key = (struct neighbor_ident_key){
+		.from_bts = from_bts,
+		.arfcn = arfcn,
+		.bsic_kind = kind,
+		.bsic = bsic,
+	};
+	return &key;
+}
+
+static const struct gsm0808_cell_id_list2 cgi1 = {
+	.id_discr = CELL_IDENT_WHOLE_GLOBAL,
+	.id_list_len = 1,
+	.id_list = {
+		{
+			.global = {
+				.lai = {
+					.plmn = { .mcc = 1, .mnc = 2, .mnc_3_digits = false },
+					.lac = 3,
+				},
+				.cell_identity = 4,
+			}
+		},
+	},
+};
+
+static const struct gsm0808_cell_id_list2 cgi2 = {
+	.id_discr = CELL_IDENT_WHOLE_GLOBAL,
+	.id_list_len = 2,
+	.id_list = {
+		{
+			.global = {
+				.lai = {
+					.plmn = { .mcc = 1, .mnc = 2, .mnc_3_digits = false },
+					.lac = 3,
+				},
+				.cell_identity = 4,
+			}
+		},
+		{
+			.global = {
+				.lai = {
+					.plmn = { .mcc = 5, .mnc = 6, .mnc_3_digits = true },
+					.lac = 7,
+				},
+				.cell_identity = 8,
+			}
+		},
+	},
+};
+
+static const struct gsm0808_cell_id_list2 lac1 = {
+	.id_discr = CELL_IDENT_LAC,
+	.id_list_len = 1,
+	.id_list = {
+		{
+			.lac = 123
+		},
+	},
+};
+
+static const struct gsm0808_cell_id_list2 lac2 = {
+	.id_discr = CELL_IDENT_LAC,
+	.id_list_len = 2,
+	.id_list = {
+		{
+			.lac = 456
+		},
+		{
+			.lac = 789
+		},
+	},
+};
+
+void print_cil(const struct gsm0808_cell_id_list2 *cil)
+{
+	unsigned int i;
+	if (!cil) {
+		printf("     cell_id_list == NULL\n");
+		return;
+	}
+	switch (cil->id_discr) {
+	case CELL_IDENT_WHOLE_GLOBAL:
+		printf("     cell_id_list cgi[%u] = {\n", cil->id_list_len);
+		for (i = 0; i < cil->id_list_len; i++)
+			printf("       %2d: %s\n", i, osmo_cgi_name(&cil->id_list[i].global));
+		printf("     }\n");
+		break;
+	case CELL_IDENT_LAC:
+		printf("     cell_id_list lac[%u] = {\n", cil->id_list_len);
+		for (i = 0; i < cil->id_list_len; i++)
+			printf("      %2d: %u\n", i, cil->id_list[i].lac);
+		printf("     }\n");
+		break;
+	default:
+		printf("     Unimplemented id_disc\n");
+	}
+}
+
+static int print_nil_i;
+
+bool nil_cb(const struct neighbor_ident_key *key, const struct gsm0808_cell_id_list2 *val,
+	    void *cb_data)
+{
+	printf(" %2d: %s\n", print_nil_i++, neighbor_ident_key_name(key));
+	print_cil(val);
+	return true;
+}
+
+void print_nil()
+{
+	print_nil_i = 0;
+	neighbor_ident_iter(nil, nil_cb, NULL);
+	if (!print_nil_i)
+		printf("     (empty)\n");
+}
+
+#define check_add(key, val, expect_rc) \
+	do { \
+		int rc; \
+		rc = neighbor_ident_add(nil, key, val); \
+		printf("neighbor_ident_add(" #key ", " #val ") --> expect rc=" #expect_rc ", got %d\n", rc); \
+		if (rc != expect_rc) \
+			printf("ERROR\n"); \
+		print_nil(); \
+	} while(0)
+
+#define check_del(key, expect_rc) \
+	do { \
+		bool rc; \
+		rc = neighbor_ident_del(nil, key); \
+		printf("neighbor_ident_del(" #key ") --> %s\n", rc ? "entry deleted" : "nothing deleted"); \
+		if (rc != expect_rc) \
+			printf("ERROR: expected: %s\n", expect_rc ? "entry deleted" : "nothing deleted"); \
+		print_nil(); \
+	} while(0)
+
+#define check_get(key, expect_rc) \
+	do { \
+		const struct gsm0808_cell_id_list2 *rc; \
+		rc = neighbor_ident_get(nil, key); \
+		printf("neighbor_ident_get(" #key ") --> %s\n", \
+		       rc ? "entry returned" : "NULL"); \
+		if (((bool)expect_rc) != ((bool) rc)) \
+			printf("ERROR: expected %s\n", expect_rc ? "an entry" : "NULL"); \
+		if (rc) \
+			print_cil(rc); \
+	} while(0)
+
+int main(void)
+{
+	void *ctx = talloc_named_const(NULL, 0, "neighbor_ident_test");
+
+	printf("\n--- testing NULL neighbor_ident_list\n");
+	nil = NULL;
+	check_add(k(0, 1, BSIC_6BIT, 2), &cgi1, -ENOMEM);
+	check_get(k(0, 1, BSIC_6BIT, 2), false);
+	check_del(k(0, 1, BSIC_6BIT, 2), false);
+
+	printf("\n--- adding entries, test that no two identical entries are added\n");
+	nil = neighbor_ident_init(ctx);
+	check_add(k(0, 1, BSIC_6BIT, 2), &cgi1, 1);
+	check_get(k(0, 1, BSIC_6BIT, 2), true);
+	check_add(k(0, 1, BSIC_6BIT, 2), &cgi1, 1);
+	check_add(k(0, 1, BSIC_6BIT, 2), &cgi2, 2);
+	check_add(k(0, 1, BSIC_6BIT, 2), &cgi2, 2);
+	check_del(k(0, 1, BSIC_6BIT, 2), true);
+
+	printf("\n--- Cannot mix cell identifier types for one entry\n");
+	check_add(k(0, 1, BSIC_6BIT, 2), &cgi1, 1);
+	check_add(k(0, 1, BSIC_6BIT, 2), &lac1, -EINVAL);
+	check_del(k(0, 1, BSIC_6BIT, 2), true);
+	neighbor_ident_free(nil);
+
+	printf("\n--- BTS matching: specific BTS is stronger\n");
+	nil = neighbor_ident_init(ctx);
+	check_add(k(NEIGHBOR_IDENT_KEY_ANY_BTS, 1, BSIC_6BIT, 2), &lac1, 1);
+	check_add(k(3, 1, BSIC_6BIT, 2), &lac2, 2);
+	check_get(k(2, 1, BSIC_6BIT, 2), true);
+	check_get(k(3, 1, BSIC_6BIT, 2), true);
+	check_get(k(4, 1, BSIC_6BIT, 2), true);
+	check_get(k(NEIGHBOR_IDENT_KEY_ANY_BTS, 1, BSIC_6BIT, 2), true);
+	neighbor_ident_free(nil);
+
+	printf("\n--- BSIC matching: 6bit and 9bit are different realms, and wildcard match is weaker\n");
+	nil = neighbor_ident_init(ctx);
+	check_add(k(0, 1, BSIC_NONE, 0), &cgi1, 1);
+	check_add(k(0, 1, BSIC_6BIT, 2), &lac1, 1);
+	check_add(k(0, 1, BSIC_9BIT, 2), &lac2, 2);
+	check_get(k(0, 1, BSIC_6BIT, 2), true);
+	check_get(k(0, 1, BSIC_9BIT, 2), true);
+	printf("--- wildcard matches both 6bit and 9bit BSIC regardless:\n");
+	check_get(k(0, 1, BSIC_6BIT, 23), true);
+	check_get(k(0, 1, BSIC_9BIT, 23), true);
+	neighbor_ident_free(nil);
+
+	printf("\n--- Value ranges\n");
+	nil = neighbor_ident_init(ctx);
+	check_add(k(0, 6, BSIC_6BIT, 1 << 6), &lac1, -ERANGE);
+	check_add(k(0, 9, BSIC_9BIT, 1 << 9), &lac1, -ERANGE);
+	check_add(k(0, 6, BSIC_6BIT, -1), &lac1, -ERANGE);
+	check_add(k(0, 9, BSIC_9BIT, -1), &lac1, -ERANGE);
+	check_add(k(NEIGHBOR_IDENT_KEY_ANY_BTS - 1, 1, BSIC_NONE, 1), &cgi2, -ERANGE);
+	check_add(k(256, 1, BSIC_NONE, 1), &cgi2, -ERANGE);
+	check_add(k(0, 0, BSIC_NONE, 0), &cgi1, 1);
+	check_add(k(255, 65535, BSIC_NONE, 65535), &lac1, 1);
+	check_add(k(0, 0, BSIC_6BIT, 0), &cgi2, 2);
+	check_add(k(255, 65535, BSIC_6BIT, 0x3f), &lac2, 2);
+	check_add(k(0, 0, BSIC_9BIT, 0), &cgi1, 1);
+	check_add(k(255, 65535, BSIC_9BIT, 0x1ff), &cgi2, 2);
+
+	neighbor_ident_free(nil);
+
+	printf("\n--- size limits\n");
+	{
+		int i;
+		struct gsm0808_cell_id_list2 a = { .id_discr = CELL_IDENT_LAC };
+		struct gsm0808_cell_id_list2 b = {
+			.id_discr = CELL_IDENT_LAC,
+			.id_list = {
+				{ .lac = 423 }
+			},
+			.id_list_len = 1,
+		};
+		for (i = 0; i < ARRAY_SIZE(a.id_list); i++) {
+			a.id_list[a.id_list_len ++].lac = i;
+		}
+
+		nil = neighbor_ident_init(ctx);
+
+		i = neighbor_ident_add(nil, k(0, 1, BSIC_6BIT, 2), &a);
+		printf("Added first cell identifier list (added %u) --> rc = %d\n", a.id_list_len, i);
+		i = neighbor_ident_add(nil, k(0, 1, BSIC_6BIT, 2), &b);
+		printf("Added second cell identifier list (tried to add %u) --> rc = %d\n", b.id_list_len, i);
+		if (i != -ENOSPC)
+			printf("ERROR: expected rc=%d\n", -ENOSPC);
+		neighbor_ident_free(nil);
+	}
+
+	OSMO_ASSERT(talloc_total_blocks(ctx) == 1);
+	talloc_free(ctx);
+
+	return 0;
+}
diff --git a/tests/handover/neighbor_ident_test.err b/tests/handover/neighbor_ident_test.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/handover/neighbor_ident_test.err
diff --git a/tests/handover/neighbor_ident_test.ok b/tests/handover/neighbor_ident_test.ok
new file mode 100644
index 0000000..280b6f2
--- /dev/null
+++ b/tests/handover/neighbor_ident_test.ok
@@ -0,0 +1,249 @@
+
+--- testing NULL neighbor_ident_list
+neighbor_ident_add(k(0, 1, BSIC_6BIT, 2), &cgi1) --> expect rc=-ENOMEM, got -12
+     (empty)
+neighbor_ident_get(k(0, 1, BSIC_6BIT, 2)) --> NULL
+neighbor_ident_del(k(0, 1, BSIC_6BIT, 2)) --> nothing deleted
+     (empty)
+
+--- adding entries, test that no two identical entries are added
+neighbor_ident_add(k(0, 1, BSIC_6BIT, 2), &cgi1) --> expect rc=1, got 1
+  0: BTS 0 to ARFCN 1 BSIC 2
+     cell_id_list cgi[1] = {
+        0: 001-02-3-4
+     }
+neighbor_ident_get(k(0, 1, BSIC_6BIT, 2)) --> entry returned
+     cell_id_list cgi[1] = {
+        0: 001-02-3-4
+     }
+neighbor_ident_add(k(0, 1, BSIC_6BIT, 2), &cgi1) --> expect rc=1, got 1
+  0: BTS 0 to ARFCN 1 BSIC 2
+     cell_id_list cgi[1] = {
+        0: 001-02-3-4
+     }
+neighbor_ident_add(k(0, 1, BSIC_6BIT, 2), &cgi2) --> expect rc=2, got 2
+  0: BTS 0 to ARFCN 1 BSIC 2
+     cell_id_list cgi[2] = {
+        0: 001-02-3-4
+        1: 005-006-7-8
+     }
+neighbor_ident_add(k(0, 1, BSIC_6BIT, 2), &cgi2) --> expect rc=2, got 2
+  0: BTS 0 to ARFCN 1 BSIC 2
+     cell_id_list cgi[2] = {
+        0: 001-02-3-4
+        1: 005-006-7-8
+     }
+neighbor_ident_del(k(0, 1, BSIC_6BIT, 2)) --> entry deleted
+     (empty)
+
+--- Cannot mix cell identifier types for one entry
+neighbor_ident_add(k(0, 1, BSIC_6BIT, 2), &cgi1) --> expect rc=1, got 1
+  0: BTS 0 to ARFCN 1 BSIC 2
+     cell_id_list cgi[1] = {
+        0: 001-02-3-4
+     }
+neighbor_ident_add(k(0, 1, BSIC_6BIT, 2), &lac1) --> expect rc=-EINVAL, got -22
+  0: BTS 0 to ARFCN 1 BSIC 2
+     cell_id_list cgi[1] = {
+        0: 001-02-3-4
+     }
+neighbor_ident_del(k(0, 1, BSIC_6BIT, 2)) --> entry deleted
+     (empty)
+
+--- BTS matching: specific BTS is stronger
+neighbor_ident_add(k(NEIGHBOR_IDENT_KEY_ANY_BTS, 1, BSIC_6BIT, 2), &lac1) --> expect rc=1, got 1
+  0: BTS * to ARFCN 1 BSIC 2
+     cell_id_list lac[1] = {
+       0: 123
+     }
+neighbor_ident_add(k(3, 1, BSIC_6BIT, 2), &lac2) --> expect rc=2, got 2
+  0: BTS * to ARFCN 1 BSIC 2
+     cell_id_list lac[1] = {
+       0: 123
+     }
+  1: BTS 3 to ARFCN 1 BSIC 2
+     cell_id_list lac[2] = {
+       0: 456
+       1: 789
+     }
+neighbor_ident_get(k(2, 1, BSIC_6BIT, 2)) --> entry returned
+     cell_id_list lac[1] = {
+       0: 123
+     }
+neighbor_ident_get(k(3, 1, BSIC_6BIT, 2)) --> entry returned
+     cell_id_list lac[2] = {
+       0: 456
+       1: 789
+     }
+neighbor_ident_get(k(4, 1, BSIC_6BIT, 2)) --> entry returned
+     cell_id_list lac[1] = {
+       0: 123
+     }
+neighbor_ident_get(k(NEIGHBOR_IDENT_KEY_ANY_BTS, 1, BSIC_6BIT, 2)) --> entry returned
+     cell_id_list lac[1] = {
+       0: 123
+     }
+
+--- BSIC matching: 6bit and 9bit are different realms, and wildcard match is weaker
+neighbor_ident_add(k(0, 1, BSIC_NONE, 0), &cgi1) --> expect rc=1, got 1
+  0: BTS 0 to ARFCN 1 (any BSIC)
+     cell_id_list cgi[1] = {
+        0: 001-02-3-4
+     }
+neighbor_ident_add(k(0, 1, BSIC_6BIT, 2), &lac1) --> expect rc=1, got 1
+  0: BTS 0 to ARFCN 1 (any BSIC)
+     cell_id_list cgi[1] = {
+        0: 001-02-3-4
+     }
+  1: BTS 0 to ARFCN 1 BSIC 2
+     cell_id_list lac[1] = {
+       0: 123
+     }
+neighbor_ident_add(k(0, 1, BSIC_9BIT, 2), &lac2) --> expect rc=2, got 2
+  0: BTS 0 to ARFCN 1 (any BSIC)
+     cell_id_list cgi[1] = {
+        0: 001-02-3-4
+     }
+  1: BTS 0 to ARFCN 1 BSIC 2
+     cell_id_list lac[1] = {
+       0: 123
+     }
+  2: BTS 0 to ARFCN 1 BSIC 2(9bit)
+     cell_id_list lac[2] = {
+       0: 456
+       1: 789
+     }
+neighbor_ident_get(k(0, 1, BSIC_6BIT, 2)) --> entry returned
+     cell_id_list lac[1] = {
+       0: 123
+     }
+neighbor_ident_get(k(0, 1, BSIC_9BIT, 2)) --> entry returned
+     cell_id_list lac[2] = {
+       0: 456
+       1: 789
+     }
+--- wildcard matches both 6bit and 9bit BSIC regardless:
+neighbor_ident_get(k(0, 1, BSIC_6BIT, 23)) --> entry returned
+     cell_id_list cgi[1] = {
+        0: 001-02-3-4
+     }
+neighbor_ident_get(k(0, 1, BSIC_9BIT, 23)) --> entry returned
+     cell_id_list cgi[1] = {
+        0: 001-02-3-4
+     }
+
+--- Value ranges
+neighbor_ident_add(k(0, 6, BSIC_6BIT, 1 << 6), &lac1) --> expect rc=-ERANGE, got -34
+     (empty)
+neighbor_ident_add(k(0, 9, BSIC_9BIT, 1 << 9), &lac1) --> expect rc=-ERANGE, got -34
+     (empty)
+neighbor_ident_add(k(0, 6, BSIC_6BIT, -1), &lac1) --> expect rc=-ERANGE, got -34
+     (empty)
+neighbor_ident_add(k(0, 9, BSIC_9BIT, -1), &lac1) --> expect rc=-ERANGE, got -34
+     (empty)
+neighbor_ident_add(k(NEIGHBOR_IDENT_KEY_ANY_BTS - 1, 1, BSIC_NONE, 1), &cgi2) --> expect rc=-ERANGE, got -34
+     (empty)
+neighbor_ident_add(k(256, 1, BSIC_NONE, 1), &cgi2) --> expect rc=-ERANGE, got -34
+     (empty)
+neighbor_ident_add(k(0, 0, BSIC_NONE, 0), &cgi1) --> expect rc=1, got 1
+  0: BTS 0 to ARFCN 0 (any BSIC)
+     cell_id_list cgi[1] = {
+        0: 001-02-3-4
+     }
+neighbor_ident_add(k(255, 65535, BSIC_NONE, 65535), &lac1) --> expect rc=1, got 1
+  0: BTS 0 to ARFCN 0 (any BSIC)
+     cell_id_list cgi[1] = {
+        0: 001-02-3-4
+     }
+  1: BTS 255 to ARFCN 65535 (any BSIC)
+     cell_id_list lac[1] = {
+       0: 123
+     }
+neighbor_ident_add(k(0, 0, BSIC_6BIT, 0), &cgi2) --> expect rc=2, got 2
+  0: BTS 0 to ARFCN 0 (any BSIC)
+     cell_id_list cgi[1] = {
+        0: 001-02-3-4
+     }
+  1: BTS 255 to ARFCN 65535 (any BSIC)
+     cell_id_list lac[1] = {
+       0: 123
+     }
+  2: BTS 0 to ARFCN 0 BSIC 0
+     cell_id_list cgi[2] = {
+        0: 001-02-3-4
+        1: 005-006-7-8
+     }
+neighbor_ident_add(k(255, 65535, BSIC_6BIT, 0x3f), &lac2) --> expect rc=2, got 2
+  0: BTS 0 to ARFCN 0 (any BSIC)
+     cell_id_list cgi[1] = {
+        0: 001-02-3-4
+     }
+  1: BTS 255 to ARFCN 65535 (any BSIC)
+     cell_id_list lac[1] = {
+       0: 123
+     }
+  2: BTS 0 to ARFCN 0 BSIC 0
+     cell_id_list cgi[2] = {
+        0: 001-02-3-4
+        1: 005-006-7-8
+     }
+  3: BTS 255 to ARFCN 65535 BSIC 63
+     cell_id_list lac[2] = {
+       0: 456
+       1: 789
+     }
+neighbor_ident_add(k(0, 0, BSIC_9BIT, 0), &cgi1) --> expect rc=1, got 1
+  0: BTS 0 to ARFCN 0 (any BSIC)
+     cell_id_list cgi[1] = {
+        0: 001-02-3-4
+     }
+  1: BTS 255 to ARFCN 65535 (any BSIC)
+     cell_id_list lac[1] = {
+       0: 123
+     }
+  2: BTS 0 to ARFCN 0 BSIC 0
+     cell_id_list cgi[2] = {
+        0: 001-02-3-4
+        1: 005-006-7-8
+     }
+  3: BTS 255 to ARFCN 65535 BSIC 63
+     cell_id_list lac[2] = {
+       0: 456
+       1: 789
+     }
+  4: BTS 0 to ARFCN 0 BSIC 0(9bit)
+     cell_id_list cgi[1] = {
+        0: 001-02-3-4
+     }
+neighbor_ident_add(k(255, 65535, BSIC_9BIT, 0x1ff), &cgi2) --> expect rc=2, got 2
+  0: BTS 0 to ARFCN 0 (any BSIC)
+     cell_id_list cgi[1] = {
+        0: 001-02-3-4
+     }
+  1: BTS 255 to ARFCN 65535 (any BSIC)
+     cell_id_list lac[1] = {
+       0: 123
+     }
+  2: BTS 0 to ARFCN 0 BSIC 0
+     cell_id_list cgi[2] = {
+        0: 001-02-3-4
+        1: 005-006-7-8
+     }
+  3: BTS 255 to ARFCN 65535 BSIC 63
+     cell_id_list lac[2] = {
+       0: 456
+       1: 789
+     }
+  4: BTS 0 to ARFCN 0 BSIC 0(9bit)
+     cell_id_list cgi[1] = {
+        0: 001-02-3-4
+     }
+  5: BTS 255 to ARFCN 65535 BSIC 511(9bit)
+     cell_id_list cgi[2] = {
+        0: 001-02-3-4
+        1: 005-006-7-8
+     }
+
+--- size limits
+Added first cell identifier list (added 127) --> rc = 127
+Added second cell identifier list (tried to add 1) --> rc = -28
diff --git a/tests/neighbor_ident.vty b/tests/neighbor_ident.vty
new file mode 100644
index 0000000..505eb72
--- /dev/null
+++ b/tests/neighbor_ident.vty
@@ -0,0 +1,251 @@
+OsmoBSC> ### Neighbor-BSS Cell Identifier List config
+
+OsmoBSC> enable
+OsmoBSC# configure terminal
+OsmoBSC(config)# network
+
+OsmoBSC(config-net)# bts 0
+OsmoBSC(config-net-bts)# type sysmobts
+OsmoBSC(config-net-bts)# base_station_id_code 10
+OsmoBSC(config-net-bts)# location_area_code 20
+OsmoBSC(config-net-bts)# cell_identity 30
+OsmoBSC(config-net-bts)# trx 0
+OsmoBSC(config-net-bts-trx)# arfcn 40
+OsmoBSC(config-net-bts-trx)# exit
+OsmoBSC(config-net-bts)# exit
+
+OsmoBSC(config-net)# bts 1
+OsmoBSC(config-net-bts)# type sysmobts
+OsmoBSC(config-net-bts)# base_station_id_code 11
+OsmoBSC(config-net-bts)# location_area_code 21
+OsmoBSC(config-net-bts)# cell_identity 31
+OsmoBSC(config-net-bts)# trx 0
+OsmoBSC(config-net-bts-trx)# arfcn 41
+OsmoBSC(config-net-bts-trx)# exit
+OsmoBSC(config-net-bts)# exit
+
+OsmoBSC(config-net)# bts 2
+OsmoBSC(config-net-bts)# type sysmobts
+OsmoBSC(config-net-bts)# base_station_id_code 12
+OsmoBSC(config-net-bts)# location_area_code 22
+OsmoBSC(config-net-bts)# cell_identity 32
+OsmoBSC(config-net-bts)# trx 0
+OsmoBSC(config-net-bts-trx)# arfcn 42
+OsmoBSC(config-net-bts-trx)# exit
+OsmoBSC(config-net-bts)# exit
+
+OsmoBSC(config-net)# show running-config
+...
+ bts 0
+...
+  cell_identity 30
+  location_area_code 20
+  base_station_id_code 10
+...
+  trx 0
+...
+   arfcn 40
+...
+ bts 1
+...
+  cell_identity 31
+  location_area_code 21
+  base_station_id_code 11
+...
+  trx 0
+...
+   arfcn 41
+...
+ bts 2
+...
+  cell_identity 32
+  location_area_code 22
+  base_station_id_code 12
+...
+  trx 0
+...
+   arfcn 42
+...
+
+OsmoBSC(config-net)# bts 0
+OsmoBSC(config-net-bts)# list
+...
+  neighbor add bts <0-255>
+  neighbor add lac <0-65535>
+  neighbor add lac-ci <0-65535> <0-255>
+  neighbor add lac <0-65535> arfcn <0-1023> (bsic|bsic9) (<0-511>|any)
+  neighbor add lac-ci <0-65535> <0-255> arfcn <0-1023> (bsic|bsic9) (<0-511>|any)
+  neighbor add cgi <0-999> <0-999> <0-65535> <0-255> arfcn <0-1023> (bsic|bsic9) (<0-511>|any)
+  neighbor del bts <0-255>
+  neighbor del arfcn <0-1023> (bsic|bsic9) (<0-511>|any)
+  neighbor resolve arfcn <0-1023> (bsic|bsic9) (<0-511>|any)
+...
+
+OsmoBSC(config-net-bts)# show running-config
+... !neighbor add
+
+OsmoBSC(config-net-bts)# neighbor add cgi 23 42 423 5 arfcn 23 bsic 64
+% Error: BSIC value surpasses 6-bit range: 64, use 'bsic9' instead
+
+OsmoBSC(config-net-bts)# neighbor add bts 0
+% Error: cannot add local BTS 0 as neighbor to BTS 0: Invalid argument
+
+OsmoBSC(config-net-bts)# show running-config
+... !neighbor add
+
+OsmoBSC(config-net-bts)# neighbor add bts 1
+% BTS 0 now has local neighbor BTS 1 with LAC 21 CI 31 and ARFCN 41 BSIC 11
+
+OsmoBSC(config-net-bts)# neighbor add lac 22
+% BTS 0 now has local neighbor BTS 2 with LAC 22 CI 32 and ARFCN 42 BSIC 12
+
+OsmoBSC(config-net-bts)# neighbor add cgi 23 42 423 5 arfcn 23 bsic 42
+% BTS 0 to ARFCN 23 BSIC 42 now has 1 remote BSS Cell Identifier List entry
+
+OsmoBSC(config-net-bts)# ### adding the same entry again results in no change
+OsmoBSC(config-net-bts)# neighbor add bts 1
+% BTS 0 already had local neighbor BTS 1 with LAC 21 CI 31 and ARFCN 41 BSIC 11
+OsmoBSC(config-net-bts)# neighbor add lac-ci 21 31
+% BTS 0 already had local neighbor BTS 1 with LAC 21 CI 31 and ARFCN 41 BSIC 11
+OsmoBSC(config-net-bts)# neighbor add cgi 23 42 423 5 arfcn 23 bsic 42
+% BTS 0 to ARFCN 23 BSIC 42 now has 1 remote BSS Cell Identifier List entry
+OsmoBSC(config-net-bts)# neighbor add cgi 23 42 423 5 arfcn 23 bsic 42
+% BTS 0 to ARFCN 23 BSIC 42 now has 1 remote BSS Cell Identifier List entry
+OsmoBSC(config-net-bts)# neighbor add cgi 23 42 423 5 arfcn 23 bsic 42
+% BTS 0 to ARFCN 23 BSIC 42 now has 1 remote BSS Cell Identifier List entry
+
+OsmoBSC(config-net-bts)# neighbor add cgi 23 042 423 6 arfcn 23 bsic 42
+% BTS 0 to ARFCN 23 BSIC 42 now has 2 remote BSS Cell Identifier List entries
+
+OsmoBSC(config-net-bts)# neighbor add lac 456 arfcn 123 bsic 45
+% BTS 0 to ARFCN 123 BSIC 45 now has 1 remote BSS Cell Identifier List entry
+
+OsmoBSC(config-net-bts)# neighbor add cgi 23 042 234 56 arfcn 23 bsic 42
+% BTS 0 to ARFCN 23 BSIC 42 now has 3 remote BSS Cell Identifier List entries
+
+OsmoBSC(config-net-bts)# neighbor add lac-ci 789 10 arfcn 423 bsic any
+% BTS 0 to ARFCN 423 (any BSIC) now has 1 remote BSS Cell Identifier List entry
+
+OsmoBSC(config-net-bts)# neighbor add lac-ci 789 10 arfcn 423 bsic9 511
+% BTS 0 to ARFCN 423 BSIC 511(9bit) now has 1 remote BSS Cell Identifier List entry
+
+OsmoBSC(config-net-bts)# neighbor add lac-ci 789 10 arfcn 423 bsic9 1
+% BTS 0 to ARFCN 423 BSIC 1(9bit) now has 1 remote BSS Cell Identifier List entry
+
+OsmoBSC(config-net-bts)# neighbor add lac-ci 789 10 arfcn 423 bsic 1
+% BTS 0 to ARFCN 423 BSIC 1 now has 1 remote BSS Cell Identifier List entry
+
+OsmoBSC(config-net-bts)# show running-config
+...
+network
+... !neighbor add
+ bts 0
+... !neighbor add
+  neighbor add lac-ci 21 31
+  neighbor add lac-ci 22 32
+  neighbor add cgi 023 42 423 5 arfcn 23 bsic 42
+  neighbor add cgi 023 042 423 6 arfcn 23 bsic 42
+  neighbor add cgi 023 042 234 56 arfcn 23 bsic 42
+  neighbor add lac 456 arfcn 123 bsic 45
+  neighbor add lac-ci 789 10 arfcn 423 bsic any
+  neighbor add lac-ci 789 10 arfcn 423 bsic9 511
+  neighbor add lac-ci 789 10 arfcn 423 bsic9 1
+  neighbor add lac-ci 789 10 arfcn 423 bsic 1
+... !neighbor add
+
+OsmoBSC(config-net-bts)# neighbor resolve arfcn 99 bsic any
+% No entry for BTS 0 to ARFCN 99 (any BSIC)
+
+OsmoBSC(config-net-bts)# neighbor resolve arfcn 41 bsic any
+% BTS 0 to ARFCN 41 (any BSIC) resolves to local BTS 1 lac-ci 21 31
+
+OsmoBSC(config-net-bts)# neighbor resolve arfcn 423 bsic 1
+% neighbor add lac-ci 789 10 arfcn 423 bsic 1
+
+OsmoBSC(config-net-bts)# neighbor resolve arfcn 423 bsic 23
+% neighbor add lac-ci 789 10 arfcn 423 bsic 23
+
+OsmoBSC(config-net-bts)# neighbor del arfcn 99 bsic 7
+% Cannot remove, no such neighbor: BTS 0 to ARFCN 99 BSIC 7
+
+OsmoBSC(config-net-bts)# neighbor del arfcn 23 bsic 42
+% Removed remote BSS neighbor BTS 0 to ARFCN 23 BSIC 42
+
+OsmoBSC(config-net-bts)# show running-config
+... !neighbor add
+  neighbor add lac-ci 21 31
+  neighbor add lac-ci 22 32
+  neighbor add lac 456 arfcn 123 bsic 45
+  neighbor add lac-ci 789 10 arfcn 423 bsic any
+  neighbor add lac-ci 789 10 arfcn 423 bsic9 511
+  neighbor add lac-ci 789 10 arfcn 423 bsic9 1
+  neighbor add lac-ci 789 10 arfcn 423 bsic 1
+... !neighbor add
+
+OsmoBSC(config-net-bts)# neighbor del arfcn 123 bsic 45
+% Removed remote BSS neighbor BTS 0 to ARFCN 123 BSIC 45
+
+OsmoBSC(config-net-bts)# show running-config
+... !neighbor add
+  neighbor add lac-ci 21 31
+  neighbor add lac-ci 22 32
+  neighbor add lac-ci 789 10 arfcn 423 bsic any
+  neighbor add lac-ci 789 10 arfcn 423 bsic9 511
+  neighbor add lac-ci 789 10 arfcn 423 bsic9 1
+  neighbor add lac-ci 789 10 arfcn 423 bsic 1
+... !neighbor add
+
+OsmoBSC(config-net-bts)# neighbor del arfcn 423 bsic any
+% Removed remote BSS neighbor BTS 0 to ARFCN 423 (any BSIC)
+
+OsmoBSC(config-net-bts)# show running-config
+... !neighbor add
+  neighbor add lac-ci 21 31
+  neighbor add lac-ci 22 32
+  neighbor add lac-ci 789 10 arfcn 423 bsic9 511
+  neighbor add lac-ci 789 10 arfcn 423 bsic9 1
+  neighbor add lac-ci 789 10 arfcn 423 bsic 1
+... !neighbor add
+
+OsmoBSC(config-net-bts)# neighbor del arfcn 423 bsic9 511
+% Removed remote BSS neighbor BTS 0 to ARFCN 423 BSIC 511(9bit)
+OsmoBSC(config-net-bts)# neighbor del arfcn 423 bsic9 1
+% Removed remote BSS neighbor BTS 0 to ARFCN 423 BSIC 1(9bit)
+
+OsmoBSC(config-net-bts)# show running-config
+... !neighbor add
+  neighbor add lac-ci 21 31
+  neighbor add lac-ci 22 32
+  neighbor add lac-ci 789 10 arfcn 423 bsic 1
+... !neighbor add
+
+OsmoBSC(config-net-bts)# neighbor del arfcn 423 bsic 1
+% Removed remote BSS neighbor BTS 0 to ARFCN 423 BSIC 1
+
+OsmoBSC(config-net-bts)# show running-config
+... !neighbor add
+  neighbor add lac-ci 21 31
+  neighbor add lac-ci 22 32
+... !neighbor add
+
+OsmoBSC(config-net-bts)# neighbor del arfcn 41 bsic any
+% Removed local neighbor bts 0 to bts 1
+
+OsmoBSC(config-net-bts)# show running-config
+... !neighbor add
+  neighbor add lac-ci 22 32
+... !neighbor add
+
+OsmoBSC(config-net-bts)# neighbor del arfcn 41 bsic any
+% Cannot remove, no such neighbor: BTS 0 to ARFCN 41 (any BSIC)
+
+OsmoBSC(config-net-bts)# show running-config
+... !neighbor add
+  neighbor add lac-ci 22 32
+... !neighbor add
+
+OsmoBSC(config-net-bts)# neighbor del arfcn 42 bsic 12
+% Removed local neighbor bts 0 to bts 2
+
+OsmoBSC(config-net-bts)# show running-config
+... !neighbor add
diff --git a/tests/testsuite.at b/tests/testsuite.at
index 515ffa0..aba4a0c 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -32,6 +32,13 @@
 AT_CHECK([$abs_top_builddir/tests/nanobts_omlattr/nanobts_omlattr_test], [], [expout], [ignore])
 AT_CLEANUP
 
+AT_SETUP([neighbor_ident])
+AT_KEYWORDS([neighbor_ident])
+cat $abs_srcdir/handover/neighbor_ident_test.ok > expout
+cat $abs_srcdir/handover/neighbor_ident_test.err > experr
+AT_CHECK([$abs_top_builddir/tests/handover/neighbor_ident_test], [], [expout], [experr])
+AT_CLEANUP
+
 AT_SETUP([handover test 0])
 AT_KEYWORDS([handover])
 cat $abs_srcdir/handover/handover_test.ok > expout

-- 
To view, visit https://gerrit.osmocom.org/9666
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings

Gerrit-Project: osmo-bsc
Gerrit-Branch: master
Gerrit-MessageType: newchange
Gerrit-Change-Id: I0153d7069817fba9146ddc11214de2757d7d37bf
Gerrit-Change-Number: 9666
Gerrit-PatchSet: 1
Gerrit-Owner: Neels Hofmeyr <nhofmeyr at sysmocom.de>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.osmocom.org/pipermail/gerrit-log/attachments/20180618/9d749004/attachment.htm>


More information about the gerrit-log mailing list