#!/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 . 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'. version_added: "1.1" options: state: description: - Should the VMs be "present" or "absent." cmd: description: - vagrant subcommand to execute. required: false default: null aliases: ['command'] choices: [ "up", "status", "config", "ssh", "halt", "destroy", "clear" ] 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", "python-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 'box_name' in this_instance_dict: 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 vm_name in instances: 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 'ram' in instance_dict: vfile.write(VAGRANT_FILE_MEMORY_LINE % (name, instance_dict['ram'])) vfile.write(VAGRANT_FILE_HOSTNAME_LINE % (name, name.replace('_','-'))) if 'forward_ports' in instance_dict: 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: a vm_name is required when calling config.") (changd, cnf) = vgw.config(vm_name) module.exit_json(changed = changd, config = cnf) elif cmd == 'ssh': # this doesn't really seem to belong here, should just manage the VM with ansible -- MPD if vm_name == None: module.fail_json(msg = "Error: a vm_name is required 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 #<> main()