From 37e18dfb13e74d22f46fde5ceda11cea7b61c213 Mon Sep 17 00:00:00 2001 From: Jeroen Hoekx Date: Tue, 17 Apr 2012 11:14:58 +0200 Subject: [PATCH 01/15] template: expand path if metadata is in user home. --- template | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/template b/template index 0b13422d5ab..31a2095e502 100755 --- a/template +++ b/template @@ -47,7 +47,8 @@ for x in items: source = params['src'] dest = params['dest'] -metadata = params.get('metadata', '/etc/ansible/setup') +metadata = params.get('metadata', '/etc/ansible/setup') +metadata = os.path.expanduser(metadata) module_vars = params.get('vars') # raise an error if there is no template metadata From c98ed049c327ed990a4ccf897b1d1d0cda85fbaa Mon Sep 17 00:00:00 2001 From: Tim Bielawa Date: Fri, 13 Apr 2012 15:10:45 -0400 Subject: [PATCH 02/15] Fix FSF address in virt header. --- virt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 From cfbd9b282bf2e9b533c3c09b42f2c6294259d7cb Mon Sep 17 00:00:00 2001 From: Stephen Fromm Date: Tue, 17 Apr 2012 16:59:23 -0700 Subject: [PATCH 03/15] Add native facts to library/setup This collects various facts from the host so that it isn't necessary to have facter or ohai installed. It gets various platform/distribution facts, information about the type of hardware, whether a virtual environment and what type, assorted interface facts, and ssh host public keys. Most facts are flat. The two exceptions are 'processor' and all interface facts. Interface facts are presented as: ansible_lo : { "macaddress": "00:00:00:00:00:00", "ipv4": { "address": "127.0.0.1", "netmask": "255.0.0.0" }, "ipv6": [ { "address": "::1", "prefix": "128", "scope": "host" } ] } --- setup | 249 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) 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 From 573d4b4badf4bc96daa97d21edfe25f4be9c2704 Mon Sep 17 00:00:00 2001 From: Stephen Fromm Date: Tue, 17 Apr 2012 17:12:09 -0700 Subject: [PATCH 04/15] Rename serange to selevel to be consistent with selinux docs --- file | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/file b/file index 25ea749d02b..742a7d7b227 100755 --- a/file +++ b/file @@ -102,8 +102,8 @@ 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') +secontext = [seuser, serole, setype, selevel] if state not in [ 'file', 'directory', 'link', 'absent']: fail_json(msg='invalid state: %s' % state) From eeb3cf7bd7375608aeddabc91ba8087a7c6f0f96 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Wed, 18 Apr 2012 22:43:17 -0400 Subject: [PATCH 05/15] make all templating happen locally, so no jinja2 deps are ever required --- template | 120 ++----------------------------------------------------- 1 file changed, 4 insertions(+), 116 deletions(-) diff --git a/template b/template index 31a2095e502..a290899c5ca 100755 --- a/template +++ b/template @@ -17,120 +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') -metadata = os.path.expanduser(metadata) -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. From 908f01044c8a4ed4f242daa59d606d4ca830c91f Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Thu, 19 Apr 2012 09:03:40 -0400 Subject: [PATCH 06/15] Add missing file (slurp) used by templates in non-playbook mode. --- slurp | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100755 slurp diff --git a/slurp b/slurp new file mode 100755 index 00000000000..8b80e5da4b9 --- /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 = 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" % metadata + )) + 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) + From f6d0cc56a065eed5e19e3308cc5899f76228c9d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20N=C3=A9ri?= Date: Thu, 19 Apr 2012 23:40:44 +0200 Subject: [PATCH 07/15] Fix two misspellings of the apt module's "fail_json" function --- apt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apt b/apt index 90e4b17a1bd..81b40ea2a08 100755 --- a/apt +++ b/apt @@ -76,7 +76,7 @@ def install(pkgspec, cache, upgrade=False): cmd = "%s -q -y install '%s'" % (APT, pkgspec) rc, out, err = run_apt(cmd) if rc: - json_fail(msg="'apt-get install %s' failed: %s" % (pkgspec, err)) + fail_json(msg="'apt-get install %s' failed: %s" % (pkgspec, err)) return True else: return False @@ -90,7 +90,7 @@ def remove(pkgspec, cache, purge=False): cmd = "%s -q -y %s remove '%s'" % (APT, purge, pkgspec) rc, out, err = run_apt(cmd) if rc: - json_fail(msg="'apt-get remove %s' failed: %s" % (pkgspec, err)) + fail_json(msg="'apt-get remove %s' failed: %s" % (pkgspec, err)) return True From 4a72e71cd78806bdd6c36c6ab28ffac24615a163 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Thu, 19 Apr 2012 11:38:44 -0400 Subject: [PATCH 08/15] Fixup slurp module usage when not running as root, fix error handling path in slurp module. --- slurp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slurp b/slurp index 8b80e5da4b9..ab0c879db39 100755 --- a/slurp +++ b/slurp @@ -50,7 +50,7 @@ source = params['src'] if not os.path.exists(source): print json.dumps(dict( failed = 1, - msg = "file not found: %s" % metadata + msg = "file not found: %s" % source )) sys.exit(1) From f74984fb4a190aa45129ebec09965925fff82846 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Fri, 20 Apr 2012 07:54:38 -0400 Subject: [PATCH 09/15] A better fix for slurp, expand path in the module. --- slurp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slurp b/slurp index ab0c879db39..36e84ecc09d 100755 --- a/slurp +++ b/slurp @@ -42,7 +42,7 @@ params = {} for x in items: (k, v) = x.split("=") params[k] = v -source = params['src'] +source = os.path.expanduser(params['src']) # ========================================== From ba882d98e1150322d5dfebbdc1fd6accc3886c11 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Fri, 20 Apr 2012 07:57:39 -0400 Subject: [PATCH 10/15] Call os.path.expanduser in modules so things work as expected even when using ./hacking/test-module script --- copy | 4 ++-- file | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/copy b/copy index fecfeeafacb..ef6c30a6b66 100755 --- a/copy +++ b/copy @@ -40,8 +40,8 @@ for x in items: (k, v) = x.split("=") params[k] = v -src = params['src'] -dest = params['dest'] +src = os.path.expanduser(params['src']) +dest = os.path.expanduser(params['dest']) # raise an error if there is no src file diff --git a/file b/file index 742a7d7b227..2922c3d0803 100755 --- a/file +++ b/file @@ -88,8 +88,8 @@ for x in items: params[k] = v state = params.get('state','file') -path = params.get('path', params.get('dest', params.get('name', None))) -src = params.get('src', None) +path = os.path.expanduser(params.get('path', params.get('dest', params.get('name', None)))) +src = os.path.expanduser(params.get('src', None)) dest = params.get('dest', None) mode = params.get('mode', None) owner = params.get('owner', None) From d35de23c53ce9f2175fba804380e7a61331fe25d Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Fri, 20 Apr 2012 08:02:12 -0400 Subject: [PATCH 11/15] Don't try to expand path for None values --- copy | 9 ++++++--- file | 8 ++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/copy b/copy index ef6c30a6b66..b5a52647a95 100755 --- a/copy +++ b/copy @@ -40,9 +40,12 @@ for x in items: (k, v) = x.split("=") params[k] = v -src = os.path.expanduser(params['src']) -dest = os.path.expanduser(params['dest']) - +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 2922c3d0803..b672934e4ee 100755 --- a/file +++ b/file @@ -88,8 +88,12 @@ for x in items: params[k] = v state = params.get('state','file') -path = os.path.expanduser(params.get('path', params.get('dest', params.get('name', None)))) -src = os.path.expanduser(params.get('src', None)) +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: + path = os.path.expanduser(src) dest = params.get('dest', None) mode = params.get('mode', None) owner = params.get('owner', None) From 73412513b8129862dafbc230804e8e6045a895e0 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Fri, 20 Apr 2012 08:09:43 -0400 Subject: [PATCH 12/15] Fix bug in src. Should not code this early :) --- file | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/file b/file index b672934e4ee..90d6892601d 100755 --- a/file +++ b/file @@ -93,7 +93,7 @@ if path: path = os.path.expanduser(path) src = params.get('src', None) if src: - path = os.path.expanduser(src) + src = os.path.expanduser(src) dest = params.get('dest', None) mode = params.get('mode', None) owner = params.get('owner', None) From 187ab3f9186e3a80533d6f9641126f5c59c8b635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20N=C3=A9ri?= Date: Sun, 22 Apr 2012 02:48:58 +0200 Subject: [PATCH 13/15] Add apt module support for installing/removing specific version of package This uses standard APT syntax, e.g.: ansible webservers -m apt -a "pkg=nginx=1.1.19-1 state=installed" --- apt | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/apt b/apt index 81b40ea2a08..51191d4fd97 100755 --- a/apt +++ b/apt @@ -63,16 +63,27 @@ def run_apt(command): rc = cmd.returncode return rc, out, err -def package_status(pkgspec, cache): +def package_split(pkgspec): + parts = pkgspec.split('=') + if len(parts) > 1: + return parts[0], parts[1] + else: + return parts[0], None + +def package_status(pkgname, version, cache): try: - pkg = cache[pkgspec] - except: - fail_json(msg="No package matching '%s' is available" % pkgspec) - return (pkg.is_installed, pkg.is_upgradable) + 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): - (installed, upgradable) = package_status(pkgspec, cache) - if (not installed) or (upgrade and upgradable): + 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) rc, out, err = run_apt(cmd) if rc: @@ -82,15 +93,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,7 +121,7 @@ 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') @@ -137,7 +149,12 @@ if update_cache == 'yes': if package == None: exit_json(changed=False) +if package.count('=') > 1: + fail_json(msg='invalid package spec') + if state == 'latest': + if '=' in package: + fail_json(msg='version number inconsistent with state=latest') changed = install(package, cache, upgrade=True) elif state == 'installed': changed = install(package, cache) From 332931b9b86fa7c61b3dc67c01740379619a3c76 Mon Sep 17 00:00:00 2001 From: Stephen Fromm Date: Sat, 21 Apr 2012 23:27:34 -0700 Subject: [PATCH 14/15] Add context=default option to file module This adjusts behavior of file module such that removal of se* option does not revert the file's selinux context to the default. In order to go back to the default context according to the policy, you can use the context=default option. --- file | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/file b/file index 90d6892601d..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] @@ -107,8 +122,16 @@ seuser = params.get('seuser', None) serole = params.get('serole', None) setype = params.get('setype', None) 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) @@ -148,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: From 922899f4bec90716ca4a787771620eeae1332dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20N=C3=A9ri?= Date: Mon, 23 Apr 2012 00:17:07 +0200 Subject: [PATCH 15/15] Add optional "default-release" argument for apt module The value is passed to apt-get's "-t" option. Useful for installing backports, e.g.: ansible webservers -m apt -a "pkg=nginx state=latest default-release=squeeze-backports" --- apt | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/apt b/apt index 51191d4fd97..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") @@ -80,11 +80,13 @@ def package_status(pkgname, version, cache): else: return pkg.is_installed, pkg.is_upgradable -def install(pkgspec, cache, upgrade=False): +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)) @@ -124,10 +126,11 @@ for x in items: (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') @@ -142,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() @@ -155,9 +162,10 @@ if package.count('=') > 1: if state == 'latest': if '=' in package: fail_json(msg='version number inconsistent with state=latest') - changed = install(package, cache, upgrade=True) + 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')