# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright 2011 OpenStack Foundation
# All Rights Reserved.
#
#    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.

import functools

import eventlet
import netaddr

from nova import exception
from nova.openstack.common import jsonutils


def ensure_string_keys(d):
    # http://bugs.python.org/issue4978
    return dict([(str(k), v) for k, v in d.iteritems()])

# Constants for the 'vif_type' field in VIF class
VIF_TYPE_OVS = 'ovs'
VIF_TYPE_IVS = 'ivs'
VIF_TYPE_BRIDGE = 'bridge'
VIF_TYPE_802_QBG = '802.1qbg'
VIF_TYPE_802_QBH = '802.1qbh'
VIF_TYPE_OTHER = 'other'

# Constant for max length of network interface names
# eg 'bridge' in the Network class or 'devname' in
# the VIF class
NIC_NAME_LEN = 14


class Model(dict):
    """Defines some necessary structures for most of the network models."""
    def __repr__(self):
        return self.__class__.__name__ + '(' + dict.__repr__(self) + ')'

    def _set_meta(self, kwargs):
        # pull meta out of kwargs if it's there
        self['meta'] = kwargs.pop('meta', {})
        # update meta with any additional kwargs that may exist
        self['meta'].update(kwargs)

    def get_meta(self, key, default=None):
        """calls get(key, default) on self['meta']."""
        return self['meta'].get(key, default)


class IP(Model):
    """Represents an IP address in Nova."""
    def __init__(self, address=None, type=None, **kwargs):
        super(IP, self).__init__()

        self['address'] = address
        self['type'] = type
        self['version'] = kwargs.pop('version', None)

        self._set_meta(kwargs)

        # determine version from address if not passed in
        if self['address'] and not self['version']:
            try:
                self['version'] = netaddr.IPAddress(self['address']).version
            except netaddr.AddrFormatError:
                raise exception.InvalidIpAddressError(self['address'])

    def __eq__(self, other):
        return self['address'] == other['address']

    def is_in_subnet(self, subnet):
        if self['address'] and subnet['cidr']:
            return (netaddr.IPAddress(self['address']) in
                    netaddr.IPNetwork(subnet['cidr']))
        else:
            return False

    @classmethod
    def hydrate(cls, ip):
        if ip:
            return IP(**ensure_string_keys(ip))
        return None


class FixedIP(IP):
    """Represents a Fixed IP address in Nova."""
    def __init__(self, floating_ips=None, **kwargs):
        super(FixedIP, self).__init__(**kwargs)
        self['floating_ips'] = floating_ips or []

        if not self['type']:
            self['type'] = 'fixed'

    def add_floating_ip(self, floating_ip):
        if floating_ip not in self['floating_ips']:
            self['floating_ips'].append(floating_ip)

    def floating_ip_addresses(self):
        return [ip['address'] for ip in self['floating_ips']]

    @classmethod
    def hydrate(cls, fixed_ip):
        fixed_ip = FixedIP(**ensure_string_keys(fixed_ip))
        fixed_ip['floating_ips'] = [IP.hydrate(floating_ip)
                                   for floating_ip in fixed_ip['floating_ips']]
        return fixed_ip


class Route(Model):
    """Represents an IP Route in Nova."""
    def __init__(self, cidr=None, gateway=None, interface=None, **kwargs):
        super(Route, self).__init__()

        self['cidr'] = cidr
        self['gateway'] = gateway
        self['interface'] = interface

        self._set_meta(kwargs)

    @classmethod
    def hydrate(cls, route):
        route = Route(**ensure_string_keys(route))
        route['gateway'] = IP.hydrate(route['gateway'])
        return route


class Subnet(Model):
    """Represents a Subnet in Nova."""
    def __init__(self, cidr=None, dns=None, gateway=None, ips=None,
                 routes=None, **kwargs):
        super(Subnet, self).__init__()

        self['cidr'] = cidr
        self['dns'] = dns or []
        self['gateway'] = gateway
        self['ips'] = ips or []
        self['routes'] = routes or []
        self['version'] = kwargs.pop('version', None)

        self._set_meta(kwargs)

        if self['cidr'] and not self['version']:
            self['version'] = netaddr.IPNetwork(self['cidr']).version

    def __eq__(self, other):
        return self['cidr'] == other['cidr']

    def add_route(self, new_route):
        if new_route not in self['routes']:
            self['routes'].append(new_route)

    def add_dns(self, dns):
        if dns not in self['dns']:
            self['dns'].append(dns)

    def add_ip(self, ip):
        if ip not in self['ips']:
            self['ips'].append(ip)

    def as_netaddr(self):
        """Convience function to get cidr as a netaddr object."""
        return netaddr.IPNetwork(self['cidr'])

    @classmethod
    def hydrate(cls, subnet):
        subnet = Subnet(**ensure_string_keys(subnet))
        subnet['dns'] = [IP.hydrate(dns) for dns in subnet['dns']]
        subnet['ips'] = [FixedIP.hydrate(ip) for ip in subnet['ips']]
        subnet['routes'] = [Route.hydrate(route) for route in subnet['routes']]
        subnet['gateway'] = IP.hydrate(subnet['gateway'])
        return subnet


class Network(Model):
    """Represents a Network in Nova."""
    def __init__(self, id=None, bridge=None, label=None,
                 subnets=None, **kwargs):
        super(Network, self).__init__()

        self['id'] = id
        self['bridge'] = bridge
        self['label'] = label
        self['subnets'] = subnets or []

        self._set_meta(kwargs)

    def add_subnet(self, subnet):
        if subnet not in self['subnets']:
            self['subnets'].append(subnet)

    @classmethod
    def hydrate(cls, network):
        if network:
            network = Network(**ensure_string_keys(network))
            network['subnets'] = [Subnet.hydrate(subnet)
                                  for subnet in network['subnets']]
        return network


class VIF8021QbgParams(Model):
    """Represents the parameters for a 802.1qbg VIF."""

    def __init__(self, managerid, typeid, typeidversion, instanceid):
        self['managerid'] = managerid
        self['typeid'] = typeid
        self['typeidversion'] = typeidversion
        self['instanceid'] = instanceid


class VIF8021QbhParams(Model):
    """Represents the parameters for a 802.1qbh VIF."""

    def __init__(self, profileid):
        self['profileid'] = profileid


class VIF(Model):
    """Represents a Virtual Interface in Nova."""
    def __init__(self, id=None, address=None, network=None, type=None,
                 devname=None, ovs_interfaceid=None,
                 qbh_params=None, qbg_params=None,
                 **kwargs):
        super(VIF, self).__init__()

        self['id'] = id
        self['address'] = address
        self['network'] = network or None
        self['type'] = type
        self['devname'] = devname

        self['ovs_interfaceid'] = ovs_interfaceid
        self['qbh_params'] = qbh_params
        self['qbg_params'] = qbg_params

        self._set_meta(kwargs)

    def __eq__(self, other):
        return self['id'] == other['id']

    def fixed_ips(self):
        return [fixed_ip for subnet in self['network']['subnets']
                         for fixed_ip in subnet['ips']]

    def floating_ips(self):
        return [floating_ip for fixed_ip in self.fixed_ips()
                            for floating_ip in fixed_ip['floating_ips']]

    def labeled_ips(self):
        """Returns the list of all IPs

        The return value looks like this flat structure::

            {'network_label': 'my_network',
             'network_id': 'n8v29837fn234782f08fjxk3ofhb84',
             'ips': [{'address': '123.123.123.123',
                      'version': 4,
                      'type: 'fixed',
                      'meta': {...}},
                     {'address': '124.124.124.124',
                      'version': 4,
                      'type': 'floating',
                      'meta': {...}},
                     {'address': 'fe80::4',
                      'version': 6,
                      'type': 'fixed',
                      'meta': {...}}]
        """
        if self['network']:
            # remove unnecessary fields on fixed_ips
            ips = [IP(**ensure_string_keys(ip)) for ip in self.fixed_ips()]
            for ip in ips:
                # remove floating ips from IP, since this is a flat structure
                # of all IPs
                del ip['meta']['floating_ips']
            # add floating ips to list (if any)
            ips.extend(self.floating_ips())
            return {'network_label': self['network']['label'],
                    'network_id': self['network']['id'],
                    'ips': ips}
        return []

    @classmethod
    def hydrate(cls, vif):
        vif = VIF(**ensure_string_keys(vif))
        vif['network'] = Network.hydrate(vif['network'])
        return vif


def get_netmask(ip, subnet):
    """Returns the netmask appropriate for injection into a guest."""
    if ip['version'] == 4:
        return str(subnet.as_netaddr().netmask)
    return subnet.as_netaddr()._prefixlen


class NetworkInfo(list):
    """Stores and manipulates network information for a Nova instance."""

    # NetworkInfo is a list of VIFs

    def fixed_ips(self):
        """Returns all fixed_ips without floating_ips attached."""
        return [ip for vif in self for ip in vif.fixed_ips()]

    def floating_ips(self):
        """Returns all floating_ips."""
        return [ip for vif in self for ip in vif.floating_ips()]

    @classmethod
    def hydrate(cls, network_info):
        if isinstance(network_info, basestring):
            network_info = jsonutils.loads(network_info)
        return NetworkInfo([VIF.hydrate(vif) for vif in network_info])

    def json(self):
        return jsonutils.dumps(self)

    def legacy(self):
        """
        Return the legacy network_info representation of self
        """
        def get_ip(ip):
            if not ip:
                return None
            return ip['address']

        def fixed_ip_dict(ip, subnet):
            netmask = get_netmask(ip, subnet)

            return {'ip': ip['address'],
                    'enabled': '1',
                    'netmask': netmask,
                    'gateway': get_ip(subnet['gateway'])}

        def convert_routes(routes):
            routes_list = []
            for route in routes:
                r = {'route': str(netaddr.IPNetwork(route['cidr']).network),
                     'netmask': str(netaddr.IPNetwork(route['cidr']).netmask),
                     'gateway': get_ip(route['gateway'])}
                routes_list.append(r)
            return routes_list

        network_info = []
        for vif in self:
            # if vif doesn't have network or that network has no subnets, quit
            if not vif['network'] or not vif['network']['subnets']:
                continue
            network = vif['network']

            # NOTE(jkoelker) The legacy format only supports one subnet per
            #                network, so we only use the 1st one of each type
            # NOTE(tr3buchet): o.O
            v4_subnets = []
            v6_subnets = []
            for subnet in vif['network']['subnets']:
                if subnet['version'] == 4:
                    v4_subnets.append(subnet)
                else:
                    v6_subnets.append(subnet)

            subnet_v4 = None
            subnet_v6 = None

            if v4_subnets:
                subnet_v4 = v4_subnets[0]

            if v6_subnets:
                subnet_v6 = v6_subnets[0]

            if not subnet_v4:
                msg = _('v4 subnets are required for legacy nw_info')
                raise exception.NovaException(message=msg)

            routes = convert_routes(subnet_v4['routes'])
            should_create_bridge = network.get_meta('should_create_bridge',
                                                    False)
            should_create_vlan = network.get_meta('should_create_vlan', False)
            gateway = get_ip(subnet_v4['gateway'])
            dhcp_server = subnet_v4.get_meta('dhcp_server', gateway)

            network_dict = {
                'bridge': network['bridge'],
                'id': network['id'],
                'cidr': subnet_v4['cidr'],
                'cidr_v6': subnet_v6['cidr'] if subnet_v6 else None,
                'vlan': network.get_meta('vlan'),
                'injected': network.get_meta('injected', False),
                'multi_host': network.get_meta('multi_host', False),
                'bridge_interface': network.get_meta('bridge_interface')
            }
            # NOTE(tr3buchet): 'ips' bit here is tricky, we support a single
            #                  subnet but we want all the IPs to be there
            #                  so use the v4_subnets[0] and its IPs are first
            #                  so that eth0 will be from subnet_v4, the rest of
            #                  the IPs will be aliased eth0:1 etc and the
            #                  gateways from their subnets will not be used
            info_dict = {'label': network['label'],
                         'broadcast': str(subnet_v4.as_netaddr().broadcast),
                         'mac': vif['address'],
                         'vif_type': vif['type'],
                         'vif_devname': vif.get('devname'),
                         'vif_uuid': vif['id'],
                         'ovs_interfaceid': vif.get('ovs_interfaceid'),
                         'qbh_params': vif.get('qbh_params'),
                         'qbg_params': vif.get('qbg_params'),
                         'rxtx_cap': vif.get_meta('rxtx_cap', 0),
                         'dns': [get_ip(ip) for ip in subnet_v4['dns']],
                         'ips': [fixed_ip_dict(ip, subnet)
                                 for subnet in v4_subnets
                                 for ip in subnet['ips']],
                         'should_create_bridge': should_create_bridge,
                         'should_create_vlan': should_create_vlan,
                         'dhcp_server': dhcp_server}
            if routes:
                info_dict['routes'] = routes

            if gateway:
                info_dict['gateway'] = gateway

            if v6_subnets:
                if subnet_v6['gateway']:
                    info_dict['gateway_v6'] = get_ip(subnet_v6['gateway'])
                # NOTE(tr3buchet): only supporting single v6 subnet here
                info_dict['ip6s'] = [fixed_ip_dict(ip, subnet_v6)
                                     for ip in subnet_v6['ips']]

            network_info.append((network_dict, info_dict))
        return network_info


class NetworkInfoAsyncWrapper(NetworkInfo):
    """Wrapper around NetworkInfo that allows retrieving NetworkInfo
    in an async manner.

    This allows one to start querying for network information before
    you know you will need it.  If you have a long-running
    operation, this allows the network model retrieval to occur in the
    background.  When you need the data, it will ensure the async
    operation has completed.

    As an example:

    def allocate_net_info(arg1, arg2)
        return call_neutron_to_allocate(arg1, arg2)

    network_info = NetworkInfoAsyncWrapper(allocate_net_info, arg1, arg2)
    [do a long running operation -- real network_info will be retrieved
    in the background]
    [do something with network_info]
    """

    def __init__(self, async_method, *args, **kwargs):
        self._gt = eventlet.spawn(async_method, *args, **kwargs)
        methods = ['json', 'legacy', 'fixed_ips', 'floating_ips']
        for method in methods:
            fn = getattr(self, method)
            wrapper = functools.partial(self._sync_wrapper, fn)
            functools.update_wrapper(wrapper, fn)
            setattr(self, method, wrapper)

    def _sync_wrapper(self, wrapped, *args, **kwargs):
        """Synchronize the model before running a method."""
        self.wait()
        return wrapped(*args, **kwargs)

    def __getitem__(self, *args, **kwargs):
        fn = super(NetworkInfoAsyncWrapper, self).__getitem__
        return self._sync_wrapper(fn, *args, **kwargs)

    def __iter__(self, *args, **kwargs):
        fn = super(NetworkInfoAsyncWrapper, self).__iter__
        return self._sync_wrapper(fn, *args, **kwargs)

    def __len__(self, *args, **kwargs):
        fn = super(NetworkInfoAsyncWrapper, self).__len__
        return self._sync_wrapper(fn, *args, **kwargs)

    def __str__(self, *args, **kwargs):
        fn = super(NetworkInfoAsyncWrapper, self).__str__
        return self._sync_wrapper(fn, *args, **kwargs)

    def __repr__(self, *args, **kwargs):
        fn = super(NetworkInfoAsyncWrapper, self).__repr__
        return self._sync_wrapper(fn, *args, **kwargs)

    def wait(self, do_raise=True):
        """Wait for async call to finish."""
        if self._gt is not None:
            try:
                # NOTE(comstud): This looks funky, but this object is
                # subclassed from list.  In other words, 'self' is really
                # just a list with a bunch of extra methods.  So this
                # line just replaces the current list (which should be
                # empty) with the result.
                self[:] = self._gt.wait()
            except Exception:
                if do_raise:
                    raise
            finally:
                self._gt = None
