From 416f25eaffaad00e3b697ae58b9e1388ccbb0d27 Mon Sep 17 00:00:00 2001 From: Rob Parrott Date: Fri, 15 Mar 2013 22:51:21 -0400 Subject: [PATCH] added vagrant module --- library/vagrant | 568 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 568 insertions(+) create mode 100644 library/vagrant diff --git a/library/vagrant b/library/vagrant new file mode 100644 index 00000000000..779dc66bca6 --- /dev/null +++ b/library/vagrant @@ -0,0 +1,568 @@ +#!/usr/bin/env python -tt +# 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'. This module has a dependency on python-vagrant. +version_added: "100.0" +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 +#<> + +main()