ansible/library/vagrant
2013-04-03 21:29:05 +02:00

569 lines
19 KiB
Python

#!/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'.
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", "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 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()