mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-05-08 06:12:51 +00:00
Relocating extras into lib/ansible/modules/ after merge
This commit is contained in:
committed by
Matt Clay
parent
c65ba07d2c
commit
011ea55a8f
170
lib/ansible/modules/system/alternatives.py
Normal file
170
lib/ansible/modules/system/alternatives.py
Normal file
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Ansible module to manage symbolic link alternatives.
|
||||
(c) 2014, Gabe Mulley <gabe.mulley@gmail.com>
|
||||
(c) 2015, David Wittman <dwittman@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/>.
|
||||
"""
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: alternatives
|
||||
short_description: Manages alternative programs for common commands
|
||||
description:
|
||||
- Manages symbolic links using the 'update-alternatives' tool
|
||||
- Useful when multiple programs are installed but provide similar functionality (e.g. different editors).
|
||||
version_added: "1.6"
|
||||
author:
|
||||
- "David Wittman (@DavidWittman)"
|
||||
- "Gabe Mulley (@mulby)"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- The generic name of the link.
|
||||
required: true
|
||||
path:
|
||||
description:
|
||||
- The path to the real executable that the link should point to.
|
||||
required: true
|
||||
link:
|
||||
description:
|
||||
- The path to the symbolic link that should point to the real executable.
|
||||
- This option is required on RHEL-based distributions
|
||||
required: false
|
||||
priority:
|
||||
description:
|
||||
- The priority of the alternative
|
||||
required: false
|
||||
default: 50
|
||||
version_added: "2.2"
|
||||
requirements: [ update-alternatives ]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: correct java version selected
|
||||
alternatives:
|
||||
name: java
|
||||
path: /usr/lib/jvm/java-7-openjdk-amd64/jre/bin/java
|
||||
|
||||
- name: alternatives link created
|
||||
alternatives:
|
||||
name: hadoop-conf
|
||||
link: /etc/hadoop/conf
|
||||
path: /etc/hadoop/conf.ansible
|
||||
|
||||
- name: make java 32 bit an alternative with low priority
|
||||
alternatives:
|
||||
name: java
|
||||
path: /usr/lib/jvm/java-7-openjdk-i386/jre/bin/java
|
||||
priority: -10
|
||||
'''
|
||||
|
||||
import re
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
name = dict(required=True),
|
||||
path = dict(required=True, type='path'),
|
||||
link = dict(required=False, type='path'),
|
||||
priority = dict(required=False, type='int',
|
||||
default=50),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
params = module.params
|
||||
name = params['name']
|
||||
path = params['path']
|
||||
link = params['link']
|
||||
priority = params['priority']
|
||||
|
||||
UPDATE_ALTERNATIVES = module.get_bin_path('update-alternatives',True)
|
||||
|
||||
current_path = None
|
||||
all_alternatives = []
|
||||
|
||||
# Run `update-alternatives --display <name>` to find existing alternatives
|
||||
(rc, display_output, _) = module.run_command(
|
||||
['env', 'LC_ALL=C', UPDATE_ALTERNATIVES, '--display', name]
|
||||
)
|
||||
|
||||
if rc == 0:
|
||||
# Alternatives already exist for this link group
|
||||
# Parse the output to determine the current path of the symlink and
|
||||
# available alternatives
|
||||
current_path_regex = re.compile(r'^\s*link currently points to (.*)$',
|
||||
re.MULTILINE)
|
||||
alternative_regex = re.compile(r'^(\/.*)\s-\spriority', re.MULTILINE)
|
||||
|
||||
current_path = current_path_regex.search(display_output).group(1)
|
||||
all_alternatives = alternative_regex.findall(display_output)
|
||||
|
||||
if not link:
|
||||
# Read the current symlink target from `update-alternatives --query`
|
||||
# in case we need to install the new alternative before setting it.
|
||||
#
|
||||
# This is only compatible on Debian-based systems, as the other
|
||||
# alternatives don't have --query available
|
||||
rc, query_output, _ = module.run_command(
|
||||
['env', 'LC_ALL=C', UPDATE_ALTERNATIVES, '--query', name]
|
||||
)
|
||||
if rc == 0:
|
||||
for line in query_output.splitlines():
|
||||
if line.startswith('Link:'):
|
||||
link = line.split()[1]
|
||||
break
|
||||
|
||||
if current_path != path:
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True, current_path=current_path)
|
||||
try:
|
||||
# install the requested path if necessary
|
||||
if path not in all_alternatives:
|
||||
if not link:
|
||||
module.fail_json(msg="Needed to install the alternative, but unable to do so as we are missing the link")
|
||||
|
||||
module.run_command(
|
||||
[UPDATE_ALTERNATIVES, '--install', link, name, path, str(priority)],
|
||||
check_rc=True
|
||||
)
|
||||
|
||||
# select the requested path
|
||||
module.run_command(
|
||||
[UPDATE_ALTERNATIVES, '--set', name, path],
|
||||
check_rc=True
|
||||
)
|
||||
|
||||
module.exit_json(changed=True)
|
||||
except subprocess.CalledProcessError:
|
||||
e = get_exception()
|
||||
module.fail_json(msg=str(dir(cpe)))
|
||||
else:
|
||||
module.exit_json(changed=False)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
215
lib/ansible/modules/system/at.py
Normal file
215
lib/ansible/modules/system/at.py
Normal file
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2014, Richard Isaacson <richard.c.isaacson@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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'core',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: at
|
||||
short_description: Schedule the execution of a command or script file via the at command.
|
||||
description:
|
||||
- Use this module to schedule a command or script file to run once in the future.
|
||||
- All jobs are executed in the 'a' queue.
|
||||
version_added: "1.5"
|
||||
options:
|
||||
command:
|
||||
description:
|
||||
- A command to be executed in the future.
|
||||
required: false
|
||||
default: null
|
||||
script_file:
|
||||
description:
|
||||
- An existing script file to be executed in the future.
|
||||
required: false
|
||||
default: null
|
||||
count:
|
||||
description:
|
||||
- The count of units in the future to execute the command or script file.
|
||||
required: true
|
||||
units:
|
||||
description:
|
||||
- The type of units in the future to execute the command or script file.
|
||||
required: true
|
||||
choices: ["minutes", "hours", "days", "weeks"]
|
||||
state:
|
||||
description:
|
||||
- The state dictates if the command or script file should be evaluated as present(added) or absent(deleted).
|
||||
required: false
|
||||
choices: ["present", "absent"]
|
||||
default: "present"
|
||||
unique:
|
||||
description:
|
||||
- If a matching job is present a new job will not be added.
|
||||
required: false
|
||||
default: false
|
||||
requirements:
|
||||
- at
|
||||
author: "Richard Isaacson (@risaacson)"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Schedule a command to execute in 20 minutes as root.
|
||||
- at:
|
||||
command: "ls -d / > /dev/null"
|
||||
count: 20
|
||||
units: minutes
|
||||
|
||||
# Match a command to an existing job and delete the job.
|
||||
- at:
|
||||
command: "ls -d / > /dev/null"
|
||||
state: absent
|
||||
|
||||
# Schedule a command to execute in 20 minutes making sure it is unique in the queue.
|
||||
- at:
|
||||
command: "ls -d / > /dev/null"
|
||||
unique: true
|
||||
count: 20
|
||||
units: minutes
|
||||
'''
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
|
||||
def add_job(module, result, at_cmd, count, units, command, script_file):
|
||||
at_command = "%s -f %s now + %s %s" % (at_cmd, script_file, count, units)
|
||||
rc, out, err = module.run_command(at_command, check_rc=True)
|
||||
if command:
|
||||
os.unlink(script_file)
|
||||
result['changed'] = True
|
||||
|
||||
|
||||
def delete_job(module, result, at_cmd, command, script_file):
|
||||
for matching_job in get_matching_jobs(module, at_cmd, script_file):
|
||||
at_command = "%s -d %s" % (at_cmd, matching_job)
|
||||
rc, out, err = module.run_command(at_command, check_rc=True)
|
||||
result['changed'] = True
|
||||
if command:
|
||||
os.unlink(script_file)
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
def get_matching_jobs(module, at_cmd, script_file):
|
||||
matching_jobs = []
|
||||
|
||||
atq_cmd = module.get_bin_path('atq', True)
|
||||
|
||||
# Get list of job numbers for the user.
|
||||
atq_command = "%s" % atq_cmd
|
||||
rc, out, err = module.run_command(atq_command, check_rc=True)
|
||||
current_jobs = out.splitlines()
|
||||
if len(current_jobs) == 0:
|
||||
return matching_jobs
|
||||
|
||||
# Read script_file into a string.
|
||||
script_file_string = open(script_file).read().strip()
|
||||
|
||||
# Loop through the jobs.
|
||||
# If the script text is contained in a job add job number to list.
|
||||
for current_job in current_jobs:
|
||||
split_current_job = current_job.split()
|
||||
at_command = "%s -c %s" % (at_cmd, split_current_job[0])
|
||||
rc, out, err = module.run_command(at_command, check_rc=True)
|
||||
if script_file_string in out:
|
||||
matching_jobs.append(split_current_job[0])
|
||||
|
||||
# Return the list.
|
||||
return matching_jobs
|
||||
|
||||
|
||||
def create_tempfile(command):
|
||||
filed, script_file = tempfile.mkstemp(prefix='at')
|
||||
fileh = os.fdopen(filed, 'w')
|
||||
fileh.write(command)
|
||||
fileh.close()
|
||||
return script_file
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
command=dict(required=False,
|
||||
type='str'),
|
||||
script_file=dict(required=False,
|
||||
type='str'),
|
||||
count=dict(required=False,
|
||||
type='int'),
|
||||
units=dict(required=False,
|
||||
default=None,
|
||||
choices=['minutes', 'hours', 'days', 'weeks'],
|
||||
type='str'),
|
||||
state=dict(required=False,
|
||||
default='present',
|
||||
choices=['present', 'absent'],
|
||||
type='str'),
|
||||
unique=dict(required=False,
|
||||
default=False,
|
||||
type='bool')
|
||||
),
|
||||
mutually_exclusive=[['command', 'script_file']],
|
||||
required_one_of=[['command', 'script_file']],
|
||||
supports_check_mode=False
|
||||
)
|
||||
|
||||
at_cmd = module.get_bin_path('at', True)
|
||||
|
||||
command = module.params['command']
|
||||
script_file = module.params['script_file']
|
||||
count = module.params['count']
|
||||
units = module.params['units']
|
||||
state = module.params['state']
|
||||
unique = module.params['unique']
|
||||
|
||||
if (state == 'present') and (not count or not units):
|
||||
module.fail_json(msg="present state requires count and units")
|
||||
|
||||
result = {'state': state, 'changed': False}
|
||||
|
||||
# If command transform it into a script_file
|
||||
if command:
|
||||
script_file = create_tempfile(command)
|
||||
|
||||
# if absent remove existing and return
|
||||
if state == 'absent':
|
||||
delete_job(module, result, at_cmd, command, script_file)
|
||||
|
||||
# if unique if existing return unchanged
|
||||
if unique:
|
||||
if len(get_matching_jobs(module, at_cmd, script_file)) != 0:
|
||||
if command:
|
||||
os.unlink(script_file)
|
||||
module.exit_json(**result)
|
||||
|
||||
result['script_file'] = script_file
|
||||
result['count'] = count
|
||||
result['units'] = units
|
||||
|
||||
add_job(module, result, at_cmd, count, units, command, script_file)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
198
lib/ansible/modules/system/capabilities.py
Normal file
198
lib/ansible/modules/system/capabilities.py
Normal file
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2014, Nate Coraor <nate@bx.psu.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/>.
|
||||
#
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: capabilities
|
||||
short_description: Manage Linux capabilities
|
||||
description:
|
||||
- This module manipulates files privileges using the Linux capabilities(7) system.
|
||||
version_added: "1.6"
|
||||
options:
|
||||
path:
|
||||
description:
|
||||
- Specifies the path to the file to be managed.
|
||||
required: true
|
||||
default: null
|
||||
capability:
|
||||
description:
|
||||
- Desired capability to set (with operator and flags, if state is C(present)) or remove (if state is C(absent))
|
||||
required: true
|
||||
default: null
|
||||
aliases: [ 'cap' ]
|
||||
state:
|
||||
description:
|
||||
- Whether the entry should be present or absent in the file's capabilities.
|
||||
choices: [ "present", "absent" ]
|
||||
default: present
|
||||
notes:
|
||||
- The capabilities system will automatically transform operators and flags
|
||||
into the effective set, so (for example, cap_foo=ep will probably become
|
||||
cap_foo+ep). This module does not attempt to determine the final operator
|
||||
and flags to compare, so you will want to ensure that your capabilities
|
||||
argument matches the final capabilities.
|
||||
requirements: []
|
||||
author: "Nate Coraor (@natefoo)"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Set cap_sys_chroot+ep on /foo
|
||||
- capabilities:
|
||||
path: /foo
|
||||
capability: cap_sys_chroot+ep
|
||||
state: present
|
||||
|
||||
# Remove cap_net_bind_service from /bar
|
||||
- capabilities:
|
||||
path: /bar
|
||||
capability: cap_net_bind_service
|
||||
state: absent
|
||||
'''
|
||||
|
||||
|
||||
OPS = ( '=', '-', '+' )
|
||||
|
||||
# ==============================================================
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import re
|
||||
|
||||
class CapabilitiesModule(object):
|
||||
|
||||
platform = 'Linux'
|
||||
distribution = None
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
self.path = module.params['path'].strip()
|
||||
self.capability = module.params['capability'].strip().lower()
|
||||
self.state = module.params['state']
|
||||
self.getcap_cmd = module.get_bin_path('getcap', required=True)
|
||||
self.setcap_cmd = module.get_bin_path('setcap', required=True)
|
||||
self.capability_tup = self._parse_cap(self.capability, op_required=self.state=='present')
|
||||
|
||||
self.run()
|
||||
|
||||
def run(self):
|
||||
|
||||
current = self.getcap(self.path)
|
||||
caps = [ cap[0] for cap in current ]
|
||||
|
||||
if self.state == 'present' and self.capability_tup not in current:
|
||||
# need to add capability
|
||||
if self.module.check_mode:
|
||||
self.module.exit_json(changed=True, msg='capabilities changed')
|
||||
else:
|
||||
# remove from current cap list if it's already set (but op/flags differ)
|
||||
current = filter(lambda x: x[0] != self.capability_tup[0], current)
|
||||
# add new cap with correct op/flags
|
||||
current.append( self.capability_tup )
|
||||
self.module.exit_json(changed=True, state=self.state, msg='capabilities changed', stdout=self.setcap(self.path, current))
|
||||
elif self.state == 'absent' and self.capability_tup[0] in caps:
|
||||
# need to remove capability
|
||||
if self.module.check_mode:
|
||||
self.module.exit_json(changed=True, msg='capabilities changed')
|
||||
else:
|
||||
# remove from current cap list and then set current list
|
||||
current = filter(lambda x: x[0] != self.capability_tup[0], current)
|
||||
self.module.exit_json(changed=True, state=self.state, msg='capabilities changed', stdout=self.setcap(self.path, current))
|
||||
self.module.exit_json(changed=False, state=self.state)
|
||||
|
||||
def getcap(self, path):
|
||||
rval = []
|
||||
cmd = "%s -v %s" % (self.getcap_cmd, path)
|
||||
rc, stdout, stderr = self.module.run_command(cmd)
|
||||
# If file xattrs are set but no caps are set the output will be:
|
||||
# '/foo ='
|
||||
# If file xattrs are unset the output will be:
|
||||
# '/foo'
|
||||
# If the file does not eixst the output will be (with rc == 0...):
|
||||
# '/foo (No such file or directory)'
|
||||
if rc != 0 or (stdout.strip() != path and stdout.count(' =') != 1):
|
||||
self.module.fail_json(msg="Unable to get capabilities of %s" % path, stdout=stdout.strip(), stderr=stderr)
|
||||
if stdout.strip() != path:
|
||||
caps = stdout.split(' =')[1].strip().split()
|
||||
for cap in caps:
|
||||
cap = cap.lower()
|
||||
# getcap condenses capabilities with the same op/flags into a
|
||||
# comma-separated list, so we have to parse that
|
||||
if ',' in cap:
|
||||
cap_group = cap.split(',')
|
||||
cap_group[-1], op, flags = self._parse_cap(cap_group[-1])
|
||||
for subcap in cap_group:
|
||||
rval.append( ( subcap, op, flags ) )
|
||||
else:
|
||||
rval.append(self._parse_cap(cap))
|
||||
return rval
|
||||
|
||||
def setcap(self, path, caps):
|
||||
caps = ' '.join([ ''.join(cap) for cap in caps ])
|
||||
cmd = "%s '%s' %s" % (self.setcap_cmd, caps, path)
|
||||
rc, stdout, stderr = self.module.run_command(cmd)
|
||||
if rc != 0:
|
||||
self.module.fail_json(msg="Unable to set capabilities of %s" % path, stdout=stdout, stderr=stderr)
|
||||
else:
|
||||
return stdout
|
||||
|
||||
def _parse_cap(self, cap, op_required=True):
|
||||
opind = -1
|
||||
try:
|
||||
i = 0
|
||||
while opind == -1:
|
||||
opind = cap.find(OPS[i])
|
||||
i += 1
|
||||
except:
|
||||
if op_required:
|
||||
self.module.fail_json(msg="Couldn't find operator (one of: %s)" % str(OPS))
|
||||
else:
|
||||
return (cap, None, None)
|
||||
op = cap[opind]
|
||||
cap, flags = cap.split(op)
|
||||
return (cap, op, flags)
|
||||
|
||||
# ==============================================================
|
||||
# main
|
||||
|
||||
def main():
|
||||
|
||||
# defining module
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
path = dict(aliases=['key'], required=True),
|
||||
capability = dict(aliases=['cap'], required=True),
|
||||
state = dict(default='present', choices=['present', 'absent']),
|
||||
),
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
CapabilitiesModule(module)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
445
lib/ansible/modules/system/cronvar.py
Normal file
445
lib/ansible/modules/system/cronvar.py
Normal file
@@ -0,0 +1,445 @@
|
||||
#!/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/>.
|
||||
#
|
||||
# Cronvar Plugin: The goal of this plugin is to provide an indempotent
|
||||
# method for set cron variable values. It should play well with the
|
||||
# existing cron module as well as allow for manually added variables.
|
||||
# Each variable entered will be preceded with a comment describing the
|
||||
# variable so that it can be found later. This is required to be
|
||||
# present in order for this plugin to find/modify the variable
|
||||
#
|
||||
# This module is based on the crontab module.
|
||||
#
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = """
|
||||
---
|
||||
module: cronvar
|
||||
short_description: Manage variables in crontabs
|
||||
description:
|
||||
- Use this module to manage crontab variables. This module allows
|
||||
you to create, update, or delete cron variable definitions.
|
||||
version_added: "2.0"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the crontab variable.
|
||||
default: null
|
||||
required: true
|
||||
value:
|
||||
description:
|
||||
- The value to set this variable to. Required if state=present.
|
||||
required: false
|
||||
default: null
|
||||
insertafter:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- Used with C(state=present). If specified, the variable will be inserted
|
||||
after the variable specified.
|
||||
insertbefore:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- Used with C(state=present). If specified, the variable will be inserted
|
||||
just before the variable specified.
|
||||
state:
|
||||
description:
|
||||
- Whether to ensure that the variable is present or absent.
|
||||
required: false
|
||||
default: present
|
||||
choices: [ "present", "absent" ]
|
||||
user:
|
||||
description:
|
||||
- The specific user whose crontab should be modified.
|
||||
required: false
|
||||
default: root
|
||||
cron_file:
|
||||
description:
|
||||
- If specified, uses this file instead of an individual user's crontab.
|
||||
Without a leading /, this is assumed to be in /etc/cron.d. With a leading
|
||||
/, this is taken as absolute.
|
||||
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
|
||||
requirements:
|
||||
- cron
|
||||
author: "Doug Luce (@dougluce)"
|
||||
"""
|
||||
|
||||
EXAMPLES = '''
|
||||
# Ensure a variable exists.
|
||||
# Creates an entry like "EMAIL=doug@ansibmod.con.com"
|
||||
- cronvar:
|
||||
name: EMAIL
|
||||
value: doug@ansibmod.con.com
|
||||
|
||||
# Make sure a variable is gone. This will remove any variable named
|
||||
# "LEGACY"
|
||||
- cronvar:
|
||||
name: LEGACY
|
||||
state: absent
|
||||
|
||||
# Adds a variable to a file under /etc/cron.d
|
||||
- cronvar:
|
||||
name: LOGFILE
|
||||
value: /var/log/yum-autoupdate.log
|
||||
user: root
|
||||
cron_file: ansible_yum-autoupdate
|
||||
'''
|
||||
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import platform
|
||||
import pipes
|
||||
import shlex
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
|
||||
CRONCMD = "/usr/bin/crontab"
|
||||
|
||||
class CronVarError(Exception):
|
||||
pass
|
||||
|
||||
class CronVar(object):
|
||||
"""
|
||||
CronVar object to write variables to crontabs.
|
||||
|
||||
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
|
||||
if self.user is None:
|
||||
self.user = 'root'
|
||||
self.lines = None
|
||||
self.wordchars = ''.join(chr(x) for x in range(128) if chr(x) not in ('=', "'", '"', ))
|
||||
|
||||
if cron_file:
|
||||
self.cron_file = ""
|
||||
if os.path.isabs(cron_file):
|
||||
self.cron_file = cron_file
|
||||
else:
|
||||
self.cron_file = os.path.join('/etc/cron.d', 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 = get_exception()
|
||||
# cron file does not exist
|
||||
return
|
||||
except:
|
||||
raise CronVarError("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 CronVarError("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):
|
||||
self.module.debug('ansible: "%s"' % message)
|
||||
|
||||
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 remove_variable_file(self):
|
||||
try:
|
||||
os.unlink(self.cron_file)
|
||||
return True
|
||||
except OSError:
|
||||
e = get_exception()
|
||||
# cron file does not exist
|
||||
return False
|
||||
except:
|
||||
raise CronVarError("Unexpected error:", sys.exc_info()[0])
|
||||
|
||||
def parse_for_var(self, line):
|
||||
lexer = shlex.shlex(line)
|
||||
lexer.wordchars = self.wordchars
|
||||
varname = lexer.get_token()
|
||||
is_env_var = lexer.get_token() == '='
|
||||
value = ''.join(lexer)
|
||||
if is_env_var:
|
||||
return (varname, value)
|
||||
raise CronVarError("Not a variable.")
|
||||
|
||||
def find_variable(self, name):
|
||||
comment = None
|
||||
for l in self.lines:
|
||||
try:
|
||||
(varname, value) = self.parse_for_var(l)
|
||||
if varname == name:
|
||||
return value
|
||||
except CronVarError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def get_var_names(self):
|
||||
var_names = []
|
||||
for l in self.lines:
|
||||
try:
|
||||
(var_name, _) = self.parse_for_var(l)
|
||||
var_names.append(var_name)
|
||||
except CronVarError:
|
||||
pass
|
||||
return var_names
|
||||
|
||||
def add_variable(self, name, value, insertbefore, insertafter):
|
||||
if insertbefore is None and insertafter is None:
|
||||
# Add the variable to the top of the file.
|
||||
self.lines.insert(0, "%s=%s" % (name, value))
|
||||
else:
|
||||
newlines = []
|
||||
for l in self.lines:
|
||||
try:
|
||||
(varname, _) = self.parse_for_var(l) # Throws if not a var line
|
||||
if varname == insertbefore:
|
||||
newlines.append("%s=%s" % (name, value))
|
||||
newlines.append(l)
|
||||
elif varname == insertafter:
|
||||
newlines.append(l)
|
||||
newlines.append("%s=%s" % (name, value))
|
||||
else:
|
||||
raise CronVarError # Append.
|
||||
except CronVarError:
|
||||
newlines.append(l)
|
||||
|
||||
self.lines = newlines
|
||||
|
||||
def remove_variable(self, name):
|
||||
self.update_variable(name, None, remove=True)
|
||||
|
||||
def update_variable(self, name, value, remove=False):
|
||||
newlines = []
|
||||
for l in self.lines:
|
||||
try:
|
||||
(varname, _) = self.parse_for_var(l) # Throws if not a var line
|
||||
if varname != name:
|
||||
raise CronVarError # Append.
|
||||
if not remove:
|
||||
newlines.append("%s=%s" % (name, value))
|
||||
except CronVarError:
|
||||
newlines.append(l)
|
||||
|
||||
self.lines = newlines
|
||||
|
||||
def render(self):
|
||||
"""
|
||||
Render a proper crontab
|
||||
"""
|
||||
result = '\n'.join(self.lines)
|
||||
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:
|
||||
#
|
||||
# - cronvar: name="SHELL" value="/bin/bash"
|
||||
#
|
||||
# - name: Set the email
|
||||
# cronvar: name="EMAILTO" value="doug@ansibmod.con.com"
|
||||
#
|
||||
# - name: Get rid of the old new host variable
|
||||
# cronvar: name="NEW_HOST" state=absent
|
||||
#
|
||||
# Would produce:
|
||||
# SHELL = /bin/bash
|
||||
# EMAILTO = doug@ansibmod.con.com
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
name=dict(required=True),
|
||||
value=dict(required=False),
|
||||
user=dict(required=False),
|
||||
cron_file=dict(required=False),
|
||||
insertafter=dict(default=None),
|
||||
insertbefore=dict(default=None),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
backup=dict(default=False, type='bool'),
|
||||
),
|
||||
mutually_exclusive=[['insertbefore', 'insertafter']],
|
||||
supports_check_mode=False,
|
||||
)
|
||||
|
||||
name = module.params['name']
|
||||
value = module.params['value']
|
||||
user = module.params['user']
|
||||
cron_file = module.params['cron_file']
|
||||
insertafter = module.params['insertafter']
|
||||
insertbefore = module.params['insertbefore']
|
||||
state = module.params['state']
|
||||
backup = module.params['backup']
|
||||
ensure_present = 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(int('022',8))
|
||||
cronvar = CronVar(module, user, cron_file)
|
||||
|
||||
module.debug('cronvar instantiated - name: "%s"' % name)
|
||||
|
||||
# --- user input validation ---
|
||||
|
||||
if name is None and ensure_present:
|
||||
module.fail_json(msg="You must specify 'name' to insert a new cron variabale")
|
||||
|
||||
if value is None and ensure_present:
|
||||
module.fail_json(msg="You must specify 'value' to insert a new cron variable")
|
||||
|
||||
if name is None and not ensure_present:
|
||||
module.fail_json(msg="You must specify 'name' to remove a cron variable")
|
||||
|
||||
# if requested make a backup before making a change
|
||||
if backup:
|
||||
(_, backup_file) = tempfile.mkstemp(prefix='cronvar')
|
||||
cronvar.write(backup_file)
|
||||
|
||||
if cronvar.cron_file and not name and not ensure_present:
|
||||
changed = cronvar.remove_job_file()
|
||||
module.exit_json(changed=changed, cron_file=cron_file, state=state)
|
||||
|
||||
old_value = cronvar.find_variable(name)
|
||||
|
||||
if ensure_present:
|
||||
if old_value is None:
|
||||
cronvar.add_variable(name, value, insertbefore, insertafter)
|
||||
changed = True
|
||||
elif old_value != value:
|
||||
cronvar.update_variable(name, value)
|
||||
changed = True
|
||||
else:
|
||||
if old_value is not None:
|
||||
cronvar.remove_variable(name)
|
||||
changed = True
|
||||
|
||||
res_args = {
|
||||
"vars": cronvar.get_var_names(),
|
||||
"changed": changed
|
||||
}
|
||||
|
||||
if changed:
|
||||
cronvar.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 cronvar task.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
378
lib/ansible/modules/system/crypttab.py
Normal file
378
lib/ansible/modules/system/crypttab.py
Normal file
@@ -0,0 +1,378 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2014, Steve <yo@groks.org>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: crypttab
|
||||
short_description: Encrypted Linux block devices
|
||||
description:
|
||||
- Control Linux encrypted block devices that are set up during system boot in C(/etc/crypttab).
|
||||
version_added: "1.9"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the encrypted block device as it appears in the C(/etc/crypttab) file, or
|
||||
optionaly prefixed with C(/dev/mapper/), as it appears in the filesystem. I(/dev/mapper/)
|
||||
will be stripped from I(name).
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
state:
|
||||
description:
|
||||
- Use I(present) to add a line to C(/etc/crypttab) or update it's definition
|
||||
if already present. Use I(absent) to remove a line with matching I(name).
|
||||
Use I(opts_present) to add options to those already present; options with
|
||||
different values will be updated. Use I(opts_absent) to remove options from
|
||||
the existing set.
|
||||
required: true
|
||||
choices: [ "present", "absent", "opts_present", "opts_absent"]
|
||||
default: null
|
||||
backing_device:
|
||||
description:
|
||||
- Path to the underlying block device or file, or the UUID of a block-device
|
||||
prefixed with I(UUID=)
|
||||
required: false
|
||||
default: null
|
||||
password:
|
||||
description:
|
||||
- Encryption password, the path to a file containing the password, or
|
||||
'none' or '-' if the password should be entered at boot.
|
||||
required: false
|
||||
default: "none"
|
||||
opts:
|
||||
description:
|
||||
- A comma-delimited list of options. See C(crypttab(5) ) for details.
|
||||
required: false
|
||||
path:
|
||||
description:
|
||||
- Path to file to use instead of C(/etc/crypttab). This might be useful
|
||||
in a chroot environment.
|
||||
required: false
|
||||
default: /etc/crypttab
|
||||
|
||||
notes: []
|
||||
requirements: []
|
||||
author: "Steve (@groks)"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
||||
# Since column is a special character in YAML, if your string contains a column, it's better to use quotes around the string
|
||||
- name: Set the options explicitly a device which must already exist
|
||||
crypttab:
|
||||
name: luks-home
|
||||
state: present
|
||||
opts: 'discard,cipher=aes-cbc-essiv:sha256'
|
||||
|
||||
- name: Add the 'discard' option to any existing options for all devices
|
||||
crypttab:
|
||||
name: '{{ item.device }}'
|
||||
state: opts_present
|
||||
opts: discard
|
||||
with_items: '{{ ansible_mounts }}'
|
||||
when: '/dev/mapper/luks-' in {{ item.device }}
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
name = dict(required=True),
|
||||
state = dict(required=True, choices=['present', 'absent', 'opts_present', 'opts_absent']),
|
||||
backing_device = dict(default=None),
|
||||
password = dict(default=None, type='path'),
|
||||
opts = dict(default=None),
|
||||
path = dict(default='/etc/crypttab', type='path')
|
||||
),
|
||||
supports_check_mode = True
|
||||
)
|
||||
|
||||
backing_device = module.params['backing_device']
|
||||
password = module.params['password']
|
||||
opts = module.params['opts']
|
||||
state = module.params['state']
|
||||
path = module.params['path']
|
||||
name = module.params['name']
|
||||
if name.startswith('/dev/mapper/'):
|
||||
name = name[len('/dev/mapper/'):]
|
||||
|
||||
|
||||
if state != 'absent' and backing_device is None and password is None and opts is None:
|
||||
module.fail_json(msg="expected one or more of 'backing_device', 'password' or 'opts'",
|
||||
**module.params)
|
||||
|
||||
if 'opts' in state and (backing_device is not None or password is not None):
|
||||
module.fail_json(msg="cannot update 'backing_device' or 'password' when state=%s" % state,
|
||||
**module.params)
|
||||
|
||||
for arg_name, arg in (('name', name),
|
||||
('backing_device', backing_device),
|
||||
('password', password),
|
||||
('opts', opts)):
|
||||
if (arg is not None
|
||||
and (' ' in arg or '\t' in arg or arg == '')):
|
||||
module.fail_json(msg="invalid '%s': contains white space or is empty" % arg_name,
|
||||
**module.params)
|
||||
|
||||
try:
|
||||
crypttab = Crypttab(path)
|
||||
existing_line = crypttab.match(name)
|
||||
except Exception:
|
||||
e = get_exception()
|
||||
module.fail_json(msg="failed to open and parse crypttab file: %s" % e,
|
||||
**module.params)
|
||||
|
||||
if 'present' in state and existing_line is None and backing_device is None:
|
||||
module.fail_json(msg="'backing_device' required to add a new entry",
|
||||
**module.params)
|
||||
|
||||
changed, reason = False, '?'
|
||||
|
||||
if state == 'absent':
|
||||
if existing_line is not None:
|
||||
changed, reason = existing_line.remove()
|
||||
|
||||
elif state == 'present':
|
||||
if existing_line is not None:
|
||||
changed, reason = existing_line.set(backing_device, password, opts)
|
||||
else:
|
||||
changed, reason = crypttab.add(Line(None, name, backing_device, password, opts))
|
||||
|
||||
elif state == 'opts_present':
|
||||
if existing_line is not None:
|
||||
changed, reason = existing_line.opts.add(opts)
|
||||
else:
|
||||
changed, reason = crypttab.add(Line(None, name, backing_device, password, opts))
|
||||
|
||||
elif state == 'opts_absent':
|
||||
if existing_line is not None:
|
||||
changed, reason = existing_line.opts.remove(opts)
|
||||
|
||||
|
||||
if changed and not module.check_mode:
|
||||
try:
|
||||
f = open(path, 'wb')
|
||||
f.write(str(crypttab))
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
module.exit_json(changed=changed, msg=reason, **module.params)
|
||||
|
||||
|
||||
class Crypttab(object):
|
||||
|
||||
_lines = []
|
||||
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
if not os.path.exists(path):
|
||||
if not os.path.exists(os.path.dirname(path)):
|
||||
os.makedirs(os.path.dirname(path))
|
||||
open(path,'a').close()
|
||||
|
||||
try:
|
||||
f = open(path, 'r')
|
||||
for line in f.readlines():
|
||||
self._lines.append(Line(line))
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
def add(self, line):
|
||||
self._lines.append(line)
|
||||
return True, 'added line'
|
||||
|
||||
def lines(self):
|
||||
for line in self._lines:
|
||||
if line.valid():
|
||||
yield line
|
||||
|
||||
def match(self, name):
|
||||
for line in self.lines():
|
||||
if line.name == name:
|
||||
return line
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
lines = []
|
||||
for line in self._lines:
|
||||
lines.append(str(line))
|
||||
crypttab = '\n'.join(lines)
|
||||
if len(crypttab) == 0:
|
||||
crypttab += '\n'
|
||||
if crypttab[-1] != '\n':
|
||||
crypttab += '\n'
|
||||
return crypttab
|
||||
|
||||
|
||||
class Line(object):
|
||||
|
||||
def __init__(self, line=None, name=None, backing_device=None, password=None, opts=None):
|
||||
self.line = line
|
||||
self.name = name
|
||||
self.backing_device = backing_device
|
||||
self.password = password
|
||||
self.opts = Options(opts)
|
||||
|
||||
if line is not None:
|
||||
if self._line_valid(line):
|
||||
self.name, backing_device, password, opts = self._split_line(line)
|
||||
|
||||
self.set(backing_device, password, opts)
|
||||
|
||||
def set(self, backing_device, password, opts):
|
||||
changed = False
|
||||
|
||||
if backing_device is not None and self.backing_device != backing_device:
|
||||
self.backing_device = backing_device
|
||||
changed = True
|
||||
|
||||
if password is not None and self.password != password:
|
||||
self.password = password
|
||||
changed = True
|
||||
|
||||
if opts is not None:
|
||||
opts = Options(opts)
|
||||
if opts != self.opts:
|
||||
self.opts = opts
|
||||
changed = True
|
||||
|
||||
return changed, 'updated line'
|
||||
|
||||
def _line_valid(self, line):
|
||||
if not line.strip() or line.startswith('#') or len(line.split()) not in (2, 3, 4):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _split_line(self, line):
|
||||
fields = line.split()
|
||||
try:
|
||||
field2 = fields[2]
|
||||
except IndexError:
|
||||
field2 = None
|
||||
try:
|
||||
field3 = fields[3]
|
||||
except IndexError:
|
||||
field3 = None
|
||||
|
||||
return (fields[0],
|
||||
fields[1],
|
||||
field2,
|
||||
field3)
|
||||
|
||||
def remove(self):
|
||||
self.line, self.name, self.backing_device = '', None, None
|
||||
return True, 'removed line'
|
||||
|
||||
def valid(self):
|
||||
if self.name is not None and self.backing_device is not None:
|
||||
return True
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
if self.valid():
|
||||
fields = [self.name, self.backing_device]
|
||||
if self.password is not None or self.opts:
|
||||
if self.password is not None:
|
||||
fields.append(self.password)
|
||||
else:
|
||||
self.password('none')
|
||||
if self.opts:
|
||||
fields.append(str(self.opts))
|
||||
return ' '.join(fields)
|
||||
return self.line
|
||||
|
||||
|
||||
class Options(dict):
|
||||
"""opts_string looks like: 'discard,foo=bar,baz=greeble' """
|
||||
|
||||
def __init__(self, opts_string):
|
||||
super(Options, self).__init__()
|
||||
self.itemlist = []
|
||||
if opts_string is not None:
|
||||
for opt in opts_string.split(','):
|
||||
kv = opt.split('=')
|
||||
if len(kv) > 1:
|
||||
k, v = (kv[0], kv[1])
|
||||
else:
|
||||
k, v = (kv[0], None)
|
||||
self[k] = v
|
||||
|
||||
def add(self, opts_string):
|
||||
changed = False
|
||||
for k, v in Options(opts_string).items():
|
||||
if k in self:
|
||||
if self[k] != v:
|
||||
changed = True
|
||||
else:
|
||||
changed = True
|
||||
self[k] = v
|
||||
return changed, 'updated options'
|
||||
|
||||
def remove(self, opts_string):
|
||||
changed = False
|
||||
for k in Options(opts_string):
|
||||
if k in self:
|
||||
del self[k]
|
||||
changed = True
|
||||
return changed, 'removed options'
|
||||
|
||||
def keys(self):
|
||||
return self.itemlist
|
||||
|
||||
def values(self):
|
||||
return [self[key] for key in self]
|
||||
|
||||
def items(self):
|
||||
return [(key, self[key]) for key in self]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.itemlist)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key not in self:
|
||||
self.itemlist.append(key)
|
||||
super(Options, self).__setitem__(key, value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
self.itemlist.remove(key)
|
||||
super(Options, self).__delitem__(key)
|
||||
|
||||
def __ne__(self, obj):
|
||||
return not (isinstance(obj, Options)
|
||||
and sorted(self.items()) == sorted(obj.items()))
|
||||
|
||||
def __str__(self):
|
||||
ret = []
|
||||
for k, v in self.items():
|
||||
if v is None:
|
||||
ret.append(k)
|
||||
else:
|
||||
ret.append('%s=%s' % (k, v))
|
||||
return ','.join(ret)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
196
lib/ansible/modules/system/debconf.py
Normal file
196
lib/ansible/modules/system/debconf.py
Normal file
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Ansible module to configure .deb packages.
|
||||
(c) 2014, Brian Coca <briancoca+ansible@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/>.
|
||||
"""
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['stableinterface'],
|
||||
'supported_by': 'core',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: debconf
|
||||
short_description: Configure a .deb package
|
||||
description:
|
||||
- Configure a .deb package using debconf-set-selections. Or just query
|
||||
existing selections.
|
||||
version_added: "1.6"
|
||||
notes:
|
||||
- This module requires the command line debconf tools.
|
||||
- A number of questions have to be answered (depending on the package).
|
||||
Use 'debconf-show <package>' on any Debian or derivative with the package
|
||||
installed to see questions/settings available.
|
||||
- Some distros will always record tasks involving the setting of passwords as changed. This is due to debconf-get-selections masking passwords.
|
||||
requirements: [ debconf, debconf-utils ]
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of package to configure.
|
||||
required: true
|
||||
default: null
|
||||
aliases: ['pkg']
|
||||
question:
|
||||
description:
|
||||
- A debconf configuration setting
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['setting', 'selection']
|
||||
vtype:
|
||||
description:
|
||||
- The type of the value supplied.
|
||||
- C(seen) was added in 2.2.
|
||||
required: false
|
||||
default: null
|
||||
choices: [string, password, boolean, select, multiselect, note, error, title, text, seen]
|
||||
value:
|
||||
description:
|
||||
- Value to set the configuration to
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['answer']
|
||||
unseen:
|
||||
description:
|
||||
- Do not set 'seen' flag when pre-seeding
|
||||
required: false
|
||||
default: False
|
||||
author: "Brian Coca (@bcoca)"
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Set default locale to fr_FR.UTF-8
|
||||
- debconf:
|
||||
name: locales
|
||||
question: locales/default_environment_locale
|
||||
value: fr_FR.UTF-8
|
||||
vtype: select
|
||||
|
||||
# set to generate locales:
|
||||
- debconf:
|
||||
name: locales
|
||||
question: locales/locales_to_be_generated
|
||||
value: en_US.UTF-8 UTF-8, fr_FR.UTF-8 UTF-8
|
||||
vtype: multiselect
|
||||
|
||||
# Accept oracle license
|
||||
- debconf:
|
||||
name: oracle-java7-installer
|
||||
question: shared/accepted-oracle-license-v1-1
|
||||
value: true
|
||||
vtype: select
|
||||
|
||||
# Specifying package you can register/return the list of questions and current values
|
||||
- debconf:
|
||||
name: tzdata
|
||||
'''
|
||||
|
||||
def get_selections(module, pkg):
|
||||
cmd = [module.get_bin_path('debconf-show', True), pkg]
|
||||
rc, out, err = module.run_command(' '.join(cmd))
|
||||
|
||||
if rc != 0:
|
||||
module.fail_json(msg=err)
|
||||
|
||||
selections = {}
|
||||
|
||||
for line in out.splitlines():
|
||||
(key, value) = line.split(':', 1)
|
||||
selections[ key.strip('*').strip() ] = value.strip()
|
||||
|
||||
return selections
|
||||
|
||||
|
||||
def set_selection(module, pkg, question, vtype, value, unseen):
|
||||
|
||||
setsel = module.get_bin_path('debconf-set-selections', True)
|
||||
cmd = [setsel]
|
||||
if unseen:
|
||||
cmd.append('-u')
|
||||
|
||||
if vtype == 'boolean':
|
||||
if value == 'True':
|
||||
value = 'true'
|
||||
elif value == 'False':
|
||||
value = 'false'
|
||||
data = ' '.join([pkg, question, vtype, value])
|
||||
|
||||
return module.run_command(cmd, data=data)
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
name = dict(required=True, aliases=['pkg'], type='str'),
|
||||
question = dict(required=False, aliases=['setting', 'selection'], type='str'),
|
||||
vtype = dict(required=False, type='str', choices=['string', 'password', 'boolean', 'select', 'multiselect', 'note', 'error', 'title', 'text', 'seen']),
|
||||
value = dict(required=False, type='str', aliases=['answer']),
|
||||
unseen = dict(required=False, type='bool'),
|
||||
),
|
||||
required_together = ( ['question','vtype', 'value'],),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
#TODO: enable passing array of options and/or debconf file from get-selections dump
|
||||
pkg = module.params["name"]
|
||||
question = module.params["question"]
|
||||
vtype = module.params["vtype"]
|
||||
value = module.params["value"]
|
||||
unseen = module.params["unseen"]
|
||||
|
||||
prev = get_selections(module, pkg)
|
||||
|
||||
changed = False
|
||||
msg = ""
|
||||
|
||||
if question is not None:
|
||||
if vtype is None or value is None:
|
||||
module.fail_json(msg="when supplying a question you must supply a valid vtype and value")
|
||||
|
||||
if not question in prev or prev[question] != value:
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
if not module.check_mode:
|
||||
rc, msg, e = set_selection(module, pkg, question, vtype, value, unseen)
|
||||
if rc:
|
||||
module.fail_json(msg=e)
|
||||
|
||||
curr = { question: value }
|
||||
if question in prev:
|
||||
prev = {question: prev[question]}
|
||||
else:
|
||||
prev[question] = ''
|
||||
if module._diff:
|
||||
after = prev.copy()
|
||||
after.update(curr)
|
||||
diff_dict = {'before': prev, 'after': after}
|
||||
else:
|
||||
diff_dict = {}
|
||||
|
||||
module.exit_json(changed=changed, msg=msg, current=curr, previous=prev, diff=diff_dict)
|
||||
|
||||
module.exit_json(changed=changed, msg=msg, current=prev)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
65
lib/ansible/modules/system/facter.py
Normal file
65
lib/ansible/modules/system/facter.py
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/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/>.
|
||||
#
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: facter
|
||||
short_description: Runs the discovery program I(facter) on the remote system
|
||||
description:
|
||||
- Runs the I(facter) discovery program
|
||||
(U(https://github.com/puppetlabs/facter)) on the remote system, returning
|
||||
JSON data that can be useful for inventory purposes.
|
||||
version_added: "0.2"
|
||||
options: {}
|
||||
notes: []
|
||||
requirements: [ "facter", "ruby-json" ]
|
||||
author:
|
||||
- "Ansible Core Team"
|
||||
- "Michael DeHaan"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Example command-line invocation
|
||||
ansible www.example.net -m facter
|
||||
'''
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict()
|
||||
)
|
||||
|
||||
facter_path = module.get_bin_path('facter', opt_dirs=['/opt/puppetlabs/bin'])
|
||||
|
||||
cmd = [facter_path, "--puppet", "--json"]
|
||||
|
||||
rc, out, err = module.run_command(cmd, check_rc=True)
|
||||
module.exit_json(**json.loads(out))
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
265
lib/ansible/modules/system/filesystem.py
Normal file
265
lib/ansible/modules/system/filesystem.py
Normal file
@@ -0,0 +1,265 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2013, Alexander Bulimov <lazywolf0@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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
author: "Alexander Bulimov (@abulimov)"
|
||||
module: filesystem
|
||||
short_description: Makes file system on block device
|
||||
description:
|
||||
- This module creates file system.
|
||||
version_added: "1.2"
|
||||
options:
|
||||
fstype:
|
||||
description:
|
||||
- File System type to be created.
|
||||
- reiserfs support was added in 2.2.
|
||||
required: true
|
||||
dev:
|
||||
description:
|
||||
- Target block device.
|
||||
required: true
|
||||
force:
|
||||
choices: [ "yes", "no" ]
|
||||
default: "no"
|
||||
description:
|
||||
- If yes, allows to create new filesystem on devices that already has filesystem.
|
||||
required: false
|
||||
resizefs:
|
||||
choices: [ "yes", "no" ]
|
||||
default: "no"
|
||||
description:
|
||||
- If yes, if the block device and filessytem size differ, grow the filesystem into the space. Note, XFS Will only grow if mounted.
|
||||
required: false
|
||||
version_added: "2.0"
|
||||
opts:
|
||||
description:
|
||||
- List of options to be passed to mkfs command.
|
||||
notes:
|
||||
- uses mkfs command
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create a ext2 filesystem on /dev/sdb1.
|
||||
- filesystem:
|
||||
fstype: ext2
|
||||
dev: /dev/sdb1
|
||||
|
||||
# Create a ext4 filesystem on /dev/sdb1 and check disk blocks.
|
||||
- filesystem:
|
||||
fstype: ext4
|
||||
dev: /dev/sdb1
|
||||
opts: -cc
|
||||
'''
|
||||
|
||||
def _get_dev_size(dev, module):
|
||||
""" Return size in bytes of device. Returns int """
|
||||
blockdev_cmd = module.get_bin_path("blockdev", required=True)
|
||||
rc, devsize_in_bytes, err = module.run_command("%s %s %s" % (blockdev_cmd, "--getsize64", dev))
|
||||
return int(devsize_in_bytes)
|
||||
|
||||
|
||||
def _get_fs_size(fssize_cmd, dev, module):
|
||||
""" Return size in bytes of filesystem on device. Returns int """
|
||||
cmd = module.get_bin_path(fssize_cmd, required=True)
|
||||
if 'tune2fs' == fssize_cmd:
|
||||
# Get Block count and Block size
|
||||
rc, size, err = module.run_command("%s %s %s" % (cmd, '-l', dev))
|
||||
if rc == 0:
|
||||
for line in size.splitlines():
|
||||
if 'Block count:' in line:
|
||||
block_count = int(line.split(':')[1].strip())
|
||||
elif 'Block size:' in line:
|
||||
block_size = int(line.split(':')[1].strip())
|
||||
break
|
||||
else:
|
||||
module.fail_json(msg="Failed to get block count and block size of %s with %s" % (dev, cmd), rc=rc, err=err )
|
||||
elif 'xfs_info' == fssize_cmd:
|
||||
# Get Block count and Block size
|
||||
rc, size, err = module.run_command("%s %s" % (cmd, dev))
|
||||
if rc == 0:
|
||||
for line in size.splitlines():
|
||||
#if 'data' in line:
|
||||
if 'data ' in line:
|
||||
block_size = int(line.split('=')[2].split()[0])
|
||||
block_count = int(line.split('=')[3].split(',')[0])
|
||||
break
|
||||
else:
|
||||
module.fail_json(msg="Failed to get block count and block size of %s with %s" % (dev, cmd), rc=rc, err=err )
|
||||
elif 'btrfs' == fssize_cmd:
|
||||
#ToDo
|
||||
# There is no way to get the blocksize and blockcount for btrfs filesystems
|
||||
block_size = 1
|
||||
block_count = 1
|
||||
|
||||
|
||||
return block_size*block_count
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
fstype=dict(required=True, aliases=['type']),
|
||||
dev=dict(required=True, aliases=['device']),
|
||||
opts=dict(),
|
||||
force=dict(type='bool', default='no'),
|
||||
resizefs=dict(type='bool', default='no'),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
# There is no "single command" to manipulate filesystems, so we map them all out and their options
|
||||
fs_cmd_map = {
|
||||
'ext2' : {
|
||||
'mkfs' : 'mkfs.ext2',
|
||||
'grow' : 'resize2fs',
|
||||
'grow_flag' : None,
|
||||
'force_flag' : '-F',
|
||||
'fsinfo': 'tune2fs',
|
||||
},
|
||||
'ext3' : {
|
||||
'mkfs' : 'mkfs.ext3',
|
||||
'grow' : 'resize2fs',
|
||||
'grow_flag' : None,
|
||||
'force_flag' : '-F',
|
||||
'fsinfo': 'tune2fs',
|
||||
},
|
||||
'ext4' : {
|
||||
'mkfs' : 'mkfs.ext4',
|
||||
'grow' : 'resize2fs',
|
||||
'grow_flag' : None,
|
||||
'force_flag' : '-F',
|
||||
'fsinfo': 'tune2fs',
|
||||
},
|
||||
'reiserfs' : {
|
||||
'mkfs' : 'mkfs.reiserfs',
|
||||
'grow' : 'resize_reiserfs',
|
||||
'grow_flag' : None,
|
||||
'force_flag' : '-f',
|
||||
'fsinfo': 'reiserfstune',
|
||||
},
|
||||
'ext4dev' : {
|
||||
'mkfs' : 'mkfs.ext4',
|
||||
'grow' : 'resize2fs',
|
||||
'grow_flag' : None,
|
||||
'force_flag' : '-F',
|
||||
'fsinfo': 'tune2fs',
|
||||
},
|
||||
'xfs' : {
|
||||
'mkfs' : 'mkfs.xfs',
|
||||
'grow' : 'xfs_growfs',
|
||||
'grow_flag' : None,
|
||||
'force_flag' : '-f',
|
||||
'fsinfo': 'xfs_info',
|
||||
},
|
||||
'btrfs' : {
|
||||
'mkfs' : 'mkfs.btrfs',
|
||||
'grow' : 'btrfs',
|
||||
'grow_flag' : 'filesystem resize',
|
||||
'force_flag' : '-f',
|
||||
'fsinfo': 'btrfs',
|
||||
}
|
||||
}
|
||||
|
||||
dev = module.params['dev']
|
||||
fstype = module.params['fstype']
|
||||
opts = module.params['opts']
|
||||
force = module.boolean(module.params['force'])
|
||||
resizefs = module.boolean(module.params['resizefs'])
|
||||
|
||||
changed = False
|
||||
|
||||
try:
|
||||
_ = fs_cmd_map[fstype]
|
||||
except KeyError:
|
||||
module.exit_json(changed=False, msg="WARNING: module does not support this filesystem yet. %s" % fstype)
|
||||
|
||||
mkfscmd = fs_cmd_map[fstype]['mkfs']
|
||||
force_flag = fs_cmd_map[fstype]['force_flag']
|
||||
growcmd = fs_cmd_map[fstype]['grow']
|
||||
fssize_cmd = fs_cmd_map[fstype]['fsinfo']
|
||||
|
||||
if not os.path.exists(dev):
|
||||
module.fail_json(msg="Device %s not found."%dev)
|
||||
|
||||
cmd = module.get_bin_path('blkid', required=True)
|
||||
|
||||
rc,raw_fs,err = module.run_command("%s -c /dev/null -o value -s TYPE %s" % (cmd, dev))
|
||||
fs = raw_fs.strip()
|
||||
|
||||
if fs == fstype and resizefs == False and not force:
|
||||
module.exit_json(changed=False)
|
||||
elif fs == fstype and resizefs == True:
|
||||
# Get dev and fs size and compare
|
||||
devsize_in_bytes = _get_dev_size(dev, module)
|
||||
fssize_in_bytes = _get_fs_size(fssize_cmd, dev, module)
|
||||
if fssize_in_bytes < devsize_in_bytes:
|
||||
fs_smaller = True
|
||||
else:
|
||||
fs_smaller = False
|
||||
|
||||
|
||||
if module.check_mode and fs_smaller:
|
||||
module.exit_json(changed=True, msg="Resizing filesystem %s on device %s" % (fstype,dev))
|
||||
elif module.check_mode and not fs_smaller:
|
||||
module.exit_json(changed=False, msg="%s filesystem is using the whole device %s" % (fstype, dev))
|
||||
elif fs_smaller:
|
||||
cmd = module.get_bin_path(growcmd, required=True)
|
||||
rc,out,err = module.run_command("%s %s" % (cmd, dev))
|
||||
# Sadly there is no easy way to determine if this has changed. For now, just say "true" and move on.
|
||||
# in the future, you would have to parse the output to determine this.
|
||||
# thankfully, these are safe operations if no change is made.
|
||||
if rc == 0:
|
||||
module.exit_json(changed=True, msg=out)
|
||||
else:
|
||||
module.fail_json(msg="Resizing filesystem %s on device '%s' failed"%(fstype,dev), rc=rc, err=err)
|
||||
else:
|
||||
module.exit_json(changed=False, msg="%s filesystem is using the whole device %s" % (fstype, dev))
|
||||
elif fs and not force:
|
||||
module.fail_json(msg="'%s' is already used as %s, use force=yes to overwrite"%(dev,fs), rc=rc, err=err)
|
||||
|
||||
### create fs
|
||||
|
||||
if module.check_mode:
|
||||
changed = True
|
||||
else:
|
||||
mkfs = module.get_bin_path(mkfscmd, required=True)
|
||||
cmd = None
|
||||
|
||||
if opts is None:
|
||||
cmd = "%s %s '%s'" % (mkfs, force_flag, dev)
|
||||
else:
|
||||
cmd = "%s %s %s '%s'" % (mkfs, force_flag, opts, dev)
|
||||
rc,_,err = module.run_command(cmd)
|
||||
if rc == 0:
|
||||
changed = True
|
||||
else:
|
||||
module.fail_json(msg="Creating filesystem %s on device '%s' failed"%(fstype,dev), rc=rc, err=err)
|
||||
|
||||
module.exit_json(changed=changed)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
1072
lib/ansible/modules/system/firewalld.py
Normal file
1072
lib/ansible/modules/system/firewalld.py
Normal file
File diff suppressed because it is too large
Load Diff
165
lib/ansible/modules/system/getent.py
Normal file
165
lib/ansible/modules/system/getent.py
Normal file
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2014, Brian Coca <brian.coca+dev@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/>.
|
||||
#
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['stableinterface'],
|
||||
'supported_by': 'core',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: getent
|
||||
short_description: a wrapper to the unix getent utility
|
||||
description:
|
||||
- Runs getent against one of it's various databases and returns information into
|
||||
the host's facts, in a getent_<database> prefixed variable
|
||||
version_added: "1.8"
|
||||
options:
|
||||
database:
|
||||
required: True
|
||||
description:
|
||||
- the name of a getent database supported by the target system (passwd, group,
|
||||
hosts, etc).
|
||||
key:
|
||||
required: False
|
||||
default: ''
|
||||
description:
|
||||
- key from which to return values from the specified database, otherwise the
|
||||
full contents are returned.
|
||||
split:
|
||||
required: False
|
||||
default: None
|
||||
description:
|
||||
- "character used to split the database values into lists/arrays such as ':' or '\t', otherwise it will try to pick one depending on the database"
|
||||
fail_key:
|
||||
required: False
|
||||
default: True
|
||||
description:
|
||||
- If a supplied key is missing this will make the task fail if True
|
||||
|
||||
notes:
|
||||
- "Not all databases support enumeration, check system documentation for details"
|
||||
requirements: [ ]
|
||||
author: "Brian Coca (@bcoca)"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# get root user info
|
||||
- getent:
|
||||
database: passwd
|
||||
key: root
|
||||
- debug:
|
||||
var: getent_passwd
|
||||
|
||||
# get all groups
|
||||
- getent:
|
||||
database: group
|
||||
split: ':'
|
||||
- debug:
|
||||
var: getent_group
|
||||
|
||||
# get all hosts, split by tab
|
||||
- getent:
|
||||
database: hosts
|
||||
- debug:
|
||||
var: getent_hosts
|
||||
|
||||
# get http service info, no error if missing
|
||||
- getent:
|
||||
database: services
|
||||
key: http
|
||||
fail_key: False
|
||||
- debug:
|
||||
var: getent_services
|
||||
|
||||
# get user password hash (requires sudo/root)
|
||||
- getent:
|
||||
database: shadow
|
||||
key: www-data
|
||||
split: ':'
|
||||
- debug:
|
||||
var: getent_shadow
|
||||
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
database = dict(required=True),
|
||||
key = dict(required=False, default=None),
|
||||
split = dict(required=False, default=None),
|
||||
fail_key = dict(required=False, type='bool', default=True),
|
||||
),
|
||||
supports_check_mode = True,
|
||||
)
|
||||
|
||||
colon = [ 'passwd', 'shadow', 'group', 'gshadow' ]
|
||||
|
||||
database = module.params['database']
|
||||
key = module.params.get('key')
|
||||
split = module.params.get('split')
|
||||
fail_key = module.params.get('fail_key')
|
||||
|
||||
getent_bin = module.get_bin_path('getent', True)
|
||||
|
||||
if key is not None:
|
||||
cmd = [ getent_bin, database, key ]
|
||||
else:
|
||||
cmd = [ getent_bin, database ]
|
||||
|
||||
if split is None and database in colon:
|
||||
split = ':'
|
||||
|
||||
try:
|
||||
rc, out, err = module.run_command(cmd)
|
||||
except Exception:
|
||||
e = get_exception()
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
msg = "Unexpected failure!"
|
||||
dbtree = 'getent_%s' % database
|
||||
results = { dbtree: {} }
|
||||
|
||||
if rc == 0:
|
||||
for line in out.splitlines():
|
||||
record = line.split(split)
|
||||
results[dbtree][record[0]] = record[1:]
|
||||
|
||||
module.exit_json(ansible_facts=results)
|
||||
|
||||
elif rc == 1:
|
||||
msg = "Missing arguments, or database unknown."
|
||||
elif rc == 2:
|
||||
msg = "One or more supplied key could not be found in the database."
|
||||
if not fail_key:
|
||||
results[dbtree][key] = None
|
||||
module.exit_json(ansible_facts=results, msg=msg)
|
||||
elif rc == 3:
|
||||
msg = "Enumeration not supported on this database."
|
||||
|
||||
module.fail_json(msg=msg)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
530
lib/ansible/modules/system/gluster_volume.py
Normal file
530
lib/ansible/modules/system/gluster_volume.py
Normal file
@@ -0,0 +1,530 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2014, Taneli Leppä <taneli@crasman.fi>
|
||||
#
|
||||
# This file is part of Ansible (sort of)
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = """
|
||||
module: gluster_volume
|
||||
short_description: Manage GlusterFS volumes
|
||||
description:
|
||||
- Create, remove, start, stop and tune GlusterFS volumes
|
||||
version_added: "1.9"
|
||||
options:
|
||||
name:
|
||||
required: true
|
||||
description:
|
||||
- The volume name
|
||||
state:
|
||||
required: true
|
||||
choices: [ 'present', 'absent', 'started', 'stopped' ]
|
||||
description:
|
||||
- Use present/absent ensure if a volume exists or not,
|
||||
use started/stopped to control it's availability.
|
||||
cluster:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- List of hosts to use for probing and brick setup
|
||||
host:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- Override local hostname (for peer probing purposes)
|
||||
replicas:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- Replica count for volume
|
||||
stripes:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- Stripe count for volume
|
||||
disperses:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- Disperse count for volume
|
||||
version_added: "2.2"
|
||||
redundancies:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- Redundancy count for volume
|
||||
version_added: "2.2"
|
||||
transport:
|
||||
required: false
|
||||
choices: [ 'tcp', 'rdma', 'tcp,rdma' ]
|
||||
default: 'tcp'
|
||||
description:
|
||||
- Transport type for volume
|
||||
bricks:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- Brick paths on servers. Multiple brick paths can be separated by commas
|
||||
aliases: ['brick']
|
||||
start_on_create:
|
||||
choices: [ 'yes', 'no']
|
||||
required: false
|
||||
default: 'yes'
|
||||
description:
|
||||
- Controls whether the volume is started after creation or not, defaults to yes
|
||||
rebalance:
|
||||
choices: [ 'yes', 'no']
|
||||
required: false
|
||||
default: 'no'
|
||||
description:
|
||||
- Controls whether the cluster is rebalanced after changes
|
||||
directory:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- Directory for limit-usage
|
||||
options:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- A dictionary/hash with options/settings for the volume
|
||||
quota:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- Quota value for limit-usage (be sure to use 10.0MB instead of 10MB, see quota list)
|
||||
force:
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
- If brick is being created in the root partition, module will fail.
|
||||
Set force to true to override this behaviour
|
||||
notes:
|
||||
- "Requires cli tools for GlusterFS on servers"
|
||||
- "Will add new bricks, but not remove them"
|
||||
author: "Taneli Leppä (@rosmo)"
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
- name: create gluster volume
|
||||
gluster_volume:
|
||||
state: present
|
||||
name: test1
|
||||
bricks: /bricks/brick1/g1
|
||||
rebalance: yes
|
||||
cluster:
|
||||
- 192.0.2.10
|
||||
- 192.0.2.11
|
||||
run_once: true
|
||||
|
||||
- name: tune
|
||||
gluster_volume:
|
||||
state: present
|
||||
name: test1
|
||||
options:
|
||||
performance.cache-size: 256MB
|
||||
|
||||
- name: start gluster volume
|
||||
gluster_volume:
|
||||
state: started
|
||||
name: test1
|
||||
|
||||
- name: limit usage
|
||||
gluster_volume:
|
||||
state: present
|
||||
name: test1
|
||||
directory: /foo
|
||||
quota: 20.0MB
|
||||
|
||||
- name: stop gluster volume
|
||||
gluster_volume:
|
||||
state: stopped
|
||||
name: test1
|
||||
|
||||
- name: remove gluster volume
|
||||
gluster_volume:
|
||||
state: absent
|
||||
name: test1
|
||||
|
||||
- name: create gluster volume with multiple bricks
|
||||
gluster_volume:
|
||||
state: present
|
||||
name: test2
|
||||
bricks: /bricks/brick1/g2,/bricks/brick2/g2
|
||||
cluster:
|
||||
- 192.0.2.10
|
||||
- 192.0.2.11
|
||||
run_once: true
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import time
|
||||
import socket
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
glusterbin = ''
|
||||
|
||||
def run_gluster(gargs, **kwargs):
|
||||
global glusterbin
|
||||
global module
|
||||
args = [glusterbin]
|
||||
args.extend(gargs)
|
||||
try:
|
||||
rc, out, err = module.run_command(args, **kwargs)
|
||||
if rc != 0:
|
||||
module.fail_json(msg='error running gluster (%s) command (rc=%d): %s' % (' '.join(args), rc, out or err))
|
||||
except Exception:
|
||||
e = get_exception()
|
||||
module.fail_json(msg='error running gluster (%s) command: %s' % (' '.join(args), str(e)))
|
||||
return out
|
||||
|
||||
def run_gluster_nofail(gargs, **kwargs):
|
||||
global glusterbin
|
||||
global module
|
||||
args = [glusterbin]
|
||||
args.extend(gargs)
|
||||
rc, out, err = module.run_command(args, **kwargs)
|
||||
if rc != 0:
|
||||
return None
|
||||
return out
|
||||
|
||||
def run_gluster_yes(gargs):
|
||||
global glusterbin
|
||||
global module
|
||||
args = [glusterbin]
|
||||
args.extend(gargs)
|
||||
rc, out, err = module.run_command(args, data='y\n')
|
||||
if rc != 0:
|
||||
module.fail_json(msg='error running gluster (%s) command (rc=%d): %s' % (' '.join(args), rc, out or err))
|
||||
return out
|
||||
|
||||
def get_peers():
|
||||
out = run_gluster([ 'peer', 'status'])
|
||||
i = 0
|
||||
peers = {}
|
||||
hostname = None
|
||||
uuid = None
|
||||
state = None
|
||||
shortNames = False
|
||||
for row in out.split('\n'):
|
||||
if ': ' in row:
|
||||
key, value = row.split(': ')
|
||||
if key.lower() == 'hostname':
|
||||
hostname = value
|
||||
shortNames = False
|
||||
if key.lower() == 'uuid':
|
||||
uuid = value
|
||||
if key.lower() == 'state':
|
||||
state = value
|
||||
peers[hostname] = [ uuid, state ]
|
||||
elif row.lower() == 'other names:':
|
||||
shortNames = True
|
||||
elif row != '' and shortNames == True:
|
||||
peers[row] = [ uuid, state ]
|
||||
elif row == '':
|
||||
shortNames = False
|
||||
return peers
|
||||
|
||||
def get_volumes():
|
||||
out = run_gluster([ 'volume', 'info' ])
|
||||
|
||||
volumes = {}
|
||||
volume = {}
|
||||
for row in out.split('\n'):
|
||||
if ': ' in row:
|
||||
key, value = row.split(': ')
|
||||
if key.lower() == 'volume name':
|
||||
volume['name'] = value
|
||||
volume['options'] = {}
|
||||
volume['quota'] = False
|
||||
if key.lower() == 'volume id':
|
||||
volume['id'] = value
|
||||
if key.lower() == 'status':
|
||||
volume['status'] = value
|
||||
if key.lower() == 'transport-type':
|
||||
volume['transport'] = value
|
||||
if key.lower() != 'bricks' and key.lower()[:5] == 'brick':
|
||||
if not 'bricks' in volume:
|
||||
volume['bricks'] = []
|
||||
volume['bricks'].append(value)
|
||||
# Volume options
|
||||
if '.' in key:
|
||||
if not 'options' in volume:
|
||||
volume['options'] = {}
|
||||
volume['options'][key] = value
|
||||
if key == 'features.quota' and value == 'on':
|
||||
volume['quota'] = True
|
||||
else:
|
||||
if row.lower() != 'bricks:' and row.lower() != 'options reconfigured:':
|
||||
if len(volume) > 0:
|
||||
volumes[volume['name']] = volume
|
||||
volume = {}
|
||||
return volumes
|
||||
|
||||
def get_quotas(name, nofail):
|
||||
quotas = {}
|
||||
if nofail:
|
||||
out = run_gluster_nofail([ 'volume', 'quota', name, 'list' ])
|
||||
if not out:
|
||||
return quotas
|
||||
else:
|
||||
out = run_gluster([ 'volume', 'quota', name, 'list' ])
|
||||
for row in out.split('\n'):
|
||||
if row[:1] == '/':
|
||||
q = re.split('\s+', row)
|
||||
quotas[q[0]] = q[1]
|
||||
return quotas
|
||||
|
||||
def wait_for_peer(host):
|
||||
for x in range(0, 4):
|
||||
peers = get_peers()
|
||||
if host in peers and peers[host][1].lower().find('peer in cluster') != -1:
|
||||
return True
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
def probe(host, myhostname):
|
||||
global module
|
||||
out = run_gluster([ 'peer', 'probe', host ])
|
||||
if out.find('localhost') == -1 and not wait_for_peer(host):
|
||||
module.fail_json(msg='failed to probe peer %s on %s' % (host, myhostname))
|
||||
changed = True
|
||||
|
||||
def probe_all_peers(hosts, peers, myhostname):
|
||||
for host in hosts:
|
||||
host = host.strip() # Clean up any extra space for exact comparison
|
||||
if host not in peers:
|
||||
probe(host, myhostname)
|
||||
|
||||
def create_volume(name, stripe, replica, disperse, redundancy, transport, hosts, bricks, force):
|
||||
args = [ 'volume', 'create' ]
|
||||
args.append(name)
|
||||
if stripe:
|
||||
args.append('stripe')
|
||||
args.append(str(stripe))
|
||||
if replica:
|
||||
args.append('replica')
|
||||
args.append(str(replica))
|
||||
if disperse:
|
||||
args.append('disperse')
|
||||
args.append(str(disperse))
|
||||
if redundancy:
|
||||
args.append('redundancy')
|
||||
args.append(str(redundancy))
|
||||
args.append('transport')
|
||||
args.append(transport)
|
||||
for brick in bricks:
|
||||
for host in hosts:
|
||||
args.append(('%s:%s' % (host, brick)))
|
||||
if force:
|
||||
args.append('force')
|
||||
run_gluster(args)
|
||||
|
||||
def start_volume(name):
|
||||
run_gluster([ 'volume', 'start', name ])
|
||||
|
||||
def stop_volume(name):
|
||||
run_gluster_yes([ 'volume', 'stop', name ])
|
||||
|
||||
def set_volume_option(name, option, parameter):
|
||||
run_gluster([ 'volume', 'set', name, option, parameter ])
|
||||
|
||||
def add_bricks(name, new_bricks, stripe, replica, force):
|
||||
args = [ 'volume', 'add-brick', name ]
|
||||
if stripe:
|
||||
args.append('stripe')
|
||||
args.append(str(stripe))
|
||||
if replica:
|
||||
args.append('replica')
|
||||
args.append(str(replica))
|
||||
args.extend(new_bricks)
|
||||
if force:
|
||||
args.append('force')
|
||||
run_gluster(args)
|
||||
|
||||
def do_rebalance(name):
|
||||
run_gluster([ 'volume', 'rebalance', name, 'start' ])
|
||||
|
||||
def enable_quota(name):
|
||||
run_gluster([ 'volume', 'quota', name, 'enable' ])
|
||||
|
||||
def set_quota(name, directory, value):
|
||||
run_gluster([ 'volume', 'quota', name, 'limit-usage', directory, value ])
|
||||
|
||||
|
||||
def main():
|
||||
### MAIN ###
|
||||
|
||||
global module
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
name=dict(required=True, default=None, aliases=['volume']),
|
||||
state=dict(required=True, choices=[ 'present', 'absent', 'started', 'stopped', 'rebalanced' ]),
|
||||
cluster=dict(required=False, default=None, type='list'),
|
||||
host=dict(required=False, default=None),
|
||||
stripes=dict(required=False, default=None, type='int'),
|
||||
replicas=dict(required=False, default=None, type='int'),
|
||||
disperses=dict(required=False, default=None, type='int'),
|
||||
redundancies=dict(required=False, default=None, type='int'),
|
||||
transport=dict(required=False, default='tcp', choices=[ 'tcp', 'rdma', 'tcp,rdma' ]),
|
||||
bricks=dict(required=False, default=None, aliases=['brick']),
|
||||
start_on_create=dict(required=False, default=True, type='bool'),
|
||||
rebalance=dict(required=False, default=False, type='bool'),
|
||||
options=dict(required=False, default={}, type='dict'),
|
||||
quota=dict(required=False),
|
||||
directory=dict(required=False, default=None),
|
||||
force=dict(required=False, default=False, type='bool'),
|
||||
)
|
||||
)
|
||||
|
||||
global glusterbin
|
||||
glusterbin = module.get_bin_path('gluster', True)
|
||||
|
||||
changed = False
|
||||
|
||||
action = module.params['state']
|
||||
volume_name = module.params['name']
|
||||
cluster= module.params['cluster']
|
||||
brick_paths = module.params['bricks']
|
||||
stripes = module.params['stripes']
|
||||
replicas = module.params['replicas']
|
||||
disperses = module.params['disperses']
|
||||
redundancies = module.params['redundancies']
|
||||
transport = module.params['transport']
|
||||
myhostname = module.params['host']
|
||||
start_on_create = module.boolean(module.params['start_on_create'])
|
||||
rebalance = module.boolean(module.params['rebalance'])
|
||||
force = module.boolean(module.params['force'])
|
||||
|
||||
if not myhostname:
|
||||
myhostname = socket.gethostname()
|
||||
|
||||
# Clean up if last element is empty. Consider that yml can look like this:
|
||||
# cluster="{% for host in groups['glusterfs'] %}{{ hostvars[host]['private_ip'] }},{% endfor %}"
|
||||
if cluster != None and len(cluster) > 1 and cluster[-1] == '':
|
||||
cluster = cluster[0:-1]
|
||||
|
||||
if cluster == None or cluster[0] == '':
|
||||
cluster = [myhostname]
|
||||
|
||||
if brick_paths != None and "," in brick_paths:
|
||||
brick_paths = brick_paths.split(",")
|
||||
else:
|
||||
brick_paths = [brick_paths]
|
||||
|
||||
options = module.params['options']
|
||||
quota = module.params['quota']
|
||||
directory = module.params['directory']
|
||||
|
||||
|
||||
# get current state info
|
||||
peers = get_peers()
|
||||
volumes = get_volumes()
|
||||
quotas = {}
|
||||
if volume_name in volumes and volumes[volume_name]['quota'] and volumes[volume_name]['status'].lower() == 'started':
|
||||
quotas = get_quotas(volume_name, True)
|
||||
|
||||
# do the work!
|
||||
if action == 'absent':
|
||||
if volume_name in volumes:
|
||||
if volumes[volume_name]['status'].lower() != 'stopped':
|
||||
stop_volume(volume_name)
|
||||
run_gluster_yes([ 'volume', 'delete', volume_name ])
|
||||
changed = True
|
||||
|
||||
if action == 'present':
|
||||
probe_all_peers(cluster, peers, myhostname)
|
||||
|
||||
# create if it doesn't exist
|
||||
if volume_name not in volumes:
|
||||
create_volume(volume_name, stripes, replicas, disperses, redundancies, transport, cluster, brick_paths, force)
|
||||
volumes = get_volumes()
|
||||
changed = True
|
||||
|
||||
if volume_name in volumes:
|
||||
if volumes[volume_name]['status'].lower() != 'started' and start_on_create:
|
||||
start_volume(volume_name)
|
||||
changed = True
|
||||
|
||||
# switch bricks
|
||||
new_bricks = []
|
||||
removed_bricks = []
|
||||
all_bricks = []
|
||||
for node in cluster:
|
||||
for brick_path in brick_paths:
|
||||
brick = '%s:%s' % (node, brick_path)
|
||||
all_bricks.append(brick)
|
||||
if brick not in volumes[volume_name]['bricks']:
|
||||
new_bricks.append(brick)
|
||||
|
||||
# this module does not yet remove bricks, but we check those anyways
|
||||
for brick in volumes[volume_name]['bricks']:
|
||||
if brick not in all_bricks:
|
||||
removed_bricks.append(brick)
|
||||
|
||||
if new_bricks:
|
||||
add_bricks(volume_name, new_bricks, stripes, replicas, force)
|
||||
changed = True
|
||||
|
||||
# handle quotas
|
||||
if quota:
|
||||
if not volumes[volume_name]['quota']:
|
||||
enable_quota(volume_name)
|
||||
quotas = get_quotas(volume_name, False)
|
||||
if directory not in quotas or quotas[directory] != quota:
|
||||
set_quota(volume_name, directory, quota)
|
||||
changed = True
|
||||
|
||||
# set options
|
||||
for option in options.keys():
|
||||
if option not in volumes[volume_name]['options'] or volumes[volume_name]['options'][option] != options[option]:
|
||||
set_volume_option(volume_name, option, options[option])
|
||||
changed = True
|
||||
|
||||
else:
|
||||
module.fail_json(msg='failed to create volume %s' % volume_name)
|
||||
|
||||
if action != 'delete' and volume_name not in volumes:
|
||||
module.fail_json(msg='volume not found %s' % volume_name)
|
||||
|
||||
if action == 'started':
|
||||
if volumes[volume_name]['status'].lower() != 'started':
|
||||
start_volume(volume_name)
|
||||
changed = True
|
||||
|
||||
if action == 'stopped':
|
||||
if volumes[volume_name]['status'].lower() != 'stopped':
|
||||
stop_volume(volume_name)
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
volumes = get_volumes()
|
||||
if rebalance:
|
||||
do_rebalance(volume_name)
|
||||
|
||||
facts = {}
|
||||
facts['glusterfs'] = { 'peers': peers, 'volumes': volumes, 'quotas': quotas }
|
||||
|
||||
module.exit_json(changed=changed, ansible_facts=facts)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
564
lib/ansible/modules/system/iptables.py
Normal file
564
lib/ansible/modules/system/iptables.py
Normal file
@@ -0,0 +1,564 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2015, Linus Unnebäck <linus@folkdatorn.se>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
BINS = dict(
|
||||
ipv4='iptables',
|
||||
ipv6='ip6tables',
|
||||
)
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'core',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: iptables
|
||||
short_description: Modify the systems iptables
|
||||
requirements: []
|
||||
version_added: "2.0"
|
||||
author: Linus Unnebäck (@LinusU) <linus@folkdatorn.se>
|
||||
description:
|
||||
- Iptables is used to set up, maintain, and inspect the tables of IP packet
|
||||
filter rules in the Linux kernel. This module does not handle the saving
|
||||
and/or loading of rules, but rather only manipulates the current rules
|
||||
that are present in memory. This is the same as the behaviour of the
|
||||
"iptables" and "ip6tables" command which this module uses internally.
|
||||
notes:
|
||||
- This module just deals with individual rules. If you need advanced
|
||||
chaining of rules the recommended way is to template the iptables restore
|
||||
file.
|
||||
options:
|
||||
table:
|
||||
description:
|
||||
- This option specifies the packet matching table which the command
|
||||
should operate on. If the kernel is configured with automatic module
|
||||
loading, an attempt will be made to load the appropriate module for
|
||||
that table if it is not already there.
|
||||
required: false
|
||||
default: filter
|
||||
choices: [ "filter", "nat", "mangle", "raw", "security" ]
|
||||
state:
|
||||
description:
|
||||
- Whether the rule should be absent or present.
|
||||
required: false
|
||||
default: present
|
||||
choices: [ "present", "absent" ]
|
||||
action:
|
||||
version_added: "2.2"
|
||||
description:
|
||||
- Whether the rule should be appended at the bottom or inserted at the
|
||||
top. If the rule already exists the chain won't be modified.
|
||||
required: false
|
||||
default: append
|
||||
choices: [ "append", "insert" ]
|
||||
ip_version:
|
||||
description:
|
||||
- Which version of the IP protocol this rule should apply to.
|
||||
required: false
|
||||
default: ipv4
|
||||
choices: [ "ipv4", "ipv6" ]
|
||||
chain:
|
||||
description:
|
||||
- "Chain to operate on. This option can either be the name of a user
|
||||
defined chain or any of the builtin chains: 'INPUT', 'FORWARD',
|
||||
'OUTPUT', 'PREROUTING', 'POSTROUTING', 'SECMARK', 'CONNSECMARK'."
|
||||
required: false
|
||||
protocol:
|
||||
description:
|
||||
- The protocol of the rule or of the packet to check. The specified
|
||||
protocol can be one of tcp, udp, udplite, icmp, esp, ah, sctp or the
|
||||
special keyword "all", or it can be a numeric value, representing one
|
||||
of these protocols or a different one. A protocol name from
|
||||
/etc/protocols is also allowed. A "!" argument before the protocol
|
||||
inverts the test. The number zero is equivalent to all. "all" will
|
||||
match with all protocols and is taken as default when this option is
|
||||
omitted.
|
||||
required: false
|
||||
default: null
|
||||
source:
|
||||
description:
|
||||
- Source specification. Address can be either a network name,
|
||||
a hostname, a network IP address (with /mask), or a plain IP address.
|
||||
Hostnames will be resolved once only, before the rule is submitted to
|
||||
the kernel. Please note that specifying any name to be resolved with
|
||||
a remote query such as DNS is a really bad idea. The mask can be
|
||||
either a network mask or a plain number, specifying the number of 1's
|
||||
at the left side of the network mask. Thus, a mask of 24 is equivalent
|
||||
to 255.255.255.0. A "!" argument before the address specification
|
||||
inverts the sense of the address.
|
||||
required: false
|
||||
default: null
|
||||
destination:
|
||||
description:
|
||||
- Destination specification. Address can be either a network name,
|
||||
a hostname, a network IP address (with /mask), or a plain IP address.
|
||||
Hostnames will be resolved once only, before the rule is submitted to
|
||||
the kernel. Please note that specifying any name to be resolved with
|
||||
a remote query such as DNS is a really bad idea. The mask can be
|
||||
either a network mask or a plain number, specifying the number of 1's
|
||||
at the left side of the network mask. Thus, a mask of 24 is equivalent
|
||||
to 255.255.255.0. A "!" argument before the address specification
|
||||
inverts the sense of the address.
|
||||
required: false
|
||||
default: null
|
||||
match:
|
||||
description:
|
||||
- Specifies a match to use, that is, an extension module that tests for
|
||||
a specific property. The set of matches make up the condition under
|
||||
which a target is invoked. Matches are evaluated first to last if
|
||||
specified as an array and work in short-circuit fashion, i.e. if one
|
||||
extension yields false, evaluation will stop.
|
||||
required: false
|
||||
default: []
|
||||
jump:
|
||||
description:
|
||||
- This specifies the target of the rule; i.e., what to do if the packet
|
||||
matches it. The target can be a user-defined chain (other than the one
|
||||
this rule is in), one of the special builtin targets which decide the
|
||||
fate of the packet immediately, or an extension (see EXTENSIONS
|
||||
below). If this option is omitted in a rule (and the goto paramater
|
||||
is not used), then matching the rule will have no effect on the
|
||||
packet's fate, but the counters on the rule will be incremented.
|
||||
required: false
|
||||
default: null
|
||||
goto:
|
||||
description:
|
||||
- This specifies that the processing should continue in a user specified
|
||||
chain. Unlike the jump argument return will not continue processing in
|
||||
this chain but instead in the chain that called us via jump.
|
||||
required: false
|
||||
default: null
|
||||
in_interface:
|
||||
description:
|
||||
- Name of an interface via which a packet was received (only for packets
|
||||
entering the INPUT, FORWARD and PREROUTING chains). When the "!"
|
||||
argument is used before the interface name, the sense is inverted. If
|
||||
the interface name ends in a "+", then any interface which begins with
|
||||
this name will match. If this option is omitted, any interface name
|
||||
will match.
|
||||
required: false
|
||||
default: null
|
||||
out_interface:
|
||||
description:
|
||||
- Name of an interface via which a packet is going to be sent (for
|
||||
packets entering the FORWARD, OUTPUT and POSTROUTING chains). When the
|
||||
"!" argument is used before the interface name, the sense is inverted.
|
||||
If the interface name ends in a "+", then any interface which begins
|
||||
with this name will match. If this option is omitted, any interface
|
||||
name will match.
|
||||
required: false
|
||||
default: null
|
||||
fragment:
|
||||
description:
|
||||
- This means that the rule only refers to second and further fragments
|
||||
of fragmented packets. Since there is no way to tell the source or
|
||||
destination ports of such a packet (or ICMP type), such a packet will
|
||||
not match any rules which specify them. When the "!" argument precedes
|
||||
fragment argument, the rule will only match head fragments, or
|
||||
unfragmented packets.
|
||||
required: false
|
||||
default: null
|
||||
set_counters:
|
||||
description:
|
||||
- This enables the administrator to initialize the packet and byte
|
||||
counters of a rule (during INSERT, APPEND, REPLACE operations).
|
||||
required: false
|
||||
default: null
|
||||
source_port:
|
||||
description:
|
||||
- "Source port or port range specification. This can either be a service
|
||||
name or a port number. An inclusive range can also be specified, using
|
||||
the format first:last. If the first port is omitted, '0' is assumed;
|
||||
if the last is omitted, '65535' is assumed. If the first port is
|
||||
greater than the second one they will be swapped."
|
||||
required: false
|
||||
default: null
|
||||
destination_port:
|
||||
description:
|
||||
- "Destination port or port range specification. This can either be
|
||||
a service name or a port number. An inclusive range can also be
|
||||
specified, using the format first:last. If the first port is omitted,
|
||||
'0' is assumed; if the last is omitted, '65535' is assumed. If the
|
||||
first port is greater than the second one they will be swapped."
|
||||
required: false
|
||||
default: null
|
||||
to_ports:
|
||||
description:
|
||||
- "This specifies a destination port or range of ports to use: without
|
||||
this, the destination port is never altered. This is only valid if the
|
||||
rule also specifies one of the following protocols: tcp, udp, dccp or
|
||||
sctp."
|
||||
required: false
|
||||
default: null
|
||||
to_destination:
|
||||
version_added: "2.1"
|
||||
description:
|
||||
- "This specifies a destination address to use with DNAT: without
|
||||
this, the destination address is never altered."
|
||||
required: false
|
||||
default: null
|
||||
to_source:
|
||||
version_added: "2.2"
|
||||
description:
|
||||
- "This specifies a source address to use with SNAT: without
|
||||
this, the source address is never altered."
|
||||
required: false
|
||||
default: null
|
||||
set_dscp_mark:
|
||||
version_added: "2.1"
|
||||
description:
|
||||
- "This allows specifying a DSCP mark to be added to packets.
|
||||
It takes either an integer or hex value. Mutually exclusive with
|
||||
C(set_dscp_mark_class)."
|
||||
required: false
|
||||
default: null
|
||||
set_dscp_mark_class:
|
||||
version_added: "2.1"
|
||||
description:
|
||||
- "This allows specifying a predefined DiffServ class which will be
|
||||
translated to the corresponding DSCP mark. Mutually exclusive with
|
||||
C(set_dscp_mark)."
|
||||
required: false
|
||||
default: null
|
||||
comment:
|
||||
description:
|
||||
- "This specifies a comment that will be added to the rule"
|
||||
required: false
|
||||
default: null
|
||||
ctstate:
|
||||
description:
|
||||
- "ctstate is a list of the connection states to match in the conntrack
|
||||
module.
|
||||
Possible states are: 'INVALID', 'NEW', 'ESTABLISHED', 'RELATED',
|
||||
'UNTRACKED', 'SNAT', 'DNAT'"
|
||||
required: false
|
||||
default: []
|
||||
limit:
|
||||
description:
|
||||
- "Specifies the maximum average number of matches to allow per second.
|
||||
The number can specify units explicitly, using `/second', `/minute',
|
||||
`/hour' or `/day', or parts of them (so `5/second' is the same as
|
||||
`5/s')."
|
||||
required: false
|
||||
default: null
|
||||
limit_burst:
|
||||
version_added: "2.1"
|
||||
description:
|
||||
- "Specifies the maximum burst before the above limit kicks in."
|
||||
required: false
|
||||
default: null
|
||||
uid_owner:
|
||||
version_added: "2.1"
|
||||
description:
|
||||
- "Specifies the UID or username to use in match by owner rule."
|
||||
required: false
|
||||
reject_with:
|
||||
version_added: "2.1"
|
||||
description:
|
||||
- "Specifies the error packet type to return while rejecting."
|
||||
required: false
|
||||
icmp_type:
|
||||
version_added: "2.2"
|
||||
description:
|
||||
- "This allows specification of the ICMP type, which can be a numeric
|
||||
ICMP type, type/code pair, or one of the ICMP type names shown by the
|
||||
command 'iptables -p icmp -h'"
|
||||
required: false
|
||||
flush:
|
||||
version_added: "2.2"
|
||||
description:
|
||||
- "Flushes the specified table and chain of all rules. If no chain is
|
||||
specified then the entire table is purged. Ignores all other
|
||||
parameters."
|
||||
required: false
|
||||
policy:
|
||||
version_added: "2.2"
|
||||
description:
|
||||
- "Set the policy for the chain to the given target. Valid targets are
|
||||
ACCEPT, DROP, QUEUE, RETURN. Only built in chains can have policies.
|
||||
This parameter requires the chain parameter. Ignores all other
|
||||
parameters."
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Block specific IP
|
||||
- iptables:
|
||||
chain: INPUT
|
||||
source: 8.8.8.8
|
||||
jump: DROP
|
||||
become: yes
|
||||
|
||||
# Forward port 80 to 8600
|
||||
- iptables:
|
||||
table: nat
|
||||
chain: PREROUTING
|
||||
in_interface: eth0
|
||||
protocol: tcp
|
||||
match: tcp
|
||||
destination_port: 80
|
||||
jump: REDIRECT
|
||||
to_ports: 8600
|
||||
comment: Redirect web traffic to port 8600
|
||||
become: yes
|
||||
|
||||
# Allow related and established connections
|
||||
- iptables:
|
||||
chain: INPUT
|
||||
ctstate: ESTABLISHED,RELATED
|
||||
jump: ACCEPT
|
||||
become: yes
|
||||
|
||||
# Tag all outbound tcp packets with DSCP mark 8
|
||||
- iptables:
|
||||
chain: OUTPUT
|
||||
jump: DSCP
|
||||
table: mangle
|
||||
set_dscp_mark: 8
|
||||
protocol: tcp
|
||||
|
||||
# Tag all outbound tcp packets with DSCP DiffServ class CS1
|
||||
- iptables:
|
||||
chain: OUTPUT
|
||||
jump: DSCP
|
||||
table: mangle
|
||||
set_dscp_mark_class: CS1
|
||||
protocol: tcp
|
||||
'''
|
||||
|
||||
def append_param(rule, param, flag, is_list):
|
||||
if is_list:
|
||||
for item in param:
|
||||
append_param(rule, item, flag, False)
|
||||
else:
|
||||
if param is not None:
|
||||
rule.extend([flag, param])
|
||||
|
||||
|
||||
def append_csv(rule, param, flag):
|
||||
if param:
|
||||
rule.extend([flag, ','.join(param)])
|
||||
|
||||
|
||||
def append_match(rule, param, match):
|
||||
if param:
|
||||
rule.extend(['-m', match])
|
||||
|
||||
|
||||
def append_jump(rule, param, jump):
|
||||
if param:
|
||||
rule.extend(['-j', jump])
|
||||
|
||||
|
||||
def construct_rule(params):
|
||||
rule = []
|
||||
append_param(rule, params['protocol'], '-p', False)
|
||||
append_param(rule, params['source'], '-s', False)
|
||||
append_param(rule, params['destination'], '-d', False)
|
||||
append_param(rule, params['match'], '-m', True)
|
||||
append_param(rule, params['jump'], '-j', False)
|
||||
append_param(rule, params['to_destination'], '--to-destination', False)
|
||||
append_param(rule, params['to_source'], '--to-source', False)
|
||||
append_param(rule, params['goto'], '-g', False)
|
||||
append_param(rule, params['in_interface'], '-i', False)
|
||||
append_param(rule, params['out_interface'], '-o', False)
|
||||
append_param(rule, params['fragment'], '-f', False)
|
||||
append_param(rule, params['set_counters'], '-c', False)
|
||||
append_param(rule, params['source_port'], '--source-port', False)
|
||||
append_param(rule, params['destination_port'], '--destination-port', False)
|
||||
append_param(rule, params['to_ports'], '--to-ports', False)
|
||||
append_param(rule, params['set_dscp_mark'], '--set-dscp', False)
|
||||
append_param(
|
||||
rule,
|
||||
params['set_dscp_mark_class'],
|
||||
'--set-dscp-class',
|
||||
False)
|
||||
append_match(rule, params['comment'], 'comment')
|
||||
append_param(rule, params['comment'], '--comment', False)
|
||||
append_match(rule, params['ctstate'], 'state')
|
||||
append_csv(rule, params['ctstate'], '--state')
|
||||
append_match(rule, params['limit'] or params['limit_burst'], 'limit')
|
||||
append_param(rule, params['limit'], '--limit', False)
|
||||
append_param(rule, params['limit_burst'], '--limit-burst', False)
|
||||
append_match(rule, params['uid_owner'], 'owner')
|
||||
append_param(rule, params['uid_owner'], '--uid-owner', False)
|
||||
append_jump(rule, params['reject_with'], 'REJECT')
|
||||
append_param(rule, params['reject_with'], '--reject-with', False)
|
||||
append_param(rule, params['icmp_type'], '--icmp-type', False)
|
||||
return rule
|
||||
|
||||
|
||||
def push_arguments(iptables_path, action, params, make_rule=True):
|
||||
cmd = [iptables_path]
|
||||
cmd.extend(['-t', params['table']])
|
||||
cmd.extend([action, params['chain']])
|
||||
if make_rule:
|
||||
cmd.extend(construct_rule(params))
|
||||
return cmd
|
||||
|
||||
|
||||
def check_present(iptables_path, module, params):
|
||||
cmd = push_arguments(iptables_path, '-C', params)
|
||||
rc, _, __ = module.run_command(cmd, check_rc=False)
|
||||
return (rc == 0)
|
||||
|
||||
|
||||
def append_rule(iptables_path, module, params):
|
||||
cmd = push_arguments(iptables_path, '-A', params)
|
||||
module.run_command(cmd, check_rc=True)
|
||||
|
||||
|
||||
def insert_rule(iptables_path, module, params):
|
||||
cmd = push_arguments(iptables_path, '-I', params)
|
||||
module.run_command(cmd, check_rc=True)
|
||||
|
||||
|
||||
def remove_rule(iptables_path, module, params):
|
||||
cmd = push_arguments(iptables_path, '-D', params)
|
||||
module.run_command(cmd, check_rc=True)
|
||||
|
||||
|
||||
def flush_table(iptables_path, module, params):
|
||||
cmd = push_arguments(iptables_path, '-F', params, make_rule=False)
|
||||
module.run_command(cmd, check_rc=True)
|
||||
|
||||
|
||||
def set_chain_policy(iptables_path, module, params):
|
||||
cmd = push_arguments(iptables_path, '-P', params, make_rule=False)
|
||||
cmd.append(params['policy'])
|
||||
module.run_command(cmd, check_rc=True)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
supports_check_mode=True,
|
||||
argument_spec=dict(
|
||||
table=dict(
|
||||
required=False,
|
||||
default='filter',
|
||||
choices=['filter', 'nat', 'mangle', 'raw', 'security']),
|
||||
state=dict(
|
||||
required=False,
|
||||
default='present',
|
||||
choices=['present', 'absent']),
|
||||
action=dict(
|
||||
required=False,
|
||||
default='append',
|
||||
type='str',
|
||||
choices=['append', 'insert']),
|
||||
ip_version=dict(
|
||||
required=False,
|
||||
default='ipv4',
|
||||
choices=['ipv4', 'ipv6']),
|
||||
chain=dict(required=False, default=None, type='str'),
|
||||
protocol=dict(required=False, default=None, type='str'),
|
||||
source=dict(required=False, default=None, type='str'),
|
||||
to_source=dict(required=False, default=None, type='str'),
|
||||
destination=dict(required=False, default=None, type='str'),
|
||||
to_destination=dict(required=False, default=None, type='str'),
|
||||
match=dict(required=False, default=[], type='list'),
|
||||
jump=dict(required=False, default=None, type='str'),
|
||||
goto=dict(required=False, default=None, type='str'),
|
||||
in_interface=dict(required=False, default=None, type='str'),
|
||||
out_interface=dict(required=False, default=None, type='str'),
|
||||
fragment=dict(required=False, default=None, type='str'),
|
||||
set_counters=dict(required=False, default=None, type='str'),
|
||||
source_port=dict(required=False, default=None, type='str'),
|
||||
destination_port=dict(required=False, default=None, type='str'),
|
||||
to_ports=dict(required=False, default=None, type='str'),
|
||||
set_dscp_mark=dict(required=False, default=None, type='str'),
|
||||
set_dscp_mark_class=dict(required=False, default=None, type='str'),
|
||||
comment=dict(required=False, default=None, type='str'),
|
||||
ctstate=dict(required=False, default=[], type='list'),
|
||||
limit=dict(required=False, default=None, type='str'),
|
||||
limit_burst=dict(required=False, default=None, type='str'),
|
||||
uid_owner=dict(required=False, default=None, type='str'),
|
||||
reject_with=dict(required=False, default=None, type='str'),
|
||||
icmp_type=dict(required=False, default=None, type='str'),
|
||||
flush=dict(required=False, default=False, type='bool'),
|
||||
policy=dict(
|
||||
required=False,
|
||||
default=None,
|
||||
type='str',
|
||||
choices=['ACCEPT', 'DROP', 'QUEUE', 'RETURN']),
|
||||
),
|
||||
mutually_exclusive=(
|
||||
['set_dscp_mark', 'set_dscp_mark_class'],
|
||||
['flush', 'policy'],
|
||||
),
|
||||
)
|
||||
args = dict(
|
||||
changed=False,
|
||||
failed=False,
|
||||
ip_version=module.params['ip_version'],
|
||||
table=module.params['table'],
|
||||
chain=module.params['chain'],
|
||||
flush=module.params['flush'],
|
||||
rule=' '.join(construct_rule(module.params)),
|
||||
state=module.params['state'],
|
||||
)
|
||||
|
||||
ip_version = module.params['ip_version']
|
||||
iptables_path = module.get_bin_path(BINS[ip_version], True)
|
||||
|
||||
# Check if chain option is required
|
||||
if args['flush'] is False and args['chain'] is None:
|
||||
module.fail_json(
|
||||
msg="Either chain or flush parameter must be specified.")
|
||||
|
||||
# Flush the table
|
||||
if args['flush'] is True:
|
||||
flush_table(iptables_path, module, module.params)
|
||||
module.exit_json(**args)
|
||||
|
||||
# Set the policy
|
||||
if module.params['policy']:
|
||||
set_chain_policy(iptables_path, module, module.params)
|
||||
module.exit_json(**args)
|
||||
|
||||
insert = (module.params['action'] == 'insert')
|
||||
rule_is_present = check_present(iptables_path, module, module.params)
|
||||
should_be_present = (args['state'] == 'present')
|
||||
|
||||
# Check if target is up to date
|
||||
args['changed'] = (rule_is_present != should_be_present)
|
||||
|
||||
# Check only; don't modify
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=args['changed'])
|
||||
|
||||
# Target is already up to date
|
||||
if args['changed'] is False:
|
||||
module.exit_json(**args)
|
||||
|
||||
if should_be_present:
|
||||
if insert:
|
||||
insert_rule(iptables_path, module, module.params)
|
||||
else:
|
||||
append_rule(iptables_path, module, module.params)
|
||||
else:
|
||||
remove_rule(iptables_path, module, module.params)
|
||||
|
||||
module.exit_json(**args)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
149
lib/ansible/modules/system/kernel_blacklist.py
Normal file
149
lib/ansible/modules/system/kernel_blacklist.py
Normal file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/python
|
||||
# encoding: utf-8 -*-
|
||||
|
||||
# (c) 2013, Matthias Vogelgesang <matthias.vogelgesang@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 re
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: kernel_blacklist
|
||||
author: "Matthias Vogelgesang (@matze)"
|
||||
version_added: 1.4
|
||||
short_description: Blacklist kernel modules
|
||||
description:
|
||||
- Add or remove kernel modules from blacklist.
|
||||
options:
|
||||
name:
|
||||
required: true
|
||||
description:
|
||||
- Name of kernel module to black- or whitelist.
|
||||
state:
|
||||
required: false
|
||||
default: "present"
|
||||
choices: [ present, absent ]
|
||||
description:
|
||||
- Whether the module should be present in the blacklist or absent.
|
||||
blacklist_file:
|
||||
required: false
|
||||
description:
|
||||
- If specified, use this blacklist file instead of
|
||||
C(/etc/modprobe.d/blacklist-ansible.conf).
|
||||
default: null
|
||||
requirements: []
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Blacklist the nouveau driver module
|
||||
- kernel_blacklist:
|
||||
name: nouveau
|
||||
state: present
|
||||
'''
|
||||
|
||||
|
||||
class Blacklist(object):
|
||||
def __init__(self, module, filename):
|
||||
if not os.path.exists(filename):
|
||||
open(filename, 'a').close()
|
||||
|
||||
self.filename = filename
|
||||
self.module = module
|
||||
|
||||
def get_pattern(self):
|
||||
return '^blacklist\s*' + self.module + '$'
|
||||
|
||||
def readlines(self):
|
||||
f = open(self.filename, 'r')
|
||||
lines = f.readlines()
|
||||
f.close()
|
||||
return lines
|
||||
|
||||
def module_listed(self):
|
||||
lines = self.readlines()
|
||||
pattern = self.get_pattern()
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('#'):
|
||||
continue
|
||||
|
||||
if re.match(pattern, stripped):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def remove_module(self):
|
||||
lines = self.readlines()
|
||||
pattern = self.get_pattern()
|
||||
|
||||
f = open(self.filename, 'w')
|
||||
|
||||
for line in lines:
|
||||
if not re.match(pattern, line.strip()):
|
||||
f.write(line)
|
||||
|
||||
f.close()
|
||||
|
||||
def add_module(self):
|
||||
f = open(self.filename, 'a')
|
||||
f.write('blacklist %s\n' % self.module)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
name=dict(required=True),
|
||||
state=dict(required=False, choices=['present', 'absent'],
|
||||
default='present'),
|
||||
blacklist_file=dict(required=False, default=None)
|
||||
),
|
||||
supports_check_mode=False,
|
||||
)
|
||||
|
||||
args = dict(changed=False, failed=False,
|
||||
name=module.params['name'], state=module.params['state'])
|
||||
|
||||
filename = '/etc/modprobe.d/blacklist-ansible.conf'
|
||||
|
||||
if module.params['blacklist_file']:
|
||||
filename = module.params['blacklist_file']
|
||||
|
||||
blacklist = Blacklist(args['name'], filename)
|
||||
|
||||
if blacklist.module_listed():
|
||||
if args['state'] == 'absent':
|
||||
blacklist.remove_module()
|
||||
args['changed'] = True
|
||||
else:
|
||||
if args['state'] == 'present':
|
||||
blacklist.add_module()
|
||||
args['changed'] = True
|
||||
|
||||
module.exit_json(**args)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
313
lib/ansible/modules/system/known_hosts.py
Normal file
313
lib/ansible/modules/system/known_hosts.py
Normal file
@@ -0,0 +1,313 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
"""
|
||||
Ansible module to manage the ssh known_hosts file.
|
||||
Copyright(c) 2014, Matthew Vernon <mcv21@cam.ac.uk>
|
||||
|
||||
This module is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This module is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this module. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: known_hosts
|
||||
short_description: Add or remove a host from the C(known_hosts) file
|
||||
description:
|
||||
- The M(known_hosts) module lets you add or remove a host keys from the C(known_hosts) file.
|
||||
- Starting at Ansible 2.2, multiple entries per host are allowed, but only one for each key type supported by ssh.
|
||||
This is useful if you're going to want to use the M(git) module over ssh, for example.
|
||||
- If you have a very large number of host keys to manage, you will find the M(template) module more useful.
|
||||
version_added: "1.9"
|
||||
options:
|
||||
name:
|
||||
aliases: [ 'host' ]
|
||||
description:
|
||||
- The host to add or remove (must match a host specified in key)
|
||||
required: true
|
||||
default: null
|
||||
key:
|
||||
description:
|
||||
- The SSH public host key, as a string (required if state=present, optional when state=absent, in which case all keys for the host are removed). The key must be in the right format for ssh (see sshd(1), section "SSH_KNOWN_HOSTS FILE FORMAT")
|
||||
required: false
|
||||
default: null
|
||||
path:
|
||||
description:
|
||||
- The known_hosts file to edit
|
||||
required: no
|
||||
default: "(homedir)+/.ssh/known_hosts"
|
||||
hash_host:
|
||||
description:
|
||||
- Hash the hostname in the known_hosts file
|
||||
required: no
|
||||
default: no
|
||||
version_added: "2.3"
|
||||
state:
|
||||
description:
|
||||
- I(present) to add the host key, I(absent) to remove it.
|
||||
choices: [ "present", "absent" ]
|
||||
required: no
|
||||
default: present
|
||||
requirements: [ ]
|
||||
author: "Matthew Vernon (@mcv21)"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: tell the host about our servers it might want to ssh to
|
||||
known_hosts:
|
||||
path: /etc/ssh/ssh_known_hosts
|
||||
name: foo.com.invalid
|
||||
key: "{{ lookup('file', 'pubkeys/foo.com.invalid') }}"
|
||||
'''
|
||||
|
||||
# Makes sure public host keys are present or absent in the given known_hosts
|
||||
# file.
|
||||
#
|
||||
# Arguments
|
||||
# =========
|
||||
# name = hostname whose key should be added (alias: host)
|
||||
# key = line(s) to add to known_hosts file
|
||||
# path = the known_hosts file to edit (default: ~/.ssh/known_hosts)
|
||||
# hash_host = yes|no (default: no) hash the hostname in the known_hosts file
|
||||
# state = absent|present (default: present)
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import tempfile
|
||||
import errno
|
||||
import re
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
def enforce_state(module, params):
|
||||
"""
|
||||
Add or remove key.
|
||||
"""
|
||||
|
||||
host = params["name"]
|
||||
key = params.get("key",None)
|
||||
port = params.get("port",None)
|
||||
path = params.get("path")
|
||||
hash_host = params.get("hash_host")
|
||||
state = params.get("state")
|
||||
#Find the ssh-keygen binary
|
||||
sshkeygen = module.get_bin_path("ssh-keygen",True)
|
||||
|
||||
# Trailing newline in files gets lost, so re-add if necessary
|
||||
if key and key[-1] != '\n':
|
||||
key+='\n'
|
||||
|
||||
if key is None and state != "absent":
|
||||
module.fail_json(msg="No key specified when adding a host")
|
||||
|
||||
sanity_check(module,host,key,sshkeygen)
|
||||
|
||||
found,replace_or_add,found_line,key=search_for_host_key(module,host,key,hash_host,path,sshkeygen)
|
||||
|
||||
#We will change state if found==True & state!="present"
|
||||
#or found==False & state=="present"
|
||||
#i.e found XOR (state=="present")
|
||||
#Alternatively, if replace is true (i.e. key present, and we must change it)
|
||||
if module.check_mode:
|
||||
module.exit_json(changed = replace_or_add or (state=="present") != found)
|
||||
|
||||
#Now do the work.
|
||||
|
||||
#Only remove whole host if found and no key provided
|
||||
if found and key is None and state=="absent":
|
||||
module.run_command([sshkeygen,'-R',host,'-f',path], check_rc=True)
|
||||
params['changed'] = True
|
||||
|
||||
#Next, add a new (or replacing) entry
|
||||
if replace_or_add or found != (state=="present"):
|
||||
try:
|
||||
inf=open(path,"r")
|
||||
except IOError:
|
||||
e = get_exception()
|
||||
if e.errno == errno.ENOENT:
|
||||
inf=None
|
||||
else:
|
||||
module.fail_json(msg="Failed to read %s: %s" % \
|
||||
(path,str(e)))
|
||||
try:
|
||||
outf=tempfile.NamedTemporaryFile(dir=os.path.dirname(path))
|
||||
if inf is not None:
|
||||
for line_number, line in enumerate(inf, start=1):
|
||||
if found_line==line_number and (replace_or_add or state=='absent'):
|
||||
continue # skip this line to replace its key
|
||||
outf.write(line)
|
||||
inf.close()
|
||||
if state == 'present':
|
||||
outf.write(key)
|
||||
outf.flush()
|
||||
module.atomic_move(outf.name,path)
|
||||
except (IOError,OSError):
|
||||
e = get_exception()
|
||||
module.fail_json(msg="Failed to write to file %s: %s" % \
|
||||
(path,str(e)))
|
||||
|
||||
try:
|
||||
outf.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
params['changed'] = True
|
||||
|
||||
return params
|
||||
|
||||
def sanity_check(module,host,key,sshkeygen):
|
||||
'''Check supplied key is sensible
|
||||
|
||||
host and key are parameters provided by the user; If the host
|
||||
provided is inconsistent with the key supplied, then this function
|
||||
quits, providing an error to the user.
|
||||
sshkeygen is the path to ssh-keygen, found earlier with get_bin_path
|
||||
'''
|
||||
#If no key supplied, we're doing a removal, and have nothing to check here.
|
||||
if key is None:
|
||||
return
|
||||
#Rather than parsing the key ourselves, get ssh-keygen to do it
|
||||
#(this is essential for hashed keys, but otherwise useful, as the
|
||||
#key question is whether ssh-keygen thinks the key matches the host).
|
||||
|
||||
#The approach is to write the key to a temporary file,
|
||||
#and then attempt to look up the specified host in that file.
|
||||
try:
|
||||
outf=tempfile.NamedTemporaryFile()
|
||||
outf.write(key)
|
||||
outf.flush()
|
||||
except IOError:
|
||||
e = get_exception()
|
||||
module.fail_json(msg="Failed to write to temporary file %s: %s" % \
|
||||
(outf.name,str(e)))
|
||||
rc,stdout,stderr=module.run_command([sshkeygen,'-F',host,
|
||||
'-f',outf.name],
|
||||
check_rc=True)
|
||||
try:
|
||||
outf.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
if stdout=='': #host not found
|
||||
module.fail_json(msg="Host parameter does not match hashed host field in supplied key")
|
||||
|
||||
def search_for_host_key(module,host,key,hash_host,path,sshkeygen):
|
||||
'''search_for_host_key(module,host,key,path,sshkeygen) -> (found,replace_or_add,found_line)
|
||||
|
||||
Looks up host and keytype in the known_hosts file path; if it's there, looks to see
|
||||
if one of those entries matches key. Returns:
|
||||
found (Boolean): is host found in path?
|
||||
replace_or_add (Boolean): is the key in path different to that supplied by user?
|
||||
found_line (int or None): the line where a key of the same type was found
|
||||
if found=False, then replace is always False.
|
||||
sshkeygen is the path to ssh-keygen, found earlier with get_bin_path
|
||||
'''
|
||||
if os.path.exists(path)==False:
|
||||
return False, False, None, key
|
||||
|
||||
sshkeygen_command=[sshkeygen,'-F',host,'-f',path]
|
||||
|
||||
#openssh >=6.4 has changed ssh-keygen behaviour such that it returns
|
||||
#1 if no host is found, whereas previously it returned 0
|
||||
rc,stdout,stderr=module.run_command(sshkeygen_command,
|
||||
check_rc=False)
|
||||
if stdout=='' and stderr=='' and (rc==0 or rc==1):
|
||||
return False, False, None, key #host not found, no other errors
|
||||
if rc!=0: #something went wrong
|
||||
module.fail_json(msg="ssh-keygen failed (rc=%d,stdout='%s',stderr='%s')" % (rc,stdout,stderr))
|
||||
|
||||
#If user supplied no key, we don't want to try and replace anything with it
|
||||
if key is None:
|
||||
return True, False, None, key
|
||||
|
||||
lines=stdout.split('\n')
|
||||
new_key = normalize_known_hosts_key(key)
|
||||
|
||||
sshkeygen_command.insert(1,'-H')
|
||||
rc,stdout,stderr=module.run_command(sshkeygen_command,check_rc=False)
|
||||
if rc!=0: #something went wrong
|
||||
module.fail_json(msg="ssh-keygen failed to hash host (rc=%d,stdout='%s',stderr='%s')" % (rc,stdout,stderr))
|
||||
hashed_lines=stdout.split('\n')
|
||||
|
||||
for lnum,l in enumerate(lines):
|
||||
if l=='':
|
||||
continue
|
||||
elif l[0]=='#': # info output from ssh-keygen; contains the line number where key was found
|
||||
try:
|
||||
# This output format has been hardcoded in ssh-keygen since at least OpenSSH 4.0
|
||||
# It always outputs the non-localized comment before the found key
|
||||
found_line = int(re.search(r'found: line (\d+)', l).group(1))
|
||||
except IndexError:
|
||||
e = get_exception()
|
||||
module.fail_json(msg="failed to parse output of ssh-keygen for line number: '%s'" % l)
|
||||
else:
|
||||
found_key = normalize_known_hosts_key(l)
|
||||
if hash_host==True:
|
||||
if found_key['host'][:3]=='|1|':
|
||||
new_key['host']=found_key['host']
|
||||
else:
|
||||
hashed_host=normalize_known_hosts_key(hashed_lines[lnum])
|
||||
found_key['host']=hashed_host['host']
|
||||
key=key.replace(host,found_key['host'])
|
||||
if new_key==found_key: #found a match
|
||||
return True, False, found_line, key #found exactly the same key, don't replace
|
||||
elif new_key['type'] == found_key['type']: # found a different key for the same key type
|
||||
return True, True, found_line, key
|
||||
#No match found, return found and replace, but no line
|
||||
return True, True, None, key
|
||||
|
||||
def normalize_known_hosts_key(key):
|
||||
'''
|
||||
Transform a key, either taken from a known_host file or provided by the
|
||||
user, into a normalized form.
|
||||
The host part (which might include multiple hostnames or be hashed) gets
|
||||
replaced by the provided host. Also, any spurious information gets removed
|
||||
from the end (like the username@host tag usually present in hostkeys, but
|
||||
absent in known_hosts files)
|
||||
'''
|
||||
k=key.strip() #trim trailing newline
|
||||
k=key.split()
|
||||
d = dict()
|
||||
#The optional "marker" field, used for @cert-authority or @revoked
|
||||
if k[0][0] == '@':
|
||||
d['options'] = k[0]
|
||||
d['host']=k[1]
|
||||
d['type']=k[2]
|
||||
d['key']=k[3]
|
||||
else:
|
||||
d['host']=k[0]
|
||||
d['type']=k[1]
|
||||
d['key']=k[2]
|
||||
return d
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
name = dict(required=True, type='str', aliases=['host']),
|
||||
key = dict(required=False, type='str'),
|
||||
path = dict(default="~/.ssh/known_hosts", type='path'),
|
||||
hash_host = dict(required=False, type='bool' ,default=False),
|
||||
state = dict(default='present', choices=['absent','present']),
|
||||
),
|
||||
supports_check_mode = True
|
||||
)
|
||||
|
||||
results = enforce_state(module,module.params)
|
||||
module.exit_json(**results)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
246
lib/ansible/modules/system/locale_gen.py
Normal file
246
lib/ansible/modules/system/locale_gen.py
Normal file
@@ -0,0 +1,246 @@
|
||||
#!/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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: locale_gen
|
||||
short_description: Creates or removes locales.
|
||||
description:
|
||||
- Manages locales by editing /etc/locale.gen and invoking locale-gen.
|
||||
version_added: "1.6"
|
||||
author: "Augustus Kling (@AugustusKling)"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name and encoding of the locale, such as "en_GB.UTF-8".
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
state:
|
||||
description:
|
||||
- Whether the locale shall be present.
|
||||
required: false
|
||||
choices: ["present", "absent"]
|
||||
default: "present"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Ensure a locale exists.
|
||||
- locale_gen:
|
||||
name: de_CH.UTF-8
|
||||
state: present
|
||||
'''
|
||||
|
||||
import os
|
||||
import os.path
|
||||
from subprocess import Popen, PIPE, call
|
||||
import re
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
|
||||
LOCALE_NORMALIZATION = {
|
||||
".utf8": ".UTF-8",
|
||||
".eucjp": ".EUC-JP",
|
||||
".iso885915": ".ISO-8859-15",
|
||||
".cp1251": ".CP1251",
|
||||
".koi8r": ".KOI8-R",
|
||||
".armscii8": ".ARMSCII-8",
|
||||
".euckr": ".EUC-KR",
|
||||
".gbk": ".GBK",
|
||||
".gb18030": ".GB18030",
|
||||
".euctw": ".EUC-TW",
|
||||
}
|
||||
|
||||
# ===========================================
|
||||
# location module specific support methods.
|
||||
#
|
||||
|
||||
def is_available(name, ubuntuMode):
|
||||
"""Check if the given locale is available on the system. This is done by
|
||||
checking either :
|
||||
* if the locale is present in /etc/locales.gen
|
||||
* or if the locale is present in /usr/share/i18n/SUPPORTED"""
|
||||
if ubuntuMode:
|
||||
__regexp = '^(?P<locale>\S+_\S+) (?P<charset>\S+)\s*$'
|
||||
__locales_available = '/usr/share/i18n/SUPPORTED'
|
||||
else:
|
||||
__regexp = '^#{0,1}\s*(?P<locale>\S+_\S+) (?P<charset>\S+)\s*$'
|
||||
__locales_available = '/etc/locale.gen'
|
||||
|
||||
re_compiled = re.compile(__regexp)
|
||||
fd = open(__locales_available, 'r')
|
||||
for line in fd:
|
||||
result = re_compiled.match(line)
|
||||
if result and result.group('locale') == name:
|
||||
return True
|
||||
fd.close()
|
||||
return False
|
||||
|
||||
def is_present(name):
|
||||
"""Checks if the given locale is currently installed."""
|
||||
output = Popen(["locale", "-a"], stdout=PIPE).communicate()[0]
|
||||
return any(fix_case(name) == fix_case(line) for line in output.splitlines())
|
||||
|
||||
def fix_case(name):
|
||||
"""locale -a might return the encoding in either lower or upper case.
|
||||
Passing through this function makes them uniform for comparisons."""
|
||||
for s, r in LOCALE_NORMALIZATION.items():
|
||||
name = name.replace(s, r)
|
||||
return name
|
||||
|
||||
def replace_line(existing_line, new_line):
|
||||
"""Replaces lines in /etc/locale.gen"""
|
||||
try:
|
||||
f = open("/etc/locale.gen", "r")
|
||||
lines = [line.replace(existing_line, new_line) for line in f]
|
||||
finally:
|
||||
f.close()
|
||||
try:
|
||||
f = open("/etc/locale.gen", "w")
|
||||
f.write("".join(lines))
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
def set_locale(name, enabled=True):
|
||||
""" Sets the state of the locale. Defaults to enabled. """
|
||||
search_string = '#{0,1}\s*%s (?P<charset>.+)' % name
|
||||
if enabled:
|
||||
new_string = '%s \g<charset>' % (name)
|
||||
else:
|
||||
new_string = '# %s \g<charset>' % (name)
|
||||
try:
|
||||
f = open("/etc/locale.gen", "r")
|
||||
lines = [re.sub(search_string, new_string, line) for line in f]
|
||||
finally:
|
||||
f.close()
|
||||
try:
|
||||
f = open("/etc/locale.gen", "w")
|
||||
f.write("".join(lines))
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
def apply_change(targetState, name):
|
||||
"""Create or remove locale.
|
||||
|
||||
Keyword arguments:
|
||||
targetState -- Desired state, either present or absent.
|
||||
name -- Name including encoding such as de_CH.UTF-8.
|
||||
"""
|
||||
if targetState=="present":
|
||||
# Create locale.
|
||||
set_locale(name, enabled=True)
|
||||
else:
|
||||
# Delete locale.
|
||||
set_locale(name, enabled=False)
|
||||
|
||||
localeGenExitValue = call("locale-gen")
|
||||
if localeGenExitValue!=0:
|
||||
raise EnvironmentError(localeGenExitValue, "locale.gen failed to execute, it returned "+str(localeGenExitValue))
|
||||
|
||||
def apply_change_ubuntu(targetState, name):
|
||||
"""Create or remove locale.
|
||||
|
||||
Keyword arguments:
|
||||
targetState -- Desired state, either present or absent.
|
||||
name -- Name including encoding such as de_CH.UTF-8.
|
||||
"""
|
||||
if targetState=="present":
|
||||
# Create locale.
|
||||
# Ubuntu's patched locale-gen automatically adds the new locale to /var/lib/locales/supported.d/local
|
||||
localeGenExitValue = call(["locale-gen", name])
|
||||
else:
|
||||
# Delete locale involves discarding the locale from /var/lib/locales/supported.d/local and regenerating all locales.
|
||||
try:
|
||||
f = open("/var/lib/locales/supported.d/local", "r")
|
||||
content = f.readlines()
|
||||
finally:
|
||||
f.close()
|
||||
try:
|
||||
f = open("/var/lib/locales/supported.d/local", "w")
|
||||
for line in content:
|
||||
locale, charset = line.split(' ')
|
||||
if locale != name:
|
||||
f.write(line)
|
||||
finally:
|
||||
f.close()
|
||||
# Purge locales and regenerate.
|
||||
# Please provide a patch if you know how to avoid regenerating the locales to keep!
|
||||
localeGenExitValue = call(["locale-gen", "--purge"])
|
||||
|
||||
if localeGenExitValue!=0:
|
||||
raise EnvironmentError(localeGenExitValue, "locale.gen failed to execute, it returned "+str(localeGenExitValue))
|
||||
|
||||
# ==============================================================
|
||||
# main
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
name = dict(required=True),
|
||||
state = dict(choices=['present','absent'], default='present'),
|
||||
),
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
name = module.params['name']
|
||||
state = module.params['state']
|
||||
|
||||
if not os.path.exists("/etc/locale.gen"):
|
||||
if os.path.exists("/var/lib/locales/supported.d/"):
|
||||
# Ubuntu created its own system to manage locales.
|
||||
ubuntuMode = True
|
||||
else:
|
||||
module.fail_json(msg="/etc/locale.gen and /var/lib/locales/supported.d/local are missing. Is the package \"locales\" installed?")
|
||||
else:
|
||||
# We found the common way to manage locales.
|
||||
ubuntuMode = False
|
||||
|
||||
if not is_available(name, ubuntuMode):
|
||||
module.fail_json(msg="The locales you've entered is not available "
|
||||
"on your system.")
|
||||
|
||||
if is_present(name):
|
||||
prev_state = "present"
|
||||
else:
|
||||
prev_state = "absent"
|
||||
changed = (prev_state!=state)
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=changed)
|
||||
else:
|
||||
if changed:
|
||||
try:
|
||||
if ubuntuMode==False:
|
||||
apply_change(state, name)
|
||||
else:
|
||||
apply_change_ubuntu(state, name)
|
||||
except EnvironmentError:
|
||||
e = get_exception()
|
||||
module.fail_json(msg=e.strerror, exitValue=e.errno)
|
||||
|
||||
module.exit_json(name=name, changed=changed, msg="OK")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
269
lib/ansible/modules/system/lvg.py
Normal file
269
lib/ansible/modules/system/lvg.py
Normal file
@@ -0,0 +1,269 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2013, Alexander Bulimov <lazywolf0@gmail.com>
|
||||
# based on lvol module by Jeroen Hoekx <jeroen.hoekx@dsquare.be>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
author: "Alexander Bulimov (@abulimov)"
|
||||
module: lvg
|
||||
short_description: Configure LVM volume groups
|
||||
description:
|
||||
- This module creates, removes or resizes volume groups.
|
||||
version_added: "1.1"
|
||||
options:
|
||||
vg:
|
||||
description:
|
||||
- The name of the volume group.
|
||||
required: true
|
||||
pvs:
|
||||
description:
|
||||
- List of comma-separated devices to use as physical devices in this volume group. Required when creating or resizing volume group.
|
||||
- The module will take care of running pvcreate if needed.
|
||||
required: false
|
||||
pesize:
|
||||
description:
|
||||
- The size of the physical extent in megabytes. Must be a power of 2.
|
||||
default: 4
|
||||
required: false
|
||||
vg_options:
|
||||
description:
|
||||
- Additional options to pass to C(vgcreate) when creating the volume group.
|
||||
default: null
|
||||
required: false
|
||||
version_added: "1.6"
|
||||
state:
|
||||
choices: [ "present", "absent" ]
|
||||
default: present
|
||||
description:
|
||||
- Control if the volume group exists.
|
||||
required: false
|
||||
force:
|
||||
choices: [ "yes", "no" ]
|
||||
default: "no"
|
||||
description:
|
||||
- If yes, allows to remove volume group with logical volumes.
|
||||
required: false
|
||||
notes:
|
||||
- module does not modify PE size for already present volume group
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create a volume group on top of /dev/sda1 with physical extent size = 32MB.
|
||||
- lvg:
|
||||
vg: vg.services
|
||||
pvs: /dev/sda1
|
||||
pesize: 32
|
||||
|
||||
# Create or resize a volume group on top of /dev/sdb1 and /dev/sdc5.
|
||||
# If, for example, we already have VG vg.services on top of /dev/sdb1,
|
||||
# this VG will be extended by /dev/sdc5. Or if vg.services was created on
|
||||
# top of /dev/sda5, we first extend it with /dev/sdb1 and /dev/sdc5,
|
||||
# and then reduce by /dev/sda5.
|
||||
- lvg:
|
||||
vg: vg.services
|
||||
pvs: /dev/sdb1,/dev/sdc5
|
||||
|
||||
# Remove a volume group with name vg.services.
|
||||
- lvg:
|
||||
vg: vg.services
|
||||
state: absent
|
||||
'''
|
||||
|
||||
def parse_vgs(data):
|
||||
vgs = []
|
||||
for line in data.splitlines():
|
||||
parts = line.strip().split(';')
|
||||
vgs.append({
|
||||
'name': parts[0],
|
||||
'pv_count': int(parts[1]),
|
||||
'lv_count': int(parts[2]),
|
||||
})
|
||||
return vgs
|
||||
|
||||
def find_mapper_device_name(module, dm_device):
|
||||
dmsetup_cmd = module.get_bin_path('dmsetup', True)
|
||||
mapper_prefix = '/dev/mapper/'
|
||||
rc, dm_name, err = module.run_command("%s info -C --noheadings -o name %s" % (dmsetup_cmd, dm_device))
|
||||
if rc != 0:
|
||||
module.fail_json(msg="Failed executing dmsetup command.", rc=rc, err=err)
|
||||
mapper_device = mapper_prefix + dm_name.rstrip()
|
||||
return mapper_device
|
||||
|
||||
def parse_pvs(module, data):
|
||||
pvs = []
|
||||
dm_prefix = '/dev/dm-'
|
||||
for line in data.splitlines():
|
||||
parts = line.strip().split(';')
|
||||
if parts[0].startswith(dm_prefix):
|
||||
parts[0] = find_mapper_device_name(module, parts[0])
|
||||
pvs.append({
|
||||
'name': parts[0],
|
||||
'vg_name': parts[1],
|
||||
})
|
||||
return pvs
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
vg=dict(required=True),
|
||||
pvs=dict(type='list'),
|
||||
pesize=dict(type='int', default=4),
|
||||
vg_options=dict(default=''),
|
||||
state=dict(choices=["absent", "present"], default='present'),
|
||||
force=dict(type='bool', default='no'),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
vg = module.params['vg']
|
||||
state = module.params['state']
|
||||
force = module.boolean(module.params['force'])
|
||||
pesize = module.params['pesize']
|
||||
vgoptions = module.params['vg_options'].split()
|
||||
|
||||
dev_list = []
|
||||
if module.params['pvs']:
|
||||
dev_list = module.params['pvs']
|
||||
elif state == 'present':
|
||||
module.fail_json(msg="No physical volumes given.")
|
||||
|
||||
# LVM always uses real paths not symlinks so replace symlinks with actual path
|
||||
for idx, dev in enumerate(dev_list):
|
||||
dev_list[idx] = os.path.realpath(dev)
|
||||
|
||||
if state=='present':
|
||||
### check given devices
|
||||
for test_dev in dev_list:
|
||||
if not os.path.exists(test_dev):
|
||||
module.fail_json(msg="Device %s not found."%test_dev)
|
||||
|
||||
### get pv list
|
||||
pvs_cmd = module.get_bin_path('pvs', True)
|
||||
rc,current_pvs,err = module.run_command("%s --noheadings -o pv_name,vg_name --separator ';'" % pvs_cmd)
|
||||
if rc != 0:
|
||||
module.fail_json(msg="Failed executing pvs command.",rc=rc, err=err)
|
||||
|
||||
### check pv for devices
|
||||
pvs = parse_pvs(module, current_pvs)
|
||||
used_pvs = [ pv for pv in pvs if pv['name'] in dev_list and pv['vg_name'] and pv['vg_name'] != vg ]
|
||||
if used_pvs:
|
||||
module.fail_json(msg="Device %s is already in %s volume group."%(used_pvs[0]['name'],used_pvs[0]['vg_name']))
|
||||
|
||||
vgs_cmd = module.get_bin_path('vgs', True)
|
||||
rc,current_vgs,err = module.run_command("%s --noheadings -o vg_name,pv_count,lv_count --separator ';'" % vgs_cmd)
|
||||
|
||||
if rc != 0:
|
||||
module.fail_json(msg="Failed executing vgs command.",rc=rc, err=err)
|
||||
|
||||
changed = False
|
||||
|
||||
vgs = parse_vgs(current_vgs)
|
||||
|
||||
for test_vg in vgs:
|
||||
if test_vg['name'] == vg:
|
||||
this_vg = test_vg
|
||||
break
|
||||
else:
|
||||
this_vg = None
|
||||
|
||||
if this_vg is None:
|
||||
if state == 'present':
|
||||
### create VG
|
||||
if module.check_mode:
|
||||
changed = True
|
||||
else:
|
||||
### create PV
|
||||
pvcreate_cmd = module.get_bin_path('pvcreate', True)
|
||||
for current_dev in dev_list:
|
||||
rc,_,err = module.run_command("%s -f %s" % (pvcreate_cmd,current_dev))
|
||||
if rc == 0:
|
||||
changed = True
|
||||
else:
|
||||
module.fail_json(msg="Creating physical volume '%s' failed" % current_dev, rc=rc, err=err)
|
||||
vgcreate_cmd = module.get_bin_path('vgcreate')
|
||||
rc,_,err = module.run_command([vgcreate_cmd] + vgoptions + ['-s', str(pesize), vg] + dev_list)
|
||||
if rc == 0:
|
||||
changed = True
|
||||
else:
|
||||
module.fail_json(msg="Creating volume group '%s' failed"%vg, rc=rc, err=err)
|
||||
else:
|
||||
if state == 'absent':
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
else:
|
||||
if this_vg['lv_count'] == 0 or force:
|
||||
### remove VG
|
||||
vgremove_cmd = module.get_bin_path('vgremove', True)
|
||||
rc,_,err = module.run_command("%s --force %s" % (vgremove_cmd, vg))
|
||||
if rc == 0:
|
||||
module.exit_json(changed=True)
|
||||
else:
|
||||
module.fail_json(msg="Failed to remove volume group %s"%(vg),rc=rc, err=err)
|
||||
else:
|
||||
module.fail_json(msg="Refuse to remove non-empty volume group %s without force=yes"%(vg))
|
||||
|
||||
### resize VG
|
||||
current_devs = [ os.path.realpath(pv['name']) for pv in pvs if pv['vg_name'] == vg ]
|
||||
devs_to_remove = list(set(current_devs) - set(dev_list))
|
||||
devs_to_add = list(set(dev_list) - set(current_devs))
|
||||
|
||||
if devs_to_add or devs_to_remove:
|
||||
if module.check_mode:
|
||||
changed = True
|
||||
else:
|
||||
if devs_to_add:
|
||||
devs_to_add_string = ' '.join(devs_to_add)
|
||||
### create PV
|
||||
pvcreate_cmd = module.get_bin_path('pvcreate', True)
|
||||
for current_dev in devs_to_add:
|
||||
rc,_,err = module.run_command("%s -f %s" % (pvcreate_cmd, current_dev))
|
||||
if rc == 0:
|
||||
changed = True
|
||||
else:
|
||||
module.fail_json(msg="Creating physical volume '%s' failed"%current_dev, rc=rc, err=err)
|
||||
### add PV to our VG
|
||||
vgextend_cmd = module.get_bin_path('vgextend', True)
|
||||
rc,_,err = module.run_command("%s %s %s" % (vgextend_cmd, vg, devs_to_add_string))
|
||||
if rc == 0:
|
||||
changed = True
|
||||
else:
|
||||
module.fail_json(msg="Unable to extend %s by %s."%(vg, devs_to_add_string),rc=rc,err=err)
|
||||
|
||||
### remove some PV from our VG
|
||||
if devs_to_remove:
|
||||
devs_to_remove_string = ' '.join(devs_to_remove)
|
||||
vgreduce_cmd = module.get_bin_path('vgreduce', True)
|
||||
rc,_,err = module.run_command("%s --force %s %s" % (vgreduce_cmd, vg, devs_to_remove_string))
|
||||
if rc == 0:
|
||||
changed = True
|
||||
else:
|
||||
module.fail_json(msg="Unable to reduce %s by %s."%(vg, devs_to_remove_string),rc=rc,err=err)
|
||||
|
||||
module.exit_json(changed=changed)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
495
lib/ansible/modules/system/lvol.py
Normal file
495
lib/ansible/modules/system/lvol.py
Normal file
@@ -0,0 +1,495 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2013, Jeroen Hoekx <jeroen.hoekx@dsquare.be>, Alexander Bulimov <lazywolf0@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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
author:
|
||||
- "Jeroen Hoekx (@jhoekx)"
|
||||
- "Alexander Bulimov (@abulimov)"
|
||||
module: lvol
|
||||
short_description: Configure LVM logical volumes
|
||||
description:
|
||||
- This module creates, removes or resizes logical volumes.
|
||||
version_added: "1.1"
|
||||
options:
|
||||
vg:
|
||||
description:
|
||||
- The volume group this logical volume is part of.
|
||||
required: true
|
||||
lv:
|
||||
description:
|
||||
- The name of the logical volume.
|
||||
required: true
|
||||
size:
|
||||
description:
|
||||
- The size of the logical volume, according to lvcreate(8) --size, by
|
||||
default in megabytes or optionally with one of [bBsSkKmMgGtTpPeE] units; or
|
||||
according to lvcreate(8) --extents as a percentage of [VG|PVS|FREE];
|
||||
Float values must begin with a digit.
|
||||
Resizing using percentage values was not supported prior to 2.1.
|
||||
state:
|
||||
choices: [ "present", "absent" ]
|
||||
default: present
|
||||
description:
|
||||
- Control if the logical volume exists. If C(present) and the
|
||||
volume does not already exist then the C(size) option is required.
|
||||
required: false
|
||||
active:
|
||||
version_added: "2.2"
|
||||
choices: [ "yes", "no" ]
|
||||
default: "yes"
|
||||
description:
|
||||
- Whether the volume is activate and visible to the host.
|
||||
required: false
|
||||
force:
|
||||
version_added: "1.5"
|
||||
choices: [ "yes", "no" ]
|
||||
default: "no"
|
||||
description:
|
||||
- Shrink or remove operations of volumes requires this switch. Ensures that
|
||||
that filesystems get never corrupted/destroyed by mistake.
|
||||
required: false
|
||||
opts:
|
||||
version_added: "2.0"
|
||||
description:
|
||||
- Free-form options to be passed to the lvcreate command
|
||||
snapshot:
|
||||
version_added: "2.1"
|
||||
description:
|
||||
- The name of the snapshot volume
|
||||
required: false
|
||||
pvs:
|
||||
version_added: "2.2"
|
||||
description:
|
||||
- Comma separated list of physical volumes e.g. /dev/sda,/dev/sdb
|
||||
required: false
|
||||
shrink:
|
||||
version_added: "2.2"
|
||||
description:
|
||||
- shrink if current size is higher than size requested
|
||||
required: false
|
||||
default: yes
|
||||
notes:
|
||||
- Filesystems on top of the volume are not resized.
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create a logical volume of 512m.
|
||||
- lvol:
|
||||
vg: firefly
|
||||
lv: test
|
||||
size: 512
|
||||
|
||||
# Create a logical volume of 512m with disks /dev/sda and /dev/sdb
|
||||
- lvol:
|
||||
vg: firefly
|
||||
lv: test
|
||||
size: 512
|
||||
pvs: /dev/sda,/dev/sdb
|
||||
|
||||
# Create cache pool logical volume
|
||||
- lvol:
|
||||
vg: firefly
|
||||
lv: lvcache
|
||||
size: 512m
|
||||
opts: --type cache-pool
|
||||
|
||||
# Create a logical volume of 512g.
|
||||
- lvol:
|
||||
vg: firefly
|
||||
lv: test
|
||||
size: 512g
|
||||
|
||||
# Create a logical volume the size of all remaining space in the volume group
|
||||
- lvol:
|
||||
vg: firefly
|
||||
lv: test
|
||||
size: 100%FREE
|
||||
|
||||
# Create a logical volume with special options
|
||||
- lvol:
|
||||
vg: firefly
|
||||
lv: test
|
||||
size: 512g
|
||||
opts: -r 16
|
||||
|
||||
# Extend the logical volume to 1024m.
|
||||
- lvol:
|
||||
vg: firefly
|
||||
lv: test
|
||||
size: 1024
|
||||
|
||||
# Extend the logical volume to consume all remaining space in the volume group
|
||||
- lvol:
|
||||
vg: firefly
|
||||
lv: test
|
||||
size: +100%FREE
|
||||
|
||||
# Extend the logical volume to take all remaining space of the PVs
|
||||
- lvol:
|
||||
vg: firefly
|
||||
lv: test
|
||||
size: 100%PVS
|
||||
|
||||
# Resize the logical volume to % of VG
|
||||
- lvol:
|
||||
vg: firefly
|
||||
lv: test
|
||||
size: 80%VG
|
||||
force: yes
|
||||
|
||||
# Reduce the logical volume to 512m
|
||||
- lvol:
|
||||
vg: firefly
|
||||
lv: test
|
||||
size: 512
|
||||
force: yes
|
||||
|
||||
# Set the logical volume to 512m and do not try to shrink if size is lower than current one
|
||||
- lvol:
|
||||
vg: firefly
|
||||
lv: test
|
||||
size: 512
|
||||
shrink: no
|
||||
|
||||
# Remove the logical volume.
|
||||
- lvol:
|
||||
vg: firefly
|
||||
lv: test
|
||||
state: absent
|
||||
force: yes
|
||||
|
||||
# Create a snapshot volume of the test logical volume.
|
||||
- lvol:
|
||||
vg: firefly
|
||||
lv: test
|
||||
snapshot: snap1
|
||||
size: 100m
|
||||
|
||||
# Deactivate a logical volume
|
||||
- lvol:
|
||||
vg: firefly
|
||||
lv: test
|
||||
active: false
|
||||
|
||||
# Create a deactivated logical volume
|
||||
- lvol:
|
||||
vg: firefly
|
||||
lv: test
|
||||
size: 512g
|
||||
active: false
|
||||
'''
|
||||
|
||||
import re
|
||||
|
||||
decimal_point = re.compile(r"(\d+)")
|
||||
|
||||
def mkversion(major, minor, patch):
|
||||
return (1000 * 1000 * int(major)) + (1000 * int(minor)) + int(patch)
|
||||
|
||||
def parse_lvs(data):
|
||||
lvs = []
|
||||
for line in data.splitlines():
|
||||
parts = line.strip().split(';')
|
||||
lvs.append({
|
||||
'name': parts[0].replace('[','').replace(']',''),
|
||||
'size': int(decimal_point.match(parts[1]).group(1)),
|
||||
'active': (parts[2][4] == 'a')
|
||||
})
|
||||
return lvs
|
||||
|
||||
def parse_vgs(data):
|
||||
vgs = []
|
||||
for line in data.splitlines():
|
||||
parts = line.strip().split(';')
|
||||
vgs.append({
|
||||
'name': parts[0],
|
||||
'size': int(decimal_point.match(parts[1]).group(1)),
|
||||
'free': int(decimal_point.match(parts[2]).group(1)),
|
||||
'ext_size': int(decimal_point.match(parts[3]).group(1))
|
||||
})
|
||||
return vgs
|
||||
|
||||
|
||||
def get_lvm_version(module):
|
||||
ver_cmd = module.get_bin_path("lvm", required=True)
|
||||
rc, out, err = module.run_command("%s version" % (ver_cmd))
|
||||
if rc != 0:
|
||||
return None
|
||||
m = re.search("LVM version:\s+(\d+)\.(\d+)\.(\d+).*(\d{4}-\d{2}-\d{2})", out)
|
||||
if not m:
|
||||
return None
|
||||
return mkversion(m.group(1), m.group(2), m.group(3))
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
vg=dict(required=True),
|
||||
lv=dict(required=True),
|
||||
size=dict(type='str'),
|
||||
opts=dict(type='str'),
|
||||
state=dict(choices=["absent", "present"], default='present'),
|
||||
force=dict(type='bool', default='no'),
|
||||
shrink=dict(type='bool', default='yes'),
|
||||
active=dict(type='bool', default='yes'),
|
||||
snapshot=dict(type='str', default=None),
|
||||
pvs=dict(type='str')
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
# Determine if the "--yes" option should be used
|
||||
version_found = get_lvm_version(module)
|
||||
if version_found == None:
|
||||
module.fail_json(msg="Failed to get LVM version number")
|
||||
version_yesopt = mkversion(2, 2, 99) # First LVM with the "--yes" option
|
||||
if version_found >= version_yesopt:
|
||||
yesopt = "--yes"
|
||||
else:
|
||||
yesopt = ""
|
||||
|
||||
vg = module.params['vg']
|
||||
lv = module.params['lv']
|
||||
size = module.params['size']
|
||||
opts = module.params['opts']
|
||||
state = module.params['state']
|
||||
force = module.boolean(module.params['force'])
|
||||
shrink = module.boolean(module.params['shrink'])
|
||||
active = module.boolean(module.params['active'])
|
||||
size_opt = 'L'
|
||||
size_unit = 'm'
|
||||
snapshot = module.params['snapshot']
|
||||
pvs = module.params['pvs']
|
||||
|
||||
if pvs is None:
|
||||
pvs = ""
|
||||
else:
|
||||
pvs = pvs.replace(",", " ")
|
||||
|
||||
if opts is None:
|
||||
opts = ""
|
||||
|
||||
# Add --test option when running in check-mode
|
||||
if module.check_mode:
|
||||
test_opt = ' --test'
|
||||
else:
|
||||
test_opt = ''
|
||||
|
||||
if size:
|
||||
# LVCREATE(8) -l --extents option with percentage
|
||||
if '%' in size:
|
||||
size_parts = size.split('%', 1)
|
||||
size_percent = int(size_parts[0])
|
||||
if size_percent > 100:
|
||||
module.fail_json(msg="Size percentage cannot be larger than 100%")
|
||||
size_whole = size_parts[1]
|
||||
if size_whole == 'ORIGIN':
|
||||
module.fail_json(msg="Snapshot Volumes are not supported")
|
||||
elif size_whole not in ['VG', 'PVS', 'FREE']:
|
||||
module.fail_json(msg="Specify extents as a percentage of VG|PVS|FREE")
|
||||
size_opt = 'l'
|
||||
size_unit = ''
|
||||
|
||||
if not '%' in size:
|
||||
# LVCREATE(8) -L --size option unit
|
||||
if size[-1].lower() in 'bskmgtpe':
|
||||
size_unit = size[-1].lower()
|
||||
size = size[0:-1]
|
||||
|
||||
try:
|
||||
float(size)
|
||||
if not size[0].isdigit(): raise ValueError()
|
||||
except ValueError:
|
||||
module.fail_json(msg="Bad size specification of '%s'" % size)
|
||||
|
||||
# when no unit, megabytes by default
|
||||
if size_opt == 'l':
|
||||
unit = 'm'
|
||||
else:
|
||||
unit = size_unit
|
||||
|
||||
# Get information on volume group requested
|
||||
vgs_cmd = module.get_bin_path("vgs", required=True)
|
||||
rc, current_vgs, err = module.run_command(
|
||||
"%s --noheadings -o vg_name,size,free,vg_extent_size --units %s --separator ';' %s" % (vgs_cmd, unit, vg))
|
||||
|
||||
if rc != 0:
|
||||
if state == 'absent':
|
||||
module.exit_json(changed=False, stdout="Volume group %s does not exist." % vg)
|
||||
else:
|
||||
module.fail_json(msg="Volume group %s does not exist." % vg, rc=rc, err=err)
|
||||
|
||||
vgs = parse_vgs(current_vgs)
|
||||
this_vg = vgs[0]
|
||||
|
||||
# Get information on logical volume requested
|
||||
lvs_cmd = module.get_bin_path("lvs", required=True)
|
||||
rc, current_lvs, err = module.run_command(
|
||||
"%s -a --noheadings --nosuffix -o lv_name,size,lv_attr --units %s --separator ';' %s" % (lvs_cmd, unit, vg))
|
||||
|
||||
if rc != 0:
|
||||
if state == 'absent':
|
||||
module.exit_json(changed=False, stdout="Volume group %s does not exist." % vg)
|
||||
else:
|
||||
module.fail_json(msg="Volume group %s does not exist." % vg, rc=rc, err=err)
|
||||
|
||||
changed = False
|
||||
|
||||
lvs = parse_lvs(current_lvs)
|
||||
|
||||
if snapshot is None:
|
||||
check_lv = lv
|
||||
else:
|
||||
check_lv = snapshot
|
||||
for test_lv in lvs:
|
||||
if test_lv['name'] == check_lv:
|
||||
this_lv = test_lv
|
||||
break
|
||||
else:
|
||||
this_lv = None
|
||||
|
||||
if state == 'present' and not size:
|
||||
if this_lv is None:
|
||||
module.fail_json(msg="No size given.")
|
||||
|
||||
msg = ''
|
||||
if this_lv is None:
|
||||
if state == 'present':
|
||||
### create LV
|
||||
lvcreate_cmd = module.get_bin_path("lvcreate", required=True)
|
||||
if snapshot is not None:
|
||||
cmd = "%s %s %s -%s %s%s -s -n %s %s %s/%s" % (lvcreate_cmd, test_opt, yesopt, size_opt, size, size_unit, snapshot, opts, vg, lv)
|
||||
else:
|
||||
cmd = "%s %s %s -n %s -%s %s%s %s %s %s" % (lvcreate_cmd, test_opt, yesopt, lv, size_opt, size, size_unit, opts, vg, pvs)
|
||||
rc, _, err = module.run_command(cmd)
|
||||
if rc == 0:
|
||||
changed = True
|
||||
else:
|
||||
module.fail_json(msg="Creating logical volume '%s' failed" % lv, rc=rc, err=err)
|
||||
else:
|
||||
if state == 'absent':
|
||||
### remove LV
|
||||
if not force:
|
||||
module.fail_json(msg="Sorry, no removal of logical volume %s without force=yes." % (this_lv['name']))
|
||||
lvremove_cmd = module.get_bin_path("lvremove", required=True)
|
||||
rc, _, err = module.run_command("%s %s --force %s/%s" % (lvremove_cmd, test_opt, vg, this_lv['name']))
|
||||
if rc == 0:
|
||||
module.exit_json(changed=True)
|
||||
else:
|
||||
module.fail_json(msg="Failed to remove logical volume %s" % (lv), rc=rc, err=err)
|
||||
|
||||
elif not size:
|
||||
pass
|
||||
|
||||
elif size_opt == 'l':
|
||||
### Resize LV based on % value
|
||||
tool = None
|
||||
size_free = this_vg['free']
|
||||
if size_whole == 'VG' or size_whole == 'PVS':
|
||||
size_requested = size_percent * this_vg['size'] / 100
|
||||
else: # size_whole == 'FREE':
|
||||
size_requested = size_percent * this_vg['free'] / 100
|
||||
if '+' in size:
|
||||
size_requested += this_lv['size']
|
||||
if this_lv['size'] < size_requested:
|
||||
if (size_free > 0) and (('+' not in size) or (size_free >= (size_requested - this_lv['size']))):
|
||||
tool = module.get_bin_path("lvextend", required=True)
|
||||
else:
|
||||
module.fail_json(msg="Logical Volume %s could not be extended. Not enough free space left (%s%s required / %s%s available)" % (this_lv['name'], (size_requested - this_lv['size']), unit, size_free, unit))
|
||||
elif shrink and this_lv['size'] > size_requested + this_vg['ext_size']: # more than an extent too large
|
||||
if size_requested == 0:
|
||||
module.fail_json(msg="Sorry, no shrinking of %s to 0 permitted." % (this_lv['name']))
|
||||
elif not force:
|
||||
module.fail_json(msg="Sorry, no shrinking of %s without force=yes" % (this_lv['name']))
|
||||
else:
|
||||
tool = module.get_bin_path("lvreduce", required=True)
|
||||
tool = '%s %s' % (tool, '--force')
|
||||
|
||||
if tool:
|
||||
cmd = "%s %s -%s %s%s %s/%s %s" % (tool, test_opt, size_opt, size, size_unit, vg, this_lv['name'], pvs)
|
||||
rc, out, err = module.run_command(cmd)
|
||||
if "Reached maximum COW size" in out:
|
||||
module.fail_json(msg="Unable to resize %s to %s%s" % (lv, size, size_unit), rc=rc, err=err, out=out)
|
||||
elif rc == 0:
|
||||
changed = True
|
||||
msg="Volume %s resized to %s%s" % (this_lv['name'], size_requested, unit)
|
||||
elif "matches existing size" in err:
|
||||
module.exit_json(changed=False, vg=vg, lv=this_lv['name'], size=this_lv['size'])
|
||||
elif "not larger than existing size" in err:
|
||||
module.exit_json(changed=False, vg=vg, lv=this_lv['name'], size=this_lv['size'], msg="Original size is larger than requested size", err=err)
|
||||
else:
|
||||
module.fail_json(msg="Unable to resize %s to %s%s" % (lv, size, size_unit), rc=rc, err=err)
|
||||
|
||||
else:
|
||||
### resize LV based on absolute values
|
||||
tool = None
|
||||
if int(size) > this_lv['size']:
|
||||
tool = module.get_bin_path("lvextend", required=True)
|
||||
elif shrink and int(size) < this_lv['size']:
|
||||
if int(size) == 0:
|
||||
module.fail_json(msg="Sorry, no shrinking of %s to 0 permitted." % (this_lv['name']))
|
||||
if not force:
|
||||
module.fail_json(msg="Sorry, no shrinking of %s without force=yes." % (this_lv['name']))
|
||||
else:
|
||||
tool = module.get_bin_path("lvreduce", required=True)
|
||||
tool = '%s %s' % (tool, '--force')
|
||||
|
||||
if tool:
|
||||
cmd = "%s %s -%s %s%s %s/%s %s" % (tool, test_opt, size_opt, size, size_unit, vg, this_lv['name'], pvs)
|
||||
rc, out, err = module.run_command(cmd)
|
||||
if "Reached maximum COW size" in out:
|
||||
module.fail_json(msg="Unable to resize %s to %s%s" % (lv, size, size_unit), rc=rc, err=err, out=out)
|
||||
elif rc == 0:
|
||||
changed = True
|
||||
elif "matches existing size" in err:
|
||||
module.exit_json(changed=False, vg=vg, lv=this_lv['name'], size=this_lv['size'])
|
||||
elif "not larger than existing size" in err:
|
||||
module.exit_json(changed=False, vg=vg, lv=this_lv['name'], size=this_lv['size'], msg="Original size is larger than requested size", err=err)
|
||||
else:
|
||||
module.fail_json(msg="Unable to resize %s to %s%s" % (lv, size, size_unit), rc=rc, err=err)
|
||||
|
||||
if this_lv is not None:
|
||||
if active:
|
||||
lvchange_cmd = module.get_bin_path("lvchange", required=True)
|
||||
rc, _, err = module.run_command("%s -ay %s/%s" % (lvchange_cmd, vg, this_lv['name']))
|
||||
if rc == 0:
|
||||
module.exit_json(changed=((not this_lv['active']) or changed), vg=vg, lv=this_lv['name'], size=this_lv['size'])
|
||||
else:
|
||||
module.fail_json(msg="Failed to activate logical volume %s" % (lv), rc=rc, err=err)
|
||||
else:
|
||||
lvchange_cmd = module.get_bin_path("lvchange", required=True)
|
||||
rc, _, err = module.run_command("%s -an %s/%s" % (lvchange_cmd, vg, this_lv['name']))
|
||||
if rc == 0:
|
||||
module.exit_json(changed=(this_lv['active'] or changed), vg=vg, lv=this_lv['name'], size=this_lv['size'])
|
||||
else:
|
||||
module.fail_json(msg="Failed to deactivate logical volume %s" % (lv), rc=rc, err=err)
|
||||
|
||||
module.exit_json(changed=changed, msg=msg)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
161
lib/ansible/modules/system/make.py
Normal file
161
lib/ansible/modules/system/make.py
Normal file
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2015, Linus Unnebäck <linus@folkdatorn.se>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: make
|
||||
short_description: Run targets in a Makefile
|
||||
requirements: [ make ]
|
||||
version_added: "2.1"
|
||||
author: Linus Unnebäck (@LinusU) <linus@folkdatorn.se>
|
||||
description:
|
||||
- Run targets in a Makefile.
|
||||
options:
|
||||
target:
|
||||
description:
|
||||
- The target to run
|
||||
required: false
|
||||
default: none
|
||||
params:
|
||||
description:
|
||||
- Any extra parameters to pass to make
|
||||
required: false
|
||||
default: none
|
||||
chdir:
|
||||
description:
|
||||
- cd into this directory before running make
|
||||
required: true
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Build the default target
|
||||
- make:
|
||||
chdir: /home/ubuntu/cool-project
|
||||
|
||||
# Run `install` target as root
|
||||
- make:
|
||||
chdir: /home/ubuntu/cool-project
|
||||
target: install
|
||||
become: yes
|
||||
|
||||
# Pass in extra arguments to build
|
||||
- make:
|
||||
chdir: /home/ubuntu/cool-project
|
||||
target: all
|
||||
params:
|
||||
NUM_THREADS: 4
|
||||
BACKEND: lapack
|
||||
'''
|
||||
|
||||
# TODO: Disabled the RETURN as it was breaking docs building. Someone needs to
|
||||
# fix this
|
||||
RETURN = '''# '''
|
||||
|
||||
from ansible.module_utils.six import iteritems
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
|
||||
def run_command(command, module, check_rc=True):
|
||||
"""
|
||||
Run a command using the module, return
|
||||
the result code and std{err,out} content.
|
||||
|
||||
:param command: list of command arguments
|
||||
:param module: Ansible make module instance
|
||||
:return: return code, stdout content, stderr content
|
||||
"""
|
||||
rc, out, err = module.run_command(command, check_rc=check_rc, cwd=module.params['chdir'])
|
||||
return rc, sanitize_output(out), sanitize_output(err)
|
||||
|
||||
|
||||
def sanitize_output(output):
|
||||
"""
|
||||
Sanitize the output string before we
|
||||
pass it to module.fail_json. Defaults
|
||||
the string to empty if it is None, else
|
||||
strips trailing newlines.
|
||||
|
||||
:param output: output to sanitize
|
||||
:return: sanitized output
|
||||
"""
|
||||
if output is None:
|
||||
return ''
|
||||
else:
|
||||
return output.rstrip("\r\n")
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
supports_check_mode=True,
|
||||
argument_spec=dict(
|
||||
target=dict(required=False, default=None, type='str'),
|
||||
params=dict(required=False, default=None, type='dict'),
|
||||
chdir=dict(required=True, default=None, type='path'),
|
||||
),
|
||||
)
|
||||
# Build up the invocation of `make` we are going to use
|
||||
make_path = module.get_bin_path('make', True)
|
||||
make_target = module.params['target']
|
||||
if module.params['params'] is not None:
|
||||
make_parameters = [k + '=' + str(v) for k, v in iteritems(module.params['params'])]
|
||||
else:
|
||||
make_parameters = []
|
||||
|
||||
base_command = [make_path, make_target]
|
||||
base_command.extend(make_parameters)
|
||||
|
||||
# Check if the target is already up to date
|
||||
rc, out, err = run_command(base_command + ['--question'], module, check_rc=False)
|
||||
if module.check_mode:
|
||||
# If we've been asked to do a dry run, we only need
|
||||
# to report whether or not the target is up to date
|
||||
changed = (rc != 0)
|
||||
else:
|
||||
if rc == 0:
|
||||
# The target is up to date, so we don't have to
|
||||
# do anything
|
||||
changed = False
|
||||
else:
|
||||
# The target isn't upd to date, so we need to run it
|
||||
rc, out, err = run_command(base_command, module)
|
||||
changed = True
|
||||
|
||||
# We don't report the return code, as if this module failed
|
||||
# we would be calling fail_json from run_command, so even if
|
||||
# we had a non-zero return code, we did not fail. However, if
|
||||
# we report a non-zero return code here, we will be marked as
|
||||
# failed regardless of what we signal using the failed= kwarg.
|
||||
module.exit_json(
|
||||
changed=changed,
|
||||
failed=False,
|
||||
stdout=out,
|
||||
stderr=err,
|
||||
target=module.params['target'],
|
||||
params=module.params['params'],
|
||||
chdir=module.params['chdir']
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
135
lib/ansible/modules/system/modprobe.py
Normal file
135
lib/ansible/modules/system/modprobe.py
Normal file
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/python
|
||||
#coding: utf-8 -*-
|
||||
|
||||
# (c) 2013, David Stygstra <david.stygstra@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: modprobe
|
||||
short_description: Add or remove kernel modules
|
||||
requirements: []
|
||||
version_added: 1.4
|
||||
author:
|
||||
- "David Stygstra (@stygstra)"
|
||||
- "Julien Dauphant"
|
||||
- "Matt Jeffery"
|
||||
description:
|
||||
- Add or remove kernel modules.
|
||||
options:
|
||||
name:
|
||||
required: true
|
||||
description:
|
||||
- Name of kernel module to manage.
|
||||
state:
|
||||
required: false
|
||||
default: "present"
|
||||
choices: [ present, absent ]
|
||||
description:
|
||||
- Whether the module should be present or absent.
|
||||
params:
|
||||
required: false
|
||||
default: ""
|
||||
version_added: "1.6"
|
||||
description:
|
||||
- Modules parameters.
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Add the 802.1q module
|
||||
- modprobe:
|
||||
name: 8021q
|
||||
state: present
|
||||
|
||||
# Add the dummy module
|
||||
- modprobe:
|
||||
name: dummy
|
||||
state: present
|
||||
params: 'numdummies=2'
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
import shlex
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec={
|
||||
'name': {'required': True},
|
||||
'state': {'default': 'present', 'choices': ['present', 'absent']},
|
||||
'params': {'default': ''},
|
||||
},
|
||||
supports_check_mode=True,
|
||||
)
|
||||
args = {
|
||||
'changed': False,
|
||||
'failed': False,
|
||||
'name': module.params['name'],
|
||||
'state': module.params['state'],
|
||||
'params': module.params['params'],
|
||||
}
|
||||
|
||||
# Check if module is present
|
||||
try:
|
||||
modules = open('/proc/modules')
|
||||
present = False
|
||||
module_name = args['name'].replace('-', '_') + ' '
|
||||
for line in modules:
|
||||
if line.startswith(module_name):
|
||||
present = True
|
||||
break
|
||||
modules.close()
|
||||
except IOError:
|
||||
e = get_exception()
|
||||
module.fail_json(msg=str(e), **args)
|
||||
|
||||
# Check only; don't modify
|
||||
if module.check_mode:
|
||||
if args['state'] == 'present' and not present:
|
||||
changed = True
|
||||
elif args['state'] == 'absent' and present:
|
||||
changed = True
|
||||
else:
|
||||
changed = False
|
||||
module.exit_json(changed=changed)
|
||||
|
||||
# Add/remove module as needed
|
||||
if args['state'] == 'present':
|
||||
if not present:
|
||||
command = [module.get_bin_path('modprobe', True), args['name']]
|
||||
command.extend(shlex.split(args['params']))
|
||||
rc, _, err = module.run_command(command)
|
||||
if rc != 0:
|
||||
module.fail_json(msg=err, **args)
|
||||
args['changed'] = True
|
||||
elif args['state'] == 'absent':
|
||||
if present:
|
||||
rc, _, err = module.run_command([module.get_bin_path('modprobe', True), '-r', args['name']])
|
||||
if rc != 0:
|
||||
module.fail_json(msg=err, **args)
|
||||
args['changed'] = True
|
||||
|
||||
module.exit_json(**args)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
61
lib/ansible/modules/system/ohai.py
Normal file
61
lib/ansible/modules/system/ohai.py
Normal file
@@ -0,0 +1,61 @@
|
||||
#!/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/>.
|
||||
#
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: ohai
|
||||
short_description: Returns inventory data from I(Ohai)
|
||||
description:
|
||||
- Similar to the M(facter) module, this runs the I(Ohai) discovery program
|
||||
(U(http://wiki.opscode.com/display/chef/Ohai)) on the remote host and
|
||||
returns JSON inventory data.
|
||||
I(Ohai) data is a bit more verbose and nested than I(facter).
|
||||
version_added: "0.6"
|
||||
options: {}
|
||||
notes: []
|
||||
requirements: [ "ohai" ]
|
||||
author:
|
||||
- "Ansible Core Team"
|
||||
- "Michael DeHaan (@mpdehaan)"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Retrieve (ohai) data from all Web servers and store in one-file per host
|
||||
ansible webservers -m ohai --tree=/tmp/ohaidata
|
||||
'''
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict()
|
||||
)
|
||||
cmd = ["/usr/bin/env", "ohai"]
|
||||
rc, out, err = module.run_command(cmd, check_rc=True)
|
||||
module.exit_json(**json.loads(out))
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
389
lib/ansible/modules/system/open_iscsi.py
Normal file
389
lib/ansible/modules/system/open_iscsi.py
Normal file
@@ -0,0 +1,389 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2013, Serge van Ginderachter <serge@vanginderachter.be>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: open_iscsi
|
||||
author: "Serge van Ginderachter (@srvg)"
|
||||
version_added: "1.4"
|
||||
short_description: Manage iscsi targets with open-iscsi
|
||||
description:
|
||||
- Discover targets on given portal, (dis)connect targets, mark targets to
|
||||
manually or auto start, return device nodes of connected targets.
|
||||
requirements:
|
||||
- open_iscsi library and tools (iscsiadm)
|
||||
options:
|
||||
portal:
|
||||
required: false
|
||||
aliases: [ip]
|
||||
description:
|
||||
- the ip address of the iscsi target
|
||||
port:
|
||||
required: false
|
||||
default: 3260
|
||||
description:
|
||||
- the port on which the iscsi target process listens
|
||||
target:
|
||||
required: false
|
||||
aliases: [name, targetname]
|
||||
description:
|
||||
- the iscsi target name
|
||||
login:
|
||||
required: false
|
||||
choices: [true, false]
|
||||
description:
|
||||
- whether the target node should be connected
|
||||
node_auth:
|
||||
required: false
|
||||
default: CHAP
|
||||
description:
|
||||
- discovery.sendtargets.auth.authmethod
|
||||
node_user:
|
||||
required: false
|
||||
description:
|
||||
- discovery.sendtargets.auth.username
|
||||
node_pass:
|
||||
required: false
|
||||
description:
|
||||
- discovery.sendtargets.auth.password
|
||||
auto_node_startup:
|
||||
aliases: [automatic]
|
||||
required: false
|
||||
choices: [true, false]
|
||||
description:
|
||||
- whether the target node should be automatically connected at startup
|
||||
discover:
|
||||
required: false
|
||||
choices: [true, false]
|
||||
description:
|
||||
- whether the list of target nodes on the portal should be
|
||||
(re)discovered and added to the persistent iscsi database.
|
||||
Keep in mind that iscsiadm discovery resets configurtion, like node.startup
|
||||
to manual, hence combined with auto_node_startup=yes will allways return
|
||||
a changed state.
|
||||
show_nodes:
|
||||
required: false
|
||||
choices: [true, false]
|
||||
description:
|
||||
- whether the list of nodes in the persistent iscsi database should be
|
||||
returned by the module
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# perform a discovery on 10.1.2.3 and show available target nodes
|
||||
- open_iscsi:
|
||||
show_nodes: yes
|
||||
discover: yes
|
||||
portal: 10.1.2.3
|
||||
|
||||
# discover targets on portal and login to the one available
|
||||
# (only works if exactly one target is exported to the initiator)
|
||||
- open_iscsi:
|
||||
portal: '{{ iscsi_target }}'
|
||||
login: yes
|
||||
discover: yes
|
||||
|
||||
# description: connect to the named target, after updating the local
|
||||
# persistent database (cache)
|
||||
- open_iscsi:
|
||||
login: yes
|
||||
target: 'iqn.1986-03.com.sun:02:f8c1f9e0-c3ec-ec84-c9c9-8bfb0cd5de3d'
|
||||
|
||||
# description: discconnect from the cached named target
|
||||
- open_iscsi:
|
||||
login: no
|
||||
target: 'iqn.1986-03.com.sun:02:f8c1f9e0-c3ec-ec84-c9c9-8bfb0cd5de3d'
|
||||
'''
|
||||
|
||||
import glob
|
||||
import time
|
||||
|
||||
ISCSIADM = 'iscsiadm'
|
||||
|
||||
|
||||
def compare_nodelists(l1, l2):
|
||||
|
||||
l1.sort()
|
||||
l2.sort()
|
||||
return l1 == l2
|
||||
|
||||
|
||||
def iscsi_get_cached_nodes(module, portal=None):
|
||||
|
||||
cmd = '%s --mode node' % iscsiadm_cmd
|
||||
(rc, out, err) = module.run_command(cmd)
|
||||
|
||||
if rc == 0:
|
||||
lines = out.splitlines()
|
||||
nodes = []
|
||||
for line in lines:
|
||||
# line format is "ip:port,target_portal_group_tag targetname"
|
||||
parts = line.split()
|
||||
if len(parts) > 2:
|
||||
module.fail_json(msg='error parsing output', cmd=cmd)
|
||||
target = parts[1]
|
||||
parts = parts[0].split(':')
|
||||
target_portal = parts[0]
|
||||
|
||||
if portal is None or portal == target_portal:
|
||||
nodes.append(target)
|
||||
|
||||
# older versions of scsiadm don't have nice return codes
|
||||
# for newer versions see iscsiadm(8); also usr/iscsiadm.c for details
|
||||
# err can contain [N|n]o records...
|
||||
elif rc == 21 or (rc == 255 and "o records found" in err):
|
||||
nodes = []
|
||||
else:
|
||||
module.fail_json(cmd=cmd, rc=rc, msg=err)
|
||||
|
||||
return nodes
|
||||
|
||||
|
||||
def iscsi_discover(module, portal, port):
|
||||
|
||||
cmd = '%s --mode discovery --type sendtargets --portal %s:%s' % (iscsiadm_cmd, portal, port)
|
||||
(rc, out, err) = module.run_command(cmd)
|
||||
|
||||
if rc > 0:
|
||||
module.fail_json(cmd=cmd, rc=rc, msg=err)
|
||||
|
||||
|
||||
def target_loggedon(module, target):
|
||||
|
||||
cmd = '%s --mode session' % iscsiadm_cmd
|
||||
(rc, out, err) = module.run_command(cmd)
|
||||
|
||||
if rc == 0:
|
||||
return target in out
|
||||
elif rc == 21:
|
||||
return False
|
||||
else:
|
||||
module.fail_json(cmd=cmd, rc=rc, msg=err)
|
||||
|
||||
|
||||
def target_login(module, target):
|
||||
|
||||
node_auth = module.params['node_auth']
|
||||
node_user = module.params['node_user']
|
||||
node_pass = module.params['node_pass']
|
||||
|
||||
if node_user:
|
||||
params = [('node.session.auth.authmethod', node_auth),
|
||||
('node.session.auth.username', node_user),
|
||||
('node.session.auth.password', node_pass)]
|
||||
for (name, value) in params:
|
||||
cmd = '%s --mode node --targetname %s --op=update --name %s --value %s' % (iscsiadm_cmd, target, name, value)
|
||||
(rc, out, err) = module.run_command(cmd)
|
||||
if rc > 0:
|
||||
module.fail_json(cmd=cmd, rc=rc, msg=err)
|
||||
|
||||
cmd = '%s --mode node --targetname %s --login' % (iscsiadm_cmd, target)
|
||||
(rc, out, err) = module.run_command(cmd)
|
||||
|
||||
if rc > 0:
|
||||
module.fail_json(cmd=cmd, rc=rc, msg=err)
|
||||
|
||||
|
||||
def target_logout(module, target):
|
||||
|
||||
cmd = '%s --mode node --targetname %s --logout' % (iscsiadm_cmd, target)
|
||||
(rc, out, err) = module.run_command(cmd)
|
||||
|
||||
if rc > 0:
|
||||
module.fail_json(cmd=cmd, rc=rc, msg=err)
|
||||
|
||||
|
||||
def target_device_node(module, target):
|
||||
|
||||
# if anyone know a better way to find out which devicenodes get created for
|
||||
# a given target...
|
||||
|
||||
devices = glob.glob('/dev/disk/by-path/*%s*' % target)
|
||||
devdisks = []
|
||||
for dev in devices:
|
||||
# exclude partitions
|
||||
if "-part" not in dev:
|
||||
devdisk = os.path.realpath(dev)
|
||||
# only add once (multi-path?)
|
||||
if devdisk not in devdisks:
|
||||
devdisks.append(devdisk)
|
||||
return devdisks
|
||||
|
||||
|
||||
def target_isauto(module, target):
|
||||
|
||||
cmd = '%s --mode node --targetname %s' % (iscsiadm_cmd, target)
|
||||
(rc, out, err) = module.run_command(cmd)
|
||||
|
||||
if rc == 0:
|
||||
lines = out.splitlines()
|
||||
for line in lines:
|
||||
if 'node.startup' in line:
|
||||
return 'automatic' in line
|
||||
return False
|
||||
else:
|
||||
module.fail_json(cmd=cmd, rc=rc, msg=err)
|
||||
|
||||
|
||||
def target_setauto(module, target):
|
||||
|
||||
cmd = '%s --mode node --targetname %s --op=update --name node.startup --value automatic' % (iscsiadm_cmd, target)
|
||||
(rc, out, err) = module.run_command(cmd)
|
||||
|
||||
if rc > 0:
|
||||
module.fail_json(cmd=cmd, rc=rc, msg=err)
|
||||
|
||||
|
||||
def target_setmanual(module, target):
|
||||
|
||||
cmd = '%s --mode node --targetname %s --op=update --name node.startup --value manual' % (iscsiadm_cmd, target)
|
||||
(rc, out, err) = module.run_command(cmd)
|
||||
|
||||
if rc > 0:
|
||||
module.fail_json(cmd=cmd, rc=rc, msg=err)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
# load ansible module object
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
|
||||
# target
|
||||
portal = dict(required=False, aliases=['ip']),
|
||||
port = dict(required=False, default=3260),
|
||||
target = dict(required=False, aliases=['name', 'targetname']),
|
||||
node_auth = dict(required=False, default='CHAP'),
|
||||
node_user = dict(required=False),
|
||||
node_pass = dict(required=False),
|
||||
|
||||
# actions
|
||||
login = dict(type='bool', aliases=['state']),
|
||||
auto_node_startup = dict(type='bool', aliases=['automatic']),
|
||||
discover = dict(type='bool', default=False),
|
||||
show_nodes = dict(type='bool', default=False)
|
||||
),
|
||||
|
||||
required_together=[['discover_user', 'discover_pass'],
|
||||
['node_user', 'node_pass']],
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
global iscsiadm_cmd
|
||||
iscsiadm_cmd = module.get_bin_path('iscsiadm', required=True)
|
||||
|
||||
# parameters
|
||||
portal = module.params['portal']
|
||||
target = module.params['target']
|
||||
port = module.params['port']
|
||||
login = module.params['login']
|
||||
automatic = module.params['auto_node_startup']
|
||||
discover = module.params['discover']
|
||||
show_nodes = module.params['show_nodes']
|
||||
|
||||
check = module.check_mode
|
||||
|
||||
cached = iscsi_get_cached_nodes(module, portal)
|
||||
|
||||
# return json dict
|
||||
result = {}
|
||||
result['changed'] = False
|
||||
|
||||
if discover:
|
||||
if portal is None:
|
||||
module.fail_json(msg = "Need to specify at least the portal (ip) to discover")
|
||||
elif check:
|
||||
nodes = cached
|
||||
else:
|
||||
iscsi_discover(module, portal, port)
|
||||
nodes = iscsi_get_cached_nodes(module, portal)
|
||||
if not compare_nodelists(cached, nodes):
|
||||
result['changed'] |= True
|
||||
result['cache_updated'] = True
|
||||
else:
|
||||
nodes = cached
|
||||
|
||||
if login is not None or automatic is not None:
|
||||
if target is None:
|
||||
if len(nodes) > 1:
|
||||
module.fail_json(msg = "Need to specify a target")
|
||||
else:
|
||||
target = nodes[0]
|
||||
else:
|
||||
# check given target is in cache
|
||||
check_target = False
|
||||
for node in nodes:
|
||||
if node == target:
|
||||
check_target = True
|
||||
break
|
||||
if not check_target:
|
||||
module.fail_json(msg = "Specified target not found")
|
||||
|
||||
if show_nodes:
|
||||
result['nodes'] = nodes
|
||||
|
||||
if login is not None:
|
||||
loggedon = target_loggedon(module, target)
|
||||
if (login and loggedon) or (not login and not loggedon):
|
||||
result['changed'] |= False
|
||||
if login:
|
||||
result['devicenodes'] = target_device_node(module, target)
|
||||
elif not check:
|
||||
if login:
|
||||
target_login(module, target)
|
||||
# give udev some time
|
||||
time.sleep(1)
|
||||
result['devicenodes'] = target_device_node(module, target)
|
||||
else:
|
||||
target_logout(module, target)
|
||||
result['changed'] |= True
|
||||
result['connection_changed'] = True
|
||||
else:
|
||||
result['changed'] |= True
|
||||
result['connection_changed'] = True
|
||||
|
||||
if automatic is not None:
|
||||
isauto = target_isauto(module, target)
|
||||
if (automatic and isauto) or (not automatic and not isauto):
|
||||
result['changed'] |= False
|
||||
result['automatic_changed'] = False
|
||||
elif not check:
|
||||
if automatic:
|
||||
target_setauto(module, target)
|
||||
else:
|
||||
target_setmanual(module, target)
|
||||
result['changed'] |= True
|
||||
result['automatic_changed'] = True
|
||||
else:
|
||||
result['changed'] |= True
|
||||
result['automatic_changed'] = True
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
213
lib/ansible/modules/system/openwrt_init.py
Normal file
213
lib/ansible/modules/system/openwrt_init.py
Normal file
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
# (c) 2016, Andrew Gaffney <andrew@agaffney.org>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'committer',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
module: openwrt_init
|
||||
author:
|
||||
- "Andrew Gaffney (@agaffney)"
|
||||
version_added: "2.3"
|
||||
short_description: Manage services on OpenWrt.
|
||||
description:
|
||||
- Controls OpenWrt services on remote hosts.
|
||||
options:
|
||||
name:
|
||||
required: true
|
||||
description:
|
||||
- Name of the service.
|
||||
aliases: ['service']
|
||||
state:
|
||||
required: false
|
||||
default: null
|
||||
choices: [ 'started', 'stopped', 'restarted', 'reloaded' ]
|
||||
description:
|
||||
- C(started)/C(stopped) are idempotent actions that will not run commands unless necessary.
|
||||
C(restarted) will always bounce the service. C(reloaded) will always reload.
|
||||
enabled:
|
||||
required: false
|
||||
choices: [ "yes", "no" ]
|
||||
default: null
|
||||
description:
|
||||
- Whether the service should start on boot. B(At least one of state and enabled are required.)
|
||||
pattern:
|
||||
required: false
|
||||
description:
|
||||
- If the service does not respond to the 'running' command, name a
|
||||
substring to look for as would be found in the output of the I(ps)
|
||||
command as a stand-in for a 'running' result. If the string is found,
|
||||
the service will be assumed to be running.
|
||||
notes:
|
||||
- One option other than name is required.
|
||||
requirements:
|
||||
- An OpenWrt system
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Example action to start service httpd, if not running
|
||||
- openwrt_init:
|
||||
state: started
|
||||
name: httpd
|
||||
|
||||
# Example action to stop service cron, if running
|
||||
- openwrt_init:
|
||||
name: cron
|
||||
state: stopped
|
||||
|
||||
# Example action to reload service httpd, in all cases
|
||||
- openwrt_init:
|
||||
name: httpd
|
||||
state: reloaded
|
||||
|
||||
# Example action to enable service httpd
|
||||
- openwrt_init:
|
||||
name: httpd
|
||||
enabled: yes
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
'''
|
||||
|
||||
import os
|
||||
import glob
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_bytes, to_native
|
||||
|
||||
module = None
|
||||
init_script = None
|
||||
|
||||
# ===============================
|
||||
# Check if service is enabled
|
||||
def is_enabled():
|
||||
(rc, out, err) = module.run_command("%s enabled" % init_script)
|
||||
if rc == 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
# ===========================================
|
||||
# Main control flow
|
||||
|
||||
def main():
|
||||
global module, init_script
|
||||
# init
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
name = dict(required=True, type='str', aliases=['service']),
|
||||
state = dict(choices=['started', 'stopped', 'restarted', 'reloaded'], type='str'),
|
||||
enabled = dict(type='bool'),
|
||||
pattern = dict(required=False, default=None),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
required_one_of=[['state', 'enabled']],
|
||||
)
|
||||
|
||||
# initialize
|
||||
service = module.params['name']
|
||||
init_script = '/etc/init.d/' + service
|
||||
rc = 0
|
||||
out = err = ''
|
||||
result = {
|
||||
'name': service,
|
||||
'changed': False,
|
||||
}
|
||||
|
||||
# check if service exists
|
||||
if not os.path.exists(init_script):
|
||||
module.fail_json(msg='service %s does not exist' % service)
|
||||
|
||||
# Enable/disable service startup at boot if requested
|
||||
if module.params['enabled'] is not None:
|
||||
# do we need to enable the service?
|
||||
enabled = is_enabled()
|
||||
|
||||
# default to current state
|
||||
result['enabled'] = enabled
|
||||
|
||||
# Change enable/disable if needed
|
||||
if enabled != module.params['enabled']:
|
||||
result['changed'] = True
|
||||
if module.params['enabled']:
|
||||
action = 'enable'
|
||||
else:
|
||||
action = 'disable'
|
||||
|
||||
if not module.check_mode:
|
||||
(rc, out, err) = module.run_command("%s %s" % (init_script, action))
|
||||
# openwrt init scripts can return a non-zero exit code on a successful 'enable'
|
||||
# command if the init script doesn't contain a STOP value, so we ignore the exit
|
||||
# code and explicitly check if the service is now in the desired state
|
||||
if is_enabled() != module.params['enabled']:
|
||||
module.fail_json(msg="Unable to %s service %s: %s" % (action, service, err))
|
||||
|
||||
result['enabled'] = not enabled
|
||||
|
||||
if module.params['state'] is not None:
|
||||
running = False
|
||||
|
||||
# check if service is currently running
|
||||
if module.params['pattern']:
|
||||
# Find ps binary
|
||||
psbin = module.get_bin_path('ps', True)
|
||||
|
||||
# this should be busybox ps, so we only want/need to the 'w' option
|
||||
(rc, psout, pserr) = module.run_command('%s w' % psbin)
|
||||
# If rc is 0, set running as appropriate
|
||||
if rc == 0:
|
||||
lines = psout.split("\n")
|
||||
for line in lines:
|
||||
if module.params['pattern'] in line and not "pattern=" in line:
|
||||
# so as to not confuse ./hacking/test-module
|
||||
running = True
|
||||
break
|
||||
else:
|
||||
(rc, out, err) = module.run_command("%s running" % init_script)
|
||||
if rc == 0:
|
||||
running = True
|
||||
|
||||
# default to desired state
|
||||
result['state'] = module.params['state']
|
||||
|
||||
# determine action, if any
|
||||
action = None
|
||||
if module.params['state'] == 'started':
|
||||
if not running:
|
||||
action = 'start'
|
||||
result['changed'] = True
|
||||
elif module.params['state'] == 'stopped':
|
||||
if running:
|
||||
action = 'stop'
|
||||
result['changed'] = True
|
||||
else:
|
||||
action = module.params['state'][:-2] # remove 'ed' from restarted/reloaded
|
||||
result['state'] = 'started'
|
||||
result['changed'] = True
|
||||
|
||||
if action:
|
||||
if not module.check_mode:
|
||||
(rc, out, err) = module.run_command("%s %s" % (init_script, action))
|
||||
if rc != 0:
|
||||
module.fail_json(msg="Unable to %s service %s: %s" % (action, service, err))
|
||||
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
420
lib/ansible/modules/system/osx_defaults.py
Normal file
420
lib/ansible/modules/system/osx_defaults.py
Normal file
@@ -0,0 +1,420 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2014, GeekChimp - Franck Nijhof <franck@geekchimp.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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['stableinterface'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: osx_defaults
|
||||
author: Franck Nijhof (@frenck)
|
||||
short_description: osx_defaults allows users to read, write, and delete Mac OS X user defaults from Ansible
|
||||
description:
|
||||
- osx_defaults allows users to read, write, and delete Mac OS X user defaults from Ansible scripts.
|
||||
Mac OS X applications and other programs use the defaults system to record user preferences and other
|
||||
information that must be maintained when the applications aren't running (such as default font for new
|
||||
documents, or the position of an Info panel).
|
||||
version_added: "2.0"
|
||||
options:
|
||||
domain:
|
||||
description:
|
||||
- The domain is a domain name of the form com.companyname.appname.
|
||||
required: false
|
||||
default: NSGlobalDomain
|
||||
host:
|
||||
description:
|
||||
- The host on which the preference should apply. The special value "currentHost" corresponds to the
|
||||
"-currentHost" switch of the defaults commandline tool.
|
||||
required: false
|
||||
default: null
|
||||
version_added: "2.1"
|
||||
key:
|
||||
description:
|
||||
- The key of the user preference
|
||||
required: true
|
||||
type:
|
||||
description:
|
||||
- The type of value to write.
|
||||
required: false
|
||||
default: string
|
||||
choices: [ "array", "bool", "boolean", "date", "float", "int", "integer", "string" ]
|
||||
array_add:
|
||||
description:
|
||||
- Add new elements to the array for a key which has an array as its value.
|
||||
required: false
|
||||
default: false
|
||||
choices: [ "true", "false" ]
|
||||
value:
|
||||
description:
|
||||
- The value to write. Only required when state = present.
|
||||
required: false
|
||||
default: null
|
||||
state:
|
||||
description:
|
||||
- The state of the user defaults
|
||||
required: false
|
||||
default: present
|
||||
choices: [ "present", "absent" ]
|
||||
notes:
|
||||
- Apple Mac caches defaults. You may need to logout and login to apply the changes.
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- osx_defaults:
|
||||
domain: com.apple.Safari
|
||||
key: IncludeInternalDebugMenu
|
||||
type: bool
|
||||
value: true
|
||||
state: present
|
||||
|
||||
- osx_defaults:
|
||||
domain: NSGlobalDomain
|
||||
key: AppleMeasurementUnits
|
||||
type: string
|
||||
value: Centimeters
|
||||
state: present
|
||||
|
||||
- osx_defaults:
|
||||
domain: com.apple.screensaver
|
||||
host: currentHost
|
||||
key: showClock
|
||||
type: int
|
||||
value: 1
|
||||
|
||||
- osx_defaults:
|
||||
key: AppleMeasurementUnits
|
||||
type: string
|
||||
value: Centimeters
|
||||
|
||||
- osx_defaults:
|
||||
key: AppleLanguages
|
||||
type: array
|
||||
value:
|
||||
- en
|
||||
- nl
|
||||
|
||||
- osx_defaults:
|
||||
domain: com.geekchimp.macable
|
||||
key: ExampleKeyToRemove
|
||||
state: absent
|
||||
'''
|
||||
|
||||
import datetime
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
|
||||
# exceptions --------------------------------------------------------------- {{{
|
||||
class OSXDefaultsException(Exception):
|
||||
pass
|
||||
# /exceptions -------------------------------------------------------------- }}}
|
||||
|
||||
# class MacDefaults -------------------------------------------------------- {{{
|
||||
class OSXDefaults(object):
|
||||
|
||||
""" Class to manage Mac OS user defaults """
|
||||
|
||||
# init ---------------------------------------------------------------- {{{
|
||||
""" Initialize this module. Finds 'defaults' executable and preps the parameters """
|
||||
def __init__(self, **kwargs):
|
||||
|
||||
# Initial var for storing current defaults value
|
||||
self.current_value = None
|
||||
|
||||
# Just set all given parameters
|
||||
for key, val in kwargs.iteritems():
|
||||
setattr(self, key, val)
|
||||
|
||||
# Try to find the defaults executable
|
||||
self.executable = self.module.get_bin_path(
|
||||
'defaults',
|
||||
required=False,
|
||||
opt_dirs=self.path.split(':'),
|
||||
)
|
||||
|
||||
if not self.executable:
|
||||
raise OSXDefaultsException("Unable to locate defaults executable.")
|
||||
|
||||
# When state is present, we require a parameter
|
||||
if self.state == "present" and self.value is None:
|
||||
raise OSXDefaultsException("Missing value parameter")
|
||||
|
||||
# Ensure the value is the correct type
|
||||
self.value = self._convert_type(self.type, self.value)
|
||||
|
||||
# /init --------------------------------------------------------------- }}}
|
||||
|
||||
# tools --------------------------------------------------------------- {{{
|
||||
""" Converts value to given type """
|
||||
def _convert_type(self, type, value):
|
||||
|
||||
if type == "string":
|
||||
return str(value)
|
||||
elif type in ["bool", "boolean"]:
|
||||
if isinstance(value, basestring):
|
||||
value = value.lower()
|
||||
if value in [True, 1, "true", "1", "yes"]:
|
||||
return True
|
||||
elif value in [False, 0, "false", "0", "no"]:
|
||||
return False
|
||||
raise OSXDefaultsException("Invalid boolean value: {0}".format(repr(value)))
|
||||
elif type == "date":
|
||||
try:
|
||||
return datetime.datetime.strptime(value.split("+")[0].strip(), "%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
raise OSXDefaultsException(
|
||||
"Invalid date value: {0}. Required format yyy-mm-dd hh:mm:ss.".format(repr(value))
|
||||
)
|
||||
elif type in ["int", "integer"]:
|
||||
if not str(value).isdigit():
|
||||
raise OSXDefaultsException("Invalid integer value: {0}".format(repr(value)))
|
||||
return int(value)
|
||||
elif type == "float":
|
||||
try:
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
raise OSXDefaultsException("Invalid float value: {0}".format(repr(value)))
|
||||
return value
|
||||
elif type == "array":
|
||||
if not isinstance(value, list):
|
||||
raise OSXDefaultsException("Invalid value. Expected value to be an array")
|
||||
return value
|
||||
|
||||
raise OSXDefaultsException('Type is not supported: {0}'.format(type))
|
||||
|
||||
""" Returns a normalized list of commandline arguments based on the "host" attribute """
|
||||
def _host_args(self):
|
||||
if self.host is None:
|
||||
return []
|
||||
elif self.host == 'currentHost':
|
||||
return ['-currentHost']
|
||||
else:
|
||||
return ['-host', self.host]
|
||||
|
||||
""" Returns a list containing the "defaults" executable and any common base arguments """
|
||||
def _base_command(self):
|
||||
return [self.executable] + self._host_args()
|
||||
|
||||
""" Converts array output from defaults to an list """
|
||||
@staticmethod
|
||||
def _convert_defaults_str_to_list(value):
|
||||
|
||||
# Split output of defaults. Every line contains a value
|
||||
value = value.splitlines()
|
||||
|
||||
# Remove first and last item, those are not actual values
|
||||
value.pop(0)
|
||||
value.pop(-1)
|
||||
|
||||
# Remove extra spaces and comma (,) at the end of values
|
||||
value = [re.sub(',$', '', x.strip(' ')) for x in value]
|
||||
|
||||
return value
|
||||
# /tools -------------------------------------------------------------- }}}
|
||||
|
||||
# commands ------------------------------------------------------------ {{{
|
||||
""" Reads value of this domain & key from defaults """
|
||||
def read(self):
|
||||
# First try to find out the type
|
||||
rc, out, err = self.module.run_command(self._base_command() + ["read-type", self.domain, self.key])
|
||||
|
||||
# If RC is 1, the key does not exists
|
||||
if rc == 1:
|
||||
return None
|
||||
|
||||
# If the RC is not 0, then terrible happened! Ooooh nooo!
|
||||
if rc != 0:
|
||||
raise OSXDefaultsException("An error occurred while reading key type from defaults: " + out)
|
||||
|
||||
# Ok, lets parse the type from output
|
||||
type = out.strip().replace('Type is ', '')
|
||||
|
||||
# Now get the current value
|
||||
rc, out, err = self.module.run_command(self._base_command() + ["read", self.domain, self.key])
|
||||
|
||||
# Strip output
|
||||
out = out.strip()
|
||||
|
||||
# An non zero RC at this point is kinda strange...
|
||||
if rc != 0:
|
||||
raise OSXDefaultsException("An error occurred while reading key value from defaults: " + out)
|
||||
|
||||
# Convert string to list when type is array
|
||||
if type == "array":
|
||||
out = self._convert_defaults_str_to_list(out)
|
||||
|
||||
# Store the current_value
|
||||
self.current_value = self._convert_type(type, out)
|
||||
|
||||
""" Writes value to this domain & key to defaults """
|
||||
def write(self):
|
||||
|
||||
# We need to convert some values so the defaults commandline understands it
|
||||
if isinstance(self.value, bool):
|
||||
if self.value:
|
||||
value = "TRUE"
|
||||
else:
|
||||
value = "FALSE"
|
||||
elif isinstance(self.value, (int, float)):
|
||||
value = str(self.value)
|
||||
elif self.array_add and self.current_value is not None:
|
||||
value = list(set(self.value) - set(self.current_value))
|
||||
elif isinstance(self.value, datetime.datetime):
|
||||
value = self.value.strftime('%Y-%m-%d %H:%M:%S')
|
||||
else:
|
||||
value = self.value
|
||||
|
||||
# When the type is array and array_add is enabled, morph the type :)
|
||||
if self.type == "array" and self.array_add:
|
||||
self.type = "array-add"
|
||||
|
||||
# All values should be a list, for easy passing it to the command
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
|
||||
rc, out, err = self.module.run_command(self._base_command() + ['write', self.domain, self.key, '-' + self.type] + value)
|
||||
|
||||
if rc != 0:
|
||||
raise OSXDefaultsException('An error occurred while writing value to defaults: ' + out)
|
||||
|
||||
""" Deletes defaults key from domain """
|
||||
def delete(self):
|
||||
rc, out, err = self.module.run_command(self._base_command() + ['delete', self.domain, self.key])
|
||||
if rc != 0:
|
||||
raise OSXDefaultsException("An error occurred while deleting key from defaults: " + out)
|
||||
|
||||
# /commands ----------------------------------------------------------- }}}
|
||||
|
||||
# run ----------------------------------------------------------------- {{{
|
||||
""" Does the magic! :) """
|
||||
def run(self):
|
||||
|
||||
# Get the current value from defaults
|
||||
self.read()
|
||||
|
||||
# Handle absent state
|
||||
if self.state == "absent":
|
||||
if self.current_value is None:
|
||||
return False
|
||||
if self.module.check_mode:
|
||||
return True
|
||||
self.delete()
|
||||
return True
|
||||
|
||||
# There is a type mismatch! Given type does not match the type in defaults
|
||||
value_type = type(self.value)
|
||||
if self.current_value is not None and not isinstance(self.current_value, value_type):
|
||||
raise OSXDefaultsException("Type mismatch. Type in defaults: " + type(self.current_value).__name__)
|
||||
|
||||
# Current value matches the given value. Nothing need to be done. Arrays need extra care
|
||||
if self.type == "array" and self.current_value is not None and not self.array_add and \
|
||||
set(self.current_value) == set(self.value):
|
||||
return False
|
||||
elif self.type == "array" and self.current_value is not None and self.array_add and \
|
||||
len(list(set(self.value) - set(self.current_value))) == 0:
|
||||
return False
|
||||
elif self.current_value == self.value:
|
||||
return False
|
||||
|
||||
if self.module.check_mode:
|
||||
return True
|
||||
|
||||
# Change/Create/Set given key/value for domain in defaults
|
||||
self.write()
|
||||
return True
|
||||
|
||||
# /run ---------------------------------------------------------------- }}}
|
||||
|
||||
# /class MacDefaults ------------------------------------------------------ }}}
|
||||
|
||||
|
||||
# main -------------------------------------------------------------------- {{{
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
domain=dict(
|
||||
default="NSGlobalDomain",
|
||||
required=False,
|
||||
),
|
||||
host=dict(
|
||||
default=None,
|
||||
required=False,
|
||||
),
|
||||
key=dict(
|
||||
default=None,
|
||||
),
|
||||
type=dict(
|
||||
default="string",
|
||||
required=False,
|
||||
choices=[
|
||||
"array",
|
||||
"bool",
|
||||
"boolean",
|
||||
"date",
|
||||
"float",
|
||||
"int",
|
||||
"integer",
|
||||
"string",
|
||||
],
|
||||
),
|
||||
array_add=dict(
|
||||
default=False,
|
||||
required=False,
|
||||
type='bool',
|
||||
),
|
||||
value=dict(
|
||||
default=None,
|
||||
required=False,
|
||||
),
|
||||
state=dict(
|
||||
default="present",
|
||||
required=False,
|
||||
choices=[
|
||||
"absent", "present"
|
||||
],
|
||||
),
|
||||
path=dict(
|
||||
default="/usr/bin:/usr/local/bin",
|
||||
required=False,
|
||||
)
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
domain = module.params['domain']
|
||||
host = module.params['host']
|
||||
key = module.params['key']
|
||||
type = module.params['type']
|
||||
array_add = module.params['array_add']
|
||||
value = module.params['value']
|
||||
state = module.params['state']
|
||||
path = module.params['path']
|
||||
|
||||
try:
|
||||
defaults = OSXDefaults(module=module, domain=domain, host=host, key=key, type=type,
|
||||
array_add=array_add, value=value, state=state, path=path)
|
||||
changed = defaults.run()
|
||||
module.exit_json(changed=changed)
|
||||
except OSXDefaultsException:
|
||||
e = get_exception()
|
||||
module.fail_json(msg=e.message)
|
||||
|
||||
# /main ------------------------------------------------------------------- }}}
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
290
lib/ansible/modules/system/pam_limits.py
Normal file
290
lib/ansible/modules/system/pam_limits.py
Normal file
@@ -0,0 +1,290 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2014, Sebastien Rohaut <sebastien.rohaut@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 re
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: pam_limits
|
||||
version_added: "2.0"
|
||||
authors:
|
||||
- "Sebastien Rohaut (@usawa)"
|
||||
short_description: Modify Linux PAM limits
|
||||
description:
|
||||
- The M(pam_limits) module modify PAM limits, default in /etc/security/limits.conf.
|
||||
For the full documentation, see man limits.conf(5).
|
||||
options:
|
||||
domain:
|
||||
description:
|
||||
- A username, @groupname, wildcard, uid/gid range.
|
||||
required: true
|
||||
limit_type:
|
||||
description:
|
||||
- Limit type, see C(man limits) for an explanation
|
||||
required: true
|
||||
choices: [ "hard", "soft", "-" ]
|
||||
limit_item:
|
||||
description:
|
||||
- The limit to be set
|
||||
required: true
|
||||
choices: [ "core", "data", "fsize", "memlock", "nofile", "rss", "stack", "cpu", "nproc", "as", "maxlogins", "maxsyslogins", "priority", "locks", "sigpending", "msgqueue", "nice", "rtprio", "chroot" ]
|
||||
value:
|
||||
description:
|
||||
- The value of the limit.
|
||||
required: true
|
||||
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"
|
||||
use_min:
|
||||
description:
|
||||
- If set to C(yes), the minimal value will be used or conserved.
|
||||
If the specified value is inferior to the value in the file, file content is replaced with the new value,
|
||||
else content is not modified.
|
||||
required: false
|
||||
choices: [ "yes", "no" ]
|
||||
default: "no"
|
||||
use_max:
|
||||
description:
|
||||
- If set to C(yes), the maximal value will be used or conserved.
|
||||
If the specified value is superior to the value in the file, file content is replaced with the new value,
|
||||
else content is not modified.
|
||||
required: false
|
||||
choices: [ "yes", "no" ]
|
||||
default: "no"
|
||||
dest:
|
||||
description:
|
||||
- Modify the limits.conf path.
|
||||
required: false
|
||||
default: "/etc/security/limits.conf"
|
||||
comment:
|
||||
description:
|
||||
- Comment associated with the limit.
|
||||
required: false
|
||||
default: ''
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Add or modify nofile soft limit for the user joe
|
||||
- pam_limits:
|
||||
domain: joe
|
||||
limit_type: soft
|
||||
limit_item: nofile
|
||||
value: 64000
|
||||
|
||||
# Add or modify fsize hard limit for the user smith. Keep or set the maximal value.
|
||||
- pam_limits:
|
||||
domain: smith
|
||||
limit_type: hard
|
||||
limit_item: fsize
|
||||
value: 1000000
|
||||
use_max: yes
|
||||
|
||||
# Add or modify memlock, both soft and hard, limit for the user james with a comment.
|
||||
- pam_limits:
|
||||
domain: james
|
||||
limit_type: -
|
||||
limit_item: memlock
|
||||
value: unlimited
|
||||
comment: unlimited memory lock for james
|
||||
'''
|
||||
|
||||
def main():
|
||||
|
||||
pam_items = [ 'core', 'data', 'fsize', 'memlock', 'nofile', 'rss', 'stack', 'cpu', 'nproc', 'as', 'maxlogins', 'maxsyslogins', 'priority', 'locks', 'sigpending', 'msgqueue', 'nice', 'rtprio', 'chroot' ]
|
||||
|
||||
pam_types = [ 'soft', 'hard', '-' ]
|
||||
|
||||
limits_conf = '/etc/security/limits.conf'
|
||||
|
||||
module = AnsibleModule(
|
||||
# not checking because of daisy chain to file module
|
||||
argument_spec = dict(
|
||||
domain = dict(required=True, type='str'),
|
||||
limit_type = dict(required=True, type='str', choices=pam_types),
|
||||
limit_item = dict(required=True, type='str', choices=pam_items),
|
||||
value = dict(required=True, type='str'),
|
||||
use_max = dict(default=False, type='bool'),
|
||||
use_min = dict(default=False, type='bool'),
|
||||
backup = dict(default=False, type='bool'),
|
||||
dest = dict(default=limits_conf, type='str'),
|
||||
comment = dict(required=False, default='', type='str')
|
||||
)
|
||||
)
|
||||
|
||||
domain = module.params['domain']
|
||||
limit_type = module.params['limit_type']
|
||||
limit_item = module.params['limit_item']
|
||||
value = module.params['value']
|
||||
use_max = module.params['use_max']
|
||||
use_min = module.params['use_min']
|
||||
backup = module.params['backup']
|
||||
limits_conf = module.params['dest']
|
||||
new_comment = module.params['comment']
|
||||
|
||||
changed = False
|
||||
|
||||
if os.path.isfile(limits_conf):
|
||||
if not os.access(limits_conf, os.W_OK):
|
||||
module.fail_json(msg="%s is not writable. Use sudo" % (limits_conf) )
|
||||
else:
|
||||
module.fail_json(msg="%s is not visible (check presence, access rights, use sudo)" % (limits_conf) )
|
||||
|
||||
if use_max and use_min:
|
||||
module.fail_json(msg="Cannot use use_min and use_max at the same time." )
|
||||
|
||||
if not (value in ['unlimited', 'infinity', '-1'] or value.isdigit()):
|
||||
module.fail_json(msg="Argument 'value' can be one of 'unlimited', 'infinity', '-1' or positive number. Refer to manual pages for more details.")
|
||||
|
||||
# Backup
|
||||
if backup:
|
||||
backup_file = module.backup_local(limits_conf)
|
||||
|
||||
space_pattern = re.compile(r'\s+')
|
||||
|
||||
message = ''
|
||||
f = open (limits_conf, 'r')
|
||||
# Tempfile
|
||||
nf = tempfile.NamedTemporaryFile()
|
||||
|
||||
found = False
|
||||
new_value = value
|
||||
|
||||
for line in f:
|
||||
|
||||
if line.startswith('#'):
|
||||
nf.write(line)
|
||||
continue
|
||||
|
||||
newline = re.sub(space_pattern, ' ', line).strip()
|
||||
if not newline:
|
||||
nf.write(line)
|
||||
continue
|
||||
|
||||
# Remove comment in line
|
||||
newline = newline.split('#',1)[0]
|
||||
try:
|
||||
old_comment = line.split('#',1)[1]
|
||||
except:
|
||||
old_comment = ''
|
||||
|
||||
newline = newline.rstrip()
|
||||
|
||||
if not new_comment:
|
||||
new_comment = old_comment
|
||||
|
||||
if new_comment:
|
||||
new_comment = "\t#"+new_comment
|
||||
|
||||
line_fields = newline.split(' ')
|
||||
|
||||
if len(line_fields) != 4:
|
||||
nf.write(line)
|
||||
continue
|
||||
|
||||
line_domain = line_fields[0]
|
||||
line_type = line_fields[1]
|
||||
line_item = line_fields[2]
|
||||
actual_value = line_fields[3]
|
||||
|
||||
if not (actual_value in ['unlimited', 'infinity', '-1'] or actual_value.isdigit()):
|
||||
module.fail_json(msg="Invalid configuration of '%s'. Current value of %s is unsupported." % (limits_conf, line_item))
|
||||
|
||||
# Found the line
|
||||
if line_domain == domain and line_type == limit_type and line_item == limit_item:
|
||||
found = True
|
||||
if value == actual_value:
|
||||
message = line
|
||||
nf.write(line)
|
||||
continue
|
||||
|
||||
actual_value_unlimited = actual_value in ['unlimited', 'infinity', '-1']
|
||||
value_unlimited = value in ['unlimited', 'infinity', '-1']
|
||||
|
||||
if use_max:
|
||||
if value.isdigit() and actual_value.isdigit():
|
||||
new_value = str(max(int(value), int(actual_value)))
|
||||
elif actual_value_unlimited:
|
||||
new_value = actual_value
|
||||
else:
|
||||
new_value = value
|
||||
|
||||
if use_min:
|
||||
if value.isdigit() and actual_value.isdigit():
|
||||
new_value = str(min(int(value), int(actual_value)))
|
||||
elif value_unlimited:
|
||||
new_value = actual_value
|
||||
else:
|
||||
new_value = value
|
||||
|
||||
# Change line only if value has changed
|
||||
if new_value != actual_value:
|
||||
changed = True
|
||||
new_limit = domain + "\t" + limit_type + "\t" + limit_item + "\t" + new_value + new_comment + "\n"
|
||||
message = new_limit
|
||||
nf.write(new_limit)
|
||||
else:
|
||||
message = line
|
||||
nf.write(line)
|
||||
else:
|
||||
nf.write(line)
|
||||
|
||||
if not found:
|
||||
changed = True
|
||||
new_limit = domain + "\t" + limit_type + "\t" + limit_item + "\t" + new_value + new_comment + "\n"
|
||||
message = new_limit
|
||||
nf.write(new_limit)
|
||||
|
||||
f.close()
|
||||
nf.flush()
|
||||
|
||||
# Copy tempfile to newfile
|
||||
module.atomic_move(nf.name, f.name)
|
||||
|
||||
try:
|
||||
nf.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
res_args = dict(
|
||||
changed = changed, msg = message
|
||||
)
|
||||
|
||||
if backup:
|
||||
res_args['backup_file'] = backup_file
|
||||
|
||||
module.exit_json(**res_args)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
292
lib/ansible/modules/system/puppet.py
Normal file
292
lib/ansible/modules/system/puppet.py
Normal file
@@ -0,0 +1,292 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import pipes
|
||||
import stat
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
# Let snippet from module_utils/basic.py return a proper error in this case
|
||||
pass
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['stableinterface'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: puppet
|
||||
short_description: Runs puppet
|
||||
description:
|
||||
- Runs I(puppet) agent or apply in a reliable manner
|
||||
version_added: "2.0"
|
||||
options:
|
||||
timeout:
|
||||
description:
|
||||
- How long to wait for I(puppet) to finish.
|
||||
required: false
|
||||
default: 30m
|
||||
puppetmaster:
|
||||
description:
|
||||
- The hostname of the puppetmaster to contact.
|
||||
required: false
|
||||
default: None
|
||||
manifest:
|
||||
description:
|
||||
- Path to the manifest file to run puppet apply on.
|
||||
required: false
|
||||
default: None
|
||||
facts:
|
||||
description:
|
||||
- A dict of values to pass in as persistent external facter facts
|
||||
required: false
|
||||
default: None
|
||||
facter_basename:
|
||||
description:
|
||||
- Basename of the facter output file
|
||||
required: false
|
||||
default: ansible
|
||||
environment:
|
||||
description:
|
||||
- Puppet environment to be used.
|
||||
required: false
|
||||
default: None
|
||||
logdest:
|
||||
description:
|
||||
- Where the puppet logs should go, if puppet apply is being used
|
||||
required: false
|
||||
default: stdout
|
||||
choices: [ 'stdout', 'syslog' ]
|
||||
version_added: "2.1"
|
||||
certname:
|
||||
description:
|
||||
- The name to use when handling certificates.
|
||||
required: false
|
||||
default: None
|
||||
version_added: "2.1"
|
||||
tags:
|
||||
description:
|
||||
- A comma-separated list of puppet tags to be used.
|
||||
required: false
|
||||
default: None
|
||||
version_added: "2.1"
|
||||
execute:
|
||||
description:
|
||||
- Execute a specific piece of Puppet code. It has no effect with
|
||||
a puppetmaster.
|
||||
required: false
|
||||
default: None
|
||||
version_added: "2.1"
|
||||
requirements: [ puppet ]
|
||||
author: "Monty Taylor (@emonty)"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Run puppet agent and fail if anything goes wrong
|
||||
- puppet
|
||||
|
||||
# Run puppet and timeout in 5 minutes
|
||||
- puppet:
|
||||
timeout: 5m
|
||||
|
||||
# Run puppet using a different environment
|
||||
- puppet:
|
||||
environment: testing
|
||||
|
||||
# Run puppet using a specific certname
|
||||
- puppet:
|
||||
certname: agent01.example.com
|
||||
|
||||
# Run puppet using a specific piece of Puppet code. Has no effect with a
|
||||
# puppetmaster.
|
||||
- puppet:
|
||||
execute: 'include ::mymodule'
|
||||
|
||||
# Run puppet using a specific tags
|
||||
- puppet:
|
||||
tags: update,nginx
|
||||
'''
|
||||
|
||||
|
||||
def _get_facter_dir():
|
||||
if os.getuid() == 0:
|
||||
return '/etc/facter/facts.d'
|
||||
else:
|
||||
return os.path.expanduser('~/.facter/facts.d')
|
||||
|
||||
|
||||
def _write_structured_data(basedir, basename, data):
|
||||
if not os.path.exists(basedir):
|
||||
os.makedirs(basedir)
|
||||
file_path = os.path.join(basedir, "{0}.json".format(basename))
|
||||
# This is more complex than you might normally expect because we want to
|
||||
# open the file with only u+rw set. Also, we use the stat constants
|
||||
# because ansible still supports python 2.4 and the octal syntax changed
|
||||
out_file = os.fdopen(
|
||||
os.open(
|
||||
file_path, os.O_CREAT | os.O_WRONLY,
|
||||
stat.S_IRUSR | stat.S_IWUSR), 'w')
|
||||
out_file.write(json.dumps(data).encode('utf8'))
|
||||
out_file.close()
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
timeout=dict(default="30m"),
|
||||
puppetmaster=dict(required=False, default=None),
|
||||
manifest=dict(required=False, default=None),
|
||||
logdest=dict(
|
||||
required=False, default='stdout',
|
||||
choices=['stdout', 'syslog']),
|
||||
show_diff=dict(
|
||||
# internal code to work with --diff, do not use
|
||||
default=False, aliases=['show-diff'], type='bool'),
|
||||
facts=dict(default=None),
|
||||
facter_basename=dict(default='ansible'),
|
||||
environment=dict(required=False, default=None),
|
||||
certname=dict(required=False, default=None),
|
||||
tags=dict(required=False, default=None, type='list'),
|
||||
execute=dict(required=False, default=None),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
mutually_exclusive=[
|
||||
('puppetmaster', 'manifest'),
|
||||
('puppetmaster', 'manifest', 'execute'),
|
||||
],
|
||||
)
|
||||
p = module.params
|
||||
|
||||
global PUPPET_CMD
|
||||
PUPPET_CMD = module.get_bin_path("puppet", False, ['/opt/puppetlabs/bin'])
|
||||
|
||||
if not PUPPET_CMD:
|
||||
module.fail_json(
|
||||
msg="Could not find puppet. Please ensure it is installed.")
|
||||
|
||||
global TIMEOUT_CMD
|
||||
TIMEOUT_CMD = module.get_bin_path("timeout", False)
|
||||
|
||||
if p['manifest']:
|
||||
if not os.path.exists(p['manifest']):
|
||||
module.fail_json(
|
||||
msg="Manifest file %(manifest)s not found." % dict(
|
||||
manifest=p['manifest']))
|
||||
|
||||
# Check if puppet is disabled here
|
||||
if not p['manifest']:
|
||||
rc, stdout, stderr = module.run_command(
|
||||
PUPPET_CMD + " config print agent_disabled_lockfile")
|
||||
if os.path.exists(stdout.strip()):
|
||||
module.fail_json(
|
||||
msg="Puppet agent is administratively disabled.",
|
||||
disabled=True)
|
||||
elif rc != 0:
|
||||
module.fail_json(
|
||||
msg="Puppet agent state could not be determined.")
|
||||
|
||||
if module.params['facts'] and not module.check_mode:
|
||||
_write_structured_data(
|
||||
_get_facter_dir(),
|
||||
module.params['facter_basename'],
|
||||
module.params['facts'])
|
||||
|
||||
if TIMEOUT_CMD:
|
||||
base_cmd = "%(timeout_cmd)s -s 9 %(timeout)s %(puppet_cmd)s" % dict(
|
||||
timeout_cmd=TIMEOUT_CMD,
|
||||
timeout=pipes.quote(p['timeout']),
|
||||
puppet_cmd=PUPPET_CMD)
|
||||
else:
|
||||
base_cmd = PUPPET_CMD
|
||||
|
||||
if not p['manifest']:
|
||||
cmd = ("%(base_cmd)s agent --onetime"
|
||||
" --ignorecache --no-daemonize --no-usecacheonfailure --no-splay"
|
||||
" --detailed-exitcodes --verbose --color 0") % dict(
|
||||
base_cmd=base_cmd,
|
||||
)
|
||||
if p['puppetmaster']:
|
||||
cmd += " --server %s" % pipes.quote(p['puppetmaster'])
|
||||
if p['show_diff']:
|
||||
cmd += " --show_diff"
|
||||
if p['environment']:
|
||||
cmd += " --environment '%s'" % p['environment']
|
||||
if p['tags']:
|
||||
cmd += " --tags '%s'" % ','.join(p['tags'])
|
||||
if p['certname']:
|
||||
cmd += " --certname='%s'" % p['certname']
|
||||
if module.check_mode:
|
||||
cmd += " --noop"
|
||||
else:
|
||||
cmd += " --no-noop"
|
||||
else:
|
||||
cmd = "%s apply --detailed-exitcodes " % base_cmd
|
||||
if p['logdest'] == 'syslog':
|
||||
cmd += "--logdest syslog "
|
||||
if p['environment']:
|
||||
cmd += "--environment '%s' " % p['environment']
|
||||
if p['certname']:
|
||||
cmd += " --certname='%s'" % p['certname']
|
||||
if p['execute']:
|
||||
cmd += " --execute '%s'" % p['execute']
|
||||
if p['tags']:
|
||||
cmd += " --tags '%s'" % ','.join(p['tags'])
|
||||
if module.check_mode:
|
||||
cmd += "--noop "
|
||||
else:
|
||||
cmd += "--no-noop "
|
||||
cmd += pipes.quote(p['manifest'])
|
||||
rc, stdout, stderr = module.run_command(cmd)
|
||||
|
||||
if rc == 0:
|
||||
# success
|
||||
module.exit_json(rc=rc, changed=False, stdout=stdout, stderr=stderr)
|
||||
elif rc == 1:
|
||||
# rc==1 could be because it's disabled
|
||||
# rc==1 could also mean there was a compilation failure
|
||||
disabled = "administratively disabled" in stdout
|
||||
if disabled:
|
||||
msg = "puppet is disabled"
|
||||
else:
|
||||
msg = "puppet did not run"
|
||||
module.exit_json(
|
||||
rc=rc, disabled=disabled, msg=msg,
|
||||
error=True, stdout=stdout, stderr=stderr)
|
||||
elif rc == 2:
|
||||
# success with changes
|
||||
module.exit_json(rc=0, changed=True, stdout=stdout, stderr=stderr)
|
||||
elif rc == 124:
|
||||
# timeout
|
||||
module.exit_json(
|
||||
rc=rc, msg="%s timed out" % cmd, stdout=stdout, stderr=stderr)
|
||||
else:
|
||||
# failure
|
||||
module.fail_json(
|
||||
rc=rc, msg="%s failed with return code: %d" % (cmd, rc),
|
||||
stdout=stdout, stderr=stderr)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
265
lib/ansible/modules/system/sefcontext.py
Normal file
265
lib/ansible/modules/system/sefcontext.py
Normal file
@@ -0,0 +1,265 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# (c) 2016, Dag Wieers <dag@wieers.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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: sefcontext
|
||||
short_description: Manages SELinux file context mapping definitions
|
||||
description:
|
||||
- Manages SELinux file context mapping definitions
|
||||
- Similar to the C(semanage fcontext) command
|
||||
version_added: "2.2"
|
||||
options:
|
||||
target:
|
||||
description:
|
||||
- Target path (expression).
|
||||
required: true
|
||||
default: null
|
||||
aliases: ['path']
|
||||
ftype:
|
||||
description:
|
||||
- File type.
|
||||
required: false
|
||||
default: a
|
||||
setype:
|
||||
description:
|
||||
- SELinux type for the specified target.
|
||||
required: true
|
||||
default: null
|
||||
seuser:
|
||||
description:
|
||||
- SELinux user for the specified target.
|
||||
required: false
|
||||
default: null
|
||||
selevel:
|
||||
description:
|
||||
- SELinux range for the specified target.
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['serange']
|
||||
state:
|
||||
description:
|
||||
- Desired boolean value.
|
||||
required: false
|
||||
default: present
|
||||
choices: [ 'present', 'absent' ]
|
||||
reload:
|
||||
description:
|
||||
- Reload SELinux policy after commit.
|
||||
required: false
|
||||
default: yes
|
||||
notes:
|
||||
- The changes are persistent across reboots
|
||||
requirements: [ 'libselinux-python', 'policycoreutils-python' ]
|
||||
author: Dag Wieers
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Allow apache to modify files in /srv/git_repos
|
||||
- sefcontext:
|
||||
target: '/srv/git_repos(/.*)?'
|
||||
setype: httpd_git_rw_content_t
|
||||
state: present
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
# Default return values
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
try:
|
||||
import selinux
|
||||
HAVE_SELINUX=True
|
||||
except ImportError:
|
||||
HAVE_SELINUX=False
|
||||
|
||||
try:
|
||||
import seobject
|
||||
HAVE_SEOBJECT=True
|
||||
except ImportError:
|
||||
HAVE_SEOBJECT=False
|
||||
|
||||
### Add missing entries (backward compatible)
|
||||
seobject.file_types.update(dict(
|
||||
a = seobject.SEMANAGE_FCONTEXT_ALL,
|
||||
b = seobject.SEMANAGE_FCONTEXT_BLOCK,
|
||||
c = seobject.SEMANAGE_FCONTEXT_CHAR,
|
||||
d = seobject.SEMANAGE_FCONTEXT_DIR,
|
||||
f = seobject.SEMANAGE_FCONTEXT_REG,
|
||||
l = seobject.SEMANAGE_FCONTEXT_LINK,
|
||||
p = seobject.SEMANAGE_FCONTEXT_PIPE,
|
||||
s = seobject.SEMANAGE_FCONTEXT_SOCK,
|
||||
))
|
||||
|
||||
### Make backward compatible
|
||||
option_to_file_type_str = dict(
|
||||
a = 'all files',
|
||||
b = 'block device',
|
||||
c = 'character device',
|
||||
d = 'directory',
|
||||
f = 'regular file',
|
||||
l = 'symbolic link',
|
||||
p = 'named pipe',
|
||||
s = 'socket file',
|
||||
)
|
||||
|
||||
def semanage_fcontext_exists(sefcontext, target, ftype):
|
||||
''' Get the SELinux file context mapping definition from policy. Return None if it does not exist. '''
|
||||
|
||||
# Beware that records comprise of a string representation of the file_type
|
||||
record = (target, option_to_file_type_str[ftype])
|
||||
records = sefcontext.get_all()
|
||||
try:
|
||||
return records[record]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def semanage_fcontext_modify(module, result, target, ftype, setype, do_reload, serange, seuser, sestore=''):
|
||||
''' Add or modify SELinux file context mapping definition to the policy. '''
|
||||
|
||||
changed = False
|
||||
prepared_diff = ''
|
||||
|
||||
try:
|
||||
sefcontext = seobject.fcontextRecords(sestore)
|
||||
sefcontext.set_reload(do_reload)
|
||||
exists = semanage_fcontext_exists(sefcontext, target, ftype)
|
||||
if exists:
|
||||
# Modify existing entry
|
||||
orig_seuser, orig_serole, orig_setype, orig_serange = exists
|
||||
|
||||
if seuser is None:
|
||||
seuser = orig_seuser
|
||||
if serange is None:
|
||||
serange = orig_serange
|
||||
|
||||
if setype != orig_setype or seuser != orig_seuser or serange != orig_serange:
|
||||
if not module.check_mode:
|
||||
sefcontext.modify(target, setype, ftype, serange, seuser)
|
||||
changed = True
|
||||
|
||||
if module._diff:
|
||||
prepared_diff += '# Change to semanage file context mappings\n'
|
||||
prepared_diff += '-%s %s %s:%s:%s:%s\n' % (target, ftype, orig_seuser, orig_serole, orig_setype, orig_serange)
|
||||
prepared_diff += '+%s %s %s:%s:%s:%s\n' % (target, ftype, seuser, orig_serole, setype, serange)
|
||||
else:
|
||||
# Add missing entry
|
||||
if seuser is None:
|
||||
seuser = 'system_u'
|
||||
if serange is None:
|
||||
serange = 's0'
|
||||
|
||||
if not module.check_mode:
|
||||
sefcontext.add(target, setype, ftype, serange, seuser)
|
||||
changed = True
|
||||
|
||||
if module._diff:
|
||||
prepared_diff += '# Addition to semanage file context mappings\n'
|
||||
prepared_diff += '+%s %s %s:%s:%s:%s\n' % (target, ftype, seuser, 'object_r', setype, serange)
|
||||
|
||||
except Exception:
|
||||
e = get_exception()
|
||||
module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, to_native(e)))
|
||||
|
||||
if module._diff and prepared_diff:
|
||||
result['diff'] = dict(prepared=prepared_diff)
|
||||
|
||||
module.exit_json(changed=changed, seuser=seuser, serange=serange, **result)
|
||||
|
||||
def semanage_fcontext_delete(module, result, target, ftype, do_reload, sestore=''):
|
||||
''' Delete SELinux file context mapping definition from the policy. '''
|
||||
|
||||
changed = False
|
||||
prepared_diff = ''
|
||||
|
||||
try:
|
||||
sefcontext = seobject.fcontextRecords(sestore)
|
||||
sefcontext.set_reload(do_reload)
|
||||
exists = semanage_fcontext_exists(sefcontext, target, ftype)
|
||||
if exists:
|
||||
# Remove existing entry
|
||||
orig_seuser, orig_serole, orig_setype, orig_serange = exists
|
||||
|
||||
if not module.check_mode:
|
||||
sefcontext.delete(target, ftype)
|
||||
changed = True
|
||||
|
||||
if module._diff:
|
||||
prepared_diff += '# Deletion to semanage file context mappings\n'
|
||||
prepared_diff += '-%s %s %s:%s:%s:%s\n' % (target, ftype, exists[0], exists[1], exists[2], exists[3])
|
||||
|
||||
except Exception:
|
||||
e = get_exception()
|
||||
module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, to_native(e)))
|
||||
|
||||
if module._diff and prepared_diff:
|
||||
result['diff'] = dict(prepared=prepared_diff)
|
||||
|
||||
module.exit_json(changed=changed, **result)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
target = dict(required=True, aliases=['path']),
|
||||
ftype = dict(required=False, choices=option_to_file_type_str.keys(), default='a'),
|
||||
setype = dict(required=True),
|
||||
seuser = dict(required=False, default=None),
|
||||
selevel = dict(required=False, default=None, aliases=['serange']),
|
||||
state = dict(required=False, choices=['present', 'absent'], default='present'),
|
||||
reload = dict(required=False, type='bool', default='yes'),
|
||||
),
|
||||
supports_check_mode = True,
|
||||
)
|
||||
if not HAVE_SELINUX:
|
||||
module.fail_json(msg="This module requires libselinux-python")
|
||||
|
||||
if not HAVE_SEOBJECT:
|
||||
module.fail_json(msg="This module requires policycoreutils-python")
|
||||
|
||||
if not selinux.is_selinux_enabled():
|
||||
module.fail_json(msg="SELinux is disabled on this host.")
|
||||
|
||||
target = module.params['target']
|
||||
ftype = module.params['ftype']
|
||||
setype = module.params['setype']
|
||||
seuser = module.params['seuser']
|
||||
serange = module.params['selevel']
|
||||
state = module.params['state']
|
||||
do_reload = module.params['reload']
|
||||
|
||||
result = dict(target=target, ftype=ftype, setype=setype, state=state)
|
||||
|
||||
if state == 'present':
|
||||
semanage_fcontext_modify(module, result, target, ftype, setype, do_reload, serange, seuser)
|
||||
elif state == 'absent':
|
||||
semanage_fcontext_delete(module, result, target, ftype, do_reload)
|
||||
else:
|
||||
module.fail_json(msg='Invalid value of argument "state": {0}'.format(state))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
139
lib/ansible/modules/system/selinux_permissive.py
Normal file
139
lib/ansible/modules/system/selinux_permissive.py
Normal file
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2015, Michael Scherer <misc@zarb.org>
|
||||
# inspired by code of github.com/dandiker/
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: selinux_permissive
|
||||
short_description: Change permissive domain in SELinux policy
|
||||
description:
|
||||
- Add and remove domain from the list of permissive domain.
|
||||
version_added: "2.0"
|
||||
options:
|
||||
domain:
|
||||
description:
|
||||
- "the domain that will be added or removed from the list of permissive domains"
|
||||
required: true
|
||||
permissive:
|
||||
description:
|
||||
- "indicate if the domain should or should not be set as permissive"
|
||||
required: true
|
||||
choices: [ 'True', 'False' ]
|
||||
no_reload:
|
||||
description:
|
||||
- "automatically reload the policy after a change"
|
||||
- "default is set to 'false' as that's what most people would want after changing one domain"
|
||||
- "Note that this doesn't work on older version of the library (example EL 6), the module will silently ignore it in this case"
|
||||
required: false
|
||||
default: False
|
||||
choices: [ 'True', 'False' ]
|
||||
store:
|
||||
description:
|
||||
- "name of the SELinux policy store to use"
|
||||
required: false
|
||||
default: null
|
||||
notes:
|
||||
- Requires a version of SELinux recent enough ( ie EL 6 or newer )
|
||||
requirements: [ policycoreutils-python ]
|
||||
author: Michael Scherer <misc@zarb.org>
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- selinux_permissive:
|
||||
name: httpd_t
|
||||
permissive: true
|
||||
'''
|
||||
|
||||
HAVE_SEOBJECT = False
|
||||
try:
|
||||
import seobject
|
||||
HAVE_SEOBJECT = True
|
||||
except ImportError:
|
||||
pass
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
domain=dict(aliases=['name'], required=True),
|
||||
store=dict(required=False, default=''),
|
||||
permissive=dict(type='bool', required=True),
|
||||
no_reload=dict(type='bool', required=False, default=False),
|
||||
),
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
# global vars
|
||||
changed = False
|
||||
store = module.params['store']
|
||||
permissive = module.params['permissive']
|
||||
domain = module.params['domain']
|
||||
no_reload = module.params['no_reload']
|
||||
|
||||
if not HAVE_SEOBJECT:
|
||||
module.fail_json(changed=False, msg="policycoreutils-python required for this module")
|
||||
|
||||
try:
|
||||
permissive_domains = seobject.permissiveRecords(store)
|
||||
except ValueError:
|
||||
e = get_exception()
|
||||
module.fail_json(domain=domain, msg=str(e))
|
||||
|
||||
# not supported on EL 6
|
||||
if 'set_reload' in dir(permissive_domains):
|
||||
permissive_domains.set_reload(not no_reload)
|
||||
|
||||
try:
|
||||
all_domains = permissive_domains.get_all()
|
||||
except ValueError:
|
||||
e = get_exception()
|
||||
module.fail_json(domain=domain, msg=str(e))
|
||||
|
||||
if permissive:
|
||||
if domain not in all_domains:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
permissive_domains.add(domain)
|
||||
except ValueError:
|
||||
e = get_exception()
|
||||
module.fail_json(domain=domain, msg=str(e))
|
||||
changed = True
|
||||
else:
|
||||
if domain in all_domains:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
permissive_domains.delete(domain)
|
||||
except ValueError:
|
||||
e = get_exception()
|
||||
module.fail_json(domain=domain, msg=str(e))
|
||||
changed = True
|
||||
|
||||
module.exit_json(changed=changed, store=store,
|
||||
permissive=permissive, domain=domain)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
324
lib/ansible/modules/system/seport.py
Normal file
324
lib/ansible/modules/system/seport.py
Normal file
@@ -0,0 +1,324 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# (c) 2014, Dan Keder <dan.keder@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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: seport
|
||||
short_description: Manages SELinux network port type definitions
|
||||
description:
|
||||
- Manages SELinux network port type definitions.
|
||||
version_added: "2.0"
|
||||
options:
|
||||
ports:
|
||||
description:
|
||||
- Ports or port ranges, separated by a comma
|
||||
required: true
|
||||
default: null
|
||||
proto:
|
||||
description:
|
||||
- Protocol for the specified port.
|
||||
required: true
|
||||
default: null
|
||||
choices: [ 'tcp', 'udp' ]
|
||||
setype:
|
||||
description:
|
||||
- SELinux type for the specified port.
|
||||
required: true
|
||||
default: null
|
||||
state:
|
||||
description:
|
||||
- Desired boolean value.
|
||||
required: true
|
||||
default: present
|
||||
choices: [ 'present', 'absent' ]
|
||||
reload:
|
||||
description:
|
||||
- Reload SELinux policy after commit.
|
||||
required: false
|
||||
default: yes
|
||||
notes:
|
||||
- The changes are persistent across reboots
|
||||
- Not tested on any debian based system
|
||||
requirements: [ 'libselinux-python', 'policycoreutils-python' ]
|
||||
author: Dan Keder
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Allow Apache to listen on tcp port 8888
|
||||
- seport:
|
||||
ports: 8888
|
||||
proto: tcp
|
||||
setype: http_port_t
|
||||
state: present
|
||||
|
||||
# Allow sshd to listen on tcp port 8991
|
||||
- seport:
|
||||
ports: 8991
|
||||
proto: tcp
|
||||
setype: ssh_port_t
|
||||
state: present
|
||||
|
||||
# Allow memcached to listen on tcp ports 10000-10100 and 10112
|
||||
- seport:
|
||||
ports: 10000-10100,10112
|
||||
proto: tcp
|
||||
setype: memcache_port_t
|
||||
state: present
|
||||
'''
|
||||
|
||||
try:
|
||||
import selinux
|
||||
HAVE_SELINUX=True
|
||||
except ImportError:
|
||||
HAVE_SELINUX=False
|
||||
|
||||
try:
|
||||
import seobject
|
||||
HAVE_SEOBJECT=True
|
||||
except ImportError:
|
||||
HAVE_SEOBJECT=False
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
|
||||
|
||||
def semanage_port_get_ports(seport, setype, proto):
|
||||
""" Get the list of ports that have the specified type definition.
|
||||
|
||||
:param seport: Instance of seobject.portRecords
|
||||
|
||||
:type setype: str
|
||||
:param setype: SELinux type.
|
||||
|
||||
:type proto: str
|
||||
:param proto: Protocol ('tcp' or 'udp')
|
||||
|
||||
:rtype: list
|
||||
:return: List of ports that have the specified SELinux type.
|
||||
"""
|
||||
records = seport.get_all_by_type()
|
||||
if (setype, proto) in records:
|
||||
return records[(setype, proto)]
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
def semanage_port_get_type(seport, port, proto):
|
||||
""" Get the SELinux type of the specified port.
|
||||
|
||||
:param seport: Instance of seobject.portRecords
|
||||
|
||||
:type port: str
|
||||
:param port: Port or port range (example: "8080", "8080-9090")
|
||||
|
||||
:type proto: str
|
||||
:param proto: Protocol ('tcp' or 'udp')
|
||||
|
||||
:rtype: tuple
|
||||
:return: Tuple containing the SELinux type and MLS/MCS level, or None if not found.
|
||||
"""
|
||||
ports = port.split('-', 1)
|
||||
if len(ports) == 1:
|
||||
ports.extend(ports)
|
||||
key = (int(ports[0]), int(ports[1]), proto)
|
||||
|
||||
records = seport.get_all()
|
||||
if key in records:
|
||||
return records[key]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def semanage_port_add(module, ports, proto, setype, do_reload, serange='s0', sestore=''):
|
||||
""" Add SELinux port type definition to the policy.
|
||||
|
||||
:type module: AnsibleModule
|
||||
:param module: Ansible module
|
||||
|
||||
:type ports: list
|
||||
:param ports: List of ports and port ranges to add (e.g. ["8080", "8080-9090"])
|
||||
|
||||
:type proto: str
|
||||
:param proto: Protocol ('tcp' or 'udp')
|
||||
|
||||
:type setype: str
|
||||
:param setype: SELinux type
|
||||
|
||||
:type do_reload: bool
|
||||
:param do_reload: Whether to reload SELinux policy after commit
|
||||
|
||||
:type serange: str
|
||||
:param serange: SELinux MLS/MCS range (defaults to 's0')
|
||||
|
||||
:type sestore: str
|
||||
:param sestore: SELinux store
|
||||
|
||||
:rtype: bool
|
||||
:return: True if the policy was changed, otherwise False
|
||||
"""
|
||||
try:
|
||||
seport = seobject.portRecords(sestore)
|
||||
seport.set_reload(do_reload)
|
||||
change = False
|
||||
ports_by_type = semanage_port_get_ports(seport, setype, proto)
|
||||
for port in ports:
|
||||
if port not in ports_by_type:
|
||||
change = True
|
||||
port_type = semanage_port_get_type(seport, port, proto)
|
||||
if port_type is None and not module.check_mode:
|
||||
seport.add(port, proto, serange, setype)
|
||||
elif port_type is not None and not module.check_mode:
|
||||
seport.modify(port, proto, serange, setype)
|
||||
|
||||
except ValueError:
|
||||
e = get_exception()
|
||||
module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e)))
|
||||
except IOError:
|
||||
e = get_exception()
|
||||
module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e)))
|
||||
except KeyError:
|
||||
e = get_exception()
|
||||
module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e)))
|
||||
except OSError:
|
||||
e = get_exception()
|
||||
module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e)))
|
||||
except RuntimeError:
|
||||
e = get_exception()
|
||||
module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e)))
|
||||
|
||||
return change
|
||||
|
||||
|
||||
def semanage_port_del(module, ports, proto, setype, do_reload, sestore=''):
|
||||
""" Delete SELinux port type definition from the policy.
|
||||
|
||||
:type module: AnsibleModule
|
||||
:param module: Ansible module
|
||||
|
||||
:type ports: list
|
||||
:param ports: List of ports and port ranges to delete (e.g. ["8080", "8080-9090"])
|
||||
|
||||
:type proto: str
|
||||
:param proto: Protocol ('tcp' or 'udp')
|
||||
|
||||
:type setype: str
|
||||
:param setype: SELinux type.
|
||||
|
||||
:type do_reload: bool
|
||||
:param do_reload: Whether to reload SELinux policy after commit
|
||||
|
||||
:type sestore: str
|
||||
:param sestore: SELinux store
|
||||
|
||||
:rtype: bool
|
||||
:return: True if the policy was changed, otherwise False
|
||||
"""
|
||||
try:
|
||||
seport = seobject.portRecords(sestore)
|
||||
seport.set_reload(do_reload)
|
||||
change = False
|
||||
ports_by_type = semanage_port_get_ports(seport, setype, proto)
|
||||
for port in ports:
|
||||
if port in ports_by_type:
|
||||
change = True
|
||||
if not module.check_mode:
|
||||
seport.delete(port, proto)
|
||||
|
||||
except ValueError:
|
||||
e = get_exception()
|
||||
module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e)))
|
||||
except IOError:
|
||||
e = get_exception()
|
||||
module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e)))
|
||||
except KeyError:
|
||||
e = get_exception()
|
||||
module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e)))
|
||||
except OSError:
|
||||
e = get_exception()
|
||||
module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e)))
|
||||
except RuntimeError:
|
||||
e = get_exception()
|
||||
module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e)))
|
||||
|
||||
return change
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec={
|
||||
'ports': {
|
||||
'required': True,
|
||||
},
|
||||
'proto': {
|
||||
'required': True,
|
||||
'choices': ['tcp', 'udp'],
|
||||
},
|
||||
'setype': {
|
||||
'required': True,
|
||||
},
|
||||
'state': {
|
||||
'required': True,
|
||||
'choices': ['present', 'absent'],
|
||||
},
|
||||
'reload': {
|
||||
'required': False,
|
||||
'type': 'bool',
|
||||
'default': 'yes',
|
||||
},
|
||||
},
|
||||
supports_check_mode=True
|
||||
)
|
||||
if not HAVE_SELINUX:
|
||||
module.fail_json(msg="This module requires libselinux-python")
|
||||
|
||||
if not HAVE_SEOBJECT:
|
||||
module.fail_json(msg="This module requires policycoreutils-python")
|
||||
|
||||
if not selinux.is_selinux_enabled():
|
||||
module.fail_json(msg="SELinux is disabled on this host.")
|
||||
|
||||
ports = [x.strip() for x in str(module.params['ports']).split(',')]
|
||||
proto = module.params['proto']
|
||||
setype = module.params['setype']
|
||||
state = module.params['state']
|
||||
do_reload = module.params['reload']
|
||||
|
||||
result = {
|
||||
'ports': ports,
|
||||
'proto': proto,
|
||||
'setype': setype,
|
||||
'state': state,
|
||||
}
|
||||
|
||||
if state == 'present':
|
||||
result['changed'] = semanage_port_add(module, ports, proto, setype, do_reload)
|
||||
elif state == 'absent':
|
||||
result['changed'] = semanage_port_del(module, ports, proto, setype, do_reload)
|
||||
else:
|
||||
module.fail_json(msg='Invalid value of argument "state": {0}'.format(state))
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
486
lib/ansible/modules/system/solaris_zone.py
Normal file
486
lib/ansible/modules/system/solaris_zone.py
Normal file
@@ -0,0 +1,486 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# (c) 2015, Paul Markham <pmarkham@netrefinery.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 sys
|
||||
import os
|
||||
import platform
|
||||
import tempfile
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: solaris_zone
|
||||
short_description: Manage Solaris zones
|
||||
description:
|
||||
- Create, start, stop and delete Solaris zones. This module doesn't currently allow
|
||||
changing of options for a zone that's already been created.
|
||||
version_added: "2.0"
|
||||
author: Paul Markham
|
||||
requirements:
|
||||
- Solaris 10 or 11
|
||||
options:
|
||||
state:
|
||||
required: true
|
||||
description:
|
||||
- C(present), configure and install the zone.
|
||||
- C(installed), synonym for C(present).
|
||||
- C(running), if the zone already exists, boot it, otherwise, configure and install
|
||||
the zone first, then boot it.
|
||||
- C(started), synonym for C(running).
|
||||
- C(stopped), shutdown a zone.
|
||||
- C(absent), destroy the zone.
|
||||
- C(configured), configure the ready so that it's to be attached.
|
||||
- C(attached), attach a zone, but do not boot it.
|
||||
- C(detached), shutdown and detach a zone
|
||||
choices: ['present', 'installed', 'started', 'running', 'stopped', 'absent', 'configured', 'attached', 'detached']
|
||||
default: present
|
||||
name:
|
||||
description:
|
||||
- Zone name.
|
||||
required: true
|
||||
path:
|
||||
description:
|
||||
- The path where the zone will be created. This is required when the zone is created, but not
|
||||
used otherwise.
|
||||
required: false
|
||||
default: null
|
||||
sparse:
|
||||
description:
|
||||
- Whether to create a sparse (C(true)) or whole root (C(false)) zone.
|
||||
required: false
|
||||
default: false
|
||||
root_password:
|
||||
description:
|
||||
- The password hash for the root account. If not specified, the zone's root account
|
||||
will not have a password.
|
||||
required: false
|
||||
default: null
|
||||
config:
|
||||
description:
|
||||
- 'The zonecfg configuration commands for this zone. See zonecfg(1M) for the valid options
|
||||
and syntax. Typically this is a list of options separated by semi-colons or new lines, e.g.
|
||||
"set auto-boot=true;add net;set physical=bge0;set address=10.1.1.1;end"'
|
||||
required: false
|
||||
default: empty string
|
||||
create_options:
|
||||
description:
|
||||
- 'Extra options to the zonecfg(1M) create command.'
|
||||
required: false
|
||||
default: empty string
|
||||
install_options:
|
||||
description:
|
||||
- 'Extra options to the zoneadm(1M) install command. To automate Solaris 11 zone creation,
|
||||
use this to specify the profile XML file, e.g. install_options="-c sc_profile.xml"'
|
||||
required: false
|
||||
default: empty string
|
||||
attach_options:
|
||||
description:
|
||||
- 'Extra options to the zoneadm attach command. For example, this can be used to specify
|
||||
whether a minimum or full update of packages is required and if any packages need to
|
||||
be deleted. For valid values, see zoneadm(1M)'
|
||||
required: false
|
||||
default: empty string
|
||||
timeout:
|
||||
description:
|
||||
- Timeout, in seconds, for zone to boot.
|
||||
required: false
|
||||
default: 600
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create and install a zone, but don't boot it
|
||||
- solaris_zone:
|
||||
name: zone1
|
||||
state: present
|
||||
path: /zones/zone1
|
||||
sparse: true
|
||||
root_password: Be9oX7OSwWoU.
|
||||
config: 'set autoboot=true; add net; set physical=bge0; set address=10.1.1.1; end'
|
||||
|
||||
# Create and install a zone and boot it
|
||||
- solaris_zone:
|
||||
name: zone1
|
||||
state: running
|
||||
path: /zones/zone1
|
||||
root_password: Be9oX7OSwWoU.
|
||||
config: 'set autoboot=true; add net; set physical=bge0; set address=10.1.1.1; end'
|
||||
|
||||
# Boot an already installed zone
|
||||
- solaris_zone:
|
||||
name: zone1
|
||||
state: running
|
||||
|
||||
# Stop a zone
|
||||
- solaris_zone:
|
||||
name: zone1
|
||||
state: stopped
|
||||
|
||||
# Destroy a zone
|
||||
- solaris_zone:
|
||||
name: zone1
|
||||
state: absent
|
||||
|
||||
# Detach a zone
|
||||
- solaris_zone:
|
||||
name: zone1
|
||||
state: detached
|
||||
|
||||
# Configure a zone, ready to be attached
|
||||
- solaris_zone:
|
||||
name: zone1
|
||||
state: configured
|
||||
path: /zones/zone1
|
||||
root_password: Be9oX7OSwWoU.
|
||||
config: 'set autoboot=true; add net; set physical=bge0; set address=10.1.1.1; end'
|
||||
|
||||
# Attach a zone
|
||||
- solaris_zone:
|
||||
name: zone1
|
||||
state: attached
|
||||
attach_options=: -u
|
||||
'''
|
||||
|
||||
class Zone(object):
|
||||
def __init__(self, module):
|
||||
self.changed = False
|
||||
self.msg = []
|
||||
|
||||
self.module = module
|
||||
self.path = self.module.params['path']
|
||||
self.name = self.module.params['name']
|
||||
self.sparse = self.module.params['sparse']
|
||||
self.root_password = self.module.params['root_password']
|
||||
self.timeout = self.module.params['timeout']
|
||||
self.config = self.module.params['config']
|
||||
self.create_options = self.module.params['create_options']
|
||||
self.install_options = self.module.params['install_options']
|
||||
self.attach_options = self.module.params['attach_options']
|
||||
|
||||
self.zoneadm_cmd = self.module.get_bin_path('zoneadm', True)
|
||||
self.zonecfg_cmd = self.module.get_bin_path('zonecfg', True)
|
||||
self.ssh_keygen_cmd = self.module.get_bin_path('ssh-keygen', True)
|
||||
|
||||
if self.module.check_mode:
|
||||
self.msg.append('Running in check mode')
|
||||
|
||||
if platform.system() != 'SunOS':
|
||||
self.module.fail_json(msg='This module requires Solaris')
|
||||
|
||||
(self.os_major, self.os_minor) = platform.release().split('.')
|
||||
if int(self.os_minor) < 10:
|
||||
self.module.fail_json(msg='This module requires Solaris 10 or later')
|
||||
|
||||
def configure(self):
|
||||
if not self.path:
|
||||
self.module.fail_json(msg='Missing required argument: path')
|
||||
|
||||
if not self.module.check_mode:
|
||||
t = tempfile.NamedTemporaryFile(delete = False)
|
||||
|
||||
if self.sparse:
|
||||
t.write('create %s\n' % self.create_options)
|
||||
self.msg.append('creating sparse-root zone')
|
||||
else:
|
||||
t.write('create -b %s\n' % self.create_options)
|
||||
self.msg.append('creating whole-root zone')
|
||||
|
||||
t.write('set zonepath=%s\n' % self.path)
|
||||
t.write('%s\n' % self.config)
|
||||
t.close()
|
||||
|
||||
cmd = '%s -z %s -f %s' % (self.zonecfg_cmd, self.name, t.name)
|
||||
(rc, out, err) = self.module.run_command(cmd)
|
||||
if rc != 0:
|
||||
self.module.fail_json(msg='Failed to create zone. %s' % (out + err))
|
||||
os.unlink(t.name)
|
||||
|
||||
self.changed = True
|
||||
self.msg.append('zone configured')
|
||||
|
||||
def install(self):
|
||||
if not self.module.check_mode:
|
||||
cmd = '%s -z %s install %s' % (self.zoneadm_cmd, self.name, self.install_options)
|
||||
(rc, out, err) = self.module.run_command(cmd)
|
||||
if rc != 0:
|
||||
self.module.fail_json(msg='Failed to install zone. %s' % (out + err))
|
||||
if int(self.os_minor) == 10:
|
||||
self.configure_sysid()
|
||||
self.configure_password()
|
||||
self.configure_ssh_keys()
|
||||
self.changed = True
|
||||
self.msg.append('zone installed')
|
||||
|
||||
def uninstall(self):
|
||||
if self.is_installed():
|
||||
if not self.module.check_mode:
|
||||
cmd = '%s -z %s uninstall -F' % (self.zoneadm_cmd, self.name)
|
||||
(rc, out, err) = self.module.run_command(cmd)
|
||||
if rc != 0:
|
||||
self.module.fail_json(msg='Failed to uninstall zone. %s' % (out + err))
|
||||
self.changed = True
|
||||
self.msg.append('zone uninstalled')
|
||||
|
||||
def configure_sysid(self):
|
||||
if os.path.isfile('%s/root/etc/.UNCONFIGURED' % self.path):
|
||||
os.unlink('%s/root/etc/.UNCONFIGURED' % self.path)
|
||||
|
||||
open('%s/root/noautoshutdown' % self.path, 'w').close()
|
||||
|
||||
node = open('%s/root/etc/nodename' % self.path, 'w')
|
||||
node.write(self.name)
|
||||
node.close()
|
||||
|
||||
id = open('%s/root/etc/.sysIDtool.state' % self.path, 'w')
|
||||
id.write('1 # System previously configured?\n')
|
||||
id.write('1 # Bootparams succeeded?\n')
|
||||
id.write('1 # System is on a network?\n')
|
||||
id.write('1 # Extended network information gathered?\n')
|
||||
id.write('0 # Autobinder succeeded?\n')
|
||||
id.write('1 # Network has subnets?\n')
|
||||
id.write('1 # root password prompted for?\n')
|
||||
id.write('1 # locale and term prompted for?\n')
|
||||
id.write('1 # security policy in place\n')
|
||||
id.write('1 # NFSv4 domain configured\n')
|
||||
id.write('0 # Auto Registration Configured\n')
|
||||
id.write('vt100')
|
||||
id.close()
|
||||
|
||||
def configure_ssh_keys(self):
|
||||
rsa_key_file = '%s/root/etc/ssh/ssh_host_rsa_key' % self.path
|
||||
dsa_key_file = '%s/root/etc/ssh/ssh_host_dsa_key' % self.path
|
||||
|
||||
if not os.path.isfile(rsa_key_file):
|
||||
cmd = '%s -f %s -t rsa -N ""' % (self.ssh_keygen_cmd, rsa_key_file)
|
||||
(rc, out, err) = self.module.run_command(cmd)
|
||||
if rc != 0:
|
||||
self.module.fail_json(msg='Failed to create rsa key. %s' % (out + err))
|
||||
|
||||
if not os.path.isfile(dsa_key_file):
|
||||
cmd = '%s -f %s -t dsa -N ""' % (self.ssh_keygen_cmd, dsa_key_file)
|
||||
(rc, out, err) = self.module.run_command(cmd)
|
||||
if rc != 0:
|
||||
self.module.fail_json(msg='Failed to create dsa key. %s' % (out + err))
|
||||
|
||||
def configure_password(self):
|
||||
shadow = '%s/root/etc/shadow' % self.path
|
||||
if self.root_password:
|
||||
f = open(shadow, 'r')
|
||||
lines = f.readlines()
|
||||
f.close()
|
||||
|
||||
for i in range(0, len(lines)):
|
||||
fields = lines[i].split(':')
|
||||
if fields[0] == 'root':
|
||||
fields[1] = self.root_password
|
||||
lines[i] = ':'.join(fields)
|
||||
|
||||
f = open(shadow, 'w')
|
||||
for line in lines:
|
||||
f.write(line)
|
||||
f.close()
|
||||
|
||||
def boot(self):
|
||||
if not self.module.check_mode:
|
||||
cmd = '%s -z %s boot' % (self.zoneadm_cmd, self.name)
|
||||
(rc, out, err) = self.module.run_command(cmd)
|
||||
if rc != 0:
|
||||
self.module.fail_json(msg='Failed to boot zone. %s' % (out + err))
|
||||
|
||||
"""
|
||||
The boot command can return before the zone has fully booted. This is especially
|
||||
true on the first boot when the zone initializes the SMF services. Unless the zone
|
||||
has fully booted, subsequent tasks in the playbook may fail as services aren't running yet.
|
||||
Wait until the zone's console login is running; once that's running, consider the zone booted.
|
||||
"""
|
||||
|
||||
elapsed = 0
|
||||
while True:
|
||||
if elapsed > self.timeout:
|
||||
self.module.fail_json(msg='timed out waiting for zone to boot')
|
||||
rc = os.system('ps -z %s -o args|grep "ttymon.*-d /dev/console" > /dev/null 2>/dev/null' % self.name)
|
||||
if rc == 0:
|
||||
break
|
||||
time.sleep(10)
|
||||
elapsed += 10
|
||||
self.changed = True
|
||||
self.msg.append('zone booted')
|
||||
|
||||
def destroy(self):
|
||||
if self.is_running():
|
||||
self.stop()
|
||||
if self.is_installed():
|
||||
self.uninstall()
|
||||
if not self.module.check_mode:
|
||||
cmd = '%s -z %s delete -F' % (self.zonecfg_cmd, self.name)
|
||||
(rc, out, err) = self.module.run_command(cmd)
|
||||
if rc != 0:
|
||||
self.module.fail_json(msg='Failed to delete zone. %s' % (out + err))
|
||||
self.changed = True
|
||||
self.msg.append('zone deleted')
|
||||
|
||||
def stop(self):
|
||||
if not self.module.check_mode:
|
||||
cmd = '%s -z %s halt' % (self.zoneadm_cmd, self.name)
|
||||
(rc, out, err) = self.module.run_command(cmd)
|
||||
if rc != 0:
|
||||
self.module.fail_json(msg='Failed to stop zone. %s' % (out + err))
|
||||
self.changed = True
|
||||
self.msg.append('zone stopped')
|
||||
|
||||
def detach(self):
|
||||
if not self.module.check_mode:
|
||||
cmd = '%s -z %s detach' % (self.zoneadm_cmd, self.name)
|
||||
(rc, out, err) = self.module.run_command(cmd)
|
||||
if rc != 0:
|
||||
self.module.fail_json(msg='Failed to detach zone. %s' % (out + err))
|
||||
self.changed = True
|
||||
self.msg.append('zone detached')
|
||||
|
||||
def attach(self):
|
||||
if not self.module.check_mode:
|
||||
cmd = '%s -z %s attach %s' % (self.zoneadm_cmd, self.name, self.attach_options)
|
||||
(rc, out, err) = self.module.run_command(cmd)
|
||||
if rc != 0:
|
||||
self.module.fail_json(msg='Failed to attach zone. %s' % (out + err))
|
||||
self.changed = True
|
||||
self.msg.append('zone attached')
|
||||
|
||||
def exists(self):
|
||||
cmd = '%s -z %s list' % (self.zoneadm_cmd, self.name)
|
||||
(rc, out, err) = self.module.run_command(cmd)
|
||||
if rc == 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def is_running(self):
|
||||
return self.status() == 'running'
|
||||
|
||||
def is_installed(self):
|
||||
return self.status() == 'installed'
|
||||
|
||||
def is_configured(self):
|
||||
return self.status() == 'configured'
|
||||
|
||||
def status(self):
|
||||
cmd = '%s -z %s list -p' % (self.zoneadm_cmd, self.name)
|
||||
(rc, out, err) = self.module.run_command(cmd)
|
||||
if rc == 0:
|
||||
return out.split(':')[2]
|
||||
else:
|
||||
return 'undefined'
|
||||
|
||||
def state_present(self):
|
||||
if self.exists():
|
||||
self.msg.append('zone already exists')
|
||||
else:
|
||||
self.configure()
|
||||
self.install()
|
||||
|
||||
def state_running(self):
|
||||
self.state_present()
|
||||
if self.is_running():
|
||||
self.msg.append('zone already running')
|
||||
else:
|
||||
self.boot()
|
||||
|
||||
def state_stopped(self):
|
||||
if self.exists():
|
||||
self.stop()
|
||||
else:
|
||||
self.module.fail_json(msg='zone does not exist')
|
||||
|
||||
def state_absent(self):
|
||||
if self.exists():
|
||||
if self.is_running():
|
||||
self.stop()
|
||||
self.destroy()
|
||||
else:
|
||||
self.msg.append('zone does not exist')
|
||||
|
||||
def state_configured(self):
|
||||
if self.exists():
|
||||
self.msg.append('zone already exists')
|
||||
else:
|
||||
self.configure()
|
||||
|
||||
def state_detached(self):
|
||||
if not self.exists():
|
||||
self.module.fail_json(msg='zone does not exist')
|
||||
if self.is_configured():
|
||||
self.msg.append('zone already detached')
|
||||
else:
|
||||
self.stop()
|
||||
self.detach()
|
||||
|
||||
def state_attached(self):
|
||||
if not self.exists():
|
||||
self.msg.append('zone does not exist')
|
||||
if self.is_configured():
|
||||
self.attach()
|
||||
else:
|
||||
self.msg.append('zone already attached')
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
name = dict(required=True),
|
||||
state = dict(default='present', choices=['running', 'started', 'present', 'installed', 'stopped', 'absent', 'configured', 'detached', 'attached']),
|
||||
path = dict(default=None),
|
||||
sparse = dict(default=False, type='bool'),
|
||||
root_password = dict(default=None, no_log=True),
|
||||
timeout = dict(default=600, type='int'),
|
||||
config = dict(default=''),
|
||||
create_options = dict(default=''),
|
||||
install_options = dict(default=''),
|
||||
attach_options = dict(default=''),
|
||||
),
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
zone = Zone(module)
|
||||
|
||||
state = module.params['state']
|
||||
|
||||
if state == 'running' or state == 'started':
|
||||
zone.state_running()
|
||||
elif state == 'present' or state == 'installed':
|
||||
zone.state_present()
|
||||
elif state == 'stopped':
|
||||
zone.state_stopped()
|
||||
elif state == 'absent':
|
||||
zone.state_absent()
|
||||
elif state == 'configured':
|
||||
zone.state_configured()
|
||||
elif state == 'detached':
|
||||
zone.state_detached()
|
||||
elif state == 'attached':
|
||||
zone.state_attached()
|
||||
else:
|
||||
module.fail_json(msg='Invalid state: %s' % state)
|
||||
|
||||
module.exit_json(changed=zone.changed, msg=', '.join(zone.msg))
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
320
lib/ansible/modules/system/svc.py
Executable file
320
lib/ansible/modules/system/svc.py
Executable file
@@ -0,0 +1,320 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2015, Brian Coca <bcoca@ansible.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/>
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['stableinterface'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: svc
|
||||
author: "Brian Coca (@bcoca)"
|
||||
version_added: "1.9"
|
||||
short_description: Manage daemontools services.
|
||||
description:
|
||||
- Controls daemontools services on remote hosts using the svc utility.
|
||||
options:
|
||||
name:
|
||||
required: true
|
||||
description:
|
||||
- Name of the service to manage.
|
||||
state:
|
||||
required: false
|
||||
choices: [ started, stopped, restarted, reloaded, once ]
|
||||
description:
|
||||
- C(Started)/C(stopped) are idempotent actions that will not run
|
||||
commands unless necessary. C(restarted) will always bounce the
|
||||
svc (svc -t) and C(killed) will always bounce the svc (svc -k).
|
||||
C(reloaded) will send a sigusr1 (svc -1).
|
||||
C(once) will run a normally downed svc once (svc -o), not really
|
||||
an idempotent operation.
|
||||
downed:
|
||||
required: false
|
||||
choices: [ "yes", "no" ]
|
||||
default: no
|
||||
description:
|
||||
- Should a 'down' file exist or not, if it exists it disables auto startup.
|
||||
defaults to no. Downed does not imply stopped.
|
||||
enabled:
|
||||
required: false
|
||||
choices: [ "yes", "no" ]
|
||||
description:
|
||||
- Wheater the service is enabled or not, if disabled it also implies stopped.
|
||||
Make note that a service can be enabled and downed (no auto restart).
|
||||
service_dir:
|
||||
required: false
|
||||
default: /service
|
||||
description:
|
||||
- directory svscan watches for services
|
||||
service_src:
|
||||
required: false
|
||||
description:
|
||||
- directory where services are defined, the source of symlinks to service_dir.
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Example action to start svc dnscache, if not running
|
||||
- svc:
|
||||
name: dnscache
|
||||
state: started
|
||||
|
||||
# Example action to stop svc dnscache, if running
|
||||
- svc:
|
||||
name: dnscache
|
||||
state: stopped
|
||||
|
||||
# Example action to kill svc dnscache, in all cases
|
||||
- svc:
|
||||
name: dnscache
|
||||
state: killed
|
||||
|
||||
# Example action to restart svc dnscache, in all cases
|
||||
- svc:
|
||||
name: dnscache
|
||||
state: restarted
|
||||
|
||||
# Example action to reload svc dnscache, in all cases
|
||||
- svc:
|
||||
name: dnscache
|
||||
state: reloaded
|
||||
|
||||
# Example using alt svc directory location
|
||||
- svc:
|
||||
name: dnscache
|
||||
state: reloaded
|
||||
service_dir: /var/service
|
||||
'''
|
||||
|
||||
import platform
|
||||
import shlex
|
||||
from ansible.module_utils.pycompat24 import get_exception
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
def _load_dist_subclass(cls, *args, **kwargs):
|
||||
'''
|
||||
Used for derivative implementations
|
||||
'''
|
||||
subclass = None
|
||||
|
||||
distro = kwargs['module'].params['distro']
|
||||
|
||||
# get the most specific superclass for this platform
|
||||
if distro is not None:
|
||||
for sc in cls.__subclasses__():
|
||||
if sc.distro is not None and sc.distro == distro:
|
||||
subclass = sc
|
||||
if subclass is None:
|
||||
subclass = cls
|
||||
|
||||
return super(cls, subclass).__new__(subclass)
|
||||
|
||||
class Svc(object):
|
||||
"""
|
||||
Main class that handles daemontools, can be subclassed and overriden in case
|
||||
we want to use a 'derivative' like encore, s6, etc
|
||||
"""
|
||||
|
||||
|
||||
#def __new__(cls, *args, **kwargs):
|
||||
# return _load_dist_subclass(cls, args, kwargs)
|
||||
|
||||
|
||||
|
||||
def __init__(self, module):
|
||||
self.extra_paths = [ '/command', '/usr/local/bin' ]
|
||||
self.report_vars = ['state', 'enabled', 'downed', 'svc_full', 'src_full', 'pid', 'duration', 'full_state']
|
||||
|
||||
self.module = module
|
||||
|
||||
self.name = module.params['name']
|
||||
self.service_dir = module.params['service_dir']
|
||||
self.service_src = module.params['service_src']
|
||||
self.enabled = None
|
||||
self.downed = None
|
||||
self.full_state = None
|
||||
self.state = None
|
||||
self.pid = None
|
||||
self.duration = None
|
||||
|
||||
self.svc_cmd = module.get_bin_path('svc', opt_dirs=self.extra_paths)
|
||||
self.svstat_cmd = module.get_bin_path('svstat', opt_dirs=self.extra_paths)
|
||||
self.svc_full = '/'.join([ self.service_dir, self.name ])
|
||||
self.src_full = '/'.join([ self.service_src, self.name ])
|
||||
|
||||
self.enabled = os.path.lexists(self.svc_full)
|
||||
if self.enabled:
|
||||
self.downed = os.path.lexists('%s/down' % self.svc_full)
|
||||
self.get_status()
|
||||
else:
|
||||
self.downed = os.path.lexists('%s/down' % self.src_full)
|
||||
self.state = 'stopped'
|
||||
|
||||
|
||||
def enable(self):
|
||||
if os.path.exists(self.src_full):
|
||||
try:
|
||||
os.symlink(self.src_full, self.svc_full)
|
||||
except OSError:
|
||||
e = get_exception()
|
||||
self.module.fail_json(path=self.src_full, msg='Error while linking: %s' % str(e))
|
||||
else:
|
||||
self.module.fail_json(msg="Could not find source for service to enable (%s)." % self.src_full)
|
||||
|
||||
def disable(self):
|
||||
try:
|
||||
os.unlink(self.svc_full)
|
||||
except OSError:
|
||||
e = get_exception()
|
||||
self.module.fail_json(path=self.svc_full, msg='Error while unlinking: %s' % str(e))
|
||||
self.execute_command([self.svc_cmd,'-dx',self.src_full])
|
||||
|
||||
src_log = '%s/log' % self.src_full
|
||||
if os.path.exists(src_log):
|
||||
self.execute_command([self.svc_cmd,'-dx',src_log])
|
||||
|
||||
def get_status(self):
|
||||
(rc, out, err) = self.execute_command([self.svstat_cmd, self.svc_full])
|
||||
|
||||
if err is not None and err:
|
||||
self.full_state = self.state = err
|
||||
else:
|
||||
self.full_state = out
|
||||
|
||||
m = re.search('\(pid (\d+)\)', out)
|
||||
if m:
|
||||
self.pid = m.group(1)
|
||||
|
||||
m = re.search('(\d+) seconds', out)
|
||||
if m:
|
||||
self.duration = m.group(1)
|
||||
|
||||
if re.search(' up ', out):
|
||||
self.state = 'start'
|
||||
elif re.search(' down ', out):
|
||||
self.state = 'stopp'
|
||||
else:
|
||||
self.state = 'unknown'
|
||||
return
|
||||
|
||||
if re.search(' want ', out):
|
||||
self.state += 'ing'
|
||||
else:
|
||||
self.state += 'ed'
|
||||
|
||||
def start(self):
|
||||
return self.execute_command([self.svc_cmd, '-u', self.svc_full])
|
||||
|
||||
def stopp(self):
|
||||
return self.stop()
|
||||
|
||||
def stop(self):
|
||||
return self.execute_command([self.svc_cmd, '-d', self.svc_full])
|
||||
|
||||
def once(self):
|
||||
return self.execute_command([self.svc_cmd, '-o', self.svc_full])
|
||||
|
||||
def reload(self):
|
||||
return self.execute_command([self.svc_cmd, '-1', self.svc_full])
|
||||
|
||||
def restart(self):
|
||||
return self.execute_command([self.svc_cmd, '-t', self.svc_full])
|
||||
|
||||
def kill(self):
|
||||
return self.execute_command([self.svc_cmd, '-k', self.svc_full])
|
||||
|
||||
def execute_command(self, cmd):
|
||||
try:
|
||||
(rc, out, err) = self.module.run_command(' '.join(cmd))
|
||||
except Exception:
|
||||
e = get_exception()
|
||||
self.module.fail_json(msg="failed to execute: %s" % str(e))
|
||||
return (rc, out, err)
|
||||
|
||||
def report(self):
|
||||
self.get_status()
|
||||
states = {}
|
||||
for k in self.report_vars:
|
||||
states[k] = self.__dict__[k]
|
||||
return states
|
||||
|
||||
# ===========================================
|
||||
# Main control flow
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
name = dict(required=True),
|
||||
state = dict(choices=['started', 'stopped', 'restarted', 'killed', 'reloaded', 'once']),
|
||||
enabled = dict(required=False, type='bool'),
|
||||
downed = dict(required=False, type='bool'),
|
||||
dist = dict(required=False, default='daemontools'),
|
||||
service_dir = dict(required=False, default='/service'),
|
||||
service_src = dict(required=False, default='/etc/service'),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
|
||||
|
||||
state = module.params['state']
|
||||
enabled = module.params['enabled']
|
||||
downed = module.params['downed']
|
||||
|
||||
svc = Svc(module)
|
||||
changed = False
|
||||
orig_state = svc.report()
|
||||
|
||||
if enabled is not None and enabled != svc.enabled:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
try:
|
||||
if enabled:
|
||||
svc.enable()
|
||||
else:
|
||||
svc.disable()
|
||||
except (OSError, IOError):
|
||||
e = get_exception()
|
||||
module.fail_json(msg="Could change service link: %s" % str(e))
|
||||
|
||||
if state is not None and state != svc.state:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
getattr(svc,state[:-2])()
|
||||
|
||||
if downed is not None and downed != svc.downed:
|
||||
changed = True
|
||||
if not module.check_mode:
|
||||
d_file = "%s/down" % svc.svc_full
|
||||
try:
|
||||
if downed:
|
||||
open(d_file, "a").close()
|
||||
else:
|
||||
os.unlink(d_file)
|
||||
except (OSError, IOError):
|
||||
e = get_exception()
|
||||
module.fail_json(msg="Could change downed file: %s " % (str(e)))
|
||||
|
||||
module.exit_json(changed=changed, svc=svc.report())
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
467
lib/ansible/modules/system/timezone.py
Normal file
467
lib/ansible/modules/system/timezone.py
Normal file
@@ -0,0 +1,467 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2016, Shinichi TAMURA (@tmshn)
|
||||
#
|
||||
# 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 re
|
||||
from ansible.module_utils.basic import AnsibleModule, get_platform
|
||||
from ansible.module_utils.six import iteritems
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'committer',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: timezone
|
||||
short_description: Configure timezone setting
|
||||
description:
|
||||
- This module configures the timezone setting, both of the system clock
|
||||
and of the hardware clock. I(Currently only Linux platform is supported.)
|
||||
It is recommended to restart C(crond) after changing the timezone,
|
||||
otherwise the jobs may run at the wrong time.
|
||||
It uses the C(timedatectl) command if available. Otherwise, it edits
|
||||
C(/etc/sysconfig/clock) or C(/etc/timezone) for the system clock,
|
||||
and uses the C(hwclock) command for the hardware clock.
|
||||
If you want to set up the NTP, use M(service) module.
|
||||
version_added: "2.2.0"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the timezone for the system clock.
|
||||
Default is to keep current setting.
|
||||
required: false
|
||||
hwclock:
|
||||
description:
|
||||
- Whether the hardware clock is in UTC or in local timezone.
|
||||
Default is to keep current setting.
|
||||
Note that this option is recommended not to change and may fail
|
||||
to configure, especially on virtual environments such as AWS.
|
||||
required: false
|
||||
aliases: ['rtc']
|
||||
author: "Shinichi TAMURA (@tmshn)"
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
diff:
|
||||
description: The differences about the given arguments.
|
||||
returned: success
|
||||
type: dictionary
|
||||
contains:
|
||||
before:
|
||||
description: The values before change
|
||||
type: dict
|
||||
after:
|
||||
description: The values after change
|
||||
type: dict
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: set timezone to Asia/Tokyo
|
||||
timezone:
|
||||
name: Asia/Tokyo
|
||||
'''
|
||||
|
||||
|
||||
class Timezone(object):
|
||||
"""This is a generic Timezone manipulation class that is subclassed based on platform.
|
||||
|
||||
A subclass may wish to override the following action methods:
|
||||
- get(key, phase) ... get the value from the system at `phase`
|
||||
- set(key, value) ... set the value to the current system
|
||||
"""
|
||||
|
||||
def __new__(cls, module):
|
||||
"""Return the platform-specific subclass.
|
||||
|
||||
It does not use load_platform_subclass() because it need to judge based
|
||||
on whether the `timedatectl` command exists.
|
||||
|
||||
Args:
|
||||
module: The AnsibleModule.
|
||||
"""
|
||||
if get_platform() == 'Linux':
|
||||
if module.get_bin_path('timedatectl') is not None:
|
||||
return super(Timezone, SystemdTimezone).__new__(SystemdTimezone)
|
||||
else:
|
||||
return super(Timezone, NosystemdTimezone).__new__(NosystemdTimezone)
|
||||
else:
|
||||
# Not supported yet
|
||||
return super(Timezone, Timezone).__new__(Timezone)
|
||||
|
||||
def __init__(self, module):
|
||||
"""Initialize of the class.
|
||||
|
||||
Args:
|
||||
module: The AnsibleModule.
|
||||
"""
|
||||
super(Timezone, self).__init__()
|
||||
self.msg = []
|
||||
# `self.value` holds the values for each params on each phases.
|
||||
# Initially there's only info of "planned" phase, but the
|
||||
# `self.check()` function will fill out it.
|
||||
self.value = dict()
|
||||
for key in module.argument_spec:
|
||||
value = module.params[key]
|
||||
if value is not None:
|
||||
self.value[key] = dict(planned=value)
|
||||
self.module = module
|
||||
|
||||
def abort(self, msg):
|
||||
"""Abort the process with error message.
|
||||
|
||||
This is just the wrapper of module.fail_json().
|
||||
|
||||
Args:
|
||||
msg: The error message.
|
||||
"""
|
||||
error_msg = ['Error message:', msg]
|
||||
if len(self.msg) > 0:
|
||||
error_msg.append('Other message(s):')
|
||||
error_msg.extend(self.msg)
|
||||
self.module.fail_json(msg='\n'.join(error_msg))
|
||||
|
||||
def execute(self, *commands, **kwargs):
|
||||
"""Execute the shell command.
|
||||
|
||||
This is just the wrapper of module.run_command().
|
||||
|
||||
Args:
|
||||
*commands: The command to execute.
|
||||
It will be concatenated with single space.
|
||||
**kwargs: Only 'log' key is checked.
|
||||
If kwargs['log'] is true, record the command to self.msg.
|
||||
|
||||
Returns:
|
||||
stdout: Standard output of the command.
|
||||
"""
|
||||
command = ' '.join(commands)
|
||||
(rc, stdout, stderr) = self.module.run_command(command, check_rc=True)
|
||||
if kwargs.get('log', False):
|
||||
self.msg.append('executed `%s`' % command)
|
||||
return stdout
|
||||
|
||||
def diff(self, phase1='before', phase2='after'):
|
||||
"""Calculate the difference between given 2 phases.
|
||||
|
||||
Args:
|
||||
phase1, phase2: The names of phase to compare.
|
||||
|
||||
Returns:
|
||||
diff: The difference of value between phase1 and phase2.
|
||||
This is in the format which can be used with the
|
||||
`--diff` option of ansible-playbook.
|
||||
"""
|
||||
diff = {phase1: {}, phase2: {}}
|
||||
for key, value in iteritems(self.value):
|
||||
diff[phase1][key] = value[phase1]
|
||||
diff[phase2][key] = value[phase2]
|
||||
return diff
|
||||
|
||||
def check(self, phase):
|
||||
"""Check the state in given phase and set it to `self.value`.
|
||||
|
||||
Args:
|
||||
phase: The name of the phase to check.
|
||||
|
||||
Returns:
|
||||
NO RETURN VALUE
|
||||
"""
|
||||
if phase == 'planned':
|
||||
return
|
||||
for key, value in iteritems(self.value):
|
||||
value[phase] = self.get(key, phase)
|
||||
|
||||
def change(self):
|
||||
"""Make the changes effect based on `self.value`."""
|
||||
for key, value in iteritems(self.value):
|
||||
if value['before'] != value['planned']:
|
||||
self.set(key, value['planned'])
|
||||
|
||||
# ===========================================
|
||||
# Platform specific methods (must be replaced by subclass).
|
||||
|
||||
def get(self, key, phase):
|
||||
"""Get the value for the key at the given phase.
|
||||
|
||||
Called from self.check().
|
||||
|
||||
Args:
|
||||
key: The key to get the value
|
||||
phase: The phase to get the value
|
||||
|
||||
Return:
|
||||
value: The value for the key at the given phase.
|
||||
"""
|
||||
self.abort('get(key, phase) is not implemented on target platform')
|
||||
|
||||
def set(self, key, value):
|
||||
"""Set the value for the key (of course, for the phase 'after').
|
||||
|
||||
Called from self.change().
|
||||
|
||||
Args:
|
||||
key: Key to set the value
|
||||
value: Value to set
|
||||
"""
|
||||
self.abort('set(key, value) is not implemented on target platform')
|
||||
|
||||
def _verify_timezone(self):
|
||||
tz = self.value['name']['planned']
|
||||
tzfile = '/usr/share/zoneinfo/%s' % tz
|
||||
if not os.path.isfile(tzfile):
|
||||
self.abort('given timezone "%s" is not available' % tz)
|
||||
|
||||
|
||||
class SystemdTimezone(Timezone):
|
||||
"""This is a Timezone manipulation class systemd-powered Linux.
|
||||
|
||||
It uses the `timedatectl` command to check/set all arguments.
|
||||
"""
|
||||
|
||||
regexps = dict(
|
||||
hwclock=re.compile(r'^\s*RTC in local TZ\s*:\s*([^\s]+)', re.MULTILINE),
|
||||
name =re.compile(r'^\s*Time ?zone\s*:\s*([^\s]+)', re.MULTILINE)
|
||||
)
|
||||
|
||||
subcmds = dict(
|
||||
hwclock='set-local-rtc',
|
||||
name ='set-timezone'
|
||||
)
|
||||
|
||||
def __init__(self, module):
|
||||
super(SystemdTimezone, self).__init__(module)
|
||||
self.timedatectl = module.get_bin_path('timedatectl', required=True)
|
||||
self.status = dict()
|
||||
# Validate given timezone
|
||||
if 'name' in self.value:
|
||||
self._verify_timezone()
|
||||
|
||||
def _get_status(self, phase):
|
||||
if phase not in self.status:
|
||||
self.status[phase] = self.execute(self.timedatectl, 'status')
|
||||
return self.status[phase]
|
||||
|
||||
def get(self, key, phase):
|
||||
status = self._get_status(phase)
|
||||
value = self.regexps[key].search(status).group(1)
|
||||
if key == 'hwclock':
|
||||
# For key='hwclock'; convert yes/no -> local/UTC
|
||||
if self.module.boolean(value):
|
||||
value = 'local'
|
||||
else:
|
||||
value = 'UTC'
|
||||
return value
|
||||
|
||||
def set(self, key, value):
|
||||
# For key='hwclock'; convert UTC/local -> yes/no
|
||||
if key == 'hwclock':
|
||||
if value == 'local':
|
||||
value = 'yes'
|
||||
else:
|
||||
value = 'no'
|
||||
self.execute(self.timedatectl, self.subcmds[key], value, log=True)
|
||||
|
||||
|
||||
class NosystemdTimezone(Timezone):
|
||||
"""This is a Timezone manipulation class for non systemd-powered Linux.
|
||||
|
||||
For timezone setting, it edits the following file and reflect changes:
|
||||
- /etc/sysconfig/clock ... RHEL/CentOS
|
||||
- /etc/timezone ... Debian/Ubuntu
|
||||
For hwclock setting, it executes `hwclock --systohc` command with the
|
||||
'--utc' or '--localtime' option.
|
||||
"""
|
||||
|
||||
conf_files = dict(
|
||||
name =None, # To be set in __init__
|
||||
hwclock=None, # To be set in __init__
|
||||
adjtime='/etc/adjtime'
|
||||
)
|
||||
|
||||
regexps = dict(
|
||||
name =None, # To be set in __init__
|
||||
hwclock=re.compile(r'^UTC\s*=\s*([^\s]+)', re.MULTILINE),
|
||||
adjtime=re.compile(r'^(UTC|LOCAL)$', re.MULTILINE)
|
||||
)
|
||||
|
||||
def __init__(self, module):
|
||||
super(NosystemdTimezone, self).__init__(module)
|
||||
# Validate given timezone
|
||||
if 'name' in self.value:
|
||||
self._verify_timezone()
|
||||
self.update_timezone = self.module.get_bin_path('cp', required=True)
|
||||
self.update_timezone += ' %s /etc/localtime' % tzfile
|
||||
self.update_hwclock = self.module.get_bin_path('hwclock', required=True)
|
||||
# Distribution-specific configurations
|
||||
if self.module.get_bin_path('dpkg-reconfigure') is not None:
|
||||
# Debian/Ubuntu
|
||||
self.update_timezone = self.module.get_bin_path('dpkg-reconfigure', required=True)
|
||||
self.update_timezone += ' --frontend noninteractive tzdata'
|
||||
self.conf_files['name'] = '/etc/timezone'
|
||||
self.conf_files['hwclock'] = '/etc/default/rcS'
|
||||
self.regexps['name'] = re.compile(r'^([^\s]+)', re.MULTILINE)
|
||||
self.tzline_format = '%s\n'
|
||||
else:
|
||||
# RHEL/CentOS
|
||||
if self.module.get_bin_path('tzdata-update') is not None:
|
||||
self.update_timezone = self.module.get_bin_path('tzdata-update', required=True)
|
||||
# else:
|
||||
# self.update_timezone = 'cp ...' <- configured above
|
||||
self.conf_files['name'] = '/etc/sysconfig/clock'
|
||||
self.conf_files['hwclock'] = '/etc/sysconfig/clock'
|
||||
self.regexps['name'] = re.compile(r'^ZONE\s*=\s*"?([^"\s]+)"?', re.MULTILINE)
|
||||
self.tzline_format = 'ZONE="%s"\n'
|
||||
self.update_hwclock = self.module.get_bin_path('hwclock', required=True)
|
||||
|
||||
def _edit_file(self, filename, regexp, value):
|
||||
"""Replace the first matched line with given `value`.
|
||||
|
||||
If `regexp` matched more than once, other than the first line will be deleted.
|
||||
|
||||
Args:
|
||||
filename: The name of the file to edit.
|
||||
regexp: The regular expression to search with.
|
||||
value: The line which will be inserted.
|
||||
"""
|
||||
# Read the file
|
||||
try:
|
||||
file = open(filename, 'r')
|
||||
except IOError:
|
||||
self.abort('cannot read "%s"' % filename)
|
||||
else:
|
||||
lines = file.readlines()
|
||||
file.close()
|
||||
# Find the all matched lines
|
||||
matched_indices = []
|
||||
for i, line in enumerate(lines):
|
||||
if regexp.search(line):
|
||||
matched_indices.append(i)
|
||||
if len(matched_indices) > 0:
|
||||
insert_line = matched_indices[0]
|
||||
else:
|
||||
insert_line = 0
|
||||
# Remove all matched lines
|
||||
for i in matched_indices[::-1]:
|
||||
del lines[i]
|
||||
# ...and insert the value
|
||||
lines.insert(insert_line, value)
|
||||
# Write the changes
|
||||
try:
|
||||
file = open(filename, 'w')
|
||||
except IOError:
|
||||
self.abort('cannot write to "%s"' % filename)
|
||||
else:
|
||||
file.writelines(lines)
|
||||
file.close()
|
||||
self.msg.append('Added 1 line and deleted %s line(s) on %s' % (len(matched_indices), filename))
|
||||
|
||||
def get(self, key, phase):
|
||||
if key == 'hwclock' and os.path.isfile('/etc/adjtime'):
|
||||
# If /etc/adjtime exists, use that file.
|
||||
key = 'adjtime'
|
||||
|
||||
filename = self.conf_files[key]
|
||||
|
||||
try:
|
||||
file = open(filename, mode='r')
|
||||
except IOError:
|
||||
self.abort('cannot read configuration file "%s" for %s' % (filename, key))
|
||||
else:
|
||||
status = file.read()
|
||||
file.close()
|
||||
try:
|
||||
value = self.regexps[key].search(status).group(1)
|
||||
except AttributeError:
|
||||
self.abort('cannot find the valid value from configuration file "%s" for %s' % (filename, key))
|
||||
else:
|
||||
if key == 'hwclock':
|
||||
# For key='hwclock'; convert yes/no -> UTC/local
|
||||
if self.module.boolean(value):
|
||||
value = 'UTC'
|
||||
else:
|
||||
value = 'local'
|
||||
elif key == 'adjtime':
|
||||
# For key='adjtime'; convert LOCAL -> local
|
||||
if value != 'UTC':
|
||||
value = value.lower()
|
||||
return value
|
||||
|
||||
def set_timezone(self, value):
|
||||
self._edit_file(filename=self.conf_files['name'],
|
||||
regexp=self.regexps['name'],
|
||||
value=self.tzline_format % value)
|
||||
self.execute(self.update_timezone)
|
||||
|
||||
def set_hwclock(self, value):
|
||||
if value == 'local':
|
||||
option = '--localtime'
|
||||
else:
|
||||
option = '--utc'
|
||||
self.execute(self.update_hwclock, '--systohc', option, log=True)
|
||||
|
||||
def set(self, key, value):
|
||||
if key == 'name':
|
||||
self.set_timezone(value)
|
||||
elif key == 'hwclock':
|
||||
self.set_hwclock(value)
|
||||
else:
|
||||
self.abort('unknown parameter "%s"' % key)
|
||||
|
||||
|
||||
def main():
|
||||
# Construct 'module' and 'tz'
|
||||
arg_spec = dict(
|
||||
hwclock=dict(choices=['UTC', 'local'], aliases=['rtc']),
|
||||
name =dict(),
|
||||
)
|
||||
module = AnsibleModule(
|
||||
argument_spec=arg_spec,
|
||||
required_one_of=[arg_spec.keys()],
|
||||
supports_check_mode=True
|
||||
)
|
||||
tz = Timezone(module)
|
||||
|
||||
# Check the current state
|
||||
tz.check(phase='before')
|
||||
if module.check_mode:
|
||||
diff = tz.diff('before', 'planned')
|
||||
# In check mode, 'planned' state is treated as 'after' state
|
||||
diff['after'] = diff.pop('planned')
|
||||
else:
|
||||
# Make change
|
||||
tz.change()
|
||||
# Check the current state
|
||||
tz.check(phase='after')
|
||||
# Examine if the current state matches planned state
|
||||
(after, planned) = tz.diff('after', 'planned').values()
|
||||
if after != planned:
|
||||
tz.abort('still not desired state, though changes have made')
|
||||
diff = tz.diff('before', 'after')
|
||||
|
||||
changed = (diff['before'] != diff['after'])
|
||||
if len(tz.msg) > 0:
|
||||
module.exit_json(changed=changed, diff=diff, msg='\n'.join(tz.msg))
|
||||
else:
|
||||
module.exit_json(changed=changed, diff=diff)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
337
lib/ansible/modules/system/ufw.py
Normal file
337
lib/ansible/modules/system/ufw.py
Normal file
@@ -0,0 +1,337 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2014, Ahti Kitsik <ak@ahtik.com>
|
||||
# (c) 2014, Jarno Keskikangas <jarno.keskikangas@gmail.com>
|
||||
# (c) 2013, Aleksey Ovcharenko <aleksey.ovcharenko@gmail.com>
|
||||
# (c) 2013, James Martin <jmartin@basho.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/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: ufw
|
||||
short_description: Manage firewall with UFW
|
||||
description:
|
||||
- Manage firewall with UFW.
|
||||
version_added: 1.6
|
||||
author:
|
||||
- "Aleksey Ovcharenko (@ovcharenko)"
|
||||
- "Jarno Keskikangas (@pyykkis)"
|
||||
- "Ahti Kitsik (@ahtik)"
|
||||
notes:
|
||||
- See C(man ufw) for more examples.
|
||||
requirements:
|
||||
- C(ufw) package
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- C(enabled) reloads firewall and enables firewall on boot.
|
||||
- C(disabled) unloads firewall and disables firewall on boot.
|
||||
- C(reloaded) reloads firewall.
|
||||
- C(reset) disables and resets firewall to installation defaults.
|
||||
required: false
|
||||
choices: ['enabled', 'disabled', 'reloaded', 'reset']
|
||||
policy:
|
||||
description:
|
||||
- Change the default policy for incoming or outgoing traffic.
|
||||
required: false
|
||||
alias: default
|
||||
choices: ['allow', 'deny', 'reject']
|
||||
direction:
|
||||
description:
|
||||
- Select direction for a rule or default policy command.
|
||||
required: false
|
||||
choices: ['in', 'out', 'incoming', 'outgoing', 'routed']
|
||||
logging:
|
||||
description:
|
||||
- Toggles logging. Logged packets use the LOG_KERN syslog facility.
|
||||
choices: ['on', 'off', 'low', 'medium', 'high', 'full']
|
||||
required: false
|
||||
insert:
|
||||
description:
|
||||
- Insert the corresponding rule as rule number NUM
|
||||
required: false
|
||||
rule:
|
||||
description:
|
||||
- Add firewall rule
|
||||
required: false
|
||||
choices: ['allow', 'deny', 'reject', 'limit']
|
||||
log:
|
||||
description:
|
||||
- Log new connections matched to this rule
|
||||
required: false
|
||||
choices: ['yes', 'no']
|
||||
from_ip:
|
||||
description:
|
||||
- Source IP address.
|
||||
required: false
|
||||
aliases: ['from', 'src']
|
||||
default: 'any'
|
||||
from_port:
|
||||
description:
|
||||
- Source port.
|
||||
required: false
|
||||
to_ip:
|
||||
description:
|
||||
- Destination IP address.
|
||||
required: false
|
||||
aliases: ['to', 'dest']
|
||||
default: 'any'
|
||||
to_port:
|
||||
description:
|
||||
- Destination port.
|
||||
required: false
|
||||
aliases: ['port']
|
||||
proto:
|
||||
description:
|
||||
- TCP/IP protocol.
|
||||
choices: ['any', 'tcp', 'udp', 'ipv6', 'esp', 'ah']
|
||||
required: false
|
||||
name:
|
||||
description:
|
||||
- Use profile located in C(/etc/ufw/applications.d)
|
||||
required: false
|
||||
aliases: ['app']
|
||||
delete:
|
||||
description:
|
||||
- Delete rule.
|
||||
required: false
|
||||
choices: ['yes', 'no']
|
||||
interface:
|
||||
description:
|
||||
- Specify interface for rule.
|
||||
required: false
|
||||
aliases: ['if']
|
||||
route:
|
||||
description:
|
||||
- Apply the rule to routed/forwarded packets.
|
||||
required: false
|
||||
choices: ['yes', 'no']
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Allow everything and enable UFW
|
||||
- ufw:
|
||||
state: enabled
|
||||
policy: allow
|
||||
|
||||
# Set logging
|
||||
- ufw:
|
||||
logging: on
|
||||
|
||||
# Sometimes it is desirable to let the sender know when traffic is
|
||||
# being denied, rather than simply ignoring it. In these cases, use
|
||||
# reject instead of deny. In addition, log rejected connections:
|
||||
- ufw:
|
||||
rule: reject
|
||||
port: auth
|
||||
log: yes
|
||||
|
||||
# ufw supports connection rate limiting, which is useful for protecting
|
||||
# against brute-force login attacks. ufw will deny connections if an IP
|
||||
# address has attempted to initiate 6 or more connections in the last
|
||||
# 30 seconds. See http://www.debian-administration.org/articles/187
|
||||
# for details. Typical usage is:
|
||||
- ufw:
|
||||
rule: limit
|
||||
port: ssh
|
||||
proto: tcp
|
||||
|
||||
# Allow OpenSSH. (Note that as ufw manages its own state, simply removing
|
||||
# a rule=allow task can leave those ports exposed. Either use delete=yes
|
||||
# or a separate state=reset task)
|
||||
- ufw:
|
||||
rule: allow
|
||||
name: OpenSSH
|
||||
|
||||
# Delete OpenSSH rule
|
||||
- ufw:
|
||||
rule: allow
|
||||
name: OpenSSH
|
||||
delete: yes
|
||||
|
||||
# Deny all access to port 53:
|
||||
- ufw:
|
||||
rule: deny
|
||||
port: 53
|
||||
|
||||
# Allow port range 60000-61000
|
||||
- ufw:
|
||||
rule: allow
|
||||
port: '60000:61000'
|
||||
|
||||
# Allow all access to tcp port 80:
|
||||
- ufw:
|
||||
rule: allow
|
||||
port: 80
|
||||
proto: tcp
|
||||
|
||||
# Allow all access from RFC1918 networks to this host:
|
||||
- ufw:
|
||||
rule: allow
|
||||
src: '{{ item }}'
|
||||
with_items:
|
||||
- 10.0.0.0/8
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
|
||||
# Deny access to udp port 514 from host 1.2.3.4:
|
||||
- ufw:
|
||||
rule: deny
|
||||
proto: udp
|
||||
src: 1.2.3.4
|
||||
port: 514
|
||||
|
||||
# Allow incoming access to eth0 from 1.2.3.5 port 5469 to 1.2.3.4 port 5469
|
||||
- ufw:
|
||||
rule: allow
|
||||
interface: eth0
|
||||
direction: in
|
||||
proto: udp
|
||||
src: 1.2.3.5
|
||||
from_port: 5469
|
||||
dest: 1.2.3.4
|
||||
to_port: 5469
|
||||
|
||||
# Deny all traffic from the IPv6 2001:db8::/32 to tcp port 25 on this host.
|
||||
# Note that IPv6 must be enabled in /etc/default/ufw for IPv6 firewalling to work.
|
||||
- ufw:
|
||||
rule: deny
|
||||
proto: tcp
|
||||
src: '2001:db8::/32'
|
||||
port: 25
|
||||
|
||||
# Deny forwarded/routed traffic from subnet 1.2.3.0/24 to subnet 4.5.6.0/24.
|
||||
# Can be used to further restrict a global FORWARD policy set to allow
|
||||
- ufw:
|
||||
rule: deny
|
||||
route: yes
|
||||
src: 1.2.3.0/24
|
||||
dest: 4.5.6.0/24
|
||||
'''
|
||||
|
||||
from operator import itemgetter
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
state = dict(default=None, choices=['enabled', 'disabled', 'reloaded', 'reset']),
|
||||
default = dict(default=None, aliases=['policy'], choices=['allow', 'deny', 'reject']),
|
||||
logging = dict(default=None, choices=['on', 'off', 'low', 'medium', 'high', 'full']),
|
||||
direction = dict(default=None, choices=['in', 'incoming', 'out', 'outgoing', 'routed']),
|
||||
delete = dict(default=False, type='bool'),
|
||||
route = dict(default=False, type='bool'),
|
||||
insert = dict(default=None),
|
||||
rule = dict(default=None, choices=['allow', 'deny', 'reject', 'limit']),
|
||||
interface = dict(default=None, aliases=['if']),
|
||||
log = dict(default=False, type='bool'),
|
||||
from_ip = dict(default='any', aliases=['src', 'from']),
|
||||
from_port = dict(default=None),
|
||||
to_ip = dict(default='any', aliases=['dest', 'to']),
|
||||
to_port = dict(default=None, aliases=['port']),
|
||||
proto = dict(default=None, aliases=['protocol'], choices=['any', 'tcp', 'udp', 'ipv6', 'esp', 'ah']),
|
||||
app = dict(default=None, aliases=['name'])
|
||||
),
|
||||
supports_check_mode = True,
|
||||
mutually_exclusive = [['app', 'proto', 'logging']]
|
||||
)
|
||||
|
||||
cmds = []
|
||||
|
||||
def execute(cmd):
|
||||
cmd = ' '.join(map(itemgetter(-1), filter(itemgetter(0), cmd)))
|
||||
|
||||
cmds.append(cmd)
|
||||
(rc, out, err) = module.run_command(cmd)
|
||||
|
||||
if rc != 0:
|
||||
module.fail_json(msg=err or out)
|
||||
|
||||
params = module.params
|
||||
|
||||
# Ensure at least one of the command arguments are given
|
||||
command_keys = ['state', 'default', 'rule', 'logging']
|
||||
commands = dict((key, params[key]) for key in command_keys if params[key])
|
||||
|
||||
if len(commands) < 1:
|
||||
module.fail_json(msg="Not any of the command arguments %s given" % commands)
|
||||
|
||||
if(params['interface'] is not None and params['direction'] is None):
|
||||
module.fail_json(msg="Direction must be specified when creating a rule on an interface")
|
||||
|
||||
# Ensure ufw is available
|
||||
ufw_bin = module.get_bin_path('ufw', True)
|
||||
|
||||
# Save the pre state and rules in order to recognize changes
|
||||
(_, pre_state, _) = module.run_command(ufw_bin + ' status verbose')
|
||||
(_, pre_rules, _) = module.run_command("grep '^### tuple' /lib/ufw/user*.rules")
|
||||
|
||||
# Execute commands
|
||||
for (command, value) in commands.iteritems():
|
||||
cmd = [[ufw_bin], [module.check_mode, '--dry-run']]
|
||||
|
||||
if command == 'state':
|
||||
states = { 'enabled': 'enable', 'disabled': 'disable',
|
||||
'reloaded': 'reload', 'reset': 'reset' }
|
||||
execute(cmd + [['-f'], [states[value]]])
|
||||
|
||||
elif command == 'logging':
|
||||
execute(cmd + [[command], [value]])
|
||||
|
||||
elif command == 'default':
|
||||
execute(cmd + [[command], [value], [params['direction']]])
|
||||
|
||||
elif command == 'rule':
|
||||
# Rules are constructed according to the long format
|
||||
#
|
||||
# ufw [--dry-run] [delete] [insert NUM] [route] allow|deny|reject|limit [in|out on INTERFACE] [log|log-all] \
|
||||
# [from ADDRESS [port PORT]] [to ADDRESS [port PORT]] \
|
||||
# [proto protocol] [app application]
|
||||
cmd.append([module.boolean(params['delete']), 'delete'])
|
||||
cmd.append([module.boolean(params['route']), 'route'])
|
||||
cmd.append([params['insert'], "insert %s" % params['insert']])
|
||||
cmd.append([value])
|
||||
cmd.append([params['direction'], "%s" % params['direction']])
|
||||
cmd.append([params['interface'], "on %s" % params['interface']])
|
||||
cmd.append([module.boolean(params['log']), 'log'])
|
||||
|
||||
for (key, template) in [('from_ip', "from %s" ), ('from_port', "port %s" ),
|
||||
('to_ip', "to %s" ), ('to_port', "port %s" ),
|
||||
('proto', "proto %s"), ('app', "app '%s'")]:
|
||||
|
||||
value = params[key]
|
||||
cmd.append([value, template % (value)])
|
||||
|
||||
execute(cmd)
|
||||
|
||||
# Get the new state
|
||||
(_, post_state, _) = module.run_command(ufw_bin + ' status verbose')
|
||||
(_, post_rules, _) = module.run_command("grep '^### tuple' /lib/ufw/user*.rules")
|
||||
changed = (pre_state != post_state) or (pre_rules != post_rules)
|
||||
|
||||
return module.exit_json(changed=changed, commands=cmds, msg=post_state.rstrip())
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
281
lib/ansible/modules/system/zfs.py
Normal file
281
lib/ansible/modules/system/zfs.py
Normal file
@@ -0,0 +1,281 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2013, Johan Wiren <johan.wiren.se@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/>.
|
||||
#
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: zfs
|
||||
short_description: Manage zfs
|
||||
description:
|
||||
- Manages ZFS file systems, volumes, clones and snapshots.
|
||||
version_added: "1.1"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- File system, snapshot or volume name e.g. C(rpool/myfs)
|
||||
required: true
|
||||
state:
|
||||
description:
|
||||
- Whether to create (C(present)), or remove (C(absent)) a
|
||||
file system, snapshot or volume. All parents/children
|
||||
will be created/destroyed as needed to reach the desired state.
|
||||
choices: ['present', 'absent']
|
||||
required: true
|
||||
origin:
|
||||
description:
|
||||
- Snapshot from which to create a clone
|
||||
default: null
|
||||
required: false
|
||||
key_value:
|
||||
description:
|
||||
- The C(zfs) module takes key=value pairs for zfs properties to be set. See the zfs(8) man page for more information.
|
||||
default: null
|
||||
required: false
|
||||
|
||||
author: "Johan Wiren (@johanwiren)"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create a new file system called myfs in pool rpool with the setuid property turned off
|
||||
- zfs:
|
||||
name: rpool/myfs
|
||||
state: present
|
||||
setuid: off
|
||||
|
||||
# Create a new volume called myvol in pool rpool.
|
||||
- zfs:
|
||||
name: rpool/myvol
|
||||
state: present
|
||||
volsize: 10M
|
||||
|
||||
# Create a snapshot of rpool/myfs file system.
|
||||
- zfs:
|
||||
name: rpool/myfs@mysnapshot
|
||||
state: present
|
||||
|
||||
# Create a new file system called myfs2 with snapdir enabled
|
||||
- zfs:
|
||||
name: rpool/myfs2
|
||||
state: present
|
||||
snapdir: enabled
|
||||
|
||||
# Create a new file system by cloning a snapshot
|
||||
- zfs:
|
||||
name: rpool/cloned_fs
|
||||
state: present
|
||||
origin: rpool/myfs@mysnapshot
|
||||
|
||||
# Destroy a filesystem
|
||||
- zfs:
|
||||
name: rpool/myfs
|
||||
state: absent
|
||||
'''
|
||||
|
||||
|
||||
import os
|
||||
|
||||
|
||||
class Zfs(object):
|
||||
|
||||
def __init__(self, module, name, properties):
|
||||
self.module = module
|
||||
self.name = name
|
||||
self.properties = properties
|
||||
self.changed = False
|
||||
self.zfs_cmd = module.get_bin_path('zfs', True)
|
||||
self.zpool_cmd = module.get_bin_path('zpool', True)
|
||||
self.pool = name.split('/')[0]
|
||||
self.is_solaris = os.uname()[0] == 'SunOS'
|
||||
self.is_openzfs = self.check_openzfs()
|
||||
self.enhanced_sharing = self.check_enhanced_sharing()
|
||||
|
||||
def check_openzfs(self):
|
||||
cmd = [self.zpool_cmd]
|
||||
cmd.extend(['get', 'version'])
|
||||
cmd.append(self.pool)
|
||||
(rc, out, err) = self.module.run_command(cmd, check_rc=True)
|
||||
version = out.splitlines()[-1].split()[2]
|
||||
if version == '-':
|
||||
return True
|
||||
if int(version) == 5000:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_enhanced_sharing(self):
|
||||
if self.is_solaris and not self.is_openzfs:
|
||||
cmd = [self.zpool_cmd]
|
||||
cmd.extend(['get', 'version'])
|
||||
cmd.append(self.pool)
|
||||
(rc, out, err) = self.module.run_command(cmd, check_rc=True)
|
||||
version = out.splitlines()[-1].split()[2]
|
||||
if int(version) >= 34:
|
||||
return True
|
||||
return False
|
||||
|
||||
def exists(self):
|
||||
cmd = [self.zfs_cmd, 'list', '-t', 'all', self.name]
|
||||
(rc, out, err) = self.module.run_command(' '.join(cmd))
|
||||
if rc == 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def create(self):
|
||||
if self.module.check_mode:
|
||||
self.changed = True
|
||||
return
|
||||
properties = self.properties
|
||||
volsize = properties.pop('volsize', None)
|
||||
volblocksize = properties.pop('volblocksize', None)
|
||||
origin = properties.pop('origin', None)
|
||||
cmd = [self.zfs_cmd]
|
||||
|
||||
if "@" in self.name:
|
||||
action = 'snapshot'
|
||||
elif origin:
|
||||
action = 'clone'
|
||||
else:
|
||||
action = 'create'
|
||||
|
||||
cmd.append(action)
|
||||
|
||||
if action in ['create', 'clone']:
|
||||
cmd += ['-p']
|
||||
|
||||
if volsize:
|
||||
cmd += ['-V', volsize]
|
||||
if volblocksize:
|
||||
cmd += ['-b', 'volblocksize']
|
||||
if properties:
|
||||
for prop, value in properties.iteritems():
|
||||
cmd += ['-o', '%s="%s"' % (prop, value)]
|
||||
if origin:
|
||||
cmd.append(origin)
|
||||
cmd.append(self.name)
|
||||
(rc, out, err) = self.module.run_command(' '.join(cmd))
|
||||
if rc == 0:
|
||||
self.changed = True
|
||||
else:
|
||||
self.module.fail_json(msg=err)
|
||||
|
||||
def destroy(self):
|
||||
if self.module.check_mode:
|
||||
self.changed = True
|
||||
return
|
||||
cmd = [self.zfs_cmd, 'destroy', '-R', self.name]
|
||||
(rc, out, err) = self.module.run_command(' '.join(cmd))
|
||||
if rc == 0:
|
||||
self.changed = True
|
||||
else:
|
||||
self.module.fail_json(msg=err)
|
||||
|
||||
def set_property(self, prop, value):
|
||||
if self.module.check_mode:
|
||||
self.changed = True
|
||||
return
|
||||
cmd = [self.zfs_cmd, 'set', prop + '=' + str(value), self.name]
|
||||
(rc, out, err) = self.module.run_command(cmd)
|
||||
if rc == 0:
|
||||
self.changed = True
|
||||
else:
|
||||
self.module.fail_json(msg=err)
|
||||
|
||||
def set_properties_if_changed(self):
|
||||
current_properties = self.get_current_properties()
|
||||
for prop, value in self.properties.iteritems():
|
||||
if current_properties.get(prop, None) != value:
|
||||
self.set_property(prop, value)
|
||||
|
||||
def get_current_properties(self):
|
||||
cmd = [self.zfs_cmd, 'get', '-H']
|
||||
if self.enhanced_sharing:
|
||||
cmd += ['-e']
|
||||
cmd += ['all', self.name]
|
||||
rc, out, err = self.module.run_command(" ".join(cmd))
|
||||
properties = dict()
|
||||
for prop, value, source in [l.split('\t')[1:4] for l in out.splitlines()]:
|
||||
if source == 'local':
|
||||
properties[prop] = value
|
||||
# Add alias for enhanced sharing properties
|
||||
if self.enhanced_sharing:
|
||||
properties['sharenfs'] = properties.get('share.nfs', None)
|
||||
properties['sharesmb'] = properties.get('share.smb', None)
|
||||
return properties
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
name = dict(type='str', required=True),
|
||||
state = dict(type='str', required=True, choices=['present', 'absent']),
|
||||
# No longer used. Kept here to not interfere with zfs properties
|
||||
createparent = dict(type='bool', required=False)
|
||||
),
|
||||
supports_check_mode=True,
|
||||
check_invalid_arguments=False
|
||||
)
|
||||
|
||||
state = module.params.pop('state')
|
||||
name = module.params.pop('name')
|
||||
|
||||
# Get all valid zfs-properties
|
||||
properties = dict()
|
||||
for prop, value in module.params.iteritems():
|
||||
# All freestyle params are zfs properties
|
||||
if prop not in module.argument_spec:
|
||||
# Reverse the boolification of freestyle zfs properties
|
||||
if isinstance(value, bool):
|
||||
if value is True:
|
||||
properties[prop] = 'on'
|
||||
else:
|
||||
properties[prop] = 'off'
|
||||
else:
|
||||
properties[prop] = value
|
||||
|
||||
result = {}
|
||||
result['name'] = name
|
||||
result['state'] = state
|
||||
|
||||
zfs = Zfs(module, name, properties)
|
||||
|
||||
if state == 'present':
|
||||
if zfs.exists():
|
||||
zfs.set_properties_if_changed()
|
||||
else:
|
||||
zfs.create()
|
||||
|
||||
elif state == 'absent':
|
||||
if zfs.exists():
|
||||
zfs.destroy()
|
||||
|
||||
result.update(zfs.properties)
|
||||
result['changed'] = zfs.changed
|
||||
module.exit_json(**result)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user