/*
 * Copyright (c) 2011, 2013, 2014 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 "vlandev.h"

#include <errno.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/stat.h>

#include "dummy.h"
#include "hash.h"
#include "shash.h"
#include "socket-util.h"
#include "openvswitch/vlog.h"

VLOG_DEFINE_THIS_MODULE(vlandev);

/* A vlandev implementation. */
struct vlandev_class {
    int (*vd_refresh)(void);
    int (*vd_add)(const char *real_dev, int vid);
    int (*vd_del)(const char *vlan_dev);
};

#ifdef __linux__
static const struct vlandev_class vlandev_linux_class;
#endif
static const struct vlandev_class vlandev_stub_class;
static const struct vlandev_class vlandev_dummy_class;

/* The in-use vlandev implementation. */
static const struct vlandev_class *vd_class;

/* Maps from a VLAN device name (e.g. "eth0.10") to struct vlan_dev. */
static struct shash vlan_devs = SHASH_INITIALIZER(&vlan_devs);

/* Maps from a VLAN real device name (e.g. "eth0") to struct vlan_real_dev. */
static struct shash vlan_real_devs = SHASH_INITIALIZER(&vlan_real_devs);

static int vlandev_add__(const char *vlan_dev, const char *real_dev, int vid);
static int vlandev_del__(const char *vlan_dev);
static void vlandev_clear__(void);

static const struct vlandev_class *
vlandev_get_class(void)
{
    if (!vd_class) {
#if __linux__
        vd_class = &vlandev_linux_class;
#else
        vd_class = &vlandev_stub_class;
#endif
    }
    return vd_class;
}

/* On Linux, the default implementation of VLAN devices creates and destroys
 * Linux VLAN devices.  On other OSess, the default implementation is a
 * nonfunctional stub.  In either case, this function replaces this default
 * implementation by a "dummy" implementation that simply reports back whatever
 * the client sets up with vlandev_add() and vlandev_del().
 *
 * Don't call this function directly; use dummy_enable() from dummy.h. */
void
vlandev_dummy_enable(void)
{
    if (vd_class != &vlandev_dummy_class) {
        vd_class = &vlandev_dummy_class;
        vlandev_clear__();
    }
}

/* Creates a new VLAN device for VLAN 'vid' on top of real Ethernet device
 * 'real_dev'.  Returns 0 if successful, otherwise a positive errno value.  On
 * OSes other than Linux, in the absence of dummies (see
 * vlandev_dummy_enable()), this always fails.
 *
 * The name of the new VLAN device is not easily predictable, because Linux
 * provides multiple naming schemes, does not allow the client to specify a
 * name, and does not directly report the new VLAN device's name.  Use
 * vlandev_refresh() then vlandev_get_name() to find out the new VLAN device's
 * name,. */
int
vlandev_add(const char *real_dev, int vid)
{
    return vlandev_get_class()->vd_add(real_dev, vid);
}

/* Deletes the VLAN device named 'vlan_dev'.  Returns 0 if successful,
 * otherwise a positive errno value.  On OSes other than Linux, in the absence
 * of dummies (see vlandev_dummy_enable()), this always fails. */
int
vlandev_del(const char *vlan_dev)
{
    return vlandev_get_class()->vd_del(vlan_dev);
}

/* Refreshes the cache of real device to VLAN device mappings reported by
 * vlandev_get_real_devs() and vlandev_get_name().  Without calling this
 * function, changes made by vlandev_add() and vlandev_del() may not be
 * reflected by vlandev_get_real_devs() and vlandev_get_name() output. */
int
vlandev_refresh(void)
{
    const struct vlandev_class *class = vlandev_get_class();
    return class->vd_refresh ? class->vd_refresh() : 0;
}

/* Returns a shash mapping from the name of real Ethernet devices used as the
 * basis of VLAN devices to struct vlan_real_devs.  The caller must not modify
 * or free anything in the returned shash.
 *
 * Changes made by vlandev_add() and vlandev_del() may not be reflected in this
 * function's output without an intervening call to vlandev_refresh(). */
struct shash *
vlandev_get_real_devs(void)
{
    return &vlan_real_devs;
}

/* Returns the name of the VLAN device for VLAN 'vid' on top of
 * 'real_dev_name', or NULL if there is no such VLAN device.
 *
 * Changes made by vlandev_add() and vlandev_del() may not be reflected in this
 * function's output without an intervening call to vlandev_refresh(). */
const char *
vlandev_get_name(const char *real_dev_name, int vid)
{
    const struct vlan_real_dev *real_dev;

    real_dev = shash_find_data(&vlan_real_devs, real_dev_name);
    if (real_dev) {
        const struct vlan_dev *vlan_dev;

        HMAP_FOR_EACH_WITH_HASH (vlan_dev, hmap_node, hash_int(vid, 0),
                                 &real_dev->vlan_devs) {
            if (vlan_dev->vid == vid) {
                return vlan_dev->name;
            }
        }
    }

    return NULL;
}

/* The Linux vlandev implementation. */

#ifdef __linux__
#include "rtnetlink.h"
#include <linux/if_vlan.h>
#include <linux/sockios.h>
#include "netdev-linux.h"

static struct nln_notifier *vlan_cache_notifier;
static bool cache_valid;

static void
vlan_cache_cb(const struct rtnetlink_change *change OVS_UNUSED,
              void *aux OVS_UNUSED)
{
    cache_valid = false;
}

static int
vlandev_linux_refresh(void)
{
    const char *fn = "/proc/net/vlan/config";
    char line[128];
    FILE *stream;

    if (!vlan_cache_notifier) {
        vlan_cache_notifier = rtnetlink_notifier_create(vlan_cache_cb,
                                                        NULL);
        if (!vlan_cache_notifier) {
            return EINVAL;
        }
    }

    if (cache_valid) {
        return 0;
    }

    vlandev_clear__();

    /* Repopulate cache. */
    stream = fopen(fn, "r");
    if (!stream) {
        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
        int error = errno;
        struct stat s;

        if (error == ENOENT && !stat("/proc", &s)) {
            /* Probably the vlan module just isn't loaded, and probably that's
             * because no VLAN devices have been created.
             *
             * Not really an error. */
            return 0;
        }

        VLOG_WARN_RL(&rl, "%s: open failed (%s)", fn, ovs_strerror(error));
        return error;
    }

    while (fgets(line, sizeof line, stream)) {
        char vlan_dev[16], real_dev[16];
        int vid;

        if (ovs_scan(line, "%15[^ |] | %d | %15s", vlan_dev, &vid, real_dev)) {
            vlandev_add__(vlan_dev, real_dev, vid);
        }
    }
    fclose(stream);

    cache_valid = true;
    return 0;
}

static int
do_vlan_ioctl(const char *netdev_name, struct vlan_ioctl_args *via,
              int cmd, const char *cmd_name)
{
    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
    int error;

    via->cmd = cmd;
    ovs_strlcpy(via->device1, netdev_name, sizeof via->device1);

    error = af_inet_ioctl(SIOCSIFVLAN, via);
    if (error) {
        VLOG_WARN_RL(&rl, "%s: VLAN ioctl %s failed (%s)",
                     netdev_name, cmd_name, ovs_strerror(error));
    }
    return error;
}

static int
vlandev_linux_add(const char *real_dev, int vid)
{
    struct vlan_ioctl_args via;
    int error;

    memset(&via, 0, sizeof via);
    via.u.VID = vid;

    error = do_vlan_ioctl(real_dev, &via, ADD_VLAN_CMD, "ADD_VLAN_CMD");
    if (!error) {
        cache_valid = false;
    }
    return error;
}

static int
vlandev_linux_del(const char *vlan_dev)
{
    struct vlan_ioctl_args via;
    int error;

    memset(&via, 0, sizeof via);
    error = do_vlan_ioctl(vlan_dev, &via, DEL_VLAN_CMD, "DEL_VLAN_CMD");
    if (!error) {
        cache_valid = false;
    }
    return error;
}

static const struct vlandev_class vlandev_linux_class = {
    vlandev_linux_refresh,
    vlandev_linux_add,
    vlandev_linux_del
};
#endif

/* Stub implementation. */

static int
vlandev_stub_add(const char *real_dev OVS_UNUSED, int vid OVS_UNUSED)
{
    VLOG_ERR("not supported on non-Linux platform");
    return EOPNOTSUPP;
}

static int
vlandev_stub_del(const char *vlan_dev OVS_UNUSED)
{
    VLOG_ERR("not supported on non-Linux platform");
    return EOPNOTSUPP;
}

static const struct vlandev_class OVS_UNUSED vlandev_stub_class = {
    NULL,                       /* vd_refresh */
    vlandev_stub_add,
    vlandev_stub_del
};

/* Dummy implementation. */

static int
vlandev_dummy_add(const char *real_dev, int vid)
{
    char name[IFNAMSIZ];

    if (snprintf(name, sizeof name, "%s.%d", real_dev, vid) >= sizeof name) {
        return ENAMETOOLONG;
    }
    return vlandev_add__(name, real_dev, vid);
}

static int
vlandev_dummy_del(const char *vlan_dev)
{
    return vlandev_del__(vlan_dev);
}

static const struct vlandev_class vlandev_dummy_class = {
    NULL,                       /* vd_refresh */
    vlandev_dummy_add,
    vlandev_dummy_del
};

static int
vlandev_add__(const char *vlan_dev, const char *real_dev, int vid)
{
    uint32_t vid_hash = hash_int(vid, 0);
    struct vlan_real_dev *vrd;
    struct vlan_dev *vd;

    if (vid < 0 || vid > 4095) {
        return EINVAL;
    } else if (shash_find(&vlan_devs, vlan_dev)) {
        return EEXIST;
    }

    vrd = shash_find_data(&vlan_real_devs, real_dev);
    if (!vrd) {
        vrd = xmalloc(sizeof *vrd);
        vrd->name = xstrdup(real_dev);
        hmap_init(&vrd->vlan_devs);
        shash_add_nocopy(&vlan_real_devs, vrd->name, vrd);
    } else {
        HMAP_FOR_EACH_WITH_HASH (vd, hmap_node, vid_hash, &vrd->vlan_devs) {
            if (vd->vid == vid) {
                return EEXIST;
            }
        }
    }

    vd = xmalloc(sizeof *vd);
    hmap_insert(&vrd->vlan_devs, &vd->hmap_node, vid_hash);
    vd->name = xstrdup(vlan_dev);
    vd->vid = vid;
    vd->real_dev = vrd;
    shash_add_nocopy(&vlan_devs, vd->name, vd);

    return 0;
}

static int
vlandev_del__(const char *vlan_dev)
{
    struct shash_node *vd_node = shash_find(&vlan_devs, vlan_dev);
    if (vd_node) {
        struct vlan_dev *vd = vd_node->data;
        struct vlan_real_dev *vrd = vd->real_dev;

        hmap_remove(&vrd->vlan_devs, &vd->hmap_node);
        if (hmap_is_empty(&vrd->vlan_devs)) {
            shash_find_and_delete_assert(&vlan_real_devs, vrd->name);
            free(vrd);
        }

        shash_delete(&vlan_devs, vd_node);
        free(vd);

        return 0;
    } else {
        return ENOENT;
    }
}

/* Clear 'vlan_devs' and 'vlan_real_devs' in preparation for repopulating. */
static void
vlandev_clear__(void)
{
    /* We do not free the 'name' members of struct vlan_dev and struct
     * vlan_real_dev, because the "shash"es own them.. */
    struct shash_node *node;

    shash_clear_free_data(&vlan_devs);
    SHASH_FOR_EACH (node, &vlan_real_devs) {
        struct vlan_real_dev *vrd = node->data;

        hmap_destroy(&vrd->vlan_devs);
    }
    shash_clear_free_data(&vlan_real_devs);
}
