Skip to content

Commit

Permalink
Update launchd module
Browse files Browse the repository at this point in the history
* Updated launchd module with latest Ansible guidelines
* Added unit testcases

Signed-off-by: Abhijeet Kasurde <akasurde@redhat.com>
  • Loading branch information
Akasurde committed May 14, 2018
1 parent 1e9c50b commit 69d715f
Show file tree
Hide file tree
Showing 2 changed files with 335 additions and 67 deletions.
175 changes: 108 additions & 67 deletions lib/ansible/modules/system/launchd.py
@@ -1,57 +1,53 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2016, Brian Coca <bcoca@ansible.com>
# (c) 2106, Theo Crevon (https://github.com/tcr-ableton)
#
# 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.

# Copyright: (c) 2016, Brian Coca <bcoca@ansible.com>
# Copyright: (c) 2016, Theo Crevon (https://github.com/tcr-ableton)
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function
__metaclass__ = type

ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}

DOCUMENTATION = '''

DOCUMENTATION = r'''
module: launchd
author:
- "Ansible Core Team"
- Theo Crevon (@tcr-ableton)
version_added: "2.2"
version_added: "2.6"
short_description: Manage OS X services.
description:
- Controls launchd services on target hosts.
- This module can be used to control launchd services on target OS X hosts.
options:
name:
required: true
description:
- Name of the service.
aliases: ['service']
required: true
description:
- Name of the service.
aliases: ['service']
state:
required: false
default: null
choices: [ 'started', 'stopped', 'restarted', 'reloaded' ]
description:
- C(started)/C(stopped) are idempotent actions that will not run commands unless necessary.
Launchd does not support C(restarted) nor C(reloaded) natively, so these will both trigger a stop and start as needed.
default: null
choices: [ 'started', 'stopped', 'restarted', 'reloaded' ]
description:
- C(started)/C(stopped) are idempotent actions that will not run commands unless necessary.
- Launchd does not support C(restarted) nor C(reloaded) natively, so these will both trigger a stop and start as needed.
enabled:
required: false
choices: [ "yes", "no" ]
default: null
description:
- Whether the service should start on boot. B(At least one of state and enabled are required.)
choices: [ True, False ]
default: null
type: 'bool'
description:
- Whether the service should start on boot. B(At least one of state and enabled are required.)
sleep:
required: false
default: 1
description:
- If the service is being C(restarted) or C(reloaded) then sleep this many seconds between the stop and start command. This helps to workaround badly behaving services.
default: 1
description:
- If the service is being C(restarted) or C(reloaded) then sleep this many seconds between the stop and start command.
- This helps to workaround badly behaving services.
notes:
- One option other than name is required.
requirements:
Expand All @@ -60,28 +56,41 @@
'''

EXAMPLES = '''
# make sure spotify webhelper is started
- launchd:
name: com.spotify.webhelper
state: started
become: yes
- name: Make sure spotify webhelper is started
launchd:
name: com.spotify.webhelper
state: started
become: yes
'''

RETURN = '''
#
status:
description: metadata about service status
returned: always
type: dict
sample:
{
"current_pid": "-",
"current_state": "stopped",
"previous_pid": "82636",
"previous_state": "running"
}
'''

import os
from time import sleep

try:
import plistlib
HAS_PLIST = True
except ImportError:
HAS_PLIST = False

from ansible.module_utils.basic import AnsibleModule


def find_service_plist(service_name):
''' finds the plist file associated with a service '''
"""Finds the plist file associated with a service"""

launchd_paths = [
'~/Library/LaunchAgents',
Expand All @@ -101,56 +110,73 @@ def find_service_plist(service_name):
if filename == '%s.plist' % service_name:
return os.path.join(path, filename)


def main():
# init
module = AnsibleModule(
argument_spec = dict(
name = dict(required=True, type='str', aliases=['service']),
state = dict(choices=[ 'started', 'stopped', 'restarted', 'reloaded'], type='str'),
enabled = dict(type='bool'),
sleep = dict(type='int', default=1),
argument_spec=dict(
name=dict(
required=True,
aliases=['service']
),
state=dict(
choices=['started', 'stopped', 'restarted', 'reloaded'],
),
supports_check_mode=True,
required_one_of=[['state', 'enabled']],
)
enabled=dict(type='bool'),
sleep=dict(type='int',
default=1),
),
supports_check_mode=True,
required_one_of=[
['state', 'enabled']
],
)

launch = module.get_bin_path('launchctl', True)
service = module.params['name']
action = module.params['state']
rc = 0
out = err = ''
result = {
'name': service,
'name': service,
'changed': False,
'status': {},
}


rc, dout, err = module.run_command('%s list' % (launch))
rc, out, err = module.run_command('%s list' % launch)
if rc != 0:
module.fail_json(msg='running %s list failed' % (launch))
module.fail_json(msg='running %s list failed' % launch)

running = False
found = False

for line in out.splitlines():
if line.strip():
pid, last_exit_code, label = line.split('\t')
if label.strip() == service:
found = True
if pid != '-':
running = True
result['status']['previous_state'] = 'running'
result['status']['previous_pid'] = pid
else:
result['status']['previous_state'] = 'stopped'
result['status']['previous_pid'] = '-'
break

if not found:
module.fail_json(msg="Unable to find the service %s among active services." % service)

# Enable/disable service startup at boot if requested
if module.params['enabled'] is not None:
plist_file = find_service_plist(service)
if plist_file is None:
msg='unable to infer the path of %s service plist file' % service
msg = 'Unable to infer the path of %s service plist file' % service
if not found:
msg += ' and it was not found among active services'
module.fail_json(msg=msg)

# Launchctl does not expose functionalities to set the RunAtLoad
# Launchctl does not expose functionality to set the RunAtLoad
# attribute of a job definition. So we parse and modify the job definition
# plist file directly for this purpose.
service_plist = plistlib.readPlist(plist_file)
Expand All @@ -164,9 +190,9 @@ def main():

do = None
if action is not None:
if action == 'started':
if action == 'started' and not running:
do = 'start'
elif action == 'stopped':
elif action == 'stopped' and running:
do = 'stop'
elif action in ['restarted', 'reloaded']:
# launchctl does not support a restart/reload command.
Expand All @@ -182,14 +208,29 @@ def main():
if do is not None:
result['changed'] = True
if not module.check_mode:
rc, out, err = module.run_command('%s %s %s %s' % (launch, do, service))
rc, out, err = module.run_command('%s %s %s' % (launch, do, service))
if rc != 0:
msg = "Unable to %s service %s: %s" % (do, service, err)
module.fail_json(msg=msg)

rc, out, err = module.run_command('%s list' % launch)
if rc != 0:
msg="Unable to %s service %s: %s" % (do, service,err)
if not found:
msg += '\nAlso the service was not found among active services'
module.fail_json(msg=msg)
module.fail_json(msg='Failed to get status of %s after %s' % (launch, do))

for line in out.splitlines():
if line.strip():
pid, last_exit_code, label = line.split('\t')
if label.strip() == service:
if pid != '-':
result['status']['current_state'] = 'running'
result['status']['current_pid'] = pid
else:
result['status']['current_state'] = 'stopped'
result['status']['current_pid'] = '-'
break

module.exit_json(**result)

if __name__ == 'main':

if __name__ == '__main__':
main()

0 comments on commit 69d715f

Please sign in to comment.