Merge branch 'integration'

Conflicts:
	lib/ansible/playbook.py
	lib/ansible/runner.py
	library/apt
This commit is contained in:
Michael DeHaan 2012-04-23 21:05:06 -04:00
commit f4bca7ea22
7 changed files with 404 additions and 161 deletions

65
apt
View file

@ -42,7 +42,7 @@ def fail_json(**kwargs):
exit_json(rc=1, **kwargs)
try:
import apt
import apt, apt_pkg
except ImportError:
fail_json(msg="could not import apt, please install the python-apt package on this host")
@ -63,17 +63,30 @@ def run_apt(command):
rc = cmd.returncode
return rc, out, err
def package_status(pkgspec, cache):
try:
pkg = cache[pkgspec]
except:
fail_json(msg="No package matching '%s' is available" % pkgspec)
return (pkg.is_installed, pkg.is_upgradable)
def package_split(pkgspec):
parts = pkgspec.split('=')
if len(parts) > 1:
return parts[0], parts[1]
else:
return parts[0], None
def install(pkgspec, cache, upgrade=False):
(installed, upgradable) = package_status(pkgspec, cache)
if (not installed) or (upgrade and upgradable):
def package_status(pkgname, version, cache):
try:
pkg = cache[pkgname]
except KeyError:
fail_json(msg="No package matching '%s' is available" % pkgname)
if version:
return pkg.is_installed and pkg.installed.version == version, False
else:
return pkg.is_installed, pkg.is_upgradable
def install(pkgspec, cache, upgrade=False, default_release=None):
name, version = package_split(pkgspec)
installed, upgradable = package_status(name, version, cache)
if not installed or (upgrade and upgradable):
cmd = "%s -q -y install '%s'" % (APT, pkgspec)
if default_release:
cmd += " -t '%s'" % (default_release,)
rc, out, err = run_apt(cmd)
if rc:
fail_json(msg="'apt-get install %s' failed: %s" % (pkgspec, err))
@ -82,15 +95,16 @@ def install(pkgspec, cache, upgrade=False):
return False
def remove(pkgspec, cache, purge=False):
(installed, upgradable) = package_status(pkgspec, cache)
name, version = package_split(pkgspec)
installed, upgradable = package_status(name, version, cache)
if not installed:
return False
else:
purge = '--purge' if purge else ''
cmd = "%s -q -y %s remove '%s'" % (APT, purge, pkgspec)
cmd = "%s -q -y %s remove '%s'" % (APT, purge, name)
rc, out, err = run_apt(cmd)
if rc:
fail_json(msg="'apt-get remove %s' failed: %s" % (pkgspec, err))
fail_json(msg="'apt-get remove %s' failed: %s" % (name, err))
return True
@ -109,13 +123,14 @@ if not len(items):
params = {}
for x in items:
(k, v) = x.split("=")
(k, v) = x.split("=", 1)
params[k] = v
state = params.get('state','installed')
package = params.get('pkg', params.get('package', params.get('name', None)))
update_cache = params.get('update-cache', 'no')
purge = params.get('purge', 'no')
state = params.get('state', 'installed')
package = params.get('pkg', params.get('package', params.get('name', None)))
update_cache = params.get('update-cache', 'no')
purge = params.get('purge', 'no')
default_release = params.get('default-release', None)
if state not in ['installed', 'latest', 'removed']:
fail_json(msg='invalid state')
@ -130,6 +145,10 @@ if package is None and update_cache != 'yes':
fail_json(msg='pkg=name and/or update-cache=yes is required')
cache = apt.Cache()
if default_release:
apt_pkg.config['APT::Default-Release'] = default_release
# reopen cache w/ modified config
cache.open()
if update_cache == 'yes':
cache.update()
@ -137,10 +156,16 @@ if update_cache == 'yes':
if package == None:
exit_json(changed=False)
if package.count('=') > 1:
fail_json(msg='invalid package spec')
if state == 'latest':
changed = install(package, cache, upgrade=True)
if '=' in package:
fail_json(msg='version number inconsistent with state=latest')
changed = install(package, cache, upgrade=True,
default_release=default_release)
elif state == 'installed':
changed = install(package, cache)
changed = install(package, cache, default_release=default_release)
elif state == 'removed':
changed = remove(package, cache, purge == 'yes')

5
copy
View file

@ -42,7 +42,10 @@ for x in items:
src = params['src']
dest = params['dest']
if src:
src = os.path.expanduser(src)
if dest:
dest = os.path.expanduser(dest)
# raise an error if there is no src file
if not os.path.exists(src):

53
file
View file

@ -72,6 +72,21 @@ def add_path_info(kwargs):
kwargs['state'] = 'absent'
return kwargs
# If selinux fails to find a default, return an array of None
def selinux_default_context(path, mode=0):
context = [None, None, None, None]
if not HAVE_SELINUX:
return context
try:
ret = selinux.matchpathcon(path, mode)
except OSError:
return context
if ret[0] == -1:
return context
context = ret[1].split(':')
debug("got default secontext=%s" % ret[1])
return context
# ===========================================
argfile = sys.argv[1]
@ -89,7 +104,11 @@ for x in items:
state = params.get('state','file')
path = params.get('path', params.get('dest', params.get('name', None)))
if path:
path = os.path.expanduser(path)
src = params.get('src', None)
if src:
src = os.path.expanduser(src)
dest = params.get('dest', None)
mode = params.get('mode', None)
owner = params.get('owner', None)
@ -102,8 +121,16 @@ recurse = params.get('recurse', 'false')
seuser = params.get('seuser', None)
serole = params.get('serole', None)
setype = params.get('setype', None)
serange = params.get('serange', 's0')
secontext = [seuser, serole, setype, serange]
selevel = params.get('serange', 's0')
context = params.get('context', None)
secontext = [seuser, serole, setype, selevel]
if context is not None:
if context != 'default':
fail_json(msg='invalid context: %s' % context)
if seuser is not None or serole is not None or setype is not None:
fail_json(msg='cannot define context=default and seuser, serole or setype')
secontext = selinux_default_context(path)
if state not in [ 'file', 'directory', 'link', 'absent']:
fail_json(msg='invalid state: %s' % state)
@ -144,34 +171,14 @@ def selinux_context(path):
debug("got current secontext=%s" % ret[1])
return context
# If selinux fails to find a default, return an array of None
def selinux_default_context(path, mode=0):
context = [None, None, None, None]
print >>sys.stderr, path
if not HAVE_SELINUX:
return context
try:
ret = selinux.matchpathcon(path, mode)
except OSError:
return context
if ret[0] == -1:
return context
context = ret[1].split(':')
debug("got default secontext=%s" % ret[1])
return context
def set_context_if_different(path, context, changed):
if not HAVE_SELINUX:
return changed
cur_context = selinux_context(path)
new_context = selinux_default_context(path)
new_context = list(cur_context)
for i in range(len(context)):
if context[i] is not None and context[i] != cur_context[i]:
debug('new context was %s' % new_context[i])
new_context[i] = context[i]
debug('new context is %s' % new_context[i])
elif new_context[i] is None:
new_context[i] = cur_context[i]
debug("current secontext is %s" % ':'.join(cur_context))
debug("new secontext is %s" % ':'.join(new_context))
if cur_context != new_context:

249
setup
View file

@ -19,9 +19,16 @@
DEFAULT_ANSIBLE_SETUP = "/etc/ansible/setup"
import array
import fcntl
import glob
import sys
import os
import platform
import re
import shlex
import socket
import struct
import subprocess
import traceback
@ -30,6 +37,244 @@ try:
except ImportError:
import simplejson as json
_I386RE = re.compile(r'i[3456]86')
SIOCGIFCONF = 0x8912
SIOCGIFHWADDR = 0x8927
MEMORY_FACTS = ['MemTotal', 'SwapTotal', 'MemFree', 'SwapFree']
# DMI bits
DMI_DICT = { 'form_factor': '/sys/devices/virtual/dmi/id/chassis_type',
'product_name': '/sys/devices/virtual/dmi/id/product_name',
'product_serial': '/sys/devices/virtual/dmi/id/product_serial',
'product_uuid': '/sys/devices/virtual/dmi/id/product_uuid',
'product_version': '/sys/devices/virtual/dmi/id/product_version',
'system_vendor': '/sys/devices/virtual/dmi/id/sys_vendor' }
# From smolt and DMI spec
FORM_FACTOR = [ "Unknown", "Other", "Unknown", "Desktop",
"Low Profile Desktop", "Pizza Box", "Mini Tower", "Tower",
"Portable", "Laptop", "Notebook", "Hand Held", "Docking Station",
"All In One", "Sub Notebook", "Space-saving", "Lunch Box",
"Main Server Chassis", "Expansion Chassis", "Sub Chassis",
"Bus Expansion Chassis", "Peripheral Chassis", "RAID Chassis",
"Rack Mount Chassis", "Sealed-case PC", "Multi-system",
"CompactPCI", "AdvancedTCA" ]
# For the most part, we assume that platform.dist() will tell the truth.
# This is the fallback to handle unknowns or exceptions
OSDIST_DICT = { '/etc/redhat-release': 'RedHat',
'/etc/vmware-release': 'VMwareESX' }
def get_file_content(path):
if os.path.exists(path) and os.access(path, os.R_OK):
data = open(path).read().strip()
if len(data) == 0:
data = None
else:
data = None
return data
# platform.dist() is deprecated in 2.6
# in 2.6 and newer, you should use platform.linux_distribution()
def get_distribution_facts(facts):
dist = platform.dist()
facts['distribution'] = dist[0].capitalize() or 'NA'
facts['distribution_version'] = dist[1] or 'NA'
facts['distribution_release'] = dist[2] or 'NA'
# Try to handle the exceptions now ...
for (path, name) in OSDIST_DICT.items():
if os.path.exists(path):
if facts['distribution'] == 'Fedora':
pass
elif name == 'RedHat':
data = get_file_content(path)
if 'Red Hat' in data:
facts['distribution'] = name
else:
facts['distribution'] = data.split()[0]
else:
facts['distribution'] = name
# Platform
# patform.system() can be Linux, Darwin, Java, or Windows
def get_platform_facts(facts):
facts['system'] = platform.system()
facts['kernel'] = platform.release()
facts['machine'] = platform.machine()
facts['python_version'] = platform.python_version()
if facts['machine'] == 'x86_64':
facts['architecture'] = facts['machine']
elif _I386RE.search(facts['machine']):
facts['architecture'] = 'i386'
else:
facts['archtecture'] = facts['machine']
if facts['system'] == 'Linux':
get_distribution_facts(facts)
def get_memory_facts(facts):
if not os.access("/proc/meminfo", os.R_OK):
return facts
for line in open("/proc/meminfo").readlines():
data = line.split(":", 1)
key = data[0]
if key in MEMORY_FACTS:
val = data[1].strip().split(' ')[0]
facts["%s_mb" % key.lower()] = long(val) / 1024
def get_cpu_facts(facts):
i = 0
physid = 0
sockets = {}
if not os.access("/proc/cpuinfo", os.R_OK):
return facts
for line in open("/proc/cpuinfo").readlines():
data = line.split(":", 1)
key = data[0].strip()
if key == 'model name':
if 'processor' not in facts:
facts['processor'] = []
facts['processor'].append(data[1].strip())
i += 1
elif key == 'physical id':
physid = data[1].strip()
if physid not in sockets:
sockets[physid] = 1
elif key == 'cpu cores':
sockets[physid] = int(data[1].strip())
if len(sockets) > 0:
facts['processor_count'] = len(sockets)
facts['processor_cores'] = reduce(lambda x, y: x + y, sockets.values())
else:
facts['processor_count'] = i
facts['processor_cores'] = 'NA'
def get_hardware_facts(facts):
get_memory_facts(facts)
get_cpu_facts(facts)
for (key,path) in DMI_DICT.items():
data = get_file_content(path)
if data is not None:
if key == 'form_factor':
facts['form_factor'] = FORM_FACTOR[int(data)]
else:
facts[key] = data
else:
facts[key] = 'NA'
def get_linux_virtual_facts(facts):
if os.path.exists("/proc/xen"):
facts['virtualization_type'] = 'xen'
facts['virtualization_role'] = 'guest'
if os.path.exists("/proc/xen/capabilities"):
facts['virtualization_role'] = 'host'
if os.path.exists("/proc/modules"):
modules = []
for line in open("/proc/modules").readlines():
data = line.split(" ", 1)
modules.append(data[0])
if 'kvm' in modules:
facts['virtualization_type'] = 'kvm'
facts['virtualization_role'] = 'host'
elif 'vboxdrv' in modules:
facts['virtualization_type'] = 'virtualbox'
facts['virtualization_role'] = 'host'
elif 'vboxguest' in modules:
facts['virtualization_type'] = 'virtualbox'
facts['virtualization_role'] = 'guest'
if 'QEMU' in facts['processor'][0]:
facts['virtualization_type'] = 'kvm'
facts['virtualization_role'] = 'guest'
if facts['distribution'] == 'VMwareESX':
facts['virtualization_type'] = 'VMware'
facts['virtualization_role'] = 'host'
# You can spawn a dmidecode process and parse that or infer from devices
for dev_model in glob.glob('/proc/ide/hd*/model'):
info = open(dev_model).read()
if 'VMware' in info:
facts['virtualization_type'] = 'VMware'
facts['virtualization_role'] = 'guest'
elif 'Virtual HD' in info or 'Virtual CD' in info:
facts['virtualization_type'] = 'VirtualPC'
facts['virtualization_role'] = 'guest'
def get_virtual_facts(facts):
facts['virtualization_type'] = 'None'
facts['virtualization_role'] = 'None'
if facts['system'] == 'Linux':
facts = get_linux_virtual_facts(facts)
# get list of interfaces that are up
def get_interfaces():
length = 4096
offset = 32
step = 32
if platform.architecture()[0] == '64bit':
offset = 16
step = 40
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
names = array.array('B', '\0' * length)
bytelen = struct.unpack('iL', fcntl.ioctl(
s.fileno(), SIOCGIFCONF, struct.pack(
'iL', length, names.buffer_info()[0])
))[0]
return [names.tostring()[i:i+offset].split('\0', 1)[0]
for i in range(0, bytelen, step)]
def get_iface_hwaddr(iface):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
info = fcntl.ioctl(s.fileno(), SIOCGIFHWADDR,
struct.pack('256s', iface[:15]))
return ''.join(['%02x:' % ord(char) for char in info[18:24]])[:-1]
def get_network_facts(facts):
facts['fqdn'] = socket.gethostname()
facts['hostname'] = facts['fqdn'].split('.')[0]
facts['interfaces'] = get_interfaces()
for iface in facts['interfaces']:
facts[iface] = { 'macaddress': get_iface_hwaddr(iface) }
# This is lame, but there doesn't appear to be a good way
# to get all addresses for both IPv4 and IPv6.
cmd = subprocess.Popen("/sbin/ifconfig %s" % iface, shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
for line in out.split('\n'):
data = line.split()
if 'inet addr' in line:
if 'ipv4' not in facts[iface]:
facts[iface]['ipv4'] = {}
facts[iface]['ipv4'] = { 'address': data[1].split(':')[1],
'netmask': data[-1].split(':')[1] }
if 'inet6 addr' in line:
(ip, prefix) = data[2].split('/')
scope = data[3].split(':')[1].lower()
if 'ipv6' not in facts[iface]:
facts[iface]['ipv6'] = []
facts[iface]['ipv6'].append( { 'address': ip,
'prefix': prefix,
'scope': scope } )
return facts
def get_public_ssh_host_keys(facts):
dsa = get_file_content('/etc/ssh/ssh_host_dsa_key.pub')
rsa = get_file_content('/etc/ssh/ssh_host_rsa_key.pub')
if dsa is None:
dsa = 'NA'
else:
facts['ssh_host_key_dsa_public'] = dsa.split()[1]
if rsa is None:
rsa = 'NA'
else:
facts['ssh_host_key_rsa_public'] = rsa.split()[1]
def get_service_facts(facts):
get_public_ssh_host_keys(facts)
def ansible_facts():
facts = {}
get_platform_facts(facts)
get_hardware_facts(facts)
get_virtual_facts(facts)
get_network_facts(facts)
get_service_facts(facts)
return facts
# load config & template variables
if len(sys.argv) == 1:
@ -65,6 +310,10 @@ if not os.path.exists(ansible_file):
else:
md5sum = os.popen("md5sum %s" % ansible_file).read().split()[0]
# Get some basic facts in case facter or ohai are not installed
for (k, v) in ansible_facts().items():
setup_options["ansible_%s" % k] = v
# if facter is installed, and we can use --json because
# ruby-json is ALSO installed, include facter data in the JSON

71
slurp Executable file
View file

@ -0,0 +1,71 @@
#!/usr/bin/python
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
# 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/>.
import sys
import os
import shlex
import base64
try:
import json
except ImportError:
import simplejson as json
# ===========================================
# convert arguments of form a=b c=d
# to a dictionary
if len(sys.argv) == 1:
sys.exit(1)
argfile = sys.argv[1]
if not os.path.exists(argfile):
sys.exit(1)
items = shlex.split(open(argfile, 'r').read())
params = {}
for x in items:
(k, v) = x.split("=")
params[k] = v
source = os.path.expanduser(params['src'])
# ==========================================
# raise an error if there is no template metadata
if not os.path.exists(source):
print json.dumps(dict(
failed = 1,
msg = "file not found: %s" % source
))
sys.exit(1)
if not os.access(source, os.R_OK):
print json.dumps(dict(
failed = 1,
msg = "file is not readable: %s" % source
))
sys.exit(1)
# ==========================================
data = file(source).read()
data = base64.b64encode(data)
print json.dumps(dict(content=data, encoding='base64'))
sys.exit(0)

119
template
View file

@ -17,119 +17,8 @@
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
import sys
import os
import jinja2
import shlex
try:
import json
except ImportError:
import simplejson as json
environment = jinja2.Environment()
# ===========================================
# convert arguments of form a=b c=d
# to a dictionary
# FIXME: make more idiomatic
if len(sys.argv) == 1:
sys.exit(1)
argfile = sys.argv[1]
if not os.path.exists(argfile):
sys.exit(1)
items = shlex.split(open(argfile, 'r').read())
params = {}
for x in items:
(k, v) = x.split("=")
params[k] = v
source = params['src']
dest = params['dest']
metadata = params.get('metadata', '/etc/ansible/setup')
module_vars = params.get('vars')
# raise an error if there is no template metadata
if not os.path.exists(metadata):
print json.dumps({
"failed" : 1,
"msg" : "Missing %s, did you run the setup module yet?" % metadata
})
sys.exit(1)
# raise an error if we can't parse the template metadata
#data = {}
try:
f = open(metadata)
data = json.loads(f.read())
f.close()
except:
print json.dumps({
"failed" : 1,
"msg" : "Failed to parse/load %s, rerun the setup module?" % metadata
})
sys.exit(1)
if module_vars:
try:
f = open(module_vars)
vars = json.loads(f.read())
data.update(vars)
f.close()
except:
print json.dumps({
"failed" : 1,
"msg" : "Failed to parse/load %s." % module_vars
})
sys.exit(1)
if not os.path.exists(source):
print json.dumps({
"failed" : 1,
"msg" : "Source template could not be read: %s" % source
})
sys.exit(1)
source = file(source).read()
if os.path.isdir(dest):
print json.dumps({
"failed" : 1,
"msg" : "Destination is a directory"
})
sys.exit(1)
# record md5sum of original source file so we can report if it changed
changed = False
md5sum = None
if os.path.exists(dest):
md5sum = os.popen("md5sum %s" % dest).read().split()[0]
try:
# call Jinja2 here and save the new template file
template = environment.from_string(source)
data_out = template.render(data)
except jinja2.TemplateError, e:
print json.dumps({
"failed": True,
"msg" : e.message
})
sys.exit(1)
f = open(dest, "w+")
f.write(data_out)
f.close()
# record m5sum and return success and whether things have changed
md5sum2 = os.popen("md5sum %s" % dest).read().split()[0]
if md5sum != md5sum2:
changed = True
# mission accomplished
print json.dumps({
"md5sum" : md5sum2,
"changed" : changed
})
# hey the Ansible template module isn't really a remote transferred
# module. All the magic happens in Runner.py making use of the
# copy module, and if not running from a playbook, also the 'slurp'
# module.

3
virt
View file

@ -10,8 +10,7 @@ This software may be freely redistributed under the terms of the GNU
general public license.
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., 675 Mass Ave, Cambridge, MA 02139, USA.
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
VIRT_FAILED = 1