mirror of https://github.com/ansible/ansible.git
Merge remote-tracking branch 'master/devel' into devel
commit
e321d0f34d
@ -0,0 +1,168 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
|
||||||
|
module: uptimerobot
|
||||||
|
short_description: Pause and start Uptime Robot monitoring
|
||||||
|
description:
|
||||||
|
- This module will let you start and pause Uptime Robot Monitoring
|
||||||
|
author: Nate Kingsley
|
||||||
|
version_added: "1.9"
|
||||||
|
requirements:
|
||||||
|
- Valid Uptime Robot API Key
|
||||||
|
options:
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Define whether or not the monitor should be running or paused.
|
||||||
|
required: true
|
||||||
|
default: null
|
||||||
|
choices: [ "started", "paused" ]
|
||||||
|
aliases: []
|
||||||
|
monitorid:
|
||||||
|
description:
|
||||||
|
- ID of the monitor to check.
|
||||||
|
required: true
|
||||||
|
default: null
|
||||||
|
choices: []
|
||||||
|
aliases: []
|
||||||
|
apikey:
|
||||||
|
description:
|
||||||
|
- Uptime Robot API key.
|
||||||
|
required: true
|
||||||
|
default: null
|
||||||
|
choices: []
|
||||||
|
aliases: []
|
||||||
|
notes:
|
||||||
|
- Support for adding and removing monitors and alert contacts has not yet been implemented.
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Pause the monitor with an ID of 12345.
|
||||||
|
- uptimerobot: monitorid=12345
|
||||||
|
apikey=12345-1234512345
|
||||||
|
state=paused
|
||||||
|
|
||||||
|
# Start the monitor with an ID of 12345.
|
||||||
|
- uptimerobot: monitorid=12345
|
||||||
|
apikey=12345-1234512345
|
||||||
|
state=started
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import json
|
||||||
|
import urllib
|
||||||
|
import urllib2
|
||||||
|
import time
|
||||||
|
|
||||||
|
API_BASE = "http://api.uptimerobot.com/"
|
||||||
|
|
||||||
|
API_ACTIONS = dict(
|
||||||
|
status='getMonitors?',
|
||||||
|
editMonitor='editMonitor?'
|
||||||
|
)
|
||||||
|
|
||||||
|
API_FORMAT = 'json'
|
||||||
|
|
||||||
|
API_NOJSONCALLBACK = 1
|
||||||
|
|
||||||
|
CHANGED_STATE = False
|
||||||
|
|
||||||
|
SUPPORTS_CHECK_MODE = False
|
||||||
|
|
||||||
|
def checkID(params):
|
||||||
|
|
||||||
|
data = urllib.urlencode(params)
|
||||||
|
|
||||||
|
full_uri = API_BASE + API_ACTIONS['status'] + data
|
||||||
|
|
||||||
|
req = urllib2.urlopen(full_uri)
|
||||||
|
|
||||||
|
result = req.read()
|
||||||
|
|
||||||
|
jsonresult = json.loads(result)
|
||||||
|
|
||||||
|
req.close()
|
||||||
|
|
||||||
|
return jsonresult
|
||||||
|
|
||||||
|
|
||||||
|
def startMonitor(params):
|
||||||
|
|
||||||
|
params['monitorStatus'] = 1
|
||||||
|
|
||||||
|
data = urllib.urlencode(params)
|
||||||
|
|
||||||
|
full_uri = API_BASE + API_ACTIONS['editMonitor'] + data
|
||||||
|
|
||||||
|
req = urllib2.urlopen(full_uri)
|
||||||
|
|
||||||
|
result = req.read()
|
||||||
|
|
||||||
|
jsonresult = json.loads(result)
|
||||||
|
|
||||||
|
req.close()
|
||||||
|
|
||||||
|
return jsonresult['stat']
|
||||||
|
|
||||||
|
|
||||||
|
def pauseMonitor(params):
|
||||||
|
|
||||||
|
params['monitorStatus'] = 0
|
||||||
|
|
||||||
|
data = urllib.urlencode(params)
|
||||||
|
|
||||||
|
full_uri = API_BASE + API_ACTIONS['editMonitor'] + data
|
||||||
|
|
||||||
|
req = urllib2.urlopen(full_uri)
|
||||||
|
|
||||||
|
result = req.read()
|
||||||
|
|
||||||
|
jsonresult = json.loads(result)
|
||||||
|
|
||||||
|
req.close()
|
||||||
|
|
||||||
|
return jsonresult['stat']
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
state = dict(required=True, choices=['started', 'paused']),
|
||||||
|
apikey = dict(required=True),
|
||||||
|
monitorid = dict(required=True)
|
||||||
|
),
|
||||||
|
supports_check_mode=SUPPORTS_CHECK_MODE
|
||||||
|
)
|
||||||
|
|
||||||
|
params = dict(
|
||||||
|
apiKey=module.params['apikey'],
|
||||||
|
monitors=module.params['monitorid'],
|
||||||
|
monitorID=module.params['monitorid'],
|
||||||
|
format=API_FORMAT,
|
||||||
|
noJsonCallback=API_NOJSONCALLBACK
|
||||||
|
)
|
||||||
|
|
||||||
|
check_result = checkID(params)
|
||||||
|
|
||||||
|
if check_result['stat'] != "ok":
|
||||||
|
module.fail_json(
|
||||||
|
msg="failed",
|
||||||
|
result=check_result['message']
|
||||||
|
)
|
||||||
|
|
||||||
|
if module.params['state'] == 'started':
|
||||||
|
monitor_result = startMonitor(params)
|
||||||
|
else:
|
||||||
|
monitor_result = pauseMonitor(params)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exit_json(
|
||||||
|
msg="success",
|
||||||
|
result=monitor_result
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
||||||
@ -0,0 +1,212 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2014, René Moser <mail@renemoser.net>
|
||||||
|
#
|
||||||
|
# This file is part of Ansible
|
||||||
|
#
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: zabbix_group
|
||||||
|
short_description: Add or remove a host group to Zabbix.
|
||||||
|
description:
|
||||||
|
- This module uses the Zabbix API to add and remove host groups.
|
||||||
|
version_added: '1.8'
|
||||||
|
requirements: [ 'zabbix-api' ]
|
||||||
|
options:
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Whether the host group should be added or removed.
|
||||||
|
required: false
|
||||||
|
default: present
|
||||||
|
choices: [ 'present', 'absent' ]
|
||||||
|
host_group:
|
||||||
|
description:
|
||||||
|
- Name of the host group to be added or removed.
|
||||||
|
required: true
|
||||||
|
default: null
|
||||||
|
aliases: [ ]
|
||||||
|
server_url:
|
||||||
|
description:
|
||||||
|
- Url of Zabbix server, with protocol (http or https) e.g.
|
||||||
|
https://monitoring.example.com/zabbix. C(url) is an alias
|
||||||
|
for C(server_url). If not set environment variable
|
||||||
|
C(ZABBIX_SERVER_URL) is used.
|
||||||
|
required: true
|
||||||
|
default: null
|
||||||
|
aliases: [ 'url' ]
|
||||||
|
login_user:
|
||||||
|
description:
|
||||||
|
- Zabbix user name. If not set environment variable
|
||||||
|
C(ZABBIX_LOGIN_USER) is used.
|
||||||
|
required: true
|
||||||
|
default: null
|
||||||
|
login_password:
|
||||||
|
description:
|
||||||
|
- Zabbix user password. If not set environment variable
|
||||||
|
C(ZABBIX_LOGIN_PASSWORD) is used.
|
||||||
|
required: true
|
||||||
|
notes:
|
||||||
|
- The module has been tested with Zabbix Server 2.2.
|
||||||
|
author: René Moser
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
---
|
||||||
|
# Add a new host group to Zabbix
|
||||||
|
- zabbix_group: host_group='Linux servers'
|
||||||
|
server_url=https://monitoring.example.com/zabbix
|
||||||
|
login_user=ansible
|
||||||
|
login_password=secure
|
||||||
|
|
||||||
|
# Add a new host group, login data is provided by environment variables:
|
||||||
|
# ZABBIX_LOGIN_USER, ZABBIX_LOGIN_PASSWORD, ZABBIX_SERVER_URL:
|
||||||
|
- zabbix_group: host_group=Webservers
|
||||||
|
|
||||||
|
# Remove a host group from Zabbix
|
||||||
|
- zabbix_group: host_group='Linux servers'
|
||||||
|
state=absent
|
||||||
|
server_url=https://monitoring.example.com/zabbix
|
||||||
|
login_user=ansible
|
||||||
|
login_password=secure
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
from zabbix_api import ZabbixAPI
|
||||||
|
HAS_ZABBIX_API = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_ZABBIX_API = False
|
||||||
|
|
||||||
|
|
||||||
|
def create_group(zbx, host_group):
|
||||||
|
try:
|
||||||
|
result = zbx.hostgroup.create(
|
||||||
|
{
|
||||||
|
'name': host_group
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except BaseException as e:
|
||||||
|
return 1, None, str(e)
|
||||||
|
return 0, result['groupids'], None
|
||||||
|
|
||||||
|
|
||||||
|
def get_group(zbx, host_group):
|
||||||
|
try:
|
||||||
|
result = zbx.hostgroup.get(
|
||||||
|
{
|
||||||
|
'filter':
|
||||||
|
{
|
||||||
|
'name': host_group,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except BaseException as e:
|
||||||
|
return 1, None, str(e)
|
||||||
|
|
||||||
|
return 0, result[0]['groupid'], None
|
||||||
|
|
||||||
|
|
||||||
|
def delete_group(zbx, group_id):
|
||||||
|
try:
|
||||||
|
zbx.hostgroup.delete([ group_id ])
|
||||||
|
except BaseException as e:
|
||||||
|
return 1, None, str(e)
|
||||||
|
return 0, None, None
|
||||||
|
|
||||||
|
|
||||||
|
def check_group(zbx, host_group):
|
||||||
|
try:
|
||||||
|
result = zbx.hostgroup.exists(
|
||||||
|
{
|
||||||
|
'name': host_group
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except BaseException as e:
|
||||||
|
return 1, None, str(e)
|
||||||
|
return 0, result, None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
state=dict(default='present', choices=['present', 'absent']),
|
||||||
|
host_group=dict(required=True, default=None),
|
||||||
|
server_url=dict(default=None, aliases=['url']),
|
||||||
|
login_user=dict(default=None),
|
||||||
|
login_password=dict(default=None),
|
||||||
|
),
|
||||||
|
supports_check_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not HAS_ZABBIX_API:
|
||||||
|
module.fail_json(msg='Missing requried zabbix-api module (check docs or install with: pip install zabbix-api)')
|
||||||
|
|
||||||
|
try:
|
||||||
|
login_user = module.params['login_user'] or os.environ['ZABBIX_LOGIN_USER']
|
||||||
|
login_password = module.params['login_password'] or os.environ['ZABBIX_LOGIN_PASSWORD']
|
||||||
|
server_url = module.params['server_url'] or os.environ['ZABBIX_SERVER_URL']
|
||||||
|
except KeyError, e:
|
||||||
|
module.fail_json(msg='Missing login data: %s is not set.' % e.message)
|
||||||
|
|
||||||
|
host_group = module.params['host_group']
|
||||||
|
state = module.params['state']
|
||||||
|
|
||||||
|
try:
|
||||||
|
zbx = ZabbixAPI(server_url)
|
||||||
|
zbx.login(login_user, login_password)
|
||||||
|
except BaseException as e:
|
||||||
|
module.fail_json(msg='Failed to connect to Zabbix server: %s' % e)
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
msg = ''
|
||||||
|
|
||||||
|
if state == 'present':
|
||||||
|
(rc, exists, error) = check_group(zbx, host_group)
|
||||||
|
if rc != 0:
|
||||||
|
module.fail_json(msg='Failed to check host group %s existance: %s' % (host_group, error))
|
||||||
|
if not exists:
|
||||||
|
if module.check_mode:
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
(rc, group, error) = create_group(zbx, host_group)
|
||||||
|
if rc == 0:
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
module.fail_json(msg='Failed to get host group: %s' % error)
|
||||||
|
|
||||||
|
if state == 'absent':
|
||||||
|
(rc, exists, error) = check_group(zbx, host_group)
|
||||||
|
if rc != 0:
|
||||||
|
module.fail_json(msg='Failed to check host group %s existance: %s' % (host_group, error))
|
||||||
|
if exists:
|
||||||
|
if module.check_mode:
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
(rc, group_id, error) = get_group(zbx, host_group)
|
||||||
|
if rc != 0:
|
||||||
|
module.fail_json(msg='Failed to get host group: %s' % error)
|
||||||
|
|
||||||
|
(rc, _, error) = delete_group(zbx, group_id)
|
||||||
|
if rc == 0:
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
module.fail_json(msg='Failed to remove host group: %s' % error)
|
||||||
|
|
||||||
|
module.exit_json(changed=changed)
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
||||||
@ -0,0 +1,252 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2014, Ravi Bhure <ravibhure@gmail.com>
|
||||||
|
#
|
||||||
|
# This file is part of Ansible
|
||||||
|
#
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: haproxy
|
||||||
|
version_added: "1.9"
|
||||||
|
short_description: An Ansible module to handle states enable/disable server and set weight to backend host in haproxy using socket commands.
|
||||||
|
description:
|
||||||
|
- The Enable Haproxy Backend Server, with
|
||||||
|
supports get current weight for server (default) and
|
||||||
|
set weight for haproxy backend server when provides.
|
||||||
|
|
||||||
|
- The Disable Haproxy Backend Server, with
|
||||||
|
supports get current weight for server (default) and
|
||||||
|
shutdown sessions while disabling backend host server.
|
||||||
|
notes:
|
||||||
|
- "enable or disable commands are restricted and can only be issued on sockets configured for level 'admin', "
|
||||||
|
- "Check - http://haproxy.1wt.eu/download/1.5/doc/configuration.txt, "
|
||||||
|
- "Example: 'stats socket /var/run/haproxy.sock level admin'"
|
||||||
|
options:
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- describe the desired state of the given host in lb pool.
|
||||||
|
required: true
|
||||||
|
default: null
|
||||||
|
choices: [ "enabled", "disabled" ]
|
||||||
|
host:
|
||||||
|
description:
|
||||||
|
- Host (backend) to operate in Haproxy.
|
||||||
|
required: true
|
||||||
|
default: null
|
||||||
|
socket:
|
||||||
|
description:
|
||||||
|
- Haproxy socket file name with path.
|
||||||
|
required: false
|
||||||
|
default: /var/run/haproxy.sock
|
||||||
|
backend:
|
||||||
|
description:
|
||||||
|
- Name of the haproxy backend pool.
|
||||||
|
Required, else auto-detection applied.
|
||||||
|
required: false
|
||||||
|
default: auto-detected
|
||||||
|
weight:
|
||||||
|
description:
|
||||||
|
- The value passed in argument. If the value ends with the '%' sign, then the new weight will be relative to the initially cnfigured weight. Relative weights are only permitted between 0 and 100% and absolute weights are permitted between 0 and 256.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
shutdown_sessions:
|
||||||
|
description:
|
||||||
|
- When disabling server, immediately terminate all the sessions attached to the specified server. This can be used to terminate long-running sessions after a server is put into maintenance mode, for instance.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
examples:
|
||||||
|
|
||||||
|
# disable server in 'www' backend pool
|
||||||
|
- haproxy: state=disabled host={{ inventory_hostname }} backend=www
|
||||||
|
|
||||||
|
# disable server without backend pool name (apply to all available backend pool)
|
||||||
|
- haproxy: state=disabled host={{ inventory_hostname }}
|
||||||
|
|
||||||
|
# disable server, provide socket file
|
||||||
|
- haproxy: state=disabled host={{ inventory_hostname }} socket=/var/run/haproxy.sock backend=www
|
||||||
|
|
||||||
|
# disable backend server in 'www' backend pool and drop open sessions to it
|
||||||
|
- haproxy: state=disabled host={{ inventory_hostname }} backend=www socket=/var/run/haproxy.sock shutdown_sessions=true
|
||||||
|
|
||||||
|
# enable server in 'www' backend pool
|
||||||
|
- haproxy: state=enabled host={{ inventory_hostname }} backend=www
|
||||||
|
|
||||||
|
# enable server in 'www' backend pool with change server(s) weight
|
||||||
|
- haproxy: state=enabled host={{ inventory_hostname }} socket=/var/run/haproxy.sock weight=10 backend=www
|
||||||
|
|
||||||
|
author: Ravi Bhure <ravibhure@gmail.com>
|
||||||
|
'''
|
||||||
|
|
||||||
|
import socket
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_SOCKET_LOCATION="/var/run/haproxy.sock"
|
||||||
|
RECV_SIZE = 1024
|
||||||
|
ACTION_CHOICES = ['enabled', 'disabled']
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
class TimeoutException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class HAProxy(object):
|
||||||
|
"""
|
||||||
|
Used for communicating with HAProxy through its local UNIX socket interface.
|
||||||
|
Perform common tasks in Haproxy related to enable server and
|
||||||
|
disable server.
|
||||||
|
|
||||||
|
The complete set of external commands Haproxy handles is documented
|
||||||
|
on their website:
|
||||||
|
|
||||||
|
http://haproxy.1wt.eu/download/1.5/doc/configuration.txt#Unix Socket commands
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, module):
|
||||||
|
self.module = module
|
||||||
|
|
||||||
|
self.state = self.module.params['state']
|
||||||
|
self.host = self.module.params['host']
|
||||||
|
self.backend = self.module.params['backend']
|
||||||
|
self.weight = self.module.params['weight']
|
||||||
|
self.socket = self.module.params['socket']
|
||||||
|
self.shutdown_sessions = self.module.params['shutdown_sessions']
|
||||||
|
|
||||||
|
self.command_results = []
|
||||||
|
|
||||||
|
def execute(self, cmd, timeout=200):
|
||||||
|
"""
|
||||||
|
Executes a HAProxy command by sending a message to a HAProxy's local
|
||||||
|
UNIX socket and waiting up to 'timeout' milliseconds for the response.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
self.client.connect(self.socket)
|
||||||
|
self.client.sendall('%s\n' % cmd)
|
||||||
|
result = ''
|
||||||
|
buf = ''
|
||||||
|
buf = self.client.recv(RECV_SIZE)
|
||||||
|
while buf:
|
||||||
|
result += buf
|
||||||
|
buf = self.client.recv(RECV_SIZE)
|
||||||
|
self.command_results = result.strip()
|
||||||
|
self.client.close()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def enabled(self, host, backend, weight):
|
||||||
|
"""
|
||||||
|
Enabled action, marks server to UP and checks are re-enabled,
|
||||||
|
also supports to get current weight for server (default) and
|
||||||
|
set the weight for haproxy backend server when provides.
|
||||||
|
"""
|
||||||
|
svname = host
|
||||||
|
if self.backend is None:
|
||||||
|
output = self.execute('show stat')
|
||||||
|
#sanitize and make a list of lines
|
||||||
|
output = output.lstrip('# ').strip()
|
||||||
|
output = output.split('\n')
|
||||||
|
result = output
|
||||||
|
|
||||||
|
for line in result:
|
||||||
|
if 'BACKEND' in line:
|
||||||
|
result = line.split(',')[0]
|
||||||
|
pxname = result
|
||||||
|
cmd = "get weight %s/%s ; enable server %s/%s" % (pxname, svname, pxname, svname)
|
||||||
|
if weight:
|
||||||
|
cmd += "; set weight %s/%s %s" % (pxname, svname, weight)
|
||||||
|
self.execute(cmd)
|
||||||
|
|
||||||
|
else:
|
||||||
|
pxname = backend
|
||||||
|
cmd = "get weight %s/%s ; enable server %s/%s" % (pxname, svname, pxname, svname)
|
||||||
|
if weight:
|
||||||
|
cmd += "; set weight %s/%s %s" % (pxname, svname, weight)
|
||||||
|
self.execute(cmd)
|
||||||
|
|
||||||
|
def disabled(self, host, backend, shutdown_sessions):
|
||||||
|
"""
|
||||||
|
Disabled action, marks server to DOWN for maintenance. In this mode, no more checks will be
|
||||||
|
performed on the server until it leaves maintenance,
|
||||||
|
also it shutdown sessions while disabling backend host server.
|
||||||
|
"""
|
||||||
|
svname = host
|
||||||
|
if self.backend is None:
|
||||||
|
output = self.execute('show stat')
|
||||||
|
#sanitize and make a list of lines
|
||||||
|
output = output.lstrip('# ').strip()
|
||||||
|
output = output.split('\n')
|
||||||
|
result = output
|
||||||
|
|
||||||
|
for line in result:
|
||||||
|
if 'BACKEND' in line:
|
||||||
|
result = line.split(',')[0]
|
||||||
|
pxname = result
|
||||||
|
cmd = "get weight %s/%s ; disable server %s/%s" % (pxname, svname, pxname, svname)
|
||||||
|
if shutdown_sessions:
|
||||||
|
cmd += "; shutdown sessions server %s/%s" % (pxname, svname)
|
||||||
|
self.execute(cmd)
|
||||||
|
|
||||||
|
else:
|
||||||
|
pxname = backend
|
||||||
|
cmd = "get weight %s/%s ; disable server %s/%s" % (pxname, svname, pxname, svname)
|
||||||
|
if shutdown_sessions:
|
||||||
|
cmd += "; shutdown sessions server %s/%s" % (pxname, svname)
|
||||||
|
self.execute(cmd)
|
||||||
|
|
||||||
|
def act(self):
|
||||||
|
"""
|
||||||
|
Figure out what you want to do from ansible, and then do it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# toggle enable/disbale server
|
||||||
|
if self.state == 'enabled':
|
||||||
|
self.enabled(self.host, self.backend, self.weight)
|
||||||
|
|
||||||
|
elif self.state == 'disabled':
|
||||||
|
self.disabled(self.host, self.backend, self.shutdown_sessions)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.module.fail_json(msg="unknown state specified: '%s'" % self.state)
|
||||||
|
|
||||||
|
self.module.exit_json(stdout=self.command_results, changed=True)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
# load ansible module object
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
state = dict(required=True, default=None, choices=ACTION_CHOICES),
|
||||||
|
host=dict(required=True, default=None),
|
||||||
|
backend=dict(required=False, default=None),
|
||||||
|
weight=dict(required=False, default=None),
|
||||||
|
socket = dict(required=False, default=DEFAULT_SOCKET_LOCATION),
|
||||||
|
shutdown_sessions=dict(required=False, default=False),
|
||||||
|
),
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
if not socket:
|
||||||
|
module.fail_json(msg="unable to locate haproxy socket")
|
||||||
|
|
||||||
|
ansible_haproxy = HAProxy(module, **module.params)
|
||||||
|
ansible_haproxy.act()
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
|
||||||
|
main()
|
||||||
@ -0,0 +1,366 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
# This file is part of Networklore's snmp library for Ansible
|
||||||
|
#
|
||||||
|
# The 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.
|
||||||
|
#
|
||||||
|
# The 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: snmp_facts
|
||||||
|
version_added: "1.9"
|
||||||
|
author: Patrick Ogenstad (@networklore)
|
||||||
|
short_description: Retrive facts for a device using SNMP.
|
||||||
|
description:
|
||||||
|
- Retrieve facts for a device using SNMP, the facts will be
|
||||||
|
inserted to the ansible_facts key.
|
||||||
|
requirements:
|
||||||
|
- pysnmp
|
||||||
|
options:
|
||||||
|
host:
|
||||||
|
description:
|
||||||
|
- Set to target snmp server (normally {{inventory_hostname}})
|
||||||
|
required: true
|
||||||
|
version:
|
||||||
|
description:
|
||||||
|
- SNMP Version to use, v2/v2c or v3
|
||||||
|
choices: [ 'v2', 'v2c', 'v3' ]
|
||||||
|
required: true
|
||||||
|
community:
|
||||||
|
description:
|
||||||
|
- The SNMP community string, required if version is v2/v2c
|
||||||
|
required: false
|
||||||
|
level:
|
||||||
|
description:
|
||||||
|
- Authentication level, required if version is v3
|
||||||
|
choices: [ 'authPriv', 'authNoPriv' ]
|
||||||
|
required: false
|
||||||
|
username:
|
||||||
|
description:
|
||||||
|
- Username for SNMPv3, required if version is v3
|
||||||
|
required: false
|
||||||
|
integrity:
|
||||||
|
description:
|
||||||
|
- Hashing algoritm, required if version is v3
|
||||||
|
choices: [ 'md5', 'sha' ]
|
||||||
|
required: false
|
||||||
|
authkey:
|
||||||
|
description:
|
||||||
|
- Authentication key, required if version is v3
|
||||||
|
required: false
|
||||||
|
privacy:
|
||||||
|
description:
|
||||||
|
- Encryption algoritm, required if level is authPriv
|
||||||
|
choices: [ 'des', 'aes' ]
|
||||||
|
required: false
|
||||||
|
privkey:
|
||||||
|
description:
|
||||||
|
- Encryption key, required if version is authPriv
|
||||||
|
required: false
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Gather facts with SNMP version 2
|
||||||
|
- snmp_facts: host={{ inventory_hostname }} version=2c community=public
|
||||||
|
connection: local
|
||||||
|
|
||||||
|
# Gather facts using SNMP version 3
|
||||||
|
- snmp_facts:
|
||||||
|
host={{ inventory_hostname }}
|
||||||
|
version=v3
|
||||||
|
level=authPriv
|
||||||
|
integrity=sha
|
||||||
|
privacy=aes
|
||||||
|
username=snmp-user
|
||||||
|
authkey=abc12345
|
||||||
|
privkey=def6789
|
||||||
|
delegate_to: localhost
|
||||||
|
'''
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pysnmp.entity.rfc3413.oneliner import cmdgen
|
||||||
|
has_pysnmp = True
|
||||||
|
except:
|
||||||
|
has_pysnmp = False
|
||||||
|
|
||||||
|
class DefineOid(object):
|
||||||
|
|
||||||
|
def __init__(self,dotprefix=False):
|
||||||
|
if dotprefix:
|
||||||
|
dp = "."
|
||||||
|
else:
|
||||||
|
dp = ""
|
||||||
|
|
||||||
|
# From SNMPv2-MIB
|
||||||
|
self.sysDescr = dp + "1.3.6.1.2.1.1.1.0"
|
||||||
|
self.sysObjectId = dp + "1.3.6.1.2.1.1.2.0"
|
||||||
|
self.sysUpTime = dp + "1.3.6.1.2.1.1.3.0"
|
||||||
|
self.sysContact = dp + "1.3.6.1.2.1.1.4.0"
|
||||||
|
self.sysName = dp + "1.3.6.1.2.1.1.5.0"
|
||||||
|
self.sysLocation = dp + "1.3.6.1.2.1.1.6.0"
|
||||||
|
|
||||||
|
# From IF-MIB
|
||||||
|
self.ifIndex = dp + "1.3.6.1.2.1.2.2.1.1"
|
||||||
|
self.ifDescr = dp + "1.3.6.1.2.1.2.2.1.2"
|
||||||
|
self.ifMtu = dp + "1.3.6.1.2.1.2.2.1.4"
|
||||||
|
self.ifSpeed = dp + "1.3.6.1.2.1.2.2.1.5"
|
||||||
|
self.ifPhysAddress = dp + "1.3.6.1.2.1.2.2.1.6"
|
||||||
|
self.ifAdminStatus = dp + "1.3.6.1.2.1.2.2.1.7"
|
||||||
|
self.ifOperStatus = dp + "1.3.6.1.2.1.2.2.1.8"
|
||||||
|
self.ifAlias = dp + "1.3.6.1.2.1.31.1.1.1.18"
|
||||||
|
|
||||||
|
# From IP-MIB
|
||||||
|
self.ipAdEntAddr = dp + "1.3.6.1.2.1.4.20.1.1"
|
||||||
|
self.ipAdEntIfIndex = dp + "1.3.6.1.2.1.4.20.1.2"
|
||||||
|
self.ipAdEntNetMask = dp + "1.3.6.1.2.1.4.20.1.3"
|
||||||
|
|
||||||
|
|
||||||
|
def decode_hex(hexstring):
|
||||||
|
|
||||||
|
if len(hexstring) < 3:
|
||||||
|
return hexstring
|
||||||
|
if hexstring[:2] == "0x":
|
||||||
|
return hexstring[2:].decode("hex")
|
||||||
|
else:
|
||||||
|
return hexstring
|
||||||
|
|
||||||
|
def decode_mac(hexstring):
|
||||||
|
|
||||||
|
if len(hexstring) != 14:
|
||||||
|
return hexstring
|
||||||
|
if hexstring[:2] == "0x":
|
||||||
|
return hexstring[2:]
|
||||||
|
else:
|
||||||
|
return hexstring
|
||||||
|
|
||||||
|
def lookup_adminstatus(int_adminstatus):
|
||||||
|
adminstatus_options = {
|
||||||
|
1: 'up',
|
||||||
|
2: 'down',
|
||||||
|
3: 'testing'
|
||||||
|
}
|
||||||
|
if int_adminstatus in adminstatus_options.keys():
|
||||||
|
return adminstatus_options[int_adminstatus]
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def lookup_operstatus(int_operstatus):
|
||||||
|
operstatus_options = {
|
||||||
|
1: 'up',
|
||||||
|
2: 'down',
|
||||||
|
3: 'testing',
|
||||||
|
4: 'unknown',
|
||||||
|
5: 'dormant',
|
||||||
|
6: 'notPresent',
|
||||||
|
7: 'lowerLayerDown'
|
||||||
|
}
|
||||||
|
if int_operstatus in operstatus_options.keys():
|
||||||
|
return operstatus_options[int_operstatus]
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
host=dict(required=True),
|
||||||
|
version=dict(required=True, choices=['v2', 'v2c', 'v3']),
|
||||||
|
community=dict(required=False, default=False),
|
||||||
|
username=dict(required=False),
|
||||||
|
level=dict(required=False, choices=['authNoPriv', 'authPriv']),
|
||||||
|
integrity=dict(required=False, choices=['md5', 'sha']),
|
||||||
|
privacy=dict(required=False, choices=['des', 'aes']),
|
||||||
|
authkey=dict(required=False),
|
||||||
|
privkey=dict(required=False),
|
||||||
|
removeplaceholder=dict(required=False)),
|
||||||
|
required_together = ( ['username','level','integrity','authkey'],['privacy','privkey'],),
|
||||||
|
supports_check_mode=False)
|
||||||
|
|
||||||
|
m_args = module.params
|
||||||
|
|
||||||
|
if not has_pysnmp:
|
||||||
|
module.fail_json(msg='Missing required pysnmp module (check docs)')
|
||||||
|
|
||||||
|
cmdGen = cmdgen.CommandGenerator()
|
||||||
|
|
||||||
|
# Verify that we receive a community when using snmp v2
|
||||||
|
if m_args['version'] == "v2" or m_args['version'] == "v2c":
|
||||||
|
if m_args['community'] == False:
|
||||||
|
module.fail_json(msg='Community not set when using snmp version 2')
|
||||||
|
|
||||||
|
if m_args['version'] == "v3":
|
||||||
|
if m_args['username'] == None:
|
||||||
|
module.fail_json(msg='Username not set when using snmp version 3')
|
||||||
|
|
||||||
|
if m_args['level'] == "authPriv" and m_args['privacy'] == None:
|
||||||
|
module.fail_json(msg='Privacy algorithm not set when using authPriv')
|
||||||
|
|
||||||
|
|
||||||
|
if m_args['integrity'] == "sha":
|
||||||
|
integrity_proto = cmdgen.usmHMACSHAAuthProtocol
|
||||||
|
elif m_args['integrity'] == "md5":
|
||||||
|
integrity_proto = cmdgen.usmHMACMD5AuthProtocol
|
||||||
|
|
||||||
|
if m_args['privacy'] == "aes":
|
||||||
|
privacy_proto = cmdgen.usmAesCfb128Protocol
|
||||||
|
elif m_args['privacy'] == "des":
|
||||||
|
privacy_proto = cmdgen.usmDESPrivProtocol
|
||||||
|
|
||||||
|
# Use SNMP Version 2
|
||||||
|
if m_args['version'] == "v2" or m_args['version'] == "v2c":
|
||||||
|
snmp_auth = cmdgen.CommunityData(m_args['community'])
|
||||||
|
|
||||||
|
# Use SNMP Version 3 with authNoPriv
|
||||||
|
elif m_args['level'] == "authNoPriv":
|
||||||
|
snmp_auth = cmdgen.UsmUserData(m_args['username'], authKey=m_args['authkey'], authProtocol=integrity_proto)
|
||||||
|
|
||||||
|
# Use SNMP Version 3 with authPriv
|
||||||
|
else:
|
||||||
|
snmp_auth = cmdgen.UsmUserData(m_args['username'], authKey=m_args['authkey'], privKey=m_args['privkey'], authProtocol=integrity_proto, privProtocol=privacy_proto)
|
||||||
|
|
||||||
|
# Use p to prefix OIDs with a dot for polling
|
||||||
|
p = DefineOid(dotprefix=True)
|
||||||
|
# Use v without a prefix to use with return values
|
||||||
|
v = DefineOid(dotprefix=False)
|
||||||
|
|
||||||
|
Tree = lambda: defaultdict(Tree)
|
||||||
|
|
||||||
|
results = Tree()
|
||||||
|
|
||||||
|
errorIndication, errorStatus, errorIndex, varBinds = cmdGen.getCmd(
|
||||||
|
snmp_auth,
|
||||||
|
cmdgen.UdpTransportTarget((m_args['host'], 161)),
|
||||||
|
cmdgen.MibVariable(p.sysDescr,),
|
||||||
|
cmdgen.MibVariable(p.sysObjectId,),
|
||||||
|
cmdgen.MibVariable(p.sysUpTime,),
|
||||||
|
cmdgen.MibVariable(p.sysContact,),
|
||||||
|
cmdgen.MibVariable(p.sysName,),
|
||||||
|
cmdgen.MibVariable(p.sysLocation,),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if errorIndication:
|
||||||
|
module.fail_json(msg=str(errorIndication))
|
||||||
|
|
||||||
|
for oid, val in varBinds:
|
||||||
|
current_oid = oid.prettyPrint()
|
||||||
|
current_val = val.prettyPrint()
|
||||||
|
if current_oid == v.sysDescr:
|
||||||
|
results['ansible_sysdescr'] = decode_hex(current_val)
|
||||||
|
elif current_oid == v.sysObjectId:
|
||||||
|
results['ansible_sysobjectid'] = current_val
|
||||||
|
elif current_oid == v.sysUpTime:
|
||||||
|
results['ansible_sysuptime'] = current_val
|
||||||
|
elif current_oid == v.sysContact:
|
||||||
|
results['ansible_syscontact'] = current_val
|
||||||
|
elif current_oid == v.sysName:
|
||||||
|
results['ansible_sysname'] = current_val
|
||||||
|
elif current_oid == v.sysLocation:
|
||||||
|
results['ansible_syslocation'] = current_val
|
||||||
|
|
||||||
|
errorIndication, errorStatus, errorIndex, varTable = cmdGen.nextCmd(
|
||||||
|
snmp_auth,
|
||||||
|
cmdgen.UdpTransportTarget((m_args['host'], 161)),
|
||||||
|
cmdgen.MibVariable(p.ifIndex,),
|
||||||
|
cmdgen.MibVariable(p.ifDescr,),
|
||||||
|
cmdgen.MibVariable(p.ifMtu,),
|
||||||
|
cmdgen.MibVariable(p.ifSpeed,),
|
||||||
|
cmdgen.MibVariable(p.ifPhysAddress,),
|
||||||
|
cmdgen.MibVariable(p.ifAdminStatus,),
|
||||||
|
cmdgen.MibVariable(p.ifOperStatus,),
|
||||||
|
cmdgen.MibVariable(p.ipAdEntAddr,),
|
||||||
|
cmdgen.MibVariable(p.ipAdEntIfIndex,),
|
||||||
|
cmdgen.MibVariable(p.ipAdEntNetMask,),
|
||||||
|
|
||||||
|
cmdgen.MibVariable(p.ifAlias,),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if errorIndication:
|
||||||
|
module.fail_json(msg=str(errorIndication))
|
||||||
|
|
||||||
|
interface_indexes = []
|
||||||
|
|
||||||
|
all_ipv4_addresses = []
|
||||||
|
ipv4_networks = Tree()
|
||||||
|
|
||||||
|
for varBinds in varTable:
|
||||||
|
for oid, val in varBinds:
|
||||||
|
current_oid = oid.prettyPrint()
|
||||||
|
current_val = val.prettyPrint()
|
||||||
|
if v.ifIndex in current_oid:
|
||||||
|
ifIndex = int(current_oid.rsplit('.', 1)[-1])
|
||||||
|
results['ansible_interfaces'][ifIndex]['ifindex'] = current_val
|
||||||
|
interface_indexes.append(ifIndex)
|
||||||
|
if v.ifDescr in current_oid:
|
||||||
|
ifIndex = int(current_oid.rsplit('.', 1)[-1])
|
||||||
|
results['ansible_interfaces'][ifIndex]['name'] = current_val
|
||||||
|
if v.ifMtu in current_oid:
|
||||||
|
ifIndex = int(current_oid.rsplit('.', 1)[-1])
|
||||||
|
results['ansible_interfaces'][ifIndex]['mtu'] = current_val
|
||||||
|
if v.ifMtu in current_oid:
|
||||||
|
ifIndex = int(current_oid.rsplit('.', 1)[-1])
|
||||||
|
results['ansible_interfaces'][ifIndex]['speed'] = current_val
|
||||||
|
if v.ifPhysAddress in current_oid:
|
||||||
|
ifIndex = int(current_oid.rsplit('.', 1)[-1])
|
||||||
|
results['ansible_interfaces'][ifIndex]['mac'] = decode_mac(current_val)
|
||||||
|
if v.ifAdminStatus in current_oid:
|
||||||
|
ifIndex = int(current_oid.rsplit('.', 1)[-1])
|
||||||
|
results['ansible_interfaces'][ifIndex]['adminstatus'] = lookup_adminstatus(int(current_val))
|
||||||
|
if v.ifOperStatus in current_oid:
|
||||||
|
ifIndex = int(current_oid.rsplit('.', 1)[-1])
|
||||||
|
results['ansible_interfaces'][ifIndex]['operstatus'] = lookup_operstatus(int(current_val))
|
||||||
|
if v.ipAdEntAddr in current_oid:
|
||||||
|
curIPList = current_oid.rsplit('.', 4)[-4:]
|
||||||
|
curIP = ".".join(curIPList)
|
||||||
|
ipv4_networks[curIP]['address'] = current_val
|
||||||
|
all_ipv4_addresses.append(current_val)
|
||||||
|
if v.ipAdEntIfIndex in current_oid:
|
||||||
|
curIPList = current_oid.rsplit('.', 4)[-4:]
|
||||||
|
curIP = ".".join(curIPList)
|
||||||
|
ipv4_networks[curIP]['interface'] = current_val
|
||||||
|
if v.ipAdEntNetMask in current_oid:
|
||||||
|
curIPList = current_oid.rsplit('.', 4)[-4:]
|
||||||
|
curIP = ".".join(curIPList)
|
||||||
|
ipv4_networks[curIP]['netmask'] = current_val
|
||||||
|
|
||||||
|
if v.ifAlias in current_oid:
|
||||||
|
ifIndex = int(current_oid.rsplit('.', 1)[-1])
|
||||||
|
results['ansible_interfaces'][ifIndex]['description'] = current_val
|
||||||
|
|
||||||
|
interface_to_ipv4 = {}
|
||||||
|
for ipv4_network in ipv4_networks:
|
||||||
|
current_interface = ipv4_networks[ipv4_network]['interface']
|
||||||
|
current_network = {
|
||||||
|
'address': ipv4_networks[ipv4_network]['address'],
|
||||||
|
'netmask': ipv4_networks[ipv4_network]['netmask']
|
||||||
|
}
|
||||||
|
if not current_interface in interface_to_ipv4:
|
||||||
|
interface_to_ipv4[current_interface] = []
|
||||||
|
interface_to_ipv4[current_interface].append(current_network)
|
||||||
|
else:
|
||||||
|
interface_to_ipv4[current_interface].append(current_network)
|
||||||
|
|
||||||
|
for interface in interface_to_ipv4:
|
||||||
|
results['ansible_interfaces'][int(interface)]['ipv4'] = interface_to_ipv4[interface]
|
||||||
|
|
||||||
|
results['ansible_all_ipv4_addresses'] = all_ipv4_addresses
|
||||||
|
|
||||||
|
module.exit_json(ansible_facts=results)
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
|
|
||||||
@ -0,0 +1,187 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2014, Michael Warkentin <mwarkentin@gmail.com>
|
||||||
|
#
|
||||||
|
# This file is part of Ansible
|
||||||
|
#
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: bower
|
||||||
|
short_description: Manage bower packages with bower
|
||||||
|
description:
|
||||||
|
- Manage bower packages with bower
|
||||||
|
version_added: 1.7
|
||||||
|
author: Michael Warkentin
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- The name of a bower package to install
|
||||||
|
required: false
|
||||||
|
offline:
|
||||||
|
description:
|
||||||
|
- Install packages from local cache, if the packages were installed before
|
||||||
|
required: false
|
||||||
|
default: no
|
||||||
|
choices: [ "yes", "no" ]
|
||||||
|
path:
|
||||||
|
description:
|
||||||
|
- The base path where to install the bower packages
|
||||||
|
required: true
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- The state of the bower package
|
||||||
|
required: false
|
||||||
|
default: present
|
||||||
|
choices: [ "present", "absent", "latest" ]
|
||||||
|
version:
|
||||||
|
description:
|
||||||
|
- The version to be installed
|
||||||
|
required: false
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
description: Install "bootstrap" bower package.
|
||||||
|
- bower: name=bootstrap
|
||||||
|
|
||||||
|
description: Install "bootstrap" bower package on version 3.1.1.
|
||||||
|
- bower: name=bootstrap version=3.1.1
|
||||||
|
|
||||||
|
description: Remove the "bootstrap" bower package.
|
||||||
|
- bower: name=bootstrap state=absent
|
||||||
|
|
||||||
|
description: Install packages based on bower.json.
|
||||||
|
- bower: path=/app/location
|
||||||
|
|
||||||
|
description: Update packages based on bower.json to their latest version.
|
||||||
|
- bower: path=/app/location state=latest
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
class Bower(object):
|
||||||
|
def __init__(self, module, **kwargs):
|
||||||
|
self.module = module
|
||||||
|
self.name = kwargs['name']
|
||||||
|
self.offline = kwargs['offline']
|
||||||
|
self.path = kwargs['path']
|
||||||
|
self.version = kwargs['version']
|
||||||
|
|
||||||
|
if kwargs['version']:
|
||||||
|
self.name_version = self.name + '#' + self.version
|
||||||
|
else:
|
||||||
|
self.name_version = self.name
|
||||||
|
|
||||||
|
def _exec(self, args, run_in_check_mode=False, check_rc=True):
|
||||||
|
if not self.module.check_mode or (self.module.check_mode and run_in_check_mode):
|
||||||
|
cmd = ["bower"] + args
|
||||||
|
|
||||||
|
if self.name:
|
||||||
|
cmd.append(self.name_version)
|
||||||
|
|
||||||
|
if self.offline:
|
||||||
|
cmd.append('--offline')
|
||||||
|
|
||||||
|
# If path is specified, cd into that path and run the command.
|
||||||
|
cwd = None
|
||||||
|
if self.path:
|
||||||
|
if not os.path.exists(self.path):
|
||||||
|
os.makedirs(self.path)
|
||||||
|
if not os.path.isdir(self.path):
|
||||||
|
self.module.fail_json(msg="path %s is not a directory" % self.path)
|
||||||
|
cwd = self.path
|
||||||
|
|
||||||
|
rc, out, err = self.module.run_command(cmd, check_rc=check_rc, cwd=cwd)
|
||||||
|
return out
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def list(self):
|
||||||
|
cmd = ['list', '--json']
|
||||||
|
|
||||||
|
installed = list()
|
||||||
|
missing = list()
|
||||||
|
outdated = list()
|
||||||
|
data = json.loads(self._exec(cmd, True, False))
|
||||||
|
if 'dependencies' in data:
|
||||||
|
for dep in data['dependencies']:
|
||||||
|
if 'missing' in data['dependencies'][dep] and data['dependencies'][dep]['missing']:
|
||||||
|
missing.append(dep)
|
||||||
|
elif data['dependencies'][dep]['pkgMeta']['version'] != data['dependencies'][dep]['update']['latest']:
|
||||||
|
outdated.append(dep)
|
||||||
|
elif 'incompatible' in data['dependencies'][dep] and data['dependencies'][dep]['incompatible']:
|
||||||
|
outdated.append(dep)
|
||||||
|
else:
|
||||||
|
installed.append(dep)
|
||||||
|
# Named dependency not installed
|
||||||
|
else:
|
||||||
|
missing.append(self.name)
|
||||||
|
|
||||||
|
return installed, missing, outdated
|
||||||
|
|
||||||
|
def install(self):
|
||||||
|
return self._exec(['install'])
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
return self._exec(['update'])
|
||||||
|
|
||||||
|
def uninstall(self):
|
||||||
|
return self._exec(['uninstall'])
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
arg_spec = dict(
|
||||||
|
name=dict(default=None),
|
||||||
|
offline=dict(default='no', type='bool'),
|
||||||
|
path=dict(required=True),
|
||||||
|
state=dict(default='present', choices=['present', 'absent', 'latest', ]),
|
||||||
|
version=dict(default=None),
|
||||||
|
)
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=arg_spec
|
||||||
|
)
|
||||||
|
|
||||||
|
name = module.params['name']
|
||||||
|
offline = module.params['offline']
|
||||||
|
path = module.params['path']
|
||||||
|
state = module.params['state']
|
||||||
|
version = module.params['version']
|
||||||
|
|
||||||
|
if state == 'absent' and not name:
|
||||||
|
module.fail_json(msg='uninstalling a package is only available for named packages')
|
||||||
|
|
||||||
|
bower = Bower(module, name=name, offline=offline, path=path, version=version)
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
if state == 'present':
|
||||||
|
installed, missing, outdated = bower.list()
|
||||||
|
if len(missing):
|
||||||
|
changed = True
|
||||||
|
bower.install()
|
||||||
|
elif state == 'latest':
|
||||||
|
installed, missing, outdated = bower.list()
|
||||||
|
if len(missing) or len(outdated):
|
||||||
|
changed = True
|
||||||
|
bower.update()
|
||||||
|
else: # Absent
|
||||||
|
installed, missing, outdated = bower.list()
|
||||||
|
if name in installed:
|
||||||
|
changed = True
|
||||||
|
bower.uninstall()
|
||||||
|
|
||||||
|
module.exit_json(changed=changed)
|
||||||
|
|
||||||
|
# Import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
||||||
@ -0,0 +1,841 @@
|
|||||||
|
#!/usr/bin/python -tt
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Written by Cristian van Ee <cristian at cvee.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/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
import os
|
||||||
|
import dnf
|
||||||
|
|
||||||
|
try:
|
||||||
|
from dnf import find_unfinished_transactions, find_ts_remaining
|
||||||
|
from rpmUtils.miscutils import splitFilename
|
||||||
|
transaction_helpers = True
|
||||||
|
except:
|
||||||
|
transaction_helpers = False
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: dnf
|
||||||
|
version_added: historical
|
||||||
|
short_description: Manages packages with the I(dnf) package manager
|
||||||
|
description:
|
||||||
|
- Installs, upgrade, removes, and lists packages and groups with the I(dnf) package manager.
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- "Package name, or package specifier with version, like C(name-1.0). When using state=latest, this can be '*' which means run: dnf -y update. You can also pass a url or a local path to a rpm file."
|
||||||
|
required: true
|
||||||
|
version_added: "1.8"
|
||||||
|
default: null
|
||||||
|
aliases: []
|
||||||
|
list:
|
||||||
|
description:
|
||||||
|
- Various (non-idempotent) commands for usage with C(/usr/bin/ansible) and I(not) playbooks. See examples.
|
||||||
|
required: false
|
||||||
|
version_added: "1.8"
|
||||||
|
default: null
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Whether to install (C(present), C(latest)), or remove (C(absent)) a package.
|
||||||
|
required: false
|
||||||
|
choices: [ "present", "latest", "absent" ]
|
||||||
|
version_added: "1.8"
|
||||||
|
default: "present"
|
||||||
|
enablerepo:
|
||||||
|
description:
|
||||||
|
- I(Repoid) of repositories to enable for the install/update operation.
|
||||||
|
These repos will not persist beyond the transaction.
|
||||||
|
When specifying multiple repos, separate them with a ",".
|
||||||
|
required: false
|
||||||
|
version_added: "1.8"
|
||||||
|
default: null
|
||||||
|
aliases: []
|
||||||
|
|
||||||
|
disablerepo:
|
||||||
|
description:
|
||||||
|
- I(Repoid) of repositories to disable for the install/update operation.
|
||||||
|
These repos will not persist beyond the transaction.
|
||||||
|
When specifying multiple repos, separate them with a ",".
|
||||||
|
required: false
|
||||||
|
version_added: "1.8"
|
||||||
|
default: null
|
||||||
|
aliases: []
|
||||||
|
|
||||||
|
conf_file:
|
||||||
|
description:
|
||||||
|
- The remote dnf configuration file to use for the transaction.
|
||||||
|
required: false
|
||||||
|
version_added: "1.8"
|
||||||
|
default: null
|
||||||
|
aliases: []
|
||||||
|
|
||||||
|
disable_gpg_check:
|
||||||
|
description:
|
||||||
|
- Whether to disable the GPG checking of signatures of packages being
|
||||||
|
installed. Has an effect only if state is I(present) or I(latest).
|
||||||
|
required: false
|
||||||
|
version_added: "1.8"
|
||||||
|
default: "no"
|
||||||
|
choices: ["yes", "no"]
|
||||||
|
aliases: []
|
||||||
|
|
||||||
|
notes: []
|
||||||
|
# informational: requirements for nodes
|
||||||
|
requirements: [ dnf ]
|
||||||
|
author: Cristian van Ee
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: install the latest version of Apache
|
||||||
|
dnf: name=httpd state=latest
|
||||||
|
|
||||||
|
- name: remove the Apache package
|
||||||
|
dnf: name=httpd state=absent
|
||||||
|
|
||||||
|
- name: install the latest version of Apache from the testing repo
|
||||||
|
dnf: name=httpd enablerepo=testing state=present
|
||||||
|
|
||||||
|
- name: upgrade all packages
|
||||||
|
dnf: name=* state=latest
|
||||||
|
|
||||||
|
- name: install the nginx rpm from a remote repo
|
||||||
|
dnf: name=http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm state=present
|
||||||
|
|
||||||
|
- name: install nginx rpm from a local file
|
||||||
|
dnf: name=/usr/local/src/nginx-release-centos-6-0.el6.ngx.noarch.rpm state=present
|
||||||
|
|
||||||
|
- name: install the 'Development tools' package group
|
||||||
|
dnf: name="@Development tools" state=present
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
def_qf = "%{name}-%{version}-%{release}.%{arch}"
|
||||||
|
|
||||||
|
repoquery='/usr/bin/repoquery'
|
||||||
|
if not os.path.exists(repoquery):
|
||||||
|
repoquery = None
|
||||||
|
|
||||||
|
dnfbin='/usr/bin/dnf'
|
||||||
|
|
||||||
|
import syslog
|
||||||
|
|
||||||
|
def log(msg):
|
||||||
|
syslog.openlog('ansible-dnf', 0, syslog.LOG_USER)
|
||||||
|
syslog.syslog(syslog.LOG_NOTICE, msg)
|
||||||
|
|
||||||
|
def dnf_base(conf_file=None, cachedir=False):
|
||||||
|
|
||||||
|
my = dnf.Base()
|
||||||
|
my.logging.verbose_level=0
|
||||||
|
my.logging.verbose_level=0
|
||||||
|
if conf_file and os.path.exists(conf_file):
|
||||||
|
my.config = conf_file
|
||||||
|
if cachedir or os.geteuid() != 0:
|
||||||
|
if cachedir or os.geteuid() != 0:
|
||||||
|
if hasattr(my, 'setCacheDir'):
|
||||||
|
my.setCacheDir()
|
||||||
|
else:
|
||||||
|
cachedir = cachedir.dnf.Conf()
|
||||||
|
my.repos.setCacheDir(cachedir)
|
||||||
|
my.conf.cache = 0
|
||||||
|
|
||||||
|
return my
|
||||||
|
|
||||||
|
def install_dnf_utils(module):
|
||||||
|
|
||||||
|
if not module.check_mode:
|
||||||
|
dnf_path = module.get_bin_path('dnf')
|
||||||
|
if dnf_path:
|
||||||
|
rc, so, se = module.run_command('%s -y install dnf-plugins-core' % dnf_path)
|
||||||
|
if rc == 0:
|
||||||
|
this_path = module.get_bin_path('repoquery')
|
||||||
|
global repoquery
|
||||||
|
repoquery = this_path
|
||||||
|
|
||||||
|
def po_to_nevra(po):
|
||||||
|
|
||||||
|
if hasattr(po, 'ui_nevra'):
|
||||||
|
return po.ui_nevra
|
||||||
|
else:
|
||||||
|
return '%s-%s-%s.%s' % (po.name, po.version, po.release, po.arch)
|
||||||
|
|
||||||
|
def is_installed(module, repoq, pkgspec, conf_file, qf=def_qf, en_repos=[], dis_repos=[], is_pkg=False):
|
||||||
|
|
||||||
|
if not repoq:
|
||||||
|
|
||||||
|
pkgs = []
|
||||||
|
try:
|
||||||
|
my = dnf_base(conf_file)
|
||||||
|
for rid in en_repos:
|
||||||
|
my.repos.enableRepo(rid)
|
||||||
|
for rid in dis_repos:
|
||||||
|
my.repos.disableRepo(rid)
|
||||||
|
|
||||||
|
e,m,u = my.rpmdb.matchPackageNames([pkgspec])
|
||||||
|
pkgs = e + m
|
||||||
|
if not pkgs:
|
||||||
|
pkgs.extend(my.returnInstalledPackagesByDep(pkgspec))
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="Failure talking to dnf: %s" % e)
|
||||||
|
|
||||||
|
return [ po_to_nevra(p) for p in pkgs ]
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
cmd = repoq + ["--disablerepo=*", "--pkgnarrow=installed", "--qf", qf, pkgspec]
|
||||||
|
rc,out,err = module.run_command(cmd)
|
||||||
|
if not is_pkg:
|
||||||
|
cmd = repoq + ["--disablerepo=*", "--pkgnarrow=installed", "--qf", qf, "--whatprovides", pkgspec]
|
||||||
|
rc2,out2,err2 = module.run_command(cmd)
|
||||||
|
else:
|
||||||
|
rc2,out2,err2 = (0, '', '')
|
||||||
|
|
||||||
|
if rc == 0 and rc2 == 0:
|
||||||
|
out += out2
|
||||||
|
return [ p for p in out.split('\n') if p.strip() ]
|
||||||
|
else:
|
||||||
|
module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err + err2))
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def is_available(module, repoq, pkgspec, conf_file, qf=def_qf, en_repos=[], dis_repos=[]):
|
||||||
|
|
||||||
|
if not repoq:
|
||||||
|
|
||||||
|
pkgs = []
|
||||||
|
try:
|
||||||
|
my = dnf_base(conf_file)
|
||||||
|
for rid in en_repos:
|
||||||
|
my.repos.enableRepo(rid)
|
||||||
|
for rid in dis_repos:
|
||||||
|
my.repos.disableRepo(rid)
|
||||||
|
|
||||||
|
e,m,u = my.pkgSack.matchPackageNames([pkgspec])
|
||||||
|
pkgs = e + m
|
||||||
|
if not pkgs:
|
||||||
|
pkgs.extend(my.returnPackagesByDep(pkgspec))
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="Failure talking to dnf: %s" % e)
|
||||||
|
|
||||||
|
return [ po_to_nevra(p) for p in pkgs ]
|
||||||
|
|
||||||
|
else:
|
||||||
|
myrepoq = list(repoq)
|
||||||
|
|
||||||
|
for repoid in dis_repos:
|
||||||
|
r_cmd = ['--disablerepo', repoid]
|
||||||
|
myrepoq.extend(r_cmd)
|
||||||
|
|
||||||
|
for repoid in en_repos:
|
||||||
|
r_cmd = ['--enablerepo', repoid]
|
||||||
|
myrepoq.extend(r_cmd)
|
||||||
|
|
||||||
|
cmd = myrepoq + ["--qf", qf, pkgspec]
|
||||||
|
rc,out,err = module.run_command(cmd)
|
||||||
|
if rc == 0:
|
||||||
|
return [ p for p in out.split('\n') if p.strip() ]
|
||||||
|
else:
|
||||||
|
module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err))
|
||||||
|
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def is_update(module, repoq, pkgspec, conf_file, qf=def_qf, en_repos=[], dis_repos=[]):
|
||||||
|
|
||||||
|
if not repoq:
|
||||||
|
|
||||||
|
retpkgs = []
|
||||||
|
pkgs = []
|
||||||
|
updates = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
my = dnf_base(conf_file)
|
||||||
|
for rid in en_repos:
|
||||||
|
my.repos.enableRepo(rid)
|
||||||
|
for rid in dis_repos:
|
||||||
|
my.repos.disableRepo(rid)
|
||||||
|
|
||||||
|
pkgs = my.returnPackagesByDep(pkgspec) + my.returnInstalledPackagesByDep(pkgspec)
|
||||||
|
if not pkgs:
|
||||||
|
e,m,u = my.pkgSack.matchPackageNames([pkgspec])
|
||||||
|
pkgs = e + m
|
||||||
|
updates = my.doPackageLists(pkgnarrow='updates').updates
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="Failure talking to dnf: %s" % e)
|
||||||
|
|
||||||
|
for pkg in pkgs:
|
||||||
|
if pkg in updates:
|
||||||
|
retpkgs.append(pkg)
|
||||||
|
|
||||||
|
return set([ po_to_nevra(p) for p in retpkgs ])
|
||||||
|
|
||||||
|
else:
|
||||||
|
myrepoq = list(repoq)
|
||||||
|
for repoid in dis_repos:
|
||||||
|
r_cmd = ['--disablerepo', repoid]
|
||||||
|
myrepoq.extend(r_cmd)
|
||||||
|
|
||||||
|
for repoid in en_repos:
|
||||||
|
r_cmd = ['--enablerepo', repoid]
|
||||||
|
myrepoq.extend(r_cmd)
|
||||||
|
|
||||||
|
cmd = myrepoq + ["--pkgnarrow=updates", "--qf", qf, pkgspec]
|
||||||
|
rc,out,err = module.run_command(cmd)
|
||||||
|
|
||||||
|
if rc == 0:
|
||||||
|
return set([ p for p in out.split('\n') if p.strip() ])
|
||||||
|
else:
|
||||||
|
module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err))
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def what_provides(module, repoq, req_spec, conf_file, qf=def_qf, en_repos=[], dis_repos=[]):
|
||||||
|
|
||||||
|
if not repoq:
|
||||||
|
|
||||||
|
pkgs = []
|
||||||
|
try:
|
||||||
|
my = dnf_base(conf_file)
|
||||||
|
for rid in en_repos:
|
||||||
|
my.repos.enableRepo(rid)
|
||||||
|
for rid in dis_repos:
|
||||||
|
my.repos.disableRepo(rid)
|
||||||
|
|
||||||
|
pkgs = my.returnPackagesByDep(req_spec) + my.returnInstalledPackagesByDep(req_spec)
|
||||||
|
if not pkgs:
|
||||||
|
e,m,u = my.pkgSack.matchPackageNames([req_spec])
|
||||||
|
pkgs.extend(e)
|
||||||
|
pkgs.extend(m)
|
||||||
|
e,m,u = my.rpmdb.matchPackageNames([req_spec])
|
||||||
|
pkgs.extend(e)
|
||||||
|
pkgs.extend(m)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="Failure talking to dnf: %s" % e)
|
||||||
|
|
||||||
|
return set([ po_to_nevra(p) for p in pkgs ])
|
||||||
|
|
||||||
|
else:
|
||||||
|
myrepoq = list(repoq)
|
||||||
|
for repoid in dis_repos:
|
||||||
|
r_cmd = ['--disablerepo', repoid]
|
||||||
|
myrepoq.extend(r_cmd)
|
||||||
|
|
||||||
|
for repoid in en_repos:
|
||||||
|
r_cmd = ['--enablerepo', repoid]
|
||||||
|
myrepoq.extend(r_cmd)
|
||||||
|
|
||||||
|
cmd = myrepoq + ["--qf", qf, "--whatprovides", req_spec]
|
||||||
|
rc,out,err = module.run_command(cmd)
|
||||||
|
cmd = myrepoq + ["--qf", qf, req_spec]
|
||||||
|
rc2,out2,err2 = module.run_command(cmd)
|
||||||
|
if rc == 0 and rc2 == 0:
|
||||||
|
out += out2
|
||||||
|
pkgs = set([ p for p in out.split('\n') if p.strip() ])
|
||||||
|
if not pkgs:
|
||||||
|
pkgs = is_installed(module, repoq, req_spec, conf_file, qf=qf)
|
||||||
|
return pkgs
|
||||||
|
else:
|
||||||
|
module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err + err2))
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def transaction_exists(pkglist):
|
||||||
|
"""
|
||||||
|
checks the package list to see if any packages are
|
||||||
|
involved in an incomplete transaction
|
||||||
|
"""
|
||||||
|
|
||||||
|
conflicts = []
|
||||||
|
if not transaction_helpers:
|
||||||
|
return conflicts
|
||||||
|
|
||||||
|
# first, we create a list of the package 'nvreas'
|
||||||
|
# so we can compare the pieces later more easily
|
||||||
|
pkglist_nvreas = []
|
||||||
|
for pkg in pkglist:
|
||||||
|
pkglist_nvreas.append(splitFilename(pkg))
|
||||||
|
|
||||||
|
# next, we build the list of packages that are
|
||||||
|
# contained within an unfinished transaction
|
||||||
|
unfinished_transactions = find_unfinished_transactions()
|
||||||
|
for trans in unfinished_transactions:
|
||||||
|
steps = find_ts_remaining(trans)
|
||||||
|
for step in steps:
|
||||||
|
# the action is install/erase/etc., but we only
|
||||||
|
# care about the package spec contained in the step
|
||||||
|
(action, step_spec) = step
|
||||||
|
(n,v,r,e,a) = splitFilename(step_spec)
|
||||||
|
# and see if that spec is in the list of packages
|
||||||
|
# requested for installation/updating
|
||||||
|
for pkg in pkglist_nvreas:
|
||||||
|
# if the name and arch match, we're going to assume
|
||||||
|
# this package is part of a pending transaction
|
||||||
|
# the label is just for display purposes
|
||||||
|
label = "%s-%s" % (n,a)
|
||||||
|
if n == pkg[0] and a == pkg[4]:
|
||||||
|
if label not in conflicts:
|
||||||
|
conflicts.append("%s-%s" % (n,a))
|
||||||
|
break
|
||||||
|
return conflicts
|
||||||
|
|
||||||
|
def local_nvra(module, path):
|
||||||
|
"""return nvra of a local rpm passed in"""
|
||||||
|
|
||||||
|
cmd = ['/bin/rpm', '-qp' ,'--qf',
|
||||||
|
'%{name}-%{version}-%{release}.%{arch}\n', path ]
|
||||||
|
rc, out, err = module.run_command(cmd)
|
||||||
|
if rc != 0:
|
||||||
|
return None
|
||||||
|
nvra = out.split('\n')[0]
|
||||||
|
return nvra
|
||||||
|
|
||||||
|
def pkg_to_dict(pkgstr):
|
||||||
|
|
||||||
|
if pkgstr.strip():
|
||||||
|
n,e,v,r,a,repo = pkgstr.split('|')
|
||||||
|
else:
|
||||||
|
return {'error_parsing': pkgstr}
|
||||||
|
|
||||||
|
d = {
|
||||||
|
'name':n,
|
||||||
|
'arch':a,
|
||||||
|
'epoch':e,
|
||||||
|
'release':r,
|
||||||
|
'version':v,
|
||||||
|
'repo':repo,
|
||||||
|
'nevra': '%s:%s-%s-%s.%s' % (e,n,v,r,a)
|
||||||
|
}
|
||||||
|
|
||||||
|
if repo == 'installed':
|
||||||
|
d['dnfstate'] = 'installed'
|
||||||
|
else:
|
||||||
|
d['dnfstate'] = 'available'
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
def repolist(module, repoq, qf="%{repoid}"):
|
||||||
|
|
||||||
|
cmd = repoq + ["--qf", qf, "-a"]
|
||||||
|
rc,out,err = module.run_command(cmd)
|
||||||
|
ret = []
|
||||||
|
if rc == 0:
|
||||||
|
ret = set([ p for p in out.split('\n') if p.strip() ])
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def list_stuff(module, conf_file, stuff):
|
||||||
|
|
||||||
|
qf = "%{name}|%{epoch}|%{version}|%{release}|%{arch}|%{repoid}"
|
||||||
|
repoq = [repoquery, '--show-duplicates', '--plugins', '--quiet', '-q']
|
||||||
|
if conf_file and os.path.exists(conf_file):
|
||||||
|
repoq += ['-c', conf_file]
|
||||||
|
|
||||||
|
if stuff == 'installed':
|
||||||
|
return [ pkg_to_dict(p) for p in is_installed(module, repoq, '-a', conf_file, qf=qf) if p.strip() ]
|
||||||
|
elif stuff == 'updates':
|
||||||
|
return [ pkg_to_dict(p) for p in is_update(module, repoq, '-a', conf_file, qf=qf) if p.strip() ]
|
||||||
|
elif stuff == 'available':
|
||||||
|
return [ pkg_to_dict(p) for p in is_available(module, repoq, '-a', conf_file, qf=qf) if p.strip() ]
|
||||||
|
elif stuff == 'repos':
|
||||||
|
return [ dict(repoid=name, state='enabled') for name in repolist(module, repoq) if name.strip() ]
|
||||||
|
else:
|
||||||
|
return [ pkg_to_dict(p) for p in is_installed(module, repoq, stuff, conf_file, qf=qf) + is_available(module, repoq, stuff, conf_file, qf=qf) if p.strip() ]
|
||||||
|
|
||||||
|
def install(module, items, repoq, dnf_basecmd, conf_file, en_repos, dis_repos):
|
||||||
|
|
||||||
|
res = {}
|
||||||
|
res['results'] = []
|
||||||
|
res['msg'] = ''
|
||||||
|
res['rc'] = 0
|
||||||
|
res['changed'] = False
|
||||||
|
|
||||||
|
for spec in items:
|
||||||
|
pkg = None
|
||||||
|
|
||||||
|
# check if pkgspec is installed (if possible for idempotence)
|
||||||
|
# localpkg
|
||||||
|
if spec.endswith('.rpm') and '://' not in spec:
|
||||||
|
# get the pkg name-v-r.arch
|
||||||
|
if not os.path.exists(spec):
|
||||||
|
res['msg'] += "No Package file matching '%s' found on system" % spec
|
||||||
|
module.fail_json(**res)
|
||||||
|
|
||||||
|
nvra = local_nvra(module, spec)
|
||||||
|
# look for them in the rpmdb
|
||||||
|
if is_installed(module, repoq, nvra, conf_file, en_repos=en_repos, dis_repos=dis_repos):
|
||||||
|
# if they are there, skip it
|
||||||
|
continue
|
||||||
|
pkg = spec
|
||||||
|
|
||||||
|
# URL
|
||||||
|
elif '://' in spec:
|
||||||
|
pkg = spec
|
||||||
|
|
||||||
|
#groups :(
|
||||||
|
elif spec.startswith('@'):
|
||||||
|
# complete wild ass guess b/c it's a group
|
||||||
|
pkg = spec
|
||||||
|
|
||||||
|
# range requires or file-requires or pkgname :(
|
||||||
|
else:
|
||||||
|
# most common case is the pkg is already installed and done
|
||||||
|
# short circuit all the bs - and search for it as a pkg in is_installed
|
||||||
|
# if you find it then we're done
|
||||||
|
if not set(['*','?']).intersection(set(spec)):
|
||||||
|
pkgs = is_installed(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos, is_pkg=True)
|
||||||
|
if pkgs:
|
||||||
|
res['results'].append('%s providing %s is already installed' % (pkgs[0], spec))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# look up what pkgs provide this
|
||||||
|
pkglist = what_provides(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos)
|
||||||
|
if not pkglist:
|
||||||
|
res['msg'] += "No Package matching '%s' found available, installed or updated" % spec
|
||||||
|
module.fail_json(**res)
|
||||||
|
|
||||||
|
# if any of the packages are involved in a transaction, fail now
|
||||||
|
# so that we don't hang on the dnf operation later
|
||||||
|
conflicts = transaction_exists(pkglist)
|
||||||
|
if len(conflicts) > 0:
|
||||||
|
res['msg'] += "The following packages have pending transactions: %s" % ", ".join(conflicts)
|
||||||
|
module.fail_json(**res)
|
||||||
|
|
||||||
|
# if any of them are installed
|
||||||
|
# then nothing to do
|
||||||
|
|
||||||
|
found = False
|
||||||
|
for this in pkglist:
|
||||||
|
if is_installed(module, repoq, this, conf_file, en_repos=en_repos, dis_repos=dis_repos, is_pkg=True):
|
||||||
|
found = True
|
||||||
|
res['results'].append('%s providing %s is already installed' % (this, spec))
|
||||||
|
break
|
||||||
|
|
||||||
|
# if the version of the pkg you have installed is not in ANY repo, but there are
|
||||||
|
# other versions in the repos (both higher and lower) then the previous checks won't work.
|
||||||
|
# so we check one more time. This really only works for pkgname - not for file provides or virt provides
|
||||||
|
# but virt provides should be all caught in what_provides on its own.
|
||||||
|
# highly irritating
|
||||||
|
if not found:
|
||||||
|
if is_installed(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos):
|
||||||
|
found = True
|
||||||
|
res['results'].append('package providing %s is already installed' % (spec))
|
||||||
|
|
||||||
|
if found:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# if not - then pass in the spec as what to install
|
||||||
|
# we could get here if nothing provides it but that's not
|
||||||
|
# the error we're catching here
|
||||||
|
pkg = spec
|
||||||
|
|
||||||
|
cmd = dnf_basecmd + ['install', pkg]
|
||||||
|
|
||||||
|
if module.check_mode:
|
||||||
|
module.exit_json(changed=True)
|
||||||
|
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
rc, out, err = module.run_command(cmd)
|
||||||
|
|
||||||
|
# Fail on invalid urls:
|
||||||
|
if (rc == 1 and '://' in spec and ('No package %s available.' % spec in out or 'Cannot open: %s. Skipping.' % spec in err)):
|
||||||
|
err = 'Package at %s could not be installed' % spec
|
||||||
|
module.fail_json(changed=False,msg=err,rc=1)
|
||||||
|
elif (rc != 0 and 'Nothing to do' in err) or 'Nothing to do' in out:
|
||||||
|
# avoid failing in the 'Nothing To Do' case
|
||||||
|
# this may happen with an URL spec.
|
||||||
|
# for an already installed group,
|
||||||
|
# we get rc = 0 and 'Nothing to do' in out, not in err.
|
||||||
|
rc = 0
|
||||||
|
err = ''
|
||||||
|
out = '%s: Nothing to do' % spec
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
res['rc'] += rc
|
||||||
|
res['results'].append(out)
|
||||||
|
res['msg'] += err
|
||||||
|
|
||||||
|
# FIXME - if we did an install - go and check the rpmdb to see if it actually installed
|
||||||
|
# look for the pkg in rpmdb
|
||||||
|
# look for the pkg via obsoletes
|
||||||
|
|
||||||
|
# accumulate any changes
|
||||||
|
res['changed'] |= changed
|
||||||
|
|
||||||
|
module.exit_json(**res)
|
||||||
|
|
||||||
|
|
||||||
|
def remove(module, items, repoq, dnf_basecmd, conf_file, en_repos, dis_repos):
|
||||||
|
|
||||||
|
res = {}
|
||||||
|
res['results'] = []
|
||||||
|
res['msg'] = ''
|
||||||
|
res['changed'] = False
|
||||||
|
res['rc'] = 0
|
||||||
|
|
||||||
|
for pkg in items:
|
||||||
|
is_group = False
|
||||||
|
# group remove - this is doom on a stick
|
||||||
|
if pkg.startswith('@'):
|
||||||
|
is_group = True
|
||||||
|
else:
|
||||||
|
if not is_installed(module, repoq, pkg, conf_file, en_repos=en_repos, dis_repos=dis_repos):
|
||||||
|
res['results'].append('%s is not installed' % pkg)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# run an actual dnf transaction
|
||||||
|
cmd = dnf_basecmd + ["remove", pkg]
|
||||||
|
|
||||||
|
if module.check_mode:
|
||||||
|
module.exit_json(changed=True)
|
||||||
|
|
||||||
|
rc, out, err = module.run_command(cmd)
|
||||||
|
|
||||||
|
res['rc'] += rc
|
||||||
|
res['results'].append(out)
|
||||||
|
res['msg'] += err
|
||||||
|
|
||||||
|
# compile the results into one batch. If anything is changed
|
||||||
|
# then mark changed
|
||||||
|
# at the end - if we've end up failed then fail out of the rest
|
||||||
|
# of the process
|
||||||
|
|
||||||
|
# at this point we should check to see if the pkg is no longer present
|
||||||
|
|
||||||
|
if not is_group: # we can't sensibly check for a group being uninstalled reliably
|
||||||
|
# look to see if the pkg shows up from is_installed. If it doesn't
|
||||||
|
if not is_installed(module, repoq, pkg, conf_file, en_repos=en_repos, dis_repos=dis_repos):
|
||||||
|
res['changed'] = True
|
||||||
|
else:
|
||||||
|
module.fail_json(**res)
|
||||||
|
|
||||||
|
if rc != 0:
|
||||||
|
module.fail_json(**res)
|
||||||
|
|
||||||
|
module.exit_json(**res)
|
||||||
|
|
||||||
|
def latest(module, items, repoq, dnf_basecmd, conf_file, en_repos, dis_repos):
|
||||||
|
|
||||||
|
res = {}
|
||||||
|
res['results'] = []
|
||||||
|
res['msg'] = ''
|
||||||
|
res['changed'] = False
|
||||||
|
res['rc'] = 0
|
||||||
|
|
||||||
|
for spec in items:
|
||||||
|
|
||||||
|
pkg = None
|
||||||
|
basecmd = 'update'
|
||||||
|
cmd = ''
|
||||||
|
# groups, again
|
||||||
|
if spec.startswith('@'):
|
||||||
|
pkg = spec
|
||||||
|
|
||||||
|
elif spec == '*': #update all
|
||||||
|
# use check-update to see if there is any need
|
||||||
|
rc,out,err = module.run_command(dnf_basecmd + ['check-update'])
|
||||||
|
if rc == 100:
|
||||||
|
cmd = dnf_basecmd + [basecmd]
|
||||||
|
else:
|
||||||
|
res['results'].append('All packages up to date')
|
||||||
|
continue
|
||||||
|
|
||||||
|
# dep/pkgname - find it
|
||||||
|
else:
|
||||||
|
if is_installed(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos):
|
||||||
|
basecmd = 'update'
|
||||||
|
else:
|
||||||
|
basecmd = 'install'
|
||||||
|
|
||||||
|
pkglist = what_provides(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos)
|
||||||
|
if not pkglist:
|
||||||
|
res['msg'] += "No Package matching '%s' found available, installed or updated" % spec
|
||||||
|
module.fail_json(**res)
|
||||||
|
|
||||||
|
nothing_to_do = True
|
||||||
|
for this in pkglist:
|
||||||
|
if basecmd == 'install' and is_available(module, repoq, this, conf_file, en_repos=en_repos, dis_repos=dis_repos):
|
||||||
|
nothing_to_do = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if basecmd == 'update' and is_update(module, repoq, this, conf_file, en_repos=en_repos, dis_repos=en_repos):
|
||||||
|
nothing_to_do = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if nothing_to_do:
|
||||||
|
res['results'].append("All packages providing %s are up to date" % spec)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# if any of the packages are involved in a transaction, fail now
|
||||||
|
# so that we don't hang on the dnf operation later
|
||||||
|
conflicts = transaction_exists(pkglist)
|
||||||
|
if len(conflicts) > 0:
|
||||||
|
res['msg'] += "The following packages have pending transactions: %s" % ", ".join(conflicts)
|
||||||
|
module.fail_json(**res)
|
||||||
|
|
||||||
|
pkg = spec
|
||||||
|
if not cmd:
|
||||||
|
cmd = dnf_basecmd + [basecmd, pkg]
|
||||||
|
|
||||||
|
if module.check_mode:
|
||||||
|
return module.exit_json(changed=True)
|
||||||
|
|
||||||
|
rc, out, err = module.run_command(cmd)
|
||||||
|
|
||||||
|
res['rc'] += rc
|
||||||
|
res['results'].append(out)
|
||||||
|
res['msg'] += err
|
||||||
|
|
||||||
|
# FIXME if it is - update it and check to see if it applied
|
||||||
|
# check to see if there is no longer an update available for the pkgspec
|
||||||
|
|
||||||
|
if rc:
|
||||||
|
res['failed'] = True
|
||||||
|
else:
|
||||||
|
res['changed'] = True
|
||||||
|
|
||||||
|
module.exit_json(**res)
|
||||||
|
|
||||||
|
def ensure(module, state, pkgspec, conf_file, enablerepo, disablerepo,
|
||||||
|
disable_gpg_check):
|
||||||
|
|
||||||
|
# take multiple args comma separated
|
||||||
|
items = pkgspec.split(',')
|
||||||
|
|
||||||
|
# need debug level 2 to get 'Nothing to do' for groupinstall.
|
||||||
|
dnf_basecmd = [dnfbin, '-d', '2', '-y']
|
||||||
|
|
||||||
|
|
||||||
|
if not repoquery:
|
||||||
|
repoq = None
|
||||||
|
else:
|
||||||
|
repoq = [repoquery, '--show-duplicates', '--plugins', '--quiet', '-q']
|
||||||
|
|
||||||
|
if conf_file and os.path.exists(conf_file):
|
||||||
|
dnf_basecmd += ['-c', conf_file]
|
||||||
|
if repoq:
|
||||||
|
repoq += ['-c', conf_file]
|
||||||
|
|
||||||
|
dis_repos =[]
|
||||||
|
en_repos = []
|
||||||
|
if disablerepo:
|
||||||
|
dis_repos = disablerepo.split(',')
|
||||||
|
if enablerepo:
|
||||||
|
en_repos = enablerepo.split(',')
|
||||||
|
|
||||||
|
for repoid in dis_repos:
|
||||||
|
r_cmd = ['--disablerepo=%s' % repoid]
|
||||||
|
dnf_basecmd.extend(r_cmd)
|
||||||
|
|
||||||
|
for repoid in en_repos:
|
||||||
|
r_cmd = ['--enablerepo=%s' % repoid]
|
||||||
|
dnf_basecmd.extend(r_cmd)
|
||||||
|
|
||||||
|
if state in ['installed', 'present', 'latest']:
|
||||||
|
my = dnf_base(conf_file)
|
||||||
|
try:
|
||||||
|
for r in dis_repos:
|
||||||
|
my.repos.disableRepo(r)
|
||||||
|
|
||||||
|
current_repos = dnf.yum.config.RepoConf()
|
||||||
|
for r in en_repos:
|
||||||
|
try:
|
||||||
|
my.repos.enableRepo(r)
|
||||||
|
new_repos = my.repos.repos.keys()
|
||||||
|
for i in new_repos:
|
||||||
|
if not i in current_repos:
|
||||||
|
rid = my.repos.getRepo(i)
|
||||||
|
a = rid.repoXML.repoid
|
||||||
|
current_repos = new_repos
|
||||||
|
except dnf.exceptions.Error, e:
|
||||||
|
module.fail_json(msg="Error setting/accessing repo %s: %s" % (r, e))
|
||||||
|
except dnf.exceptions.Error, e:
|
||||||
|
module.fail_json(msg="Error accessing repos: %s" % e)
|
||||||
|
|
||||||
|
if state in ['installed', 'present']:
|
||||||
|
if disable_gpg_check:
|
||||||
|
dnf_basecmd.append('--nogpgcheck')
|
||||||
|
install(module, items, repoq, dnf_basecmd, conf_file, en_repos, dis_repos)
|
||||||
|
elif state in ['removed', 'absent']:
|
||||||
|
remove(module, items, repoq, dnf_basecmd, conf_file, en_repos, dis_repos)
|
||||||
|
elif state == 'latest':
|
||||||
|
if disable_gpg_check:
|
||||||
|
dnf_basecmd.append('--nogpgcheck')
|
||||||
|
latest(module, items, repoq, dnf_basecmd, conf_file, en_repos, dis_repos)
|
||||||
|
|
||||||
|
# should be caught by AnsibleModule argument_spec
|
||||||
|
return dict(changed=False, failed=True, results='', errors='unexpected state')
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
# state=installed name=pkgspec
|
||||||
|
# state=removed name=pkgspec
|
||||||
|
# state=latest name=pkgspec
|
||||||
|
#
|
||||||
|
# informational commands:
|
||||||
|
# list=installed
|
||||||
|
# list=updates
|
||||||
|
# list=available
|
||||||
|
# list=repos
|
||||||
|
# list=pkgspec
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
name=dict(aliases=['pkg']),
|
||||||
|
# removed==absent, installed==present, these are accepted as aliases
|
||||||
|
state=dict(default='installed', choices=['absent','present','installed','removed','latest']),
|
||||||
|
enablerepo=dict(),
|
||||||
|
disablerepo=dict(),
|
||||||
|
list=dict(),
|
||||||
|
conf_file=dict(default=None),
|
||||||
|
disable_gpg_check=dict(required=False, default="no", type='bool'),
|
||||||
|
# this should not be needed, but exists as a failsafe
|
||||||
|
install_repoquery=dict(required=False, default="yes", type='bool'),
|
||||||
|
),
|
||||||
|
required_one_of = [['name','list']],
|
||||||
|
mutually_exclusive = [['name','list']],
|
||||||
|
supports_check_mode = True
|
||||||
|
)
|
||||||
|
|
||||||
|
# this should not be needed, but exists as a failsafe
|
||||||
|
params = module.params
|
||||||
|
if params['install_repoquery'] and not repoquery and not module.check_mode:
|
||||||
|
install_dnf_utils(module)
|
||||||
|
|
||||||
|
if params['list']:
|
||||||
|
if not repoquery:
|
||||||
|
module.fail_json(msg="repoquery is required to use list= with this module. Please install the dnf-utils package.")
|
||||||
|
results = dict(results=list_stuff(module, params['conf_file'], params['list']))
|
||||||
|
module.exit_json(**results)
|
||||||
|
|
||||||
|
else:
|
||||||
|
pkg = params['name']
|
||||||
|
state = params['state']
|
||||||
|
enablerepo = params.get('enablerepo', '')
|
||||||
|
disablerepo = params.get('disablerepo', '')
|
||||||
|
disable_gpg_check = params['disable_gpg_check']
|
||||||
|
res = ensure(module, state, pkg, params['conf_file'], enablerepo,
|
||||||
|
disablerepo, disable_gpg_check)
|
||||||
|
module.fail_json(msg="we should never get here unless this all failed", **res)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
||||||
|
|
||||||
@ -0,0 +1,332 @@
|
|||||||
|
#!/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/>.
|
||||||
|
|
||||||
|
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.8"
|
||||||
|
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 pasword, 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 <yo@groks.org>
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: Set the options explicitly a deivce 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 }}
|
||||||
|
'''
|
||||||
|
|
||||||
|
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),
|
||||||
|
opts = dict(default=None),
|
||||||
|
path = dict(default='/etc/crypttab')
|
||||||
|
),
|
||||||
|
supports_check_mode = True
|
||||||
|
)
|
||||||
|
|
||||||
|
name = module.params['name'].lstrip('/dev/mapper')
|
||||||
|
backing_device = module.params['backing_device']
|
||||||
|
password = module.params['password']
|
||||||
|
opts = module.params['opts']
|
||||||
|
state = module.params['state']
|
||||||
|
path = module.params['path']
|
||||||
|
|
||||||
|
if 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:
|
||||||
|
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:
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(str(crypttab))
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
for line in f.readlines():
|
||||||
|
self._lines.append(Line(line))
|
||||||
|
|
||||||
|
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 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()
|
||||||
|
return (fields[0],
|
||||||
|
fields[1],
|
||||||
|
fields[2] if len(fields) >= 3 else None,
|
||||||
|
fields[3] if len(fields) >= 4 else None)
|
||||||
|
|
||||||
|
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:
|
||||||
|
fields.append(self.password if self.password is not None else '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('=')
|
||||||
|
k, v = (kv[0], kv[1]) if len(kv) > 1 else (kv[0], None)
|
||||||
|
self[k] = v
|
||||||
|
|
||||||
|
def add(self, opts_string):
|
||||||
|
changed = False
|
||||||
|
for k, v in Options(opts_string).items():
|
||||||
|
if self.has_key(k):
|
||||||
|
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 self.has_key(k):
|
||||||
|
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 not self.has_key(key):
|
||||||
|
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):
|
||||||
|
return ','.join([k if v is None else '%s=%s' % (k, v)
|
||||||
|
for k, v in self.items()])
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
||||||
@ -0,0 +1,402 @@
|
|||||||
|
#!/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/>.
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
description:
|
||||||
|
- List of hosts to use for probing and brick setup
|
||||||
|
host:
|
||||||
|
required: false
|
||||||
|
description:
|
||||||
|
- Override local hostname (for peer probing purposes)
|
||||||
|
replicas:
|
||||||
|
required: false
|
||||||
|
description:
|
||||||
|
- Replica count for volume
|
||||||
|
stripes:
|
||||||
|
required: false
|
||||||
|
description:
|
||||||
|
- Stripe count for volume
|
||||||
|
transport:
|
||||||
|
required: false
|
||||||
|
choices: [ 'tcp', 'rdma', 'tcp,rdma' ]
|
||||||
|
description:
|
||||||
|
- Transport type for volume
|
||||||
|
brick:
|
||||||
|
required: false
|
||||||
|
description:
|
||||||
|
- Brick path on servers
|
||||||
|
start_on_create:
|
||||||
|
choices: [ 'yes', 'no']
|
||||||
|
required: false
|
||||||
|
description:
|
||||||
|
- Controls whether the volume is started after creation or not, defaults to yes
|
||||||
|
rebalance:
|
||||||
|
choices: [ 'yes', 'no']
|
||||||
|
required: false
|
||||||
|
description:
|
||||||
|
- Controls whether the cluster is rebalanced after changes
|
||||||
|
directory:
|
||||||
|
required: false
|
||||||
|
description:
|
||||||
|
- Directory for limit-usage
|
||||||
|
options:
|
||||||
|
required: false
|
||||||
|
description:
|
||||||
|
- A dictionary/hash with options/settings for the volume
|
||||||
|
quota:
|
||||||
|
required: false
|
||||||
|
description:
|
||||||
|
- Quota value for limit-usage (be sure to use 10.0MB instead of 10MB, see quota list)
|
||||||
|
notes:
|
||||||
|
- "Requires cli tools for GlusterFS on servers"
|
||||||
|
- "Will add new bricks, but not remove them"
|
||||||
|
author: Taneli Leppä
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = """
|
||||||
|
- name: create gluster volume
|
||||||
|
gluster_volume: state=present name=test1 brick=/bricks/brick1/g1 rebalance=yes hosts:"{{ play_hosts }}"
|
||||||
|
run_once: true
|
||||||
|
|
||||||
|
- name: tune
|
||||||
|
gluster_volume: state=present name=test1 options='{performance.cache-size: 256MB}'
|
||||||
|
|
||||||
|
- name: start gluster volume
|
||||||
|
gluster_volume: status=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
|
||||||
|
"""
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import time
|
||||||
|
import socket
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
|
||||||
|
def run_gluster(gargs, **kwargs):
|
||||||
|
args = [glusterbin]
|
||||||
|
args.extend(gargs)
|
||||||
|
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 if out != '' else err))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def run_gluster_nofail(gargs, **kwargs):
|
||||||
|
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):
|
||||||
|
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 if out != '' else err))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def get_peers():
|
||||||
|
out = run_gluster([ 'peer', 'status'])
|
||||||
|
i = 0
|
||||||
|
peers = {}
|
||||||
|
hostname = None
|
||||||
|
uuid = None
|
||||||
|
state = None
|
||||||
|
for row in out.split('\n'):
|
||||||
|
if ': ' in row:
|
||||||
|
key, value = row.split(': ')
|
||||||
|
if key.lower() == 'hostname':
|
||||||
|
hostname = value
|
||||||
|
if key.lower() == 'uuid':
|
||||||
|
uuid = value
|
||||||
|
if key.lower() == 'state':
|
||||||
|
state = value
|
||||||
|
peers[hostname] = [ uuid, state ]
|
||||||
|
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):
|
||||||
|
run_gluster([ 'peer', 'probe', host ])
|
||||||
|
if not wait_for_peer(host):
|
||||||
|
module.fail_json(msg='failed to probe peer %s' % host)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
def probe_all_peers(hosts, peers, myhostname):
|
||||||
|
for host in hosts:
|
||||||
|
if host not in peers:
|
||||||
|
# dont probe ourselves
|
||||||
|
if myhostname != host:
|
||||||
|
probe(host)
|
||||||
|
|
||||||
|
def create_volume(name, stripe, replica, transport, hosts, brick):
|
||||||
|
args = [ 'volume', 'create' ]
|
||||||
|
args.append(name)
|
||||||
|
if stripe:
|
||||||
|
args.append('stripe')
|
||||||
|
args.append(str(stripe))
|
||||||
|
if replica:
|
||||||
|
args.append('replica')
|
||||||
|
args.append(str(replica))
|
||||||
|
args.append('transport')
|
||||||
|
args.append(transport)
|
||||||
|
for host in hosts:
|
||||||
|
args.append(('%s:%s' % (host, brick)))
|
||||||
|
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_brick(name, brick):
|
||||||
|
run_gluster([ 'volume', 'add-brick', name, brick ])
|
||||||
|
|
||||||
|
def 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 ])
|
||||||
|
|
||||||
|
|
||||||
|
### MAIN ###
|
||||||
|
|
||||||
|
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'),
|
||||||
|
transport=dict(required=False, default='tcp', choices=[ 'tcp', 'rdma', 'tcp,rdma' ]),
|
||||||
|
brick=dict(required=False, default=None),
|
||||||
|
start_on_create=dict(required=False, default=True, type='bool'),
|
||||||
|
rebalance=dict(required=False, default=False, taype='bool'),
|
||||||
|
options=dict(required=False, default=None, type='dict'),
|
||||||
|
quota=dict(required=False),
|
||||||
|
directory=dict(required=False, default=None),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
glusterbin = module.get_bin_path('gluster', True)
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
action = module.params['state']
|
||||||
|
volume_name = module.params['name']
|
||||||
|
cluster= module.params['cluster']
|
||||||
|
brick_path = module.params['brick']
|
||||||
|
stripes = module.params['stripes']
|
||||||
|
replicas = module.params['replicas']
|
||||||
|
transport = module.params['transport']
|
||||||
|
myhostname = module.params['host']
|
||||||
|
start_volume = module.boolean(module.params['start_on_create'])
|
||||||
|
rebalance = module.boolean(module.params['rebalance'])
|
||||||
|
|
||||||
|
if not myhostname:
|
||||||
|
myhostname = socket.gethostname()
|
||||||
|
|
||||||
|
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:
|
||||||
|
run_gluster([ 'volume', 'delete', 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, transport, cluster, brick_path)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if volume_name in volumes:
|
||||||
|
if volumes[volume_name]['status'].lower() != 'started' and start_volume:
|
||||||
|
start_volume(volume_name)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
# switch bricks
|
||||||
|
new_bricks = []
|
||||||
|
removed_bricks = []
|
||||||
|
all_bricks = []
|
||||||
|
for node in cluster:
|
||||||
|
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)
|
||||||
|
|
||||||
|
for brick in new_bricks:
|
||||||
|
add_brick(volume_name, brick)
|
||||||
|
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 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:
|
||||||
|
rebalance(volume_name)
|
||||||
|
|
||||||
|
facts = {}
|
||||||
|
facts['glusterfs'] = { 'peers': peers, 'volumes': volumes, 'quotas': quotas }
|
||||||
|
|
||||||
|
module.exit_json(changed=changed, ansible_facts=facts)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
||||||
@ -0,0 +1,248 @@
|
|||||||
|
#!powershell
|
||||||
|
# This file is part of Ansible
|
||||||
|
#
|
||||||
|
# Copyright 2014, Trond Hindenes <trond@hindenes.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/>.
|
||||||
|
|
||||||
|
# WANT_JSON
|
||||||
|
# POWERSHELL_COMMON
|
||||||
|
|
||||||
|
function Write-Log
|
||||||
|
{
|
||||||
|
param
|
||||||
|
(
|
||||||
|
[parameter(mandatory=$false)]
|
||||||
|
[System.String]
|
||||||
|
$message
|
||||||
|
)
|
||||||
|
|
||||||
|
$date = get-date -format 'yyyy-MM-dd hh:mm:ss.zz'
|
||||||
|
|
||||||
|
Write-Host "$date | $message"
|
||||||
|
|
||||||
|
Out-File -InputObject "$date $message" -FilePath $global:LoggingFile -Append
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = Parse-Args $args;
|
||||||
|
$result = New-Object PSObject;
|
||||||
|
Set-Attr $result "changed" $false;
|
||||||
|
|
||||||
|
If ($params.name)
|
||||||
|
{
|
||||||
|
$package = $params.name
|
||||||
|
}
|
||||||
|
Else
|
||||||
|
{
|
||||||
|
Fail-Json $result "missing required argument: name"
|
||||||
|
}
|
||||||
|
|
||||||
|
if(($params.logPath).length -gt 0)
|
||||||
|
{
|
||||||
|
$global:LoggingFile = $params.logPath
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$global:LoggingFile = "c:\ansible-playbook.log"
|
||||||
|
}
|
||||||
|
If ($params.force)
|
||||||
|
{
|
||||||
|
$force = $params.force | ConvertTo-Bool
|
||||||
|
}
|
||||||
|
Else
|
||||||
|
{
|
||||||
|
$force = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
If ($params.version)
|
||||||
|
{
|
||||||
|
$version = $params.version
|
||||||
|
}
|
||||||
|
Else
|
||||||
|
{
|
||||||
|
$version = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
If ($params.showlog)
|
||||||
|
{
|
||||||
|
$showlog = $params.showlog | ConvertTo-Bool
|
||||||
|
}
|
||||||
|
Else
|
||||||
|
{
|
||||||
|
$showlog = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
If ($params.state)
|
||||||
|
{
|
||||||
|
$state = $params.state.ToString().ToLower()
|
||||||
|
If (($state -ne "present") -and ($state -ne "absent"))
|
||||||
|
{
|
||||||
|
Fail-Json $result "state is $state; must be present or absent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Else
|
||||||
|
{
|
||||||
|
$state = "present"
|
||||||
|
}
|
||||||
|
|
||||||
|
$ChocoAlreadyInstalled = get-command choco -ErrorAction 0
|
||||||
|
if ($ChocoAlreadyInstalled -eq $null)
|
||||||
|
{
|
||||||
|
#We need to install chocolatey
|
||||||
|
$install_choco_result = iex ((new-object net.webclient).DownloadString("https://chocolatey.org/install.ps1"))
|
||||||
|
$result.changed = $true
|
||||||
|
$executable = "C:\ProgramData\chocolatey\bin\choco.exe"
|
||||||
|
}
|
||||||
|
Else
|
||||||
|
{
|
||||||
|
$executable = "choco.exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
If ($params.source)
|
||||||
|
{
|
||||||
|
$source = $params.source.ToString().ToLower()
|
||||||
|
If (($source -ne "chocolatey") -and ($source -ne "webpi") -and ($source -ne "windowsfeatures") -and ($source -ne "ruby"))
|
||||||
|
{
|
||||||
|
Fail-Json $result "source is $source - must be one of chocolatey, ruby, webpi or windowsfeatures."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Elseif (!$params.source)
|
||||||
|
{
|
||||||
|
$source = "chocolatey"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($source -eq "webpi")
|
||||||
|
{
|
||||||
|
# check whether 'webpi' installation source is available; if it isn't, install it
|
||||||
|
$webpi_check_cmd = "$executable list webpicmd -localonly"
|
||||||
|
$webpi_check_result = invoke-expression $webpi_check_cmd
|
||||||
|
Set-Attr $result "chocolatey_bootstrap_webpi_check_cmd" $webpi_check_cmd
|
||||||
|
Set-Attr $result "chocolatey_bootstrap_webpi_check_log" $webpi_check_result
|
||||||
|
if (
|
||||||
|
(
|
||||||
|
($webpi_check_result.GetType().Name -eq "String") -and
|
||||||
|
($webpi_check_result -match "No packages found")
|
||||||
|
) -or
|
||||||
|
($webpi_check_result -contains "No packages found.")
|
||||||
|
)
|
||||||
|
{
|
||||||
|
#lessmsi is a webpicmd dependency, but dependency resolution fails unless it's installed separately
|
||||||
|
$lessmsi_install_cmd = "$executable install lessmsi"
|
||||||
|
$lessmsi_install_result = invoke-expression $lessmsi_install_cmd
|
||||||
|
Set-Attr $result "chocolatey_bootstrap_lessmsi_install_cmd" $lessmsi_install_cmd
|
||||||
|
Set-Attr $result "chocolatey_bootstrap_lessmsi_install_log" $lessmsi_install_result
|
||||||
|
|
||||||
|
$webpi_install_cmd = "$executable install webpicmd"
|
||||||
|
$webpi_install_result = invoke-expression $webpi_install_cmd
|
||||||
|
Set-Attr $result "chocolatey_bootstrap_webpi_install_cmd" $webpi_install_cmd
|
||||||
|
Set-Attr $result "chocolatey_bootstrap_webpi_install_log" $webpi_install_result
|
||||||
|
|
||||||
|
if (($webpi_install_result | select-string "already installed").length -gt 0)
|
||||||
|
{
|
||||||
|
#no change
|
||||||
|
}
|
||||||
|
elseif (($webpi_install_result | select-string "webpicmd has finished successfully").length -gt 0)
|
||||||
|
{
|
||||||
|
$result.changed = $true
|
||||||
|
}
|
||||||
|
Else
|
||||||
|
{
|
||||||
|
Fail-Json $result "WebPI install error: $webpi_install_result"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$expression = $executable
|
||||||
|
if ($state -eq "present")
|
||||||
|
{
|
||||||
|
$expression += " install $package"
|
||||||
|
}
|
||||||
|
Elseif ($state -eq "absent")
|
||||||
|
{
|
||||||
|
$expression += " uninstall $package"
|
||||||
|
}
|
||||||
|
if ($force)
|
||||||
|
{
|
||||||
|
if ($state -eq "present")
|
||||||
|
{
|
||||||
|
$expression += " -force"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($version)
|
||||||
|
{
|
||||||
|
$expression += " -version $version"
|
||||||
|
}
|
||||||
|
if ($source -eq "chocolatey")
|
||||||
|
{
|
||||||
|
$expression += " -source https://chocolatey.org/api/v2/"
|
||||||
|
}
|
||||||
|
elseif (($source -eq "windowsfeatures") -or ($source -eq "webpi") -or ($source -eq "ruby"))
|
||||||
|
{
|
||||||
|
$expression += " -source $source"
|
||||||
|
}
|
||||||
|
|
||||||
|
Set-Attr $result "chocolatey command" $expression
|
||||||
|
$op_result = invoke-expression $expression
|
||||||
|
if ($state -eq "present")
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
(($op_result | select-string "already installed").length -gt 0) -or
|
||||||
|
# webpi has different text output, and that doesn't include the package name but instead the human-friendly name
|
||||||
|
(($op_result | select-string "No products to be installed").length -gt 0)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
#no change
|
||||||
|
}
|
||||||
|
elseif (
|
||||||
|
(($op_result | select-string "has finished successfully").length -gt 0) -or
|
||||||
|
# webpi has different text output, and that doesn't include the package name but instead the human-friendly name
|
||||||
|
(($op_result | select-string "Install of Products: SUCCESS").length -gt 0) -or
|
||||||
|
(($op_result | select-string "gem installed").length -gt 0) -or
|
||||||
|
(($op_result | select-string "gems installed").length -gt 0)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
$result.changed = $true
|
||||||
|
}
|
||||||
|
Else
|
||||||
|
{
|
||||||
|
Fail-Json $result "Install error: $op_result"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Elseif ($state -eq "absent")
|
||||||
|
{
|
||||||
|
$op_result = invoke-expression "$executable uninstall $package"
|
||||||
|
# HACK: Misleading - 'Uninstalling from folder' appears in output even when package is not installed, hence order of checks this way
|
||||||
|
if (
|
||||||
|
(($op_result | select-string "not installed").length -gt 0) -or
|
||||||
|
(($op_result | select-string "Cannot find path").length -gt 0)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
#no change
|
||||||
|
}
|
||||||
|
elseif (($op_result | select-string "Uninstalling from folder").length -gt 0)
|
||||||
|
{
|
||||||
|
$result.changed = $true
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Fail-Json $result "Uninstall error: $op_result"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($showlog)
|
||||||
|
{
|
||||||
|
Set-Attr $result "chocolatey_log" $op_result
|
||||||
|
}
|
||||||
|
Set-Attr $result "chocolatey_success" "true"
|
||||||
|
|
||||||
|
Exit-Json $result;
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2014, Trond Hindenes <trond@hindenes.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/>.
|
||||||
|
|
||||||
|
# this is a windows documentation stub. actual code lives in the .ps1
|
||||||
|
# file of the same name
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: win_chocolatey
|
||||||
|
version_added: "1.9"
|
||||||
|
short_description: Installs packages using chocolatey
|
||||||
|
description:
|
||||||
|
- Installs packages using Chocolatey (http://chocolatey.org/). If Chocolatey is missing from the system, the module will install it. List of packages can be found at http://chocolatey.org/packages
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of the package to be installed
|
||||||
|
required: true
|
||||||
|
default: null
|
||||||
|
aliases: []
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- State of the package on the system
|
||||||
|
required: false
|
||||||
|
choices:
|
||||||
|
- present
|
||||||
|
- absent
|
||||||
|
default: present
|
||||||
|
aliases: []
|
||||||
|
force:
|
||||||
|
description:
|
||||||
|
- Forces install of the package (even if it already exists). Using Force will cause ansible to always report that a change was made
|
||||||
|
required: false
|
||||||
|
choices:
|
||||||
|
- yes
|
||||||
|
- no
|
||||||
|
default: no
|
||||||
|
aliases: []
|
||||||
|
version:
|
||||||
|
description:
|
||||||
|
- Specific version of the package to be installed
|
||||||
|
- Ignored when state == 'absent'
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
aliases: []
|
||||||
|
showlog:
|
||||||
|
description:
|
||||||
|
- Outputs the chocolatey log inside a chocolatey_log property.
|
||||||
|
required: false
|
||||||
|
choices:
|
||||||
|
- yes
|
||||||
|
- no
|
||||||
|
default: no
|
||||||
|
aliases: []
|
||||||
|
source:
|
||||||
|
description:
|
||||||
|
- Which source to install from
|
||||||
|
require: false
|
||||||
|
choices:
|
||||||
|
- chocolatey
|
||||||
|
- ruby
|
||||||
|
- webpi
|
||||||
|
- windowsfeatures
|
||||||
|
default: chocolatey
|
||||||
|
aliases: []
|
||||||
|
logPath:
|
||||||
|
description:
|
||||||
|
- Where to log command output to
|
||||||
|
require: false
|
||||||
|
default: c:\\ansible-playbook.log
|
||||||
|
aliases: []
|
||||||
|
author: Trond Hindenes, Peter Mounce
|
||||||
|
'''
|
||||||
|
|
||||||
|
# TODO:
|
||||||
|
# * Better parsing when a package has dependencies - currently fails
|
||||||
|
# * Time each item that is run
|
||||||
|
# * Support 'changed' with gems - would require shelling out to `gem list` first and parsing, kinda defeating the point of using chocolatey.
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Install git
|
||||||
|
win_chocolatey:
|
||||||
|
name: git
|
||||||
|
|
||||||
|
# Install notepadplusplus version 6.6
|
||||||
|
win_chocolatey:
|
||||||
|
name: notepadplusplus.install
|
||||||
|
version: 6.6
|
||||||
|
|
||||||
|
# Uninstall git
|
||||||
|
win_chocolatey:
|
||||||
|
name: git
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
# Install Application Request Routing v3 from webpi
|
||||||
|
# Logically, this requires that you install IIS first (see win_feature)
|
||||||
|
# To find a list of packages available via webpi source, `choco list -source webpi`
|
||||||
|
win_chocolatey:
|
||||||
|
name: ARRv3
|
||||||
|
source: webpi
|
||||||
|
'''
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
#!powershell
|
||||||
|
# This file is part of Ansible
|
||||||
|
#
|
||||||
|
# Copyright 2014, Trond Hindenes <trond@hindenes.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/>.
|
||||||
|
|
||||||
|
# WANT_JSON
|
||||||
|
# POWERSHELL_COMMON
|
||||||
|
|
||||||
|
function Write-Log
|
||||||
|
{
|
||||||
|
param
|
||||||
|
(
|
||||||
|
[parameter(mandatory=$false)]
|
||||||
|
[System.String]
|
||||||
|
$message
|
||||||
|
)
|
||||||
|
|
||||||
|
$date = get-date -format 'yyyy-MM-dd hh:mm:ss.zz'
|
||||||
|
|
||||||
|
Write-Host "$date $message"
|
||||||
|
|
||||||
|
Out-File -InputObject "$date $message" -FilePath $global:LoggingFile -Append
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = Parse-Args $args;
|
||||||
|
$result = New-Object PSObject;
|
||||||
|
Set-Attr $result "changed" $false;
|
||||||
|
|
||||||
|
if(($params.logPath).Length -gt 0) {
|
||||||
|
$global:LoggingFile = $params.logPath
|
||||||
|
} else {
|
||||||
|
$global:LoggingFile = "c:\ansible-playbook.log"
|
||||||
|
}
|
||||||
|
if ($params.category) {
|
||||||
|
$category = $params.category
|
||||||
|
} else {
|
||||||
|
$category = "critical"
|
||||||
|
}
|
||||||
|
|
||||||
|
$installed_prior = get-wulist -isinstalled | foreach { $_.KBArticleIDs }
|
||||||
|
set-attr $result "updates_already_present" $installed_prior
|
||||||
|
|
||||||
|
write-log "Looking for updates in '$category'"
|
||||||
|
set-attr $result "updates_category" $category
|
||||||
|
$to_install = get-wulist -category $category
|
||||||
|
$installed = @()
|
||||||
|
foreach ($u in $to_install) {
|
||||||
|
$kb = $u.KBArticleIDs
|
||||||
|
write-log "Installing $kb - $($u.Title)"
|
||||||
|
$install_result = get-wuinstall -KBArticleID $u.KBArticleIDs -acceptall -ignorereboot
|
||||||
|
Set-Attr $result "updates_installed_KB$kb" $u.Title
|
||||||
|
$installed += $kb
|
||||||
|
}
|
||||||
|
write-log "Installed: $($installed.count)"
|
||||||
|
set-attr $result "updates_installed" $installed
|
||||||
|
set-attr $result "updates_installed_count" $installed.count
|
||||||
|
$result.changed = $installed.count -gt 0
|
||||||
|
|
||||||
|
$installed_afterwards = get-wulist -isinstalled | foreach { $_.KBArticleIDs }
|
||||||
|
set-attr $result "updates_installed_afterwards" $installed_afterwards
|
||||||
|
|
||||||
|
$reboot_needed = Get-WURebootStatus
|
||||||
|
write-log $reboot_needed
|
||||||
|
if ($reboot_needed -match "not") {
|
||||||
|
write-log "Reboot not required"
|
||||||
|
} else {
|
||||||
|
write-log "Reboot required"
|
||||||
|
Set-Attr $result "updates_reboot_needed" $true
|
||||||
|
$result.changed = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
Set-Attr $result "updates_success" "true"
|
||||||
|
Exit-Json $result;
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2014, Peter Mounce <public@neverrunwithscissors.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/>.
|
||||||
|
|
||||||
|
# this is a windows documentation stub. actual code lives in the .ps1
|
||||||
|
# file of the same name
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: win_updates
|
||||||
|
version_added: "1.9"
|
||||||
|
short_description: Lists / Installs windows updates
|
||||||
|
description:
|
||||||
|
- Installs windows updates using PSWindowsUpdate (http://gallery.technet.microsoft.com/scriptcenter/2d191bcd-3308-4edd-9de2-88dff796b0bc).
|
||||||
|
- PSWindowsUpdate needs to be installed first - use win_chocolatey.
|
||||||
|
options:
|
||||||
|
category:
|
||||||
|
description:
|
||||||
|
- Which category to install updates from
|
||||||
|
required: false
|
||||||
|
default: critical
|
||||||
|
choices:
|
||||||
|
- critical
|
||||||
|
- security
|
||||||
|
- (anything that is a valid update category)
|
||||||
|
default: critical
|
||||||
|
aliases: []
|
||||||
|
author: Peter Mounce
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Install updates from security category
|
||||||
|
win_updates:
|
||||||
|
category: security
|
||||||
|
'''
|
||||||
Loading…
Reference in New Issue