##############################################################################
#
# Copyright (c) 2010 Vifib SARL and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################
import os
import sys
from slapos.recipe.librecipe import BaseSlapRecipe
import subprocess
import binascii
import random
import zc.buildout
import pkg_resources
import ConfigParser
import hashlib

class Recipe(BaseSlapRecipe):

  def _install(self):
    """
    Set the connection dictionnary for the computer partition and create a list
    of paths to the different wrappers

    Parameters : none

    Returns    : List path_list
    """
    self.path_list = []
    
    kvm_conf = self.installKvm(vnc_ip = self.getLocalIPv4Address())
    
    vnc_port = 5900 + kvm_conf['vnc_display']
    
    noVNC_conf = self.installNoVnc(source_ip   = self.getGlobalIPv6Address(),
                                   source_port = 6080,
                                   target_ip   = kvm_conf['vnc_ip'],
                                   target_port = vnc_port,
                                   python_path = kvm_conf['python_path'])
    
    self.linkBinary()
    self.computer_partition.setConnectionDict(dict(
        vnc_connection_string = "https://[%s]:%s/vnc.html?host=[%s]&port=%s" % (noVNC_conf['source_ip'],
                                                     noVNC_conf['source_port'], noVNC_conf['source_ip'],
                                                     noVNC_conf['source_port']
                                                     ), vnc_password = kvm_conf['vnc_passwd']))
    
    return self.path_list

  def installKvm(self, vnc_ip):
    """
    Create kvm configuration dictionnary and instanciate a wrapper for kvm and 
    kvm controller 

    Parameters : IP the vnc server is listening on

    Returns    : Dictionnary kvm_conf
    """
    kvm_conf = dict(vnc_ip = vnc_ip)
    
    connection_found = False
    for tap_interface, dummy in self.parameter_dict['ip_list']:
      # Get an ip associated to a tap interface
      if tap_interface:
        connection_found = True
    if not connection_found:
      raise NotImplementedError("Do not support ip without tap interface")

    kvm_conf['tap_interface'] = tap_interface
    
    # Disk path
    kvm_conf['disk_path'] = os.path.join(self.data_root_directory,
        'virtual.qcow2')
    kvm_conf['socket_path'] = os.path.join(self.var_directory, 'qmp_socket')
    # XXX Weak password
    ##XXX -Vivien: add an option to generate one password for all instances 
    # and/or to input it yourself
    kvm_conf['vnc_passwd'] = binascii.hexlify(os.urandom(4))

    #XXX pid_file path, database_path, path to python binary and xml path
    kvm_conf['pid_file_path'] = os.path.join(self.run_directory, 'pid_file')
    kvm_conf['database_path'] = os.path.join(self.data_root_directory,
        'slapmonitor_database')
    kvm_conf['python_path']   = sys.executable
    kvm_conf['qemu_path']     = self.options['qemu_path']
    #xml_path = os.path.join(self.var_directory, 'slapreport.xml' )

    # Create disk if needed
    if not os.path.exists(kvm_conf['disk_path']):
      retcode = subprocess.call(["%s create -f qcow2 %s %iG" % (
          self.options['qemu_img_path'], kvm_conf['disk_path'],
          int(self.options['disk_size']))], shell=True)
      if retcode != 0:
        raise OSError, "Disk creation failed!"
    
    # Options nbd_ip and nbd_port are provided by slapos master
    kvm_conf['nbd_ip']   = self.parameter_dict['nbd_ip']
    kvm_conf['nbd_port'] = self.parameter_dict['nbd_port']

    # First octet has to represent a locally administered address
    octet_list              = [254] + [random.randint(0x00, 0xff) for x in range(5)]
    kvm_conf['mac_address'] = ':'.join(['%02x' % x for x in octet_list])

    kvm_conf['hostname']    = "slaposkvm"
    kvm_conf['smp_count']   = self.options['smp_count']
    kvm_conf['ram_size']    = self.options['ram_size']

    kvm_conf['vnc_display'] = 1
    # Instanciate KVM

    kvm_runner_path = self.instanciate_wrapper("kvm", kvm_conf)
    self.path_list.append(kvm_runner_path)
    # Instanciate KVM controller
    kvm_controller_runner_path = self.instanciate_wrapper("kvm_controller", 
                                                          kvm_conf)
    self.path_list.append(kvm_controller_runner_path)
    # Instanciate Slapmonitor
    ##slapmonitor_runner_path = self.instanciate_wrapper("slapmonitor",
    #    [database_path, pid_file_path, python_path])
    # Instanciate Slapreport
    ##slapreport_runner_path = self.instanciate_wrapper("slapreport",
    #    [database_path, python_path])
    
    return kvm_conf

  def installNoVnc(self, source_ip, source_port, target_ip, target_port, 
                   python_path):
    """
    Create noVNC configuration dictionnary and instanciate Websockify proxy

    Parameters : IP of the proxy, port on which is situated the proxy,
                 IP of the vnc server, port on which is situated the vnc server,
                 path to python binary

    Returns    : noVNC configuration dictionnary
    """

    noVNC_conf = {}
    noVNC_conf['websockify_path']  = self.options['websockify_path']
    noVNC_conf['noVNC_location']   = self.options['noVNC_location']
    noVNC_conf['source_ip']        = source_ip                                          
    noVNC_conf['source_port']      = source_port
    noVNC_conf['target_ip']        = target_ip
    noVNC_conf['target_port']      = target_port
    noVNC_conf['python_path']      = python_path

    noVNC_conf['ca_conf']          = self.installCertificateAuthority()
    noVNC_conf['key_path'], noVNC_conf['certificate_path'] = self.requestCertificate('noVNC')

    # Instanciate Websockify
    websockify_runner_path = self.instanciate_wrapper("websockify",
        noVNC_conf)
    self.path_list.append(websockify_runner_path)
  
    return noVNC_conf

  def instanciate_wrapper(self, name, config_dictionnary):

    """
    Define the path to the wrapper of the thing you are instanciating
    
    Parameters : name of what you are instanciating, list of arguments for the 
    configuration dictionnary of the wrapper
    
    Returns    : path to the running wrapper
    """
  
    wrapper_template_location = pkg_resources.resource_filename(          
                                             __name__, os.path.join(         
                                             'template', '%s_run.in' % name))     
    
    runner_path = self.createRunningWrapper(name,                        
          self.substituteTemplate(wrapper_template_location, config_dictionnary))
    

    return runner_path

  def linkBinary(self):
    """Links binaries to instance's bin directory for easier exposal"""
    for linkline in self.options.get('link_binary_list', '').splitlines():
      if not linkline:
        continue
      target = linkline.split()
      if len(target) == 1:
        target = target[0]
        path, linkname = os.path.split(target)
      else:
        linkname = target[1]
        target = target[0]
      link = os.path.join(self.bin_directory, linkname)
      if os.path.lexists(link):
        if not os.path.islink(link):
          raise zc.buildout.UserError(
              'Target link already %r exists but it is not link' % link)
        os.unlink(link)
      os.symlink(target, link)
      self.logger.debug('Created link %r -> %r' % (link, target))
      self.path_list.append(link)
      
  def installCertificateAuthority(self, ca_country_code='XX',
      ca_email='xx@example.com', ca_state='State', ca_city='City',
      ca_company='Company'):
    self.requirements, self.ws = self.egg.working_set()
    self.cron_d = self.installCrond()
    backup_path = self.createBackupDirectory('ca')
    self.ca_dir = os.path.join(self.data_root_directory, 'ca')
    self._createDirectory(self.ca_dir)
    self.ca_request_dir = os.path.join(self.ca_dir, 'requests')
    self._createDirectory(self.ca_request_dir)
    config = dict(ca_dir=self.ca_dir, request_dir=self.ca_request_dir)
    self.ca_private = os.path.join(self.ca_dir, 'private')
    self.ca_certs = os.path.join(self.ca_dir, 'certs')
    self.ca_crl = os.path.join(self.ca_dir, 'crl')
    self.ca_newcerts = os.path.join(self.ca_dir, 'newcerts')
    self.ca_key_ext = '.key'
    self.ca_crt_ext = '.crt'
    for d in [self.ca_private, self.ca_crl, self.ca_newcerts, self.ca_certs]:
      self._createDirectory(d)
    for f in ['crlnumber', 'serial']:
      if not os.path.exists(os.path.join(self.ca_dir, f)):
        open(os.path.join(self.ca_dir, f), 'w').write('01')
    if not os.path.exists(os.path.join(self.ca_dir, 'index.txt')):
      open(os.path.join(self.ca_dir, 'index.txt'), 'w').write('')
    openssl_configuration = os.path.join(self.ca_dir, 'openssl.cnf')
    config.update(
        working_directory=self.ca_dir,
        country_code=ca_country_code,
        state=ca_state,
        city=ca_city,
        company=ca_company,
        email_address=ca_email,
    )
    self._writeFile(openssl_configuration, pkg_resources.resource_string(
      __name__, 'template/openssl.cnf.ca.in') % config)
    self.path_list.extend(zc.buildout.easy_install.scripts([
      ('certificate_authority',
        __name__ + '.certificate_authority', 'runCertificateAuthority')],
        self.ws, sys.executable, self.wrapper_directory, arguments=[dict(
          openssl_configuration=openssl_configuration,
          openssl_binary=self.options['openssl_binary'],
          certificate=os.path.join(self.ca_dir, 'cacert.pem'),
          key=os.path.join(self.ca_private, 'cakey.pem'),
          crl=os.path.join(self.ca_crl),
          request_dir=self.ca_request_dir
          )]))
    # configure backup
    backup_cron = os.path.join(self.cron_d, 'ca_rdiff_backup')
    open(backup_cron, 'w').write(
        '''0 0 * * * %(rdiff_backup)s %(source)s %(destination)s'''%dict(
          rdiff_backup=self.options['rdiff_backup_binary'],
          source=self.ca_dir,
          destination=backup_path))
    self.path_list.append(backup_cron)

    return dict(
      ca_certificate=os.path.join(config['ca_dir'], 'cacert.pem'),
      ca_crl=os.path.join(config['ca_dir'], 'crl'),
      certificate_authority_path=config['ca_dir']
    )
  
  def requestCertificate(self, name):
    hash = hashlib.sha512(name).hexdigest()
    key = os.path.join(self.ca_private, hash + self.ca_key_ext)
    certificate = os.path.join(self.ca_certs, hash + self.ca_crt_ext)
    parser = ConfigParser.RawConfigParser()
    parser.add_section('certificate')
    parser.set('certificate', 'name', name)
    parser.set('certificate', 'key_file', key)
    parser.set('certificate', 'certificate_file', certificate)
    parser.write(open(os.path.join(self.ca_request_dir, hash), 'w'))
    return key, certificate
  
  def installCrond(self):
    timestamps = self.createDataDirectory('cronstamps')
    cron_output = os.path.join(self.log_directory, 'cron-output')
    self._createDirectory(cron_output)
    catcher = zc.buildout.easy_install.scripts([('catchcron',
      __name__ + '.catdatefile', 'catdatefile')], self.ws, sys.executable,
      self.bin_directory, arguments=[cron_output])[0]
    self.path_list.append(catcher)
    cron_d = os.path.join(self.etc_directory, 'cron.d')
    crontabs = os.path.join(self.etc_directory, 'crontabs')
    self._createDirectory(cron_d)
    self._createDirectory(crontabs)
    # Use execute from erp5.
    wrapper = zc.buildout.easy_install.scripts([('crond',
      'slapos.recipe.librecipe.execute', 'execute')], self.ws, sys.executable,
      self.wrapper_directory, arguments=[
        self.options['dcrond_binary'].strip(), '-s', cron_d, '-c', crontabs,
        '-t', timestamps, '-f', '-l', '5', '-M', catcher]
      )[0]
    self.path_list.append(wrapper)
    return cron_d