diff --git a/extras/commands/command b/extras/commands/command deleted file mode 100644 index c1fabd4f9b4..00000000000 --- a/extras/commands/command +++ /dev/null @@ -1,275 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2012, Michael DeHaan , and others -# -# 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 datetime -import traceback -import re -import shlex -import os - -DOCUMENTATION = ''' ---- -module: command -version_added: historical -short_description: Executes a command on a remote node -description: - - The M(command) module takes the command name followed by a list of space-delimited arguments. - - The given command will be executed on all selected nodes. It will not be - processed through the shell, so variables like C($HOME) and operations - like C("<"), C(">"), C("|"), and C("&") will not work (use the M(shell) - module if you need these features). -options: - free_form: - description: - - the command module takes a free form command to run. There is no parameter actually named 'free form'. - See the examples! - required: true - default: null - aliases: [] - creates: - description: - - a filename, when it already exists, this step will B(not) be run. - required: no - default: null - removes: - description: - - a filename, when it does not exist, this step will B(not) be run. - version_added: "0.8" - required: no - default: null - chdir: - description: - - cd into this directory before running the command - version_added: "0.6" - required: false - default: null - executable: - description: - - change the shell used to execute the command. Should be an absolute path to the executable. - required: false - default: null - version_added: "0.9" - warn: - version_added: "1.8" - default: yes - description: - - if command warnings are on in ansible.cfg, do not warn about this particular line if set to no/false. - required: false - default: True -notes: - - If you want to run a command through the shell (say you are using C(<), - C(>), C(|), etc), you actually want the M(shell) module instead. The - M(command) module is much more secure as it's not affected by the user's - environment. - - " C(creates), C(removes), and C(chdir) can be specified after the command. For instance, if you only want to run a command if a certain file does not exist, use this." -author: Michael DeHaan -''' - -EXAMPLES = ''' -# Example from Ansible Playbooks. -- command: /sbin/shutdown -t now - -# Run the command if the specified file does not exist. -- command: /usr/bin/make_database.sh arg1 arg2 creates=/path/to/database - -# You can also use the 'args' form to provide the options. This command -# will change the working directory to somedir/ and will only run when -# /path/to/database doesn't exist. -- command: /usr/bin/make_database.sh arg1 arg2 - args: - chdir: somedir/ - creates: /path/to/database -''' - -# This is a pretty complex regex, which functions as follows: -# -# 1. (^|\s) -# ^ look for a space or the beginning of the line -# 2. (creates|removes|chdir|executable|NO_LOG)= -# ^ look for a valid param, followed by an '=' -# 3. (?P[\'"])? -# ^ look for an optional quote character, which can either be -# a single or double quote character, and store it for later -# 4. (.*?) -# ^ match everything in a non-greedy manner until... -# 5. (?(quote)(?[\'"])?(.*?)(?(quote)(? 1 and (v.startswith('"') and v.endswith('"') or v.startswith("'") and v.endswith("'")): - v = v[1:-1] - if k in ('creates', 'removes', 'chdir', 'executable', 'NO_LOG'): - if k == "chdir": - v = os.path.abspath(os.path.expanduser(v)) - if not (os.path.exists(v) and os.path.isdir(v)): - self.fail_json(rc=258, msg="cannot change to directory '%s': path does not exist" % v) - elif k == "executable": - v = os.path.abspath(os.path.expanduser(v)) - if not (os.path.exists(v)): - self.fail_json(rc=258, msg="cannot use executable '%s': file does not exist" % v) - params[k] = v - # Remove any of the above k=v params from the args string - args = PARAM_REGEX.sub('', args) - params['args'] = args.strip() - - return (params, params['args']) - -main() diff --git a/extras/commands/raw b/extras/commands/raw deleted file mode 100644 index 87f2b5c4bdc..00000000000 --- a/extras/commands/raw +++ /dev/null @@ -1,43 +0,0 @@ -# this is a virtual module that is entirely implemented server side - -DOCUMENTATION = ''' ---- -module: raw -version_added: historical -short_description: Executes a low-down and dirty SSH command -options: - free_form: - description: - - the raw module takes a free form command to run - required: true - executable: - description: - - change the shell used to execute the command. Should be an absolute path to the executable. - required: false - version_added: "1.0" -description: - - Executes a low-down and dirty SSH command, not going through the module - subsystem. This is useful and should only be done in two cases. The - first case is installing C(python-simplejson) on older (Python 2.4 and - before) hosts that need it as a dependency to run modules, since nearly - all core modules require it. Another is speaking to any devices such as - routers that do not have any Python installed. In any other case, using - the M(shell) or M(command) module is much more appropriate. Arguments - given to M(raw) are run directly through the configured remote shell. - Standard output, error output and return code are returned when - available. There is no change handler support for this module. - - This module does not require python on the remote system, much like - the M(script) module. -notes: - - If you want to execute a command securely and predictably, it may be - better to use the M(command) module instead. Best practices when writing - playbooks will follow the trend of using M(command) unless M(shell) is - explicitly required. When running ad-hoc commands, use your best - judgement. -author: Michael DeHaan -''' - -EXAMPLES = ''' -# Bootstrap a legacy python 2.4 host -- raw: yum -y install python-simplejson -''' diff --git a/extras/commands/script b/extras/commands/script deleted file mode 100644 index 01a1ae34e71..00000000000 --- a/extras/commands/script +++ /dev/null @@ -1,47 +0,0 @@ - -DOCUMENTATION = """ ---- -module: script -version_added: "0.9" -short_description: Runs a local script on a remote node after transferring it -description: - - "The M(script) module takes the script name followed by a list of - space-delimited arguments. " - - "The local script at path will be transferred to the remote node and then executed. " - - "The given script will be processed through the shell environment on the remote node. " - - "This module does not require python on the remote system, much like - the M(raw) module. " -options: - free_form: - description: - - path to the local script file followed by optional arguments. - required: true - default: null - aliases: [] - creates: - description: - - a filename, when it already exists, this step will B(not) be run. - required: no - default: null - version_added: "1.5" - removes: - description: - - a filename, when it does not exist, this step will B(not) be run. - required: no - default: null - version_added: "1.5" -notes: - - It is usually preferable to write Ansible modules than pushing scripts. Convert your script to an Ansible module for bonus points! -author: Michael DeHaan -""" - -EXAMPLES = ''' -# Example from Ansible Playbooks -- script: /some/local/script.sh --some-arguments 1234 - -# Run a script that creates a file, but only if the file is not yet created -- script: /some/local/create_file.sh --some-arguments 1234 creates=/the/created/file.txt - -# Run a script that removes a file, but only if the file is not yet removed -- script: /some/local/remove_file.sh --some-arguments 1234 removes=/the/removed/file.txt -''' diff --git a/extras/commands/shell b/extras/commands/shell deleted file mode 100644 index b63a21080ee..00000000000 --- a/extras/commands/shell +++ /dev/null @@ -1,78 +0,0 @@ -# There is actually no actual shell module source, when you use 'shell' in ansible, -# it runs the 'command' module with special arguments and it behaves differently. -# See the command source and the comment "#USE_SHELL". - -DOCUMENTATION = ''' ---- -module: shell -short_description: Execute commands in nodes. -description: - - The M(shell) module takes the command name followed by a list of space-delimited arguments. - It is almost exactly like the M(command) module but runs - the command through a shell (C(/bin/sh)) on the remote node. -version_added: "0.2" -options: - free_form: - description: - - The shell module takes a free form command to run, as a string. There's not an actual - option named "free form". See the examples! - required: true - default: null - creates: - description: - - a filename, when it already exists, this step will B(not) be run. - required: no - default: null - removes: - description: - - a filename, when it does not exist, this step will B(not) be run. - version_added: "0.8" - required: no - default: null - chdir: - description: - - cd into this directory before running the command - required: false - default: null - version_added: "0.6" - executable: - description: - - change the shell used to execute the command. Should be an absolute path to the executable. - required: false - default: null - version_added: "0.9" - warn: - description: - - if command warnings are on in ansible.cfg, do not warn about this particular line if set to no/false. - required: false - default: True - version_added: "1.8" -notes: - - If you want to execute a command securely and predictably, it may be - better to use the M(command) module instead. Best practices when writing - playbooks will follow the trend of using M(command) unless M(shell) is - explicitly required. When running ad-hoc commands, use your best - judgement. - - To sanitize any variables passed to the shell module, you should use - "{{ var | quote }}" instead of just "{{ var }}" to make sure they don't include evil things like semicolons. - -requirements: [ ] -author: Michael DeHaan -''' - -EXAMPLES = ''' -# Execute the command in remote shell; stdout goes to the specified -# file on the remote. -- shell: somescript.sh >> somelog.txt - -# Change the working directory to somedir/ before executing the command. -- shell: somescript.sh >> somelog.txt chdir=somedir/ - -# You can also use the 'args' form to provide the options. This command -# will change the working directory to somedir/ and will only run when -# somedir/somelog.txt doesn't exist. -- shell: somescript.sh >> somelog.txt - args: - chdir: somedir/ - creates: somelog.txt -''' diff --git a/extras/database/mysql_db b/extras/database/mysql_db deleted file mode 100644 index 38dee608ba5..00000000000 --- a/extras/database/mysql_db +++ /dev/null @@ -1,363 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2012, Mark Theunissen -# Sponsored by Four Kitchens http://fourkitchens.com. -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -DOCUMENTATION = ''' ---- -module: mysql_db -short_description: Add or remove MySQL databases from a remote host. -description: - - Add or remove MySQL databases from a remote host. -version_added: "0.6" -options: - name: - description: - - name of the database to add or remove - required: true - default: null - aliases: [ db ] - login_user: - description: - - The username used to authenticate with - required: false - default: null - login_password: - description: - - The password used to authenticate with - required: false - default: null - login_host: - description: - - Host running the database - required: false - default: localhost - login_port: - description: - - Port of the MySQL server. Requires login_host be defined as other then localhost if login_port is used - required: false - default: 3306 - login_unix_socket: - description: - - The path to a Unix domain socket for local connections - required: false - default: null - state: - description: - - The database state - required: false - default: present - choices: [ "present", "absent", "dump", "import" ] - collation: - description: - - Collation mode - required: false - default: null - encoding: - description: - - Encoding mode - required: false - default: null - target: - description: - - Location, on the remote host, of the dump file to read from or write to. Uncompressed SQL - files (C(.sql)) as well as bzip2 (C(.bz2)) and gzip (C(.gz)) compressed files are supported. - required: false -notes: - - Requires the MySQLdb Python package on the remote host. For Ubuntu, this - is as easy as apt-get install python-mysqldb. (See M(apt).) - - Both I(login_password) and I(login_user) are required when you are - passing credentials. If none are present, the module will attempt to read - the credentials from C(~/.my.cnf), and finally fall back to using the MySQL - default login of C(root) with no password. -requirements: [ ConfigParser ] -author: Mark Theunissen -''' - -EXAMPLES = ''' -# Create a new database with name 'bobdata' -- mysql_db: name=bobdata state=present - -# Copy database dump file to remote host and restore it to database 'my_db' -- copy: src=dump.sql.bz2 dest=/tmp -- mysql_db: name=my_db state=import target=/tmp/dump.sql.bz2 -''' - -import ConfigParser -import os -import pipes -try: - import MySQLdb -except ImportError: - mysqldb_found = False -else: - mysqldb_found = True - -# =========================================== -# MySQL module specific support methods. -# - -def db_exists(cursor, db): - res = cursor.execute("SHOW DATABASES LIKE %s", (db.replace("_","\_"),)) - return bool(res) - -def db_delete(cursor, db): - query = "DROP DATABASE `%s`" % db - cursor.execute(query) - return True - -def db_dump(module, host, user, password, db_name, target, port, socket=None): - cmd = module.get_bin_path('mysqldump', True) - cmd += " --quick --user=%s --password=%s" % (pipes.quote(user), pipes.quote(password)) - if socket is not None: - cmd += " --socket=%s" % pipes.quote(socket) - else: - cmd += " --host=%s --port=%s" % (pipes.quote(host), pipes.quote(port)) - cmd += " %s" % pipes.quote(db_name) - if os.path.splitext(target)[-1] == '.gz': - cmd = cmd + ' | gzip > ' + pipes.quote(target) - elif os.path.splitext(target)[-1] == '.bz2': - cmd = cmd + ' | bzip2 > ' + pipes.quote(target) - else: - cmd += " > %s" % pipes.quote(target) - rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True) - return rc, stdout, stderr - -def db_import(module, host, user, password, db_name, target, port, socket=None): - if not os.path.exists(target): - return module.fail_json(msg="target %s does not exist on the host" % target) - - cmd = module.get_bin_path('mysql', True) - cmd += " --user=%s --password=%s" % (pipes.quote(user), pipes.quote(password)) - if socket is not None: - cmd += " --socket=%s" % pipes.quote(socket) - else: - cmd += " --host=%s --port=%s" % (pipes.quote(host), pipes.quote(port)) - cmd += " -D %s" % pipes.quote(db_name) - if os.path.splitext(target)[-1] == '.gz': - gunzip_path = module.get_bin_path('gunzip') - if gunzip_path: - rc, stdout, stderr = module.run_command('%s %s' % (gunzip_path, target)) - if rc != 0: - return rc, stdout, stderr - cmd += " < %s" % pipes.quote(os.path.splitext(target)[0]) - rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True) - if rc != 0: - return rc, stdout, stderr - gzip_path = module.get_bin_path('gzip') - if gzip_path: - rc, stdout, stderr = module.run_command('%s %s' % (gzip_path, os.path.splitext(target)[0])) - else: - module.fail_json(msg="gzip command not found") - else: - module.fail_json(msg="gunzip command not found") - elif os.path.splitext(target)[-1] == '.bz2': - bunzip2_path = module.get_bin_path('bunzip2') - if bunzip2_path: - rc, stdout, stderr = module.run_command('%s %s' % (bunzip2_path, target)) - if rc != 0: - return rc, stdout, stderr - cmd += " < %s" % pipes.quote(os.path.splitext(target)[0]) - rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True) - if rc != 0: - return rc, stdout, stderr - bzip2_path = module.get_bin_path('bzip2') - if bzip2_path: - rc, stdout, stderr = module.run_command('%s %s' % (bzip2_path, os.path.splitext(target)[0])) - else: - module.fail_json(msg="bzip2 command not found") - else: - module.fail_json(msg="bunzip2 command not found") - else: - cmd += " < %s" % pipes.quote(target) - rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True) - return rc, stdout, stderr - -def db_create(cursor, db, encoding, collation): - if encoding: - encoding = " CHARACTER SET %s" % encoding - if collation: - collation = " COLLATE %s" % collation - query = "CREATE DATABASE `%s`%s%s" % (db, encoding, collation) - res = cursor.execute(query) - return True - -def strip_quotes(s): - """ Remove surrounding single or double quotes - - >>> print strip_quotes('hello') - hello - >>> print strip_quotes('"hello"') - hello - >>> print strip_quotes("'hello'") - hello - >>> print strip_quotes("'hello") - 'hello - - """ - single_quote = "'" - double_quote = '"' - - if s.startswith(single_quote) and s.endswith(single_quote): - s = s.strip(single_quote) - elif s.startswith(double_quote) and s.endswith(double_quote): - s = s.strip(double_quote) - return s - - -def config_get(config, section, option): - """ Calls ConfigParser.get and strips quotes - - See: http://dev.mysql.com/doc/refman/5.0/en/option-files.html - """ - return strip_quotes(config.get(section, option)) - - -def load_mycnf(): - config = ConfigParser.RawConfigParser() - mycnf = os.path.expanduser('~/.my.cnf') - if not os.path.exists(mycnf): - return False - try: - config.readfp(open(mycnf)) - except (IOError): - return False - # We support two forms of passwords in .my.cnf, both pass= and password=, - # as these are both supported by MySQL. - try: - passwd = config_get(config, 'client', 'password') - except (ConfigParser.NoOptionError): - try: - passwd = config_get(config, 'client', 'pass') - except (ConfigParser.NoOptionError): - return False - try: - creds = dict(user=config_get(config, 'client', 'user'),passwd=passwd) - except (ConfigParser.NoOptionError): - return False - return creds - -# =========================================== -# Module execution. -# - -def main(): - module = AnsibleModule( - argument_spec = dict( - login_user=dict(default=None), - login_password=dict(default=None), - login_host=dict(default="localhost"), - login_port=dict(default="3306"), - login_unix_socket=dict(default=None), - name=dict(required=True, aliases=['db']), - encoding=dict(default=""), - collation=dict(default=""), - target=dict(default=None), - state=dict(default="present", choices=["absent", "present","dump", "import"]), - ) - ) - - if not mysqldb_found: - module.fail_json(msg="the python mysqldb module is required") - - db = module.params["name"] - encoding = module.params["encoding"] - collation = module.params["collation"] - state = module.params["state"] - target = module.params["target"] - - # make sure the target path is expanded for ~ and $HOME - if target is not None: - target = os.path.expandvars(os.path.expanduser(target)) - - # Either the caller passes both a username and password with which to connect to - # mysql, or they pass neither and allow this module to read the credentials from - # ~/.my.cnf. - login_password = module.params["login_password"] - login_user = module.params["login_user"] - if login_user is None and login_password is None: - mycnf_creds = load_mycnf() - if mycnf_creds is False: - login_user = "root" - login_password = "" - else: - login_user = mycnf_creds["user"] - login_password = mycnf_creds["passwd"] - elif login_password is None or login_user is None: - module.fail_json(msg="when supplying login arguments, both login_user and login_password must be provided") - login_host = module.params["login_host"] - - if state in ['dump','import']: - if target is None: - module.fail_json(msg="with state=%s target is required" % (state)) - connect_to_db = db - else: - connect_to_db = 'mysql' - try: - if module.params["login_unix_socket"]: - db_connection = MySQLdb.connect(host=module.params["login_host"], unix_socket=module.params["login_unix_socket"], user=login_user, passwd=login_password, db=connect_to_db) - elif module.params["login_port"] != "3306" and module.params["login_host"] == "localhost": - module.fail_json(msg="login_host is required when login_port is defined, login_host cannot be localhost when login_port is defined") - else: - db_connection = MySQLdb.connect(host=module.params["login_host"], port=int(module.params["login_port"]), user=login_user, passwd=login_password, db=connect_to_db) - cursor = db_connection.cursor() - except Exception, e: - if "Unknown database" in str(e): - errno, errstr = e.args - module.fail_json(msg="ERROR: %s %s" % (errno, errstr)) - else: - module.fail_json(msg="unable to connect, check login_user and login_password are correct, or alternatively check ~/.my.cnf contains credentials") - - changed = False - if db_exists(cursor, db): - if state == "absent": - try: - changed = db_delete(cursor, db) - except Exception, e: - module.fail_json(msg="error deleting database: " + str(e)) - elif state == "dump": - rc, stdout, stderr = db_dump(module, login_host, login_user, - login_password, db, target, - port=module.params['login_port'], - socket=module.params['login_unix_socket']) - if rc != 0: - module.fail_json(msg="%s" % stderr) - else: - module.exit_json(changed=True, db=db, msg=stdout) - elif state == "import": - rc, stdout, stderr = db_import(module, login_host, login_user, - login_password, db, target, - port=module.params['login_port'], - socket=module.params['login_unix_socket']) - if rc != 0: - module.fail_json(msg="%s" % stderr) - else: - module.exit_json(changed=True, db=db, msg=stdout) - else: - if state == "present": - try: - changed = db_create(cursor, db, encoding, collation) - except Exception, e: - module.fail_json(msg="error creating database: " + str(e)) - - module.exit_json(changed=changed, db=db) - -# import module snippets -from ansible.module_utils.basic import * -main() diff --git a/extras/database/mysql_user b/extras/database/mysql_user deleted file mode 100644 index aaec05f99f5..00000000000 --- a/extras/database/mysql_user +++ /dev/null @@ -1,476 +0,0 @@ -#!/usr/bin/python - -# (c) 2012, Mark Theunissen -# Sponsored by Four Kitchens http://fourkitchens.com. -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -DOCUMENTATION = ''' ---- -module: mysql_user -short_description: Adds or removes a user from a MySQL database. -description: - - Adds or removes a user from a MySQL database. -version_added: "0.6" -options: - name: - description: - - name of the user (role) to add or remove - required: true - default: null - password: - description: - - set the user's password - required: false - default: null - host: - description: - - the 'host' part of the MySQL username - required: false - default: localhost - login_user: - description: - - The username used to authenticate with - required: false - default: null - login_password: - description: - - The password used to authenticate with - required: false - default: null - login_host: - description: - - Host running the database - required: false - default: localhost - login_port: - description: - - Port of the MySQL server - required: false - default: 3306 - version_added: '1.4' - login_unix_socket: - description: - - The path to a Unix domain socket for local connections - required: false - default: null - priv: - description: - - "MySQL privileges string in the format: C(db.table:priv1,priv2)" - required: false - default: null - append_privs: - description: - - Append the privileges defined by priv to the existing ones for this - user instead of overwriting existing ones. - required: false - choices: [ "yes", "no" ] - default: "no" - version_added: "1.4" - state: - description: - - Whether the user should exist. When C(absent), removes - the user. - required: false - default: present - choices: [ "present", "absent" ] - check_implicit_admin: - description: - - Check if mysql allows login as root/nopassword before trying supplied credentials. - required: false - default: false - version_added: "1.3" -notes: - - Requires the MySQLdb Python package on the remote host. For Ubuntu, this - is as easy as apt-get install python-mysqldb. - - Both C(login_password) and C(login_username) are required when you are - passing credentials. If none are present, the module will attempt to read - the credentials from C(~/.my.cnf), and finally fall back to using the MySQL - default login of 'root' with no password. - - "MySQL server installs with default login_user of 'root' and no password. To secure this user - as part of an idempotent playbook, you must create at least two tasks: the first must change the root user's password, - without providing any login_user/login_password details. The second must drop a ~/.my.cnf file containing - the new root credentials. Subsequent runs of the playbook will then succeed by reading the new credentials from - the file." - -requirements: [ "ConfigParser", "MySQLdb" ] -author: Mark Theunissen -''' - -EXAMPLES = """ -# Create database user with name 'bob' and password '12345' with all database privileges -- mysql_user: name=bob password=12345 priv=*.*:ALL state=present - -# Creates database user 'bob' and password '12345' with all database privileges and 'WITH GRANT OPTION' -- mysql_user: name=bob password=12345 priv=*.*:ALL,GRANT state=present - -# Ensure no user named 'sally' exists, also passing in the auth credentials. -- mysql_user: login_user=root login_password=123456 name=sally state=absent - -# Specify grants composed of more than one word -- mysql_user: name=replication password=12345 priv=*.*:"REPLICATION CLIENT" state=present - -# Revoke all privileges for user 'bob' and password '12345' -- mysql_user: name=bob password=12345 priv=*.*:USAGE state=present - -# Example privileges string format -mydb.*:INSERT,UPDATE/anotherdb.*:SELECT/yetanotherdb.*:ALL - -# Example using login_unix_socket to connect to server -- mysql_user: name=root password=abc123 login_unix_socket=/var/run/mysqld/mysqld.sock - -# Example .my.cnf file for setting the root password -# Note: don't use quotes around the password, because the mysql_user module -# will include them in the password but the mysql client will not - -[client] -user=root -password=n<_665{vS43y -""" - -import ConfigParser -import getpass -import tempfile -try: - import MySQLdb -except ImportError: - mysqldb_found = False -else: - mysqldb_found = True - -# =========================================== -# MySQL module specific support methods. -# - -def user_exists(cursor, user, host): - cursor.execute("SELECT count(*) FROM user WHERE user = %s AND host = %s", (user,host)) - count = cursor.fetchone() - return count[0] > 0 - -def user_add(cursor, user, host, password, new_priv): - cursor.execute("CREATE USER %s@%s IDENTIFIED BY %s", (user,host,password)) - if new_priv is not None: - for db_table, priv in new_priv.iteritems(): - privileges_grant(cursor, user,host,db_table,priv) - return True - -def user_mod(cursor, user, host, password, new_priv, append_privs): - changed = False - grant_option = False - - # Handle passwords. - if password is not None: - cursor.execute("SELECT password FROM user WHERE user = %s AND host = %s", (user,host)) - current_pass_hash = cursor.fetchone() - cursor.execute("SELECT PASSWORD(%s)", (password,)) - new_pass_hash = cursor.fetchone() - if current_pass_hash[0] != new_pass_hash[0]: - cursor.execute("SET PASSWORD FOR %s@%s = PASSWORD(%s)", (user,host,password)) - changed = True - - # Handle privileges. - if new_priv is not None: - curr_priv = privileges_get(cursor, user,host) - - # If the user has privileges on a db.table that doesn't appear at all in - # the new specification, then revoke all privileges on it. - for db_table, priv in curr_priv.iteritems(): - # If the user has the GRANT OPTION on a db.table, revoke it first. - if "GRANT" in priv: - grant_option = True - if db_table not in new_priv: - if user != "root" and "PROXY" not in priv and not append_privs: - privileges_revoke(cursor, user,host,db_table,grant_option) - changed = True - - # If the user doesn't currently have any privileges on a db.table, then - # we can perform a straight grant operation. - for db_table, priv in new_priv.iteritems(): - if db_table not in curr_priv: - privileges_grant(cursor, user,host,db_table,priv) - changed = True - - # If the db.table specification exists in both the user's current privileges - # and in the new privileges, then we need to see if there's a difference. - db_table_intersect = set(new_priv.keys()) & set(curr_priv.keys()) - for db_table in db_table_intersect: - priv_diff = set(new_priv[db_table]) ^ set(curr_priv[db_table]) - if (len(priv_diff) > 0): - if not append_privs: - privileges_revoke(cursor, user,host,db_table,grant_option) - privileges_grant(cursor, user,host,db_table,new_priv[db_table]) - changed = True - - return changed - -def user_delete(cursor, user, host): - cursor.execute("DROP USER %s@%s", (user,host)) - return True - -def privileges_get(cursor, user,host): - """ MySQL doesn't have a better method of getting privileges aside from the - SHOW GRANTS query syntax, which requires us to then parse the returned string. - Here's an example of the string that is returned from MySQL: - - GRANT USAGE ON *.* TO 'user'@'localhost' IDENTIFIED BY 'pass'; - - This function makes the query and returns a dictionary containing the results. - The dictionary format is the same as that returned by privileges_unpack() below. - """ - output = {} - cursor.execute("SHOW GRANTS FOR %s@%s", (user,host)) - grants = cursor.fetchall() - - def pick(x): - if x == 'ALL PRIVILEGES': - return 'ALL' - else: - return x - - for grant in grants: - res = re.match("GRANT (.+) ON (.+) TO '.+'@'.+'( IDENTIFIED BY PASSWORD '.+')? ?(.*)", grant[0]) - if res is None: - module.fail_json(msg="unable to parse the MySQL grant string") - privileges = res.group(1).split(", ") - privileges = [ pick(x) for x in privileges] - if "WITH GRANT OPTION" in res.group(4): - privileges.append('GRANT') - db = res.group(2) - output[db] = privileges - return output - -def privileges_unpack(priv): - """ Take a privileges string, typically passed as a parameter, and unserialize - it into a dictionary, the same format as privileges_get() above. We have this - custom format to avoid using YAML/JSON strings inside YAML playbooks. Example - of a privileges string: - - mydb.*:INSERT,UPDATE/anotherdb.*:SELECT/yetanother.*:ALL - - The privilege USAGE stands for no privileges, so we add that in on *.* if it's - not specified in the string, as MySQL will always provide this by default. - """ - output = {} - for item in priv.split('/'): - pieces = item.split(':') - if '.' in pieces[0]: - pieces[0] = pieces[0].split('.') - for idx, piece in enumerate(pieces): - if pieces[0][idx] != "*": - pieces[0][idx] = "`" + pieces[0][idx] + "`" - pieces[0] = '.'.join(pieces[0]) - - output[pieces[0]] = pieces[1].upper().split(',') - - if '*.*' not in output: - output['*.*'] = ['USAGE'] - - return output - -def privileges_revoke(cursor, user,host,db_table,grant_option): - if grant_option: - query = "REVOKE GRANT OPTION ON %s FROM '%s'@'%s'" % (db_table,user,host) - cursor.execute(query) - query = "REVOKE ALL PRIVILEGES ON %s FROM '%s'@'%s'" % (db_table,user,host) - cursor.execute(query) - -def privileges_grant(cursor, user,host,db_table,priv): - - priv_string = ",".join(filter(lambda x: x != 'GRANT', priv)) - query = "GRANT %s ON %s TO '%s'@'%s'" % (priv_string,db_table,user,host) - if 'GRANT' in priv: - query = query + " WITH GRANT OPTION" - cursor.execute(query) - - -def strip_quotes(s): - """ Remove surrounding single or double quotes - - >>> print strip_quotes('hello') - hello - >>> print strip_quotes('"hello"') - hello - >>> print strip_quotes("'hello'") - hello - >>> print strip_quotes("'hello") - 'hello - - """ - single_quote = "'" - double_quote = '"' - - if s.startswith(single_quote) and s.endswith(single_quote): - s = s.strip(single_quote) - elif s.startswith(double_quote) and s.endswith(double_quote): - s = s.strip(double_quote) - return s - - -def config_get(config, section, option): - """ Calls ConfigParser.get and strips quotes - - See: http://dev.mysql.com/doc/refman/5.0/en/option-files.html - """ - return strip_quotes(config.get(section, option)) - - -def _safe_cnf_load(config, path): - - data = {'user':'', 'password':''} - - # read in user/pass - f = open(path, 'r') - for line in f.readlines(): - line = line.strip() - if line.startswith('user='): - data['user'] = line.split('=', 1)[1].strip() - if line.startswith('password=') or line.startswith('pass='): - data['password'] = line.split('=', 1)[1].strip() - f.close() - - # write out a new cnf file with only user/pass - fh, newpath = tempfile.mkstemp(prefix=path + '.') - f = open(newpath, 'wb') - f.write('[client]\n') - f.write('user=%s\n' % data['user']) - f.write('password=%s\n' % data['password']) - f.close() - - config.readfp(open(newpath)) - os.remove(newpath) - return config - -def load_mycnf(): - config = ConfigParser.RawConfigParser() - mycnf = os.path.expanduser('~/.my.cnf') - if not os.path.exists(mycnf): - return False - try: - config.readfp(open(mycnf)) - except (IOError): - return False - except: - config = _safe_cnf_load(config, mycnf) - - # We support two forms of passwords in .my.cnf, both pass= and password=, - # as these are both supported by MySQL. - try: - passwd = config_get(config, 'client', 'password') - except (ConfigParser.NoOptionError): - try: - passwd = config_get(config, 'client', 'pass') - except (ConfigParser.NoOptionError): - return False - - # If .my.cnf doesn't specify a user, default to user login name - try: - user = config_get(config, 'client', 'user') - except (ConfigParser.NoOptionError): - user = getpass.getuser() - creds = dict(user=user,passwd=passwd) - return creds - -def connect(module, login_user, login_password): - if module.params["login_unix_socket"]: - db_connection = MySQLdb.connect(host=module.params["login_host"], unix_socket=module.params["login_unix_socket"], user=login_user, passwd=login_password, db="mysql") - else: - db_connection = MySQLdb.connect(host=module.params["login_host"], port=int(module.params["login_port"]), user=login_user, passwd=login_password, db="mysql") - return db_connection.cursor() - -# =========================================== -# Module execution. -# - -def main(): - module = AnsibleModule( - argument_spec = dict( - login_user=dict(default=None), - login_password=dict(default=None), - login_host=dict(default="localhost"), - login_port=dict(default="3306"), - login_unix_socket=dict(default=None), - user=dict(required=True, aliases=['name']), - password=dict(default=None), - host=dict(default="localhost"), - state=dict(default="present", choices=["absent", "present"]), - priv=dict(default=None), - append_privs=dict(type="bool", default="no"), - check_implicit_admin=dict(default=False), - ) - ) - user = module.params["user"] - password = module.params["password"] - host = module.params["host"] - state = module.params["state"] - priv = module.params["priv"] - check_implicit_admin = module.params['check_implicit_admin'] - append_privs = module.boolean(module.params["append_privs"]) - - if not mysqldb_found: - module.fail_json(msg="the python mysqldb module is required") - - if priv is not None: - try: - priv = privileges_unpack(priv) - except: - module.fail_json(msg="invalid privileges string") - - # Either the caller passes both a username and password with which to connect to - # mysql, or they pass neither and allow this module to read the credentials from - # ~/.my.cnf. - login_password = module.params["login_password"] - login_user = module.params["login_user"] - if login_user is None and login_password is None: - mycnf_creds = load_mycnf() - if mycnf_creds is False: - login_user = "root" - login_password = "" - else: - login_user = mycnf_creds["user"] - login_password = mycnf_creds["passwd"] - elif login_password is None or login_user is None: - module.fail_json(msg="when supplying login arguments, both login_user and login_password must be provided") - - cursor = None - try: - if check_implicit_admin: - try: - cursor = connect(module, 'root', '') - except: - pass - - if not cursor: - cursor = connect(module, login_user, login_password) - except Exception, e: - module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or ~/.my.cnf has the credentials") - - if state == "present": - if user_exists(cursor, user, host): - changed = user_mod(cursor, user, host, password, priv, append_privs) - else: - if password is None: - module.fail_json(msg="password parameter required when adding a user") - changed = user_add(cursor, user, host, password, priv) - elif state == "absent": - if user_exists(cursor, user, host): - changed = user_delete(cursor, user, host) - else: - changed = False - module.exit_json(changed=changed, user=user) - -# import module snippets -from ansible.module_utils.basic import * -main() diff --git a/extras/database/mysql_variables b/extras/database/mysql_variables deleted file mode 100644 index 7353fdd485d..00000000000 --- a/extras/database/mysql_variables +++ /dev/null @@ -1,253 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" - -Ansible module to manage mysql variables -(c) 2013, Balazs Pocze -Certain parts are taken from Mark Theunissen's mysqldb module - -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: mysql_variables - -short_description: Manage MySQL global variables -description: - - Query / Set MySQL variables -version_added: 1.3 -options: - variable: - description: - - Variable name to operate - required: True - value: - description: - - If set, then sets variable value to this - required: False - login_user: - description: - - username to connect mysql host, if defined login_password also needed. - required: False - login_password: - description: - - password to connect mysql host, if defined login_user also needed. - required: False - login_host: - description: - - mysql host to connect - required: False - login_unix_socket: - description: - - unix socket to connect mysql server -''' -EXAMPLES = ''' -# Check for sync_binlog setting -- mysql_variables: variable=sync_binlog - -# Set read_only variable to 1 -- mysql_variables: variable=read_only value=1 -''' - - -import ConfigParser -import os -import warnings - -try: - import MySQLdb -except ImportError: - mysqldb_found = False -else: - mysqldb_found = True - - -def typedvalue(value): - """ - Convert value to number whenever possible, return same value - otherwise. - - >>> typedvalue('3') - 3 - >>> typedvalue('3.0') - 3.0 - >>> typedvalue('foobar') - 'foobar' - - """ - try: - return int(value) - except ValueError: - pass - - try: - return float(value) - except ValueError: - pass - - return value - - -def getvariable(cursor, mysqlvar): - cursor.execute("SHOW VARIABLES LIKE '" + mysqlvar + "'") - mysqlvar_val = cursor.fetchall() - return mysqlvar_val - - -def setvariable(cursor, mysqlvar, value): - """ Set a global mysql variable to a given value - - The DB driver will handle quoting of the given value based on its - type, thus numeric strings like '3.0' or '8' are illegal, they - should be passed as numeric literals. - - """ - try: - cursor.execute("SET GLOBAL " + mysqlvar + " = %s", (value,)) - cursor.fetchall() - result = True - except Exception, e: - result = str(e) - return result - - -def strip_quotes(s): - """ Remove surrounding single or double quotes - - >>> print strip_quotes('hello') - hello - >>> print strip_quotes('"hello"') - hello - >>> print strip_quotes("'hello'") - hello - >>> print strip_quotes("'hello") - 'hello - - """ - single_quote = "'" - double_quote = '"' - - if s.startswith(single_quote) and s.endswith(single_quote): - s = s.strip(single_quote) - elif s.startswith(double_quote) and s.endswith(double_quote): - s = s.strip(double_quote) - return s - - -def config_get(config, section, option): - """ Calls ConfigParser.get and strips quotes - - See: http://dev.mysql.com/doc/refman/5.0/en/option-files.html - """ - return strip_quotes(config.get(section, option)) - - -def load_mycnf(): - config = ConfigParser.RawConfigParser() - mycnf = os.path.expanduser('~/.my.cnf') - if not os.path.exists(mycnf): - return False - try: - config.readfp(open(mycnf)) - except (IOError): - return False - # We support two forms of passwords in .my.cnf, both pass= and password=, - # as these are both supported by MySQL. - try: - passwd = config_get(config, 'client', 'password') - except (ConfigParser.NoOptionError): - try: - passwd = config_get(config, 'client', 'pass') - except (ConfigParser.NoOptionError): - return False - - # If .my.cnf doesn't specify a user, default to user login name - try: - user = config_get(config, 'client', 'user') - except (ConfigParser.NoOptionError): - user = getpass.getuser() - creds = dict(user=user, passwd=passwd) - return creds - - -def main(): - module = AnsibleModule( - argument_spec = dict( - login_user=dict(default=None), - login_password=dict(default=None), - login_host=dict(default="localhost"), - login_unix_socket=dict(default=None), - variable=dict(default=None), - value=dict(default=None) - - ) - ) - user = module.params["login_user"] - password = module.params["login_password"] - host = module.params["login_host"] - mysqlvar = module.params["variable"] - value = module.params["value"] - if not mysqldb_found: - module.fail_json(msg="the python mysqldb module is required") - else: - warnings.filterwarnings('error', category=MySQLdb.Warning) - - # Either the caller passes both a username and password with which to connect to - # mysql, or they pass neither and allow this module to read the credentials from - # ~/.my.cnf. - login_password = module.params["login_password"] - login_user = module.params["login_user"] - if login_user is None and login_password is None: - mycnf_creds = load_mycnf() - if mycnf_creds is False: - login_user = "root" - login_password = "" - else: - login_user = mycnf_creds["user"] - login_password = mycnf_creds["passwd"] - elif login_password is None or login_user is None: - module.fail_json(msg="when supplying login arguments, both login_user and login_password must be provided") - try: - if module.params["login_unix_socket"]: - db_connection = MySQLdb.connect(host=module.params["login_host"], unix_socket=module.params["login_unix_socket"], user=login_user, passwd=login_password, db="mysql") - else: - db_connection = MySQLdb.connect(host=module.params["login_host"], user=login_user, passwd=login_password, db="mysql") - cursor = db_connection.cursor() - except Exception, e: - module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or ~/.my.cnf has the credentials") - if mysqlvar is None: - module.fail_json(msg="Cannot run without variable to operate with") - mysqlvar_val = getvariable(cursor, mysqlvar) - if value is None: - module.exit_json(msg=mysqlvar_val) - else: - if len(mysqlvar_val) < 1: - module.fail_json(msg="Variable not available", changed=False) - # Type values before using them - value_wanted = typedvalue(value) - value_actual = typedvalue(mysqlvar_val[0][1]) - if value_wanted == value_actual: - module.exit_json(msg="Variable already set to requested value", changed=False) - result = setvariable(cursor, mysqlvar, value_wanted) - if result is True: - module.exit_json(msg="Variable change succeeded prev_value=%s" % value_actual, changed=True) - else: - module.fail_json(msg=result, changed=False) - -# import module snippets -from ansible.module_utils.basic import * -main() diff --git a/extras/database/postgresql_db b/extras/database/postgresql_db deleted file mode 100644 index 605be621601..00000000000 --- a/extras/database/postgresql_db +++ /dev/null @@ -1,301 +0,0 @@ -#!/usr/bin/python -# -*- 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: postgresql_db -short_description: Add or remove PostgreSQL databases from a remote host. -description: - - Add or remove PostgreSQL databases from a remote host. -version_added: "0.6" -options: - name: - description: - - name of the database to add or remove - required: true - default: null - login_user: - description: - - The username used to authenticate with - required: false - default: null - login_password: - description: - - The password used to authenticate with - required: false - default: null - login_host: - description: - - Host running the database - required: false - default: localhost - owner: - description: - - Name of the role to set as owner of the database - required: false - default: null - port: - description: - - Database port to connect to. - required: false - default: 5432 - template: - description: - - Template used to create the database - required: false - default: null - encoding: - description: - - Encoding of the database - required: false - default: null - encoding: - description: - - Encoding of the database - required: false - default: null - lc_collate: - description: - - Collation order (LC_COLLATE) to use in the database. Must match collation order of template database unless C(template0) is used as template. - required: false - default: null - lc_ctype: - description: - - Character classification (LC_CTYPE) to use in the database (e.g. lower, upper, ...) Must match LC_CTYPE of template database unless C(template0) is used as template. - required: false - default: null - state: - description: - - The database state - required: false - default: present - choices: [ "present", "absent" ] -notes: - - The default authentication assumes that you are either logging in as or sudo'ing to the C(postgres) account on the host. - - This module uses I(psycopg2), a Python PostgreSQL database adapter. You must ensure that psycopg2 is installed on - the host before using this module. If the remote host is the PostgreSQL server (which is the default case), then PostgreSQL must also be installed on the remote host. For Ubuntu-based systems, install the C(postgresql), C(libpq-dev), and C(python-psycopg2) packages on the remote host before using this module. -requirements: [ psycopg2 ] -author: Lorin Hochstein -''' - -EXAMPLES = ''' -# Create a new database with name "acme" -- postgresql_db: name=acme - -# Create a new database with name "acme" and specific encoding and locale -# settings. If a template different from "template0" is specified, encoding -# and locale settings must match those of the template. -- postgresql_db: name=acme - encoding='UTF-8' - lc_collate='de_DE.UTF-8' - lc_ctype='de_DE.UTF-8' - template='template0' -''' - -try: - import psycopg2 - import psycopg2.extras -except ImportError: - postgresqldb_found = False -else: - postgresqldb_found = True - -class NotSupportedError(Exception): - pass - - -# =========================================== -# PostgreSQL module specific support methods. -# - -def set_owner(cursor, db, owner): - query = "ALTER DATABASE \"%s\" OWNER TO \"%s\"" % (db, owner) - cursor.execute(query) - return True - -def get_encoding_id(cursor, encoding): - query = "SELECT pg_char_to_encoding(%(encoding)s) AS encoding_id;" - cursor.execute(query, {'encoding': encoding}) - return cursor.fetchone()['encoding_id'] - -def get_db_info(cursor, db): - query = """ - SELECT rolname AS owner, - pg_encoding_to_char(encoding) AS encoding, encoding AS encoding_id, - datcollate AS lc_collate, datctype AS lc_ctype - FROM pg_database JOIN pg_roles ON pg_roles.oid = pg_database.datdba - WHERE datname = %(db)s - """ - cursor.execute(query, {'db':db}) - return cursor.fetchone() - -def db_exists(cursor, db): - query = "SELECT * FROM pg_database WHERE datname=%(db)s" - cursor.execute(query, {'db': db}) - return cursor.rowcount == 1 - -def db_delete(cursor, db): - if db_exists(cursor, db): - query = "DROP DATABASE \"%s\"" % db - cursor.execute(query) - return True - else: - return False - -def db_create(cursor, db, owner, template, encoding, lc_collate, lc_ctype): - if not db_exists(cursor, db): - if owner: - owner = " OWNER \"%s\"" % owner - if template: - template = " TEMPLATE \"%s\"" % template - if encoding: - encoding = " ENCODING '%s'" % encoding - if lc_collate: - lc_collate = " LC_COLLATE '%s'" % lc_collate - if lc_ctype: - lc_ctype = " LC_CTYPE '%s'" % lc_ctype - query = 'CREATE DATABASE "%s"%s%s%s%s%s' % (db, owner, - template, encoding, - lc_collate, lc_ctype) - cursor.execute(query) - return True - else: - db_info = get_db_info(cursor, db) - if (encoding and - get_encoding_id(cursor, encoding) != db_info['encoding_id']): - raise NotSupportedError( - 'Changing database encoding is not supported. ' - 'Current encoding: %s' % db_info['encoding'] - ) - elif lc_collate and lc_collate != db_info['lc_collate']: - raise NotSupportedError( - 'Changing LC_COLLATE is not supported. ' - 'Current LC_COLLATE: %s' % db_info['lc_collate'] - ) - elif lc_ctype and lc_ctype != db_info['lc_ctype']: - raise NotSupportedError( - 'Changing LC_CTYPE is not supported.' - 'Current LC_CTYPE: %s' % db_info['lc_ctype'] - ) - elif owner and owner != db_info['owner']: - return set_owner(cursor, db, owner) - else: - return False - -def db_matches(cursor, db, owner, template, encoding, lc_collate, lc_ctype): - if not db_exists(cursor, db): - return False - else: - db_info = get_db_info(cursor, db) - if (encoding and - get_encoding_id(cursor, encoding) != db_info['encoding_id']): - return False - elif lc_collate and lc_collate != db_info['lc_collate']: - return False - elif lc_ctype and lc_ctype != db_info['lc_ctype']: - return False - elif owner and owner != db_info['owner']: - return False - else: - return True - -# =========================================== -# Module execution. -# - -def main(): - module = AnsibleModule( - argument_spec=dict( - login_user=dict(default="postgres"), - login_password=dict(default=""), - login_host=dict(default=""), - port=dict(default="5432"), - db=dict(required=True, aliases=['name']), - owner=dict(default=""), - template=dict(default=""), - encoding=dict(default=""), - lc_collate=dict(default=""), - lc_ctype=dict(default=""), - state=dict(default="present", choices=["absent", "present"]), - ), - supports_check_mode = True - ) - - if not postgresqldb_found: - module.fail_json(msg="the python psycopg2 module is required") - - db = module.params["db"] - port = module.params["port"] - owner = module.params["owner"] - template = module.params["template"] - encoding = module.params["encoding"] - lc_collate = module.params["lc_collate"] - lc_ctype = module.params["lc_ctype"] - state = module.params["state"] - changed = False - - # To use defaults values, keyword arguments must be absent, so - # check which values are empty and don't include in the **kw - # dictionary - params_map = { - "login_host":"host", - "login_user":"user", - "login_password":"password", - "port":"port" - } - kw = dict( (params_map[k], v) for (k, v) in module.params.iteritems() - if k in params_map and v != '' ) - try: - db_connection = psycopg2.connect(database="template1", **kw) - # Enable autocommit so we can create databases - if psycopg2.__version__ >= '2.4.2': - db_connection.autocommit = True - else: - db_connection.set_isolation_level(psycopg2 - .extensions - .ISOLATION_LEVEL_AUTOCOMMIT) - cursor = db_connection.cursor( - cursor_factory=psycopg2.extras.DictCursor) - except Exception, e: - module.fail_json(msg="unable to connect to database: %s" % e) - - try: - if module.check_mode: - if state == "absent": - changed = not db_exists(cursor, db) - elif state == "present": - changed = not db_matches(cursor, db, owner, template, encoding, - lc_collate, lc_ctype) - module.exit_json(changed=changed,db=db) - - if state == "absent": - changed = db_delete(cursor, db) - - elif state == "present": - changed = db_create(cursor, db, owner, template, encoding, - lc_collate, lc_ctype) - except NotSupportedError, e: - module.fail_json(msg=str(e)) - except Exception, e: - module.fail_json(msg="Database query failed: %s" % e) - - module.exit_json(changed=changed, db=db) - -# import module snippets -from ansible.module_utils.basic import * -main() diff --git a/extras/database/postgresql_privs b/extras/database/postgresql_privs deleted file mode 100644 index de5fa94fa48..00000000000 --- a/extras/database/postgresql_privs +++ /dev/null @@ -1,613 +0,0 @@ -#!/usr/bin/python -# -*- 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: postgresql_privs -version_added: "1.2" -short_description: Grant or revoke privileges on PostgreSQL database objects. -description: - - Grant or revoke privileges on PostgreSQL database objects. - - This module is basically a wrapper around most of the functionality of - PostgreSQL's GRANT and REVOKE statements with detection of changes - (GRANT/REVOKE I(privs) ON I(type) I(objs) TO/FROM I(roles)) -options: - database: - description: - - Name of database to connect to. - - 'Alias: I(db)' - required: yes - state: - description: - - If C(present), the specified privileges are granted, if C(absent) they - are revoked. - required: no - default: present - choices: [present, absent] - privs: - description: - - Comma separated list of privileges to grant/revoke. - - 'Alias: I(priv)' - required: no - type: - description: - - Type of database object to set privileges on. - required: no - default: table - choices: [table, sequence, function, database, - schema, language, tablespace, group] - objs: - description: - - Comma separated list of database objects to set privileges on. - - If I(type) is C(table) or C(sequence), the special value - C(ALL_IN_SCHEMA) can be provided instead to specify all database - objects of type I(type) in the schema specified via I(schema). (This - also works with PostgreSQL < 9.0.) - - If I(type) is C(database), this parameter can be omitted, in which case - privileges are set for the database specified via I(database). - - 'If I(type) is I(function), colons (":") in object names will be - replaced with commas (needed to specify function signatures, see - examples)' - - 'Alias: I(obj)' - required: no - schema: - description: - - Schema that contains the database objects specified via I(objs). - - May only be provided if I(type) is C(table), C(sequence) or - C(function). Defaults to C(public) in these cases. - required: no - roles: - description: - - Comma separated list of role (user/group) names to set permissions for. - - The special value C(PUBLIC) can be provided instead to set permissions - for the implicitly defined PUBLIC group. - - 'Alias: I(role)' - required: yes - grant_option: - description: - - Whether C(role) may grant/revoke the specified privileges/group - memberships to others. - - Set to C(no) to revoke GRANT OPTION, leave unspecified to - make no changes. - - I(grant_option) only has an effect if I(state) is C(present). - - 'Alias: I(admin_option)' - required: no - choices: ['yes', 'no'] - host: - description: - - Database host address. If unspecified, connect via Unix socket. - - 'Alias: I(login_host)' - default: null - required: no - port: - description: - - Database port to connect to. - required: no - default: 5432 - login: - description: - - The username to authenticate with. - - 'Alias: I(login_user)' - default: postgres - password: - description: - - The password to authenticate with. - - 'Alias: I(login_password))' - default: null - required: no -notes: - - Default authentication assumes that postgresql_privs is run by the - C(postgres) user on the remote host. (Ansible's C(user) or C(sudo-user)). - - This module requires Python package I(psycopg2) to be installed on the - remote host. In the default case of the remote host also being the - PostgreSQL server, PostgreSQL has to be installed there as well, obviously. - For Debian/Ubuntu-based systems, install packages I(postgresql) and - I(python-psycopg2). - - Parameters that accept comma separated lists (I(privs), I(objs), I(roles)) - have singular alias names (I(priv), I(obj), I(role)). - - To revoke only C(GRANT OPTION) for a specific object, set I(state) to - C(present) and I(grant_option) to C(no) (see examples). - - Note that when revoking privileges from a role R, this role may still have - access via privileges granted to any role R is a member of including - C(PUBLIC). - - Note that when revoking privileges from a role R, you do so as the user - specified via I(login). If R has been granted the same privileges by - another user also, R can still access database objects via these privileges. - - When revoking privileges, C(RESTRICT) is assumed (see PostgreSQL docs). -requirements: [psycopg2] -author: Bernhard Weitzhofer -""" - -EXAMPLES = """ -# On database "library": -# GRANT SELECT, INSERT, UPDATE ON TABLE public.books, public.authors -# TO librarian, reader WITH GRANT OPTION -- postgresql_privs: > - database=library - state=present - privs=SELECT,INSERT,UPDATE - type=table - objs=books,authors - schema=public - roles=librarian,reader - grant_option=yes - -# Same as above leveraging default values: -- postgresql_privs: > - db=library - privs=SELECT,INSERT,UPDATE - objs=books,authors - roles=librarian,reader - grant_option=yes - -# REVOKE GRANT OPTION FOR INSERT ON TABLE books FROM reader -# Note that role "reader" will be *granted* INSERT privilege itself if this -# isn't already the case (since state=present). -- postgresql_privs: > - db=library - state=present - priv=INSERT - obj=books - role=reader - grant_option=no - -# REVOKE INSERT, UPDATE ON ALL TABLES IN SCHEMA public FROM reader -# "public" is the default schema. This also works for PostgreSQL 8.x. -- postgresql_privs: > - db=library - state=absent - privs=INSERT,UPDATE - objs=ALL_IN_SCHEMA - role=reader - -# GRANT ALL PRIVILEGES ON SCHEMA public, math TO librarian -- postgresql_privs: > - db=library - privs=ALL - type=schema - objs=public,math - role=librarian - -# GRANT ALL PRIVILEGES ON FUNCTION math.add(int, int) TO librarian, reader -# Note the separation of arguments with colons. -- postgresql_privs: > - db=library - privs=ALL - type=function - obj=add(int:int) - schema=math - roles=librarian,reader - -# GRANT librarian, reader TO alice, bob WITH ADMIN OPTION -# Note that group role memberships apply cluster-wide and therefore are not -# restricted to database "library" here. -- postgresql_privs: > - db=library - type=group - objs=librarian,reader - roles=alice,bob - admin_option=yes - -# GRANT ALL PRIVILEGES ON DATABASE library TO librarian -# Note that here "db=postgres" specifies the database to connect to, not the -# database to grant privileges on (which is specified via the "objs" param) -- postgresql_privs: > - db=postgres - privs=ALL - type=database - obj=library - role=librarian - -# GRANT ALL PRIVILEGES ON DATABASE library TO librarian -# If objs is omitted for type "database", it defaults to the database -# to which the connection is established -- postgresql_privs: > - db=library - privs=ALL - type=database - role=librarian -""" - -try: - import psycopg2 - import psycopg2.extensions -except ImportError: - psycopg2 = None - - -class Error(Exception): - pass - - -# We don't have functools.partial in Python < 2.5 -def partial(f, *args, **kwargs): - """Partial function application""" - def g(*g_args, **g_kwargs): - new_kwargs = kwargs.copy() - new_kwargs.update(g_kwargs) - return f(*(args + g_args), **g_kwargs) - g.f = f - g.args = args - g.kwargs = kwargs - return g - - -class Connection(object): - """Wrapper around a psycopg2 connection with some convenience methods""" - - def __init__(self, params): - self.database = params.database - # To use defaults values, keyword arguments must be absent, so - # check which values are empty and don't include in the **kw - # dictionary - params_map = { - "host":"host", - "login":"user", - "password":"password", - "port":"port", - "database": "database", - } - kw = dict( (params_map[k], getattr(params, k)) for k in params_map - if getattr(params, k) != '' ) - self.connection = psycopg2.connect(**kw) - self.cursor = self.connection.cursor() - - - def commit(self): - self.connection.commit() - - - def rollback(self): - self.connection.rollback() - - @property - def encoding(self): - """Connection encoding in Python-compatible form""" - return psycopg2.extensions.encodings[self.connection.encoding] - - - ### Methods for querying database objects - - # PostgreSQL < 9.0 doesn't support "ALL TABLES IN SCHEMA schema"-like - # phrases in GRANT or REVOKE statements, therefore alternative methods are - # provided here. - - def schema_exists(self, schema): - query = """SELECT count(*) - FROM pg_catalog.pg_namespace WHERE nspname = %s""" - self.cursor.execute(query, (schema,)) - return self.cursor.fetchone()[0] > 0 - - - def get_all_tables_in_schema(self, schema): - if not self.schema_exists(schema): - raise Error('Schema "%s" does not exist.' % schema) - query = """SELECT relname - FROM pg_catalog.pg_class c - JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace - WHERE nspname = %s AND relkind = 'r'""" - self.cursor.execute(query, (schema,)) - return [t[0] for t in self.cursor.fetchall()] - - - def get_all_sequences_in_schema(self, schema): - if not self.schema_exists(schema): - raise Error('Schema "%s" does not exist.' % schema) - query = """SELECT relname - FROM pg_catalog.pg_class c - JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace - WHERE nspname = %s AND relkind = 'S'""" - self.cursor.execute(query, (schema,)) - return [t[0] for t in self.cursor.fetchall()] - - - - ### Methods for getting access control lists and group membership info - - # To determine whether anything has changed after granting/revoking - # privileges, we compare the access control lists of the specified database - # objects before and afterwards. Python's list/string comparison should - # suffice for change detection, we should not actually have to parse ACLs. - # The same should apply to group membership information. - - def get_table_acls(self, schema, tables): - query = """SELECT relacl - FROM pg_catalog.pg_class c - JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace - WHERE nspname = %s AND relkind = 'r' AND relname = ANY (%s) - ORDER BY relname""" - self.cursor.execute(query, (schema, tables)) - return [t[0] for t in self.cursor.fetchall()] - - - def get_sequence_acls(self, schema, sequences): - query = """SELECT relacl - FROM pg_catalog.pg_class c - JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace - WHERE nspname = %s AND relkind = 'S' AND relname = ANY (%s) - ORDER BY relname""" - self.cursor.execute(query, (schema, sequences)) - return [t[0] for t in self.cursor.fetchall()] - - - def get_function_acls(self, schema, function_signatures): - funcnames = [f.split('(', 1)[0] for f in function_signatures] - query = """SELECT proacl - FROM pg_catalog.pg_proc p - JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace - WHERE nspname = %s AND proname = ANY (%s) - ORDER BY proname, proargtypes""" - self.cursor.execute(query, (schema, funcnames)) - return [t[0] for t in self.cursor.fetchall()] - - - def get_schema_acls(self, schemas): - query = """SELECT nspacl FROM pg_catalog.pg_namespace - WHERE nspname = ANY (%s) ORDER BY nspname""" - self.cursor.execute(query, (schemas,)) - return [t[0] for t in self.cursor.fetchall()] - - - def get_language_acls(self, languages): - query = """SELECT lanacl FROM pg_catalog.pg_language - WHERE lanname = ANY (%s) ORDER BY lanname""" - self.cursor.execute(query, (languages,)) - return [t[0] for t in self.cursor.fetchall()] - - - def get_tablespace_acls(self, tablespaces): - query = """SELECT spcacl FROM pg_catalog.pg_tablespace - WHERE spcname = ANY (%s) ORDER BY spcname""" - self.cursor.execute(query, (tablespaces,)) - return [t[0] for t in self.cursor.fetchall()] - - - def get_database_acls(self, databases): - query = """SELECT datacl FROM pg_catalog.pg_database - WHERE datname = ANY (%s) ORDER BY datname""" - self.cursor.execute(query, (databases,)) - return [t[0] for t in self.cursor.fetchall()] - - - def get_group_memberships(self, groups): - query = """SELECT roleid, grantor, member, admin_option - FROM pg_catalog.pg_auth_members am - JOIN pg_catalog.pg_roles r ON r.oid = am.roleid - WHERE r.rolname = ANY(%s) - ORDER BY roleid, grantor, member""" - self.cursor.execute(query, (groups,)) - return self.cursor.fetchall() - - - ### Manipulating privileges - - def manipulate_privs(self, obj_type, privs, objs, roles, - state, grant_option, schema_qualifier=None): - """Manipulate database object privileges. - - :param obj_type: Type of database object to grant/revoke - privileges for. - :param privs: Either a list of privileges to grant/revoke - or None if type is "group". - :param objs: List of database objects to grant/revoke - privileges for. - :param roles: Either a list of role names or "PUBLIC" - for the implicitly defined "PUBLIC" group - :param state: "present" to grant privileges, "absent" to revoke. - :param grant_option: Only for state "present": If True, set - grant/admin option. If False, revoke it. - If None, don't change grant option. - :param schema_qualifier: Some object types ("TABLE", "SEQUENCE", - "FUNCTION") must be qualified by schema. - Ignored for other Types. - """ - # get_status: function to get current status - if obj_type == 'table': - get_status = partial(self.get_table_acls, schema_qualifier) - elif obj_type == 'sequence': - get_status = partial(self.get_sequence_acls, schema_qualifier) - elif obj_type == 'function': - get_status = partial(self.get_function_acls, schema_qualifier) - elif obj_type == 'schema': - get_status = self.get_schema_acls - elif obj_type == 'language': - get_status = self.get_language_acls - elif obj_type == 'tablespace': - get_status = self.get_tablespace_acls - elif obj_type == 'database': - get_status = self.get_database_acls - elif obj_type == 'group': - get_status = self.get_group_memberships - else: - raise Error('Unsupported database object type "%s".' % obj_type) - - # Return False (nothing has changed) if there are no objs to work on. - if not objs: - return False - - # obj_ids: quoted db object identifiers (sometimes schema-qualified) - if obj_type == 'function': - obj_ids = [] - for obj in objs: - try: - f, args = obj.split('(', 1) - except: - raise Error('Illegal function signature: "%s".' % obj) - obj_ids.append('"%s"."%s"(%s' % (schema_qualifier, f, args)) - elif obj_type in ['table', 'sequence']: - obj_ids = ['"%s"."%s"' % (schema_qualifier, o) for o in objs] - else: - obj_ids = ['"%s"' % o for o in objs] - - # set_what: SQL-fragment specifying what to set for the target roless: - # Either group membership or privileges on objects of a certain type. - if obj_type == 'group': - set_what = ','.join(obj_ids) - else: - set_what = '%s ON %s %s' % (','.join(privs), obj_type, - ','.join(obj_ids)) - - # for_whom: SQL-fragment specifying for whom to set the above - if roles == 'PUBLIC': - for_whom = 'PUBLIC' - else: - for_whom = ','.join(['"%s"' % r for r in roles]) - - status_before = get_status(objs) - if state == 'present': - if grant_option: - if obj_type == 'group': - query = 'GRANT %s TO %s WITH ADMIN OPTION' - else: - query = 'GRANT %s TO %s WITH GRANT OPTION' - else: - query = 'GRANT %s TO %s' - self.cursor.execute(query % (set_what, for_whom)) - - # Only revoke GRANT/ADMIN OPTION if grant_option actually is False. - if grant_option == False: - if obj_type == 'group': - query = 'REVOKE ADMIN OPTION FOR %s FROM %s' - else: - query = 'REVOKE GRANT OPTION FOR %s FROM %s' - self.cursor.execute(query % (set_what, for_whom)) - else: - query = 'REVOKE %s FROM %s' - self.cursor.execute(query % (set_what, for_whom)) - status_after = get_status(objs) - return status_before != status_after - - -def main(): - module = AnsibleModule( - argument_spec = dict( - database=dict(required=True, aliases=['db']), - state=dict(default='present', choices=['present', 'absent']), - privs=dict(required=False, aliases=['priv']), - type=dict(default='table', - choices=['table', - 'sequence', - 'function', - 'database', - 'schema', - 'language', - 'tablespace', - 'group']), - objs=dict(required=False, aliases=['obj']), - schema=dict(required=False), - roles=dict(required=True, aliases=['role']), - grant_option=dict(required=False, type='bool', - aliases=['admin_option']), - host=dict(default='', aliases=['login_host']), - port=dict(type='int', default=5432), - login=dict(default='postgres', aliases=['login_user']), - password=dict(default='', aliases=['login_password']) - ), - supports_check_mode = True - ) - - # Create type object as namespace for module params - p = type('Params', (), module.params) - - # param "schema": default, allowed depends on param "type" - if p.type in ['table', 'sequence', 'function']: - p.schema = p.schema or 'public' - elif p.schema: - module.fail_json(msg='Argument "schema" is not allowed ' - 'for type "%s".' % p.type) - - # param "objs": default, required depends on param "type" - if p.type == 'database': - p.objs = p.objs or p.database - elif not p.objs: - module.fail_json(msg='Argument "objs" is required ' - 'for type "%s".' % p.type) - - # param "privs": allowed, required depends on param "type" - if p.type == 'group': - if p.privs: - module.fail_json(msg='Argument "privs" is not allowed ' - 'for type "group".') - elif not p.privs: - module.fail_json(msg='Argument "privs" is required ' - 'for type "%s".' % p.type) - - # Connect to Database - if not psycopg2: - module.fail_json(msg='Python module "psycopg2" must be installed.') - try: - conn = Connection(p) - except psycopg2.Error, e: - module.fail_json(msg='Could not connect to database: %s' % e) - - try: - # privs - if p.privs: - privs = p.privs.split(',') - else: - privs = None - - # objs: - if p.type == 'table' and p.objs == 'ALL_IN_SCHEMA': - objs = conn.get_all_tables_in_schema(p.schema) - elif p.type == 'sequence' and p.objs == 'ALL_IN_SCHEMA': - objs = conn.get_all_sequences_in_schema(p.schema) - else: - objs = p.objs.split(',') - - # function signatures are encoded using ':' to separate args - if p.type == 'function': - objs = [obj.replace(':', ',') for obj in objs] - - # roles - if p.roles == 'PUBLIC': - roles = 'PUBLIC' - else: - roles = p.roles.split(',') - - changed = conn.manipulate_privs( - obj_type = p.type, - privs = privs, - objs = objs, - roles = roles, - state = p.state, - grant_option = p.grant_option, - schema_qualifier=p.schema - ) - - except Error, e: - conn.rollback() - module.fail_json(msg=e.message) - - except psycopg2.Error, e: - conn.rollback() - # psycopg2 errors come in connection encoding, reencode - msg = e.message.decode(conn.encoding).encode(sys.getdefaultencoding(), - 'replace') - module.fail_json(msg=msg) - - if module.check_mode: - conn.rollback() - else: - conn.commit() - module.exit_json(changed=changed) - - -# import module snippets -from ansible.module_utils.basic import * -main() diff --git a/extras/database/postgresql_user b/extras/database/postgresql_user deleted file mode 100644 index 8af8c45d0c5..00000000000 --- a/extras/database/postgresql_user +++ /dev/null @@ -1,526 +0,0 @@ -#!/usr/bin/python -# -*- 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: postgresql_user -short_description: Adds or removes a users (roles) from a PostgreSQL database. -description: - - Add or remove PostgreSQL users (roles) from a remote host and, optionally, - grant the users access to an existing database or tables. - - The fundamental function of the module is to create, or delete, roles from - a PostgreSQL cluster. Privilege assignment, or removal, is an optional - step, which works on one database at a time. This allows for the module to - be called several times in the same module to modify the permissions on - different databases, or to grant permissions to already existing users. - - A user cannot be removed until all the privileges have been stripped from - the user. In such situation, if the module tries to remove the user it - will fail. To avoid this from happening the fail_on_user option signals - the module to try to remove the user, but if not possible keep going; the - module will report if changes happened and separately if the user was - removed or not. -version_added: "0.6" -options: - name: - description: - - name of the user (role) to add or remove - required: true - default: null - password: - description: - - set the user's password, before 1.4 this was required. - - "When passing an encrypted password, the encrypted parameter must also be true, and it must be generated with the format C('str[\\"md5\\"] + md5[ password + username ]'), resulting in a total of 35 characters. An easy way to do this is: C(echo \\"md5`echo -n \\"verysecretpasswordJOE\\" | md5`\\")." - required: false - default: null - db: - description: - - name of database where permissions will be granted - required: false - default: null - fail_on_user: - description: - - if C(yes), fail when user can't be removed. Otherwise just log and continue - required: false - default: 'yes' - choices: [ "yes", "no" ] - port: - description: - - Database port to connect to. - required: false - default: 5432 - login_user: - description: - - User (role) used to authenticate with PostgreSQL - required: false - default: postgres - login_password: - description: - - Password used to authenticate with PostgreSQL - required: false - default: null - login_host: - description: - - Host running PostgreSQL. - required: false - default: localhost - priv: - description: - - "PostgreSQL privileges string in the format: C(table:priv1,priv2)" - required: false - default: null - role_attr_flags: - description: - - "PostgreSQL role attributes string in the format: CREATEDB,CREATEROLE,SUPERUSER" - required: false - default: null - choices: [ "[NO]SUPERUSER","[NO]CREATEROLE", "[NO]CREATEUSER", "[NO]CREATEDB", - "[NO]INHERIT", "[NO]LOGIN", "[NO]REPLICATION" ] - state: - description: - - The user (role) state - required: false - default: present - choices: [ "present", "absent" ] - encrypted: - description: - - denotes if the password is already encrypted. boolean. - required: false - default: false - version_added: '1.4' - expires: - description: - - sets the user's password expiration. - required: false - default: null - version_added: '1.4' -notes: - - The default authentication assumes that you are either logging in as or - sudo'ing to the postgres account on the host. - - This module uses psycopg2, a Python PostgreSQL database adapter. You must - ensure that psycopg2 is installed on the host before using this module. If - the remote host is the PostgreSQL server (which is the default case), then - PostgreSQL must also be installed on the remote host. For Ubuntu-based - systems, install the postgresql, libpq-dev, and python-psycopg2 packages - on the remote host before using this module. - - If you specify PUBLIC as the user, then the privilege changes will apply - to all users. You may not specify password or role_attr_flags when the - PUBLIC user is specified. -requirements: [ psycopg2 ] -author: Lorin Hochstein -''' - -EXAMPLES = ''' -# Create django user and grant access to database and products table -- postgresql_user: db=acme name=django password=ceec4eif7ya priv=CONNECT/products:ALL - -# Create rails user, grant privilege to create other databases and demote rails from super user status -- postgresql_user: name=rails password=secret role_attr_flags=CREATEDB,NOSUPERUSER - -# Remove test user privileges from acme -- postgresql_user: db=acme name=test priv=ALL/products:ALL state=absent fail_on_user=no - -# Remove test user from test database and the cluster -- postgresql_user: db=test name=test priv=ALL state=absent - -# Example privileges string format -INSERT,UPDATE/table:SELECT/anothertable:ALL - -# Remove an existing user's password -- postgresql_user: db=test user=test password=NULL -''' - -import re - -try: - import psycopg2 -except ImportError: - postgresqldb_found = False -else: - postgresqldb_found = True - -# =========================================== -# PostgreSQL module specific support methods. -# - - -def user_exists(cursor, user): - # The PUBLIC user is a special case that is always there - if user == 'PUBLIC': - return True - query = "SELECT rolname FROM pg_roles WHERE rolname=%(user)s" - cursor.execute(query, {'user': user}) - return cursor.rowcount > 0 - - -def user_add(cursor, user, password, role_attr_flags, encrypted, expires): - """Create a new database user (role).""" - query_password_data = dict() - query = 'CREATE USER "%(user)s"' % { "user": user} - if password is not None: - query = query + " WITH %(crypt)s" % { "crypt": encrypted } - query = query + " PASSWORD %(password)s" - query_password_data.update(password=password) - if expires is not None: - query = query + " VALID UNTIL '%(expires)s'" % { "expires": expires } - query = query + " " + role_attr_flags - cursor.execute(query, query_password_data) - return True - -def user_alter(cursor, module, user, password, role_attr_flags, encrypted, expires): - """Change user password and/or attributes. Return True if changed, False otherwise.""" - changed = False - - if user == 'PUBLIC': - if password is not None: - module.fail_json(msg="cannot change the password for PUBLIC user") - elif role_attr_flags != '': - module.fail_json(msg="cannot change the role_attr_flags for PUBLIC user") - else: - return False - - # Handle passwords. - if password is not None or role_attr_flags is not None: - # Select password and all flag-like columns in order to verify changes. - query_password_data = dict() - select = "SELECT * FROM pg_authid where rolname=%(user)s" - cursor.execute(select, {"user": user}) - # Grab current role attributes. - current_role_attrs = cursor.fetchone() - - alter = 'ALTER USER "%(user)s"' % {"user": user} - if password is not None: - query_password_data.update(password=password) - alter = alter + " WITH %(crypt)s" % {"crypt": encrypted} - alter = alter + " PASSWORD %(password)s" - alter = alter + " %(flags)s" % {'flags': role_attr_flags} - elif role_attr_flags: - alter = alter + ' WITH ' + role_attr_flags - if expires is not None: - alter = alter + " VALID UNTIL '%(expires)s'" % { "exipres": expires } - - try: - cursor.execute(alter, query_password_data) - except psycopg2.InternalError, e: - if e.pgcode == '25006': - # Handle errors due to read-only transactions indicated by pgcode 25006 - # ERROR: cannot execute ALTER ROLE in a read-only transaction - changed = False - module.fail_json(msg=e.pgerror) - return changed - else: - raise psycopg2.InternalError, e - - # Grab new role attributes. - cursor.execute(select, {"user": user}) - new_role_attrs = cursor.fetchone() - - # Detect any differences between current_ and new_role_attrs. - for i in range(len(current_role_attrs)): - if current_role_attrs[i] != new_role_attrs[i]: - changed = True - - return changed - -def user_delete(cursor, user): - """Try to remove a user. Returns True if successful otherwise False""" - cursor.execute("SAVEPOINT ansible_pgsql_user_delete") - try: - cursor.execute("DROP USER \"%s\"" % user) - except: - cursor.execute("ROLLBACK TO SAVEPOINT ansible_pgsql_user_delete") - cursor.execute("RELEASE SAVEPOINT ansible_pgsql_user_delete") - return False - - cursor.execute("RELEASE SAVEPOINT ansible_pgsql_user_delete") - return True - -def has_table_privilege(cursor, user, table, priv): - query = 'SELECT has_table_privilege(%s, %s, %s)' - cursor.execute(query, (user, table, priv)) - return cursor.fetchone()[0] - -def get_table_privileges(cursor, user, table): - if '.' in table: - schema, table = table.split('.', 1) - else: - schema = 'public' - query = '''SELECT privilege_type FROM information_schema.role_table_grants - WHERE grantee=%s AND table_name=%s AND table_schema=%s''' - cursor.execute(query, (user, table, schema)) - return set([x[0] for x in cursor.fetchall()]) - - -def quote_pg_identifier(identifier): - """ - quote postgresql identifiers involving zero or more namespaces - """ - - if '"' in identifier: - # the user has supplied their own quoting. we have to hope they're - # doing it right. Maybe they have an unfortunately named table - # containing a period in the name, such as: "public"."users.2013" - return identifier - - tokens = identifier.strip().split(".") - quoted_tokens = [] - for token in tokens: - quoted_tokens.append('"%s"' % (token, )) - return ".".join(quoted_tokens) - -def grant_table_privilege(cursor, user, table, priv): - prev_priv = get_table_privileges(cursor, user, table) - query = 'GRANT %s ON TABLE %s TO %s' % ( - priv, quote_pg_identifier(table), quote_pg_identifier(user), ) - cursor.execute(query) - curr_priv = get_table_privileges(cursor, user, table) - return len(curr_priv) > len(prev_priv) - -def revoke_table_privilege(cursor, user, table, priv): - prev_priv = get_table_privileges(cursor, user, table) - query = 'REVOKE %s ON TABLE %s FROM %s' % ( - priv, quote_pg_identifier(table), quote_pg_identifier(user), ) - cursor.execute(query) - curr_priv = get_table_privileges(cursor, user, table) - return len(curr_priv) < len(prev_priv) - - -def get_database_privileges(cursor, user, db): - priv_map = { - 'C':'CREATE', - 'T':'TEMPORARY', - 'c':'CONNECT', - } - query = 'SELECT datacl FROM pg_database WHERE datname = %s' - cursor.execute(query, (db,)) - datacl = cursor.fetchone()[0] - if datacl is None: - return [] - r = re.search('%s=(C?T?c?)/[a-z]+\,?' % user, datacl) - if r is None: - return [] - o = [] - for v in r.group(1): - o.append(priv_map[v]) - return o - -def has_database_privilege(cursor, user, db, priv): - query = 'SELECT has_database_privilege(%s, %s, %s)' - cursor.execute(query, (user, db, priv)) - return cursor.fetchone()[0] - -def grant_database_privilege(cursor, user, db, priv): - prev_priv = get_database_privileges(cursor, user, db) - if user == "PUBLIC": - query = 'GRANT %s ON DATABASE \"%s\" TO PUBLIC' % (priv, db) - else: - query = 'GRANT %s ON DATABASE \"%s\" TO \"%s\"' % (priv, db, user) - cursor.execute(query) - curr_priv = get_database_privileges(cursor, user, db) - return len(curr_priv) > len(prev_priv) - -def revoke_database_privilege(cursor, user, db, priv): - prev_priv = get_database_privileges(cursor, user, db) - if user == "PUBLIC": - query = 'REVOKE %s ON DATABASE \"%s\" FROM PUBLIC' % (priv, db) - else: - query = 'REVOKE %s ON DATABASE \"%s\" FROM \"%s\"' % (priv, db, user) - cursor.execute(query) - curr_priv = get_database_privileges(cursor, user, db) - return len(curr_priv) < len(prev_priv) - -def revoke_privileges(cursor, user, privs): - if privs is None: - return False - - changed = False - for type_ in privs: - revoke_func = { - 'table':revoke_table_privilege, - 'database':revoke_database_privilege - }[type_] - for name, privileges in privs[type_].iteritems(): - for privilege in privileges: - changed = revoke_func(cursor, user, name, privilege)\ - or changed - - return changed - -def grant_privileges(cursor, user, privs): - if privs is None: - return False - - changed = False - for type_ in privs: - grant_func = { - 'table':grant_table_privilege, - 'database':grant_database_privilege - }[type_] - for name, privileges in privs[type_].iteritems(): - for privilege in privileges: - changed = grant_func(cursor, user, name, privilege)\ - or changed - - return changed - -def parse_role_attrs(role_attr_flags): - """ - Parse role attributes string for user creation. - Format: - - attributes[,attributes,...] - - Where: - - attributes := CREATEDB,CREATEROLE,NOSUPERUSER,... - """ - if ',' not in role_attr_flags: - return role_attr_flags - flag_set = role_attr_flags.split(",") - o_flags = " ".join(flag_set) - return o_flags - -def parse_privs(privs, db): - """ - Parse privilege string to determine permissions for database db. - Format: - - privileges[/privileges/...] - - Where: - - privileges := DATABASE_PRIVILEGES[,DATABASE_PRIVILEGES,...] | - TABLE_NAME:TABLE_PRIVILEGES[,TABLE_PRIVILEGES,...] - """ - if privs is None: - return privs - - o_privs = { - 'database':{}, - 'table':{} - } - for token in privs.split('/'): - if ':' not in token: - type_ = 'database' - name = db - priv_set = set(x.strip() for x in token.split(',')) - else: - type_ = 'table' - name, privileges = token.split(':', 1) - priv_set = set(x.strip() for x in privileges.split(',')) - - o_privs[type_][name] = priv_set - - return o_privs - -# =========================================== -# Module execution. -# - -def main(): - module = AnsibleModule( - argument_spec=dict( - login_user=dict(default="postgres"), - login_password=dict(default=""), - login_host=dict(default=""), - user=dict(required=True, aliases=['name']), - password=dict(default=None), - state=dict(default="present", choices=["absent", "present"]), - priv=dict(default=None), - db=dict(default=''), - port=dict(default='5432'), - fail_on_user=dict(type='bool', default='yes'), - role_attr_flags=dict(default=''), - encrypted=dict(type='bool', default='no'), - expires=dict(default=None) - ), - supports_check_mode = True - ) - - user = module.params["user"] - password = module.params["password"] - state = module.params["state"] - fail_on_user = module.params["fail_on_user"] - db = module.params["db"] - if db == '' and module.params["priv"] is not None: - module.fail_json(msg="privileges require a database to be specified") - privs = parse_privs(module.params["priv"], db) - port = module.params["port"] - role_attr_flags = parse_role_attrs(module.params["role_attr_flags"]) - if module.params["encrypted"]: - encrypted = "ENCRYPTED" - else: - encrypted = "UNENCRYPTED" - expires = module.params["expires"] - - if not postgresqldb_found: - module.fail_json(msg="the python psycopg2 module is required") - - # To use defaults values, keyword arguments must be absent, so - # check which values are empty and don't include in the **kw - # dictionary - params_map = { - "login_host":"host", - "login_user":"user", - "login_password":"password", - "port":"port", - "db":"database" - } - kw = dict( (params_map[k], v) for (k, v) in module.params.iteritems() - if k in params_map and v != "" ) - try: - db_connection = psycopg2.connect(**kw) - cursor = db_connection.cursor() - except Exception, e: - module.fail_json(msg="unable to connect to database: %s" % e) - - kw = dict(user=user) - changed = False - user_removed = False - - if state == "present": - if user_exists(cursor, user): - changed = user_alter(cursor, module, user, password, role_attr_flags, encrypted, expires) - else: - changed = user_add(cursor, user, password, role_attr_flags, encrypted, expires) - changed = grant_privileges(cursor, user, privs) or changed - else: - if user_exists(cursor, user): - if module.check_mode: - changed = True - kw['user_removed'] = True - else: - changed = revoke_privileges(cursor, user, privs) - user_removed = user_delete(cursor, user) - changed = changed or user_removed - if fail_on_user and not user_removed: - msg = "unable to remove user" - module.fail_json(msg=msg) - kw['user_removed'] = user_removed - - if changed: - if module.check_mode: - db_connection.rollback() - else: - db_connection.commit() - - kw['changed'] = changed - module.exit_json(**kw) - -# import module snippets -from ansible.module_utils.basic import * -main() diff --git a/extras/files/acl b/extras/files/acl deleted file mode 100644 index 30c533e006c..00000000000 --- a/extras/files/acl +++ /dev/null @@ -1,295 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -DOCUMENTATION = ''' ---- -module: acl -version_added: "1.4" -short_description: Sets and retrieves file ACL information. -description: - - Sets and retrieves file ACL information. -options: - name: - required: true - default: null - description: - - The full path of the file or object. - aliases: ['path'] - - state: - required: false - default: query - choices: [ 'query', 'present', 'absent' ] - description: - - defines whether the ACL should be present or not. The C(query) state gets the current acl without changing it, for use in 'register' operations. - - follow: - required: false - default: yes - choices: [ 'yes', 'no' ] - description: - - whether to follow symlinks on the path if a symlink is encountered. - - default: - version_added: "1.5" - required: false - default: no - choices: [ 'yes', 'no' ] - description: - - if the target is a directory, setting this to yes will make it the default acl for entities created inside the directory. It causes an error if name is a file. - - entity: - version_added: "1.5" - required: false - description: - - actual user or group that the ACL applies to when matching entity types user or group are selected. - - etype: - version_added: "1.5" - required: false - default: null - choices: [ 'user', 'group', 'mask', 'other' ] - description: - - the entity type of the ACL to apply, see setfacl documentation for more info. - - - permissions: - version_added: "1.5" - required: false - default: null - description: - - Permissions to apply/remove can be any combination of r, w and x (read, write and execute respectively) - - entry: - required: false - default: null - description: - - DEPRECATED. The acl to set or remove. This must always be quoted in the form of '::'. The qualifier may be empty for some types, but the type and perms are always requried. '-' can be used as placeholder when you do not care about permissions. This is now superseded by entity, type and permissions fields. - -author: Brian Coca -notes: - - The "acl" module requires that acls are enabled on the target filesystem and that the setfacl and getfacl binaries are installed. -''' - -EXAMPLES = ''' -# Grant user Joe read access to a file -- acl: name=/etc/foo.conf entity=joe etype=user permissions="r" state=present - -# Removes the acl for Joe on a specific file -- acl: name=/etc/foo.conf entity=joe etype=user state=absent - -# Sets default acl for joe on foo.d -- acl: name=/etc/foo.d entity=joe etype=user permissions=rw default=yes state=present - -# Same as previous but using entry shorthand -- acl: name=/etc/foo.d entry="default:user:joe:rw-" state=present - -# Obtain the acl for a specific file -- acl: name=/etc/foo.conf - register: acl_info -''' - -def normalize_permissions(p): - perms = ['-','-','-'] - for char in p: - if char == 'r': - perms[0] = 'r' - if char == 'w': - perms[1] = 'w' - if char == 'x': - perms[2] = 'x' - return ''.join(perms) - -def split_entry(entry): - ''' splits entry and ensures normalized return''' - - a = entry.split(':') - a.reverse() - if len(a) == 3: - a.append(False) - try: - p,e,t,d = a - except ValueError, e: - print "wtf?? %s => %s" % (entry,a) - raise e - - if d: - d = True - - if t.startswith("u"): - t = "user" - elif t.startswith("g"): - t = "group" - elif t.startswith("m"): - t = "mask" - elif t.startswith("o"): - t = "other" - else: - t = None - - p = normalize_permissions(p) - - return [d,t,e,p] - -def get_acls(module,path,follow): - - cmd = [ module.get_bin_path('getfacl', True) ] - if not follow: - cmd.append('-h') - # prevents absolute path warnings and removes headers - cmd.append('--omit-header') - cmd.append('--absolute-names') - cmd.append(path) - - return _run_acl(module,cmd) - -def set_acl(module,path,entry,follow,default): - - cmd = [ module.get_bin_path('setfacl', True) ] - if not follow: - cmd.append('-h') - if default: - cmd.append('-d') - cmd.append('-m "%s"' % entry) - cmd.append(path) - - return _run_acl(module,cmd) - -def rm_acl(module,path,entry,follow,default): - - cmd = [ module.get_bin_path('setfacl', True) ] - if not follow: - cmd.append('-h') - if default: - cmd.append('-k') - entry = entry[0:entry.rfind(':')] - cmd.append('-x "%s"' % entry) - cmd.append(path) - - return _run_acl(module,cmd,False) - -def _run_acl(module,cmd,check_rc=True): - - try: - (rc, out, err) = module.run_command(' '.join(cmd), check_rc=check_rc) - except Exception, e: - module.fail_json(msg=e.strerror) - - # trim last line as it is always empty - ret = out.splitlines() - return ret[0:len(ret)-1] - -def main(): - module = AnsibleModule( - argument_spec = dict( - name = dict(required=True,aliases=['path'], type='str'), - entry = dict(required=False, etype='str'), - entity = dict(required=False, type='str', default=''), - etype = dict(required=False, choices=['other', 'user', 'group', 'mask'], type='str'), - permissions = dict(required=False, type='str'), - state = dict(required=False, default='query', choices=[ 'query', 'present', 'absent' ], type='str'), - follow = dict(required=False, type='bool', default=True), - default= dict(required=False, type='bool', default=False), - ), - supports_check_mode=True, - ) - - path = os.path.expanduser(module.params.get('name')) - entry = module.params.get('entry') - entity = module.params.get('entity') - etype = module.params.get('etype') - permissions = module.params.get('permissions') - state = module.params.get('state') - follow = module.params.get('follow') - default = module.params.get('default') - - if permissions: - permissions = normalize_permissions(permissions) - - if not os.path.exists(path): - module.fail_json(msg="path not found or not accessible!") - - if state in ['present','absent']: - if not entry and not etype: - module.fail_json(msg="%s requires either etype and permissions or just entry be set" % state) - - if entry: - if etype or entity or permissions: - module.fail_json(msg="entry and another incompatible field (entity, etype or permissions) are also set") - if entry.count(":") not in [2,3]: - module.fail_json(msg="Invalid entry: '%s', it requires 3 or 4 sections divided by ':'" % entry) - - default, etype, entity, permissions = split_entry(entry) - - changed=False - msg = "" - currentacls = get_acls(module,path,follow) - - if (state == 'present'): - matched = False - for oldentry in currentacls: - if oldentry.count(":") == 0: - continue - old_default, old_type, old_entity, old_permissions = split_entry(oldentry) - if old_default == default: - if old_type == etype: - if etype in ['user', 'group']: - if old_entity == entity: - matched = True - if not old_permissions == permissions: - changed = True - break - else: - matched = True - if not old_permissions == permissions: - changed = True - break - if not matched: - changed=True - - if changed and not module.check_mode: - set_acl(module,path,':'.join([etype, str(entity), permissions]),follow,default) - msg="%s is present" % ':'.join([etype, str(entity), permissions]) - - elif state == 'absent': - for oldentry in currentacls: - if oldentry.count(":") == 0: - continue - old_default, old_type, old_entity, old_permissions = split_entry(oldentry) - if old_default == default: - if old_type == etype: - if etype in ['user', 'group']: - if old_entity == entity: - changed=True - break - else: - changed=True - break - if changed and not module.check_mode: - rm_acl(module,path,':'.join([etype, entity, '---']),follow,default) - msg="%s is absent" % ':'.join([etype, entity, '---']) - else: - msg="current acl" - - if changed: - currentacls = get_acls(module,path,follow) - - module.exit_json(changed=changed, msg=msg, acl=currentacls) - -# import module snippets -from ansible.module_utils.basic import * - -main() diff --git a/extras/files/assemble b/extras/files/assemble deleted file mode 100644 index a16431b9f52..00000000000 --- a/extras/files/assemble +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2012, Stephen Fromm -# -# 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 os.path -import shutil -import tempfile -import re - -DOCUMENTATION = ''' ---- -module: assemble -short_description: Assembles a configuration file from fragments -description: - - Assembles a configuration file from fragments. Often a particular - program will take a single configuration file and does not support a - C(conf.d) style structure where it is easy to build up the configuration - from multiple sources. M(assemble) will take a directory of files that can be - local or have already been transferred to the system, and concatenate them - together to produce a destination file. Files are assembled in string sorting order. - Puppet calls this idea I(fragments). -version_added: "0.5" -options: - src: - description: - - An already existing directory full of source files. - required: true - default: null - aliases: [] - dest: - description: - - A file to create using the concatenation of all of the source files. - required: true - default: null - backup: - description: - - Create a backup file (if C(yes)), including the timestamp information so - you can get the original file back if you somehow clobbered it - incorrectly. - required: false - choices: [ "yes", "no" ] - default: "no" - delimiter: - description: - - A delimiter to separate the file contents. - version_added: "1.4" - required: false - default: null - remote_src: - description: - - If False, it will search for src at originating/master machine, if True it will - go to the remote/target machine for the src. Default is True. - choices: [ "True", "False" ] - required: false - default: "True" - version_added: "1.4" - regexp: - description: - - Assemble files only if C(regex) matches the filename. If not set, - all files are assembled. All "\\" (backslash) must be escaped as - "\\\\" to comply yaml syntax. Uses Python regular expressions; see - U(http://docs.python.org/2/library/re.html). - required: false - default: null -author: Stephen Fromm -extends_documentation_fragment: files -''' - -EXAMPLES = ''' -# Example from Ansible Playbooks -- assemble: src=/etc/someapp/fragments dest=/etc/someapp/someapp.conf - -# When a delimiter is specified, it will be inserted in between each fragment -- assemble: src=/etc/someapp/fragments dest=/etc/someapp/someapp.conf delimiter='### START FRAGMENT ###' -''' - -# =========================================== -# Support method - -def assemble_from_fragments(src_path, delimiter=None, compiled_regexp=None): - ''' assemble a file from a directory of fragments ''' - tmpfd, temp_path = tempfile.mkstemp() - tmp = os.fdopen(tmpfd,'w') - delimit_me = False - add_newline = False - - for f in sorted(os.listdir(src_path)): - if compiled_regexp and not compiled_regexp.search(f): - continue - fragment = "%s/%s" % (src_path, f) - if not os.path.isfile(fragment): - continue - fragment_content = file(fragment).read() - - # always put a newline between fragments if the previous fragment didn't end with a newline. - if add_newline: - tmp.write('\n') - - # delimiters should only appear between fragments - if delimit_me: - if delimiter: - # un-escape anything like newlines - delimiter = delimiter.decode('unicode-escape') - tmp.write(delimiter) - # always make sure there's a newline after the - # delimiter, so lines don't run together - if delimiter[-1] != '\n': - tmp.write('\n') - - tmp.write(fragment_content) - delimit_me = True - if fragment_content.endswith('\n'): - add_newline = False - else: - add_newline = True - - tmp.close() - return temp_path - -# ============================================================== -# main - -def main(): - - module = AnsibleModule( - # not checking because of daisy chain to file module - argument_spec = dict( - src = dict(required=True), - delimiter = dict(required=False), - dest = dict(required=True), - backup=dict(default=False, type='bool'), - remote_src=dict(default=False, type='bool'), - regexp = dict(required=False), - ), - add_file_common_args=True - ) - - changed = False - pathmd5 = None - destmd5 = None - src = os.path.expanduser(module.params['src']) - dest = os.path.expanduser(module.params['dest']) - backup = module.params['backup'] - delimiter = module.params['delimiter'] - regexp = module.params['regexp'] - compiled_regexp = None - - if not os.path.exists(src): - module.fail_json(msg="Source (%s) does not exist" % src) - - if not os.path.isdir(src): - module.fail_json(msg="Source (%s) is not a directory" % src) - - if regexp != None: - try: - compiled_regexp = re.compile(regexp) - except re.error, e: - module.fail_json(msg="Invalid Regexp (%s) in \"%s\"" % (e, regexp)) - - path = assemble_from_fragments(src, delimiter, compiled_regexp) - pathmd5 = module.md5(path) - - if os.path.exists(dest): - destmd5 = module.md5(dest) - - if pathmd5 != destmd5: - if backup and destmd5 is not None: - module.backup_local(dest) - shutil.copy(path, dest) - changed = True - - os.remove(path) - - file_args = module.load_file_common_arguments(module.params) - changed = module.set_fs_attributes_if_different(file_args, changed) - # Mission complete - module.exit_json(src=src, dest=dest, md5sum=pathmd5, changed=changed, msg="OK") - -# import module snippets -from ansible.module_utils.basic import * - -main() - diff --git a/extras/files/copy b/extras/files/copy deleted file mode 100644 index eff46dae982..00000000000 --- a/extras/files/copy +++ /dev/null @@ -1,254 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (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 os -import time - -DOCUMENTATION = ''' ---- -module: copy -version_added: "historical" -short_description: Copies files to remote locations. -description: - - The M(copy) module copies a file on the local box to remote locations. -options: - src: - description: - - Local path to a file to copy to the remote server; can be absolute or relative. - If path is a directory, it is copied recursively. In this case, if path ends - with "/", only inside contents of that directory are copied to destination. - Otherwise, if it does not end with "/", the directory itself with all contents - is copied. This behavior is similar to Rsync. - required: false - default: null - aliases: [] - content: - version_added: "1.1" - description: - - When used instead of 'src', sets the contents of a file directly to the specified value. - required: false - default: null - dest: - description: - - Remote absolute path where the file should be copied to. If src is a directory, - this must be a directory too. - required: true - default: null - backup: - description: - - Create a backup file including the timestamp information so you can get - the original file back if you somehow clobbered it incorrectly. - version_added: "0.7" - required: false - choices: [ "yes", "no" ] - default: "no" - force: - description: - - the default is C(yes), which will replace the remote file when contents - are different than the source. If C(no), the file will only be transferred - if the destination does not exist. - version_added: "1.1" - required: false - choices: [ "yes", "no" ] - default: "yes" - aliases: [ "thirsty" ] - validate: - description: - - The validation command to run before copying into place. The path to the file to - validate is passed in via '%s' which must be present as in the visudo example below. - The command is passed securely so shell features like expansion and pipes won't work. - required: false - default: "" - version_added: "1.2" - directory_mode: - description: - - When doing a recursive copy set the mode for the directories. If this is not set we will use the system - defaults. The mode is only set on directories which are newly created, and will not affect those that - already existed. - required: false - version_added: "1.5" -extends_documentation_fragment: files -author: Michael DeHaan -notes: - - The "copy" module recursively copy facility does not scale to lots (>hundreds) of files. - For alternative, see synchronize module, which is a wrapper around rsync. -''' - -EXAMPLES = ''' -# Example from Ansible Playbooks -- copy: src=/srv/myfiles/foo.conf dest=/etc/foo.conf owner=foo group=foo mode=0644 - -# The same example as above, but using a symbolic mode equivalent to 0644 -- copy: src=/srv/myfiles/foo.conf dest=/etc/foo.conf owner=foo group=foo mode="u=rw,g=r,o=r" - -# Another symbolic mode example, adding some permissions and removing others -- copy: src=/srv/myfiles/foo.conf dest=/etc/foo.conf owner=foo group=foo mode="u+rw,g-wx,o-rwx" - -# Copy a new "ntp.conf file into place, backing up the original if it differs from the copied version -- copy: src=/mine/ntp.conf dest=/etc/ntp.conf owner=root group=root mode=644 backup=yes - -# Copy a new "sudoers" file into place, after passing validation with visudo -- copy: src=/mine/sudoers dest=/etc/sudoers validate='visudo -cf %s' -''' - - -def split_pre_existing_dir(dirname): - ''' - Return the first pre-existing directory and a list of the new directories that will be created. - ''' - - head, tail = os.path.split(dirname) - if not os.path.exists(head): - (pre_existing_dir, new_directory_list) = split_pre_existing_dir(head) - else: - return (head, [ tail ]) - new_directory_list.append(tail) - return (pre_existing_dir, new_directory_list) - - -def adjust_recursive_directory_permissions(pre_existing_dir, new_directory_list, module, directory_args, changed): - ''' - Walk the new directories list and make sure that permissions are as we would expect - ''' - - if len(new_directory_list) > 0: - working_dir = os.path.join(pre_existing_dir, new_directory_list.pop(0)) - directory_args['path'] = working_dir - changed = module.set_fs_attributes_if_different(directory_args, changed) - changed = adjust_recursive_directory_permissions(working_dir, new_directory_list, module, directory_args, changed) - return changed - - -def main(): - - module = AnsibleModule( - # not checking because of daisy chain to file module - argument_spec = dict( - src = dict(required=False), - original_basename = dict(required=False), # used to handle 'dest is a directory' via template, a slight hack - content = dict(required=False, no_log=True), - dest = dict(required=True), - backup = dict(default=False, type='bool'), - force = dict(default=True, aliases=['thirsty'], type='bool'), - validate = dict(required=False, type='str'), - directory_mode = dict(required=False) - ), - add_file_common_args=True, - supports_check_mode=True, - ) - - src = os.path.expanduser(module.params['src']) - dest = os.path.expanduser(module.params['dest']) - backup = module.params['backup'] - force = module.params['force'] - original_basename = module.params.get('original_basename',None) - validate = module.params.get('validate',None) - follow = module.params['follow'] - - if not os.path.exists(src): - module.fail_json(msg="Source %s failed to transfer" % (src)) - if not os.access(src, os.R_OK): - module.fail_json(msg="Source %s not readable" % (src)) - - md5sum_src = module.md5(src) - md5sum_dest = None - - changed = False - - # Special handling for recursive copy - create intermediate dirs - if original_basename and dest.endswith("/"): - dest = os.path.join(dest, original_basename) - dirname = os.path.dirname(dest) - if not os.path.exists(dirname): - (pre_existing_dir, new_directory_list) = split_pre_existing_dir(dirname) - os.makedirs(dirname) - directory_args = module.load_file_common_arguments(module.params) - directory_mode = module.params["directory_mode"] - if directory_mode is not None: - directory_args['mode'] = directory_mode - else: - directory_args['mode'] = None - adjust_recursive_directory_permissions(pre_existing_dir, new_directory_list, module, directory_args, changed) - - if os.path.exists(dest): - if os.path.islink(dest) and follow: - dest = os.path.realpath(dest) - if not force: - module.exit_json(msg="file already exists", src=src, dest=dest, changed=False) - if (os.path.isdir(dest)): - basename = os.path.basename(src) - if original_basename: - basename = original_basename - dest = os.path.join(dest, basename) - if os.access(dest, os.R_OK): - md5sum_dest = module.md5(dest) - else: - if not os.path.exists(os.path.dirname(dest)): - try: - # os.path.exists() can return false in some - # circumstances where the directory does not have - # the execute bit for the current user set, in - # which case the stat() call will raise an OSError - os.stat(os.path.dirname(dest)) - except OSError, e: - if "permission denied" in str(e).lower(): - module.fail_json(msg="Destination directory %s is not accessible" % (os.path.dirname(dest))) - module.fail_json(msg="Destination directory %s does not exist" % (os.path.dirname(dest))) - if not os.access(os.path.dirname(dest), os.W_OK): - module.fail_json(msg="Destination %s not writable" % (os.path.dirname(dest))) - - backup_file = None - if md5sum_src != md5sum_dest or os.path.islink(dest): - try: - if backup: - if os.path.exists(dest): - backup_file = module.backup_local(dest) - # allow for conversion from symlink. - if os.path.islink(dest): - os.unlink(dest) - open(dest, 'w').close() - if validate: - if "%s" not in validate: - module.fail_json(msg="validate must contain %%s: %s" % (validate)) - (rc,out,err) = module.run_command(validate % src) - if rc != 0: - module.fail_json(msg="failed to validate: rc:%s error:%s" % (rc,err)) - module.atomic_move(src, dest) - except IOError: - module.fail_json(msg="failed to copy: %s to %s" % (src, dest)) - changed = True - else: - changed = False - - res_args = dict( - dest = dest, src = src, md5sum = md5sum_src, changed = changed - ) - if backup_file: - res_args['backup_file'] = backup_file - - module.params['dest'] = dest - file_args = module.load_file_common_arguments(module.params) - res_args['changed'] = module.set_fs_attributes_if_different(file_args, res_args['changed']) - - module.exit_json(**res_args) - -# import module snippets -from ansible.module_utils.basic import * -main() diff --git a/extras/files/fetch b/extras/files/fetch deleted file mode 100644 index 5b47d87a856..00000000000 --- a/extras/files/fetch +++ /dev/null @@ -1,67 +0,0 @@ -# this is a virtual module that is entirely implemented server side - -DOCUMENTATION = ''' ---- -module: fetch -short_description: Fetches a file from remote nodes -description: - - This module works like M(copy), but in reverse. It is used for fetching - files from remote machines and storing them locally in a file tree, - organized by hostname. Note that this module is written to transfer - log files that might not be present, so a missing remote file won't - be an error unless fail_on_missing is set to 'yes'. -version_added: "0.2" -options: - src: - description: - - The file on the remote system to fetch. This I(must) be a file, not a - directory. Recursive fetching may be supported in a later release. - required: true - default: null - aliases: [] - dest: - description: - - A directory to save the file into. For example, if the I(dest) - directory is C(/backup) a I(src) file named C(/etc/profile) on host - C(host.example.com), would be saved into - C(/backup/host.example.com/etc/profile) - required: true - default: null - fail_on_missing: - version_added: "1.1" - description: - - Makes it fails when the source file is missing. - required: false - choices: [ "yes", "no" ] - default: "no" - validate_md5: - version_added: "1.4" - description: - - Verify that the source and destination md5sums match after the files are fetched. - required: false - choices: [ "yes", "no" ] - default: "yes" - flat: - version_added: "1.2" - description: - Allows you to override the default behavior of prepending hostname/path/to/file to - the destination. If dest ends with '/', it will use the basename of the source - file, similar to the copy module. Obviously this is only handy if the filenames - are unique. -requirements: [] -author: Michael DeHaan -''' - -EXAMPLES = ''' -# Store file into /tmp/fetched/host.example.com/tmp/somefile -- fetch: src=/tmp/somefile dest=/tmp/fetched - -# Specifying a path directly -- fetch: src=/tmp/somefile dest=/tmp/prefix-{{ ansible_hostname }} flat=yes - -# Specifying a destination path -- fetch: src=/tmp/uniquefile dest=/tmp/special/ flat=yes - -# Storing in a path relative to the playbook -- fetch: src=/tmp/uniquefile dest=special/prefix-{{ ansible_hostname }} flat=yes -''' diff --git a/extras/files/file b/extras/files/file deleted file mode 100644 index ff9feb41ee3..00000000000 --- a/extras/files/file +++ /dev/null @@ -1,358 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (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 shutil -import stat -import grp -import pwd -try: - import selinux - HAVE_SELINUX=True -except ImportError: - HAVE_SELINUX=False - -DOCUMENTATION = ''' ---- -module: file -version_added: "historical" -short_description: Sets attributes of files -extends_documentation_fragment: files -description: - - Sets attributes of files, symlinks, and directories, or removes - files/symlinks/directories. Many other modules support the same options as - the M(file) module - including M(copy), M(template), and M(assemble). -notes: - - See also M(copy), M(template), M(assemble) -requirements: [ ] -author: Michael DeHaan -options: - path: - description: - - 'path to the file being managed. Aliases: I(dest), I(name)' - required: true - default: [] - aliases: ['dest', 'name'] - state: - description: - - If C(directory), all immediate subdirectories will be created if they - do not exist, since 1.7 they will be created with the supplied permissions. - If C(file), the file will NOT be created if it does not exist, see the M(copy) - or M(template) module if you want that behavior. If C(link), the symbolic - link will be created or changed. Use C(hard) for hardlinks. If C(absent), - directories will be recursively deleted, and files or symlinks will be unlinked. - If C(touch) (new in 1.4), an empty file will be created if the c(path) does not - exist, while an existing file or directory will receive updated file access and - modification times (similar to the way `touch` works from the command line). - required: false - default: file - choices: [ file, link, directory, hard, touch, absent ] - src: - required: false - default: null - choices: [] - description: - - path of the file to link to (applies only to C(state=link)). Will accept absolute, - relative and nonexisting paths. Relative paths are not expanded. - recurse: - required: false - default: "no" - choices: [ "yes", "no" ] - version_added: "1.1" - description: - - recursively set the specified file attributes (applies only to state=directory) - force: - required: false - default: "no" - choices: [ "yes", "no" ] - description: - - 'force the creation of the symlinks in two cases: the source file does - not exist (but will appear later); the destination exists and is a file (so, we need to unlink the - "path" file and create symlink to the "src" file in place of it).' -''' - -EXAMPLES = ''' -- file: path=/etc/foo.conf owner=foo group=foo mode=0644 -- file: src=/file/to/link/to dest=/path/to/symlink owner=foo group=foo state=link -- file: src=/tmp/{{ item.path }} dest={{ item.dest }} state=link - with_items: - - { path: 'x', dest: 'y' } - - { path: 'z', dest: 'k' } - -# touch a file, using symbolic modes to set the permissions (equivalent to 0644) -- file: path=/etc/foo.conf state=touch mode="u=rw,g=r,o=r" - -# touch the same file, but add/remove some permissions -- file: path=/etc/foo.conf state=touch mode="u+rw,g-wx,o-rwx" - -''' - -def main(): - - module = AnsibleModule( - argument_spec = dict( - state = dict(choices=['file','directory','link','hard','touch','absent'], default=None), - path = dict(aliases=['dest', 'name'], required=True), - original_basename = dict(required=False), # Internal use only, for recursive ops - recurse = dict(default='no', type='bool'), - force = dict(required=False,default=False,type='bool'), - diff_peek = dict(default=None), - validate = dict(required=False, default=None), - src = dict(required=False, default=None), - ), - add_file_common_args=True, - supports_check_mode=True - ) - - params = module.params - state = params['state'] - force = params['force'] - diff_peek = params['diff_peek'] - src = params['src'] - follow = params['follow'] - - # modify source as we later reload and pass, specially relevant when used by other modules. - params['path'] = path = os.path.expanduser(params['path']) - - # short-circuit for diff_peek - if diff_peek is not None: - appears_binary = False - try: - f = open(path) - b = f.read(8192) - f.close() - if "\x00" in b: - appears_binary = True - except: - pass - module.exit_json(path=path, changed=False, appears_binary=appears_binary) - - # Find out current state - prev_state = 'absent' - if os.path.lexists(path): - if os.path.islink(path): - prev_state = 'link' - elif os.path.isdir(path): - prev_state = 'directory' - elif os.stat(path).st_nlink > 1: - prev_state = 'hard' - else: - # could be many other things, but defaulting to file - prev_state = 'file' - - # state should default to file, but since that creates many conflicts, - # default to 'current' when it exists. - if state is None: - if prev_state != 'absent': - state = prev_state - else: - state = 'file' - - # source is both the source of a symlink or an informational passing of the src for a template module - # or copy module, even if this module never uses it, it is needed to key off some things - if src is not None: - src = os.path.expanduser(src) - - # original_basename is used by other modules that depend on file. - if os.path.isdir(path) and state not in ["link", "absent"]: - if params['original_basename']: - basename = params['original_basename'] - else: - basename = os.path.basename(src) - params['path'] = path = os.path.join(path, basename) - else: - if state in ['link','hard']: - if follow: - # use the current target of the link as the source - src = os.readlink(path) - else: - module.fail_json(msg='src and dest are required for creating links') - - # make sure the target path is a directory when we're doing a recursive operation - recurse = params['recurse'] - if recurse and state != 'directory': - module.fail_json(path=path, msg="recurse option requires state to be 'directory'") - - file_args = module.load_file_common_arguments(params) - changed = False - - if state == 'absent': - if state != prev_state: - if not module.check_mode: - if prev_state == 'directory': - try: - shutil.rmtree(path, ignore_errors=False) - except Exception, e: - module.fail_json(msg="rmtree failed: %s" % str(e)) - else: - try: - os.unlink(path) - except Exception, e: - module.fail_json(path=path, msg="unlinking failed: %s " % str(e)) - module.exit_json(path=path, changed=True) - else: - module.exit_json(path=path, changed=False) - - elif state == 'file': - if state != prev_state: - # file is not absent and any other state is a conflict - module.fail_json(path=path, msg='file (%s) is %s, cannot continue' % (path, prev_state)) - - changed = module.set_fs_attributes_if_different(file_args, changed) - module.exit_json(path=path, changed=changed) - - elif state == 'directory': - if prev_state == 'absent': - if module.check_mode: - module.exit_json(changed=True) - changed = True - curpath = '' - # Split the path so we can apply filesystem attributes recursively - # from the root (/) directory for absolute paths or the base path - # of a relative path. We can then walk the appropriate directory - # path to apply attributes. - for dirname in path.strip('/').split('/'): - curpath = '/'.join([curpath, dirname]) - # Remove leading slash if we're creating a relative path - if not os.path.isabs(path): - curpath = curpath.lstrip('/') - if not os.path.exists(curpath): - os.mkdir(curpath) - tmp_file_args = file_args.copy() - tmp_file_args['path']=curpath - changed = module.set_fs_attributes_if_different(tmp_file_args, changed) - - changed = module.set_fs_attributes_if_different(file_args, changed) - - if recurse: - for root,dirs,files in os.walk( file_args['path'] ): - for fsobj in dirs + files: - fsname=os.path.join(root, fsobj) - tmp_file_args = file_args.copy() - tmp_file_args['path']=fsname - changed = module.set_fs_attributes_if_different(tmp_file_args, changed) - - module.exit_json(path=path, changed=changed) - - elif state in ['link','hard']: - - if os.path.isdir(path) and not os.path.islink(path): - relpath = path - else: - relpath = os.path.dirname(path) - - absrc = os.path.join(relpath, src) - if not os.path.exists(absrc) and not force: - module.fail_json(path=path, src=src, msg='src file does not exist, use "force=yes" if you really want to create the link: %s' % absrc) - - if state == 'hard': - if not os.path.isabs(src): - module.fail_json(msg="absolute paths are required") - elif prev_state == 'directory': - if not force: - module.fail_json(path=path, msg='refusing to convert between %s and %s for %s' % (prev_state, state, path)) - elif len(os.listdir(path)) > 0: - # refuse to replace a directory that has files in it - module.fail_json(path=path, msg='the directory %s is not empty, refusing to convert it' % path) - elif prev_state in ['file', 'hard'] and not force: - module.fail_json(path=path, msg='refusing to convert between %s and %s for %s' % (prev_state, state, path)) - - if prev_state == 'absent': - changed = True - elif prev_state == 'link': - old_src = os.readlink(path) - if old_src != src: - changed = True - elif prev_state == 'hard': - if not (state == 'hard' and os.stat(path).st_ino == os.stat(src).st_ino): - changed = True - if not force: - module.fail_json(dest=path, src=src, msg='Cannot link, different hard link exists at destination') - elif prev_state in ['file', 'directory']: - changed = True - if not force: - module.fail_json(dest=path, src=src, msg='Cannot link, %s exists at destination' % prev_state) - else: - module.fail_json(dest=path, src=src, msg='unexpected position reached') - - if changed and not module.check_mode: - if prev_state != 'absent': - # try to replace atomically - tmppath = '/'.join([os.path.dirname(path), ".%s.%s.tmp" % (os.getpid(),time.time())]) - try: - if prev_state == 'directory' and (state == 'hard' or state == 'link'): - os.rmdir(path) - if state == 'hard': - os.link(src,tmppath) - else: - os.symlink(src, tmppath) - os.rename(tmppath, path) - except OSError, e: - if os.path.exists(tmppath): - os.unlink(tmppath) - module.fail_json(path=path, msg='Error while replacing: %s' % str(e)) - else: - try: - if state == 'hard': - os.link(src,path) - else: - os.symlink(src, path) - except OSError, e: - module.fail_json(path=path, msg='Error while linking: %s' % str(e)) - - if module.check_mode and not os.path.exists(path): - module.exit_json(dest=path, src=src, changed=changed) - - changed = module.set_fs_attributes_if_different(file_args, changed) - module.exit_json(dest=path, src=src, changed=changed) - - elif state == 'touch': - if not module.check_mode: - - if prev_state == 'absent': - try: - open(path, 'w').close() - except OSError, e: - module.fail_json(path=path, msg='Error, could not touch target: %s' % str(e)) - elif prev_state in ['file', 'directory']: - try: - os.utime(path, None) - except OSError, e: - module.fail_json(path=path, msg='Error while touching existing target: %s' % str(e)) - else: - module.fail_json(msg='Cannot touch other than files and directories') - try: - module.set_fs_attributes_if_different(file_args, True) - except SystemExit, e: - if e.code: - # We take this to mean that fail_json() was called from - # somewhere in basic.py - if prev_state == 'absent': - # If we just created the file we can safely remove it - os.remove(path) - raise e - - module.exit_json(dest=path, changed=True) - - module.fail_json(path=path, msg='unexpected position reached') - -# import module snippets -from ansible.module_utils.basic import * -main() - diff --git a/extras/files/ini_file b/extras/files/ini_file deleted file mode 100644 index 83a980f5ba8..00000000000 --- a/extras/files/ini_file +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2012, Jan-Piet Mens -# -# 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: ini_file -short_description: Tweak settings in INI files -description: - - Manage (add, remove, change) individual settings in an INI-style file without having - to manage the file as a whole with, say, M(template) or M(assemble). Adds missing - sections if they don't exist. - - Comments are discarded when the source file is read, and therefore will not - show up in the destination file. -version_added: "0.9" -options: - dest: - description: - - Path to the INI-style file; this file is created if required - required: true - default: null - section: - description: - - Section name in INI file. This is added if C(state=present) automatically when - a single value is being set. - required: true - default: null - option: - description: - - if set (required for changing a I(value)), this is the name of the option. - - May be omitted if adding/removing a whole I(section). - required: false - default: null - value: - description: - - the string value to be associated with an I(option). May be omitted when removing an I(option). - required: false - default: null - backup: - description: - - Create a backup file including the timestamp information so you can get - the original file back if you somehow clobbered it incorrectly. - required: false - default: "no" - choices: [ "yes", "no" ] - others: - description: - - all arguments accepted by the M(file) module also work here - required: false -notes: - - While it is possible to add an I(option) without specifying a I(value), this makes - no sense. - - A section named C(default) cannot be added by the module, but if it exists, individual - options within the section can be updated. (This is a limitation of Python's I(ConfigParser).) - Either use M(template) to create a base INI file with a C([default]) section, or use - M(lineinfile) to add the missing line. -requirements: [ ConfigParser ] -author: Jan-Piet Mens -''' - -EXAMPLES = ''' -# Ensure "fav=lemonade is in section "[drinks]" in specified file -- ini_file: dest=/etc/conf section=drinks option=fav value=lemonade mode=0600 backup=yes - -- ini_file: dest=/etc/anotherconf - section=drinks - option=temperature - value=cold - backup=yes -''' - -import ConfigParser -import sys - -# ============================================================== -# do_ini - -def do_ini(module, filename, section=None, option=None, value=None, state='present', backup=False): - - changed = False - if (sys.version_info[0] == 2 and sys.version_info[1] >= 7) or sys.version_info[0] >= 3: - cp = ConfigParser.ConfigParser(allow_no_value=True) - else: - cp = ConfigParser.ConfigParser() - cp.optionxform = identity - - try: - f = open(filename) - cp.readfp(f) - except IOError: - pass - - - if state == 'absent': - if option is None and value is None: - if cp.has_section(section): - cp.remove_section(section) - changed = True - else: - if option is not None: - try: - if cp.get(section, option): - cp.remove_option(section, option) - changed = True - except: - pass - - if state == 'present': - - # DEFAULT section is always there by DEFAULT, so never try to add it. - if cp.has_section(section) == False and section.upper() != 'DEFAULT': - - cp.add_section(section) - changed = True - - if option is not None and value is not None: - try: - oldvalue = cp.get(section, option) - if str(value) != str(oldvalue): - cp.set(section, option, value) - changed = True - except ConfigParser.NoSectionError: - cp.set(section, option, value) - changed = True - except ConfigParser.NoOptionError: - cp.set(section, option, value) - changed = True - - if changed: - if backup: - module.backup_local(filename) - - try: - f = open(filename, 'w') - cp.write(f) - except: - module.fail_json(msg="Can't creat %s" % filename) - - return changed - -# ============================================================== -# identity - -def identity(arg): - """ - This function simply returns its argument. It serves as a - replacement for ConfigParser.optionxform, which by default - changes arguments to lower case. The identity function is a - better choice than str() or unicode(), because it is - encoding-agnostic. - """ - return arg - -# ============================================================== -# main - -def main(): - - module = AnsibleModule( - argument_spec = dict( - dest = dict(required=True), - section = dict(required=True), - option = dict(required=False), - value = dict(required=False), - backup = dict(default='no', type='bool'), - state = dict(default='present', choices=['present', 'absent']) - ), - add_file_common_args = True - ) - - info = dict() - - dest = os.path.expanduser(module.params['dest']) - section = module.params['section'] - option = module.params['option'] - value = module.params['value'] - state = module.params['state'] - backup = module.params['backup'] - - changed = do_ini(module, dest, section, option, value, state, backup) - - file_args = module.load_file_common_arguments(module.params) - changed = module.set_fs_attributes_if_different(file_args, changed) - - # Mission complete - module.exit_json(dest=dest, changed=changed, msg="OK") - -# import module snippets -from ansible.module_utils.basic import * -main() diff --git a/extras/files/lineinfile b/extras/files/lineinfile deleted file mode 100644 index 12f8dc89a7d..00000000000 --- a/extras/files/lineinfile +++ /dev/null @@ -1,400 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2012, Daniel Hokka Zakrisson -# (c) 2014, Ahti Kitsik -# -# 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 pipes -import re -import os -import tempfile - -DOCUMENTATION = """ ---- -module: lineinfile -author: Daniel Hokka Zakrisson, Ahti Kitsik -short_description: Ensure a particular line is in a file, or replace an - existing line using a back-referenced regular expression. -description: - - This module will search a file for a line, and ensure that it is present or absent. - - This is primarily useful when you want to change a single line in a - file only. For other cases, see the M(copy) or M(template) modules. -version_added: "0.7" -options: - dest: - required: true - aliases: [ name, destfile ] - description: - - The file to modify. - regexp: - required: false - version_added: 1.7 - description: - - The regular expression to look for in every line of the file. For - C(state=present), the pattern to replace if found; only the last line - found will be replaced. For C(state=absent), the pattern of the line - to remove. Uses Python regular expressions; see - U(http://docs.python.org/2/library/re.html). - state: - required: false - choices: [ present, absent ] - default: "present" - aliases: [] - description: - - Whether the line should be there or not. - line: - required: false - description: - - Required for C(state=present). The line to insert/replace into the - file. If C(backrefs) is set, may contain backreferences that will get - expanded with the C(regexp) capture groups if the regexp matches. The - backreferences should be double escaped (see examples). - backrefs: - required: false - default: "no" - choices: [ "yes", "no" ] - version_added: "1.1" - description: - - Used with C(state=present). If set, line can contain backreferences - (both positional and named) that will get populated if the C(regexp) - matches. This flag changes the operation of the module slightly; - C(insertbefore) and C(insertafter) will be ignored, and if the C(regexp) - doesn't match anywhere in the file, the file will be left unchanged. - If the C(regexp) does match, the last matching line will be replaced by - the expanded line parameter. - insertafter: - required: false - default: EOF - description: - - Used with C(state=present). If specified, the line will be inserted - after the specified regular expression. A special value is - available; C(EOF) for inserting the line at the end of the file. - May not be used with C(backrefs). - choices: [ 'EOF', '*regex*' ] - insertbefore: - required: false - version_added: "1.1" - description: - - Used with C(state=present). If specified, the line will be inserted - before the specified regular expression. A value is available; - C(BOF) for inserting the line at the beginning of the file. - May not be used with C(backrefs). - choices: [ 'BOF', '*regex*' ] - create: - required: false - choices: [ "yes", "no" ] - default: "no" - description: - - Used with C(state=present). If specified, the file will be created - if it does not already exist. By default it will fail if the file - is missing. - backup: - required: false - default: "no" - choices: [ "yes", "no" ] - description: - - Create a backup file including the timestamp information so you can - get the original file back if you somehow clobbered it incorrectly. - validate: - required: false - description: - - validation to run before copying into place. - Use %s in the command to indicate the current file to validate. - The command is passed securely so shell features like - expansion and pipes won't work. - required: false - default: None - version_added: "1.4" - others: - description: - - All arguments accepted by the M(file) module also work here. - required: false -""" - -EXAMPLES = r""" -- lineinfile: dest=/etc/selinux/config regexp=^SELINUX= line=SELINUX=disabled - -- lineinfile: dest=/etc/sudoers state=absent regexp="^%wheel" - -- lineinfile: dest=/etc/hosts regexp='^127\.0\.0\.1' line='127.0.0.1 localhost' owner=root group=root mode=0644 - -- lineinfile: dest=/etc/httpd/conf/httpd.conf regexp="^Listen " insertafter="^#Listen " line="Listen 8080" - -- lineinfile: dest=/etc/services regexp="^# port for http" insertbefore="^www.*80/tcp" line="# port for http by default" - -# Add a line to a file if it does not exist, without passing regexp -- lineinfile: dest=/tmp/testfile line="192.168.1.99 foo.lab.net foo" - -# Fully quoted because of the ': ' on the line. See the Gotchas in the YAML docs. -- lineinfile: "dest=/etc/sudoers state=present regexp='^%wheel' line='%wheel ALL=(ALL) NOPASSWD: ALL'" - -- lineinfile: dest=/opt/jboss-as/bin/standalone.conf regexp='^(.*)Xms(\d+)m(.*)$' line='\1Xms${xms}m\3' backrefs=yes - -# Validate a the sudoers file before saving -- lineinfile: dest=/etc/sudoers state=present regexp='^%ADMIN ALL\=' line='%ADMIN ALL=(ALL) NOPASSWD:ALL' validate='visudo -cf %s' -""" - -def write_changes(module,lines,dest): - - tmpfd, tmpfile = tempfile.mkstemp() - f = os.fdopen(tmpfd,'wb') - f.writelines(lines) - f.close() - - validate = module.params.get('validate', None) - valid = not validate - if validate: - if "%s" not in validate: - module.fail_json(msg="validate must contain %%s: %s" % (validate)) - (rc, out, err) = module.run_command(validate % tmpfile) - valid = rc == 0 - if rc != 0: - module.fail_json(msg='failed to validate: ' - 'rc:%s error:%s' % (rc,err)) - if valid: - module.atomic_move(tmpfile, os.path.realpath(dest)) - -def check_file_attrs(module, changed, message): - - file_args = module.load_file_common_arguments(module.params) - if module.set_fs_attributes_if_different(file_args, False): - - if changed: - message += " and " - changed = True - message += "ownership, perms or SE linux context changed" - - return message, changed - - -def present(module, dest, regexp, line, insertafter, insertbefore, create, - backup, backrefs): - - if not os.path.exists(dest): - if not create: - module.fail_json(rc=257, msg='Destination %s does not exist !' % dest) - destpath = os.path.dirname(dest) - if not os.path.exists(destpath): - os.makedirs(destpath) - lines = [] - else: - f = open(dest, 'rb') - lines = f.readlines() - f.close() - - msg = "" - - if regexp is not None: - mre = re.compile(regexp) - - if insertafter not in (None, 'BOF', 'EOF'): - insre = re.compile(insertafter) - elif insertbefore not in (None, 'BOF'): - insre = re.compile(insertbefore) - else: - insre = None - - # index[0] is the line num where regexp has been found - # index[1] is the line num where insertafter/inserbefore has been found - index = [-1, -1] - m = None - for lineno, cur_line in enumerate(lines): - if regexp is not None: - match_found = mre.search(cur_line) - else: - match_found = line == cur_line.rstrip('\r\n') - if match_found: - index[0] = lineno - m = match_found - elif insre is not None and insre.search(cur_line): - if insertafter: - # + 1 for the next line - index[1] = lineno + 1 - if insertbefore: - # + 1 for the previous line - index[1] = lineno - - msg = '' - changed = False - # Regexp matched a line in the file - if index[0] != -1: - if backrefs: - new_line = m.expand(line) - else: - # Don't do backref expansion if not asked. - new_line = line - - if lines[index[0]] != new_line + os.linesep: - lines[index[0]] = new_line + os.linesep - msg = 'line replaced' - changed = True - elif backrefs: - # Do absolutely nothing, since it's not safe generating the line - # without the regexp matching to populate the backrefs. - pass - # Add it to the beginning of the file - elif insertbefore == 'BOF' or insertafter == 'BOF': - lines.insert(0, line + os.linesep) - msg = 'line added' - changed = True - # Add it to the end of the file if requested or - # if insertafter=/insertbefore didn't match anything - # (so default behaviour is to add at the end) - elif insertafter == 'EOF': - - # If the file is not empty then ensure there's a newline before the added line - if len(lines)>0 and not (lines[-1].endswith('\n') or lines[-1].endswith('\r')): - lines.append(os.linesep) - - lines.append(line + os.linesep) - msg = 'line added' - changed = True - # Do nothing if insert* didn't match - elif index[1] == -1: - pass - # insert* matched, but not the regexp - else: - lines.insert(index[1], line + os.linesep) - msg = 'line added' - changed = True - - backupdest = "" - if changed and not module.check_mode: - if backup and os.path.exists(dest): - backupdest = module.backup_local(dest) - write_changes(module, lines, dest) - - msg, changed = check_file_attrs(module, changed, msg) - module.exit_json(changed=changed, msg=msg, backup=backupdest) - - -def absent(module, dest, regexp, line, backup): - - if not os.path.exists(dest): - module.exit_json(changed=False, msg="file not present") - - msg = "" - - f = open(dest, 'rb') - lines = f.readlines() - f.close() - if regexp is not None: - cre = re.compile(regexp) - found = [] - - def matcher(cur_line): - if regexp is not None: - match_found = cre.search(cur_line) - else: - match_found = line == cur_line.rstrip('\r\n') - if match_found: - found.append(cur_line) - return not match_found - - lines = filter(matcher, lines) - changed = len(found) > 0 - backupdest = "" - if changed and not module.check_mode: - if backup: - backupdest = module.backup_local(dest) - write_changes(module, lines, dest) - - if changed: - msg = "%s line(s) removed" % len(found) - - msg, changed = check_file_attrs(module, changed, msg) - module.exit_json(changed=changed, found=len(found), msg=msg, backup=backupdest) - - -def main(): - module = AnsibleModule( - argument_spec=dict( - dest=dict(required=True, aliases=['name', 'destfile']), - state=dict(default='present', choices=['absent', 'present']), - regexp=dict(default=None), - line=dict(aliases=['value']), - insertafter=dict(default=None), - insertbefore=dict(default=None), - backrefs=dict(default=False, type='bool'), - create=dict(default=False, type='bool'), - backup=dict(default=False, type='bool'), - validate=dict(default=None, type='str'), - ), - mutually_exclusive=[['insertbefore', 'insertafter']], - add_file_common_args=True, - supports_check_mode=True - ) - - params = module.params - create = module.params['create'] - backup = module.params['backup'] - backrefs = module.params['backrefs'] - dest = os.path.expanduser(params['dest']) - - - if os.path.isdir(dest): - module.fail_json(rc=256, msg='Destination %s is a directory !' % dest) - - if params['state'] == 'present': - if backrefs and params['regexp'] is None: - module.fail_json(msg='regexp= is required with backrefs=true') - - if params.get('line', None) is None: - module.fail_json(msg='line= is required with state=present') - - # Deal with the insertafter default value manually, to avoid errors - # because of the mutually_exclusive mechanism. - ins_bef, ins_aft = params['insertbefore'], params['insertafter'] - if ins_bef is None and ins_aft is None: - ins_aft = 'EOF' - - line = params['line'] - - # The safe_eval call will remove some quoting, but not others, - # so we need to know if we should specifically unquote it. - should_unquote = not is_quoted(line) - - # always add one layer of quotes - line = "'%s'" % line - - # Replace escape sequences like '\n' while being sure - # not to replace octal escape sequences (\ooo) since they - # match the backref syntax. - if backrefs: - line = re.sub(r'(\\[0-9]{1,3})', r'\\\1', line) - line = module.safe_eval(line) - - # Now remove quotes around the string, if needed after - # removing the layer we added above - line = unquote(line) - if should_unquote: - line = unquote(line) - - present(module, dest, params['regexp'], line, - ins_aft, ins_bef, create, backup, backrefs) - else: - if params['regexp'] is None and params.get('line', None) is None: - module.fail_json(msg='one of line= or regexp= is required with state=absent') - - absent(module, dest, params['regexp'], params.get('line', None), backup) - -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.splitter import * - -main() diff --git a/extras/files/replace b/extras/files/replace deleted file mode 100644 index 57b522dd773..00000000000 --- a/extras/files/replace +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2013, Evan Kaufman . - -import re -import os -import tempfile - -DOCUMENTATION = """ ---- -module: replace -author: Evan Kaufman -short_description: Replace all instances of a particular string in a - file using a back-referenced regular expression. -description: - - This module will replace all instances of a pattern within a file. - - It is up to the user to maintain idempotence by ensuring that the - same pattern would never match any replacements made. -version_added: "1.6" -options: - dest: - required: true - aliases: [ name, destfile ] - description: - - The file to modify. - regexp: - required: true - description: - - The regular expression to look for in the contents of the file. - Uses Python regular expressions; see - U(http://docs.python.org/2/library/re.html). - Uses multiline mode, which means C(^) and C($) match the beginning - and end respectively of I(each line) of the file. - replace: - required: false - description: - - The string to replace regexp matches. May contain backreferences - that will get expanded with the regexp capture groups if the regexp - matches. If not set, matches are removed entirely. - backup: - required: false - default: "no" - choices: [ "yes", "no" ] - description: - - Create a backup file including the timestamp information so you can - get the original file back if you somehow clobbered it incorrectly. - validate: - required: false - description: - - validation to run before copying into place - required: false - default: None - others: - description: - - All arguments accepted by the M(file) module also work here. - required: false -""" - -EXAMPLES = r""" -- replace: dest=/etc/hosts regexp='(\s+)old\.host\.name(\s+.*)?$' replace='\1new.host.name\2' backup=yes - -- replace: dest=/home/jdoe/.ssh/known_hosts regexp='^old\.host\.name[^\n]*\n' owner=jdoe group=jdoe mode=644 - -- replace: dest=/etc/apache/ports regexp='^(NameVirtualHost|Listen)\s+80\s*$' replace='\1 127.0.0.1:8080' validate='/usr/sbin/apache2ctl -f %s -t' -""" - -def write_changes(module,contents,dest): - - tmpfd, tmpfile = tempfile.mkstemp() - f = os.fdopen(tmpfd,'wb') - f.write(contents) - f.close() - - validate = module.params.get('validate', None) - valid = not validate - if validate: - if "%s" not in validate: - module.fail_json(msg="validate must contain %%s: %s" % (validate)) - (rc, out, err) = module.run_command(validate % tmpfile) - valid = rc == 0 - if rc != 0: - module.fail_json(msg='failed to validate: ' - 'rc:%s error:%s' % (rc,err)) - if valid: - module.atomic_move(tmpfile, dest) - -def check_file_attrs(module, changed, message): - - file_args = module.load_file_common_arguments(module.params) - if module.set_file_attributes_if_different(file_args, False): - - if changed: - message += " and " - changed = True - message += "ownership, perms or SE linux context changed" - - return message, changed - -def main(): - module = AnsibleModule( - argument_spec=dict( - dest=dict(required=True, aliases=['name', 'destfile']), - regexp=dict(required=True), - replace=dict(default='', type='str'), - backup=dict(default=False, type='bool'), - validate=dict(default=None, type='str'), - ), - add_file_common_args=True, - supports_check_mode=True - ) - - params = module.params - dest = os.path.expanduser(params['dest']) - - if os.path.isdir(dest): - module.fail_json(rc=256, msg='Destination %s is a directory !' % dest) - - if not os.path.exists(dest): - module.fail_json(rc=257, msg='Destination %s does not exist !' % dest) - else: - f = open(dest, 'rb') - contents = f.read() - f.close() - - mre = re.compile(params['regexp'], re.MULTILINE) - result = re.subn(mre, params['replace'], contents, 0) - - if result[1] > 0 and contents != result[0]: - msg = '%s replacements made' % result[1] - changed = True - else: - msg = '' - changed = False - - if changed and not module.check_mode: - if params['backup'] and os.path.exists(dest): - module.backup_local(dest) - write_changes(module, result[0], dest) - - msg, changed = check_file_attrs(module, changed, msg) - module.exit_json(changed=changed, msg=msg) - -# this is magic, see lib/ansible/module_common.py -#<> - -main() diff --git a/extras/files/stat b/extras/files/stat deleted file mode 100644 index 8c717a395c4..00000000000 --- a/extras/files/stat +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -DOCUMENTATION = ''' ---- -module: stat -version_added: "1.3" -short_description: retrieve file or file system status -description: - - Retrieves facts for a file similar to the linux/unix 'stat' command. -options: - path: - description: - - The full path of the file/object to get the facts of - required: true - default: null - aliases: [] - follow: - description: - - Whether to follow symlinks - required: false - default: no - aliases: [] - get_md5: - description: - - Whether to return the md5 sum of the file - required: false - default: yes - aliases: [] -author: Bruce Pennypacker -''' - -EXAMPLES = ''' -# Obtain the stats of /etc/foo.conf, and check that the file still belongs -# to 'root'. Fail otherwise. -- stat: path=/etc/foo.conf - register: st -- fail: msg="Whoops! file ownership has changed" - when: st.stat.pw_name != 'root' - -# Determine if a path exists and is a directory. Note we need to test -# both that p.stat.isdir actually exists, and also that it's set to true. -- stat: path=/path/to/something - register: p -- debug: msg="Path exists and is a directory" - when: p.stat.isdir is defined and p.stat.isdir == true - -# Don't do md5 checksum -- stat: path=/path/to/myhugefile get_md5=no -''' - -import os -import sys -from stat import * -import pwd - -def main(): - module = AnsibleModule( - argument_spec = dict( - path = dict(required=True), - follow = dict(default='no', type='bool'), - get_md5 = dict(default='yes', type='bool') - ), - supports_check_mode = True - ) - - path = module.params.get('path') - path = os.path.expanduser(path) - follow = module.params.get('follow') - get_md5 = module.params.get('get_md5') - - try: - if follow: - st = os.stat(path) - else: - st = os.lstat(path) - except OSError, e: - if e.errno == errno.ENOENT: - d = { 'exists' : False } - module.exit_json(changed=False, stat=d) - - module.fail_json(msg = e.strerror) - - mode = st.st_mode - - # back to ansible - d = { - 'exists' : True, - 'mode' : "%04o" % S_IMODE(mode), - 'isdir' : S_ISDIR(mode), - 'ischr' : S_ISCHR(mode), - 'isblk' : S_ISBLK(mode), - 'isreg' : S_ISREG(mode), - 'isfifo' : S_ISFIFO(mode), - 'islnk' : S_ISLNK(mode), - 'issock' : S_ISSOCK(mode), - 'uid' : st.st_uid, - 'gid' : st.st_gid, - 'size' : st.st_size, - 'inode' : st.st_ino, - 'dev' : st.st_dev, - 'nlink' : st.st_nlink, - 'atime' : st.st_atime, - 'mtime' : st.st_mtime, - 'ctime' : st.st_ctime, - 'wusr' : bool(mode & stat.S_IWUSR), - 'rusr' : bool(mode & stat.S_IRUSR), - 'xusr' : bool(mode & stat.S_IXUSR), - 'wgrp' : bool(mode & stat.S_IWGRP), - 'rgrp' : bool(mode & stat.S_IRGRP), - 'xgrp' : bool(mode & stat.S_IXGRP), - 'woth' : bool(mode & stat.S_IWOTH), - 'roth' : bool(mode & stat.S_IROTH), - 'xoth' : bool(mode & stat.S_IXOTH), - 'isuid' : bool(mode & stat.S_ISUID), - 'isgid' : bool(mode & stat.S_ISGID), - } - - if S_ISLNK(mode): - d['lnk_source'] = os.path.realpath(path) - - if S_ISREG(mode) and get_md5 and os.access(path,os.R_OK): - d['md5'] = module.md5(path) - - - try: - pw = pwd.getpwuid(st.st_uid) - - d['pw_name'] = pw.pw_name - except: - pass - - - module.exit_json(changed=False, stat=d) - -# import module snippets -from ansible.module_utils.basic import * - -main() diff --git a/extras/files/synchronize b/extras/files/synchronize deleted file mode 100644 index 842dd863849..00000000000 --- a/extras/files/synchronize +++ /dev/null @@ -1,345 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2012-2013, Timothy Appnel -# -# 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: synchronize -version_added: "1.4" -short_description: Uses rsync to make synchronizing file paths in your playbooks quick and easy. -description: - - This is a wrapper around rsync. Of course you could just use the command action to call rsync yourself, but you also have to add a fair number of boilerplate options and host facts. You still may need to call rsync directly via C(command) or C(shell) depending on your use case. The synchronize action is meant to do common things with C(rsync) easily. It does not provide access to the full power of rsync, but does make most invocations easier to follow. -options: - src: - description: - - Path on the source machine that will be synchronized to the destination; The path can be absolute or relative. - required: true - dest: - description: - - Path on the destination machine that will be synchronized from the source; The path can be absolute or relative. - required: true - dest_port: - description: - - Port number for ssh on the destination host. The ansible_ssh_port inventory var takes precedence over this value. - default: 22 - version_added: "1.5" - mode: - description: - - Specify the direction of the synchroniztion. In push mode the localhost or delegate is the source; In pull mode the remote host in context is the source. - required: false - choices: [ 'push', 'pull' ] - default: 'push' - archive: - description: - - Mirrors the rsync archive flag, enables recursive, links, perms, times, owner, group flags and -D. - choices: [ 'yes', 'no' ] - default: 'yes' - required: false - checksum: - description: - - Skip based on checksum, rather than mod-time & size; Note that that "archive" option is still enabled by default - the "checksum" option will not disable it. - choices: [ 'yes', 'no' ] - default: 'no' - required: false - version_added: "1.6" - compress: - description: - - Compress file data during the transfer. In most cases, leave this enabled unless it causes problems. - choices: [ 'yes', 'no' ] - default: 'yes' - required: false - version_added: "1.7" - existing_only: - description: - - Skip creating new files on receiver. - choices: [ 'yes', 'no' ] - default: 'no' - required: false - version_added: "1.5" - delete: - description: - - Delete files that don't exist (after transfer, not before) in the C(src) path. This option requires C(recursive=yes). - choices: [ 'yes', 'no' ] - default: 'no' - required: false - dirs: - description: - - Transfer directories without recursing - choices: [ 'yes', 'no' ] - default: 'no' - required: false - recursive: - description: - - Recurse into directories. - choices: [ 'yes', 'no' ] - default: the value of the archive option - required: false - links: - description: - - Copy symlinks as symlinks. - choices: [ 'yes', 'no' ] - default: the value of the archive option - required: false - copy_links: - description: - - Copy symlinks as the item that they point to (the referent) is copied, rather than the symlink. - choices: [ 'yes', 'no' ] - default: 'no' - required: false - perms: - description: - - Preserve permissions. - choices: [ 'yes', 'no' ] - default: the value of the archive option - required: false - times: - description: - - Preserve modification times - choices: [ 'yes', 'no' ] - default: the value of the archive option - required: false - owner: - description: - - Preserve owner (super user only) - choices: [ 'yes', 'no' ] - default: the value of the archive option - required: false - group: - description: - - Preserve group - choices: [ 'yes', 'no' ] - default: the value of the archive option - required: false - rsync_path: - description: - - Specify the rsync command to run on the remote machine. See C(--rsync-path) on the rsync man page. - required: false - rsync_timeout: - description: - - Specify a --timeout for the rsync command in seconds. - default: 0 - required: false - set_remote_user: - description: - - put user@ for the remote paths. If you have a custom ssh config to define the remote user for a host - that does not match the inventory user, you should set this parameter to "no". - default: yes - rsync_opts: - description: - - Specify additional rsync options by passing in an array. - default: - required: false - version_added: "1.6" -notes: - - Inspect the verbose output to validate the destination user/host/path - are what was expected. - - The remote user for the dest path will always be the remote_user, not - the sudo_user. - - Expect that dest=~/x will be ~/x even if using sudo. - - To exclude files and directories from being synchronized, you may add - C(.rsync-filter) files to the source directory. - - -author: Timothy Appnel -''' - -EXAMPLES = ''' -# Synchronization of src on the control machine to dest on the remote hosts -synchronize: src=some/relative/path dest=/some/absolute/path - -# Synchronization without any --archive options enabled -synchronize: src=some/relative/path dest=/some/absolute/path archive=no - -# Synchronization with --archive options enabled except for --recursive -synchronize: src=some/relative/path dest=/some/absolute/path recursive=no - -# Synchronization with --archive options enabled except for --times, with --checksum option enabled -synchronize: src=some/relative/path dest=/some/absolute/path checksum=yes times=no - -# Synchronization without --archive options enabled except use --links -synchronize: src=some/relative/path dest=/some/absolute/path archive=no links=yes - -# Synchronization of two paths both on the control machine -local_action: synchronize src=some/relative/path dest=/some/absolute/path - -# Synchronization of src on the inventory host to the dest on the localhost in -pull mode -synchronize: mode=pull src=some/relative/path dest=/some/absolute/path - -# Synchronization of src on delegate host to dest on the current inventory host -synchronize: > - src=some/relative/path dest=/some/absolute/path - delegate_to: delegate.host - -# Synchronize and delete files in dest on the remote host that are not found in src of localhost. -synchronize: src=some/relative/path dest=/some/absolute/path delete=yes - -# Synchronize using an alternate rsync command -synchronize: src=some/relative/path dest=/some/absolute/path rsync_path="sudo rsync" - -# Example .rsync-filter file in the source directory -- var # exclude any path whose last part is 'var' -- /var # exclude any path starting with 'var' starting at the source directory -+ /var/conf # include /var/conf even though it was previously excluded - -# Synchronize passing in extra rsync options -synchronize: src=/tmp/helloworld dest=/var/www/helloword rsync_opts=--no-motd,--exclude=.git -''' - - -def main(): - module = AnsibleModule( - argument_spec = dict( - src = dict(required=True), - dest = dict(required=True), - dest_port = dict(default=22), - delete = dict(default='no', type='bool'), - private_key = dict(default=None), - rsync_path = dict(default=None), - archive = dict(default='yes', type='bool'), - checksum = dict(default='no', type='bool'), - compress = dict(default='yes', type='bool'), - existing_only = dict(default='no', type='bool'), - dirs = dict(default='no', type='bool'), - recursive = dict(type='bool'), - links = dict(type='bool'), - copy_links = dict(type='bool'), - perms = dict(type='bool'), - times = dict(type='bool'), - owner = dict(type='bool'), - group = dict(type='bool'), - set_remote_user = dict(default='yes', type='bool'), - rsync_timeout = dict(type='int', default=0), - rsync_opts = dict(type='list') - ), - supports_check_mode = True - ) - - source = '"' + module.params['src'] + '"' - dest = '"' + module.params['dest'] + '"' - dest_port = module.params['dest_port'] - delete = module.params['delete'] - private_key = module.params['private_key'] - rsync_path = module.params['rsync_path'] - rsync = module.params.get('local_rsync_path', 'rsync') - rsync_timeout = module.params.get('rsync_timeout', 'rsync_timeout') - archive = module.params['archive'] - checksum = module.params['checksum'] - compress = module.params['compress'] - existing_only = module.params['existing_only'] - dirs = module.params['dirs'] - # the default of these params depends on the value of archive - recursive = module.params['recursive'] - links = module.params['links'] - copy_links = module.params['copy_links'] - perms = module.params['perms'] - times = module.params['times'] - owner = module.params['owner'] - group = module.params['group'] - rsync_opts = module.params['rsync_opts'] - - cmd = '%s --delay-updates -FF' % rsync - if compress: - cmd = cmd + ' --compress' - if rsync_timeout: - cmd = cmd + ' --timeout=%s' % rsync_timeout - if module.check_mode: - cmd = cmd + ' --dry-run' - if delete: - cmd = cmd + ' --delete-after' - if existing_only: - cmd = cmd + ' --existing' - if checksum: - cmd = cmd + ' --checksum' - if archive: - cmd = cmd + ' --archive' - if recursive is False: - cmd = cmd + ' --no-recursive' - if links is False: - cmd = cmd + ' --no-links' - if copy_links is True: - cmd = cmd + ' --copy-links' - if perms is False: - cmd = cmd + ' --no-perms' - if times is False: - cmd = cmd + ' --no-times' - if owner is False: - cmd = cmd + ' --no-owner' - if group is False: - cmd = cmd + ' --no-group' - else: - if recursive is True: - cmd = cmd + ' --recursive' - if links is True: - cmd = cmd + ' --links' - if copy_links is True: - cmd = cmd + ' --copy-links' - if perms is True: - cmd = cmd + ' --perms' - if times is True: - cmd = cmd + ' --times' - if owner is True: - cmd = cmd + ' --owner' - if group is True: - cmd = cmd + ' --group' - if dirs: - cmd = cmd + ' --dirs' - if private_key is None: - private_key = '' - else: - private_key = '-i '+ private_key - - ssh_opts = '-S none -o StrictHostKeyChecking=no' - if dest_port != 22: - cmd += " --rsh 'ssh %s %s -o Port=%s'" % (private_key, ssh_opts, dest_port) - else: - cmd += " --rsh 'ssh %s %s'" % (private_key, ssh_opts) # need ssh param - - if rsync_path: - cmd = cmd + " --rsync-path=%s" % (rsync_path) - - if rsync_opts: - cmd = cmd + " " + " ".join(rsync_opts) - - changed_marker = '<>' - cmd = cmd + " --out-format='" + changed_marker + "%i %n%L'" - - # expand the paths - if '@' not in source: - source = os.path.expanduser(source) - if '@' not in dest: - dest = os.path.expanduser(dest) - - cmd = ' '.join([cmd, source, dest]) - cmdstr = cmd - (rc, out, err) = module.run_command(cmd) - if rc: - return module.fail_json(msg=err, rc=rc, cmd=cmdstr) - else: - changed = changed_marker in out - out_clean=out.replace(changed_marker,'') - out_lines=out_clean.split('\n') - while '' in out_lines: - out_lines.remove('') - return module.exit_json(changed=changed, msg=out_clean, - rc=rc, cmd=cmdstr, stdout_lines=out_lines) - -# import module snippets -from ansible.module_utils.basic import * - -main() - diff --git a/extras/files/template b/extras/files/template deleted file mode 100644 index 7ba072fcdc1..00000000000 --- a/extras/files/template +++ /dev/null @@ -1,66 +0,0 @@ -# this is a virtual module that is entirely implemented server side - -DOCUMENTATION = ''' ---- -module: template -version_added: historical -short_description: Templates a file out to a remote server. -description: - - Templates are processed by the Jinja2 templating language - (U(http://jinja.pocoo.org/docs/)) - documentation on the template - formatting can be found in the Template Designer Documentation - (U(http://jinja.pocoo.org/docs/templates/)). - - "Six additional variables can be used in templates: C(ansible_managed) - (configurable via the C(defaults) section of C(ansible.cfg)) contains a string - which can be used to describe the template name, host, modification time of the - template file and the owner uid, C(template_host) contains the node name of - the template's machine, C(template_uid) the owner, C(template_path) the - absolute path of the template, C(template_fullpath) is the absolute path of the - template, and C(template_run_date) is the date that the template was rendered. Note that including - a string that uses a date in the template will result in the template being marked 'changed' - each time." -options: - src: - description: - - Path of a Jinja2 formatted template on the local server. This can be a relative or absolute path. - required: true - default: null - aliases: [] - dest: - description: - - Location to render the template to on the remote machine. - required: true - default: null - backup: - description: - - Create a backup file including the timestamp information so you can get - the original file back if you somehow clobbered it incorrectly. - required: false - choices: [ "yes", "no" ] - default: "no" - validate: - description: - - The validation command to run before copying into place. - - The path to the file to validate is passed in via '%s' which must be present as in the visudo example below. - - validation to run before copying into place. The command is passed - securely so shell features like expansion and pipes won't work. - required: false - default: "" - version_added: "1.2" -notes: - - "Since Ansible version 0.9, templates are loaded with C(trim_blocks=True)." -requirements: [] -author: Michael DeHaan -extends_documentation_fragment: files -''' - -EXAMPLES = ''' -# Example from Ansible Playbooks -- template: src=/mytemplates/foo.j2 dest=/etc/file.conf owner=bin group=wheel mode=0644 - -# The same example, but using symbolic modes equivalent to 0644 -- template: src=/mytemplates/foo.j2 dest=/etc/file.conf owner=bin group=wheel mode="u=rw,g=r,o=r" - -# Copy a new "sudoers" file into place, after passing validation with visudo -- template: src=/mine/sudoers dest=/etc/sudoers validate='visudo -cf %s' -''' diff --git a/extras/files/unarchive b/extras/files/unarchive deleted file mode 100644 index 657e464937b..00000000000 --- a/extras/files/unarchive +++ /dev/null @@ -1,250 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2012, Michael DeHaan -# (c) 2013, Dylan Martin -# -# 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: unarchive -version_added: 1.4 -short_description: Copies an archive to a remote location and unpack it -description: - - The M(unarchive) module copies an archive file from the local machine to a remote and unpacks it. -options: - src: - description: - - Local path to archive file to copy to the remote server; can be absolute or relative. - required: true - default: null - dest: - description: - - Remote absolute path where the archive should be unpacked - required: true - default: null - copy: - description: - - "if true, the file is copied from the 'master' to the target machine, otherwise, the plugin will look for src archive at the target machine." - required: false - choices: [ "yes", "no" ] - default: "yes" - creates: - description: - - a filename, when it already exists, this step will B(not) be run. - required: no - default: null - version_added: "1.6" -author: Dylan Martin -todo: - - detect changed/unchanged for .zip files - - handle common unarchive args, like preserve owner/timestamp etc... -notes: - - requires C(tar)/C(unzip) command on target host - - can handle I(gzip), I(bzip2) and I(xz) compressed as well as uncompressed tar files - - detects type of archive automatically - - uses tar's C(--diff arg) to calculate if changed or not. If this C(arg) is not - supported, it will always unpack the archive - - does not detect if a .zip file is different from destination - always unzips - - existing files/directories in the destination which are not in the archive - are not touched. This is the same behavior as a normal archive extraction - - existing files/directories in the destination which are not in the archive - are ignored for purposes of deciding if the archive should be unpacked or not -''' - -EXAMPLES = ''' -# Example from Ansible Playbooks -- unarchive: src=foo.tgz dest=/var/lib/foo - -# Unarchive a file that is already on the remote machine -- unarchive: src=/tmp/foo.zip dest=/usr/local/bin copy=no -''' - -import os - - -# class to handle .zip files -class ZipFile(object): - - def __init__(self, src, dest, module): - self.src = src - self.dest = dest - self.module = module - self.cmd_path = self.module.get_bin_path('unzip') - - def is_unarchived(self): - return dict(unarchived=False) - - def unarchive(self): - cmd = '%s -o "%s" -d "%s"' % (self.cmd_path, self.src, self.dest) - rc, out, err = self.module.run_command(cmd) - return dict(cmd=cmd, rc=rc, out=out, err=err) - - def can_handle_archive(self): - if not self.cmd_path: - return False - cmd = '%s -l "%s"' % (self.cmd_path, self.src) - rc, out, err = self.module.run_command(cmd) - if rc == 0: - return True - return False - - -# class to handle gzipped tar files -class TgzFile(object): - - def __init__(self, src, dest, module): - self.src = src - self.dest = dest - self.module = module - self.cmd_path = self.module.get_bin_path('tar') - self.zipflag = 'z' - - def is_unarchived(self): - cmd = '%s -v -C "%s" --diff -%sf "%s"' % (self.cmd_path, self.dest, self.zipflag, self.src) - rc, out, err = self.module.run_command(cmd) - unarchived = (rc == 0) - return dict(unarchived=unarchived, rc=rc, out=out, err=err, cmd=cmd) - - def unarchive(self): - cmd = '%s -x%sf "%s"' % (self.cmd_path, self.zipflag, self.src) - rc, out, err = self.module.run_command(cmd, cwd=self.dest) - return dict(cmd=cmd, rc=rc, out=out, err=err) - - def can_handle_archive(self): - if not self.cmd_path: - return False - cmd = '%s -t%sf "%s"' % (self.cmd_path, self.zipflag, self.src) - rc, out, err = self.module.run_command(cmd) - if rc == 0: - if len(out.splitlines(True)) > 0: - return True - return False - - -# class to handle tar files that aren't compressed -class TarFile(TgzFile): - def __init__(self, src, dest, module): - self.src = src - self.dest = dest - self.module = module - self.cmd_path = self.module.get_bin_path('tar') - self.zipflag = '' - - -# class to handle bzip2 compressed tar files -class TarBzip(TgzFile): - def __init__(self, src, dest, module): - self.src = src - self.dest = dest - self.module = module - self.cmd_path = self.module.get_bin_path('tar') - self.zipflag = 'j' - - -# class to handle xz compressed tar files -class TarXz(TgzFile): - def __init__(self, src, dest, module): - self.src = src - self.dest = dest - self.module = module - self.cmd_path = self.module.get_bin_path('tar') - self.zipflag = 'J' - - -# try handlers in order and return the one that works or bail if none work -def pick_handler(src, dest, module): - handlers = [TgzFile, ZipFile, TarFile, TarBzip, TarXz] - for handler in handlers: - obj = handler(src, dest, module) - if obj.can_handle_archive(): - return obj - module.fail_json(msg='Failed to find handler to unarchive. Make sure the required command to extract the file is installed.') - - -def main(): - module = AnsibleModule( - # not checking because of daisy chain to file module - argument_spec = dict( - src = dict(required=True), - original_basename = dict(required=False), # used to handle 'dest is a directory' via template, a slight hack - dest = dict(required=True), - copy = dict(default=True, type='bool'), - creates = dict(required=False), - ), - add_file_common_args=True, - ) - - src = os.path.expanduser(module.params['src']) - dest = os.path.expanduser(module.params['dest']) - copy = module.params['copy'] - creates = module.params['creates'] - - # did tar file arrive? - if not os.path.exists(src): - if copy: - module.fail_json(msg="Source '%s' failed to transfer" % src) - else: - module.fail_json(msg="Source '%s' does not exist" % src) - if not os.access(src, os.R_OK): - module.fail_json(msg="Source '%s' not readable" % src) - - if creates: - # do not run the command if the line contains creates=filename - # and the filename already exists. This allows idempotence - # of command executions. - v = os.path.expanduser(creates) - if os.path.exists(v): - module.exit_json( - stdout="skipped, since %s exists" % v, - skipped=True, - changed=False, - stderr=False, - rc=0 - ) - - # is dest OK to receive tar file? - if not os.path.isdir(dest): - module.fail_json(msg="Destination '%s' is not a directory" % dest) - if not os.access(dest, os.W_OK): - module.fail_json(msg="Destination '%s' not writable" % dest) - - handler = pick_handler(src, dest, module) - - res_args = dict(handler=handler.__class__.__name__, dest=dest, src=src) - - # do we need to do unpack? - res_args['check_results'] = handler.is_unarchived() - if res_args['check_results']['unarchived']: - res_args['changed'] = False - module.exit_json(**res_args) - - # do the unpack - try: - res_args['extract_results'] = handler.unarchive() - if res_args['extract_results']['rc'] != 0: - module.fail_json(msg="failed to unpack %s to %s" % (src, dest), **res_args) - except IOError: - module.fail_json(msg="failed to unpack %s to %s" % (src, dest)) - - res_args['changed'] = True - - module.exit_json(**res_args) - -# import module snippets -from ansible.module_utils.basic import * -main() diff --git a/extras/files/xattr b/extras/files/xattr deleted file mode 100644 index 94115ae3b51..00000000000 --- a/extras/files/xattr +++ /dev/null @@ -1,206 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -DOCUMENTATION = ''' ---- -module: xattr -version_added: "1.3" -short_description: set/retrieve extended attributes -description: - - Manages filesystem user defined extended attributes, requires that they are enabled - on the target filesystem and that the setfattr/getfattr utilities are present. -options: - name: - required: true - default: None - aliases: ['path'] - description: - - The full path of the file/object to get the facts of - key: - required: false - default: None - description: - - The name of a specific Extended attribute key to set/retrieve - value: - required: false - default: None - description: - - The value to set the named name/key to, it automatically sets the C(state) to 'set' - state: - required: false - default: get - choices: [ 'read', 'present', 'all', 'keys', 'absent' ] - description: - - defines which state you want to do. - C(read) retrieves the current value for a C(key) (default) - C(present) sets C(name) to C(value), default if value is set - C(all) dumps all data - C(keys) retrieves all keys - C(absent) deletes the key - follow: - required: false - default: yes - choices: [ 'yes', 'no' ] - description: - - if yes, dereferences symlinks and sets/gets attributes on symlink target, - otherwise acts on symlink itself. - -author: Brian Coca -''' - -EXAMPLES = ''' -# Obtain the extended attributes of /etc/foo.conf -- xattr: name=/etc/foo.conf - -# Sets the key 'foo' to value 'bar' -- xattr: path=/etc/foo.conf key=user.foo value=bar - -# Removes the key 'foo' -- xattr: name=/etc/foo.conf key=user.foo state=absent -''' - -import operator - -def get_xattr_keys(module,path,follow): - cmd = [ module.get_bin_path('getfattr', True) ] - # prevents warning and not sure why it's not default - cmd.append('--absolute-names') - if not follow: - cmd.append('-h') - cmd.append(path) - - return _run_xattr(module,cmd) - -def get_xattr(module,path,key,follow): - - cmd = [ module.get_bin_path('getfattr', True) ] - # prevents warning and not sure why it's not default - cmd.append('--absolute-names') - if not follow: - cmd.append('-h') - if key is None: - cmd.append('-d') - else: - cmd.append('-n %s' % key) - cmd.append(path) - - return _run_xattr(module,cmd,False) - -def set_xattr(module,path,key,value,follow): - - cmd = [ module.get_bin_path('setfattr', True) ] - if not follow: - cmd.append('-h') - cmd.append('-n %s' % key) - cmd.append('-v %s' % value) - cmd.append(path) - - return _run_xattr(module,cmd) - -def rm_xattr(module,path,key,follow): - - cmd = [ module.get_bin_path('setfattr', True) ] - if not follow: - cmd.append('-h') - cmd.append('-x %s' % key) - cmd.append(path) - - return _run_xattr(module,cmd,False) - -def _run_xattr(module,cmd,check_rc=True): - - try: - (rc, out, err) = module.run_command(' '.join(cmd), check_rc=check_rc) - except Exception, e: - module.fail_json(msg="%s!" % e.strerror) - - #result = {'raw': out} - result = {} - for line in out.splitlines(): - if re.match("^#", line) or line == "": - pass - elif re.search('=', line): - (key, val) = line.split("=") - result[key] = val.strip('"') - else: - result[line] = '' - return result - -def main(): - module = AnsibleModule( - argument_spec = dict( - name = dict(required=True, aliases=['path']), - key = dict(required=False, default=None), - value = dict(required=False, default=None), - state = dict(required=False, default='read', choices=[ 'read', 'present', 'all', 'keys', 'absent' ], type='str'), - follow = dict(required=False, type='bool', default=True), - ), - supports_check_mode=True, - ) - path = module.params.get('name') - key = module.params.get('key') - value = module.params.get('value') - state = module.params.get('state') - follow = module.params.get('follow') - - if not os.path.exists(path): - module.fail_json(msg="path not found or not accessible!") - - - changed=False - msg = "" - res = {} - - if key is None and state in ['present','absent']: - module.fail_json(msg="%s needs a key parameter" % state) - - # All xattr must begin in user namespace - if key is not None and not re.match('^user\.',key): - key = 'user.%s' % key - - - if (state == 'present' or value is not None): - current=get_xattr(module,path,key,follow) - if current is None or not key in current or value != current[key]: - if not module.check_mode: - res = set_xattr(module,path,key,value,follow) - changed=True - res=current - msg="%s set to %s" % (key, value) - elif state == 'absent': - current=get_xattr(module,path,key,follow) - if current is not None and key in current: - if not module.check_mode: - res = rm_xattr(module,path,key,follow) - changed=True - res=current - msg="%s removed" % (key) - elif state == 'keys': - res=get_xattr_keys(module,path,follow) - msg="returning all keys" - elif state == 'all': - res=get_xattr(module,path,None,follow) - msg="dumping all" - else: - res=get_xattr(module,path,key,follow) - msg="returning %s" % key - - module.exit_json(changed=changed, msg=msg, xattr=res) - -# import module snippets -from ansible.module_utils.basic import * - -main() diff --git a/extras/internal/async_status b/extras/internal/async_status deleted file mode 100644 index f991b50064b..00000000000 --- a/extras/internal/async_status +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2012, Michael DeHaan , and others -# -# 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: async_status -short_description: Obtain status of asynchronous task -description: - - "This module gets the status of an asynchronous task." -version_added: "0.5" -options: - jid: - description: - - Job or task identifier - required: true - default: null - aliases: [] - mode: - description: - - if C(status), obtain the status; if C(cleanup), clean up the async job cache - located in C(~/.ansible_async/) for the specified job I(jid). - required: false - choices: [ "status", "cleanup" ] - default: "status" -notes: - - See also U(http://docs.ansible.com/playbooks_async.html) -requirements: [] -author: Michael DeHaan -''' - -import datetime -import traceback - -def main(): - - module = AnsibleModule(argument_spec=dict( - jid=dict(required=True), - mode=dict(default='status', choices=['status','cleanup']), - )) - - mode = module.params['mode'] - jid = module.params['jid'] - - # setup logging directory - logdir = os.path.expanduser("~/.ansible_async") - log_path = os.path.join(logdir, jid) - - if not os.path.exists(log_path): - module.fail_json(msg="could not find job", ansible_job_id=jid) - - if mode == 'cleanup': - os.unlink(log_path) - module.exit_json(ansible_job_id=jid, erased=log_path) - - # NOT in cleanup mode, assume regular status mode - # no remote kill mode currently exists, but probably should - # consider log_path + ".pid" file and also unlink that above - - data = file(log_path).read() - try: - data = json.loads(data) - except Exception, e: - if data == '': - # file not written yet? That means it is running - module.exit_json(results_file=log_path, ansible_job_id=jid, started=1, finished=0) - else: - module.fail_json(ansible_job_id=jid, results_file=log_path, - msg="Could not parse job output: %s" % data) - - if not 'started' in data: - data['finished'] = 1 - data['ansible_job_id'] = jid - - # Fix error: TypeError: exit_json() keywords must be strings - data = dict([(str(k), v) for k, v in data.iteritems()]) - - module.exit_json(**data) - -# import module snippets -from ansible.module_utils.basic import * -main() diff --git a/extras/internal/async_wrapper b/extras/internal/async_wrapper deleted file mode 100644 index 2bc2dc21823..00000000000 --- a/extras/internal/async_wrapper +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2012, Michael DeHaan , and others -# -# 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 . -# - -try: - import json -except ImportError: - import simplejson as json -import shlex -import os -import subprocess -import sys -import datetime -import traceback -import signal -import time -import syslog - -def daemonize_self(): - # daemonizing code: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012 - # logger.info("cobblerd started") - try: - pid = os.fork() - if pid > 0: - # exit first parent - sys.exit(0) - except OSError, e: - print >>sys.stderr, "fork #1 failed: %d (%s)" % (e.errno, e.strerror) - sys.exit(1) - - # decouple from parent environment - os.chdir("/") - os.setsid() - os.umask(022) - - # do second fork - try: - pid = os.fork() - if pid > 0: - # print "Daemon PID %d" % pid - sys.exit(0) - except OSError, e: - print >>sys.stderr, "fork #2 failed: %d (%s)" % (e.errno, e.strerror) - sys.exit(1) - - dev_null = file('/dev/null','rw') - os.dup2(dev_null.fileno(), sys.stdin.fileno()) - os.dup2(dev_null.fileno(), sys.stdout.fileno()) - os.dup2(dev_null.fileno(), sys.stderr.fileno()) - -if len(sys.argv) < 3: - print json.dumps({ - "failed" : True, - "msg" : "usage: async_wrapper . Humans, do not call directly!" - }) - sys.exit(1) - -jid = "%s.%d" % (sys.argv[1], os.getpid()) -time_limit = sys.argv[2] -wrapped_module = sys.argv[3] -argsfile = sys.argv[4] -cmd = "%s %s" % (wrapped_module, argsfile) - -syslog.openlog('ansible-%s' % os.path.basename(__file__)) -syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % " ".join(sys.argv[1:])) - -# setup logging directory -logdir = os.path.expanduser("~/.ansible_async") -log_path = os.path.join(logdir, jid) - -if not os.path.exists(logdir): - try: - os.makedirs(logdir) - except: - print json.dumps({ - "failed" : 1, - "msg" : "could not create: %s" % logdir - }) - -def _run_command(wrapped_cmd, jid, log_path): - - logfile = open(log_path, "w") - logfile.write(json.dumps({ "started" : 1, "ansible_job_id" : jid })) - logfile.close() - logfile = open(log_path, "w") - result = {} - - outdata = '' - try: - cmd = shlex.split(wrapped_cmd) - script = subprocess.Popen(cmd, shell=False, - stdin=None, stdout=logfile, stderr=logfile) - script.communicate() - outdata = file(log_path).read() - result = json.loads(outdata) - - except (OSError, IOError), e: - result = { - "failed": 1, - "cmd" : wrapped_cmd, - "msg": str(e), - } - result['ansible_job_id'] = jid - logfile.write(json.dumps(result)) - except: - result = { - "failed" : 1, - "cmd" : wrapped_cmd, - "data" : outdata, # temporary debug only - "msg" : traceback.format_exc() - } - result['ansible_job_id'] = jid - logfile.write(json.dumps(result)) - logfile.close() - -# immediately exit this process, leaving an orphaned process -# running which immediately forks a supervisory timing process - -#import logging -#import logging.handlers - -#logger = logging.getLogger("ansible_async") -#logger.setLevel(logging.WARNING) -#logger.addHandler( logging.handlers.SysLogHandler("/dev/log") ) -def debug(msg): - #logger.warning(msg) - pass - -try: - pid = os.fork() - if pid: - # Notify the overlord that the async process started - - # we need to not return immmediately such that the launched command has an attempt - # to initialize PRIOR to ansible trying to clean up the launch directory (and argsfile) - # this probably could be done with some IPC later. Modules should always read - # the argsfile at the very first start of their execution anyway - time.sleep(1) - debug("Return async_wrapper task started.") - print json.dumps({ "started" : 1, "ansible_job_id" : jid, "results_file" : log_path }) - sys.stdout.flush() - sys.exit(0) - else: - # The actual wrapper process - - # Daemonize, so we keep on running - daemonize_self() - - # we are now daemonized, create a supervisory process - debug("Starting module and watcher") - - sub_pid = os.fork() - if sub_pid: - # the parent stops the process after the time limit - remaining = int(time_limit) - - # set the child process group id to kill all children - os.setpgid(sub_pid, sub_pid) - - debug("Start watching %s (%s)"%(sub_pid, remaining)) - time.sleep(5) - while os.waitpid(sub_pid, os.WNOHANG) == (0, 0): - debug("%s still running (%s)"%(sub_pid, remaining)) - time.sleep(5) - remaining = remaining - 5 - if remaining <= 0: - debug("Now killing %s"%(sub_pid)) - os.killpg(sub_pid, signal.SIGKILL) - debug("Sent kill to group %s"%sub_pid) - time.sleep(1) - sys.exit(0) - debug("Done in kid B.") - os._exit(0) - else: - # the child process runs the actual module - debug("Start module (%s)"%os.getpid()) - _run_command(cmd, jid, log_path) - debug("Module complete (%s)"%os.getpid()) - sys.exit(0) - -except Exception, err: - debug("error: %s"%(err)) - raise err diff --git a/extras/inventory/add_host b/extras/inventory/add_host deleted file mode 100644 index 4fd4e1eb15f..00000000000 --- a/extras/inventory/add_host +++ /dev/null @@ -1,36 +0,0 @@ -# -*- mode: python -*- - -DOCUMENTATION = ''' ---- -module: add_host -short_description: add a host (and alternatively a group) to the ansible-playbook in-memory inventory -description: - - Use variables to create new hosts and groups in inventory for use in later plays of the same playbook. - Takes variables so you can define the new hosts more fully. -version_added: "0.9" -options: - name: - aliases: [ 'hostname', 'host' ] - description: - - The hostname/ip of the host to add to the inventory, can include a colon and a port number. - required: true - groups: - aliases: [ 'groupname', 'group' ] - description: - - The groups to add the hostname to, comma separated. - required: false -author: Seth Vidal -''' - -EXAMPLES = ''' -# add host to group 'just_created' with variable foo=42 -- add_host: name={{ ip_from_ec2 }} groups=just_created foo=42 - -# add a host with a non-standard port local to your machines -- add_host: name={{ new_ip }}:{{ new_port }} - -# add a host alias that we reach through a tunnel -- add_host: hostname={{ new_ip }} - ansible_ssh_host={{ inventory_hostname }} - ansible_ssh_port={{ new_port }} -''' diff --git a/extras/inventory/group_by b/extras/inventory/group_by deleted file mode 100644 index d09552e662c..00000000000 --- a/extras/inventory/group_by +++ /dev/null @@ -1,25 +0,0 @@ -# -*- mode: python -*- - -DOCUMENTATION = ''' ---- -module: group_by -short_description: Create Ansible groups based on facts -description: - - Use facts to create ad-hoc groups that can be used later in a playbook. -version_added: "0.9" -options: - key: - description: - - The variables whose values will be used as groups - required: true -author: Jeroen Hoekx -notes: - - Spaces in group names are converted to dashes '-'. -''' - -EXAMPLES = ''' -# Create groups based on the machine architecture -- group_by: key=machine_{{ ansible_machine }} -# Create groups like 'kvm-host' -- group_by: key=virt_{{ ansible_virtualization_type }}_{{ ansible_virtualization_role }} -''' diff --git a/extras/system/authorized_key b/extras/system/authorized_key deleted file mode 100644 index f964113127e..00000000000 --- a/extras/system/authorized_key +++ /dev/null @@ -1,421 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -""" -Ansible module to add authorized_keys for ssh logins. -(c) 2012, Brad Olson - -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: authorized_key -short_description: Adds or removes an SSH authorized key -description: - - Adds or removes an SSH authorized key for a user from a remote host. -version_added: "0.5" -options: - user: - description: - - The username on the remote host whose authorized_keys file will be modified - required: true - default: null - aliases: [] - key: - description: - - The SSH public key, as a string - required: true - default: null - path: - description: - - Alternate path to the authorized_keys file - required: false - default: "(homedir)+/.ssh/authorized_keys" - version_added: "1.2" - manage_dir: - description: - - Whether this module should manage the directory of the authorized key file. If - set, the module will create the directory, as well as set the owner and permissions - of an existing directory. Be sure to - set C(manage_dir=no) if you are using an alternate directory for - authorized_keys, as set with C(path), since you could lock yourself out of - SSH access. See the example below. - required: false - choices: [ "yes", "no" ] - default: "yes" - version_added: "1.2" - state: - description: - - Whether the given key (with the given key_options) should or should not be in the file - required: false - choices: [ "present", "absent" ] - default: "present" - key_options: - description: - - A string of ssh key options to be prepended to the key in the authorized_keys file - required: false - default: null - version_added: "1.4" -description: - - "Adds or removes authorized keys for particular user accounts" -author: Brad Olson -''' - -EXAMPLES = ''' -# Example using key data from a local file on the management machine -- authorized_key: user=charlie key="{{ lookup('file', '/home/charlie/.ssh/id_rsa.pub') }}" - -# Using alternate directory locations: -- authorized_key: user=charlie - key="{{ lookup('file', '/home/charlie/.ssh/id_rsa.pub') }}" - path='/etc/ssh/authorized_keys/charlie' - manage_dir=no - -# Using with_file -- name: Set up authorized_keys for the deploy user - authorized_key: user=deploy - key="{{ item }}" - with_file: - - public_keys/doe-jane - - public_keys/doe-john - -# Using key_options: -- authorized_key: user=charlie - key="{{ lookup('file', '/home/charlie/.ssh/id_rsa.pub') }}" - key_options='no-port-forwarding,host="10.0.1.1"' -''' - -# Makes sure the public key line is present or absent in the user's .ssh/authorized_keys. -# -# Arguments -# ========= -# user = username -# key = line to add to authorized_keys for user -# path = path to the user's authorized_keys file (default: ~/.ssh/authorized_keys) -# manage_dir = whether to create, and control ownership of the directory (default: true) -# state = absent|present (default: present) -# -# see example in examples/playbooks - -import sys -import os -import pwd -import os.path -import tempfile -import re -import shlex - -class keydict(dict): - - """ a dictionary that maintains the order of keys as they are added """ - - # http://stackoverflow.com/questions/2328235/pythonextend-the-dict-class - - def __init__(self, *args, **kw): - super(keydict,self).__init__(*args, **kw) - self.itemlist = super(keydict,self).keys() - def __setitem__(self, key, value): - self.itemlist.append(key) - super(keydict,self).__setitem__(key, value) - def __iter__(self): - return iter(self.itemlist) - def keys(self): - return self.itemlist - def values(self): - return [self[key] for key in self] - def itervalues(self): - return (self[key] for key in self) - -def keyfile(module, user, write=False, path=None, manage_dir=True): - """ - Calculate name of authorized keys file, optionally creating the - directories and file, properly setting permissions. - - :param str user: name of user in passwd file - :param bool write: if True, write changes to authorized_keys file (creating directories if needed) - :param str path: if not None, use provided path rather than default of '~user/.ssh/authorized_keys' - :param bool manage_dir: if True, create and set ownership of the parent dir of the authorized_keys file - :return: full path string to authorized_keys for user - """ - - try: - user_entry = pwd.getpwnam(user) - except KeyError, e: - module.fail_json(msg="Failed to lookup user %s: %s" % (user, str(e))) - if path is None: - homedir = user_entry.pw_dir - sshdir = os.path.join(homedir, ".ssh") - keysfile = os.path.join(sshdir, "authorized_keys") - else: - sshdir = os.path.dirname(path) - keysfile = path - - if not write: - return keysfile - - uid = user_entry.pw_uid - gid = user_entry.pw_gid - - if manage_dir: - if not os.path.exists(sshdir): - os.mkdir(sshdir, 0700) - if module.selinux_enabled(): - module.set_default_selinux_context(sshdir, False) - os.chown(sshdir, uid, gid) - os.chmod(sshdir, 0700) - - if not os.path.exists(keysfile): - basedir = os.path.dirname(keysfile) - if not os.path.exists(basedir): - os.makedirs(basedir) - try: - f = open(keysfile, "w") #touches file so we can set ownership and perms - finally: - f.close() - if module.selinux_enabled(): - module.set_default_selinux_context(keysfile, False) - - try: - os.chown(keysfile, uid, gid) - os.chmod(keysfile, 0600) - except OSError: - pass - - return keysfile - -def parseoptions(module, options): - ''' - reads a string containing ssh-key options - and returns a dictionary of those options - ''' - options_dict = keydict() #ordered dict - if options: - try: - # the following regex will split on commas while - # ignoring those commas that fall within quotes - regex = re.compile(r'''((?:[^,"']|"[^"]*"|'[^']*')+)''') - parts = regex.split(options)[1:-1] - for part in parts: - if "=" in part: - (key, value) = part.split("=", 1) - options_dict[key] = value - elif part != ",": - options_dict[part] = None - except: - module.fail_json(msg="invalid option string: %s" % options) - - return options_dict - -def parsekey(module, raw_key): - ''' - parses a key, which may or may not contain a list - of ssh-key options at the beginning - ''' - - VALID_SSH2_KEY_TYPES = [ - 'ssh-ed25519', - 'ecdsa-sha2-nistp256', - 'ecdsa-sha2-nistp384', - 'ecdsa-sha2-nistp521', - 'ssh-dss', - 'ssh-rsa', - ] - - options = None # connection options - key = None # encrypted key string - key_type = None # type of ssh key - type_index = None # index of keytype in key string|list - - # remove comment yaml escapes - raw_key = raw_key.replace('\#', '#') - - # split key safely - lex = shlex.shlex(raw_key) - lex.quotes = [] - lex.commenters = '' #keep comment hashes - lex.whitespace_split = True - key_parts = list(lex) - - for i in range(0, len(key_parts)): - if key_parts[i] in VALID_SSH2_KEY_TYPES: - type_index = i - key_type = key_parts[i] - break - - # check for options - if type_index is None: - return None - elif type_index > 0: - options = " ".join(key_parts[:type_index]) - - # parse the options (if any) - options = parseoptions(module, options) - - # get key after the type index - key = key_parts[(type_index + 1)] - - # set comment to everything after the key - if len(key_parts) > (type_index + 1): - comment = " ".join(key_parts[(type_index + 2):]) - - return (key, key_type, options, comment) - -def readkeys(module, filename): - - if not os.path.isfile(filename): - return {} - - keys = {} - f = open(filename) - for line in f.readlines(): - key_data = parsekey(module, line) - if key_data: - # use key as identifier - keys[key_data[0]] = key_data - else: - # for an invalid line, just append the line - # to the array so it will be re-output later - keys[line] = line - f.close() - return keys - -def writekeys(module, filename, keys): - - fd, tmp_path = tempfile.mkstemp('', 'tmp', os.path.dirname(filename)) - f = open(tmp_path,"w") - try: - for index, key in keys.items(): - try: - (keyhash,type,options,comment) = key - option_str = "" - if options: - option_strings = [] - for option_key in options.keys(): - if options[option_key]: - option_strings.append("%s=%s" % (option_key, options[option_key])) - else: - option_strings.append("%s" % option_key) - - option_str = ",".join(option_strings) - option_str += " " - key_line = "%s%s %s %s\n" % (option_str, type, keyhash, comment) - except: - key_line = key - f.writelines(key_line) - except IOError, e: - module.fail_json(msg="Failed to write to file %s: %s" % (tmp_path, str(e))) - f.close() - module.atomic_move(tmp_path, filename) - -def enforce_state(module, params): - """ - Add or remove key. - """ - - user = params["user"] - key = params["key"] - path = params.get("path", None) - manage_dir = params.get("manage_dir", True) - state = params.get("state", "present") - key_options = params.get("key_options", None) - - # extract indivial keys into an array, skipping blank lines and comments - key = [s for s in key.splitlines() if s and not s.startswith('#')] - - - # check current state -- just get the filename, don't create file - do_write = False - params["keyfile"] = keyfile(module, user, do_write, path, manage_dir) - existing_keys = readkeys(module, params["keyfile"]) - - # Check our new keys, if any of them exist we'll continue. - for new_key in key: - parsed_new_key = parsekey(module, new_key) - if key_options is not None: - parsed_options = parseoptions(module, key_options) - parsed_new_key = (parsed_new_key[0], parsed_new_key[1], parsed_options, parsed_new_key[3]) - - if not parsed_new_key: - module.fail_json(msg="invalid key specified: %s" % new_key) - - present = False - matched = False - non_matching_keys = [] - - if parsed_new_key[0] in existing_keys: - present = True - # Then we check if everything matches, including - # the key type and options. If not, we append this - # existing key to the non-matching list - # We only want it to match everything when the state - # is present - if parsed_new_key != existing_keys[parsed_new_key[0]] and state == "present": - non_matching_keys.append(existing_keys[parsed_new_key[0]]) - else: - matched = True - - - # handle idempotent state=present - if state=="present": - if len(non_matching_keys) > 0: - for non_matching_key in non_matching_keys: - if non_matching_key[0] in existing_keys: - del existing_keys[non_matching_key[0]] - do_write = True - - if not matched: - existing_keys[parsed_new_key[0]] = parsed_new_key - do_write = True - - elif state=="absent": - if not matched: - continue - del existing_keys[parsed_new_key[0]] - do_write = True - - if do_write: - if module.check_mode: - module.exit_json(changed=True) - writekeys(module, keyfile(module, user, do_write, path, manage_dir), existing_keys) - params['changed'] = True - else: - if module.check_mode: - module.exit_json(changed=False) - - return params - -def main(): - - module = AnsibleModule( - argument_spec = dict( - user = dict(required=True, type='str'), - key = dict(required=True, type='str'), - path = dict(required=False, type='str'), - manage_dir = dict(required=False, type='bool', default=True), - state = dict(default='present', choices=['absent','present']), - key_options = dict(required=False, type='str'), - unique = dict(default=False, type='bool'), - ), - supports_check_mode=True - ) - - results = enforce_state(module, module.params) - module.exit_json(**results) - -# import module snippets -from ansible.module_utils.basic import * -main() diff --git a/extras/system/cron b/extras/system/cron deleted file mode 100644 index d14f36253c0..00000000000 --- a/extras/system/cron +++ /dev/null @@ -1,524 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# (c) 2012, Dane Summers -# (c) 2013, Mike Grozak -# (c) 2013, Patrick Callahan -# -# 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 . -# -# Cron Plugin: The goal of this plugin is to provide an indempotent method for -# setting up cron jobs on a host. The script will play well with other manually -# entered crons. Each cron job entered will be preceded with a comment -# describing the job so that it can be found later, which is required to be -# present in order for this plugin to find/modify the job. -# -# This module is based on python-crontab by Martin Owens. -# - -DOCUMENTATION = """ ---- -module: cron -short_description: Manage cron.d and crontab entries. -description: - - Use this module to manage crontab entries. This module allows you to create named - crontab entries, update, or delete them. - - 'The module includes one line with the description of the crontab entry C("#Ansible: ") - corresponding to the "name" passed to the module, which is used by future ansible/module calls - to find/check the state. The "name" parameter should be unique, and changing the "name" value - will result in a new cron task being created (or a different one being removed)' -version_added: "0.9" -options: - name: - description: - - Description of a crontab entry. - default: null - required: true - user: - description: - - The specific user whose crontab should be modified. - required: false - default: root - job: - description: - - The command to execute. Required if state=present. - required: false - default: null - state: - description: - - Whether to ensure the job is present or absent. - required: false - default: present - choices: [ "present", "absent" ] - cron_file: - description: - - If specified, uses this file in cron.d instead of an individual user's crontab. - required: false - default: null - backup: - description: - - If set, create a backup of the crontab before it is modified. - The location of the backup is returned in the C(backup) variable by this module. - required: false - default: false - minute: - description: - - Minute when the job should run ( 0-59, *, */2, etc ) - required: false - default: "*" - hour: - description: - - Hour when the job should run ( 0-23, *, */2, etc ) - required: false - default: "*" - day: - description: - - Day of the month the job should run ( 1-31, *, */2, etc ) - required: false - default: "*" - aliases: [ "dom" ] - month: - description: - - Month of the year the job should run ( 1-12, *, */2, etc ) - required: false - default: "*" - weekday: - description: - - Day of the week that the job should run ( 0-6 for Sunday-Saturday, *, etc ) - required: false - default: "*" - aliases: [ "dow" ] - reboot: - description: - - If the job should be run at reboot. This option is deprecated. Users should use special_time. - version_added: "1.0" - required: false - default: "no" - choices: [ "yes", "no" ] - special_time: - description: - - Special time specification nickname. - version_added: "1.3" - required: false - default: null - choices: [ "reboot", "yearly", "annually", "monthly", "weekly", "daily", "hourly" ] -requirements: - - cron -author: Dane Summers -updates: [ 'Mike Grozak', 'Patrick Callahan' ] -""" - -EXAMPLES = ''' -# Ensure a job that runs at 2 and 5 exists. -# Creates an entry like "* 5,2 * * ls -alh > /dev/null" -- cron: name="check dirs" hour="5,2" job="ls -alh > /dev/null" - -# Ensure an old job is no longer present. Removes any job that is prefixed -# by "#Ansible: an old job" from the crontab -- cron: name="an old job" state=absent - -# Creates an entry like "@reboot /some/job.sh" -- cron: name="a job for reboot" special_time=reboot job="/some/job.sh" - -# Creates a cron file under /etc/cron.d -- cron: name="yum autoupdate" weekday="2" minute=0 hour=12 - user="root" job="YUMINTERACTIVE=0 /usr/sbin/yum-autoupdate" - cron_file=ansible_yum-autoupdate - -# Removes a cron file from under /etc/cron.d -- cron: cron_file=ansible_yum-autoupdate state=absent -''' - -import os -import re -import tempfile -import platform -import pipes - -CRONCMD = "/usr/bin/crontab" - -class CronTabError(Exception): - pass - -class CronTab(object): - """ - CronTab object to write time based crontab file - - user - the user of the crontab (defaults to root) - cron_file - a cron file under /etc/cron.d - """ - def __init__(self, module, user=None, cron_file=None): - self.module = module - self.user = user - self.root = (os.getuid() == 0) - self.lines = None - self.ansible = "#Ansible: " - - # select whether we dump additional debug info through syslog - self.syslogging = False - - if cron_file: - self.cron_file = '/etc/cron.d/%s' % cron_file - else: - self.cron_file = None - - self.read() - - def read(self): - # Read in the crontab from the system - self.lines = [] - if self.cron_file: - # read the cronfile - try: - f = open(self.cron_file, 'r') - self.lines = f.read().splitlines() - f.close() - except IOError, e: - # cron file does not exist - return - except: - raise CronTabError("Unexpected error:", sys.exc_info()[0]) - else: - # using safely quoted shell for now, but this really should be two non-shell calls instead. FIXME - (rc, out, err) = self.module.run_command(self._read_user_execute(), use_unsafe_shell=True) - - if rc != 0 and rc != 1: # 1 can mean that there are no jobs. - raise CronTabError("Unable to read crontab") - - lines = out.splitlines() - count = 0 - for l in lines: - if count > 2 or (not re.match( r'# DO NOT EDIT THIS FILE - edit the master and reinstall.', l) and - not re.match( r'# \(/tmp/.*installed on.*\)', l) and - not re.match( r'# \(.*version.*\)', l)): - self.lines.append(l) - count += 1 - - def log_message(self, message): - if self.syslogging: - syslog.syslog(syslog.LOG_NOTICE, 'ansible: "%s"' % message) - - def is_empty(self): - if len(self.lines) == 0: - return True - else: - return False - - def write(self, backup_file=None): - """ - Write the crontab to the system. Saves all information. - """ - if backup_file: - fileh = open(backup_file, 'w') - elif self.cron_file: - fileh = open(self.cron_file, 'w') - else: - filed, path = tempfile.mkstemp(prefix='crontab') - fileh = os.fdopen(filed, 'w') - - fileh.write(self.render()) - fileh.close() - - # return if making a backup - if backup_file: - return - - # Add the entire crontab back to the user crontab - if not self.cron_file: - # quoting shell args for now but really this should be two non-shell calls. FIXME - (rc, out, err) = self.module.run_command(self._write_execute(path), use_unsafe_shell=True) - os.unlink(path) - - if rc != 0: - self.module.fail_json(msg=err) - - def add_job(self, name, job): - # Add the comment - self.lines.append("%s%s" % (self.ansible, name)) - - # Add the job - self.lines.append("%s" % (job)) - - def update_job(self, name, job): - return self._update_job(name, job, self.do_add_job) - - def do_add_job(self, lines, comment, job): - lines.append(comment) - - lines.append("%s" % (job)) - - def remove_job(self, name): - return self._update_job(name, "", self.do_remove_job) - - def do_remove_job(self, lines, comment, job): - return None - - def remove_job_file(self): - try: - os.unlink(self.cron_file) - return True - except OSError, e: - # cron file does not exist - return False - except: - raise CronTabError("Unexpected error:", sys.exc_info()[0]) - - def find_job(self, name): - comment = None - for l in self.lines: - if comment is not None: - if comment == name: - return [comment, l] - else: - comment = None - elif re.match( r'%s' % self.ansible, l): - comment = re.sub( r'%s' % self.ansible, '', l) - - return [] - - def get_cron_job(self,minute,hour,day,month,weekday,job,special): - if special: - if self.cron_file: - return "@%s %s %s" % (special, self.user, job) - else: - return "@%s %s" % (special, job) - else: - if self.cron_file: - return "%s %s %s %s %s %s %s" % (minute,hour,day,month,weekday,self.user,job) - else: - return "%s %s %s %s %s %s" % (minute,hour,day,month,weekday,job) - - return None - - def get_jobnames(self): - jobnames = [] - - for l in self.lines: - if re.match( r'%s' % self.ansible, l): - jobnames.append(re.sub( r'%s' % self.ansible, '', l)) - - return jobnames - - def _update_job(self, name, job, addlinesfunction): - ansiblename = "%s%s" % (self.ansible, name) - newlines = [] - comment = None - - for l in self.lines: - if comment is not None: - addlinesfunction(newlines, comment, job) - comment = None - elif l == ansiblename: - comment = l - else: - newlines.append(l) - - self.lines = newlines - - if len(newlines) == 0: - return True - else: - return False # TODO add some more error testing - - def render(self): - """ - Render this crontab as it would be in the crontab. - """ - crons = [] - for cron in self.lines: - crons.append(cron) - - result = '\n'.join(crons) - if result and result[-1] not in ['\n', '\r']: - result += '\n' - return result - - def _read_user_execute(self): - """ - Returns the command line for reading a crontab - """ - user = '' - if self.user: - if platform.system() == 'SunOS': - return "su %s -c '%s -l'" % (pipes.quote(self.user), pipes.quote(CRONCMD)) - elif platform.system() == 'AIX': - return "%s -l %s" % (pipes.quote(CRONCMD), pipes.quote(self.user)) - elif platform.system() == 'HP-UX': - return "%s %s %s" % (CRONCMD , '-l', pipes.quote(self.user)) - else: - user = '-u %s' % pipes.quote(self.user) - return "%s %s %s" % (CRONCMD , user, '-l') - - def _write_execute(self, path): - """ - Return the command line for writing a crontab - """ - user = '' - if self.user: - if platform.system() in ['SunOS', 'HP-UX', 'AIX']: - return "chown %s %s ; su '%s' -c '%s %s'" % (pipes.quote(self.user), pipes.quote(path), pipes.quote(self.user), CRONCMD, pipes.quote(path)) - else: - user = '-u %s' % pipes.quote(self.user) - return "%s %s %s" % (CRONCMD , user, pipes.quote(path)) - - - -#================================================== - -def main(): - # The following example playbooks: - # - # - cron: name="check dirs" hour="5,2" job="ls -alh > /dev/null" - # - # - name: do the job - # cron: name="do the job" hour="5,2" job="/some/dir/job.sh" - # - # - name: no job - # cron: name="an old job" state=absent - # - # Would produce: - # # Ansible: check dirs - # * * 5,2 * * ls -alh > /dev/null - # # Ansible: do the job - # * * 5,2 * * /some/dir/job.sh - - module = AnsibleModule( - argument_spec = dict( - name=dict(required=True), - user=dict(required=False), - job=dict(required=False), - cron_file=dict(required=False), - state=dict(default='present', choices=['present', 'absent']), - backup=dict(default=False, type='bool'), - minute=dict(default='*'), - hour=dict(default='*'), - day=dict(aliases=['dom'], default='*'), - month=dict(default='*'), - weekday=dict(aliases=['dow'], default='*'), - reboot=dict(required=False, default=False, type='bool'), - special_time=dict(required=False, - default=None, - choices=["reboot", "yearly", "annually", "monthly", "weekly", "daily", "hourly"], - type='str') - ), - supports_check_mode = False, - ) - - name = module.params['name'] - user = module.params['user'] - job = module.params['job'] - cron_file = module.params['cron_file'] - state = module.params['state'] - backup = module.params['backup'] - minute = module.params['minute'] - hour = module.params['hour'] - day = module.params['day'] - month = module.params['month'] - weekday = module.params['weekday'] - reboot = module.params['reboot'] - special_time = module.params['special_time'] - do_install = state == 'present' - - changed = False - res_args = dict() - - # Ensure all files generated are only writable by the owning user. Primarily relevant for the cron_file option. - os.umask(022) - crontab = CronTab(module, user, cron_file) - - if crontab.syslogging: - syslog.openlog('ansible-%s' % os.path.basename(__file__)) - syslog.syslog(syslog.LOG_NOTICE, 'cron instantiated - name: "%s"' % name) - - # --- user input validation --- - - if (special_time or reboot) and \ - (True in [(x != '*') for x in [minute, hour, day, month, weekday]]): - module.fail_json(msg="You must specify time and date fields or special time.") - - if cron_file and do_install: - if not user: - module.fail_json(msg="To use cron_file=... parameter you must specify user=... as well") - - if reboot and special_time: - module.fail_json(msg="reboot and special_time are mutually exclusive") - - if name is None and do_install: - module.fail_json(msg="You must specify 'name' to install a new cron job") - - if job is None and do_install: - module.fail_json(msg="You must specify 'job' to install a new cron job") - - if job and name is None and not do_install: - module.fail_json(msg="You must specify 'name' to remove a cron job") - - if reboot: - if special_time: - module.fail_json(msg="reboot and special_time are mutually exclusive") - else: - special_time = "reboot" - - # if requested make a backup before making a change - if backup: - (backuph, backup_file) = tempfile.mkstemp(prefix='crontab') - crontab.write(backup_file) - - if crontab.cron_file and not name and not do_install: - changed = crontab.remove_job_file() - module.exit_json(changed=changed,cron_file=cron_file,state=state) - - job = crontab.get_cron_job(minute, hour, day, month, weekday, job, special_time) - old_job = crontab.find_job(name) - - if do_install: - if len(old_job) == 0: - crontab.add_job(name, job) - changed = True - if len(old_job) > 0 and old_job[1] != job: - crontab.update_job(name, job) - changed = True - else: - if len(old_job) > 0: - crontab.remove_job(name) - changed = True - - res_args = dict( - jobs = crontab.get_jobnames(), changed = changed - ) - - if changed: - crontab.write() - - # retain the backup only if crontab or cron file have changed - if backup: - if changed: - res_args['backup_file'] = backup_file - else: - os.unlink(backup_file) - - if cron_file: - res_args['cron_file'] = cron_file - - module.exit_json(**res_args) - - # --- should never get here - module.exit_json(msg="Unable to execute cron task.") - -# import module snippets -from ansible.module_utils.basic import * - -main() - diff --git a/extras/system/group b/extras/system/group deleted file mode 100644 index 617de7c2857..00000000000 --- a/extras/system/group +++ /dev/null @@ -1,403 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2012, Stephen Fromm -# -# 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: group -author: Stephen Fromm -version_added: "0.0.2" -short_description: Add or remove groups -requirements: [ groupadd, groupdel, groupmod ] -description: - - Manage presence of groups on a host. -options: - name: - required: true - description: - - Name of the group to manage. - gid: - required: false - description: - - Optional I(GID) to set for the group. - state: - required: false - default: "present" - choices: [ present, absent ] - description: - - Whether the group should be present or not on the remote host. - system: - required: false - default: "no" - choices: [ "yes", "no" ] - description: - - If I(yes), indicates that the group created is a system group. - -''' - -EXAMPLES = ''' -# Example group command from Ansible Playbooks -- group: name=somegroup state=present -''' - -import grp -import syslog -import platform - -class Group(object): - """ - This is a generic Group manipulation class that is subclassed - based on platform. - - A subclass may wish to override the following action methods:- - - group_del() - - group_add() - - group_mod() - - All subclasses MUST define platform and distribution (which may be None). - """ - - platform = 'Generic' - distribution = None - GROUPFILE = '/etc/group' - - def __new__(cls, *args, **kwargs): - return load_platform_subclass(Group, args, kwargs) - - def __init__(self, module): - self.module = module - self.state = module.params['state'] - self.name = module.params['name'] - self.gid = module.params['gid'] - self.system = module.params['system'] - self.syslogging = False - - def execute_command(self, cmd): - if self.syslogging: - syslog.openlog('ansible-%s' % os.path.basename(__file__)) - syslog.syslog(syslog.LOG_NOTICE, 'Command %s' % '|'.join(cmd)) - - return self.module.run_command(cmd) - - def group_del(self): - cmd = [self.module.get_bin_path('groupdel', True), self.name] - return self.execute_command(cmd) - - def group_add(self, **kwargs): - cmd = [self.module.get_bin_path('groupadd', True)] - for key in kwargs: - if key == 'gid' and kwargs[key] is not None: - cmd.append('-g') - cmd.append(kwargs[key]) - elif key == 'system' and kwargs[key] == True: - cmd.append('-r') - cmd.append(self.name) - return self.execute_command(cmd) - - def group_mod(self, **kwargs): - cmd = [self.module.get_bin_path('groupmod', True)] - info = self.group_info() - for key in kwargs: - if key == 'gid': - if kwargs[key] is not None and info[2] != int(kwargs[key]): - cmd.append('-g') - cmd.append(kwargs[key]) - if len(cmd) == 1: - return (None, '', '') - if self.module.check_mode: - return (0, '', '') - cmd.append(self.name) - return self.execute_command(cmd) - - def group_exists(self): - try: - if grp.getgrnam(self.name): - return True - except KeyError: - return False - - def group_info(self): - if not self.group_exists(): - return False - try: - info = list(grp.getgrnam(self.name)) - except KeyError: - return False - return info - -# =========================================== - -class SunOS(Group): - """ - This is a SunOS Group manipulation class. Solaris doesn't have - the 'system' group concept. - - This overrides the following methods from the generic class:- - - group_add() - """ - - platform = 'SunOS' - distribution = None - GROUPFILE = '/etc/group' - - def group_add(self, **kwargs): - cmd = [self.module.get_bin_path('groupadd', True)] - for key in kwargs: - if key == 'gid' and kwargs[key] is not None: - cmd.append('-g') - cmd.append(kwargs[key]) - cmd.append(self.name) - return self.execute_command(cmd) - - -# =========================================== - -class AIX(Group): - """ - This is a AIX Group manipulation class. - - This overrides the following methods from the generic class:- - - group_del() - - group_add() - - group_mod() - """ - - platform = 'AIX' - distribution = None - GROUPFILE = '/etc/group' - - def group_del(self): - cmd = [self.module.get_bin_path('rmgroup', True), self.name] - return self.execute_command(cmd) - - def group_add(self, **kwargs): - cmd = [self.module.get_bin_path('mkgroup', True)] - for key in kwargs: - if key == 'gid' and kwargs[key] is not None: - cmd.append('id='+kwargs[key]) - elif key == 'system' and kwargs[key] == True: - cmd.append('-a') - cmd.append(self.name) - return self.execute_command(cmd) - - def group_mod(self, **kwargs): - cmd = [self.module.get_bin_path('chgroup', True)] - info = self.group_info() - for key in kwargs: - if key == 'gid': - if kwargs[key] is not None and info[2] != int(kwargs[key]): - cmd.append('id='+kwargs[key]) - if len(cmd) == 1: - return (None, '', '') - if self.module.check_mode: - return (0, '', '') - cmd.append(self.name) - return self.execute_command(cmd) - -# =========================================== - -class FreeBsdGroup(Group): - """ - This is a FreeBSD Group manipulation class. - - This overrides the following methods from the generic class:- - - group_del() - - group_add() - - group_mod() - """ - - platform = 'FreeBSD' - distribution = None - GROUPFILE = '/etc/group' - - def group_del(self): - cmd = [self.module.get_bin_path('pw', True), 'groupdel', self.name] - return self.execute_command(cmd) - - def group_add(self, **kwargs): - cmd = [self.module.get_bin_path('pw', True), 'groupadd', self.name] - if self.gid is not None: - cmd.append('-g %d' % int(self.gid)) - return self.execute_command(cmd) - - def group_mod(self, **kwargs): - cmd = [self.module.get_bin_path('pw', True), 'groupmod', self.name] - info = self.group_info() - cmd_len = len(cmd) - if self.gid is not None and int(self.gid) != info[2]: - cmd.append('-g %d' % int(self.gid)) - # modify the group if cmd will do anything - if cmd_len != len(cmd): - if self.module.check_mode: - return (0, '', '') - return self.execute_command(cmd) - return (None, '', '') - -# =========================================== - -class OpenBsdGroup(Group): - """ - This is a OpenBSD Group manipulation class. - - This overrides the following methods from the generic class:- - - group_del() - - group_add() - - group_mod() - """ - - platform = 'OpenBSD' - distribution = None - GROUPFILE = '/etc/group' - - def group_del(self): - cmd = [self.module.get_bin_path('groupdel', True), self.name] - return self.execute_command(cmd) - - def group_add(self, **kwargs): - cmd = [self.module.get_bin_path('groupadd', True)] - if self.gid is not None: - cmd.append('-g') - cmd.append('%d' % int(self.gid)) - cmd.append(self.name) - return self.execute_command(cmd) - - def group_mod(self, **kwargs): - cmd = [self.module.get_bin_path('groupmod', True)] - info = self.group_info() - cmd_len = len(cmd) - if self.gid is not None and int(self.gid) != info[2]: - cmd.append('-g') - cmd.append('%d' % int(self.gid)) - if len(cmd) == 1: - return (None, '', '') - if self.module.check_mode: - return (0, '', '') - cmd.append(self.name) - return self.execute_command(cmd) - -# =========================================== - -class NetBsdGroup(Group): - """ - This is a NetBSD Group manipulation class. - - This overrides the following methods from the generic class:- - - group_del() - - group_add() - - group_mod() - """ - - platform = 'NetBSD' - distribution = None - GROUPFILE = '/etc/group' - - def group_del(self): - cmd = [self.module.get_bin_path('groupdel', True), self.name] - return self.execute_command(cmd) - - def group_add(self, **kwargs): - cmd = [self.module.get_bin_path('groupadd', True)] - if self.gid is not None: - cmd.append('-g') - cmd.append('%d' % int(self.gid)) - cmd.append(self.name) - return self.execute_command(cmd) - - def group_mod(self, **kwargs): - cmd = [self.module.get_bin_path('groupmod', True)] - info = self.group_info() - cmd_len = len(cmd) - if self.gid is not None and int(self.gid) != info[2]: - cmd.append('-g') - cmd.append('%d' % int(self.gid)) - if len(cmd) == 1: - return (None, '', '') - if self.module.check_mode: - return (0, '', '') - cmd.append(self.name) - return self.execute_command(cmd) - -# =========================================== - -def main(): - module = AnsibleModule( - argument_spec = dict( - state=dict(default='present', choices=['present', 'absent'], type='str'), - name=dict(required=True, type='str'), - gid=dict(default=None, type='str'), - system=dict(default=False, type='bool'), - ), - supports_check_mode=True - ) - - group = Group(module) - - if group.syslogging: - syslog.openlog('ansible-%s' % os.path.basename(__file__)) - syslog.syslog(syslog.LOG_NOTICE, 'Group instantiated - platform %s' % group.platform) - if user.distribution: - syslog.syslog(syslog.LOG_NOTICE, 'Group instantiated - distribution %s' % group.distribution) - - rc = None - out = '' - err = '' - result = {} - result['name'] = group.name - result['state'] = group.state - - if group.state == 'absent': - - if group.group_exists(): - if module.check_mode: - module.exit_json(changed=True) - (rc, out, err) = group.group_del() - if rc != 0: - module.fail_json(name=group.name, msg=err) - - elif group.state == 'present': - - if not group.group_exists(): - if module.check_mode: - module.exit_json(changed=True) - (rc, out, err) = group.group_add(gid=group.gid, system=group.system) - else: - (rc, out, err) = group.group_mod(gid=group.gid) - - if rc is not None and rc != 0: - module.fail_json(name=group.name, msg=err) - - if rc is None: - result['changed'] = False - else: - result['changed'] = True - if out: - result['stdout'] = out - if err: - result['stderr'] = err - - if group.group_exists(): - info = group.group_info() - result['system'] = group.system - result['gid'] = info[2] - - module.exit_json(**result) - -# import module snippets -from ansible.module_utils.basic import * -main() diff --git a/extras/system/hostname b/extras/system/hostname deleted file mode 100755 index a426b59136b..00000000000 --- a/extras/system/hostname +++ /dev/null @@ -1,445 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2013, Hiroaki Nakamura -# -# 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: hostname -author: Hiroaki Nakamura -version_added: "1.4" -short_description: Manage hostname -requirements: [ hostname ] -description: - - Set system's hostname - - Currently implemented on Debian, Ubuntu, Fedora, RedHat, openSUSE, Linaro, ScientificLinux, Arch, CentOS, AMI. -options: - name: - required: true - description: - - Name of the host -''' - -EXAMPLES = ''' -- hostname: name=web01 -''' - -from distutils.version import LooseVersion - -# import module snippets -from ansible.module_utils.basic import * - - -# wrap get_distribution_version in case it returns a string -def _get_distribution_version(): - distribution_version = get_distribution_version() - - if type(distribution_version) is str: - distribution_version = 0 - elif type(distribution_version) is None: - distribution_version = 0 - - return distribution_version - - -class UnimplementedStrategy(object): - def __init__(self, module): - self.module = module - - def get_current_hostname(self): - self.unimplemented_error() - - def set_current_hostname(self, name): - self.unimplemented_error() - - def get_permanent_hostname(self): - self.unimplemented_error() - - def set_permanent_hostname(self, name): - self.unimplemented_error() - - def unimplemented_error(self): - platform = get_platform() - distribution = get_distribution() - if distribution is not None: - msg_platform = '%s (%s)' % (platform, distribution) - else: - msg_platform = platform - self.module.fail_json( - msg='hostname module cannot be used on platform %s' % msg_platform) - -class Hostname(object): - """ - This is a generic Hostname manipulation class that is subclassed - based on platform. - - A subclass may wish to set different strategy instance to self.strategy. - - All subclasses MUST define platform and distribution (which may be None). - """ - - platform = 'Generic' - distribution = None - strategy_class = UnimplementedStrategy - - def __new__(cls, *args, **kwargs): - return load_platform_subclass(Hostname, args, kwargs) - - def __init__(self, module): - self.module = module - self.name = module.params['name'] - self.strategy = self.strategy_class(module) - - def get_current_hostname(self): - return self.strategy.get_current_hostname() - - def set_current_hostname(self, name): - self.strategy.set_current_hostname(name) - - def get_permanent_hostname(self): - return self.strategy.get_permanent_hostname() - - def set_permanent_hostname(self, name): - self.strategy.set_permanent_hostname(name) - -class GenericStrategy(object): - """ - This is a generic Hostname manipulation strategy class. - - A subclass may wish to override some or all of these methods. - - get_current_hostname() - - get_permanent_hostname() - - set_current_hostname(name) - - set_permanent_hostname(name) - """ - def __init__(self, module): - self.module = module - - HOSTNAME_CMD = '/bin/hostname' - - def get_current_hostname(self): - cmd = [self.HOSTNAME_CMD] - rc, out, err = self.module.run_command(cmd) - if rc != 0: - self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" % - (rc, out, err)) - return out.strip() - - def set_current_hostname(self, name): - cmd = [self.HOSTNAME_CMD, name] - rc, out, err = self.module.run_command(cmd) - if rc != 0: - self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" % - (rc, out, err)) - - def get_permanent_hostname(self): - return None - - def set_permanent_hostname(self, name): - pass - - -# =========================================== - -class DebianStrategy(GenericStrategy): - """ - This is a Debian family Hostname manipulation strategy class - it edits - the /etc/hostname file. - """ - - HOSTNAME_FILE = '/etc/hostname' - - def get_permanent_hostname(self): - if not os.path.isfile(self.HOSTNAME_FILE): - try: - open(self.HOSTNAME_FILE, "a").write("") - except IOError, err: - self.module.fail_json(msg="failed to write file: %s" % - str(err)) - try: - f = open(self.HOSTNAME_FILE) - try: - return f.read().strip() - finally: - f.close() - except Exception, err: - self.module.fail_json(msg="failed to read hostname: %s" % - str(err)) - - def set_permanent_hostname(self, name): - try: - f = open(self.HOSTNAME_FILE, 'w+') - try: - f.write("%s\n" % name) - finally: - f.close() - except Exception, err: - self.module.fail_json(msg="failed to update hostname: %s" % - str(err)) - - -# =========================================== - -class RedHatStrategy(GenericStrategy): - """ - This is a Redhat Hostname strategy class - it edits the - /etc/sysconfig/network file. - """ - NETWORK_FILE = '/etc/sysconfig/network' - - def get_permanent_hostname(self): - try: - f = open(self.NETWORK_FILE, 'rb') - try: - for line in f.readlines(): - if line.startswith('HOSTNAME'): - k, v = line.split('=') - return v.strip() - finally: - f.close() - except Exception, err: - self.module.fail_json(msg="failed to read hostname: %s" % - str(err)) - - def set_permanent_hostname(self, name): - try: - lines = [] - found = False - f = open(self.NETWORK_FILE, 'rb') - try: - for line in f.readlines(): - if line.startswith('HOSTNAME'): - lines.append("HOSTNAME=%s\n" % name) - found = True - else: - lines.append(line) - finally: - f.close() - if not found: - lines.append("HOSTNAME=%s\n" % name) - f = open(self.NETWORK_FILE, 'w+') - try: - f.writelines(lines) - finally: - f.close() - except Exception, err: - self.module.fail_json(msg="failed to update hostname: %s" % - str(err)) - - -# =========================================== - -class FedoraStrategy(GenericStrategy): - """ - This is a Fedora family Hostname manipulation strategy class - it uses - the hostnamectl command. - """ - - def get_current_hostname(self): - cmd = ['hostname'] - rc, out, err = self.module.run_command(cmd) - if rc != 0: - self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" % - (rc, out, err)) - return out.strip() - - def set_current_hostname(self, name): - cmd = ['hostnamectl', '--transient', 'set-hostname', name] - rc, out, err = self.module.run_command(cmd) - if rc != 0: - self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" % - (rc, out, err)) - - def get_permanent_hostname(self): - cmd = 'hostnamectl --static status' - rc, out, err = self.module.run_command(cmd, use_unsafe_shell=True) - if rc != 0: - self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" % - (rc, out, err)) - return out.strip() - - def set_permanent_hostname(self, name): - cmd = ['hostnamectl', '--pretty', 'set-hostname', name] - rc, out, err = self.module.run_command(cmd) - if rc != 0: - self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" % - (rc, out, err)) - cmd = ['hostnamectl', '--static', 'set-hostname', name] - rc, out, err = self.module.run_command(cmd) - if rc != 0: - self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" % - (rc, out, err)) - - -# =========================================== - -class OpenRCStrategy(GenericStrategy): - """ - This is a Gentoo (OpenRC) Hostname manipulation strategy class - it edits - the /etc/conf.d/hostname file. - """ - - HOSTNAME_FILE = '/etc/conf.d/hostname' - - def get_permanent_hostname(self): - try: - with open(self.HOSTNAME_FILE, 'r') as f: - for line in f: - line = line.strip() - if line.startswith('hostname='): - return line[10:].strip('"') - return None - except Exception, err: - self.module.fail_json(msg="failed to read hostname: %s" % - str(err)) - - def set_permanent_hostname(self, name): - try: - with open(self.HOSTNAME_FILE, 'r') as f: - lines = [x.strip() for x in f] - - for i, line in enumerate(lines): - if line.startswith('hostname='): - lines[i] = 'hostname="%s"' % name - break - - with open(self.HOSTNAME_FILE, 'w') as f: - f.write('\n'.join(lines) + '\n') - except Exception, err: - self.module.fail_json(msg="failed to update hostname: %s" % - str(err)) - -# =========================================== - -class FedoraHostname(Hostname): - platform = 'Linux' - distribution = 'Fedora' - strategy_class = FedoraStrategy - -class OpenSUSEHostname(Hostname): - platform = 'Linux' - distribution = 'Opensuse ' - strategy_class = FedoraStrategy - -class ArchHostname(Hostname): - platform = 'Linux' - distribution = 'Arch' - strategy_class = FedoraStrategy - -class RedHat5Hostname(Hostname): - platform = 'Linux' - distribution = 'Redhat' - strategy_class = RedHatStrategy - -class RedHatServerHostname(Hostname): - platform = 'Linux' - distribution = 'Red hat enterprise linux server' - distribution_version = _get_distribution_version() - if distribution_version and LooseVersion(distribution_version) >= LooseVersion("7"): - strategy_class = FedoraStrategy - else: - strategy_class = RedHatStrategy - -class RedHatWorkstationHostname(Hostname): - platform = 'Linux' - distribution = 'Red hat enterprise linux workstation' - distribution_version = _get_distribution_version() - if distribution_version and LooseVersion(distribution_version) >= LooseVersion("7"): - strategy_class = FedoraStrategy - else: - strategy_class = RedHatStrategy - -class CentOSHostname(Hostname): - platform = 'Linux' - distribution = 'Centos' - distribution_version = _get_distribution_version() - if distribution_version and LooseVersion(distribution_version) >= LooseVersion("7"): - strategy_class = FedoraStrategy - else: - strategy_class = RedHatStrategy - -class CentOSLinuxHostname(Hostname): - platform = 'Linux' - distribution = 'Centos linux' - distribution_version = _get_distribution_version() - if distribution_version and LooseVersion(distribution_version) >= LooseVersion("7"): - strategy_class = FedoraStrategy - else: - strategy_class = RedHatStrategy - -class ScientificHostname(Hostname): - platform = 'Linux' - distribution = 'Scientific' - strategy_class = RedHatStrategy - -class ScientificLinuxHostname(Hostname): - platform = 'Linux' - distribution = 'Scientific linux' - strategy_class = RedHatStrategy - -class AmazonLinuxHostname(Hostname): - platform = 'Linux' - distribution = 'Amazon' - strategy_class = RedHatStrategy - -class DebianHostname(Hostname): - platform = 'Linux' - distribution = 'Debian' - strategy_class = DebianStrategy - -class UbuntuHostname(Hostname): - platform = 'Linux' - distribution = 'Ubuntu' - strategy_class = DebianStrategy - -class LinaroHostname(Hostname): - platform = 'Linux' - distribution = 'Linaro' - strategy_class = DebianStrategy - -class GentooHostname(Hostname): - platform = 'Linux' - distribution = 'Gentoo base system' - strategy_class = OpenRCStrategy - -# =========================================== - -def main(): - module = AnsibleModule( - argument_spec = dict( - name=dict(required=True, type='str') - ) - ) - - hostname = Hostname(module) - - changed = False - name = module.params['name'] - current_name = hostname.get_current_hostname() - if current_name != name: - hostname.set_current_hostname(name) - changed = True - - permanent_name = hostname.get_permanent_hostname() - if permanent_name != name: - hostname.set_permanent_hostname(name) - changed = True - - module.exit_json(changed=changed, name=name) - -main() diff --git a/extras/system/mount b/extras/system/mount deleted file mode 100755 index 9dc6fbe7b8c..00000000000 --- a/extras/system/mount +++ /dev/null @@ -1,338 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2012, Red Hat, inc -# Written by Seth Vidal -# based on the mount modules from salt and puppet -# -# 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: mount -short_description: Control active and configured mount points -description: - - This module controls active and configured mount points in C(/etc/fstab). -version_added: "0.6" -options: - name: - description: - - "path to the mount point, eg: C(/mnt/files)" - required: true - default: null - aliases: [] - src: - description: - - device to be mounted on I(name). - required: true - default: null - fstype: - description: - - file-system type - required: true - default: null - opts: - description: - - mount options (see fstab(8)) - required: false - default: null - dump: - description: - - dump (see fstab(8)) - required: false - default: null - passno: - description: - - passno (see fstab(8)) - required: false - default: null - state: - description: - - If C(mounted) or C(unmounted), the device will be actively mounted or unmounted - as needed and appropriately configured in I(fstab). - C(absent) and C(present) only deal with - I(fstab) but will not affect current mounting. If specifying C(mounted) and the mount - point is not present, the mount point will be created. Similarly, specifying C(absent) will remove the mount point directory. - required: true - choices: [ "present", "absent", "mounted", "unmounted" ] - default: null - fstab: - description: - - file to use instead of C(/etc/fstab). You shouldn't use that option - unless you really know what you are doing. This might be useful if - you need to configure mountpoints in a chroot environment. - required: false - default: /etc/fstab - -notes: [] -requirements: [] -author: Seth Vidal -''' -EXAMPLES = ''' -# Mount DVD read-only -- mount: name=/mnt/dvd src=/dev/sr0 fstype=iso9660 opts=ro state=present - -# Mount up device by label -- mount: name=/srv/disk src='LABEL=SOME_LABEL' fstype=ext4 state=present - -# Mount up device by UUID -- mount: name=/home src='UUID=b3e48f45-f933-4c8e-a700-22a159ec9077' fstype=xfs opts=noatime state=present -''' - - -def write_fstab(lines, dest): - - fs_w = open(dest, 'w') - for l in lines: - fs_w.write(l) - - fs_w.flush() - fs_w.close() - -def set_mount(**kwargs): - """ set/change a mount point location in fstab """ - - # kwargs: name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab - args = dict( - opts = 'defaults', - dump = '0', - passno = '0', - fstab = '/etc/fstab' - ) - args.update(kwargs) - - new_line = '%(src)s %(name)s %(fstype)s %(opts)s %(dump)s %(passno)s\n' - - to_write = [] - exists = False - changed = False - for line in open(args['fstab'], 'r').readlines(): - if not line.strip(): - to_write.append(line) - continue - if line.strip().startswith('#'): - to_write.append(line) - continue - if len(line.split()) != 6: - # not sure what this is or why it is here - # but it is not our fault so leave it be - to_write.append(line) - continue - - ld = {} - ld['src'], ld['name'], ld['fstype'], ld['opts'], ld['dump'], ld['passno'] = line.split() - - if ld['name'] != args['name']: - to_write.append(line) - continue - - # it exists - now see if what we have is different - exists = True - for t in ('src', 'fstype','opts', 'dump', 'passno'): - if ld[t] != args[t]: - changed = True - ld[t] = args[t] - - if changed: - to_write.append(new_line % ld) - else: - to_write.append(line) - - if not exists: - to_write.append(new_line % args) - changed = True - - if changed: - write_fstab(to_write, args['fstab']) - - return (args['name'], changed) - - -def unset_mount(**kwargs): - """ remove a mount point from fstab """ - - # kwargs: name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab - args = dict( - opts = 'default', - dump = '0', - passno = '0', - fstab = '/etc/fstab' - ) - args.update(kwargs) - - to_write = [] - changed = False - for line in open(args['fstab'], 'r').readlines(): - if not line.strip(): - to_write.append(line) - continue - if line.strip().startswith('#'): - to_write.append(line) - continue - if len(line.split()) != 6: - # not sure what this is or why it is here - # but it is not our fault so leave it be - to_write.append(line) - continue - - ld = {} - ld['src'], ld['name'], ld['fstype'], ld['opts'], ld['dump'], ld['passno'] = line.split() - - if ld['name'] != args['name']: - to_write.append(line) - continue - - # if we got here we found a match - continue and mark changed - changed = True - - if changed: - write_fstab(to_write, args['fstab']) - - return (args['name'], changed) - - -def mount(module, **kwargs): - """ mount up a path or remount if needed """ - mount_bin = module.get_bin_path('mount') - - name = kwargs['name'] - if os.path.ismount(name): - cmd = [ mount_bin , '-o', 'remount', name ] - else: - cmd = [ mount_bin, name ] - - rc, out, err = module.run_command(cmd) - if rc == 0: - return 0, '' - else: - return rc, out+err - -def umount(module, **kwargs): - """ unmount a path """ - - umount_bin = module.get_bin_path('umount') - name = kwargs['name'] - cmd = [umount_bin, name] - - rc, out, err = module.run_command(cmd) - if rc == 0: - return 0, '' - else: - return rc, out+err - -def main(): - - module = AnsibleModule( - argument_spec = dict( - state = dict(required=True, choices=['present', 'absent', 'mounted', 'unmounted']), - name = dict(required=True), - opts = dict(default=None), - passno = dict(default=None), - dump = dict(default=None), - src = dict(required=True), - fstype = dict(required=True), - fstab = dict(default='/etc/fstab') - ) - ) - - - changed = False - rc = 0 - args = { - 'name': module.params['name'], - 'src': module.params['src'], - 'fstype': module.params['fstype'] - } - if module.params['passno'] is not None: - args['passno'] = module.params['passno'] - if module.params['opts'] is not None: - args['opts'] = module.params['opts'] - if ' ' in args['opts']: - module.fail_json(msg="unexpected space in 'opts' parameter") - if module.params['dump'] is not None: - args['dump'] = module.params['dump'] - if module.params['fstab'] is not None: - args['fstab'] = module.params['fstab'] - - # if fstab file does not exist, we first need to create it. This mainly - # happens when fstab optin is passed to the module. - if not os.path.exists(args['fstab']): - if not os.path.exists(os.path.dirname(args['fstab'])): - os.makedirs(os.path.dirname(args['fstab'])) - open(args['fstab'],'a').close() - - # absent == remove from fstab and unmounted - # unmounted == do not change fstab state, but unmount - # present == add to fstab, do not change mount state - # mounted == add to fstab if not there and make sure it is mounted, if it has changed in fstab then remount it - - state = module.params['state'] - name = module.params['name'] - if state == 'absent': - name, changed = unset_mount(**args) - if changed: - if os.path.ismount(name): - res,msg = umount(module, **args) - if res: - module.fail_json(msg="Error unmounting %s: %s" % (name, msg)) - - if os.path.exists(name): - try: - os.rmdir(name) - except (OSError, IOError), e: - module.fail_json(msg="Error rmdir %s: %s" % (name, str(e))) - - module.exit_json(changed=changed, **args) - - if state == 'unmounted': - if os.path.ismount(name): - res,msg = umount(module, **args) - if res: - module.fail_json(msg="Error unmounting %s: %s" % (name, msg)) - changed = True - - module.exit_json(changed=changed, **args) - - if state in ['mounted', 'present']: - if state == 'mounted': - if not os.path.exists(name): - try: - os.makedirs(name) - except (OSError, IOError), e: - module.fail_json(msg="Error making dir %s: %s" % (name, str(e))) - - name, changed = set_mount(**args) - if state == 'mounted': - res = 0 - if os.path.ismount(name): - if changed: - res,msg = mount(module, **args) - else: - changed = True - res,msg = mount(module, **args) - - if res: - module.fail_json(msg="Error mounting %s: %s" % (name, msg)) - - - module.exit_json(changed=changed, **args) - - module.fail_json(msg='Unexpected position reached') - sys.exit(0) - -# import module snippets -from ansible.module_utils.basic import * -main() diff --git a/extras/system/ping b/extras/system/ping deleted file mode 100644 index b098d0054cd..00000000000 --- a/extras/system/ping +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (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 . - - -DOCUMENTATION = ''' ---- -module: ping -version_added: historical -short_description: Try to connect to host and return C(pong) on success. -description: - - A trivial test module, this module always returns C(pong) on successful - contact. It does not make sense in playbooks, but it is useful from - C(/usr/bin/ansible) -options: {} -author: Michael DeHaan -''' - -EXAMPLES = ''' -# Test 'webservers' status -ansible webservers -m ping -''' - -import exceptions - -def main(): - module = AnsibleModule( - argument_spec = dict( - data=dict(required=False, default=None), - ), - supports_check_mode = True - ) - result = dict(ping='pong') - if module.params['data']: - if module.params['data'] == 'crash': - raise exceptions.Exception("boom") - result['ping'] = module.params['data'] - module.exit_json(**result) - -from ansible.module_utils.basic import * - -main() - diff --git a/extras/system/seboolean b/extras/system/seboolean deleted file mode 100644 index 9799e71636a..00000000000 --- a/extras/system/seboolean +++ /dev/null @@ -1,212 +0,0 @@ -#!/usr/bin/python - -# (c) 2012, Stephen Fromm -# -# 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: seboolean -short_description: Toggles SELinux booleans. -description: - - Toggles SELinux booleans. -version_added: "0.7" -options: - name: - description: - - Name of the boolean to configure - required: true - default: null - persistent: - description: - - Set to C(yes) if the boolean setting should survive a reboot - required: false - default: no - choices: [ "yes", "no" ] - state: - description: - - Desired boolean value - required: true - default: null - choices: [ 'yes', 'no' ] -notes: - - Not tested on any debian based system -requirements: [ ] -author: Stephen Fromm -''' - -EXAMPLES = ''' -# Set (httpd_can_network_connect) flag on and keep it persistent across reboots -- seboolean: name=httpd_can_network_connect state=yes persistent=yes -''' - -try: - import selinux - HAVE_SELINUX=True -except ImportError: - HAVE_SELINUX=False - -try: - import semanage - HAVE_SEMANAGE=True -except ImportError: - HAVE_SEMANAGE=False - -def has_boolean_value(module, name): - bools = [] - try: - rc, bools = selinux.security_get_boolean_names() - except OSError, e: - module.fail_json(msg="Failed to get list of boolean names") - if name in bools: - return True - else: - return False - -def get_boolean_value(module, name): - state = 0 - try: - state = selinux.security_get_boolean_active(name) - except OSError, e: - module.fail_json(msg="Failed to determine current state for boolean %s" % name) - if state == 1: - return True - else: - return False - -# The following method implements what setsebool.c does to change -# a boolean and make it persist after reboot.. -def semanage_boolean_value(module, name, state): - rc = 0 - value = 0 - if state: - value = 1 - handle = semanage.semanage_handle_create() - if handle is None: - module.fail_json(msg="Failed to create semanage library handle") - try: - managed = semanage.semanage_is_managed(handle) - if managed < 0: - module.fail_json(msg="Failed to determine whether policy is manage") - if managed == 0: - if os.getuid() == 0: - module.fail_json(msg="Cannot set persistent booleans without managed policy") - else: - module.fail_json(msg="Cannot set persistent booleans; please try as root") - if semanage.semanage_connect(handle) < 0: - module.fail_json(msg="Failed to connect to semanage") - - if semanage.semanage_begin_transaction(handle) < 0: - module.fail_json(msg="Failed to begin semanage transaction") - - rc, sebool = semanage.semanage_bool_create(handle) - if rc < 0: - module.fail_json(msg="Failed to create seboolean with semanage") - if semanage.semanage_bool_set_name(handle, sebool, name) < 0: - module.fail_json(msg="Failed to set seboolean name with semanage") - semanage.semanage_bool_set_value(sebool, value) - - rc, boolkey = semanage.semanage_bool_key_extract(handle, sebool) - if rc < 0: - module.fail_json(msg="Failed to extract boolean key with semanage") - - if semanage.semanage_bool_modify_local(handle, boolkey, sebool) < 0: - module.fail_json(msg="Failed to modify boolean key with semanage") - - if semanage.semanage_bool_set_active(handle, boolkey, sebool) < 0: - module.fail_json(msg="Failed to set boolean key active with semanage") - - semanage.semanage_bool_key_free(boolkey) - semanage.semanage_bool_free(sebool) - - semanage.semanage_set_reload(handle, 0) - if semanage.semanage_commit(handle) < 0: - module.fail_json(msg="Failed to commit changes to semanage") - - semanage.semanage_disconnect(handle) - semanage.semanage_handle_destroy(handle) - except Exception, e: - module.fail_json(msg="Failed to manage policy for boolean %s: %s" % (name, str(e))) - return True - -def set_boolean_value(module, name, state): - rc = 0 - value = 0 - if state: - value = 1 - try: - rc = selinux.security_set_boolean(name, value) - except OSError, e: - module.fail_json(msg="Failed to set boolean %s to %s" % (name, value)) - if rc == 0: - return True - else: - return False - -def main(): - module = AnsibleModule( - argument_spec = dict( - name=dict(required=True), - persistent=dict(default='no', type='bool'), - state=dict(required=True, type='bool') - ), - supports_check_mode=True - ) - - if not HAVE_SELINUX: - module.fail_json(msg="This module requires libselinux-python support") - - if not HAVE_SEMANAGE: - module.fail_json(msg="This module requires libsemanage-python support") - - if not selinux.is_selinux_enabled(): - module.fail_json(msg="SELinux is disabled on this host.") - - name = module.params['name'] - persistent = module.params['persistent'] - state = module.params['state'] - result = {} - result['name'] = name - - if not has_boolean_value(module, name): - module.fail_json(msg="SELinux boolean %s does not exist." % name) - - cur_value = get_boolean_value(module, name) - - if cur_value == state: - result['state'] = cur_value - result['changed'] = False - module.exit_json(**result) - - if module.check_mode: - module.exit_json(changed=True) - if persistent: - r = semanage_boolean_value(module, name, state) - else: - r = set_boolean_value(module, name, state) - - result['changed'] = r - if not r: - module.fail_json(msg="Failed to set boolean %s to %s" % (name, value)) - try: - selinux.security_commit_booleans() - except: - module.fail_json(msg="Failed to commit pending boolean %s value" % name) - module.exit_json(**result) - -# import module snippets -from ansible.module_utils.basic import * -main() diff --git a/extras/system/selinux b/extras/system/selinux deleted file mode 100644 index 53e53d1d49c..00000000000 --- a/extras/system/selinux +++ /dev/null @@ -1,203 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2012, Derek Carter -# -# 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: selinux -short_description: Change policy and state of SELinux -description: - - Configures the SELinux mode and policy. A reboot may be required after usage. Ansible will not issue this reboot but will let you know when it is required. -version_added: "0.7" -options: - policy: - description: - - "name of the SELinux policy to use (example: C(targeted)) will be required if state is not C(disabled)" - required: false - default: null - state: - description: - - The SELinux mode - required: true - default: null - choices: [ "enforcing", "permissive", "disabled" ] - conf: - description: - - path to the SELinux configuration file, if non-standard - required: false - default: "/etc/selinux/config" -notes: - - Not tested on any debian based system -requirements: [ libselinux-python ] -author: Derek Carter -''' - -EXAMPLES = ''' -- selinux: policy=targeted state=enforcing -- selinux: policy=targeted state=permissive -- selinux: state=disabled -''' - -import os -import re -import sys - -try: - import selinux -except ImportError: - print "failed=True msg='libselinux-python required for this module'" - sys.exit(1) - -# getter subroutines -def get_config_state(configfile): - myfile = open(configfile, "r") - lines = myfile.readlines() - myfile.close() - for line in lines: - stateline = re.match('^SELINUX=.*$', line) - if (stateline): - return(line.split('=')[1].strip()) - -def get_config_policy(configfile): - myfile = open(configfile, "r") - lines = myfile.readlines() - myfile.close() - for line in lines: - stateline = re.match('^SELINUXTYPE=.*$', line) - if (stateline): - return(line.split('=')[1].strip()) - -# setter subroutines -def set_config_state(state, configfile): - #SELINUX=permissive - # edit config file with state value - stateline='SELINUX=%s' % state - myfile = open(configfile, "r") - lines = myfile.readlines() - myfile.close() - myfile = open(configfile, "w") - for line in lines: - myfile.write(re.sub(r'^SELINUX=.*', stateline, line)) - myfile.close() - -def set_state(state): - if (state == 'enforcing'): - selinux.security_setenforce(1) - elif (state == 'permissive'): - selinux.security_setenforce(0) - elif (state == 'disabled'): - pass - else: - msg = 'trying to set invalid runtime state %s' % state - module.fail_json(msg=msg) - -def set_config_policy(policy, configfile): - # edit config file with state value - #SELINUXTYPE=targeted - policyline='SELINUXTYPE=%s' % policy - myfile = open(configfile, "r") - lines = myfile.readlines() - myfile.close() - myfile = open(configfile, "w") - for line in lines: - myfile.write(re.sub(r'^SELINUXTYPE=.*', policyline, line)) - myfile.close() - -def main(): - - module = AnsibleModule( - argument_spec = dict( - policy=dict(required=False), - state=dict(choices=['enforcing', 'permissive', 'disabled'], required=True), - configfile=dict(aliases=['conf','file'], default='/etc/selinux/config') - ), - supports_check_mode=True - ) - - # global vars - changed=False - msgs = [] - configfile = module.params['configfile'] - policy = module.params['policy'] - state = module.params['state'] - runtime_enabled = selinux.is_selinux_enabled() - runtime_policy = selinux.selinux_getpolicytype()[1] - runtime_state = 'disabled' - if (runtime_enabled): - # enabled means 'enforcing' or 'permissive' - if (selinux.security_getenforce()): - runtime_state = 'enforcing' - else: - runtime_state = 'permissive' - config_policy = get_config_policy(configfile) - config_state = get_config_state(configfile) - - # check to see if policy is set if state is not 'disabled' - if (state != 'disabled'): - if not policy: - module.fail_json(msg='policy is required if state is not \'disabled\'') - else: - if not policy: - policy = config_policy - - # check changed values and run changes - if (policy != runtime_policy): - if module.check_mode: - module.exit_json(changed=True) - # cannot change runtime policy - msgs.append('reboot to change the loaded policy') - changed=True - - if (policy != config_policy): - if module.check_mode: - module.exit_json(changed=True) - msgs.append('config policy changed from \'%s\' to \'%s\'' % (config_policy, policy)) - set_config_policy(policy, configfile) - changed=True - - if (state != runtime_state): - if module.check_mode: - module.exit_json(changed=True) - if (state == 'disabled'): - msgs.append('state change will take effect next reboot') - else: - if (runtime_enabled): - set_state(state) - msgs.append('runtime state changed from \'%s\' to \'%s\'' % (runtime_state, state)) - else: - msgs.append('state change will take effect next reboot') - changed=True - - if (state != config_state): - if module.check_mode: - module.exit_json(changed=True) - msgs.append('config state changed from \'%s\' to \'%s\'' % (config_state, state)) - set_config_state(state, configfile) - changed=True - - module.exit_json(changed=changed, msg=', '.join(msgs), - configfile=configfile, - policy=policy, state=state) - -################################################# -# import module snippets -from ansible.module_utils.basic import * - -main() - diff --git a/extras/system/service b/extras/system/service deleted file mode 100644 index b235ee25c57..00000000000 --- a/extras/system/service +++ /dev/null @@ -1,1328 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (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 . - -DOCUMENTATION = ''' ---- -module: service -author: Michael DeHaan -version_added: "0.1" -short_description: Manage services. -description: - - Controls services on remote hosts. -options: - name: - required: true - description: - - Name of the service. - state: - required: false - choices: [ started, stopped, restarted, reloaded ] - description: - - C(started)/C(stopped) are idempotent actions that will not run - commands unless necessary. C(restarted) will always bounce the - service. C(reloaded) will always reload. B(At least one of state - and enabled are required.) - sleep: - required: false - version_added: "1.3" - description: - - If the service is being C(restarted) then sleep this many seconds - between the stop and start command. This helps to workaround badly - behaving init scripts that exit immediately after signaling a process - to stop. - pattern: - required: false - version_added: "0.7" - description: - - If the service does not respond to the status command, name a - substring to look for as would be found in the output of the I(ps) - command as a stand-in for a status result. If the string is found, - the service will be assumed to be running. - enabled: - required: false - choices: [ "yes", "no" ] - description: - - Whether the service should start on boot. B(At least one of state and - enabled are required.) - - runlevel: - required: false - default: 'default' - description: - - "For OpenRC init scripts (ex: Gentoo) only. The runlevel that this service belongs to." - arguments: - description: - - Additional arguments provided on the command line - aliases: [ 'args' ] -''' - -EXAMPLES = ''' -# Example action to start service httpd, if not running -- service: name=httpd state=started - -# Example action to stop service httpd, if running -- service: name=httpd state=stopped - -# Example action to restart service httpd, in all cases -- service: name=httpd state=restarted - -# Example action to reload service httpd, in all cases -- service: name=httpd state=reloaded - -# Example action to enable service httpd, and not touch the running state -- service: name=httpd enabled=yes - -# Example action to start service foo, based on running process /usr/bin/foo -- service: name=foo pattern=/usr/bin/foo state=started - -# Example action to restart network service for interface eth0 -- service: name=network state=restarted args=eth0 -''' - -import platform -import os -import re -import tempfile -import shlex -import select -import time -import string - -from distutils.version import LooseVersion - -class Service(object): - """ - This is the generic Service manipulation class that is subclassed - based on platform. - - A subclass should override the following action methods:- - - get_service_tools - - service_enable - - get_service_status - - service_control - - All subclasses MUST define platform and distribution (which may be None). - """ - - platform = 'Generic' - distribution = None - - def __new__(cls, *args, **kwargs): - return load_platform_subclass(Service, args, kwargs) - - def __init__(self, module): - self.module = module - self.name = module.params['name'] - self.state = module.params['state'] - self.sleep = module.params['sleep'] - self.pattern = module.params['pattern'] - self.enable = module.params['enabled'] - self.runlevel = module.params['runlevel'] - self.changed = False - self.running = None - self.crashed = None - self.action = None - self.svc_cmd = None - self.svc_initscript = None - self.svc_initctl = None - self.enable_cmd = None - self.arguments = module.params.get('arguments', '') - self.rcconf_file = None - self.rcconf_key = None - self.rcconf_value = None - self.svc_change = False - - # select whether we dump additional debug info through syslog - self.syslogging = False - - # =========================================== - # Platform specific methods (must be replaced by subclass). - - def get_service_tools(self): - self.module.fail_json(msg="get_service_tools not implemented on target platform") - - def service_enable(self): - self.module.fail_json(msg="service_enable not implemented on target platform") - - def get_service_status(self): - self.module.fail_json(msg="get_service_status not implemented on target platform") - - def service_control(self): - self.module.fail_json(msg="service_control not implemented on target platform") - - # =========================================== - # Generic methods that should be used on all platforms. - - def execute_command(self, cmd, daemonize=False): - if self.syslogging: - syslog.openlog('ansible-%s' % os.path.basename(__file__)) - syslog.syslog(syslog.LOG_NOTICE, 'Command %s, daemonize %r' % (cmd, daemonize)) - - # Most things don't need to be daemonized - if not daemonize: - return self.module.run_command(cmd) - - # This is complex because daemonization is hard for people. - # What we do is daemonize a part of this module, the daemon runs the - # command, picks up the return code and output, and returns it to the - # main process. - pipe = os.pipe() - pid = os.fork() - if pid == 0: - os.close(pipe[0]) - # Set stdin/stdout/stderr to /dev/null - fd = os.open(os.devnull, os.O_RDWR) - if fd != 0: - os.dup2(fd, 0) - if fd != 1: - os.dup2(fd, 1) - if fd != 2: - os.dup2(fd, 2) - if fd not in (0, 1, 2): - os.close(fd) - - # Make us a daemon. Yes, that's all it takes. - pid = os.fork() - if pid > 0: - os._exit(0) - os.setsid() - os.chdir("/") - pid = os.fork() - if pid > 0: - os._exit(0) - - # Start the command - if isinstance(cmd, basestring): - cmd = shlex.split(cmd) - p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=lambda: os.close(pipe[1])) - stdout = "" - stderr = "" - fds = [p.stdout, p.stderr] - # Wait for all output, or until the main process is dead and its output is done. - while fds: - rfd, wfd, efd = select.select(fds, [], fds, 1) - if not (rfd + wfd + efd) and p.poll() is not None: - break - if p.stdout in rfd: - dat = os.read(p.stdout.fileno(), 4096) - if not dat: - fds.remove(p.stdout) - stdout += dat - if p.stderr in rfd: - dat = os.read(p.stderr.fileno(), 4096) - if not dat: - fds.remove(p.stderr) - stderr += dat - p.wait() - # Return a JSON blob to parent - os.write(pipe[1], json.dumps([p.returncode, stdout, stderr])) - os.close(pipe[1]) - os._exit(0) - elif pid == -1: - self.module.fail_json(msg="unable to fork") - else: - os.close(pipe[1]) - os.waitpid(pid, 0) - # Wait for data from daemon process and process it. - data = "" - while True: - rfd, wfd, efd = select.select([pipe[0]], [], [pipe[0]]) - if pipe[0] in rfd: - dat = os.read(pipe[0], 4096) - if not dat: - break - data += dat - return json.loads(data) - - def check_ps(self): - # Set ps flags - if platform.system() == 'SunOS': - psflags = '-ef' - else: - psflags = 'auxww' - - # Find ps binary - psbin = self.module.get_bin_path('ps', True) - - (rc, psout, pserr) = self.execute_command('%s %s' % (psbin, psflags)) - # If rc is 0, set running as appropriate - if rc == 0: - self.running = False - lines = psout.split("\n") - for line in lines: - if self.pattern in line and not "pattern=" in line: - # so as to not confuse ./hacking/test-module - self.running = True - break - - def check_service_changed(self): - if self.state and self.running is None: - self.module.fail_json(msg="failed determining service state, possible typo of service name?") - # Find out if state has changed - if not self.running and self.state in ["started", "running", "reloaded"]: - self.svc_change = True - elif self.running and self.state in ["stopped","reloaded"]: - self.svc_change = True - elif self.state == "restarted": - self.svc_change = True - if self.module.check_mode and self.svc_change: - self.module.exit_json(changed=True, msg='service state changed') - - def modify_service_state(self): - - # Only do something if state will change - if self.svc_change: - # Control service - if self.state in ['started', 'running']: - self.action = "start" - elif not self.running and self.state == 'reloaded': - self.action = "start" - elif self.state == 'stopped': - self.action = "stop" - elif self.state == 'reloaded': - self.action = "reload" - elif self.state == 'restarted': - self.action = "restart" - - if self.module.check_mode: - self.module.exit_json(changed=True, msg='changing service state') - - return self.service_control() - - else: - # If nothing needs to change just say all is well - rc = 0 - err = '' - out = '' - return rc, out, err - - def service_enable_rcconf(self): - if self.rcconf_file is None or self.rcconf_key is None or self.rcconf_value is None: - self.module.fail_json(msg="service_enable_rcconf() requires rcconf_file, rcconf_key and rcconf_value") - - self.changed = None - entry = '%s="%s"\n' % (self.rcconf_key, self.rcconf_value) - RCFILE = open(self.rcconf_file, "r") - new_rc_conf = [] - - # Build a list containing the possibly modified file. - for rcline in RCFILE: - # Parse line removing whitespaces, quotes, etc. - rcarray = shlex.split(rcline, comments=True) - if len(rcarray) >= 1 and '=' in rcarray[0]: - (key, value) = rcarray[0].split("=", 1) - if key == self.rcconf_key: - if value.upper() == self.rcconf_value: - # Since the proper entry already exists we can stop iterating. - self.changed = False - break - else: - # We found the key but the value is wrong, replace with new entry. - rcline = entry - self.changed = True - - # Add line to the list. - new_rc_conf.append(rcline) - - # We are done with reading the current rc.conf, close it. - RCFILE.close() - - # If we did not see any trace of our entry we need to add it. - if self.changed is None: - new_rc_conf.append(entry) - self.changed = True - - if self.changed is True: - - if self.module.check_mode: - self.module.exit_json(changed=True, msg="changing service enablement") - - # Create a temporary file next to the current rc.conf (so we stay on the same filesystem). - # This way the replacement operation is atomic. - rcconf_dir = os.path.dirname(self.rcconf_file) - rcconf_base = os.path.basename(self.rcconf_file) - (TMP_RCCONF, tmp_rcconf_file) = tempfile.mkstemp(dir=rcconf_dir, prefix="%s-" % rcconf_base) - - # Write out the contents of the list into our temporary file. - for rcline in new_rc_conf: - os.write(TMP_RCCONF, rcline) - - # Close temporary file. - os.close(TMP_RCCONF) - - # Replace previous rc.conf. - self.module.atomic_move(tmp_rcconf_file, self.rcconf_file) - -# =========================================== -# Subclass: Linux - -class LinuxService(Service): - """ - This is the Linux Service manipulation class - it is currently supporting - a mixture of binaries and init scripts for controlling services started at - boot, as well as for controlling the current state. - """ - - platform = 'Linux' - distribution = None - - def get_service_tools(self): - - paths = [ '/sbin', '/usr/sbin', '/bin', '/usr/bin' ] - binaries = [ 'service', 'chkconfig', 'update-rc.d', 'rc-service', 'rc-update', 'initctl', 'systemctl', 'start', 'stop', 'restart' ] - initpaths = [ '/etc/init.d' ] - location = dict() - - for binary in binaries: - location[binary] = self.module.get_bin_path(binary) - - def check_systemd(name): - # verify service is managed by systemd - if not location.get('systemctl', None): - return False - - # default to .service if the unit type is not specified - if name.find('.') > 0: - unit_name, unit_type = name.rsplit('.', 1) - if unit_type not in ("service", "socket", "device", "mount", "automount", - "swap", "target", "path", "timer", "snapshot"): - name = "%s.service" % name - else: - name = "%s.service" % name - - rc, out, err = self.execute_command("%s list-unit-files" % (location['systemctl'])) - - # adjust the service name to account for template service unit files - index = name.find('@') - if index != -1: - template_name = name[:index+1] - else: - template_name = name - - self.__systemd_unit = None - for line in out.splitlines(): - if line.startswith(template_name): - self.__systemd_unit = name - return True - return False - - # Locate a tool for enable options - if location.get('chkconfig', None) and os.path.exists("/etc/init.d/%s" % self.name): - if check_systemd(self.name): - # service is managed by systemd - self.enable_cmd = location['systemctl'] - else: - # we are using a standard SysV service - self.enable_cmd = location['chkconfig'] - elif location.get('update-rc.d', None): - if check_systemd(self.name): - # service is managed by systemd - self.enable_cmd = location['systemctl'] - elif location['initctl'] and os.path.exists("/etc/init/%s.conf" % self.name): - # service is managed by upstart - self.enable_cmd = location['initctl'] - elif location['update-rc.d'] and os.path.exists("/etc/init.d/%s" % self.name): - # service is managed by with SysV init scripts, but with update-rc.d - self.enable_cmd = location['update-rc.d'] - else: - self.module.fail_json(msg="service not found: %s" % self.name) - elif location.get('rc-service', None) and not location.get('systemctl', None): - # service is managed by OpenRC - self.svc_cmd = location['rc-service'] - self.enable_cmd = location['rc-update'] - return - elif check_systemd(self.name): - # service is managed by systemd - self.enable_cmd = location['systemctl'] - elif location['initctl'] and os.path.exists("/etc/init/%s.conf" % self.name): - # service is managed by upstart - self.enable_cmd = location['initctl'] - - # if this service is managed via upstart, get the current upstart version - if self.enable_cmd == location['initctl']: - # default the upstart version to something we can compare against - self.upstart_version = LooseVersion('0.0.0') - try: - # set the upstart version based on the output of 'initctl version' - version_re = re.compile(r'\(upstart (.*)\)') - rc,stdout,stderr = self.module.run_command('initctl version') - if rc == 0: - res = version_re.search(stdout) - if res: - self.upstart_version = LooseVersion(res.groups()[0]) - except: - # we'll use the default of 0.0.0 since we couldn't - # detect the current upstart version above - pass - - # Locate a tool for runtime service management (start, stop etc.) - if location.get('service', None) and os.path.exists("/etc/init.d/%s" % self.name): - # SysV init script - self.svc_cmd = location['service'] - elif location.get('start', None) and os.path.exists("/etc/init/%s.conf" % self.name): - # upstart -- rather than being managed by one command, start/stop/restart are actual commands - self.svc_cmd = '' - else: - # still a SysV init script, but /sbin/service isn't installed - for initdir in initpaths: - initscript = "%s/%s" % (initdir,self.name) - if os.path.isfile(initscript): - self.svc_initscript = initscript - - # couldn't find anything yet, assume systemd - if self.svc_cmd is None and self.svc_initscript is None: - if location.get('systemctl'): - self.svc_cmd = location['systemctl'] - - if self.svc_cmd is None and not self.svc_initscript: - self.module.fail_json(msg='cannot find \'service\' binary or init script for service, possible typo in service name?, aborting') - - if location.get('initctl', None): - self.svc_initctl = location['initctl'] - - def get_systemd_status_dict(self): - (rc, out, err) = self.execute_command("%s show %s" % (self.enable_cmd, self.__systemd_unit,)) - if rc != 0: - self.module.fail_json(msg='failure %d running systemctl show for %r: %s' % (rc, self.__systemd_unit, err)) - return dict(line.split('=', 1) for line in out.splitlines()) - - def get_systemd_service_status(self): - d = self.get_systemd_status_dict() - if d.get('ActiveState') == 'active': - # run-once services (for which a single successful exit indicates - # that they are running as designed) should not be restarted here. - # Thus, we are not checking d['SubState']. - self.running = True - self.crashed = False - elif d.get('ActiveState') == 'failed': - self.running = False - self.crashed = True - elif d.get('ActiveState') is None: - self.module.fail_json(msg='No ActiveState value in systemctl show output for %r' % (self.__systemd_unit,)) - else: - self.running = False - self.crashed = False - return self.running - - def get_service_status(self): - if self.svc_cmd and self.svc_cmd.endswith('systemctl'): - return self.get_systemd_service_status() - - self.action = "status" - rc, status_stdout, status_stderr = self.service_control() - - # if we have decided the service is managed by upstart, we check for some additional output... - if self.svc_initctl and self.running is None: - # check the job status by upstart response - initctl_rc, initctl_status_stdout, initctl_status_stderr = self.execute_command("%s status %s" % (self.svc_initctl, self.name)) - if "stop/waiting" in initctl_status_stdout: - self.running = False - elif "start/running" in initctl_status_stdout: - self.running = True - - if self.svc_cmd and self.svc_cmd.endswith("rc-service") and self.running is None: - openrc_rc, openrc_status_stdout, openrc_status_stderr = self.execute_command("%s %s status" % (self.svc_cmd, self.name)) - self.running = "started" in openrc_status_stdout - self.crashed = "crashed" in openrc_status_stderr - - # if the job status is still not known check it by status output keywords - # Only check keywords if there's only one line of output (some init - # scripts will output verbosely in case of error and those can emit - # keywords that are picked up as false positives - if self.running is None and status_stdout.count('\n') <= 1: - # first transform the status output that could irritate keyword matching - cleanout = status_stdout.lower().replace(self.name.lower(), '') - if "stop" in cleanout: - self.running = False - elif "run" in cleanout and "not" in cleanout: - self.running = False - elif "run" in cleanout and "not" not in cleanout: - self.running = True - elif "start" in cleanout and "not" not in cleanout: - self.running = True - elif 'could not access pid file' in cleanout: - self.running = False - elif 'is dead and pid file exists' in cleanout: - self.running = False - elif 'dead but subsys locked' in cleanout: - self.running = False - elif 'dead but pid file exists' in cleanout: - self.running = False - - # if the job status is still not known check it by response code - # For reference, see: - # http://refspecs.linuxbase.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html - if self.running is None: - if rc in [1, 2, 3, 4, 69]: - self.running = False - elif rc == 0: - self.running = True - - # if the job status is still not known check it by special conditions - if self.running is None: - if self.name == 'iptables' and "ACCEPT" in status_stdout: - # iptables status command output is lame - # TODO: lookup if we can use a return code for this instead? - self.running = True - - return self.running - - - def service_enable(self): - - if self.enable_cmd is None: - self.module.fail_json(msg='cannot detect command to enable service %s, typo or init system potentially unknown' % self.name) - - self.changed = True - action = None - - # FIXME: we use chkconfig or systemctl - # to decide whether to run the command here but need something - # similar for upstart - - # - # Upstart's initctl - # - if self.enable_cmd.endswith("initctl"): - def write_to_override_file(file_name, file_contents, ): - override_file = open(file_name, 'w') - override_file.write(file_contents) - override_file.close() - - initpath = '/etc/init' - if self.upstart_version >= LooseVersion('0.6.7'): - manreg = re.compile('^manual\s*$', re.M | re.I) - config_line = 'manual\n' - else: - manreg = re.compile('^start on manual\s*$', re.M | re.I) - config_line = 'start on manual\n' - conf_file_name = "%s/%s.conf" % (initpath, self.name) - override_file_name = "%s/%s.override" % (initpath, self.name) - - # Check to see if files contain the manual line in .conf and fail if True - if manreg.search(open(conf_file_name).read()): - self.module.fail_json(msg="manual stanza not supported in a .conf file") - - self.changed = False - if os.path.exists(override_file_name): - override_file_contents = open(override_file_name).read() - # Remove manual stanza if present and service enabled - if self.enable and manreg.search(override_file_contents): - self.changed = True - override_state = manreg.sub('', override_file_contents) - # Add manual stanza if not present and service disabled - elif not (self.enable) and not (manreg.search(override_file_contents)): - self.changed = True - override_state = '\n'.join((override_file_contents, config_line)) - # service already in desired state - else: - pass - # Add file with manual stanza if service disabled - elif not (self.enable): - self.changed = True - override_state = config_line - else: - # service already in desired state - pass - - if self.module.check_mode: - self.module.exit_json(changed=self.changed) - - # The initctl method of enabling and disabling services is much - # different than for the other service methods. So actually - # committing the change is done in this conditional and then we - # skip the boilerplate at the bottom of the method - if self.changed: - try: - write_to_override_file(override_file_name, override_state) - except: - self.module.fail_json(msg='Could not modify override file') - - return - - # - # SysV's chkconfig - # - if self.enable_cmd.endswith("chkconfig"): - if self.enable: - action = 'on' - else: - action = 'off' - - (rc, out, err) = self.execute_command("%s --list %s" % (self.enable_cmd, self.name)) - if 'chkconfig --add %s' % self.name in err: - self.execute_command("%s --add %s" % (self.enable_cmd, self.name)) - (rc, out, err) = self.execute_command("%s --list %s" % (self.enable_cmd, self.name)) - if not self.name in out: - self.module.fail_json(msg="service %s does not support chkconfig" % self.name) - state = out.split()[-1] - - # Check if we're already in the correct state - if "3:%s" % action in out and "5:%s" % action in out: - self.changed = False - return - - # - # Systemd's systemctl - # - if self.enable_cmd.endswith("systemctl"): - if self.enable: - action = 'enable' - else: - action = 'disable' - - # Check if we're already in the correct state - d = self.get_systemd_status_dict() - if "UnitFileState" in d: - if self.enable and d["UnitFileState"] == "enabled": - self.changed = False - elif not self.enable and d["UnitFileState"] == "disabled": - self.changed = False - elif not self.enable: - self.changed = False - - if not self.changed: - return - - # - # OpenRC's rc-update - # - if self.enable_cmd.endswith("rc-update"): - if self.enable: - action = 'add' - else: - action = 'delete' - - (rc, out, err) = self.execute_command("%s show" % self.enable_cmd) - for line in out.splitlines(): - service_name, runlevels = line.split('|') - service_name = service_name.strip() - if service_name != self.name: - continue - runlevels = re.split(r'\s+', runlevels) - # service already enabled for the runlevel - if self.enable and self.runlevel in runlevels: - self.changed = False - # service already disabled for the runlevel - elif not self.enable and self.runlevel not in runlevels: - self.changed = False - break - else: - # service already disabled altogether - if not self.enable: - self.changed = False - - if not self.changed: - return - - # - # update-rc.d style - # - if self.enable_cmd.endswith("update-rc.d"): - if self.enable: - action = 'enable' - else: - action = 'disable' - - if self.enable: - # make sure the init.d symlinks are created - # otherwise enable might not work - (rc, out, err) = self.execute_command("%s %s defaults" \ - % (self.enable_cmd, self.name)) - if rc != 0: - if err: - self.module.fail_json(msg=err) - else: - self.module.fail_json(msg=out) - - (rc, out, err) = self.execute_command("%s -n %s %s" \ - % (self.enable_cmd, self.name, action)) - self.changed = False - for line in out.splitlines(): - if line.startswith('rename'): - self.changed = True - break - elif self.enable and 'do not exist' in line: - self.changed = True - break - elif not self.enable and 'already exist' in line: - self.changed = True - break - - # Debian compatibility - for line in err.splitlines(): - if self.enable and 'no runlevel symlinks to modify' in line: - self.changed = True - break - - if not self.changed: - return - - # - # If we've gotten to the end, the service needs to be updated - # - self.changed = True - - # we change argument order depending on real binary used: - # rc-update and systemctl need the argument order reversed - - if self.enable_cmd.endswith("rc-update"): - args = (self.enable_cmd, action, self.name + " " + self.runlevel) - elif self.enable_cmd.endswith("systemctl"): - args = (self.enable_cmd, action, self.__systemd_unit) - else: - args = (self.enable_cmd, self.name, action) - - if self.module.check_mode: - self.module.exit_json(changed=self.changed) - - (rc, out, err) = self.execute_command("%s %s %s" % args) - if rc != 0: - if err: - self.module.fail_json(msg=err) - else: - self.module.fail_json(msg=out) - - return (rc, out, err) - - - def service_control(self): - - # Decide what command to run - svc_cmd = '' - arguments = self.arguments - if self.svc_cmd: - if not self.svc_cmd.endswith("systemctl"): - # SysV and OpenRC take the form - svc_cmd = "%s %s" % (self.svc_cmd, self.name) - else: - # systemd commands take the form - svc_cmd = self.svc_cmd - arguments = "%s %s" % (self.__systemd_unit, arguments) - elif self.svc_initscript: - # upstart - svc_cmd = "%s" % self.svc_initscript - - # In OpenRC, if a service crashed, we need to reset its status to - # stopped with the zap command, before we can start it back. - if self.svc_cmd and self.svc_cmd.endswith('rc-service') and self.action == 'start' and self.crashed: - self.execute_command("%s zap" % svc_cmd, daemonize=True) - - if self.action is not "restart": - if svc_cmd != '': - # upstart or systemd or OpenRC - rc_state, stdout, stderr = self.execute_command("%s %s %s" % (svc_cmd, self.action, arguments), daemonize=True) - else: - # SysV - rc_state, stdout, stderr = self.execute_command("%s %s %s" % (self.action, self.name, arguments), daemonize=True) - elif self.svc_cmd and self.svc_cmd.endswith('rc-service'): - # All services in OpenRC support restart. - rc_state, stdout, stderr = self.execute_command("%s %s %s" % (svc_cmd, self.action, arguments), daemonize=True) - else: - # In other systems, not all services support restart. Do it the hard way. - if svc_cmd != '': - # upstart or systemd - rc1, stdout1, stderr1 = self.execute_command("%s %s %s" % (svc_cmd, 'stop', arguments), daemonize=True) - else: - # SysV - rc1, stdout1, stderr1 = self.execute_command("%s %s %s" % ('stop', self.name, arguments), daemonize=True) - - if self.sleep: - time.sleep(self.sleep) - - if svc_cmd != '': - # upstart or systemd - rc2, stdout2, stderr2 = self.execute_command("%s %s %s" % (svc_cmd, 'start', arguments), daemonize=True) - else: - # SysV - rc2, stdout2, stderr2 = self.execute_command("%s %s %s" % ('start', self.name, arguments), daemonize=True) - - # merge return information - if rc1 != 0 and rc2 == 0: - rc_state = rc2 - stdout = stdout2 - stderr = stderr2 - else: - rc_state = rc1 + rc2 - stdout = stdout1 + stdout2 - stderr = stderr1 + stderr2 - - return(rc_state, stdout, stderr) - -# =========================================== -# Subclass: FreeBSD - -class FreeBsdService(Service): - """ - This is the FreeBSD Service manipulation class - it uses the /etc/rc.conf - file for controlling services started at boot and the 'service' binary to - check status and perform direct service manipulation. - """ - - platform = 'FreeBSD' - distribution = None - - def get_service_tools(self): - self.svc_cmd = self.module.get_bin_path('service', True) - - if not self.svc_cmd: - self.module.fail_json(msg='unable to find service binary') - - def get_service_status(self): - rc, stdout, stderr = self.execute_command("%s %s %s %s" % (self.svc_cmd, self.name, 'onestatus', self.arguments)) - if rc == 1: - self.running = False - elif rc == 0: - self.running = True - - def service_enable(self): - if self.enable: - self.rcconf_value = "YES" - else: - self.rcconf_value = "NO" - - rcfiles = [ '/etc/rc.conf','/etc/rc.conf.local', '/usr/local/etc/rc.conf' ] - for rcfile in rcfiles: - if os.path.isfile(rcfile): - self.rcconf_file = rcfile - - rc, stdout, stderr = self.execute_command("%s %s %s %s" % (self.svc_cmd, self.name, 'rcvar', self.arguments)) - cmd = "%s %s %s %s" % (self.svc_cmd, self.name, 'rcvar', self.arguments) - rcvars = shlex.split(stdout, comments=True) - - if not rcvars: - self.module.fail_json(msg="unable to determine rcvar", stdout=stdout, stderr=stderr) - - # In rare cases, i.e. sendmail, rcvar can return several key=value pairs - # Usually there is just one, however. In other rare cases, i.e. uwsgi, - # rcvar can return extra uncommented data that is not at all related to - # the rcvar. We will just take the first key=value pair we come across - # and hope for the best. - for rcvar in rcvars: - if '=' in rcvar: - self.rcconf_key = rcvar.split('=')[0] - break - - if self.rcconf_key is None: - self.module.fail_json(msg="unable to determine rcvar", stdout=stdout, stderr=stderr) - - try: - return self.service_enable_rcconf() - except: - self.module.fail_json(msg='unable to set rcvar') - - def service_control(self): - - if self.action is "start": - self.action = "onestart" - if self.action is "stop": - self.action = "onestop" - if self.action is "reload": - self.action = "onereload" - - return self.execute_command("%s %s %s %s" % (self.svc_cmd, self.name, self.action, self.arguments)) - -# =========================================== -# Subclass: OpenBSD - -class OpenBsdService(Service): - """ - This is the OpenBSD Service manipulation class - it uses /etc/rc.d for - service control. Enabling a service is currently not supported because the - _flags variable is not boolean, you should supply a rc.conf.local - file in some other way. - """ - - platform = 'OpenBSD' - distribution = None - - def get_service_tools(self): - rcdir = '/etc/rc.d' - - rc_script = "%s/%s" % (rcdir, self.name) - if os.path.isfile(rc_script): - self.svc_cmd = rc_script - - if not self.svc_cmd: - self.module.fail_json(msg='unable to find rc.d script') - - def get_service_status(self): - rc, stdout, stderr = self.execute_command("%s %s" % (self.svc_cmd, 'check')) - if rc == 1: - self.running = False - elif rc == 0: - self.running = True - - def service_control(self): - return self.execute_command("%s %s" % (self.svc_cmd, self.action)) - -# =========================================== -# Subclass: NetBSD - -class NetBsdService(Service): - """ - This is the NetBSD Service manipulation class - it uses the /etc/rc.conf - file for controlling services started at boot, check status and perform - direct service manipulation. Init scripts in /etc/rcd are used for - controlling services (start/stop) as well as for controlling the current - state. - """ - - platform = 'NetBSD' - distribution = None - - def get_service_tools(self): - initpaths = [ '/etc/rc.d' ] # better: $rc_directories - how to get in here? Run: sh -c '. /etc/rc.conf ; echo $rc_directories' - - for initdir in initpaths: - initscript = "%s/%s" % (initdir,self.name) - if os.path.isfile(initscript): - self.svc_initscript = initscript - - if not self.svc_initscript: - self.module.fail_json(msg='unable to find rc.d script') - - def service_enable(self): - if self.enable: - self.rcconf_value = "YES" - else: - self.rcconf_value = "NO" - - rcfiles = [ '/etc/rc.conf' ] # Overkill? - for rcfile in rcfiles: - if os.path.isfile(rcfile): - self.rcconf_file = rcfile - - self.rcconf_key = "%s" % string.replace(self.name,"-","_") - - return self.service_enable_rcconf() - - def get_service_status(self): - self.svc_cmd = "%s" % self.svc_initscript - rc, stdout, stderr = self.execute_command("%s %s" % (self.svc_cmd, 'onestatus')) - if rc == 1: - self.running = False - elif rc == 0: - self.running = True - - def service_control(self): - if self.action is "start": - self.action = "onestart" - if self.action is "stop": - self.action = "onestop" - - self.svc_cmd = "%s" % self.svc_initscript - return self.execute_command("%s %s" % (self.svc_cmd, self.action), daemonize=True) - -# =========================================== -# Subclass: SunOS -class SunOSService(Service): - """ - This is the SunOS Service manipulation class - it uses the svcadm - command for controlling services, and svcs command for checking status. - It also tries to be smart about taking the service out of maintenance - state if necessary. - """ - platform = 'SunOS' - distribution = None - - def get_service_tools(self): - self.svcs_cmd = self.module.get_bin_path('svcs', True) - - if not self.svcs_cmd: - self.module.fail_json(msg='unable to find svcs binary') - - self.svcadm_cmd = self.module.get_bin_path('svcadm', True) - - if not self.svcadm_cmd: - self.module.fail_json(msg='unable to find svcadm binary') - - def get_service_status(self): - status = self.get_sunos_svcs_status() - # Only 'online' is considered properly running. Everything else is off - # or has some sort of problem. - if status == 'online': - self.running = True - else: - self.running = False - - def get_sunos_svcs_status(self): - rc, stdout, stderr = self.execute_command("%s %s" % (self.svcs_cmd, self.name)) - if rc == 1: - if stderr: - self.module.fail_json(msg=stderr) - else: - self.module.fail_json(msg=stdout) - - lines = stdout.rstrip("\n").split("\n") - status = lines[-1].split(" ")[0] - # status is one of: online, offline, degraded, disabled, maintenance, uninitialized - # see man svcs(1) - return status - - def service_enable(self): - # Get current service enablement status - rc, stdout, stderr = self.execute_command("%s -l %s" % (self.svcs_cmd, self.name)) - - if rc != 0: - if stderr: - self.module.fail_json(msg=stderr) - else: - self.module.fail_json(msg=stdout) - - enabled = False - temporary = False - - # look for enabled line, which could be one of: - # enabled true (temporary) - # enabled false (temporary) - # enabled true - # enabled false - for line in stdout.split("\n"): - if line.startswith("enabled"): - if "true" in line: - enabled = True - if "temporary" in line: - temporary = True - - startup_enabled = (enabled and not temporary) or (not enabled and temporary) - - if self.enable and startup_enabled: - return - elif (not self.enable) and (not startup_enabled): - return - - # Mark service as started or stopped (this will have the side effect of - # actually stopping or starting the service) - if self.enable: - subcmd = "enable -rs" - else: - subcmd = "disable -s" - - rc, stdout, stderr = self.execute_command("%s %s %s" % (self.svcadm_cmd, subcmd, self.name)) - - if rc != 0: - if stderr: - self.module.fail_json(msg=stderr) - else: - self.module.fail_json(msg=stdout) - - self.changed = True - - - def service_control(self): - status = self.get_sunos_svcs_status() - - # if starting or reloading, clear maintenace states - if self.action in ['start', 'reload', 'restart'] and status in ['maintenance', 'degraded']: - rc, stdout, stderr = self.execute_command("%s clear %s" % (self.svcadm_cmd, self.name)) - if rc != 0: - return rc, stdout, stderr - status = self.get_sunos_svcs_status() - - if status in ['maintenance', 'degraded']: - self.module.fail_json(msg="Failed to bring service out of %s status." % status) - - if self.action == 'start': - subcmd = "enable -rst" - elif self.action == 'stop': - subcmd = "disable -st" - elif self.action == 'reload': - subcmd = "refresh" - elif self.action == 'restart' and status == 'online': - subcmd = "restart" - elif self.action == 'restart' and status != 'online': - subcmd = "enable -rst" - - return self.execute_command("%s %s %s" % (self.svcadm_cmd, subcmd, self.name)) - -# =========================================== -# Subclass: AIX - -class AIX(Service): - """ - This is the AIX Service (SRC) manipulation class - it uses lssrc, startsrc, stopsrc - and refresh for service control. Enabling a service is currently not supported. - Would require to add an entry in the /etc/inittab file (mkitab, chitab and rmitab - commands) - """ - - platform = 'AIX' - distribution = None - - def get_service_tools(self): - self.lssrc_cmd = self.module.get_bin_path('lssrc', True) - - if not self.lssrc_cmd: - self.module.fail_json(msg='unable to find lssrc binary') - - self.startsrc_cmd = self.module.get_bin_path('startsrc', True) - - if not self.startsrc_cmd: - self.module.fail_json(msg='unable to find startsrc binary') - - self.stopsrc_cmd = self.module.get_bin_path('stopsrc', True) - - if not self.stopsrc_cmd: - self.module.fail_json(msg='unable to find stopsrc binary') - - self.refresh_cmd = self.module.get_bin_path('refresh', True) - - if not self.refresh_cmd: - self.module.fail_json(msg='unable to find refresh binary') - - - def get_service_status(self): - status = self.get_aix_src_status() - # Only 'active' is considered properly running. Everything else is off - # or has some sort of problem. - if status == 'active': - self.running = True - else: - self.running = False - - def get_aix_src_status(self): - rc, stdout, stderr = self.execute_command("%s -s %s" % (self.lssrc_cmd, self.name)) - if rc == 1: - if stderr: - self.module.fail_json(msg=stderr) - else: - self.module.fail_json(msg=stdout) - - lines = stdout.rstrip("\n").split("\n") - status = lines[-1].split(" ")[-1] - # status is one of: active, inoperative - return status - - def service_control(self): - if self.action == 'start': - srccmd = self.startsrc_cmd - elif self.action == 'stop': - srccmd = self.stopsrc_cmd - elif self.action == 'reload': - srccmd = self.refresh_cmd - elif self.action == 'restart': - self.execute_command("%s -s %s" % (self.stopsrc_cmd, self.name)) - srccmd = self.startsrc_cmd - - if self.arguments and self.action == 'start': - return self.execute_command("%s -a \"%s\" -s %s" % (srccmd, self.arguments, self.name)) - else: - return self.execute_command("%s -s %s" % (srccmd, self.name)) - - -# =========================================== -# Main control flow - -def main(): - module = AnsibleModule( - argument_spec = dict( - name = dict(required=True), - state = dict(choices=['running', 'started', 'stopped', 'restarted', 'reloaded']), - sleep = dict(required=False, type='int', default=None), - pattern = dict(required=False, default=None), - enabled = dict(type='bool'), - runlevel = dict(required=False, default='default'), - arguments = dict(aliases=['args'], default=''), - ), - supports_check_mode=True - ) - if module.params['state'] is None and module.params['enabled'] is None: - module.fail_json(msg="Neither 'state' nor 'enabled' set") - - service = Service(module) - - if service.syslogging: - syslog.openlog('ansible-%s' % os.path.basename(__file__)) - syslog.syslog(syslog.LOG_NOTICE, 'Service instantiated - platform %s' % service.platform) - if service.distribution: - syslog.syslog(syslog.LOG_NOTICE, 'Service instantiated - distribution %s' % service.distribution) - - rc = 0 - out = '' - err = '' - result = {} - result['name'] = service.name - - # Find service management tools - service.get_service_tools() - - # Enable/disable service startup at boot if requested - if service.module.params['enabled'] is not None: - # FIXME: ideally this should detect if we need to toggle the enablement state, though - # it's unlikely the changed handler would need to fire in this case so it's a minor thing. - service.service_enable() - result['enabled'] = service.enable - - if module.params['state'] is None: - # Not changing the running state, so bail out now. - result['changed'] = service.changed - module.exit_json(**result) - - result['state'] = service.state - - # Collect service status - if service.pattern: - service.check_ps() - else: - service.get_service_status() - - # Calculate if request will change service state - service.check_service_changed() - - # Modify service state if necessary - (rc, out, err) = service.modify_service_state() - - if rc != 0: - if err and "Job is already running" in err: - # upstart got confused, one such possibility is MySQL on Ubuntu 12.04 - # where status may report it has no start/stop links and we could - # not get accurate status - pass - else: - if err: - module.fail_json(msg=err) - else: - module.fail_json(msg=out) - - result['changed'] = service.changed | service.svc_change - if service.module.params['enabled'] is not None: - result['enabled'] = service.module.params['enabled'] - - if not service.module.params['state']: - status = service.get_service_status() - if status is None: - result['state'] = 'absent' - elif status is False: - result['state'] = 'started' - else: - result['state'] = 'stopped' - else: - # as we may have just bounced the service the service command may not - # report accurate state at this moment so just show what we ran - if service.module.params['state'] in ['started','restarted','running','reloaded']: - result['state'] = 'started' - else: - result['state'] = 'stopped' - - module.exit_json(**result) - -from ansible.module_utils.basic import * -main() diff --git a/extras/system/setup b/extras/system/setup deleted file mode 100644 index 486304230bf..00000000000 --- a/extras/system/setup +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (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 . - -DOCUMENTATION = ''' ---- -module: setup -version_added: historical -short_description: Gathers facts about remote hosts -options: - filter: - version_added: "1.1" - description: - - if supplied, only return facts that match this shell-style (fnmatch) wildcard. - required: false - default: '*' - fact_path: - version_added: "1.3" - description: - - path used for local ansible facts (*.fact) - files in this dir - will be run (if executable) and their results be added to ansible_local facts - if a file is not executable it is read. - File/results format can be json or ini-format - required: false - default: '/etc/ansible/facts.d' -description: - - This module is automatically called by playbooks to gather useful - variables about remote hosts that can be used in playbooks. It can also be - executed directly by C(/usr/bin/ansible) to check what variables are - available to a host. Ansible provides many I(facts) about the system, - automatically. -notes: - - More ansible facts will be added with successive releases. If I(facter) or - I(ohai) are installed, variables from these programs will also be snapshotted - into the JSON file for usage in templating. These variables are prefixed - with C(facter_) and C(ohai_) so it's easy to tell their source. All variables are - bubbled up to the caller. Using the ansible facts and choosing to not - install I(facter) and I(ohai) means you can avoid Ruby-dependencies on your - remote systems. (See also M(facter) and M(ohai).) - - The filter option filters only the first level subkey below ansible_facts. - - If the target host is Windows, you will not currently have the ability to use - C(fact_path) or C(filter) as this is provided by a simpler implementation of the module. - Different facts are returned for Windows hosts. -author: Michael DeHaan -''' - -EXAMPLES = """ -# Display facts from all hosts and store them indexed by I(hostname) at C(/tmp/facts). -ansible all -m setup --tree /tmp/facts - -# Display only facts regarding memory found by ansible on all hosts and output them. -ansible all -m setup -a 'filter=ansible_*_mb' - -# Display only facts returned by facter. -ansible all -m setup -a 'filter=facter_*' - -# Display only facts about certain interfaces. -ansible all -m setup -a 'filter=ansible_eth[0-2]' -""" - - -def run_setup(module): - - setup_options = dict(module_setup=True) - facts = ansible_facts(module) - - for (k, v) in facts.items(): - setup_options["ansible_%s" % k.replace('-', '_')] = v - - # Look for the path to the facter and ohai binary and set - # the variable to that path. - facter_path = module.get_bin_path('facter') - ohai_path = module.get_bin_path('ohai') - - # if facter is installed, and we can use --json because - # ruby-json is ALSO installed, include facter data in the JSON - if facter_path is not None: - rc, out, err = module.run_command(facter_path + " --puppet --json") - facter = True - try: - facter_ds = json.loads(out) - except: - facter = False - if facter: - for (k,v) in facter_ds.items(): - setup_options["facter_%s" % k] = v - - # ditto for ohai - if ohai_path is not None: - rc, out, err = module.run_command(ohai_path) - ohai = True - try: - ohai_ds = json.loads(out) - except: - ohai = False - if ohai: - for (k,v) in ohai_ds.items(): - k2 = "ohai_%s" % k.replace('-', '_') - setup_options[k2] = v - - setup_result = { 'ansible_facts': {} } - - for (k,v) in setup_options.items(): - if module.params['filter'] == '*' or fnmatch.fnmatch(k, module.params['filter']): - setup_result['ansible_facts'][k] = v - - # hack to keep --verbose from showing all the setup module results - setup_result['verbose_override'] = True - - return setup_result - -def main(): - global module - module = AnsibleModule( - argument_spec = dict( - filter=dict(default="*", required=False), - fact_path=dict(default='/etc/ansible/facts.d', required=False), - ), - supports_check_mode = True, - ) - data = run_setup(module) - module.exit_json(**data) - -# import module snippets - -from ansible.module_utils.basic import * - -from ansible.module_utils.facts import * - -main() diff --git a/extras/system/sysctl b/extras/system/sysctl deleted file mode 100644 index acf6395f071..00000000000 --- a/extras/system/sysctl +++ /dev/null @@ -1,334 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2012, David "DaviXX" CHANIAL -# (c) 2014, James Tanner -# -# 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: sysctl -short_description: Manage entries in sysctl.conf. -description: - - This module manipulates sysctl entries and optionally performs a C(/sbin/sysctl -p) after changing them. -version_added: "1.0" -options: - name: - description: - - The dot-separated path (aka I(key)) specifying the sysctl variable. - required: true - default: null - aliases: [ 'key' ] - value: - description: - - Desired value of the sysctl key. - required: false - default: null - aliases: [ 'val' ] - state: - description: - - Whether the entry should be present or absent in the sysctl file. - choices: [ "present", "absent" ] - default: present - ignoreerrors: - description: - - Use this option to ignore errors about unknown keys. - choices: [ "yes", "no" ] - default: no - reload: - description: - - If C(yes), performs a I(/sbin/sysctl -p) if the C(sysctl_file) is - updated. If C(no), does not reload I(sysctl) even if the - C(sysctl_file) is updated. - choices: [ "yes", "no" ] - default: "yes" - sysctl_file: - description: - - Specifies the absolute path to C(sysctl.conf), if not C(/etc/sysctl.conf). - required: false - default: /etc/sysctl.conf - sysctl_set: - description: - - Verify token value with the sysctl command and set with -w if necessary - choices: [ "yes", "no" ] - required: false - version_added: 1.5 - default: False -notes: [] -requirements: [] -author: David "DaviXX" CHANIAL -''' - -EXAMPLES = ''' -# Set vm.swappiness to 5 in /etc/sysctl.conf -- sysctl: name=vm.swappiness value=5 state=present - -# Remove kernel.panic entry from /etc/sysctl.conf -- sysctl: name=kernel.panic state=absent sysctl_file=/etc/sysctl.conf - -# Set kernel.panic to 3 in /tmp/test_sysctl.conf -- sysctl: name=kernel.panic value=3 sysctl_file=/tmp/test_sysctl.conf reload=no - -# Set ip fowarding on in /proc and do not reload the sysctl file -- sysctl: name="net.ipv4.ip_forward" value=1 sysctl_set=yes - -# Set ip forwarding on in /proc and in the sysctl file and reload if necessary -- sysctl: name="net.ipv4.ip_forward" value=1 sysctl_set=yes state=present reload=yes -''' - -# ============================================================== - -import os -import tempfile -import re - -class SysctlModule(object): - - def __init__(self, module): - self.module = module - self.args = self.module.params - - self.sysctl_cmd = self.module.get_bin_path('sysctl', required=True) - self.sysctl_file = self.args['sysctl_file'] - - self.proc_value = None # current token value in proc fs - self.file_value = None # current token value in file - self.file_lines = [] # all lines in the file - self.file_values = {} # dict of token values - - self.changed = False # will change occur - self.set_proc = False # does sysctl need to set value - self.write_file = False # does the sysctl file need to be reloaded - - self.process() - - # ============================================================== - # LOGIC - # ============================================================== - - def process(self): - - # Whitespace is bad - self.args['name'] = self.args['name'].strip() - self.args['value'] = self._parse_value(self.args['value']) - - thisname = self.args['name'] - - # get the current proc fs value - self.proc_value = self.get_token_curr_value(thisname) - - # get the currect sysctl file value - self.read_sysctl_file() - if thisname not in self.file_values: - self.file_values[thisname] = None - - # update file contents with desired token/value - self.fix_lines() - - # what do we need to do now? - if self.file_values[thisname] is None and self.args['state'] == "present": - self.changed = True - self.write_file = True - elif self.file_values[thisname] is None and self.args['state'] == "absent": - self.changed = False - elif self.file_values[thisname] != self.args['value']: - self.changed = True - self.write_file = True - - # use the sysctl command or not? - if self.args['sysctl_set']: - if self.proc_value is None: - self.changed = True - elif not self._values_is_equal(self.proc_value, self.args['value']): - self.changed = True - self.set_proc = True - - # Do the work - if not self.module.check_mode: - if self.write_file: - self.write_sysctl() - if self.write_file and self.args['reload']: - self.reload_sysctl() - if self.set_proc: - self.set_token_value(self.args['name'], self.args['value']) - - def _values_is_equal(self, a, b): - """Expects two string values. It will split the string by whitespace - and compare each value. It will return True if both lists are the same, - contain the same elements and the same order.""" - if a is None or b is None: - return False - - a = a.split() - b = b.split() - - if len(a) != len(b): - return False - - return len([i for i, j in zip(a, b) if i == j]) == len(a) - - def _parse_value(self, value): - if value is None: - return '' - elif value.lower() in BOOLEANS_TRUE: - return '1' - elif value.lower() in BOOLEANS_FALSE: - return '0' - else: - return value.strip() - - # ============================================================== - # SYSCTL COMMAND MANAGEMENT - # ============================================================== - - # Use the sysctl command to find the current value - def get_token_curr_value(self, token): - thiscmd = "%s -e -n %s" % (self.sysctl_cmd, token) - rc,out,err = self.module.run_command(thiscmd) - if rc != 0: - return None - else: - return out - - # Use the sysctl command to set the current value - def set_token_value(self, token, value): - if len(value.split()) > 0: - value = '"' + value + '"' - thiscmd = "%s -w %s=%s" % (self.sysctl_cmd, token, value) - rc,out,err = self.module.run_command(thiscmd) - if rc != 0: - self.module.fail_json(msg='setting %s failed: %s' % (token, out + err)) - else: - return rc - - # Run sysctl -p - def reload_sysctl(self): - # do it - if get_platform().lower() == 'freebsd': - # freebsd doesn't support -p, so reload the sysctl service - rc,out,err = self.module.run_command('/etc/rc.d/sysctl reload') - else: - # system supports reloading via the -p flag to sysctl, so we'll use that - sysctl_args = [self.sysctl_cmd, '-p', self.sysctl_file] - if self.args['ignoreerrors']: - sysctl_args.insert(1, '-e') - - rc,out,err = self.module.run_command(sysctl_args) - - if rc != 0: - self.module.fail_json(msg="Failed to reload sysctl: %s" % str(out) + str(err)) - - # ============================================================== - # SYSCTL FILE MANAGEMENT - # ============================================================== - - # Get the token value from the sysctl file - def read_sysctl_file(self): - - lines = [] - if os.path.isfile(self.sysctl_file): - try: - f = open(self.sysctl_file, "r") - lines = f.readlines() - f.close() - except IOError, e: - self.module.fail_json(msg="Failed to open %s: %s" % (self.sysctl_file, str(e))) - - for line in lines: - line = line.strip() - self.file_lines.append(line) - - # don't split empty lines or comments - if not line or line.startswith("#"): - continue - - k, v = line.split('=',1) - k = k.strip() - v = v.strip() - self.file_values[k] = v.strip() - - # Fix the value in the sysctl file content - def fix_lines(self): - checked = [] - self.fixed_lines = [] - for line in self.file_lines: - if not line.strip() or line.strip().startswith("#"): - self.fixed_lines.append(line) - continue - tmpline = line.strip() - k, v = line.split('=',1) - k = k.strip() - v = v.strip() - if k not in checked: - checked.append(k) - if k == self.args['name']: - if self.args['state'] == "present": - new_line = "%s = %s\n" % (k, self.args['value']) - self.fixed_lines.append(new_line) - else: - new_line = "%s = %s\n" % (k, v) - self.fixed_lines.append(new_line) - - if self.args['name'] not in checked and self.args['state'] == "present": - new_line = "%s=%s\n" % (self.args['name'], self.args['value']) - self.fixed_lines.append(new_line) - - # Completely rewrite the sysctl file - def write_sysctl(self): - # open a tmp file - fd, tmp_path = tempfile.mkstemp('.conf', '.ansible_m_sysctl_', os.path.dirname(self.sysctl_file)) - f = open(tmp_path,"w") - try: - for l in self.fixed_lines: - f.write(l.strip() + "\n") - except IOError, e: - self.module.fail_json(msg="Failed to write to file %s: %s" % (tmp_path, str(e))) - f.flush() - f.close() - - # replace the real one - self.module.atomic_move(tmp_path, self.sysctl_file) - - -# ============================================================== -# main - -def main(): - - # defining module - module = AnsibleModule( - argument_spec = dict( - name = dict(aliases=['key'], required=True), - value = dict(aliases=['val'], required=False), - state = dict(default='present', choices=['present', 'absent']), - reload = dict(default=True, type='bool'), - sysctl_set = dict(default=False, type='bool'), - ignoreerrors = dict(default=False, type='bool'), - sysctl_file = dict(default='/etc/sysctl.conf') - ), - supports_check_mode=True - ) - - result = SysctlModule(module) - - module.exit_json(changed=result.changed) - sys.exit(0) - -# import module snippets -from ansible.module_utils.basic import * -main() diff --git a/extras/system/user b/extras/system/user deleted file mode 100644 index 551384a7a67..00000000000 --- a/extras/system/user +++ /dev/null @@ -1,1584 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2012, Stephen Fromm -# -# 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: user -author: Stephen Fromm -version_added: "0.2" -short_description: Manage user accounts -requirements: [ useradd, userdel, usermod ] -description: - - Manage user accounts and user attributes. -options: - name: - required: true - aliases: [ "user" ] - description: - - Name of the user to create, remove or modify. - comment: - required: false - description: - - Optionally sets the description (aka I(GECOS)) of user account. - uid: - required: false - description: - - Optionally sets the I(UID) of the user. - non_unique: - required: false - default: "no" - choices: [ "yes", "no" ] - description: - - Optionally when used with the -u option, this option allows to - change the user ID to a non-unique value. - version_added: "1.1" - group: - required: false - description: - - Optionally sets the user's primary group (takes a group name). - groups: - required: false - description: - - Puts the user in this comma-delimited list of groups. When set to - the empty string ('groups='), the user is removed from all groups - except the primary group. - append: - required: false - default: "no" - choices: [ "yes", "no" ] - description: - - If C(yes), will only add groups, not set them to just the list - in I(groups). - shell: - required: false - description: - - Optionally set the user's shell. - home: - required: false - description: - - Optionally set the user's home directory. - password: - required: false - description: - - Optionally set the user's password to this crypted value. See - the user example in the github examples directory for what this looks - like in a playbook. The `FAQ `_ - contains details on various ways to generate these password values. - state: - required: false - default: "present" - choices: [ present, absent ] - description: - - Whether the account should exist. When C(absent), removes - the user account. - createhome: - required: false - default: "yes" - choices: [ "yes", "no" ] - description: - - Unless set to C(no), a home directory will be made for the user - when the account is created or if the home directory does not - exist. - move_home: - required: false - default: "no" - choices: [ "yes", "no" ] - description: - - If set to C(yes) when used with C(home=), attempt to move the - user's home directory to the specified directory if it isn't there - already. - system: - required: false - default: "no" - choices: [ "yes", "no" ] - description: - - When creating an account, setting this to C(yes) makes the user a - system account. This setting cannot be changed on existing users. - force: - required: false - default: "no" - choices: [ "yes", "no" ] - description: - - When used with C(state=absent), behavior is as with - C(userdel --force). - login_class: - required: false - description: - - Optionally sets the user's login class for FreeBSD, OpenBSD and NetBSD systems. - remove: - required: false - default: "no" - choices: [ "yes", "no" ] - description: - - When used with C(state=absent), behavior is as with - C(userdel --remove). - generate_ssh_key: - required: false - default: "no" - choices: [ "yes", "no" ] - version_added: "0.9" - description: - - Whether to generate a SSH key for the user in question. - This will B(not) overwrite an existing SSH key. - ssh_key_bits: - required: false - default: 2048 - version_added: "0.9" - description: - - Optionally specify number of bits in SSH key to create. - ssh_key_type: - required: false - default: rsa - version_added: "0.9" - description: - - Optionally specify the type of SSH key to generate. - Available SSH key types will depend on implementation - present on target host. - ssh_key_file: - required: false - default: $HOME/.ssh/id_rsa - version_added: "0.9" - description: - - Optionally specify the SSH key filename. - ssh_key_comment: - required: false - default: ansible-generated - version_added: "0.9" - description: - - Optionally define the comment for the SSH key. - ssh_key_passphrase: - required: false - version_added: "0.9" - description: - - Set a passphrase for the SSH key. If no - passphrase is provided, the SSH key will default to - having no passphrase. - update_password: - required: false - default: always - choices: ['always', 'on_create'] - version_added: "1.3" - description: - - C(always) will update passwords if they differ. C(on_create) will only set the password for newly created users. -''' - -EXAMPLES = ''' -# Add the user 'johnd' with a specific uid and a primary group of 'admin' -- user: name=johnd comment="John Doe" uid=1040 group=admin - -# Add the user 'james' with a bash shell, appending the group 'admins' and 'developers' to the user's groups -- user: name=james shell=/bin/bash groups=admins,developers append=yes - -# Remove the user 'johnd' -- user: name=johnd state=absent remove=yes - -# Create a 2048-bit SSH key for user jsmith -- user: name=jsmith generate_ssh_key=yes ssh_key_bits=2048 -''' - -import os -import pwd -import grp -import syslog -import platform - -try: - import spwd - HAVE_SPWD=True -except: - HAVE_SPWD=False - - -class User(object): - """ - This is a generic User manipulation class that is subclassed - based on platform. - - A subclass may wish to override the following action methods:- - - create_user() - - remove_user() - - modify_user() - - ssh_key_gen() - - ssh_key_fingerprint() - - user_exists() - - All subclasses MUST define platform and distribution (which may be None). - """ - - platform = 'Generic' - distribution = None - SHADOWFILE = '/etc/shadow' - - def __new__(cls, *args, **kwargs): - return load_platform_subclass(User, args, kwargs) - - def __init__(self, module): - self.module = module - self.state = module.params['state'] - self.name = module.params['name'] - self.uid = module.params['uid'] - self.non_unique = module.params['non_unique'] - self.group = module.params['group'] - self.groups = module.params['groups'] - self.comment = module.params['comment'] - self.home = module.params['home'] - self.shell = module.params['shell'] - self.password = module.params['password'] - self.force = module.params['force'] - self.remove = module.params['remove'] - self.createhome = module.params['createhome'] - self.move_home = module.params['move_home'] - self.system = module.params['system'] - self.login_class = module.params['login_class'] - self.append = module.params['append'] - self.sshkeygen = module.params['generate_ssh_key'] - self.ssh_bits = module.params['ssh_key_bits'] - self.ssh_type = module.params['ssh_key_type'] - self.ssh_comment = module.params['ssh_key_comment'] - self.ssh_passphrase = module.params['ssh_key_passphrase'] - self.update_password = module.params['update_password'] - if module.params['ssh_key_file'] is not None: - self.ssh_file = module.params['ssh_key_file'] - else: - self.ssh_file = os.path.join('.ssh', 'id_%s' % self.ssh_type) - - # select whether we dump additional debug info through syslog - self.syslogging = False - - def execute_command(self, cmd): - if self.syslogging: - syslog.openlog('ansible-%s' % os.path.basename(__file__)) - syslog.syslog(syslog.LOG_NOTICE, 'Command %s' % '|'.join(cmd)) - - return self.module.run_command(cmd) - - def remove_user_userdel(self): - cmd = [self.module.get_bin_path('userdel', True)] - if self.force: - cmd.append('-f') - if self.remove: - cmd.append('-r') - cmd.append(self.name) - - return self.execute_command(cmd) - - def create_user_useradd(self, command_name='useradd'): - cmd = [self.module.get_bin_path(command_name, True)] - - if self.uid is not None: - cmd.append('-u') - cmd.append(self.uid) - - if self.non_unique: - cmd.append('-o') - - if self.group is not None: - if not self.group_exists(self.group): - self.module.fail_json(msg="Group %s does not exist" % self.group) - cmd.append('-g') - cmd.append(self.group) - elif self.group_exists(self.name): - # use the -N option (no user group) if a group already - # exists with the same name as the user to prevent - # errors from useradd trying to create a group when - # USERGROUPS_ENAB is set in /etc/login.defs. - cmd.append('-N') - - if self.groups is not None and len(self.groups): - groups = self.get_groups_set() - cmd.append('-G') - cmd.append(','.join(groups)) - - if self.comment is not None: - cmd.append('-c') - cmd.append(self.comment) - - if self.home is not None: - cmd.append('-d') - cmd.append(self.home) - - if self.shell is not None: - cmd.append('-s') - cmd.append(self.shell) - - if self.password is not None: - cmd.append('-p') - cmd.append(self.password) - - if self.createhome: - cmd.append('-m') - else: - cmd.append('-M') - - if self.system: - cmd.append('-r') - - cmd.append(self.name) - return self.execute_command(cmd) - - - def _check_usermod_append(self): - # check if this version of usermod can append groups - usermod_path = self.module.get_bin_path('usermod', True) - - # for some reason, usermod --help cannot be used by non root - # on RH/Fedora, due to lack of execute bit for others - if not os.access(usermod_path, os.X_OK): - return False - - cmd = [usermod_path] - cmd.append('--help') - rc, data1, data2 = self.execute_command(cmd) - helpout = data1 + data2 - - # check if --append exists - lines = helpout.split('\n') - for line in lines: - if line.strip().startswith('-a, --append'): - return True - - return False - - - - def modify_user_usermod(self): - cmd = [self.module.get_bin_path('usermod', True)] - info = self.user_info() - has_append = self._check_usermod_append() - - if self.uid is not None and info[2] != int(self.uid): - cmd.append('-u') - cmd.append(self.uid) - - if self.non_unique: - cmd.append('-o') - - if self.group is not None: - if not self.group_exists(self.group): - self.module.fail_json(msg="Group %s does not exist" % self.group) - ginfo = self.group_info(self.group) - if info[3] != ginfo[2]: - cmd.append('-g') - cmd.append(self.group) - - if self.groups is not None: - current_groups = self.user_group_membership() - groups_need_mod = False - groups = [] - - if self.groups == '': - if current_groups and not self.append: - groups_need_mod = True - else: - groups = self.get_groups_set(remove_existing=False) - group_diff = set(current_groups).symmetric_difference(groups) - - if group_diff: - if self.append: - for g in groups: - if g in group_diff: - if has_append: - cmd.append('-a') - groups_need_mod = True - break - else: - groups_need_mod = True - - if groups_need_mod: - if self.append and not has_append: - cmd.append('-A') - cmd.append(','.join(group_diff)) - else: - cmd.append('-G') - cmd.append(','.join(groups)) - - - if self.comment is not None and info[4] != self.comment: - cmd.append('-c') - cmd.append(self.comment) - - if self.home is not None and info[5] != self.home: - cmd.append('-d') - cmd.append(self.home) - if self.move_home: - cmd.append('-m') - - if self.shell is not None and info[6] != self.shell: - cmd.append('-s') - cmd.append(self.shell) - - if self.update_password == 'always' and self.password is not None and info[1] != self.password: - cmd.append('-p') - cmd.append(self.password) - - # skip if no changes to be made - if len(cmd) == 1: - return (None, '', '') - elif self.module.check_mode: - return (0, '', '') - - cmd.append(self.name) - return self.execute_command(cmd) - - def group_exists(self,group): - try: - if group.isdigit(): - if grp.getgrgid(int(group)): - return True - else: - if grp.getgrnam(group): - return True - except KeyError: - return False - - def group_info(self,group): - if not self.group_exists(group): - return False - if group.isdigit(): - return list(grp.getgrgid(group)) - else: - return list(grp.getgrnam(group)) - - def get_groups_set(self, remove_existing=True): - if self.groups is None: - return None - info = self.user_info() - groups = set(filter(None, self.groups.split(','))) - for g in set(groups): - if not self.group_exists(g): - self.module.fail_json(msg="Group %s does not exist" % (g)) - if info and remove_existing and self.group_info(g)[2] == info[3]: - groups.remove(g) - return groups - - def user_group_membership(self): - groups = [] - info = self.get_pwd_info() - for group in grp.getgrall(): - if self.name in group.gr_mem and not info[3] == group.gr_gid: - groups.append(group[0]) - return groups - - def user_exists(self): - try: - if pwd.getpwnam(self.name): - return True - except KeyError: - return False - - def get_pwd_info(self): - if not self.user_exists(): - return False - return list(pwd.getpwnam(self.name)) - - def user_info(self): - if not self.user_exists(): - return False - info = self.get_pwd_info() - if len(info[1]) == 1 or len(info[1]) == 0: - info[1] = self.user_password() - return info - - def user_password(self): - passwd = '' - if HAVE_SPWD: - try: - passwd = spwd.getspnam(self.name)[1] - except KeyError: - return passwd - if not self.user_exists(): - return passwd - else: - # Read shadow file for user's encrypted password string - if os.path.exists(self.SHADOWFILE) and os.access(self.SHADOWFILE, os.R_OK): - for line in open(self.SHADOWFILE).readlines(): - if line.startswith('%s:' % self.name): - passwd = line.split(':')[1] - return passwd - - def get_ssh_key_path(self): - info = self.user_info() - if os.path.isabs(self.ssh_file): - ssh_key_file = self.ssh_file - else: - ssh_key_file = os.path.join(info[5], self.ssh_file) - return ssh_key_file - - def ssh_key_gen(self): - info = self.user_info() - if not os.path.exists(info[5]): - return (1, '', 'User %s home directory does not exist' % self.name) - ssh_key_file = self.get_ssh_key_path() - ssh_dir = os.path.dirname(ssh_key_file) - if not os.path.exists(ssh_dir): - try: - os.mkdir(ssh_dir, 0700) - os.chown(ssh_dir, info[2], info[3]) - except OSError, e: - return (1, '', 'Failed to create %s: %s' % (ssh_dir, str(e))) - if os.path.exists(ssh_key_file): - return (None, 'Key already exists', '') - cmd = [self.module.get_bin_path('ssh-keygen', True)] - cmd.append('-t') - cmd.append(self.ssh_type) - cmd.append('-b') - cmd.append(self.ssh_bits) - cmd.append('-C') - cmd.append(self.ssh_comment) - cmd.append('-f') - cmd.append(ssh_key_file) - cmd.append('-N') - if self.ssh_passphrase is not None: - cmd.append(self.ssh_passphrase) - else: - cmd.append('') - - (rc, out, err) = self.execute_command(cmd) - if rc == 0: - # If the keys were successfully created, we should be able - # to tweak ownership. - os.chown(ssh_key_file, info[2], info[3]) - os.chown('%s.pub' % ssh_key_file, info[2], info[3]) - return (rc, out, err) - - def ssh_key_fingerprint(self): - ssh_key_file = self.get_ssh_key_path() - if not os.path.exists(ssh_key_file): - return (1, 'SSH Key file %s does not exist' % ssh_key_file, '') - cmd = [ self.module.get_bin_path('ssh-keygen', True) ] - cmd.append('-l') - cmd.append('-f') - cmd.append(ssh_key_file) - - return self.execute_command(cmd) - - def get_ssh_public_key(self): - ssh_public_key_file = '%s.pub' % self.get_ssh_key_path() - try: - f = open(ssh_public_key_file) - ssh_public_key = f.read().strip() - f.close() - except IOError: - return None - return ssh_public_key - - def create_user(self): - # by default we use the create_user_useradd method - return self.create_user_useradd() - - def remove_user(self): - # by default we use the remove_user_userdel method - return self.remove_user_userdel() - - def modify_user(self): - # by default we use the modify_user_usermod method - return self.modify_user_usermod() - - def create_homedir(self, path): - if not os.path.exists(path): - # use /etc/skel if possible - if os.path.exists('/etc/skel'): - try: - shutil.copytree('/etc/skel', path, symlinks=True) - except OSError, e: - self.module.exit_json(failed=True, msg="%s" % e) - else: - try: - os.makedirs(path) - except OSError, e: - self.module.exit_json(failed=True, msg="%s" % e) - - def chown_homedir(self, uid, gid, path): - try: - os.chown(path, uid, gid) - for root, dirs, files in os.walk(path): - for d in dirs: - os.chown(path, uid, gid) - for f in files: - os.chown(os.path.join(root, f), uid, gid) - except OSError, e: - self.module.exit_json(failed=True, msg="%s" % e) - - -# =========================================== - -class FreeBsdUser(User): - """ - This is a FreeBSD User manipulation class - it uses the pw command - to manipulate the user database, followed by the chpass command - to change the password. - - This overrides the following methods from the generic class:- - - create_user() - - remove_user() - - modify_user() - """ - - platform = 'FreeBSD' - distribution = None - SHADOWFILE = '/etc/master.passwd' - - def remove_user(self): - cmd = [ - self.module.get_bin_path('pw', True), - 'userdel', - '-n', - self.name - ] - if self.remove: - cmd.append('-r') - - return self.execute_command(cmd) - - def create_user(self): - cmd = [ - self.module.get_bin_path('pw', True), - 'useradd', - '-n', - self.name, - ] - - if self.uid is not None: - cmd.append('-u') - cmd.append(self.uid) - - if self.non_unique: - cmd.append('-o') - - if self.comment is not None: - cmd.append('-c') - cmd.append(self.comment) - - if self.home is not None: - cmd.append('-d') - cmd.append(self.home) - - if self.group is not None: - if not self.group_exists(self.group): - self.module.fail_json(msg="Group %s does not exist" % self.group) - cmd.append('-g') - cmd.append(self.group) - - if self.groups is not None: - groups = self.get_groups_set() - cmd.append('-G') - cmd.append(','.join(groups)) - - if self.createhome: - cmd.append('-m') - - if self.shell is not None: - cmd.append('-s') - cmd.append(self.shell) - - if self.login_class is not None: - cmd.append('-L') - cmd.append(self.login_class) - - # system cannot be handled currently - should we error if its requested? - # create the user - (rc, out, err) = self.execute_command(cmd) - if rc is not None and rc != 0: - self.module.fail_json(name=self.name, msg=err, rc=rc) - - # we have to set the password in a second command - if self.password is not None: - cmd = [ - self.module.get_bin_path('chpass', True), - '-p', - self.password, - self.name - ] - return self.execute_command(cmd) - - return (rc, out, err) - - def modify_user(self): - cmd = [ - self.module.get_bin_path('pw', True), - 'usermod', - '-n', - self.name - ] - cmd_len = len(cmd) - info = self.user_info() - - if self.uid is not None and info[2] != int(self.uid): - cmd.append('-u') - cmd.append(self.uid) - - if self.non_unique: - cmd.append('-o') - - if self.comment is not None and info[4] != self.comment: - cmd.append('-c') - cmd.append(self.comment) - - if self.home is not None and info[5] != self.home: - if self.move_home: - cmd.append('-m') - cmd.append('-d') - cmd.append(self.home) - - if self.group is not None: - if not self.group_exists(self.group): - self.module.fail_json(msg="Group %s does not exist" % self.group) - ginfo = self.group_info(self.group) - if info[3] != ginfo[2]: - cmd.append('-g') - cmd.append(self.group) - - if self.shell is not None and info[6] != self.shell: - cmd.append('-s') - cmd.append(self.shell) - - if self.login_class is not None: - cmd.append('-L') - cmd.append(self.login_class) - - if self.groups is not None: - current_groups = self.user_group_membership() - groups = self.get_groups_set() - - group_diff = set(current_groups).symmetric_difference(groups) - groups_need_mod = False - - if group_diff: - if self.append: - for g in groups: - if g in group_diff: - groups_need_mod = True - break - else: - groups_need_mod = True - - if groups_need_mod: - cmd.append('-G') - new_groups = groups - if self.append: - new_groups = groups | set(current_groups) - cmd.append(','.join(new_groups)) - - # modify the user if cmd will do anything - if cmd_len != len(cmd): - (rc, out, err) = self.execute_command(cmd) - if rc is not None and rc != 0: - self.module.fail_json(name=self.name, msg=err, rc=rc) - else: - (rc, out, err) = (None, '', '') - - # we have to set the password in a second command - if self.update_password == 'always' and self.password is not None and info[1] != self.password: - cmd = [ - self.module.get_bin_path('chpass', True), - '-p', - self.password, - self.name - ] - return self.execute_command(cmd) - - return (rc, out, err) - -# =========================================== - -class OpenBSDUser(User): - """ - This is a OpenBSD User manipulation class. - Main differences are that OpenBSD:- - - has no concept of "system" account. - - has no force delete user - - This overrides the following methods from the generic class:- - - create_user() - - remove_user() - - modify_user() - """ - - platform = 'OpenBSD' - distribution = None - SHADOWFILE = '/etc/master.passwd' - - def create_user(self): - cmd = [self.module.get_bin_path('useradd', True)] - - if self.uid is not None: - cmd.append('-u') - cmd.append(self.uid) - - if self.non_unique: - cmd.append('-o') - - if self.group is not None: - if not self.group_exists(self.group): - self.module.fail_json(msg="Group %s does not exist" % self.group) - cmd.append('-g') - cmd.append(self.group) - - if self.groups is not None: - groups = self.get_groups_set() - cmd.append('-G') - cmd.append(','.join(groups)) - - if self.comment is not None: - cmd.append('-c') - cmd.append(self.comment) - - if self.home is not None: - cmd.append('-d') - cmd.append(self.home) - - if self.shell is not None: - cmd.append('-s') - cmd.append(self.shell) - - if self.login_class is not None: - cmd.append('-L') - cmd.append(self.login_class) - - if self.password is not None: - cmd.append('-p') - cmd.append(self.password) - - if self.createhome: - cmd.append('-m') - - cmd.append(self.name) - return self.execute_command(cmd) - - def remove_user_userdel(self): - cmd = [self.module.get_bin_path('userdel', True)] - if self.remove: - cmd.append('-r') - cmd.append(self.name) - return self.execute_command(cmd) - - def modify_user(self): - cmd = [self.module.get_bin_path('usermod', True)] - info = self.user_info() - - if self.uid is not None and info[2] != int(self.uid): - cmd.append('-u') - cmd.append(self.uid) - - if self.non_unique: - cmd.append('-o') - - if self.group is not None: - if not self.group_exists(self.group): - self.module.fail_json(msg="Group %s does not exist" % self.group) - ginfo = self.group_info(self.group) - if info[3] != ginfo[2]: - cmd.append('-g') - cmd.append(self.group) - - if self.groups is not None: - current_groups = self.user_group_membership() - groups_need_mod = False - groups_option = '-G' - groups = [] - - if self.groups == '': - if current_groups and not self.append: - groups_need_mod = True - else: - groups = self.get_groups_set() - group_diff = set(current_groups).symmetric_difference(groups) - - if group_diff: - if self.append: - for g in groups: - if g in group_diff: - groups_option = '-S' - groups_need_mod = True - break - else: - groups_need_mod = True - - if groups_need_mod: - cmd.append(groups_option) - cmd.append(','.join(groups)) - - if self.comment is not None and info[4] != self.comment: - cmd.append('-c') - cmd.append(self.comment) - - if self.home is not None and info[5] != self.home: - if self.move_home: - cmd.append('-m') - cmd.append('-d') - cmd.append(self.home) - - if self.shell is not None and info[6] != self.shell: - cmd.append('-s') - cmd.append(self.shell) - - if self.login_class is not None: - # find current login class - user_login_class = None - userinfo_cmd = [self.module.get_bin_path('userinfo', True), self.name] - (rc, out, err) = self.execute_command(userinfo_cmd) - - for line in out.splitlines(): - tokens = line.split() - - if tokens[0] == 'class' and len(tokens) == 2: - user_login_class = tokens[1] - - # act only if login_class change - if self.login_class != user_login_class: - cmd.append('-L') - cmd.append(self.login_class) - - if self.update_password == 'always' and self.password is not None and info[1] != self.password: - cmd.append('-p') - cmd.append(self.password) - - # skip if no changes to be made - if len(cmd) == 1: - return (None, '', '') - elif self.module.check_mode: - return (0, '', '') - - cmd.append(self.name) - return self.execute_command(cmd) - - -# =========================================== - -class NetBSDUser(User): - """ - This is a NetBSD User manipulation class. - Main differences are that NetBSD:- - - has no concept of "system" account. - - has no force delete user - - - This overrides the following methods from the generic class:- - - create_user() - - remove_user() - - modify_user() - """ - - platform = 'NetBSD' - distribution = None - SHADOWFILE = '/etc/master.passwd' - - def create_user(self): - cmd = [self.module.get_bin_path('useradd', True)] - - if self.uid is not None: - cmd.append('-u') - cmd.append(self.uid) - - if self.non_unique: - cmd.append('-o') - - if self.group is not None: - if not self.group_exists(self.group): - self.module.fail_json(msg="Group %s does not exist" % self.group) - cmd.append('-g') - cmd.append(self.group) - - if self.groups is not None: - groups = self.get_groups_set() - if len(groups) > 16: - self.module.fail_json(msg="Too many groups (%d) NetBSD allows for 16 max." % len(groups)) - cmd.append('-G') - cmd.append(','.join(groups)) - - if self.comment is not None: - cmd.append('-c') - cmd.append(self.comment) - - if self.home is not None: - cmd.append('-d') - cmd.append(self.home) - - if self.shell is not None: - cmd.append('-s') - cmd.append(self.shell) - - if self.login_class is not None: - cmd.append('-L') - cmd.append(self.login_class) - - if self.password is not None: - cmd.append('-p') - cmd.append(self.password) - - if self.createhome: - cmd.append('-m') - - cmd.append(self.name) - return self.execute_command(cmd) - - def remove_user_userdel(self): - cmd = [self.module.get_bin_path('userdel', True)] - if self.remove: - cmd.append('-r') - cmd.append(self.name) - return self.execute_command(cmd) - - def modify_user(self): - cmd = [self.module.get_bin_path('usermod', True)] - info = self.user_info() - - if self.uid is not None and info[2] != int(self.uid): - cmd.append('-u') - cmd.append(self.uid) - - if self.non_unique: - cmd.append('-o') - - if self.group is not None: - if not self.group_exists(self.group): - self.module.fail_json(msg="Group %s does not exist" % self.group) - ginfo = self.group_info(self.group) - if info[3] != ginfo[2]: - cmd.append('-g') - cmd.append(self.group) - - if self.groups is not None: - current_groups = self.user_group_membership() - groups_need_mod = False - groups = [] - - if self.groups == '': - if current_groups and not self.append: - groups_need_mod = True - else: - groups = self.get_groups_set() - group_diff = set(current_groups).symmetric_difference(groups) - - if group_diff: - if self.append: - for g in groups: - if g in group_diff: - groups = set(current_groups).union(groups) - groups_need_mod = True - break - else: - groups_need_mod = True - - if groups_need_mod: - if len(groups) > 16: - self.module.fail_json(msg="Too many groups (%d) NetBSD allows for 16 max." % len(groups)) - cmd.append('-G') - cmd.append(','.join(groups)) - - if self.comment is not None and info[4] != self.comment: - cmd.append('-c') - cmd.append(self.comment) - - if self.home is not None and info[5] != self.home: - if self.move_home: - cmd.append('-m') - cmd.append('-d') - cmd.append(self.home) - - if self.shell is not None and info[6] != self.shell: - cmd.append('-s') - cmd.append(self.shell) - - if self.login_class is not None: - cmd.append('-L') - cmd.append(self.login_class) - - if self.update_password == 'always' and self.password is not None and info[1] != self.password: - cmd.append('-p') - cmd.append(self.password) - - # skip if no changes to be made - if len(cmd) == 1: - return (None, '', '') - elif self.module.check_mode: - return (0, '', '') - - cmd.append(self.name) - return self.execute_command(cmd) - - -# =========================================== - -class SunOS(User): - """ - This is a SunOS User manipulation class - The main difference between - this class and the generic user class is that Solaris-type distros - don't support the concept of a "system" account and we need to - edit the /etc/shadow file manually to set a password. (Ugh) - - This overrides the following methods from the generic class:- - - create_user() - - remove_user() - - modify_user() - """ - - platform = 'SunOS' - distribution = None - SHADOWFILE = '/etc/shadow' - - def remove_user(self): - cmd = [self.module.get_bin_path('userdel', True)] - if self.remove: - cmd.append('-r') - cmd.append(self.name) - - return self.execute_command(cmd) - - def create_user(self): - cmd = [self.module.get_bin_path('useradd', True)] - - if self.uid is not None: - cmd.append('-u') - cmd.append(self.uid) - - if self.non_unique: - cmd.append('-o') - - if self.group is not None: - if not self.group_exists(self.group): - self.module.fail_json(msg="Group %s does not exist" % self.group) - cmd.append('-g') - cmd.append(self.group) - - if self.groups is not None: - groups = self.get_groups_set() - cmd.append('-G') - cmd.append(','.join(groups)) - - if self.comment is not None: - cmd.append('-c') - cmd.append(self.comment) - - if self.home is not None: - cmd.append('-d') - cmd.append(self.home) - - if self.shell is not None: - cmd.append('-s') - cmd.append(self.shell) - - if self.createhome: - cmd.append('-m') - - cmd.append(self.name) - - if self.module.check_mode: - return (0, '', '') - else: - (rc, out, err) = self.execute_command(cmd) - if rc is not None and rc != 0: - self.module.fail_json(name=self.name, msg=err, rc=rc) - - # we have to set the password by editing the /etc/shadow file - if self.password is not None: - try: - lines = [] - for line in open(self.SHADOWFILE, 'rb').readlines(): - fields = line.strip().split(':') - if not fields[0] == self.name: - lines.append(line) - continue - fields[1] = self.password - fields[2] = str(int(time.time() / 86400)) - line = ':'.join(fields) - lines.append('%s\n' % line) - open(self.SHADOWFILE, 'w+').writelines(lines) - except Exception, err: - self.module.fail_json(msg="failed to update users password: %s" % str(err)) - - return (rc, out, err) - - def modify_user_usermod(self): - cmd = [self.module.get_bin_path('usermod', True)] - cmd_len = len(cmd) - info = self.user_info() - - if self.uid is not None and info[2] != int(self.uid): - cmd.append('-u') - cmd.append(self.uid) - - if self.non_unique: - cmd.append('-o') - - if self.group is not None: - if not self.group_exists(self.group): - self.module.fail_json(msg="Group %s does not exist" % self.group) - ginfo = self.group_info(self.group) - if info[3] != ginfo[2]: - cmd.append('-g') - cmd.append(self.group) - - if self.groups is not None: - current_groups = self.user_group_membership() - groups = self.get_groups_set() - group_diff = set(current_groups).symmetric_difference(groups) - groups_need_mod = False - - if group_diff: - if self.append: - for g in groups: - if g in group_diff: - groups_need_mod = True - break - else: - groups_need_mod = True - - if groups_need_mod: - cmd.append('-G') - new_groups = groups - if self.append: - new_groups.extend(current_groups) - cmd.append(','.join(new_groups)) - - if self.comment is not None and info[4] != self.comment: - cmd.append('-c') - cmd.append(self.comment) - - if self.home is not None and info[5] != self.home: - if self.move_home: - cmd.append('-m') - cmd.append('-d') - cmd.append(self.home) - - if self.shell is not None and info[6] != self.shell: - cmd.append('-s') - cmd.append(self.shell) - - if self.module.check_mode: - return (0, '', '') - else: - # modify the user if cmd will do anything - if cmd_len != len(cmd): - cmd.append(self.name) - (rc, out, err) = self.execute_command(cmd) - if rc is not None and rc != 0: - self.module.fail_json(name=self.name, msg=err, rc=rc) - else: - (rc, out, err) = (None, '', '') - - # we have to set the password by editing the /etc/shadow file - if self.update_password == 'always' and self.password is not None and info[1] != self.password: - try: - lines = [] - for line in open(self.SHADOWFILE, 'rb').readlines(): - fields = line.strip().split(':') - if not fields[0] == self.name: - lines.append(line) - continue - fields[1] = self.password - fields[2] = str(int(time.time() / 86400)) - line = ':'.join(fields) - lines.append('%s\n' % line) - open(self.SHADOWFILE, 'w+').writelines(lines) - rc = 0 - except Exception, err: - self.module.fail_json(msg="failed to update users password: %s" % str(err)) - - return (rc, out, err) - -# =========================================== - -class AIX(User): - """ - This is a AIX User manipulation class. - - This overrides the following methods from the generic class:- - - create_user() - - remove_user() - - modify_user() - """ - - platform = 'AIX' - distribution = None - SHADOWFILE = '/etc/security/passwd' - - def remove_user(self): - cmd = [self.module.get_bin_path('userdel', True)] - if self.remove: - cmd.append('-r') - cmd.append(self.name) - - return self.execute_command(cmd) - - def create_user_useradd(self, command_name='useradd'): - cmd = [self.module.get_bin_path(command_name, True)] - - if self.uid is not None: - cmd.append('-u') - cmd.append(self.uid) - - if self.group is not None: - if not self.group_exists(self.group): - self.module.fail_json(msg="Group %s does not exist" % self.group) - cmd.append('-g') - cmd.append(self.group) - - if self.groups is not None and len(self.groups): - groups = self.get_groups_set() - cmd.append('-G') - cmd.append(','.join(groups)) - - if self.comment is not None: - cmd.append('-c') - cmd.append(self.comment) - - if self.home is not None: - cmd.append('-d') - cmd.append(self.home) - - if self.shell is not None: - cmd.append('-s') - cmd.append(self.shell) - - if self.createhome: - cmd.append('-m') - - cmd.append(self.name) - (rc, out, err) = self.execute_command(cmd) - - # set password with chpasswd - if self.password is not None: - cmd = [] - cmd.append('echo "'+self.name+':'+self.password+'" |') - cmd.append(self.module.get_bin_path('chpasswd', True)) - cmd.append('-e') - cmd.append('-c') - self.execute_command(' '.join(cmd)) - - return (rc, out, err) - - def modify_user_usermod(self): - cmd = [self.module.get_bin_path('usermod', True)] - info = self.user_info() - - if self.uid is not None and info[2] != int(self.uid): - cmd.append('-u') - cmd.append(self.uid) - - if self.group is not None: - if not self.group_exists(self.group): - self.module.fail_json(msg="Group %s does not exist" % self.group) - ginfo = self.group_info(self.group) - if info[3] != ginfo[2]: - cmd.append('-g') - cmd.append(self.group) - - if self.groups is not None: - current_groups = self.user_group_membership() - groups_need_mod = False - groups = [] - - if self.groups == '': - if current_groups and not self.append: - groups_need_mod = True - else: - groups = self.get_groups_set() - group_diff = set(current_groups).symmetric_difference(groups) - - if group_diff: - if self.append: - for g in groups: - if g in group_diff: - groups_need_mod = True - break - else: - groups_need_mod = True - - if groups_need_mod: - cmd.append('-G') - cmd.append(','.join(groups)) - - if self.comment is not None and info[4] != self.comment: - cmd.append('-c') - cmd.append(self.comment) - - if self.home is not None and info[5] != self.home: - if self.move_home: - cmd.append('-m') - cmd.append('-d') - cmd.append(self.home) - - if self.shell is not None and info[6] != self.shell: - cmd.append('-s') - cmd.append(self.shell) - - - # skip if no changes to be made - if len(cmd) == 1: - (rc, out, err) = (None, '', '') - elif self.module.check_mode: - return (True, '', '') - else: - cmd.append(self.name) - (rc, out, err) = self.execute_command(cmd) - - # set password with chpasswd - if self.update_password == 'always' and self.password is not None and info[1] != self.password: - cmd = [] - cmd.append('echo "'+self.name+':'+self.password+'" |') - cmd.append(self.module.get_bin_path('chpasswd', True)) - cmd.append('-e') - cmd.append('-c') - (rc2, out2, err2) = self.execute_command(' '.join(cmd)) - else: - (rc2, out2, err2) = (None, '', '') - - if rc != None: - return (rc, out+out2, err+err2) - else: - return (rc2, out+out2, err+err2) - -# =========================================== - -def main(): - ssh_defaults = { - 'bits': '2048', - 'type': 'rsa', - 'passphrase': None, - 'comment': 'ansible-generated' - } - module = AnsibleModule( - argument_spec = dict( - state=dict(default='present', choices=['present', 'absent'], type='str'), - name=dict(required=True, aliases=['user'], type='str'), - uid=dict(default=None, type='str'), - non_unique=dict(default='no', type='bool'), - group=dict(default=None, type='str'), - groups=dict(default=None, type='str'), - comment=dict(default=None, type='str'), - home=dict(default=None, type='str'), - shell=dict(default=None, type='str'), - password=dict(default=None, type='str'), - login_class=dict(default=None, type='str'), - # following options are specific to userdel - force=dict(default='no', type='bool'), - remove=dict(default='no', type='bool'), - # following options are specific to useradd - createhome=dict(default='yes', type='bool'), - system=dict(default='no', type='bool'), - # following options are specific to usermod - move_home=dict(default='no', type='bool'), - append=dict(default='no', type='bool'), - # following are specific to ssh key generation - generate_ssh_key=dict(type='bool'), - ssh_key_bits=dict(default=ssh_defaults['bits'], type='str'), - ssh_key_type=dict(default=ssh_defaults['type'], type='str'), - ssh_key_file=dict(default=None, type='str'), - ssh_key_comment=dict(default=ssh_defaults['comment'], type='str'), - ssh_key_passphrase=dict(default=None, type='str'), - update_password=dict(default='always',choices=['always','on_create'],type='str') - ), - supports_check_mode=True - ) - - user = User(module) - - if user.syslogging: - syslog.openlog('ansible-%s' % os.path.basename(__file__)) - syslog.syslog(syslog.LOG_NOTICE, 'User instantiated - platform %s' % user.platform) - if user.distribution: - syslog.syslog(syslog.LOG_NOTICE, 'User instantiated - distribution %s' % user.distribution) - - rc = None - out = '' - err = '' - result = {} - result['name'] = user.name - result['state'] = user.state - if user.state == 'absent': - if user.user_exists(): - if module.check_mode: - module.exit_json(changed=True) - (rc, out, err) = user.remove_user() - if rc != 0: - module.fail_json(name=user.name, msg=err, rc=rc) - result['force'] = user.force - result['remove'] = user.remove - elif user.state == 'present': - if not user.user_exists(): - if module.check_mode: - module.exit_json(changed=True) - (rc, out, err) = user.create_user() - result['system'] = user.system - result['createhome'] = user.createhome - else: - # modify user (note: this function is check mode aware) - (rc, out, err) = user.modify_user() - result['append'] = user.append - result['move_home'] = user.move_home - if rc is not None and rc != 0: - module.fail_json(name=user.name, msg=err, rc=rc) - if user.password is not None: - result['password'] = 'NOT_LOGGING_PASSWORD' - - if rc is None: - result['changed'] = False - else: - result['changed'] = True - if out: - result['stdout'] = out - if err: - result['stderr'] = err - - if user.user_exists(): - info = user.user_info() - if info == False: - result['msg'] = "failed to look up user name: %s" % user.name - result['failed'] = True - result['uid'] = info[2] - result['group'] = info[3] - result['comment'] = info[4] - result['home'] = info[5] - result['shell'] = info[6] - result['uid'] = info[2] - if user.groups is not None: - result['groups'] = user.groups - - # deal with ssh key - if user.sshkeygen: - (rc, out, err) = user.ssh_key_gen() - if rc is not None and rc != 0: - module.fail_json(name=user.name, msg=err, rc=rc) - if rc == 0: - result['changed'] = True - (rc, out, err) = user.ssh_key_fingerprint() - if rc == 0: - result['ssh_fingerprint'] = out.strip() - else: - result['ssh_fingerprint'] = err.strip() - result['ssh_key_file'] = user.get_ssh_key_path() - result['ssh_public_key'] = user.get_ssh_public_key() - - # handle missing homedirs - info = user.user_info() - if user.home is None: - user.home = info[5] - if not os.path.exists(user.home) and user.createhome: - if not module.check_mode: - user.create_homedir(user.home) - user.chown_homedir(info[2], info[3], user.home) - result['changed'] = True - - module.exit_json(**result) - -# import module snippets -from ansible.module_utils.basic import * -main()