Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions doc/content/design/lldp.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,12 @@ Default after fresh install: `nearestbridge`.

Type: `map(string, string)`

Stores the received LLDP TLVs from the corresponding PIF.
Stores the effective LLDP information for the physical NIC of the corresponding PIF:

Default: empty
- `state`: the effective LLDP state of the NIC, one of `enabled`, `disabled` or `blocked` (`blocked` means the NIC driver is in the blocking list). Always present for a managed physical NIC.
- `system_name`, `port_id`, `port_description`: the TLVs received from the neighbour, present only when a neighbour is seen.

Default: empty (also empty for PIFs that are not managed physical NICs).

## XenAPI changes

Expand Down Expand Up @@ -200,19 +203,26 @@ Some advertised values follow the default behavior of `lldpd`, while others are
networkd periodically queries statistics for individual NICs and writes them to the in-memory file `/dev/shm/network_stats`. The file format is defined in `ocaml/xapi-idl/network/network_stats.ml` and is extended with a new field, `lldp_neighbor`.

```ocaml
type lldp_rx = {
type lldp_state = Enabled | Disabled | Blocked

type lldp_neighbor = {
system_name: string option;
port_id: string option;
port_description: string option;
}

type lldp_rx = {
state: lldp_state;
neighbor: lldp_neighbor option;
}
[@@deriving rpcty]

type iface_stats = {
...
lldp_neighbor: lldp_rx option;
lldp_rx: lldp_rx option;
}
```
networkd queries `lldpd` for the LLDP TLVs received on individual NICs and writes them into `/dev/shm/network_stats`.
networkd derives the `state` from `lldpcli show interfaces` (a NIC reporting `RX and TX` is `Enabled`; otherwise it is `Blocked` when its driver is in the blocking list, else `Disabled`) and queries `lldpd` for the LLDP TLVs received on individual NICs, writing both into `/dev/shm/network_stats`.
Monitor_dbcalls.monitor_dbcall_thread in XAPI reads the in-memory file `/dev/shm/network_stats` periodically, and exposes the data through `PIF_metrics.lldp_neighbor` by storing them in XenAPI map form.

## Scenarios
Expand Down
9 changes: 9 additions & 0 deletions ocaml/idl/datamodel.ml
Original file line number Diff line number Diff line change
Expand Up @@ -3014,6 +3014,15 @@ module PIF_metrics = struct
~default_value:(Some (VMap []))
~ty:(Map (String, String))
"other_config" "additional configuration"
; field ~qualifier:DynamicRO ~lifecycle:[]
~default_value:(Some (VMap []))
~ty:(Map (String, String))
"lldp_neighbor"
"The LLDP information for the physical NIC of the corresponding \
PIF: the effective LLDP state (key 'state', one of enabled, \
disabled or blocked) and, when a neighbour is seen, the received \
TLVs (keys 'system_name', 'port_id' and 'port_description'). \
Empty for PIFs that are not managed physical NICs."
]
()
end
Expand Down
2 changes: 1 addition & 1 deletion ocaml/idl/datamodel_common.ml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ open Datamodel_roles
to leave a gap for potential hotfixes needing to increment the schema version.*)
let schema_major_vsn = 5

let schema_minor_vsn = 909
let schema_minor_vsn = 910

(* Historical schema versions just in case this is useful later *)
let rio_schema_major_vsn = 5
Expand Down
2 changes: 1 addition & 1 deletion ocaml/idl/schematest.ml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ let hash x = Digest.string x |> Digest.to_hex
(* BEWARE: if this changes, check that schema has been bumped accordingly in
ocaml/idl/datamodel_common.ml, usually schema_minor_vsn *)

let last_known_schema_hash = "4f4145585a0be563e77b01220bf3f6af"
let last_known_schema_hash = "db58f982a09d1aded5413d5632c89560"

let current_schema_hash : string =
let open Datamodel_types in
Expand Down
44 changes: 44 additions & 0 deletions ocaml/networkd/bin/network_monitor_thread.ml
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,52 @@ let get_link_stats dbg () =
in
Cache.free cache ; Socket.close s ; Socket.free s ; links

(* Cache of the latest LLDP neighbour seen per interface. lldpd is queried on a
slower cadence than the rest of the stats (LLDPDUs arrive ~every 30s), to
avoid unnecessary lldpcli calls. *)
let lldp_neighbors : (string, Network_monitor.lldp_neighbor) Hashtbl.t =
Hashtbl.create 16

let lldp_last_query = ref neg_infinity

let lldp_query_interval = 30.0

(* Interfaces on which lldpd currently has LLDP enabled (rx-and-tx), refreshed on
the same cadence as the neighbour query. *)
let lldp_enabled_interfaces = ref []

let refresh_lldp_neighbors () =
let now = Unix.gettimeofday () in
if now -. !lldp_last_query >= lldp_query_interval then (
lldp_last_query := now ;
lldp_enabled_interfaces := Lldp.get_enabled_interfaces () ;
Hashtbl.reset lldp_neighbors ;
List.iter
(fun (dev, rx) ->
if Hashtbl.mem lldp_neighbors dev then
debug "Multiple LLDP neighbours on %s; keeping the first" dev
else
Hashtbl.replace lldp_neighbors dev rx
)
(Lldp.get_neighbors ())
)

(* The effective LLDP state of physical [dev], read from lldpd's reported status
(rx-and-tx) and, for non-enabled NICs, the driver blocklist. *)
let lldp_state_of dev =
Lldp.state_of dev ~enabled:(List.mem dev !lldp_enabled_interfaces)

(* The LLDP information reported for physical [dev]: its effective state is
always populated; neighbour fields come from the last query, if any. *)
let lldp_rx_of dev =
Network_monitor.
{state= lldp_state_of dev; neighbor= Hashtbl.find_opt lldp_neighbors dev}

let rec monitor dbg () =
let open Network_interface in
let open Network_monitor in
( try
refresh_lldp_neighbors () ;
let get_stats bonds devs =
List.map
(fun dev ->
Expand Down Expand Up @@ -176,6 +218,7 @@ let rec monitor dbg () =
; nb_links
; links_up
; interfaces
; lldp_rx= Some (lldp_rx_of dev)
}
else
let carrier = List.exists (fun info -> info.up) bond_slaves in
Expand Down Expand Up @@ -219,6 +262,7 @@ let rec monitor dbg () =
; nb_links
; links_up
; interfaces
; lldp_rx= None
}
in
check_for_changes ~dev ~stat ;
Expand Down
109 changes: 109 additions & 0 deletions ocaml/networkd/lib/lldp.ml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,67 @@ module Lldp_types = struct
| Multicast_address of I.lldp_multicast_address list
end

module Lldp_parse = struct
let ( let* ) = Option.bind

(* Follow [keys] through a json0 doc. Object keys are consumed one at a time;
arrays are transparently entered at their head without consuming a key
(json0 wraps every repeatable node in an array). An exhausted path returns
the current node as-is. *)
let rec json0_get json0 keys =
match keys with
| [] ->
Some json0
| k :: ks as keys -> (
match json0 with
| `Assoc l ->
let* json' = List.assoc_opt k l in
json0_get json' ks
| `List (json' :: _) ->
json0_get json' keys
| _ ->
None
)

let json0_get_str json0 keys =
match json0_get json0 keys with Some (`String s) -> Some s | _ -> None

let interfaces (output : string) : Yojson.Safe.t list =
match Yojson.Safe.from_string output with
| exception e ->
debug "%s: could not parse lldpcli JSON: %s" __FUNCTION__
(Printexc.to_string e) ;
[]
| json0 -> (
match json0_get json0 ["lldp"; "interface"] with
| Some (`List l) ->
l
| _ ->
[]
)

let parse_neighbors (output : string) :
(string * Network_stats.lldp_neighbor) list =
interfaces output
|> List.filter_map (fun iface ->
let* dev = json0_get_str iface ["name"] in
let system_name = json0_get_str iface ["chassis"; "name"; "value"] in
let port_id = json0_get_str iface ["port"; "id"; "value"] in
let port_description = json0_get_str iface ["port"; "descr"; "value"] in
Some (dev, Network_stats.{system_name; port_id; port_description})
)

let parse_enabled_interfaces (output : string) : string list =
interfaces output
|> List.filter_map (fun iface ->
match json0_get_str iface ["status"; "value"] with
| Some "RX and TX" ->
json0_get_str iface ["name"]
| _ ->
None
)
end

module type AGENT = sig
type error = Lldp_types.error

Expand All @@ -70,6 +131,12 @@ module type AGENT = sig

val disable : string -> (unit, error) result
(** Stop LLDP (rx-and-tx) on [dev]. *)

val get_neighbors : unit -> (string * Network_stats.lldp_neighbor) list
(** Query the agent for the LLDP neighbour received on each interface. *)

val get_enabled_interfaces : unit -> string list
(** The interfaces on which the agent currently has LLDP enabled (rx-and-tx). *)
end

let management_ip_address =
Expand Down Expand Up @@ -104,6 +171,10 @@ module Lldpd : AGENT = struct

let cli = "/usr/sbin/lldpcli"

let show_neighbors_args = ["-f"; "json0"; "show"; "neighbors"]

let show_interfaces_args = ["-f"; "json0"; "show"; "interfaces"]

let systemctl = "/usr/bin/systemctl"

let service = "lldpd"
Expand Down Expand Up @@ -171,6 +242,28 @@ module Lldpd : AGENT = struct
let disable dev =
call_cli ["configure"; "ports"; dev; "lldp"; "status"; "disabled"]

let get_neighbors () =
match
try Ok (Network_utils.call_script cli show_neighbors_args)
with e -> Error (Printexc.to_string e)
with
| Ok output ->
Lldp_parse.parse_neighbors output
| Error msg ->
debug "%s: could not query LLDP neighbours: %s" __FUNCTION__ msg ;
[]

let get_enabled_interfaces () =
match
try Ok (Network_utils.call_script cli show_interfaces_args)
with e -> Error (Printexc.to_string e)
with
| Ok output ->
Lldp_parse.parse_enabled_interfaces output
| Error msg ->
debug "%s: could not query LLDP interfaces: %s" __FUNCTION__ msg ;
[]

let string_of_multicast_address = function
| I.Nearest_bridge ->
"nearest-bridge"
Expand Down Expand Up @@ -430,3 +523,19 @@ let stop = Lldp_agent.stop

let set_tlv_management_address () =
management_ip_address ~force:true () |> Lldp_agent.set_tlv_management_address

let get_neighbors = Lldpd.get_neighbors

let get_enabled_interfaces = Lldpd.get_enabled_interfaces

let parse_neighbors = Lldp_parse.parse_neighbors

let parse_enabled_interfaces = Lldp_parse.parse_enabled_interfaces

let state_of dev ~enabled : Network_stats.lldp_state =
if enabled then
Network_stats.Enabled
else if Blocklist.mem dev then
Network_stats.Blocked
else
Network_stats.Disabled
22 changes: 22 additions & 0 deletions ocaml/networkd/lib/lldp.mli
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,25 @@ val stop : unit -> unit
val set_tlv_management_address : unit -> unit
(** [set_tlv_management_address ()] retrieves the management IP address(es) of
the host and configure them in the LLDP management address TLV for advertising. *)

val get_neighbors : unit -> (string * Network_stats.lldp_neighbor) list
(** [get_neighbors ()] queries the LLDP agent and returns, per interface, the
received neighbour information (system name, port id, port description). *)

val get_enabled_interfaces : unit -> string list
(** [get_enabled_interfaces ()] queries the LLDP agent and returns the
interfaces on which LLDP is enabled (rx-and-tx). *)

val parse_neighbors : string -> (string * Network_stats.lldp_neighbor) list
(** [parse_neighbors output] parses the JSON produced by
[lldpcli -f json0 show neighbors]. Exposed for testing. *)

val parse_enabled_interfaces : string -> string list
(** [parse_enabled_interfaces output] parses the JSON produced by
[lldpcli -f json0 show interfaces], returning the rx-and-tx interfaces.
Exposed for testing. *)

val state_of : string -> enabled:bool -> Network_stats.lldp_state
(** [state_of dev ~enabled] is the effective LLDP state of physical NIC [dev]:
[Enabled] when lldpd reports it as rx-and-tx, otherwise [Blocked] when its
driver is in the blocklist, else [Disabled]. *)
1 change: 1 addition & 0 deletions ocaml/networkd/test/network_test.ml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ let () =
@ Test_jsonrpc_client.tests
@ Test_network_device_order_inherited.tests
@ Test_network_device_order.tests
@ Test_lldp.tests
)
Loading
Loading