diff --git a/apt b/apt index 81b40ea2a08..e3ebf0c6733 100755 --- a/apt +++ b/apt @@ -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') diff --git a/copy b/copy index fecfeeafacb..b5a52647a95 100755 --- a/copy +++ b/copy @@ -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): diff --git a/file b/file index 25ea749d02b..e0ebbecb171 100755 --- a/file +++ b/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: diff --git a/setup b/setup index 6efb5e973c0..134cd232e7f 100755 --- a/setup +++ b/setup @@ -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 diff --git a/slurp b/slurp new file mode 100755 index 00000000000..36e84ecc09d --- /dev/null +++ b/slurp @@ -0,0 +1,71 @@ +#!/usr/bin/python + +# (c) 2012, Michael DeHaan +# +# 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 . + +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) + diff --git a/template b/template index 0b13422d5ab..a290899c5ca 100755 --- a/template +++ b/template @@ -17,119 +17,8 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -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. diff --git a/virt b/virt index 85e88228d1b..e2dee4c182d 100755 --- a/virt +++ b/virt @@ -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 . """ VIRT_FAILED = 1