/* Copyright (c) 2015, 2016 Nicira, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <config.h>
#include "encaps.h"

#include "lib/hash.h"
#include "lib/sset.h"
#include "lib/util.h"
#include "lib/vswitch-idl.h"
#include "openvswitch/vlog.h"
#include "lib/ovn-sb-idl.h"
#include "ovn-controller.h"
#include "smap.h"

VLOG_DEFINE_THIS_MODULE(encaps);

/*
 * Given there could be multiple tunnels with different IPs to the same
 * chassis we annotate the ovn-chassis-id with
 * <chassis_name>OVN_MVTEP_CHASSISID_DELIM<IP>.
 */
#define	OVN_MVTEP_CHASSISID_DELIM '@'

void
encaps_register_ovs_idl(struct ovsdb_idl *ovs_idl)
{
    ovsdb_idl_add_table(ovs_idl, &ovsrec_table_bridge);
    ovsdb_idl_add_column(ovs_idl, &ovsrec_bridge_col_ports);
    ovsdb_idl_add_table(ovs_idl, &ovsrec_table_port);
    ovsdb_idl_track_add_column(ovs_idl, &ovsrec_port_col_name);
    ovsdb_idl_track_add_column(ovs_idl, &ovsrec_port_col_interfaces);
    ovsdb_idl_track_add_column(ovs_idl, &ovsrec_port_col_external_ids);
    ovsdb_idl_add_table(ovs_idl, &ovsrec_table_interface);
    ovsdb_idl_track_add_column(ovs_idl, &ovsrec_interface_col_name);
    ovsdb_idl_track_add_column(ovs_idl, &ovsrec_interface_col_type);
    ovsdb_idl_track_add_column(ovs_idl, &ovsrec_interface_col_options);
}

/* Enough context to create a new tunnel, using tunnel_add(). */
struct tunnel_ctx {
    /* Maps from a chassis name to "struct chassis_node *". */
    struct shash chassis;

    /* Names of all ports in the bridge, to allow checking uniqueness when
     * adding a new tunnel. */
    struct sset port_names;

    struct ovsdb_idl_txn *ovs_txn;
    const struct ovsrec_bridge *br_int;
    const struct sbrec_chassis *this_chassis;
};

struct chassis_node {
    const struct ovsrec_port *port;
    const struct ovsrec_bridge *bridge;
};

static char *
tunnel_create_name(struct tunnel_ctx *tc, const char *chassis_id)
{
    int i;

    for (i = 0; i < UINT16_MAX; i++) {
        char *port_name;
        port_name = xasprintf("ovn-%.6s-%x", chassis_id, i);

        if (!sset_contains(&tc->port_names, port_name)) {
            return port_name;
        }

        free(port_name);
    }

    return NULL;
}

/*
 * Returns a tunnel-id of the form 'chassis_id'-delimiter-'encap_ip'.
 */
char *
encaps_tunnel_id_create(const char *chassis_id, const char *encap_ip)
{
    return xasprintf("%s%c%s", chassis_id, OVN_MVTEP_CHASSISID_DELIM,
                     encap_ip);
}

/*
 * Parses a 'tunnel_id' of the form <chassis_name><delimiter><IP>.
 * If the 'chassis_id' argument is not NULL the function will allocate memory
 * and store the chassis-id part of the tunnel-id at '*chassis_id'.
 * If the 'encap_ip' argument is not NULL the function will allocate memory
 * and store the encapsulation IP part of the tunnel-id at '*encap_ip'.
 */
bool
encaps_tunnel_id_parse(const char *tunnel_id, char **chassis_id,
                       char **encap_ip)
{
    /* Find the delimiter.  Fail if there is no delimiter or if <chassis_name>
     * or <IP> is the empty string.*/
    const char *d = strchr(tunnel_id, OVN_MVTEP_CHASSISID_DELIM);
    if (d == tunnel_id || !d || !d[1]) {
        return false;
    }

    if (chassis_id) {
        *chassis_id = xmemdup0(tunnel_id, d - tunnel_id);
    }
    if (encap_ip) {
        *encap_ip = xstrdup(d + 1);
    }
    return true;
}

/*
 * Returns true if 'tunnel_id' contains 'chassis_id' and, if specified, the
 * given 'encap_ip'. Returns false otherwise.
 */
bool
encaps_tunnel_id_match(const char *tunnel_id, const char *chassis_id,
                       const char *encap_ip)
{
    while (*tunnel_id == *chassis_id) {
        if (!*tunnel_id) {
            /* 'tunnel_id' and 'chassis_id' are equal strings.  This is a
             * mismatch because 'tunnel_id' is missing the delimiter and IP. */
            return false;
        }
        tunnel_id++;
        chassis_id++;
    }

    /* We found the first byte that disagrees between 'tunnel_id' and
     * 'chassis_id'.  If we consumed all of 'chassis_id' and arrived at the
     * delimiter in 'tunnel_id' (and if 'encap_ip' is correct, if it was
     * supplied), it's a match. */
    return (*tunnel_id == OVN_MVTEP_CHASSISID_DELIM
            && *chassis_id == '\0'
            && (!encap_ip || !strcmp(tunnel_id + 1, encap_ip)));
}

static void
tunnel_add(struct tunnel_ctx *tc, const struct sbrec_sb_global *sbg,
           const char *new_chassis_id, const struct sbrec_encap *encap,
           const struct ovsrec_open_vswitch_table *ovs_table)
{
    struct smap options = SMAP_INITIALIZER(&options);
    smap_add(&options, "remote_ip", encap->ip);
    smap_add(&options, "key", "flow");
    const char *dst_port = smap_get(&encap->options, "dst_port");
    const char *csum = smap_get(&encap->options, "csum");
    char *tunnel_entry_id = NULL;

    /*
     * Since a chassis may have multiple encap-ip, we can't just add the
     * chassis name as as the "ovn-chassis-id" for the port; we use the
     * combination of the chassis_name and the encap-ip to identify
     * a specific tunnel to the chassis.
     */
    tunnel_entry_id = encaps_tunnel_id_create(new_chassis_id, encap->ip);
    if (csum && (!strcmp(csum, "true") || !strcmp(csum, "false"))) {
        smap_add(&options, "csum", csum);
    }
    if (dst_port) {
        smap_add(&options, "dst_port", dst_port);
    }

    const struct ovsrec_open_vswitch *cfg =
        ovsrec_open_vswitch_table_first(ovs_table);

    bool set_local_ip = false;
    if (cfg) {
        /* If the tos option is configured, get it */
        const char *encap_tos = smap_get_def(&cfg->external_ids,
           "ovn-encap-tos", "none");

        if (encap_tos && strcmp(encap_tos, "none")) {
            smap_add(&options, "tos", encap_tos);
        }

        /* If ovn-set-local-ip option is configured, get it */
        set_local_ip = smap_get_bool(&cfg->external_ids, "ovn-set-local-ip",
                                     false);
    }

    /* Add auth info if ipsec is enabled. */
    if (sbg->ipsec) {
        set_local_ip = true;
        smap_add(&options, "remote_name", new_chassis_id);
    }

    if (set_local_ip) {
        const struct sbrec_chassis *this_chassis = tc->this_chassis;
        const char *local_ip = NULL;

        /* Determine 'ovn-encap-ip' of the local chassis as this will be the
         * tunnel port's 'local_ip'. We do not support the case in which
         * 'ovn-encap-ip' holds multiple comma-delimited IP addresses.
         */
        for (int i = 0; i < this_chassis->n_encaps; i++) {
            if (local_ip && strcmp(local_ip, this_chassis->encaps[i]->ip)) {
                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
                VLOG_ERR_RL(&rl, "ovn-encap-ip has been configured as a list. "
                            "This is unsupported for IPsec and explicit "
                            "local_ip configuration.");
                /* No need to loop further as we know this condition has been
                 * hit */
                break;
            } else {
                local_ip = this_chassis->encaps[i]->ip;
            }
        }

        if (local_ip) {
            smap_add(&options, "local_ip", local_ip);
        }
    }

    /* If there's an existing chassis record that does not need any change,
     * keep it.  Otherwise, create a new record (if there was an existing
     * record, the new record will supplant it and encaps_run() will delete
     * it). */
    struct chassis_node *chassis = shash_find_data(&tc->chassis,
                                                   tunnel_entry_id);
    if (chassis
        && chassis->port->n_interfaces == 1
        && !strcmp(chassis->port->interfaces[0]->type, encap->type)
        && smap_equal(&chassis->port->interfaces[0]->options, &options)) {
        shash_find_and_delete(&tc->chassis, tunnel_entry_id);
        free(chassis);
        goto exit;
    }

    /* Choose a name for the new port.  If we're replacing an old port, reuse
     * its name, otherwise generate a new, unique name. */
    char *port_name = (chassis
                       ? xstrdup(chassis->port->name)
                       : tunnel_create_name(tc, new_chassis_id));
    if (!port_name) {
        VLOG_WARN("Unable to allocate unique name for '%s' tunnel",
                  new_chassis_id);
        goto exit;
    }

    struct ovsrec_interface *iface = ovsrec_interface_insert(tc->ovs_txn);
    ovsrec_interface_set_name(iface, port_name);
    ovsrec_interface_set_type(iface, encap->type);
    ovsrec_interface_set_options(iface, &options);

    struct ovsrec_port *port = ovsrec_port_insert(tc->ovs_txn);
    ovsrec_port_set_name(port, port_name);
    ovsrec_port_set_interfaces(port, &iface, 1);
    const struct smap id = SMAP_CONST1(&id, "ovn-chassis-id", tunnel_entry_id);
    ovsrec_port_set_external_ids(port, &id);

    ovsrec_bridge_update_ports_addvalue(tc->br_int, port);

    sset_add_and_free(&tc->port_names, port_name);

exit:
    free(tunnel_entry_id);
    smap_destroy(&options);
}

static bool
chassis_has_type(const struct sbrec_chassis *chassis, uint32_t tun_type) {
    for (size_t i = 0; i < chassis->n_encaps; i++) {
        if (get_tunnel_type(chassis->encaps[i]->type) == tun_type) {
            return true;
        }
    }
    return false;
}

static struct sbrec_encap *
preferred_encap(const struct sbrec_chassis *chassis_rec,
                const struct sbrec_chassis *this_chassis)
{
    struct sbrec_encap *best_encap = NULL;
    uint32_t best_type = 0;

    for (size_t i = 0; i < chassis_rec->n_encaps; i++) {
        uint32_t tun_type = get_tunnel_type(chassis_rec->encaps[i]->type);
        if (tun_type > best_type && chassis_has_type(this_chassis, tun_type)) {
            best_type = tun_type;
            best_encap = chassis_rec->encaps[i];
        }
    }

    return best_encap;
}

/*
 * For each peer chassis, get a preferred tunnel type and create as many tunnels
 * as there are VTEP of that type (differentiated by remote_ip) on that chassis.
 */
static int
chassis_tunnel_add(const struct sbrec_chassis *chassis_rec,
                   const struct sbrec_sb_global *sbg,
                   const struct ovsrec_open_vswitch_table *ovs_table,
                   struct tunnel_ctx *tc,
                   const struct sbrec_chassis *this_chassis)
{
    struct sbrec_encap *encap = preferred_encap(chassis_rec, this_chassis);
    int tuncnt = 0;

    if (!encap) {
        VLOG_INFO("chassis_tunnel_add: No supported encaps for '%s'", chassis_rec->name);
        return tuncnt;
    }

    uint32_t pref_type = get_tunnel_type(encap->type);
    for (int i = 0; i < chassis_rec->n_encaps; i++) {
        uint32_t tun_type = get_tunnel_type(chassis_rec->encaps[i]->type);
        if (tun_type != pref_type) {
            continue;
        }
        tunnel_add(tc, sbg, chassis_rec->name, chassis_rec->encaps[i],
                   ovs_table);
        tuncnt++;
    }
    return tuncnt;
}

/*
* Returns true if transport_zones and chassis_rec->transport_zones
* have at least one common transport zone.
*/
static bool
chassis_tzones_overlap(const struct sset *transport_zones,
                       const struct sbrec_chassis *chassis_rec)
{
    /* If neither Chassis belongs to any transport zones, return true to
     * form a tunnel between them */
    if (!chassis_rec->n_transport_zones && sset_is_empty(transport_zones)) {
        return true;
    }

    for (int i = 0; i < chassis_rec->n_transport_zones; i++) {
        if (sset_contains(transport_zones, chassis_rec->transport_zones[i])) {
            return true;
        }
    }
    return false;
}

void
encaps_run(struct ovsdb_idl_txn *ovs_idl_txn,
           const struct ovsrec_bridge_table *bridge_table,
           const struct ovsrec_bridge *br_int,
           const struct sbrec_chassis_table *chassis_table,
           const struct sbrec_chassis *this_chassis,
           const struct sbrec_sb_global *sbg,
           const struct ovsrec_open_vswitch_table *ovs_table,
           const struct sset *transport_zones)
{
    if (!ovs_idl_txn || !br_int) {
        return;
    }

    const struct sbrec_chassis *chassis_rec;
    const struct ovsrec_bridge *br;

    struct tunnel_ctx tc = {
        .chassis = SHASH_INITIALIZER(&tc.chassis),
        .port_names = SSET_INITIALIZER(&tc.port_names),
        .br_int = br_int,
        .this_chassis = this_chassis
    };

    tc.ovs_txn = ovs_idl_txn;
    ovsdb_idl_txn_add_comment(tc.ovs_txn,
                              "ovn-controller: modifying OVS tunnels '%s'",
                              this_chassis->name);

    /* Collect all port names into tc.port_names.
     *
     * Collect all the OVN-created tunnels into tc.tunnel_hmap. */
    OVSREC_BRIDGE_TABLE_FOR_EACH (br, bridge_table) {
        for (size_t i = 0; i < br->n_ports; i++) {
            const struct ovsrec_port *port = br->ports[i];
            sset_add(&tc.port_names, port->name);

            /*
             * note that the id here is not just the chassis name, but the
             * combination of <chassis_name><delim><encap_ip>
             */
            const char *id = smap_get(&port->external_ids, "ovn-chassis-id");
            if (id) {
                if (!shash_find(&tc.chassis, id)) {
                    struct chassis_node *chassis = xzalloc(sizeof *chassis);
                    chassis->bridge = br;
                    chassis->port = port;
                    shash_add_assert(&tc.chassis, id, chassis);
                } else {
                    /* Duplicate port for ovn-chassis-id.  Arbitrarily choose
                     * to delete this one. */
                    ovsrec_bridge_update_ports_delvalue(br, port);
                }
            }
        }
    }

    SBREC_CHASSIS_TABLE_FOR_EACH (chassis_rec, chassis_table) {
        if (strcmp(chassis_rec->name, this_chassis->name)) {
            /* Create tunnels to the other Chassis belonging to the
             * same transport zone */
            if (!chassis_tzones_overlap(transport_zones, chassis_rec)) {
                VLOG_DBG("Skipping encap creation for Chassis '%s' because "
                         "it belongs to different transport zones",
                         chassis_rec->name);
                continue;
            }

            if (smap_get_bool(&chassis_rec->other_config, "is-remote", false)
                && !smap_get_bool(&this_chassis->other_config, "is-interconn",
                                  false)) {
                VLOG_DBG("Skipping encap creation for Chassis '%s' because "
                         "it is remote but this chassis is not interconn.",
                         chassis_rec->name);
                continue;
            }

            if (chassis_tunnel_add(chassis_rec, sbg, ovs_table, &tc,
                                   this_chassis) == 0) {
                VLOG_INFO("Creating encap for '%s' failed", chassis_rec->name);
                continue;
            }
        }
    }

    /* Delete any existing OVN tunnels that were not still around. */
    struct shash_node *node;
    SHASH_FOR_EACH_SAFE (node, &tc.chassis) {
        struct chassis_node *chassis = node->data;
        ovsrec_bridge_update_ports_delvalue(chassis->bridge, chassis->port);
        shash_delete(&tc.chassis, node);
        free(chassis);
    }
    shash_destroy(&tc.chassis);
    sset_destroy(&tc.port_names);
}

/* Returns true if the database is all cleaned up, false if more work is
 * required. */
bool
encaps_cleanup(struct ovsdb_idl_txn *ovs_idl_txn,
               const struct ovsrec_bridge *br_int)
{
    if (!br_int) {
        return true;
    }

    /* Delete all the OVS-created tunnels from the integration bridge. */
    struct ovsrec_port **ports
        = xmalloc(sizeof *br_int->ports * br_int->n_ports);
    size_t n = 0;
    for (size_t i = 0; i < br_int->n_ports; i++) {
        if (!smap_get(&br_int->ports[i]->external_ids, "ovn-chassis-id")) {
            ports[n++] = br_int->ports[i];
        }
    }

    bool any_changes = n != br_int->n_ports;
    if (any_changes && ovs_idl_txn) {
        ovsdb_idl_txn_add_comment(ovs_idl_txn,
                                  "ovn-controller: destroying tunnels");
        ovsrec_bridge_verify_ports(br_int);
        ovsrec_bridge_set_ports(br_int, ports, n);
    }
    free(ports);

    return !any_changes;
}
