#!/usr/bin/python 
# This file is part of Ansible
#
# Ansible 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.
#
# Ansible 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 Ansible.  If not, see <http://www.gnu.org/licenses/>.

DOCUMENTATION = '''
---
module: vagrant
short_description: create a local instance via vagrant
description:
     - creates VM instances via vagrant and optionally waits for it to be 'running'. This module has a dependency on python-vagrant.
version_added: "1.1"
options:
  state:
    description:
      - Should the VMs be "present" or "absent."
  cmd:
    description:
      - vagrant subcommand to execute. Can be "up," "status," "config," "ssh," "halt," "destroy" or "clear." 
    required: false
    default: null
    aliases: ['command'] 
  box_name:
    description:
      - vagrant boxed image to start
    required: false
    default: null
    aliases: ['image']
  box_path:
    description:
      - path to vagrant boxed image to start
    required: false
    default: null
    aliases: []
  vm_name:
    description:
      - name to give an associated VM
    required: false
    default: null
    aliases: [] 
  count:
    description:
      - number of instances to launch
    required: False
    default: 1
    aliases: []
  forward_ports:
    description:
      - comma separated list of ports to forward to the host. If the port is under 1024, the host port will be the guest port +10000
    required: False
    aliases: []
  memory:
    description:
      - memory in MB    
    required: False
    
examples:
   - code: 'local_action: vagrant cmd=up box_name=lucid32 vm_name=webserver'
     description: 
requirements: [ "vagrant" ]
author: Rob Parrott
'''

VAGRANT_FILE = "./Vagrantfile"
VAGRANT_DICT_FILE = "./Vagrantfile.json"
VAGRANT_LOCKFILE = "./.vagrant-lock"

VAGRANT_FILE_HEAD = "Vagrant::Config.run do |config|\n"
VAGRANT_FILE_BOX_NAME = "  config.vm.box = \"%s\"\n"
VAGRANT_FILE_VM_STANZA_HEAD = """
  config.vm.define :%s do |%s_config|
    %s_config.vm.network :hostonly, "%s"
    %s_config.vm.box = "%s"
"""
VAGRANT_FILE_HOSTNAME_LINE     = "    %s_config.vm.host_name = \"%s\"\n"
VAGRANT_FILE_PORT_FORWARD_LINE = "    %s_config.vm.forward_port %s, %s\n"  
VAGRANT_FILE_MEMORY_LINE       = "    %s_config.vm.customize [\"modifyvm\", :id, \"--memory\", %s]\n" 
VAGRANT_FILE_VM_STANZA_TAIL="  end\n"

VAGRANT_FILE_TAIL = "\nend\n"

# If this is already a network on your machine, this may fail ... change it here.
VAGRANT_INT_IP = "192.168.179.%s"

DEFAULT_VM_NAME = "ansible"
DEFAULT_VM_RAM = 1024

import sys
import subprocess
#import time
import os.path
import json

try:
    import lockfile
except ImportError:
    print "Python module lockfile is not installed. Falling back to using flock(), which will fail on Windows."
    
try:
    import vagrant
except ImportError:
    print "failed=True msg='python-vagrant required for this module'"
    sys.exit(1)

class VagrantWrapper(object):

    def __init__(self):
        '''
        Wrapper around the python-vagrant module for use with ansible.
        Note that Vagrant itself is non-thread safe, as is the python-vagrant lib, so we need to lock on basically all operations ...
        '''
        # Get a lock
        self.lock = None

        try:
            self.lock = lockfile.FileLock(VAGRANT_LOCKFILE)
            self.lock.acquire()
        except:
            # fall back to using flock instead ...
            try:
                import fcntl
                self.lock = open(VAGRANT_LOCKFILE, 'w')
                fcntl.flock(self.lock, fcntl.LOCK_EX)
            except:
                print "failed=True msg='Could not get a lock for using vagrant. Install python module \"lockfile\" to use vagrant on non-POSIX filesytems.'"
                sys.exit(1)
            
        # Initialize vagrant and state files
        self.vg = vagrant.Vagrant()
        
        # operation will create a default data structure if none present
        self._deserialize()  
        self._serialize()
        
    def __del__(self):
        '''Clean up file locks'''
        try:
            self.lock.release()
        except:
            os.close(self.lock)
            os.unlink(self.lock)
        
    def prepare_box(self, box_name, box_path):
        '''Given a specified name and URL, import a Vagrant "box" for use.'''
        changed = False
        if box_name == None:
            raise Exception("You must specify a box_name with a box_path for vagrant.")
        boxes = self.vg.box_list()
        if not box_name in boxes:
            self.vg.box_add(box_name, box_path)
            changed = True
            
        return changed
             
    def up(self, box_name, vm_name=None, count=1, box_path=None, ports=[]):    
        '''Fire up a given VM and name it, using vagrant's multi-VM mode.'''

        changed = False
        if vm_name == None: 
            vm_name = DEFAULT_VM_NAME
        
        if box_name == None:
            raise Exception("You must specify a box name for Vagrant.")
        if box_path != None: 
            changed = self.prepare_box(box_name, box_path)
        
        for icount in range(int(count)):
            
            self._deserialize()
            
            this_instance_dict = self._get_instance(vm_name,icount)
            if not this_instance_dict.has_key('box_name'): 
                this_instance_dict['box_name'] = box_name   
                     
            this_instance_dict['forward_ports'] = ports  
            
            # Save our changes and run
            inst_array = self._instances()[vm_name]
            inst_array[icount] = this_instance_dict    
            
            self._serialize()
            
            # See if we need to fire it up ...
            vgn = this_instance_dict['vagrant_name']
            status = self.vg.status(vgn)
            if status != 'running':
                self.vg.up(False, this_instance_dict['vagrant_name'])
                changed =True
                
        ansible_instance_array = self._build_instance_array_for_ansible(vm_name)
        return (changed, ansible_instance_array)
    
    def status(self, vm_name = None, index = -1):
        '''Return the run status of the VM instance. If no instance N is given, returns first instance.'''
        vm_names = []
        if vm_name != None: vm_names = [vm_name]
        else:
            vm_names = self._instances().keys()
        
        statuses = {}
        for vmn in vm_names:
            stat_array = []
            instance_array = self.vg_data['instances'][vmn]
            if index >= 0:
                instance_array = [ self._get_instance(vmn,index) ]
            for inst in instance_array:
                vgn = inst['vagrant_name']
                stat_array.append(self.vg.status(vgn))
            statuses[vmn] = stat_array     
                    
        return (False, statuses)
            
    def config(self, vm_name, index = -1):
        '''Return info on SSH for the running instance.'''
        vm_names = []
        if vm_name != None: vm_names = [vm_name]
        else:
            vm_names = self._instances().keys()
        
        configs = {}
        for vmn in vm_names:
            conf_array = []
            instance_array = self.vg_data['instances'][vmn]
            if index >= 0:
                instance_array = [ self._get_instance(vmn,index) ]
            for inst in instance_array:
                cnf = self.vg.conf(None, inst['vagrant_name'])
                conf_array.append(cnf)
            configs[vmn] = conf_array     
                            
        return (False, configs)

    def halt(self, vm_name = None, index = -1):
        '''Shuts down a vm_name or all VMs.'''
        
        changed = False
        vm_names = []
        if vm_name != None: vm_names = [vm_name]
        else:
            vm_names = self._instances().keys()
        
        statuses = {}
        for vmn in vm_names:
            stat_array = []
            instance_array = self.vg_data['instances'][vmn]
            if index >= 0:
                instance_array = [ self.vg_data['instances'][vmn][index] ]
            for inst in instance_array:
                vgn = inst['vagrant_name']
                if self.vg.status(vgn) == 'running':
                    self.vg.halt(vgn)
                    changed = True
                stat_array.append(self.vg.status(vgn))
            statuses[vmn] = stat_array
            
        return (changed, statuses)         
            
    def destroy(self, vm_name=None, index = -1):
        '''Halt and remove data for a VM, or all VMs.'''
        
        self._deserialize()
        
        (changed, stats) = self.halt(vm_name, index)       
    
        self.vg.destroy(vm_name)
        if vm_name != None:
            self._instances().pop(vm_name)
        else:
            self.vg_data['instances'] = {}
            
        self._serialize()

        changed = True
        
        return changed
        
    def clear(self, vm_name=None):
        '''Halt and remove data for a VM, or all VMs. Also clear all state data.'''
        
        changed = self.vg.destroy(vm_name)
        
        if os.path.isfile(VAGRANT_FILE):
            os.remove(VAGRANT_FILE)
        if os.path.isfile(VAGRANT_DICT_FILE):
            os.remove(VAGRANT_DICT_FILE)            

        return changed           
#
# Helper Methods
#
    def _instances(self): 
        return self.vg_data['instances']
    
    def _get_instance(self, vm_name, index):
    
        instances = self._instances()

        inst_array = []        
        if instances.has_key(vm_name):
            inst_array = instances[vm_name]
            
        if len(inst_array) > index: 
            return inst_array[index]
        
        # 
        # otherwise create one afresh
        #
        this_instance_N = self.vg_data['num_inst']+1
        name_for_vagrant = "%s%d" % (vm_name.replace("-","_"),index)
        
        instance_dict = dict(
          n = index,
          N = this_instance_N,
          name = vm_name,
          vagrant_name = name_for_vagrant,
          internal_ip = VAGRANT_INT_IP % (255-this_instance_N),
          forward_ports = [],
          ram = DEFAULT_VM_RAM,
        )

        # Save this ...
        self.vg_data['num_inst'] = this_instance_N
        inst_array.append(instance_dict)
        
        self._instances()[vm_name] = inst_array
        
        return instance_dict          
    
    #
    # Serialize/Deserialize current state to a JSON representation, and 
    #  a file format for Vagrant.
    #
    # This is where we need to deal with file locking, since multiple threads/procs
    #  may be trying to operate on the same files 
    # 
    
    def _serialize(self):
        '''Save state to a JSON file, and write the Vagrantfile based on this.'''
        self._save_state()
        self._write_vagrantfile()
        
    def _deserialize(self): 
        '''Load in data from the JSON state file.'''
        self._load_state()
        
    
    #
    # Manage a JSON representation of vagrantfile for statefulness across invocations. 
    #
    def _load_state(self):        
        self.vg_data = dict(num_inst=0, instances = {})
        if os.path.isfile(VAGRANT_DICT_FILE):
            json_file=open(VAGRANT_DICT_FILE)
            self.vg_data = json.load(json_file)
            json_file.close()

#    def _state_as_string(self):
#        from StringIO import StringIO
#        io = StringIO()
#        json.dump(self.vg_data, io)
#        return io.getvalue()

    def _save_state(self):
        json_file=open(VAGRANT_DICT_FILE, 'w')
        json.dump(self.vg_data,json_file, sort_keys=True, indent=4, separators=(',', ': '))
        json_file.close()
      
    #
    # Translate the state dictionary into the Vagrantfile
    #    
    def _write_vagrantfile(self):

        vfile = open(VAGRANT_FILE, 'w')
        vfile.write(VAGRANT_FILE_HEAD)

        instances = self._instances()
        for vm_name in instances.keys():
            inst_array = instances[vm_name]
            for index in range(len(inst_array)):
                instance_dict = inst_array[index] 
                name     = instance_dict['vagrant_name']
                ip       = instance_dict['internal_ip']
                box_name = instance_dict['box_name']
                vfile.write(VAGRANT_FILE_VM_STANZA_HEAD % 
                            (name, name, name, ip, name, box_name) )
                if instance_dict.has_key('ram'):
                    vfile.write(VAGRANT_FILE_MEMORY_LINE  % (name, instance_dict['ram'])  )   
                vfile.write(VAGRANT_FILE_HOSTNAME_LINE  % (name, name.replace('_','-'))  )       
                if instance_dict.has_key('forward_ports'):
                    for port in instance_dict['forward_ports']:
                        port = int(port)
                        host_port = port
                        if port < 1024: 
                            host_port = port + 10000
                        vfile.write(VAGRANT_FILE_PORT_FORWARD_LINE % (name, port, host_port) )
                vfile.write(VAGRANT_FILE_VM_STANZA_TAIL)
  
        vfile.write(VAGRANT_FILE_TAIL)
        vfile.close()
        
    #
    # To be returned to ansible with info about instances
    #        
    def _build_instance_array_for_ansible(self, vmname=None):
    
        vm_names = []
        instances = self._instances()
        if vmname != None:
            vm_names = [vmname]
        else:
            vm_names = instances.keys()
        
        ans_instances = []   
        for vm_name in vm_names:
            for inst in instances[vm_name]:
                vagrant_name = inst['vagrant_name']
                cnf = self.vg.conf(None,vagrant_name) 
                vg_data = instances[vm_name]
                if cnf != None:
                    instance_dict = dict(
                      name            = vm_name,
                      vagrant_name    = vagrant_name, 
                      id              = cnf['Host'],
                      public_ip       = cnf['HostName'],
                      internal_ip     = inst['internal_ip'],
                      public_dns_name = cnf['HostName'],
                      port            = cnf['Port'],
                      username        = cnf['User'],
                      key             = cnf['IdentityFile'],
                      status          = self.vg.status(vagrant_name)           
                    )
                    ans_instances.append(instance_dict)

        return ans_instances
         
#--------
# MAIN
#--------
def main():
    
    module = AnsibleModule(
        argument_spec = dict(
            state=dict(),
            cmd=dict(required=False, aliases = ['command']),
            box_name=dict(required=False, aliases = ['image']),
            box_path=dict(),
            vm_name=dict(),
            forward_ports=dict(),
            count = dict(default='1'), 
       )
   )
    
    state = module.params.get('state')
    cmd = module.params.get('cmd')
    box_name = module.params.get('box_name')
    box_path = module.params.get('box_path')
    vm_name = module.params.get('vm_name')
    forward_ports = module.params.get('forward_ports')     

    if forward_ports != None:
        forward_ports=forward_ports.split(',')
    if forward_ports == None: 
        forward_ports=[]

    count = module.params.get('count') 
 
    # Initialize vagrant
    vgw = VagrantWrapper()
    
    #
    # Check if we are being invoked under an idempotency idiom of "state=present" or "state=absent"
    #
    try:
        if state != None:
            
            if state != 'present' and state != 'absent':
                module.fail_json(msg = "State must be \"present\" or \"absent\" in vagrant module.")
                 
            if state == 'present':
               
                changd, insts = vgw.up(box_name, vm_name, count, box_path, forward_ports)
                module.exit_json(changed = changd, instances = insts)
                 
            if state == 'absent':
                changd = vgw.halt(vm_name)    
                module.exit_json(changed = changd, status = vgw.status(vm_name))
                 
                 
        #
        # Main command tree for old style invocation
        #                     
            
        else:

            if cmd == 'up':
            
                if count == None: 
                    count = 1
                (changd, insts) = vgw.up(box_name, vm_name, count, box_path, forward_ports)
                module.exit_json(changed = changd, instances = insts)

            elif cmd == 'status':

#            if vm_name == None:
#                module.fail_json(msg = "Error: you must specify a vm_name when calling status." )
                
                (changd, result) = vgw.status(vm_name)
                module.exit_json(changed = changd, status = result)

            elif cmd == "config" or cmd == "conf":
            
                if vm_name == None:
                    module.fail_json(msg = "Error: you must specify a vm_name when calling config." )
                (changd, cnf) = vgw.config(vm_name)
                module.exit_json(changed = changd, config = cnf)

            elif cmd == 'ssh':
            
                if vm_name == None:
                    module.fail_json(msg = "Error: you must specify a vm_name when calling ssh." )             
                            
                (changd, cnf) = vgw.config(vm_name)
                sshcmd = "ssh -i %s -p %s %s@%s" % (cnf["IdentityFile"], cnf["Port"], cnf["User"], cnf["HostName"])
                sshmsg = "Execute the command \"vagrant ssh %s\"" % (vm_name)
                module.exit_json(changed = changd, msg = sshmsg, SshCommand = sshcmd)
           
            elif cmd == 'halt':

                (changd, stats) = vgw.halt(vm_name)
                module.exit_json(changed = changd, status = stats)

            elif cmd == 'destroy':

                changd = vgw.destroy(vm_name)    
                module.exit_json(changed = changd, status = vgw.status(vm_name))
            
            elif cmd == 'clear':
            
                changd = vgw.clear()          
                module.exit_json(changed = changd)
            
            else:
            
                module.fail_json(msg = "Unknown vagrant subcommand: \"%s\"." % (cmd))
            
    except subprocess.CalledProcessError as errer:     
        module.fail_json(msg = "Vagrant command failed: %s." % (errer))
    except Exception as errer:
        module.fail_json(msg = errer.__str__())
    module.exit_json(status = "success")
  

    

# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>

main()
