# ----------------------------------------------------------------------------
# Copyright (c) 2000-2014 New Mexico Consortium
#
# {{{EMULAB-LICENSE
#
# This file is part of the Emulab network testbed software.
#
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this file.  If not, see <http://www.gnu.org/licenses/>.
#
# }}}
# ----------------------------------------------------------------------------

import sys
import pprint
import MySQLdb as mysql
import MySQLdb.cursors
from diskless.log import Log

# Global database connection
db = None
db_host = 'localhost'
db_name = 'tbdb'
db_user = sys.argv[0]
db_pass = ''
log = None


def mac_fixed_for_emulab(mac):
    '''Emulab style MAC address, no colons, all lower case. It's saner
    than you think'''
    return mac.lower().replace(':', '')


# Database class
class EmulabMySQLdb:
    _con = None
    _log = None
    TBDB_IFACEROLE_CONTROL = 'ctrl'

    def __init__(self, log):
        self._log = log
        self._con = None

        # Connect to mysql server
        try:
            self.con = mysql.connect(db_host, db_user, db_pass, db_name,
                                     cursorclass=MySQLdb.cursors.DictCursor)
        except (mysql.Error) as e:
            self.log.critical('Could not connect to mysql db: %s' % str(e))

    @property
    def log(self):
        return self._log

    @property
    def con(self):
        return self._con

    # Perform a read query.
    #
    # @param query   Query string with placeholders
    # @param params  Tuple to fill the placeholders
    #
    # @return The cursor with the results
    def query(self, query, params):
        cursor = self.con.cursor()
        cursor.execute(query, params)
        return cursor

    # Return the first row of a query
    def first_row(self, query, params):
        cursor = self.query(query, params)
        if cursor:
            return cursor.fetchone()

        return None

    # Turn an attribute query that returns N rows as key/value pairs
    # into a dictionary
    def attribute_query(self, key_col, value_col, query, params):
        attrs = {}
        cursor = self.query(query, params)
        for row in cursor.fetchall():
            attrs[row[key_col]] = row[value_col]
        return attrs

    def interface_for_ip_addr(self, ip_addr):
        '''Returns interface dictionary for interface with the specified IP
        address'''
        query = 'select * from interfaces where IP = %s'
        return self.first_row(query, (ip_addr,))

    def interface_for_mac_addr(self, mac_addr):
        '''Returns interface dictionary for interface with the specified MAC
        address'''
        mac = mac_fixed_for_emulab(mac_addr)
        query = 'select * from interfaces where mac = %s'
        return self.first_row(query, (mac,))

    # Return node dictionary for the specified node id
    def node_for_id(self, node_id):
        query = 'select * from nodes where node_id = %s'
        return self.first_row(query, (node_id,))

    # Return a dictionary of node attributes for the specified node_id
    def node_attributes_for_id(self, node_id):
        query = 'select * from node_attributes where node_id = %s'
        return self.attribute_query('attrkey', 'attrvalue', query, (node_id,))

    def interface_for_node_id_with_role(self, node_id, role):
        query = 'select * from interfaces where node_id = %s and role = %s'
        return self.first_row(query, (node_id, role,))

    def control_interface_for_node_id(self, node_id):
        role = self.TBDB_IFACEROLE_CONTROL
        return self.interface_for_node_id_with_role(node_id, role)

    def image_from_id(self, image_id):
        query = 'select * from images where imageid = %s'
        return self.first_row(query, (image_id,))

    def reserved_from_node_id(self, node_id):
        query = 'select * from reserved where node_id = %s'
        return self.first_row(query, (node_id,))

    def new_interface_with_mac(self, mac):
        query = 'select * from new_interfaces where mac = %s'
        return self.first_row(query, (mac_fixed_for_emulab(mac), ))

    def new_node_with_id(self, new_node_id):
        query = 'select * from new_nodes where new_node_id = %s'
        return self.first_row(query, (new_node_id, ))

    def add_node_attribute(self, node_id, key, value):
        query = 'replace into node_attributes (node_id, attrkey, attrvalue) ' \
                'values (%s, %s, %s)'
        return self.query(query, (node_id, key, value,))

    def outlet_for_node(self, node_id):
        query = 'select * from outlets where node_id = %s'
        return self.first_row(query, (node_id, ))

    def get_key_value(self, table_name, key, value):
        query = 'select * from ' + table_name + ' where ' + key + ' = %s'
        return self.first_row(query, (value,))

    def update(self, table_name, primary_key_name, params):
        values = []

        query = 'update %s set' % table_name
        for (key, value) in params.iteritems():
            if value is not None and key != primary_key_name:
                query = query + ' ' + key + ' = %s,'
                values.append(value)

        # Trim the trailing comma and add the where clause
        query = query.rstrip(',') + ' where ' + primary_key_name + ' = %s'

        values.append(params[primary_key_name])
        return self.query(query, values)

    def insert(self, table_name, params):
        values = tuple(params.values())
        query = 'insert into %s (%s) values (' % (table_name,
                                                  ','.join(params.keys()))

        for i in range(len(values) - 1):
            query = query + '%s, '
        query = query + '%s)'

        self.query(query, values)

    def count(self, table_name, key, value):
        query = 'select count(*) as count from ' + table_name + \
                ' where ' + key + ' = %s'
        first_row = self.first_row(query, (value,))
        return first_row['count']

    def save(self, table_name, primary_key_name, params):
        """
        Save an object to the database

        Parameters
        ----------
        table_name : string
             Name of the table where this object will be saved
        primary_key_name : string
             Name of the primary key in this table
        params : dictionary
             Dictionary of values to persist
        """
        if self.count(table_name, primary_key_name, params[primary_key_name]) >= 1:
            self.update(table_name, primary_key_name, params)
        else:
            self.insert(table_name, params)


class DBObject (object):
    """
    Base class for Emulab database objects. Be sure to put in class properties
    table_name and primary_key_name in the subclass
    """
    def __init__(self, params):
        """
        Constructor

        Parameters
        ----------
        params : dictionary
             Set of attributes
        """
        self.__dict__.update(params)

    def __repr__(self):
        return pprint.pformat(vars(self), indent=4)

    def save(self):
        '''Persist this object to the database'''
        db.save(self.table_name, self.primary_key_name, vars(self))

    @classmethod
    def from_id(cls, value):
        '''Retrieve an object from the database using the primary key'''
        return cls.from_key_value(cls.primary_key_name, value)

    @classmethod
    def from_key_value(cls, key, value):
        '''Retrieve an object from the database using the key/value pair'''
        params = db.get_key_value(cls.table_name, key, value)
        if params is not None:
            return cls(params)
        return None


class Image:
    def __init__(self, dictionary):
        self._snapshot = '0001'
        if dictionary:
            for key, value in dictionary.items():
                setattr(self, key, value)

    @classmethod
    def from_id(cls, image_id):
        return cls(db.image_from_id(image_id))

    @property
    def snapshot(self):
        return self._snapshot

    def is_iscsi(self):
        return self.format == 'iscsi' or self.format == 'zfsiscsi'


class Interface:
    def __init__(self, dictionary):
        self._node = None
        if dictionary is not None:
            for key, value in dictionary.items():
                setattr(self, key, value)

    @classmethod
    def from_ip(cls, ip_addr):
        iface = db.interface_for_ip_addr(ip_addr)
        if iface is not None:
            return cls(db.interface_for_ip_addr(ip_addr))
        return None

    @classmethod
    def from_mac(cls, mac):
        row = db.interface_for_mac_addr(mac)
        if row is not None:
            return cls(row)
        return None

    @classmethod
    def control_for_node(cls, node):
        return cls(db.control_interface_for_node_id(node.node_id))

    @property
    def node(self):
        if not self._node:
            self._node = Node.from_id(self.node_id)
        return self._node


class Reserved:
    def __init__(self, dictionary):
        for key, value in dictionary.items():
            setattr(self, key, value)

    @classmethod
    def from_node(cls, node):
        return cls(db.reserved_from_node_id(node.node_id))


class Node:
    def __init__(self, dictionary):
        self._control_interface = None
        self._attributes = None
        self._iscsi_server = None
        self._default_image = None
        self._image = None
        self._iscsi_initiatorname = None
        self._iscsi_target_wwn = None
        self._reserved = None
        self._outlet = None

        for key, value in dictionary.items():
            setattr(self, key, value)

    @classmethod
    def from_control_ip(cls, ip_addr):
        node = None
        interface = Interface.from_ip(ip_addr)

        if interface is not None:
            if interface.node is not None:
                node = interface.node
            if node is not None:
                node._control_interface = interface

        return node

    @classmethod
    def from_id(cls, node_id):
        return cls(db.node_for_id(node_id))

    @property
    def control_interface(self):
        if not self._control_interface and self.node_id:
            self._control_interface = Interface.control_for_node(self)
        return self._control_interface

    @property
    def attributes(self):
        if not self._attributes and self.node_id:
            self._attributes = db.node_attributes_for_id(self.node_id)
        return self._attributes

    @property
    def iscsi_server(self):
        if not self._iscsi_server and self.attributes and \
                'iscsi_server' in self.attributes:
            self._iscsi_server = \
                Node.from_id(self.attributes['iscsi_server'])
        return self._iscsi_server

    @property
    def default_boot_image(self):
        if not self._default_image and self.def_boot_osid:
            self._default_image = Image.from_id(self.def_boot_osid)
        return self._default_image

    @property
    def outlet(self):
        if self._outlet is None:
            self._outlet = Outlet.for_node(self.node_id)
        return self._outlet

    @property
    def reserved(self):
        if not self._reserved:
            self._reserved = Reserved.from_node(self)
        return self._reserved

    @property
    def iscsi_initiatorname(self):
        if not self._iscsi_initiatorname:
            if self.attributes:
                if 'initiator_name' in self.attributes:
                    self._iscsi_initiatorname = \
                        self.attributes['initiator_name']
        return self._iscsi_initiatorname

    @property
    def iscsi_target_wwn(self):
        if not self._iscsi_target_wwn:
            self._iscsi_target_wwn = '%s:%s.%s.%s.%s.%s' % (self.iscsi_server.attributes['base_iqn'].lower(),
                                                            self.reserved.pid.lower(),
                                                            self.reserved.eid.lower(),
                                                            self.reserved.vname.lower(),
                                                            self.default_boot_image.imagename.lower(),
                                                            self.default_boot_image.snapshot)
        return self._iscsi_target_wwn


class NewNode (DBObject):
    table_name = 'new_nodes'
    primary_key_name = 'new_node_id'


class NewInterface (DBObject):
    table_name = 'new_interfaces'
    primary_key_name = 'new_interface_id'

    relations = {'new_node': ('new_node_id', NewNode)}

    @classmethod
    def from_mac(cls, mac):
        return cls.from_key_value('mac', mac_fixed_for_emulab(mac))

    @property
    def new_node(self):
        if getattr(self, '_new_node', None) is None:
            setattr(self, '_new_node', NewNode.from_id(self.new_node_id))

        return self._new_node


class Outlet (DBObject):
    table_name = 'outlets'
    primary_key_name = 'node_id'

    def __init__(self, params):
        super(Outlet, self).__init__(params)

    @classmethod
    def for_node(cls, node_id):
        params = db.outlet_for_node(node_id)
        if params is not None:
            return cls(params)
        return None


log = Log(sys.argv[0])
db = EmulabMySQLdb(log)
