bgpsim 0.17.6

A network control-plane simulator
Documentation
// BgpSim: BGP Network Simulator written in Rust
// Copyright 2022-2024 Tibor Schneider <sctibor@ethz.ch>
//
// 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.

//! This module contains functions to save the network to a file, and restore it from a file.

use std::collections::{BTreeSet, HashMap};

use itertools::Itertools;
use serde::{Deserialize, Serialize};
use serde_json::json;

use crate::{
    config::{ConfigExpr, ConfigModifier, NetworkConfig},
    event::EventQueue,
    network::Network,
    ospf::OspfImpl,
    types::{AsId, NetworkDeviceRef, NetworkError, Prefix, PrefixMap, RouterId},
};

const JSON_FIELD_NAME_NETWORK: &str = "net";
const JSON_FIELD_NAME_CONFIG: &str = "config_nodes_routes";

type ExportRoutes<P> = (RouterId, P, Vec<AsId>, Option<u32>, BTreeSet<u32>);

impl<P, Q, Ospf> Network<P, Q, Ospf>
where
    P: Prefix,
    Q: EventQueue<P> + Serialize,
    Ospf: OspfImpl,
{
    /// Create a json string from the network. This string will contain both the actual network
    /// state and the configuration. In case the network state can no longer be deserialized, the
    /// configuration can be used to restore the network to a similar state.
    pub fn as_json_str(&self) -> String {
        serde_json::to_string(&json!({
            JSON_FIELD_NAME_NETWORK: serde_json::to_value(self).unwrap(),
            JSON_FIELD_NAME_CONFIG: self.as_config_json_str(),
        }))
        .unwrap()
    }

    /// Create a json string from the network. This string will only contain the configuration, not
    /// the serialized network itself. Thus, this string is significantly smaller than the string
    /// generated by `Network::as_json_str`.
    pub fn as_json_str_compact(&self) -> String {
        serde_json::to_string(&json!({
            JSON_FIELD_NAME_CONFIG: self.as_config_json_str()
        }))
        .unwrap()
    }

    /// Create a json value containing the configuration.
    fn as_config_json_str(&self) -> serde_json::Value {
        let config = Vec::from_iter(self.get_config().unwrap().iter().cloned());
        let mut nodes: Vec<(RouterId, String, Option<AsId>)> = self
            .devices()
            .map(|r| match r {
                NetworkDeviceRef::InternalRouter(r) => (r.router_id(), r.name().to_string(), None),
                NetworkDeviceRef::ExternalRouter(r) => {
                    (r.router_id(), r.name().to_string(), Some(r.as_id()))
                }
            })
            .collect();
        nodes.sort_by_key(|(r, _, _)| *r);

        let links: Vec<(RouterId, RouterId)> = self
            .ospf
            .edges()
            .map(|e| (e.src(), e.dst()))
            .map(|(a, b)| if a < b { (a, b) } else { (b, a) })
            .unique()
            .collect();

        let routes: Vec<ExportRoutes<P>> = self
            .external_routers()
            .flat_map(|r| {
                let id = r.router_id();
                r.get_advertised_routes().values().map(move |route| {
                    (
                        id,
                        route.prefix,
                        route.as_path.clone(),
                        route.med,
                        route.community.clone(),
                    )
                })
            })
            .collect();
        serde_json::to_value(&(config, nodes, links, routes)).unwrap()
    }
}

impl<P, Q, Ospf> Network<P, Q, Ospf>
where
    P: Prefix,
    Q: EventQueue<P>,
    Ospf: OspfImpl,
    for<'a> Q: Deserialize<'a>,
{
    /// Read a json file containing the network and create the network. If the network cannot be
    /// deserialized directly, reconstruct it from the configuration that should also be part of the
    /// exported file.
    ///
    /// The `default_queue` function must return a queue in case the network cannot be deserialized
    /// directly, but it needs to be built up from the configuration. For instance, use
    /// `Default::default` for a queue `Q` like `BasicEventQueue` that implements `Default`.
    pub fn from_json_str<F>(s: &str, default_queue: F) -> Result<Self, NetworkError>
    where
        F: FnOnce() -> Q,
    {
        // first, try to deserialize the network. If that works, ignore the config
        let content: serde_json::Value = serde_json::from_str(s)?;
        if let Some(net) = content
            .get(JSON_FIELD_NAME_NETWORK)
            .and_then(|v| serde_json::from_value(v.clone()).ok())
        {
            Ok(net)
        } else {
            match content
                .get(JSON_FIELD_NAME_CONFIG)
                .and_then(|v| v.as_array())
            {
                Some(v) if v.len() == 4 => Self::from_config_nodes_routes(
                    v[0].clone(),
                    v[1].clone(),
                    v[2].clone(),
                    v[3].clone(),
                    default_queue,
                ),
                _ => Err(serde_json::from_str::<ConfigNodeRoutes>(s).unwrap_err())?,
            }
        }
    }
}

impl<P, Q, Ospf> Network<P, Q, Ospf>
where
    P: Prefix,
    Q: EventQueue<P>,
    Ospf: OspfImpl,
{
    /// Deserialize the json structure containing configuration, nodes and routes.
    fn from_config_nodes_routes<F>(
        config: serde_json::Value,
        nodes: serde_json::Value,
        links: serde_json::Value,
        routes: serde_json::Value,
        default_queue: F,
    ) -> Result<Self, NetworkError>
    where
        F: FnOnce() -> Q,
    {
        let config: Vec<ConfigExpr<P>> = serde_json::from_value(config)?;
        let nodes: Vec<(RouterId, String, Option<AsId>)> = serde_json::from_value(nodes)?;
        let links: Vec<(RouterId, RouterId)> = serde_json::from_value(links)?;
        let routes: Vec<ExportRoutes<P>> = serde_json::from_value(routes)?;
        let mut nodes_lut: HashMap<RouterId, RouterId> = HashMap::new();
        let mut net = Network::new(default_queue());
        // add all nodes and create the lut
        for (id, name, as_id) in nodes.into_iter() {
            let new_id = if let Some(as_id) = as_id {
                net.add_external_router(name, as_id)
            } else {
                net.add_router(name)
            };
            nodes_lut.insert(id, new_id);
        }
        // create the function to lookup nodes
        let node = |id: RouterId| {
            nodes_lut
                .get(&id)
                .copied()
                .ok_or(NetworkError::DeviceNotFound(id))
        };
        let links = links
            .into_iter()
            .map(|(a, b)| Ok::<_, NetworkError>((node(a)?, node(b)?)))
            .collect::<Result<Vec<_>, _>>()?;
        net.add_links_from(links)?;
        // apply all configurations

        for expr in config.iter() {
            let expr = match expr.clone() {
                ConfigExpr::IgpLinkWeight {
                    source,
                    target,
                    weight,
                } => ConfigExpr::IgpLinkWeight {
                    source: node(source)?,
                    target: node(target)?,
                    weight,
                },
                ConfigExpr::OspfArea {
                    source,
                    target,
                    area,
                } => ConfigExpr::OspfArea {
                    source: node(source)?,
                    target: node(target)?,
                    area,
                },
                ConfigExpr::BgpSession {
                    source,
                    target,
                    session_type,
                } => ConfigExpr::BgpSession {
                    source: node(source)?,
                    target: node(target)?,
                    session_type,
                },
                ConfigExpr::BgpRouteMap {
                    router,
                    neighbor,
                    direction,
                    map,
                } => ConfigExpr::BgpRouteMap {
                    router: node(router)?,
                    neighbor: node(neighbor)?,
                    direction,
                    map,
                },
                ConfigExpr::StaticRoute {
                    router,
                    prefix,
                    target,
                } => ConfigExpr::StaticRoute {
                    router: node(router)?,
                    prefix,
                    target,
                },
                ConfigExpr::LoadBalancing { router } => ConfigExpr::LoadBalancing {
                    router: node(router)?,
                },
            };
            net.apply_modifier(&ConfigModifier::Insert(expr))?;
        }
        for (src, prefix, as_path, med, community) in routes.into_iter() {
            net.advertise_external_route(src, prefix, as_path, med, community)?;
        }
        Ok(net)
    }
}

/// Dummy struct that allows us to create meaningful error messages
#[derive(Debug, Deserialize)]
struct ConfigNodeRoutes {
    #[allow(dead_code)]
    config_nodes_routes: (serde_json::Value, serde_json::Value, serde_json::Value),
}