mirror of https://github.com/ansible/ansible.git
Core/extras splitting WIP.
parent
c1067e329a
commit
21f7c5e01a
@ -1,275 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>, 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<quote>[\'"])?
|
||||
# ^ 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)(?<!\\)(?P=quote))((?<!\\)(?=\s)|$)
|
||||
# ^ a non-escaped space or a non-escaped quote of the same kind
|
||||
# that was matched in the first 'quote' is found, or the end of
|
||||
# the line is reached
|
||||
|
||||
PARAM_REGEX = re.compile(r'(^|\s)(creates|removes|chdir|executable|NO_LOG|warn)=(?P<quote>[\'"])?(.*?)(?(quote)(?<!\\)(?P=quote))((?<!\\)(?=\s)|$)')
|
||||
|
||||
|
||||
def check_command(commandline):
|
||||
arguments = { 'chown': 'owner', 'chmod': 'mode', 'chgrp': 'group',
|
||||
'ln': 'state=link', 'mkdir': 'state=directory',
|
||||
'rmdir': 'state=absent', 'rm': 'state=absent', 'touch': 'state=touch' }
|
||||
commands = { 'git': 'git', 'hg': 'hg', 'curl': 'get_url', 'wget': 'get_url',
|
||||
'svn': 'subversion', 'service': 'service',
|
||||
'mount': 'mount', 'rpm': 'yum', 'yum': 'yum', 'apt-get': 'apt-get',
|
||||
'tar': 'unarchive', 'unzip': 'unarchive', 'sed': 'template or lineinfile',
|
||||
'rsync': 'synchronize' }
|
||||
warnings = list()
|
||||
command = os.path.basename(commandline.split()[0])
|
||||
if command in arguments:
|
||||
warnings.append("Consider using file module with %s rather than running %s" % (arguments[command], command))
|
||||
if command in commands:
|
||||
warnings.append("Consider using %s module rather than running %s" % (commands[command], command))
|
||||
return warnings
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
# the command module is the one ansible module that does not take key=value args
|
||||
# hence don't copy this one if you are looking to build others!
|
||||
module = CommandModule(argument_spec=dict())
|
||||
|
||||
shell = module.params['shell']
|
||||
chdir = module.params['chdir']
|
||||
executable = module.params['executable']
|
||||
args = module.params['args']
|
||||
creates = module.params['creates']
|
||||
removes = module.params['removes']
|
||||
warn = module.params.get('warn', True)
|
||||
|
||||
if args.strip() == '':
|
||||
module.fail_json(rc=256, msg="no command given")
|
||||
|
||||
if chdir:
|
||||
os.chdir(chdir)
|
||||
|
||||
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(
|
||||
cmd=args,
|
||||
stdout="skipped, since %s exists" % v,
|
||||
changed=False,
|
||||
stderr=False,
|
||||
rc=0
|
||||
)
|
||||
|
||||
if removes:
|
||||
# do not run the command if the line contains removes=filename
|
||||
# and the filename does not exist. This allows idempotence
|
||||
# of command executions.
|
||||
v = os.path.expanduser(removes)
|
||||
if not os.path.exists(v):
|
||||
module.exit_json(
|
||||
cmd=args,
|
||||
stdout="skipped, since %s does not exist" % v,
|
||||
changed=False,
|
||||
stderr=False,
|
||||
rc=0
|
||||
)
|
||||
|
||||
warnings = list()
|
||||
if warn:
|
||||
warnings = check_command(args)
|
||||
|
||||
if not shell:
|
||||
args = shlex.split(args)
|
||||
startd = datetime.datetime.now()
|
||||
|
||||
rc, out, err = module.run_command(args, executable=executable, use_unsafe_shell=shell)
|
||||
|
||||
endd = datetime.datetime.now()
|
||||
delta = endd - startd
|
||||
|
||||
if out is None:
|
||||
out = ''
|
||||
if err is None:
|
||||
err = ''
|
||||
|
||||
module.exit_json(
|
||||
cmd = args,
|
||||
stdout = out.rstrip("\r\n"),
|
||||
stderr = err.rstrip("\r\n"),
|
||||
rc = rc,
|
||||
start = str(startd),
|
||||
end = str(endd),
|
||||
delta = str(delta),
|
||||
changed = True,
|
||||
warnings = warnings
|
||||
)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.splitter import *
|
||||
|
||||
# only the command module should ever need to do this
|
||||
# everything else should be simple key=value
|
||||
|
||||
class CommandModule(AnsibleModule):
|
||||
|
||||
def _handle_aliases(self):
|
||||
return {}
|
||||
|
||||
def _check_invalid_arguments(self):
|
||||
pass
|
||||
|
||||
def _load_params(self):
|
||||
''' read the input and return a dictionary and the arguments string '''
|
||||
args = MODULE_ARGS
|
||||
params = {}
|
||||
params['chdir'] = None
|
||||
params['creates'] = None
|
||||
params['removes'] = None
|
||||
params['shell'] = False
|
||||
params['executable'] = None
|
||||
params['warn'] = True
|
||||
if "#USE_SHELL" in args:
|
||||
args = args.replace("#USE_SHELL", "")
|
||||
params['shell'] = True
|
||||
|
||||
items = split_args(args)
|
||||
|
||||
for x in items:
|
||||
quoted = x.startswith('"') and x.endswith('"') or x.startswith("'") and x.endswith("'")
|
||||
if '=' in x and not quoted:
|
||||
# check to see if this is a special parameter for the command
|
||||
k, v = x.split('=', 1)
|
||||
v = unquote(v)
|
||||
# because we're not breaking out quotes in the shlex split
|
||||
# above, the value of the k=v pair may still be quoted. If
|
||||
# so, remove them.
|
||||
if len(v) > 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()
|
@ -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
|
||||
'''
|
@ -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
|
||||
'''
|
@ -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
|
||||
'''
|
@ -1,363 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Mark Theunissen <mark.theunissen@gmail.com>
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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()
|
@ -1,476 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# (c) 2012, Mark Theunissen <mark.theunissen@gmail.com>
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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()
|
@ -1,253 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
|
||||
Ansible module to manage mysql variables
|
||||
(c) 2013, Balazs Pocze <banyek@gawker.com>
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
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()
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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()
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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()
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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()
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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 '<etype>:<qualifier>:<perms>'. 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()
|
@ -1,200 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Stephen Fromm <sfromm@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import 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()
|
||||
|
@ -1,254 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import 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()
|
@ -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
|
||||
'''
|
@ -1,358 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import 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()
|
||||
|
@ -1,207 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Jan-Piet Mens <jpmens () gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
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()
|
@ -1,400 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Daniel Hokka Zakrisson <daniel@hozac.com>
|
||||
# (c) 2014, Ahti Kitsik <ak@ahtik.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import 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()
|
@ -1,162 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2013, Evan Kaufman <evan@digitalflophouse.com
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import 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
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
|
||||
main()
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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()
|
@ -1,345 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012-2013, Timothy Appnel <tim@appnel.com>
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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 ~<remote_user>/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 = '<<CHANGED>>'
|
||||
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()
|
||||
|
@ -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'
|
||||
'''
|
@ -1,250 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
# (c) 2013, Dylan Martin <dmartin@seattlecentral.edu>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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()
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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()
|
@ -1,99 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>, 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
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()
|
@ -1,200 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>, 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
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 <jid> <time_limit> <modulescript> <argsfile>. 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
|
@ -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 }}
|
||||
'''
|
@ -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 }}
|
||||
'''
|
@ -1,421 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Ansible module to add authorized_keys for ssh logins.
|
||||
(c) 2012, Brad Olson <brado@movedbylight.com>
|
||||
|
||||
This file is part of Ansible
|
||||
|
||||
Ansible is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Ansible is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
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()
|
@ -1,524 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2012, Dane Summers <dsummers@pinedesk.biz>
|
||||
# (c) 2013, Mike Grozak <mike.grozak@gmail.com>
|
||||
# (c) 2013, Patrick Callahan <pmc@patrickcallahan.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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: <name>")
|
||||
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()
|
||||
|
@ -1,403 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Stephen Fromm <sfromm@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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()
|
@ -1,445 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2013, Hiroaki Nakamura <hnakamur@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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()
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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()
|
@ -1,59 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
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()
|
||||
|
@ -1,212 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# (c) 2012, Stephen Fromm <sfromm@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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()
|
@ -1,203 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Derek Carter<goozbach@friocorte.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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 <goozbach@friocorte.com>
|
||||
'''
|
||||
|
||||
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()
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,146 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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()
|
@ -1,334 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, David "DaviXX" CHANIAL <david.chanial@gmail.com>
|
||||
# (c) 2014, James Tanner <tanner.jc@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
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 <david.chanial@gmail.com>
|
||||
'''
|
||||
|
||||
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()
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue