From e9f2cc57259f2c2455401220b81bb7e8a719f1bf Mon Sep 17 00:00:00 2001 From: Silviu Dicu Date: Tue, 22 Jan 2013 15:31:51 -0500 Subject: [PATCH 01/27] ec2 module - registers to ansible_facts key --- ec2_facts | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 ec2_facts diff --git a/ec2_facts b/ec2_facts new file mode 100644 index 00000000000..d04f3208441 --- /dev/null +++ b/ec2_facts @@ -0,0 +1,95 @@ +#!/usr/bin/python -tt +# -*- coding: utf-8 -*- + +# 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: ec2_facts +short_description: Gathers facts about remote hosts within ec2 (aws) +options: {} +description: + - This module fetches data from the metadata servers in ec2 (aws). +notes: + - The module can add parameters to filter ec2_facts based on it. + Some of the facts are not returned ( like mapping of the devices - but + can be add it on). +examples: + - code: ansible all -m ec2_facts --tree /tmp/facts + description: Obtain facts from ec2 metatdata servers. You will need to + run an instance within ec2. +author: Silviu Dicu +''' + +import urllib2 +import socket + +socket.setdefaulttimeout(5) + +class Ec2Metadata(object): + + ec2_metadata_server = 'http://169.254.169.254/latest/meta-data/' + ec2_sshdata_server = 'http://169.254.169.254/latest/meta-data/public-keys/0/openssh-key' + ec2_userdata_server = 'http://169.254.169.254/latest/user-data/' + + def __init__(self, ec2_metadata_server=None, ec2_sshdata_server=None, ec2_userdata_server=None): + self.url_meta = ec2_metadata_server or self.ec2_metadata_server + self.url_user = ec2_userdata_server or self.ec2_userdata_server + self.url_ssh = ec2_sshdata_server or self.ec2_sshdata_server + + def _fetch(self, url): + try: + return urllib2.urlopen(url).read() + except urllib2.HTTPError: + return + except urllib2.URLError: + return + + def run(self, field=None): + data = {} + raw_fields = self._fetch(self.url_meta) + if not raw_fields: + return data + fields = raw_fields.split('\n') + for field in fields: + if field.endswith('/'): continue # deal with this later + field_data = self._fetch(self.url_meta + field) + if field == 'security-groups': + sg_fields = ",".join(field_data.split('\n')) + data['ansible_ec2_%s' % field] = sg_fields + else: + data['ansible_ec2_%s' % field] = field_data + data['ansible_ec2_%s' % 'user-data'] = self._fetch(self.url_user) + data['ensible_ec2_%s' % 'public-keys'] = self._fetch(self.url_ssh) + return data + + +def main(): + ec2_facts = Ec2Metadata().run() + ec2_facts_result = { + "changed" : True, + "ansible_facts" : ec2_facts + } + module = AnsibleModule( + argument_spec = dict() + ) + module.exit_json(**ec2_facts_result) + +# this is magic, see lib/ansible/module_common.py +#<> + +main() From 39cdea0784ce5c72c278d9112dd15a3b750d6f6d Mon Sep 17 00:00:00 2001 From: Silviu Dicu Date: Wed, 23 Jan 2013 09:39:26 -0500 Subject: [PATCH 02/27] ec2 facts moduled - updated --- ec2_facts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ec2_facts b/ec2_facts index d04f3208441..c9f85dd04a3 100644 --- a/ec2_facts +++ b/ec2_facts @@ -25,9 +25,9 @@ options: {} description: - This module fetches data from the metadata servers in ec2 (aws). notes: - - The module can add parameters to filter ec2_facts based on it. + - Parameters to filter on ec2_facts may be added later. Some of the facts are not returned ( like mapping of the devices - but - can be add it on). + may be added later). examples: - code: ansible all -m ec2_facts --tree /tmp/facts description: Obtain facts from ec2 metatdata servers. You will need to @@ -42,14 +42,14 @@ socket.setdefaulttimeout(5) class Ec2Metadata(object): - ec2_metadata_server = 'http://169.254.169.254/latest/meta-data/' - ec2_sshdata_server = 'http://169.254.169.254/latest/meta-data/public-keys/0/openssh-key' - ec2_userdata_server = 'http://169.254.169.254/latest/user-data/' + ec2_metadata_url = 'http://169.254.169.254/latest/meta-data/' + ec2_sshdata_url = 'http://169.254.169.254/latest/meta-data/public-keys/0/openssh-key' + ec2_userdata_url = 'http://169.254.169.254/latest/user-data/' - def __init__(self, ec2_metadata_server=None, ec2_sshdata_server=None, ec2_userdata_server=None): - self.url_meta = ec2_metadata_server or self.ec2_metadata_server - self.url_user = ec2_userdata_server or self.ec2_userdata_server - self.url_ssh = ec2_sshdata_server or self.ec2_sshdata_server + def __init__(self, ec2_metadata_url=None, ec2_sshdata_url=None, ec2_userdata_url=None): + self.url_meta = ec2_metadata_url or self.ec2_metadata_url + self.url_user = ec2_userdata_url or self.ec2_userdata_url + self.url_ssh = ec2_sshdata_url or self.ec2_sshdata_url def _fetch(self, url): try: @@ -81,7 +81,7 @@ class Ec2Metadata(object): def main(): ec2_facts = Ec2Metadata().run() ec2_facts_result = { - "changed" : True, + "changed" : False, "ansible_facts" : ec2_facts } module = AnsibleModule( From 28fa7389b10219392c8db9b3b575a2805bfdab8b Mon Sep 17 00:00:00 2001 From: Shaun Zinck Date: Wed, 23 Jan 2013 22:09:07 -0600 Subject: [PATCH 03/27] added first version of pkgin, just does install, remove (for SmartOS) --- pkgin | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100755 pkgin diff --git a/pkgin b/pkgin new file mode 100755 index 00000000000..0d67341b2ee --- /dev/null +++ b/pkgin @@ -0,0 +1,142 @@ +#!/usr/bin/python -tt +# -*- coding: utf-8 -*- + +# (c) 2013, Shaun Zinck +# Written by Shaun Zinck +# Based on pacman module written by Afterburn +# that was based on apt module written by Matthew Williams +# +# This module 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. +# +# This software 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 this software. If not, see . + + +DOCUMENTATION = ''' +--- +module: pkgin +short_description: Package manager for SmartOS +description: + - Manages SmartOS packages + +version_added: "1.0" +options: + name: + description: + - name of package to install/remove + required: true + + state: + description: + - state of the package installed or absent. + required: false + +author: Shaun Zinck +notes: [] +examples: + - code: "pkgin: name=foo state=installed" + description: install package foo" + - code: "pkgin: name=foo state=absent" + description: remove package foo + - code: "pkgin: name=foo,bar state=absent + description: remove packages foo and bar + +''' + + +import json +import shlex +import os +import sys + +PKGIN_PATH = "/opt/local/bin/pkgin" + +def query_package(module, name, state="installed"): + + if state == "installed": + + rc = os.system("%s list | grep ^%s" % (PKGIN_PATH, name)) + + if rc == 0: + return True + + return False + + +def remove_packages(module, packages): + + remove_c = 0 + # Using a for loop incase of error, we can report the package that failed + for package in packages: + # Query the package first, to see if we even need to remove + if not query_package(module, package): + continue + + rc = os.system("%s -y remove %s" % (PKGIN_PATH, package)) + + if rc != 0: + module.fail_json(msg="failed to remove %s" % (package)) + + remove_c += 1 + + if remove_c > 0: + + module.exit_json(changed=True, msg="removed %s package(s)" % remove_c) + + module.exit_json(changed=False, msg="package(s) already absent") + + +def install_packages(module, packages): + + install_c = 0 + + for package in packages: + if query_package(module, package): + continue + + rc = os.system("%s -y install %s" % (PKGIN_PATH, package)) + + if rc != 0: + module.fail_json(msg="failed to install %s" % (package)) + + install_c += 1 + + if install_c > 0: + module.exit_json(changed=True, msg="installed %s package(s)" % (install_c)) + + module.exit_json(changed=False, msg="package(s) already installed") + + + +def main(): + module = AnsibleModule( + argument_spec = dict( + state = dict(default="installed", choices=["installed","absent"]), + name = dict(aliases=["pkg"], required=True))) + + + if not os.path.exists(PKGIN_PATH): + module.fail_json(msg="cannot find pkgin, looking for %s" % (PKGIN_PATH)) + + p = module.params + + pkgs = p["name"].split(",") + + if p["state"] == "installed": + install_packages(module, pkgs) + + elif p["state"] == "absent": + remove_packages(module, pkgs) + +# this is magic, see lib/ansible/module_common.py +#<> + +main() From 6a879562b65c45673e607038f2b93d3249e584d2 Mon Sep 17 00:00:00 2001 From: Shaun Zinck Date: Wed, 23 Jan 2013 22:12:13 -0600 Subject: [PATCH 04/27] add pkgin to list of package managers in setup --- setup | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/setup b/setup index 07e4cdaf466..f4fa950e3ee 100644 --- a/setup +++ b/setup @@ -82,10 +82,11 @@ class Facts(object): # A list of dicts. If there is a platform with more than one # package manager, put the preferred one last. If there is an # ansible module, use that as the value for the 'name' key. - PKG_MGRS = [ { 'path' : '/usr/bin/yum', 'name' : 'yum' }, - { 'path' : '/usr/bin/apt-get', 'name' : 'apt' }, - { 'path' : '/usr/bin/zypper', 'name' : 'zypper' }, - { 'path' : '/usr/bin/pacman', 'name' : 'pacman' } ] + PKG_MGRS = [ { 'path' : '/usr/bin/yum', 'name' : 'yum' }, + { 'path' : '/usr/bin/apt-get', 'name' : 'apt' }, + { 'path' : '/usr/bin/zypper', 'name' : 'zypper' }, + { 'path' : '/usr/bin/pacman', 'name' : 'pacman' }, + { 'path' : '/opt/local/bin/pkgin', 'name' : 'pkgin' } ] def __init__(self): self.facts = {} From 826a756e6195a3ea82a59a3cb587ab0e077160b6 Mon Sep 17 00:00:00 2001 From: Silviu Dicu Date: Thu, 24 Jan 2013 19:14:32 -0500 Subject: [PATCH 05/27] ec2 facts module - updated as per comments --- ec2_facts | 80 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 25 deletions(-) diff --git a/ec2_facts b/ec2_facts index c9f85dd04a3..40984f44c7d 100644 --- a/ec2_facts +++ b/ec2_facts @@ -24,32 +24,36 @@ short_description: Gathers facts about remote hosts within ec2 (aws) options: {} description: - This module fetches data from the metadata servers in ec2 (aws). + Eucalyptus cloud provides a similar service and this module should + work this cloud provider as well. notes: - Parameters to filter on ec2_facts may be added later. - Some of the facts are not returned ( like mapping of the devices - but - may be added later). examples: - code: ansible all -m ec2_facts --tree /tmp/facts description: Obtain facts from ec2 metatdata servers. You will need to run an instance within ec2. -author: Silviu Dicu + +author: Silviu Dicu: silviudicu@gmail.com ''' import urllib2 import socket +import re socket.setdefaulttimeout(5) class Ec2Metadata(object): - ec2_metadata_url = 'http://169.254.169.254/latest/meta-data/' - ec2_sshdata_url = 'http://169.254.169.254/latest/meta-data/public-keys/0/openssh-key' - ec2_userdata_url = 'http://169.254.169.254/latest/user-data/' + ec2_metadata_uri = 'http://169.254.169.254/latest/meta-data/' + ec2_sshdata_uri = 'http://169.254.169.254/latest/meta-data/public-keys/0/openssh-key' + ec2_userdata_uri = 'http://169.254.169.254/latest/user-data/' - def __init__(self, ec2_metadata_url=None, ec2_sshdata_url=None, ec2_userdata_url=None): - self.url_meta = ec2_metadata_url or self.ec2_metadata_url - self.url_user = ec2_userdata_url or self.ec2_userdata_url - self.url_ssh = ec2_sshdata_url or self.ec2_sshdata_url + def __init__(self, ec2_metadata_uri=None, ec2_sshdata_uri=None, ec2_userdata_uri=None): + self.uri_meta = ec2_metadata_uri or self.ec2_metadata_uri + self.uri_user = ec2_userdata_uri or self.ec2_userdata_uri + self.uri_ssh = ec2_sshdata_uri or self.ec2_sshdata_uri + self._data = {} + self._prefix = 'ansible_ec2_%s' def _fetch(self, url): try: @@ -59,22 +63,48 @@ class Ec2Metadata(object): except urllib2.URLError: return - def run(self, field=None): - data = {} - raw_fields = self._fetch(self.url_meta) - if not raw_fields: - return data - fields = raw_fields.split('\n') - for field in fields: - if field.endswith('/'): continue # deal with this later - field_data = self._fetch(self.url_meta + field) - if field == 'security-groups': - sg_fields = ",".join(field_data.split('\n')) - data['ansible_ec2_%s' % field] = sg_fields + def _mangle_fields(self, fields, uri, filter_patterns=['public-keys-0']): + new_fields = {} + for key, value in fields.iteritems(): + split_fields = key[len(uri):].split('/') + if len(split_fields) > 1 and split_fields[1]: + new_key = "-".join(split_fields) + new_fields[self._prefix % new_key] = value else: - data['ansible_ec2_%s' % field] = field_data - data['ansible_ec2_%s' % 'user-data'] = self._fetch(self.url_user) - data['ensible_ec2_%s' % 'public-keys'] = self._fetch(self.url_ssh) + new_key = "".join(split_fields) + new_fields[self._prefix % new_key] = value + for pattern in filter_patterns: + for key in new_fields.keys(): + match = re.search(pattern, key) + if match: new_fields.pop(key) + return new_fields + + def fetch(self, uri, recurse=True): + raw_subfields = self._fetch(uri) + if not raw_subfields: + return + subfields = raw_subfields.split('\n') + for field in subfields: + if field.endswith('/') and recurse: + self.fetch(uri + field) + if uri.endswith('/'): + new_uri = uri + field + else: + new_uri = uri + '/' + field + if new_uri not in self._data and not new_uri.endswith('/'): + content = self._fetch(new_uri) + if field == 'security-groups': + sg_fields = ",".join(content.split('\n')) + self._data['%s' % (new_uri)] = sg_fields + else: + self._data['%s' % (new_uri)] = content + + def run(self): + self.fetch(self.uri_meta) # populate _data + data = self._mangle_fields(self._data, + self.uri_meta) + data[self._prefix % 'user-data'] = self._fetch(self.uri_user) + data[self._prefix % 'public-key'] = self._fetch(self.uri_ssh) return data From dd67e5c36f248765bbf165952750ead5d4d39177 Mon Sep 17 00:00:00 2001 From: Shaun Zinck Date: Thu, 24 Jan 2013 19:58:31 -0600 Subject: [PATCH 06/27] pkgin: change install/remove to not use return code of pkgin pkgin always returns 0 so can't be used to tell if the install or remove worked. Instead this just queries the installed packages after performing an operation. --- pkgin | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgin b/pkgin index 0d67341b2ee..acbc55e0461 100755 --- a/pkgin +++ b/pkgin @@ -82,7 +82,7 @@ def remove_packages(module, packages): rc = os.system("%s -y remove %s" % (PKGIN_PATH, package)) - if rc != 0: + if query_package(module, package): module.fail_json(msg="failed to remove %s" % (package)) remove_c += 1 @@ -104,7 +104,7 @@ def install_packages(module, packages): rc = os.system("%s -y install %s" % (PKGIN_PATH, package)) - if rc != 0: + if not query_package(module, package): module.fail_json(msg="failed to install %s" % (package)) install_c += 1 From 5d1536a7c509fd3ce292442e2dcf45d42a68e4f1 Mon Sep 17 00:00:00 2001 From: Juha Litola Date: Fri, 25 Jan 2013 13:29:39 +0200 Subject: [PATCH 07/27] Fixed add_key stalling indefinitely, and test code leaking into production setting --- apt_key | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apt_key b/apt_key index dcc4d12717f..8e521a4751a 100644 --- a/apt_key +++ b/apt_key @@ -105,7 +105,7 @@ def download_key(url): def add_key(key): - return call("apt-key add -", shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE) + p = Popen("apt-key add -", shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE) (_, _) = p.communicate(key) return p.returncode == 0 @@ -163,12 +163,12 @@ if 'ANSIBLE_TEST_APT_KEY' in environ: return extra -if environ.get('ANSIBLE_TEST_APT_KEY') == 'none': - def key_present(key_id): - return False -else: - def key_present(key_id): - return key_id == environ['ANSIBLE_TEST_APT_KEY'] + if environ.get('ANSIBLE_TEST_APT_KEY') == 'none': + def key_present(key_id): + return False + else: + def key_present(key_id): + return key_id == environ['ANSIBLE_TEST_APT_KEY'] def main(): From 6e2d401a01beab3e36ac940549ca5b2c6b62b160 Mon Sep 17 00:00:00 2001 From: Juha Litola Date: Fri, 25 Jan 2013 17:10:04 +0200 Subject: [PATCH 08/27] Removed apt_key tests, as they didn't test the real functionality. Tests used heavily mocked version of the apt_key code, which meant that it didn't properly test real life scenario. --- apt_key | 50 -------------------------------------------------- 1 file changed, 50 deletions(-) diff --git a/apt_key b/apt_key index 8e521a4751a..14d1b734fda 100644 --- a/apt_key +++ b/apt_key @@ -121,56 +121,6 @@ def return_values(tb=False): else: return {} - -# use cues from the environment to mock out functions for testing -if 'ANSIBLE_TEST_APT_KEY' in environ: - orig_download_key = download_key - KEY_ADDED=0 - KEY_REMOVED=0 - KEY_DOWNLOADED=0 - - - def download_key(url): - global KEY_DOWNLOADED - KEY_DOWNLOADED += 1 - return orig_download_key(url) - - - def find_missing_binaries(): - return [] - - - def add_key(key): - global KEY_ADDED - KEY_ADDED += 1 - return True - - - def remove_key(key_id): - global KEY_REMOVED - KEY_REMOVED += 1 - return True - - - def return_values(tb=False): - extra = dict( - added=KEY_ADDED, - removed=KEY_REMOVED, - downloaded=KEY_DOWNLOADED - ) - if tb: - extra['exception'] = format_exc() - return extra - - - if environ.get('ANSIBLE_TEST_APT_KEY') == 'none': - def key_present(key_id): - return False - else: - def key_present(key_id): - return key_id == environ['ANSIBLE_TEST_APT_KEY'] - - def main(): module = AnsibleModule( argument_spec=dict( From a25575cee92d251b85f7a1248a20368b02c078cd Mon Sep 17 00:00:00 2001 From: Shaun Zinck Date: Fri, 25 Jan 2013 16:48:58 -0600 Subject: [PATCH 09/27] pkgin: use module.run_command to run stuff This also fixes an issue where some console output for packages I was installing was creating invalid JSON because it contained single-quotes. --- pkgin | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkgin b/pkgin index acbc55e0461..02fac16f27f 100755 --- a/pkgin +++ b/pkgin @@ -63,7 +63,7 @@ def query_package(module, name, state="installed"): if state == "installed": - rc = os.system("%s list | grep ^%s" % (PKGIN_PATH, name)) + (rc, stdout, stderr) = module.run_command("%s list | grep ^%s" % (PKGIN_PATH, name)) if rc == 0: return True @@ -80,7 +80,7 @@ def remove_packages(module, packages): if not query_package(module, package): continue - rc = os.system("%s -y remove %s" % (PKGIN_PATH, package)) + module.run_command("%s -y remove %s" % (PKGIN_PATH, package)) if query_package(module, package): module.fail_json(msg="failed to remove %s" % (package)) @@ -102,7 +102,7 @@ def install_packages(module, packages): if query_package(module, package): continue - rc = os.system("%s -y install %s" % (PKGIN_PATH, package)) + module.run_command("%s -y install %s" % (PKGIN_PATH, package)) if not query_package(module, package): module.fail_json(msg="failed to install %s" % (package)) From 835d7e9bc8cfb6a171db4794c8689ad3f7507927 Mon Sep 17 00:00:00 2001 From: Shaun Zinck Date: Fri, 25 Jan 2013 16:58:29 -0600 Subject: [PATCH 10/27] pkgin: add stdout to error message when shell commands fail --- pkgin | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkgin b/pkgin index 02fac16f27f..eec541135b5 100755 --- a/pkgin +++ b/pkgin @@ -63,7 +63,7 @@ def query_package(module, name, state="installed"): if state == "installed": - (rc, stdout, stderr) = module.run_command("%s list | grep ^%s" % (PKGIN_PATH, name)) + rc, out, err = module.run_command("%s list | grep ^%s" % (PKGIN_PATH, name)) if rc == 0: return True @@ -80,10 +80,10 @@ def remove_packages(module, packages): if not query_package(module, package): continue - module.run_command("%s -y remove %s" % (PKGIN_PATH, package)) + rc, out, err = module.run_command("%s -y remove %s" % (PKGIN_PATH, package)) if query_package(module, package): - module.fail_json(msg="failed to remove %s" % (package)) + module.fail_json(msg="failed to remove %s: %s" % (package, out)) remove_c += 1 @@ -102,10 +102,10 @@ def install_packages(module, packages): if query_package(module, package): continue - module.run_command("%s -y install %s" % (PKGIN_PATH, package)) + rc, out, err = module.run_command("%s -y install %s" % (PKGIN_PATH, package)) if not query_package(module, package): - module.fail_json(msg="failed to install %s" % (package)) + module.fail_json(msg="failed to install %s: %s" % (package, out)) install_c += 1 From 13bb88ef97dfeb01acebecfbf8c1602b55a602c1 Mon Sep 17 00:00:00 2001 From: igor Date: Thu, 24 Jan 2013 16:16:23 +0100 Subject: [PATCH 11/27] add support for user:password syntax in urls to get_url --- get_url | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/get_url b/get_url index 933c4445b2b..1cbc4a12a8c 100644 --- a/get_url +++ b/get_url @@ -100,6 +100,29 @@ def url_do_get(module, url, dest): USERAGENT = 'ansible-httpget' info = dict(url=url, dest=dest) r = None + parsed = urlparse.urlparse(url) + if '@' in parsed.netloc: + credentials = parsed.netloc.split('@')[0] + if ':' in credentials: + username, password = credentials.split(':') + netloc = parsed.netloc.split('@')[1] + parsed = list(parsed) + parsed[1] = netloc + + passman = urllib2.HTTPPasswordMgrWithDefaultRealm() + # this creates a password manager + passman.add_password(None, netloc, username, password) + # because we have put None at the start it will always + # use this username/password combination for urls + # for which `theurl` is a super-url + + authhandler = urllib2.HTTPBasicAuthHandler(passman) + # create the AuthHandler + + opener = urllib2.build_opener(authhandler) + urllib2.install_opener(opener) + #reconstruct url without credentials + url = urlparse.urlunparse(parsed) request = urllib2.Request(url) request.add_header('User-agent', USERAGENT) From 75bf2f7a37eb621ad62fc70fd6601f58ab12bbec Mon Sep 17 00:00:00 2001 From: igor Date: Thu, 24 Jan 2013 21:19:14 +0100 Subject: [PATCH 12/27] minor doc changes --- get_url | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/get_url b/get_url index 1cbc4a12a8c..4e896070c96 100644 --- a/get_url +++ b/get_url @@ -35,7 +35,8 @@ version_added: "0.6" options: url: description: - - HTTP, HTTPS, or FTP URL + - HTTP, HTTPS, or FTP URL. + (http|https|ftp)://[user[:pass]]@host.domain[:port]/path required: true default: null aliases: [] @@ -63,18 +64,18 @@ examples: - code: "get_url: url=http://example.com/path/file.conf dest=/etc/foo.conf mode=0440" description: "Example from Ansible Playbooks" notes: - - This module doesn't yet support configuration for proxies or passwords. + - This module doesn't yet support configuration for proxies. # informational: requirements for nodes requirements: [ urllib2, urlparse ] author: Jan-Piet Mens ''' -HAS_URLLIB2=True +HAS_URLLIB2 = True try: import urllib2 except ImportError: - HAS_URLLIB2=False -HAS_URLPARSE=True + HAS_URLLIB2 = False +HAS_URLPARSE = True try: import urlparse @@ -255,8 +256,8 @@ def main(): # Mission complete module.exit_json(url=url, dest=dest, src=tmpsrc, md5sum=md5sum_src, - changed=changed, msg=info.get('msg',''), - daisychain="file", daisychain_args=info.get('daisychain_args','')) + changed=changed, msg=info.get('msg', ''), + daisychain="file", daisychain_args=info.get('daisychain_args', '')) # this is magic, see lib/ansible/module_common.py #<> From db4be5235859cdeb75ff7eefdc29b56863c64c26 Mon Sep 17 00:00:00 2001 From: Lester Wade Date: Fri, 25 Jan 2013 15:02:53 +0000 Subject: [PATCH 13/27] Update library/ec2 I've uncommented and added a very little supporting stuff based on skvidal's work to allow us to launch more than one instance. --- ec2 | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/ec2 b/ec2 index 034da936af4..4a79bf0b2ad 100644 --- a/ec2 +++ b/ec2 @@ -66,7 +66,7 @@ options: aliases: [] ec2_url: description: - - url to use to connect to ec2 or your cloud (for example U(https://ec2.amazonaws.com) when using Amazon ec2 directly and not Eucalyptus) + - url to use to connect to ec2 or your Eucalyptus cloud (for example (https://ec2.amazonaws.com) when using Amazon ec2 directly and not Eucalyptus) required: False default: null aliases: [] @@ -82,6 +82,12 @@ options: required: False default: null aliases: [] + count: + description: + - number of instances to launch + required: False + default: 1 + aliases: [] user_data: version_added: "0.9" description: @@ -90,10 +96,10 @@ options: default: null aliases: [] examples: - - code: "local_action: ec2 keypair=admin instance_type=m1.large image=emi-40603AD1 wait=true group=webserver" + - code: "local_action: ec2 keypair=admin instance_type=m1.large image=emi-40603AD1 wait=true group=webserver count=3" description: "Examples from Ansible Playbooks" requirements: [ "boto" ] -author: Seth Vidal, Tim Gerla +author: Seth Vidal, Tim Gerla, Lester Wade ''' import sys @@ -113,7 +119,7 @@ def main(): instance_type = dict(aliases=['type']), image = dict(required=True), kernel = dict(), - #count = dict(default='1'), # maybe someday + count = dict(default='1'), ramdisk = dict(), wait = dict(choices=BOOLEANS, default=False), ec2_url = dict(aliases=['EC2_URL']), @@ -127,7 +133,7 @@ def main(): group = module.params.get('group') instance_type = module.params.get('instance_type') image = module.params.get('image') - #count = module.params.get('count') + count = module.params.get('count') kernel = module.params.get('kernel') ramdisk = module.params.get('ramdisk') wait = module.params.get('wait') @@ -148,10 +154,13 @@ def main(): ec2 = boto.connect_ec2_endpoint(ec2_url, ec2_access_key, ec2_secret_key) else: # otherwise it's Amazon. ec2 = boto.connect_ec2(ec2_access_key, ec2_secret_key) + +# Note min_count is static in value. Since we aren't interested in addressing an autoscaling use-case. +# Autoscaling means more instances are launched on a triggered event, so this is post-play/run stuff. try: res = ec2.run_instances(image, key_name = key_name, - min_count = 1, max_count = 1, + min_count = 1, max_count = count, security_groups = [group], instance_type = instance_type, kernel_id = kernel, @@ -171,9 +180,8 @@ def main(): res_list = res.connection.get_all_instances(instids) this_res = res_list[0] num_running = len([ i for i in this_res.instances if i.state=='running' ]) - time.sleep(2) + time.sleep(5) - # there's only one - but maybe one day there could be more instances = [] for inst in this_res.instances: d = { From c339dd146e83df997c5fa9d4e05275143b802095 Mon Sep 17 00:00:00 2001 From: Lester Wade Date: Fri, 25 Jan 2013 16:48:27 +0000 Subject: [PATCH 14/27] Update library/ec2 bumping up min_count value. --- ec2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ec2 b/ec2 index 4a79bf0b2ad..1fd7b098ddc 100644 --- a/ec2 +++ b/ec2 @@ -160,7 +160,7 @@ def main(): try: res = ec2.run_instances(image, key_name = key_name, - min_count = 1, max_count = count, + min_count = count, max_count = count, security_groups = [group], instance_type = instance_type, kernel_id = kernel, From b6cbd8d26814da57a7afcd8fe961eff2b83df902 Mon Sep 17 00:00:00 2001 From: Yeukhon Wong Date: Fri, 25 Jan 2013 22:51:20 -0500 Subject: [PATCH 15/27] Added hg module to the core. --- hg | 267 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 hg diff --git a/hg b/hg new file mode 100644 index 00000000000..04d74c44fde --- /dev/null +++ b/hg @@ -0,0 +1,267 @@ +#!/usr/bin/python +#-*- coding: utf-8 -*- + +# (c) 2013, Yeukhon Wong +# +# This module was originally inspired by Brad Olson's ansible-module-mercurial +# . +# +# 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 os +import shutil +import ConfigParser +from subprocess import Popen, PIPE + +DOCUMENTATION = ''' +--- +module: hg +short_description: Manages Mercurial (hg) repositories. +description: + - Manages Mercurial (hg) repositories. Supports SSH, HTTP/S and local address. +version_added: "1.0" +author: Yeukhon Wong +options: + repo: + description: + - The repository location. + required: true + default: null + dest: + description: + - Absolute path of where the repository should be cloned to. + required: true + default: null + state: + description: + - C(hg clone) is performed when state is set to C(present). C(hg pull) and C(hg update) is + performed when state is set to C(latest). If you want the latest copy of the repository, + just rely on C(present). C(latest) assumes the repository is already on disk. + required: false + default: present + choices: [ "present", "absent", "latest" ] + revision: + description: + - Equivalent C(-r) option in hg command, which can either be a changeset number or a branch + name. + required: false + default: "default" + force: + description: + - Whether to discard uncommitted changes and remove untracked files or not. Basically, it + combines C(hg up -C) and C(hg purge). + required: false + default: "yes" + choices: [ "yes", "no" ] + +examples: + - code: "hg: repo=https://bitbucket.org/user/repo_name dest=/home/user/repo_name" + description: Clone the default branch of repo_name. + + - code: "hg: repo=https://bitbucket.org/user/repo_name dest=/home/user/repo_name force=yes state=latest" + description: Ensure the repository at dest is latest and discard any uncommitted and/or untracked files. + +notes: + - If the task seems to be hanging, first verify remote host is in C(known_hosts). + SSH will prompt user to authorize the first contact with a remote host. One solution is to add + C(StrictHostKeyChecking no) in C(.ssh/config) which will accept and authorize the connection + on behalf of the user. However, if you run as a different user such as setting sudo to True), + for example, root will not look at the user .ssh/config setting. + +requirements: [ ] +''' + +class HgError(Exception): + """ Custom exception class to report hg command error. """ + + def __init__(self, msg, stderr=''): + self.msg = msg + \ + "\n\nExtra information on this error: \n" + \ + stderr + def __str__(self): + return self.msg + +def _set_hgrc(hgrc, vals): + # val is a list of triple-tuple of the form [(section, option, value),...] + parser = ConfigParser.SafeConfigParser() + parser.read(hgrc) + + for each in vals: + section,option, value = each + if not parser.has_section(section): + parser.add_section(section) + parser.set(section, option, value) + + f = open(hgrc, 'w') + parser.write(f) + f.close() + +def _undo_hgrc(hgrc, vals): + parser = ConfigParser.SafeConfigParser() + parser.read(hgrc) + + for each in vals: + section, option, value = each + if parser.has_section(section): + parser.remove_option(section, option) + + f = open(hgrc, 'w') + parser.write(f) + f.close() + +def _hg_command(args_list): + cmd = ['hg'] + args_list + p = Popen(cmd, stdout=PIPE, stderr=PIPE) + out, err = p.communicate() + return out, err, p.returncode + +def _hg_discard(dest): + out, err, code = _hg_command(['up', '-C', '-R', dest]) + if code != 0: + raise HgError(err) + +def _hg_purge(dest): + hgrc = os.path.join(dest, '.hg/hgrc') + purge_option = [('extensions', 'purge', '')] + _set_hgrc(hgrc, purge_option) + out, err, code = _hg_command(['purge', '-R', dest]) + if code == 0: + _undo_hgrc(hgrc, purge_option) + else: + raise HgError(err) + +def _hg_verify(dest): + error1 = "hg verify failed." + error2 = "{dest} is not a repository.".format(dest=dest) + + out, err, code = _hg_command(['verify', '-R', dest]) + if code == 1: + raise HgError(error1, stderr=err) + elif code == 255: + raise HgError(error2, stderr=err) + elif code == 0: + return True + +def _post_op_hg_revision_check(dest, revision): + """ + Verify the tip is the same as `revision`. + + This function is usually called after some hg operations + such as `clone`. However, this check is skipped if `revision` + is the string `default` since it will result an error. + Instead, pull is performed. + + """ + + err1 = "Unable to perform hg tip." + err2 = "tip is different from %s. See below for extended summary." % revision + + if revision == 'default': + out, err, code = _hg_command(['pull', '-R', dest]) + if "no changes found" in out: + return True + else: + raise HgError(err2, stderr=out) + else: + out, err, code = _hg_command(['tip', '-R', dest]) + if revision in out: # revision should be part of the output (changeset: $revision ...) + return True + else: + if code != 0: # something went wrong with hg tip + raise HgError(err1, stderr=err) + else: # hg tip is fine, but tip != revision + raise HgError(err2, stderr=out) + +def force_and_clean(dest): + _hg_discard(dest) + _hg_purge(dest) + +def pull_and_update(repo, dest, revision, force): + if force == 'yes': + force_and_clean(dest) + + if _hg_verify(dest): + cmd1 = ['pull', '-R', dest, '-r', revision] + out, err, code = _hg_command(cmd1) + + if code == 1: + raise HgError("Unable to perform pull on %s" % dest, stderr=err) + elif code == 0: + cmd2 = ['update', '-R', dest, '-r', revision] + out, err, code = _hg_command(cmd2) + if code == 1: + raise HgError("There are unresolved files in %s" % dest, stderr=err) + elif code == 0: + # so far pull and update seems to be working, check revision and $revision are equal + _post_op_hg_revision_check(dest, revision) + return True + # when code aren't 1 or 0 in either command + raise HgError("", stderr=err) + +def clone(repo, dest, revision, force): + if os.path.exists(dest): + if _hg_verify(dest): # make sure it's a real repo + if _post_op_hg_revision_check(dest, revision): # make sure revision and $revision are equal + if force == 'yes': + force_and_clean(dest) + return False + + cmd = ['clone', repo, dest, '-r', revision] + out, err, code = _hg_command(cmd) + if code == 0: + _hg_verify(dest) + _post_op_hg_revision_check(dest, revision) + return True + else: + raise HgError(err, stderr='') + +def main(): + module = AnsibleModule( + argument_spec = dict( + repo = dict(required=True), + dest = dict(required=True), + state = dict(default='present', choices=['present', 'absent', 'latest']), + revision = dict(default="default"), + force = dict(default='yes', choices=['yes', 'no']), + ), + ) + repo = module.params['repo'] + state = module.params['state'] + dest = module.params['dest'] + revision = module.params['revision'] + force = module.params['force'] + + try: + if state == 'absent': + if not os.path.exists(dest): + shutil.rmtree(dest) + changed = True + elif state == 'present': + changed = clone(repo, dest, revision, force) + elif state == 'latest': + changed = pull_and_update(repo, dest, revision, force) + + module.exit_json(dest=dest, changed=changed) + #except HgError as e: + # module.fail_json(msg=str(e), params=module.params) + #except IOError as e: + # module.fail_json(msg=str(e), params=module.params) + except Exception as e: + module.fail_json(msg=str(e), params=module.params) + +# include magic from lib/ansible/module_common.py +#<> +main() From 18001e0c9ce802788118270a4dea50ac882ec51d Mon Sep 17 00:00:00 2001 From: Lester Wade Date: Sat, 26 Jan 2013 11:26:51 +0000 Subject: [PATCH 16/27] Update library/ec2 updated indentation and redundant comment. --- ec2 | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ec2 b/ec2 index 1fd7b098ddc..03d1cbc6ffa 100644 --- a/ec2 +++ b/ec2 @@ -155,9 +155,8 @@ def main(): else: # otherwise it's Amazon. ec2 = boto.connect_ec2(ec2_access_key, ec2_secret_key) -# Note min_count is static in value. Since we aren't interested in addressing an autoscaling use-case. -# Autoscaling means more instances are launched on a triggered event, so this is post-play/run stuff. - + # Both min_count and max_count equal count parameter. This means the launch request is explicit (we want count, or fail) in how many instances we want. + try: res = ec2.run_instances(image, key_name = key_name, min_count = count, max_count = count, From 148fe8e74470640a73ef9e9ffb3f667d0bda5f58 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Sat, 26 Jan 2013 12:18:41 -0500 Subject: [PATCH 17/27] Standardize the hg command execution around our run command function. --- hg | 66 ++++++++++++++++++++++++++++---------------------------------- 1 file changed, 30 insertions(+), 36 deletions(-) diff --git a/hg b/hg index 04d74c44fde..2559d5a5d2b 100644 --- a/hg +++ b/hg @@ -122,32 +122,30 @@ def _undo_hgrc(hgrc, vals): parser.write(f) f.close() -def _hg_command(args_list): - cmd = ['hg'] + args_list - p = Popen(cmd, stdout=PIPE, stderr=PIPE) - out, err = p.communicate() - return out, err, p.returncode +def _hg_command(module, args_list): + (rc, out, err) = module.run_command(['hg'] + args_list) + return (out, err, rc) -def _hg_discard(dest): - out, err, code = _hg_command(['up', '-C', '-R', dest]) +def _hg_discard(module, dest): + out, err, code = _hg_command(module, ['up', '-C', '-R', dest]) if code != 0: raise HgError(err) -def _hg_purge(dest): +def _hg_purge(module, dest): hgrc = os.path.join(dest, '.hg/hgrc') purge_option = [('extensions', 'purge', '')] _set_hgrc(hgrc, purge_option) - out, err, code = _hg_command(['purge', '-R', dest]) + out, err, code = _hg_command(module, ['purge', '-R', dest]) if code == 0: _undo_hgrc(hgrc, purge_option) else: raise HgError(err) -def _hg_verify(dest): +def _hg_verify(module, dest): error1 = "hg verify failed." error2 = "{dest} is not a repository.".format(dest=dest) - out, err, code = _hg_command(['verify', '-R', dest]) + out, err, code = _hg_command(module, ['verify', '-R', dest]) if code == 1: raise HgError(error1, stderr=err) elif code == 255: @@ -155,7 +153,7 @@ def _hg_verify(dest): elif code == 0: return True -def _post_op_hg_revision_check(dest, revision): +def _post_op_hg_revision_check(module, dest, revision): """ Verify the tip is the same as `revision`. @@ -170,13 +168,13 @@ def _post_op_hg_revision_check(dest, revision): err2 = "tip is different from %s. See below for extended summary." % revision if revision == 'default': - out, err, code = _hg_command(['pull', '-R', dest]) + out, err, code = _hg_command(module, ['pull', '-R', dest]) if "no changes found" in out: return True else: raise HgError(err2, stderr=out) else: - out, err, code = _hg_command(['tip', '-R', dest]) + out, err, code = _hg_command(module, ['tip', '-R', dest]) if revision in out: # revision should be part of the output (changeset: $revision ...) return True else: @@ -185,45 +183,45 @@ def _post_op_hg_revision_check(dest, revision): else: # hg tip is fine, but tip != revision raise HgError(err2, stderr=out) -def force_and_clean(dest): - _hg_discard(dest) - _hg_purge(dest) +def force_and_clean(module, dest): + _hg_discard(module, dest) + _hg_purge(module, dest) -def pull_and_update(repo, dest, revision, force): +def pull_and_update(module, repo, dest, revision, force): if force == 'yes': - force_and_clean(dest) + force_and_clean(module, dest) - if _hg_verify(dest): + if _hg_verify(module, dest): cmd1 = ['pull', '-R', dest, '-r', revision] - out, err, code = _hg_command(cmd1) + out, err, code = _hg_command(module, cmd1) if code == 1: raise HgError("Unable to perform pull on %s" % dest, stderr=err) elif code == 0: cmd2 = ['update', '-R', dest, '-r', revision] - out, err, code = _hg_command(cmd2) + out, err, code = _hg_command(module, cmd2) if code == 1: raise HgError("There are unresolved files in %s" % dest, stderr=err) elif code == 0: # so far pull and update seems to be working, check revision and $revision are equal - _post_op_hg_revision_check(dest, revision) + _post_op_hg_revision_check(module, dest, revision) return True # when code aren't 1 or 0 in either command raise HgError("", stderr=err) -def clone(repo, dest, revision, force): +def clone(module, repo, dest, revision, force): if os.path.exists(dest): - if _hg_verify(dest): # make sure it's a real repo - if _post_op_hg_revision_check(dest, revision): # make sure revision and $revision are equal + if _hg_verify(module, dest): # make sure it's a real repo + if _post_op_hg_revision_check(module, dest, revision): # make sure revision and $revision are equal if force == 'yes': - force_and_clean(dest) + force_and_clean(module, dest) return False cmd = ['clone', repo, dest, '-r', revision] - out, err, code = _hg_command(cmd) + out, err, code = _hg_command(module, cmd) if code == 0: - _hg_verify(dest) - _post_op_hg_revision_check(dest, revision) + _hg_verify(module, dest) + _post_op_hg_revision_check(module, dest, revision) return True else: raise HgError(err, stderr='') @@ -250,15 +248,11 @@ def main(): shutil.rmtree(dest) changed = True elif state == 'present': - changed = clone(repo, dest, revision, force) + changed = clone(module, repo, dest, revision, force) elif state == 'latest': - changed = pull_and_update(repo, dest, revision, force) + changed = pull_and_update(module, repo, dest, revision, force) module.exit_json(dest=dest, changed=changed) - #except HgError as e: - # module.fail_json(msg=str(e), params=module.params) - #except IOError as e: - # module.fail_json(msg=str(e), params=module.params) except Exception as e: module.fail_json(msg=str(e), params=module.params) From 4905527e2e7ffde17d6f3da73bf4a7d3567a0e66 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Sat, 26 Jan 2013 12:34:30 -0500 Subject: [PATCH 18/27] Fixup ec2_facts docs parsing --- ec2_facts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/ec2_facts b/ec2_facts index 40984f44c7d..ef1628fac87 100644 --- a/ec2_facts +++ b/ec2_facts @@ -16,8 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . - -DOCUMENTATION = ''' +DOCUMENTATION=""" --- module: ec2_facts short_description: Gathers facts about remote hosts within ec2 (aws) @@ -29,12 +28,10 @@ description: notes: - Parameters to filter on ec2_facts may be added later. examples: - - code: ansible all -m ec2_facts --tree /tmp/facts - description: Obtain facts from ec2 metatdata servers. You will need to - run an instance within ec2. - -author: Silviu Dicu: silviudicu@gmail.com -''' + - code: ansible all -m ec2_facts + description: Obtain facts from ec2 metatdata servers. You will need to run an instance within ec2. +author: "Silviu Dicu " +""" import urllib2 import socket From 7c621a987bcf9803d8c0e6c05cd60632de01d62f Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Sat, 26 Jan 2013 12:38:08 -0500 Subject: [PATCH 19/27] fix documentation formatting for get_url --- get_url | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/get_url b/get_url index 4e896070c96..8d6e85f0d8b 100644 --- a/get_url +++ b/get_url @@ -35,8 +35,7 @@ version_added: "0.6" options: url: description: - - HTTP, HTTPS, or FTP URL. - (http|https|ftp)://[user[:pass]]@host.domain[:port]/path + - HTTP, HTTPS, or FTP URL in the form (http|https|ftp)://[user[:pass]]@host.domain[:port]/path required: true default: null aliases: [] From ae176d531099d3d66cbb4e5c5f4a3242b4edb8ef Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Sat, 26 Jan 2013 12:44:43 -0500 Subject: [PATCH 20/27] Fix documentation YAML for pkgin module --- pkgin | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/pkgin b/pkgin index eec541135b5..05891d3f634 100755 --- a/pkgin +++ b/pkgin @@ -26,29 +26,27 @@ module: pkgin short_description: Package manager for SmartOS description: - Manages SmartOS packages - version_added: "1.0" options: name: description: - name of package to install/remove required: true - state: description: - - state of the package installed or absent. + - state of the package + choices: [ 'present', 'absent' ] required: false - + default: present author: Shaun Zinck notes: [] examples: - - code: "pkgin: name=foo state=installed" + - code: "pkgin: name=foo state=present" description: install package foo" - code: "pkgin: name=foo state=absent" description: remove package foo - - code: "pkgin: name=foo,bar state=absent + - code: "pkgin: name=foo,bar state=absent" description: remove packages foo and bar - ''' @@ -59,9 +57,9 @@ import sys PKGIN_PATH = "/opt/local/bin/pkgin" -def query_package(module, name, state="installed"): +def query_package(module, name, state="present"): - if state == "installed": + if state == "present": rc, out, err = module.run_command("%s list | grep ^%s" % (PKGIN_PATH, name)) @@ -110,16 +108,16 @@ def install_packages(module, packages): install_c += 1 if install_c > 0: - module.exit_json(changed=True, msg="installed %s package(s)" % (install_c)) + module.exit_json(changed=True, msg="present %s package(s)" % (install_c)) - module.exit_json(changed=False, msg="package(s) already installed") + module.exit_json(changed=False, msg="package(s) already present") def main(): module = AnsibleModule( argument_spec = dict( - state = dict(default="installed", choices=["installed","absent"]), + state = dict(default="present", choices=["present","absent"]), name = dict(aliases=["pkg"], required=True))) @@ -130,7 +128,7 @@ def main(): pkgs = p["name"].split(",") - if p["state"] == "installed": + if p["state"] == "present": install_packages(module, pkgs) elif p["state"] == "absent": From cc53b8ae955e35ba4559149ab821f6920a925c9c Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Sat, 26 Jan 2013 12:58:12 -0500 Subject: [PATCH 21/27] Tweak sysctl docs so they'll web-render --- sysctl | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/sysctl b/sysctl index b49e7dcd217..6231f240e3c 100644 --- a/sysctl +++ b/sysctl @@ -24,47 +24,43 @@ DOCUMENTATION = ''' module: sysctl short_description: Permit to handle sysctl.conf entries description: - - This module handle the entries in C(/etc/sysctl.conf), - and perform a I(/sbin/sysctl -p) after any change + - This module manipulates sysctl entries and performs a I(/sbin/sysctl -p) after changing them. version_added: "0.6" options: name: description: - - | - also known as "key", - this is the short path, point separated to the sysctl entry eg: C(vm.swappiness)" + - "this is the short path, decimal seperated, to the sysctl entry, ex: C(vm.swappiness)" required: true default: null aliases: [ 'key' ] value: description: - - "value to affect to the sysctl entry, to not provide if state=absent" + - set the sysctl value to this entry required: false default: null aliases: [ 'val' ] state: description: - - state=present the entry is added if not exist, or updated if exist - state=absent the entry is removed if exist + - whether the entry should be present or absent choices: [ "present", "absent" ] default: present checks: description: - - C(checks)=I(none) no smart/facultative checks will be made - C(checks)=I(before) some checks performed before any update (ie. does the sysctl key is writable ?) - C(checks)=I(after) some checks performed after an update (ie. does kernel give back the setted value ?) - C(checks)=I(both) all the smart checks I(before and after) are performed + - if C(checks)=I(none) no smart/facultative checks will be made + - if C(checks)=I(before) some checks performed before any update (ie. does the sysctl key is writable ?) + - if C(checks)=I(after) some checks performed after an update (ie. does kernel give back the setted value ?) + - if C(checks)=I(both) all the smart checks I(before and after) are performed choices: [ "none", "before", "after", "both" ] default: both reload: description: - - C(reload=yes) perform a I(/sbin/sysctl -p) if C(sysctl_file) updated ! - C(reload=no) do not reload I(sysctl) even if C(sysctl_file) updated ! + - if C(reload=yes), performs a I(/sbin/sysctl -p) if the C(sysctl_file) is updated + - if C(reload=no), does not reload I(sysctl) even if the C(sysctl_file) is updated choices: [ yes, no ] default: yes sysctl_file: description: - - specify the absolute path to C(/etc/sysctl.conf) + - specifies the absolute path to C(sysctl.conf), if not /etc/sysctl.conf required: false default: /etc/sysctl.conf examples: From d7ca7e0f4b6e7ecc255272a7d8bfd32d930be076 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Sat, 26 Jan 2013 13:07:06 -0500 Subject: [PATCH 22/27] New changelog additions from today's merging --- sysctl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sysctl b/sysctl index 6231f240e3c..45e948d2249 100644 --- a/sysctl +++ b/sysctl @@ -29,7 +29,7 @@ version_added: "0.6" options: name: description: - - "this is the short path, decimal seperated, to the sysctl entry, ex: C(vm.swappiness)" + - this is the short path, decimal seperated, to the sysctl entry required: true default: null aliases: [ 'key' ] From bfd37a2d4408d6f7a88638db8ce45b7b633eb669 Mon Sep 17 00:00:00 2001 From: Blair Zajac Date: Sat, 26 Jan 2013 10:19:48 -0800 Subject: [PATCH 23/27] library/apt: consistently use underscores in examples. To be consistent with the table showing available options, use underscores in the example tasks, not hyphens, as the table doesn't list hyphenated versions of option names, so it looks like the examples could have typos in them. --- apt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apt b/apt index b15580070bd..653ad6bb0d6 100644 --- a/apt +++ b/apt @@ -70,7 +70,7 @@ options: author: Matthew Williams notes: [] examples: - - code: "apt: pkg=foo update-cache=yes" + - code: "apt: pkg=foo update_cache=yes" description: Update repositories cache and install C(foo) package - code: "apt: pkg=foo state=removed" description: Remove C(foo) package @@ -78,9 +78,9 @@ examples: description: Install the package C(foo) - code: "apt: pkg=foo=1.00 state=installed" description: Install the version '1.00' of package C(foo) - - code: "apt: pkg=nginx state=latest default-release=squeeze-backports update-cache=yes" + - code: "apt: pkg=nginx state=latest default_release=squeeze-backports update_cache=yes" description: Update the repository cache and update package C(ngnix) to latest version using default release C(squeeze-backport) - - code: "apt: pkg=openjdk-6-jdk state=latest install-recommends=no" + - code: "apt: pkg=openjdk-6-jdk state=latest install_recommends=no" description: Install latest version of C(openjdk-6-jdk) ignoring C(install-reccomends) ''' From fe712a865a407a9314a121c60916f1ef33d48be7 Mon Sep 17 00:00:00 2001 From: Les Aker Date: Mon, 28 Jan 2013 00:33:18 -0500 Subject: [PATCH 24/27] adjusted assemble to use new file attribute handling --- assemble | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/assemble b/assemble index 70f429c564f..0e0eadc05a2 100644 --- a/assemble +++ b/assemble @@ -91,12 +91,12 @@ def main(): module = AnsibleModule( # not checking because of daisy chain to file module - check_invalid_arguments = False, argument_spec = dict( src = dict(required=True), dest = dict(required=True), backup=dict(default=False, choices=BOOLEANS), - ) + ), + add_file_common_args=True ) changed=False @@ -124,11 +124,11 @@ def main(): shutil.copy(path, dest) changed = True - + file_args = module.load_file_common_arguments(module.params) + changed = module.set_file_attributes_if_different(file_args, changed) # Mission complete module.exit_json(src=src, dest=dest, md5sum=destmd5, - changed=changed, msg="OK", - daisychain="file", daisychain_args=module.params) + changed=changed, msg="OK") # this is magic, see lib/ansible/module_common.py #<> From 650089d24c87b25fa239b4a5ba8081b528951c48 Mon Sep 17 00:00:00 2001 From: Daniel Hokka Zakrisson Date: Mon, 28 Jan 2013 12:40:18 +0100 Subject: [PATCH 25/27] Fix module.run_command usage in fireball --- fireball | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fireball b/fireball index 6eb363723ed..11a0b61188d 100644 --- a/fireball +++ b/fireball @@ -143,7 +143,7 @@ def daemonize_self(module, password, port, minutes): os.dup2(dev_null.fileno(), sys.stderr.fileno()) log("daemonizing successful (%s,%s)" % (password, port)) -def command(data): +def command(module, data): if 'cmd' not in data: return dict(failed=True, msg='internal error: cmd is required') if 'tmp_path' not in data: @@ -220,7 +220,7 @@ def serve(module, password, port, minutes): response = {} if mode == 'command': - response = command(data) + response = command(module, data) elif mode == 'put': response = put(data) elif mode == 'fetch': From a3bde39e634e5e86bc28c36efcebc794103b21f3 Mon Sep 17 00:00:00 2001 From: Daniel Hokka Zakrisson Date: Mon, 28 Jan 2013 17:46:35 +0100 Subject: [PATCH 26/27] Remove last remnants of daisychaining --- get_url | 3 +-- ini_file | 14 +++++--------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/get_url b/get_url index 8d6e85f0d8b..2db31c6d7c8 100644 --- a/get_url +++ b/get_url @@ -255,8 +255,7 @@ def main(): # Mission complete module.exit_json(url=url, dest=dest, src=tmpsrc, md5sum=md5sum_src, - changed=changed, msg=info.get('msg', ''), - daisychain="file", daisychain_args=info.get('daisychain_args', '')) + changed=changed, msg=info.get('msg', '')) # this is magic, see lib/ansible/module_common.py #<> diff --git a/ini_file b/ini_file index ea78bff191b..cc9771380da 100644 --- a/ini_file +++ b/ini_file @@ -153,8 +153,6 @@ def do_ini(module, filename, section=None, option=None, value=None, state='prese def main(): module = AnsibleModule( - # not checking because of daisy chain to file module - check_invalid_arguments = False, argument_spec = dict( dest = dict(required=True), section = dict(required=True), @@ -162,7 +160,8 @@ def main(): value = dict(required=False), backup = dict(default='no', choices=BOOLEANS), state = dict(default='present', choices=['present', 'absent']) - ) + ), + add_file_common_args = True ) info = dict() @@ -176,14 +175,11 @@ def main(): changed = do_ini(module, dest, section, option, value, state, backup) - info['daisychain_args'] = module.params - info['daisychain_args']['state'] = 'file' - info['daisychain_args']['dest'] = dest + file_args = module.load_file_common_arguments(module.params) + changed = module.set_file_attributes_if_different(file_args, changed) # Mission complete - module.exit_json(dest=dest, - changed=changed, msg="OK", - daisychain="file", daisychain_args=info.get('daisychain_args','')) + module.exit_json(dest=dest, changed=changed, msg="OK") # this is magic, see lib/ansible/module_common.py #<> From f83db16c376c681d5012239388516f3180d24939 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Mon, 28 Jan 2013 15:41:46 -0500 Subject: [PATCH 27/27] This standardizes the apt_key module some * improves error handling and reporting * uses run_command to reduce code * fails quicker on errors as opposed to return codes and tracebacks * can now also specify the key as data versus needing to wget it from a file --- apt_key | 173 +++++++++++++++++++++++--------------------------------- 1 file changed, 72 insertions(+), 101 deletions(-) diff --git a/apt_key b/apt_key index 14d1b734fda..e2e66cd441f 100644 --- a/apt_key +++ b/apt_key @@ -22,7 +22,7 @@ DOCUMENTATION = ''' --- module: apt_key -author: Jayson Vantuyl +author: Jayson Vantuyl & others version_added: 1.0 short_description: Add or remove an apt key description: @@ -59,145 +59,116 @@ examples: description: Remove a Apt specific signing key ''' +# FIXME: standardize into module_common from urllib2 import urlopen, URLError from traceback import format_exc -from subprocess import Popen, PIPE, call from re import compile as re_compile +# FIXME: standardize into module_common from distutils.spawn import find_executable from os import environ from sys import exc_info +import traceback match_key = re_compile("^gpg:.*key ([0-9a-fA-F]+):.*$") REQUIRED_EXECUTABLES=['gpg', 'grep', 'apt-key'] -def find_missing_binaries(): - return [missing for missing in REQUIRED_EXECUTABLES if not find_executable(missing)] +def check_missing_binaries(module): + missing = [e for e in REQUIRED_EXECUTABLES if not find_executable(e)] + if len(missing): + module.fail_json(msg="binaries are missing", names=all) +def all_keys(module): + (rc, out, err) = module.run_command("apt-key list") + results = [] + lines = out.split('\n') + for line in lines: + if line.startswith("pub"): + tokens = line.split() + code = tokens[1] + (len_type, real_code) = code.split("/") + results.append(real_code) + return results -def get_key_ids(key_data): - p = Popen("gpg --list-only --import -", shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE) - (stdo, stde) = p.communicate(key_data) +def key_present(module, key_id): + (rc, out, err) = module.run_command("apt-key list | 2>&1 grep -q %s" % key_id) + return rc == 0 - if p.returncode > 0: - raise Exception("error running GPG to retrieve keys") - - output = stdo + stde - - for line in output.split('\n'): - match = match_key.match(line) - if match: - yield match.group(1) - - -def key_present(key_id): - return call("apt-key list | 2>&1 grep -q %s" % key_id, shell=True) == 0 - - -def download_key(url): +def download_key(module, url): + # FIXME: move get_url code to common, allow for in-memory D/L, support proxies + # and reuse here if url is None: - raise Exception("Needed URL but none specified") - connection = urlopen(url) - if connection is None: - raise Exception("error connecting to download key from %r" % url) - return connection.read() + module.fail_json(msg="needed a URL but was not specified") + try: + connection = urlopen(url) + if connection is None: + module.fail_json("error connecting to download key from url") + data = connection.read() + return data + except: + module.fail_json(msg="error getting key id from url", traceback=format_exc()) -def add_key(key): - p = Popen("apt-key add -", shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE) - (_, _) = p.communicate(key) - - return p.returncode == 0 - +def add_key(module, key): + cmd = "apt-key add -" + (rc, out, err) = module.run_command(cmd, data=key, check_rc=True) + return True def remove_key(key_id): - return call('apt-key del %s' % key_id, shell=True) == 0 - - -def return_values(tb=False): - if tb: - return {'exception': format_exc()} - else: - return {} + # FIXME: use module.run_command, fail at point of error and don't discard useful stdin/stdout + cmd = 'apt-key del %s' + (rc, out, err) = module.run_command(cmd, check_rc=True) + return True def main(): module = AnsibleModule( argument_spec=dict( id=dict(required=False, default=None), url=dict(required=False), + data=dict(required=False), + key=dict(required=False), state=dict(required=False, choices=['present', 'absent'], default='present') - ) + ), ) - expected_key_id = module.params['id'] - url = module.params['url'] - state = module.params['state'] - changed = False + key_id = module.params['id'] + url = module.params['url'] + data = module.params['data'] + state = module.params['state'] + changed = False + + # FIXME: I think we have a common facility for this, if not, want + check_missing_binaries(module) - missing = find_missing_binaries() - - if missing: - module.fail_json(msg="can't find needed binaries to run", missing=missing, - **return_values()) + keys = all_keys(module) if state == 'present': - if expected_key_id and key_present(expected_key_id): - # key is present, nothing to do - pass + if key_id and key_id in keys: + module.exit_json(changed=False) else: - # download key - try: - key = download_key(url) - (key_id,) = tuple(get_key_ids(key)) # TODO: support multiple key ids? - except Exception: - module.fail_json( - msg="error getting key id from url", - **return_values(True) - ) - - # sanity check downloaded key - if expected_key_id and key_id != expected_key_id: - module.fail_json( - msg="expected key id %s, got key id %s" % (expected_key_id, key_id), - **return_values() - ) - - # actually add key - if key_present(key_id): - changed=False - elif add_key(key): - changed=True + if not data: + data = download_key(module, url) + if key_id and key_id in keys: + module.exit_json(changed=False) else: - module.fail_json( - msg="failed to add key id %s" % key_id, - **return_values() - ) + add_key(module, data) + changed=False + keys2 = all_keys(module) + if len(keys) != len(keys2): + changed=True + if key_id and not key_id in keys2: + module.fail_json(msg="key does not seem to have been added", id=key_id) + module.exit_json(changed=changed) elif state == 'absent': - # optionally download the key and get the id - if not expected_key_id: - try: - key = download_key(url) - (key_id,) = tuple(get_key_ids(key)) # TODO: support multiple key ids? - except Exception: - module.fail_json( - msg="error getting key id from url", - **return_values(True) - ) - else: - key_id = expected_key_id - - # actually remove key - if key_present(key_id): + if not key_id: + module.fail_json(msg="key is required") + if key_id in keys: if remove_key(key_id): changed=True else: + # FIXME: module.fail_json or exit-json immediately at point of failure module.fail_json(msg="error removing key_id", **return_values(True)) - else: - module.fail_json( - msg="unexpected state: %s" % state, - **return_values() - ) module.exit_json(changed=changed, **return_values())