mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
launchd: new module to control services on macOS hosts (#305)
This commit is contained in:
parent
669b7bf090
commit
80cd8329e0
21 changed files with 947 additions and 0 deletions
1
plugins/modules/launchd.py
Symbolic link
1
plugins/modules/launchd.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
./system/launchd.py
|
480
plugins/modules/system/launchd.py
Normal file
480
plugins/modules/system/launchd.py
Normal file
|
@ -0,0 +1,480 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2018, Martin Migasiewicz <migasiew.nk@gmail.com>
|
||||
# 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
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: launchd
|
||||
author:
|
||||
- Martin Migasiewicz (@martinm82)
|
||||
short_description: Manage macOS services
|
||||
version_added: 1.0.0
|
||||
description:
|
||||
- Manage launchd services on target macOS hosts.
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the service.
|
||||
type: str
|
||||
required: true
|
||||
state:
|
||||
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.
|
||||
These will trigger a stop/start (restarted) or an unload/load
|
||||
(reloaded).
|
||||
- C(restarted) unloads and loads the service before start to ensure
|
||||
that the latest job definition (plist) is used.
|
||||
- C(reloaded) unloads and loads the service to ensure that the latest
|
||||
job definition (plist) is used. Whether a service is started or
|
||||
stopped depends on the content of the definition file.
|
||||
type: str
|
||||
choices: [ reloaded, restarted, started, stopped, unloaded ]
|
||||
enabled:
|
||||
description:
|
||||
- Whether the service should start on boot.
|
||||
- B(At least one of state and enabled are required.)
|
||||
type: bool
|
||||
force_stop:
|
||||
description:
|
||||
- Whether the service should not be restarted automatically by launchd.
|
||||
- Services might have the 'KeepAlive' attribute set to true in a launchd configuration.
|
||||
In case this is set to true, stopping a service will cause that launchd starts the service again.
|
||||
- Set this option to C(yes) to let this module change the 'KeepAlive' attribute to false.
|
||||
type: bool
|
||||
default: no
|
||||
notes:
|
||||
- A user must privileged to manage services using this module.
|
||||
requirements:
|
||||
- A system managed by launchd
|
||||
- The plistlib python library
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Make sure spotify webhelper is started
|
||||
community.general.launchd:
|
||||
name: com.spotify.webhelper
|
||||
state: started
|
||||
|
||||
- name: Deploy custom memcached job definition
|
||||
template:
|
||||
src: org.memcached.plist.j2
|
||||
dest: /Library/LaunchDaemons/org.memcached.plist
|
||||
|
||||
- name: Run memcached
|
||||
community.general.launchd:
|
||||
name: org.memcached
|
||||
state: started
|
||||
|
||||
- name: Stop memcached
|
||||
community.general.launchd:
|
||||
name: org.memcached
|
||||
state: stopped
|
||||
|
||||
- name: Stop memcached
|
||||
community.general.launchd:
|
||||
name: org.memcached
|
||||
state: stopped
|
||||
force_stop: yes
|
||||
|
||||
- name: Restart memcached
|
||||
community.general.launchd:
|
||||
name: org.memcached
|
||||
state: restarted
|
||||
|
||||
- name: Unload memcached
|
||||
community.general.launchd:
|
||||
name: org.memcached
|
||||
state: unloaded
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
status:
|
||||
description: Metadata about service status
|
||||
returned: always
|
||||
type: dict
|
||||
sample:
|
||||
{
|
||||
"current_pid": "-",
|
||||
"current_state": "stopped",
|
||||
"previous_pid": "82636",
|
||||
"previous_state": "running"
|
||||
}
|
||||
'''
|
||||
|
||||
import os
|
||||
import plistlib
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from time import sleep
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
|
||||
class ServiceState:
|
||||
UNKNOWN = 0
|
||||
LOADED = 1
|
||||
STOPPED = 2
|
||||
STARTED = 3
|
||||
UNLOADED = 4
|
||||
|
||||
@staticmethod
|
||||
def to_string(state):
|
||||
strings = {
|
||||
ServiceState.UNKNOWN: 'unknown',
|
||||
ServiceState.LOADED: 'loaded',
|
||||
ServiceState.STOPPED: 'stopped',
|
||||
ServiceState.STARTED: 'started',
|
||||
ServiceState.UNLOADED: 'unloaded'
|
||||
}
|
||||
return strings[state]
|
||||
|
||||
|
||||
class Plist:
|
||||
def __init__(self, module, service):
|
||||
self.__changed = False
|
||||
self.__service = service
|
||||
|
||||
state, pid, dummy, dummy = LaunchCtlList(module, service).run()
|
||||
|
||||
self.__file = self.__find_service_plist(service)
|
||||
if self.__file is None:
|
||||
msg = 'Unable to infer the path of %s service plist file' % service
|
||||
if pid is None and state == ServiceState.UNLOADED:
|
||||
msg += ' and it was not found among active services'
|
||||
module.fail_json(msg=msg)
|
||||
self.__update(module)
|
||||
|
||||
def __find_service_plist(self, service_name):
|
||||
"""Finds the plist file associated with a service"""
|
||||
|
||||
launchd_paths = [
|
||||
'~/Library/LaunchAgents',
|
||||
'/Library/LaunchAgents',
|
||||
'/Library/LaunchDaemons',
|
||||
'/System/Library/LaunchAgents',
|
||||
'/System/Library/LaunchDaemons'
|
||||
]
|
||||
|
||||
for path in launchd_paths:
|
||||
try:
|
||||
files = os.listdir(os.path.expanduser(path))
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
filename = '%s.plist' % service_name
|
||||
if filename in files:
|
||||
return os.path.join(path, filename)
|
||||
return None
|
||||
|
||||
def __update(self, module):
|
||||
self.__handle_param_enabled(module)
|
||||
self.__handle_param_force_stop(module)
|
||||
|
||||
def __handle_param_enabled(self, module):
|
||||
if module.params['enabled'] is not None:
|
||||
service_plist = plistlib.readPlist(self.__file)
|
||||
|
||||
# Enable/disable service startup at boot if requested
|
||||
# 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.
|
||||
if module.params['enabled'] is not None:
|
||||
enabled = service_plist.get('RunAtLoad', False)
|
||||
if module.params['enabled'] != enabled:
|
||||
service_plist['RunAtLoad'] = module.params['enabled']
|
||||
|
||||
# Update the plist with one of the changes done.
|
||||
if not module.check_mode:
|
||||
plistlib.writePlist(service_plist, self.__file)
|
||||
self.__changed = True
|
||||
|
||||
def __handle_param_force_stop(self, module):
|
||||
if module.params['force_stop'] is not None:
|
||||
service_plist = plistlib.readPlist(self.__file)
|
||||
|
||||
# Set KeepAlive to false in case force_stop is defined to avoid
|
||||
# that the service gets restarted when stopping was requested.
|
||||
if module.params['force_stop'] is not None:
|
||||
keep_alive = service_plist.get('KeepAlive', False)
|
||||
if module.params['force_stop'] and keep_alive:
|
||||
service_plist['KeepAlive'] = not module.params['force_stop']
|
||||
|
||||
# Update the plist with one of the changes done.
|
||||
if not module.check_mode:
|
||||
plistlib.writePlist(service_plist, self.__file)
|
||||
self.__changed = True
|
||||
|
||||
def is_changed(self):
|
||||
return self.__changed
|
||||
|
||||
def get_file(self):
|
||||
return self.__file
|
||||
|
||||
|
||||
class LaunchCtlTask(object):
|
||||
__metaclass__ = ABCMeta
|
||||
WAITING_TIME = 5 # seconds
|
||||
|
||||
def __init__(self, module, service, plist):
|
||||
self._module = module
|
||||
self._service = service
|
||||
self._plist = plist
|
||||
self._launch = self._module.get_bin_path('launchctl', True)
|
||||
|
||||
def run(self):
|
||||
"""Runs a launchd command like 'load', 'unload', 'start', 'stop', etc.
|
||||
and returns the new state and pid.
|
||||
"""
|
||||
self.runCommand()
|
||||
return self.get_state()
|
||||
|
||||
@abstractmethod
|
||||
def runCommand(self):
|
||||
pass
|
||||
|
||||
def get_state(self):
|
||||
rc, out, err = self._launchctl("list")
|
||||
if rc != 0:
|
||||
self._module.fail_json(
|
||||
msg='Failed to get status of %s' % (self._launch))
|
||||
|
||||
state = ServiceState.UNLOADED
|
||||
service_pid = "-"
|
||||
status_code = None
|
||||
for line in out.splitlines():
|
||||
if line.strip():
|
||||
pid, last_exit_code, label = line.split('\t')
|
||||
if label.strip() == self._service:
|
||||
service_pid = pid
|
||||
status_code = last_exit_code
|
||||
|
||||
# From launchctl man page:
|
||||
# If the number [...] is negative, it represents the
|
||||
# negative of the signal which killed the job. Thus,
|
||||
# "-15" would indicate that the job was terminated with
|
||||
# SIGTERM.
|
||||
if last_exit_code not in ['0', '-2', '-3', '-9', '-15']:
|
||||
# Something strange happened and we have no clue in
|
||||
# which state the service is now. Therefore we mark
|
||||
# the service state as UNKNOWN.
|
||||
state = ServiceState.UNKNOWN
|
||||
elif pid != '-':
|
||||
# PID seems to be an integer so we assume the service
|
||||
# is started.
|
||||
state = ServiceState.STARTED
|
||||
else:
|
||||
# Exit code is 0 and PID is not available so we assume
|
||||
# the service is stopped.
|
||||
state = ServiceState.STOPPED
|
||||
break
|
||||
return (state, service_pid, status_code, err)
|
||||
|
||||
def start(self):
|
||||
rc, out, err = self._launchctl("start")
|
||||
# Unfortunately launchd does not wait until the process really started.
|
||||
sleep(self.WAITING_TIME)
|
||||
return (rc, out, err)
|
||||
|
||||
def stop(self):
|
||||
rc, out, err = self._launchctl("stop")
|
||||
# Unfortunately launchd does not wait until the process really stopped.
|
||||
sleep(self.WAITING_TIME)
|
||||
return (rc, out, err)
|
||||
|
||||
def restart(self):
|
||||
# TODO: check for rc, out, err
|
||||
self.stop()
|
||||
return self.start()
|
||||
|
||||
def reload(self):
|
||||
# TODO: check for rc, out, err
|
||||
self.unload()
|
||||
return self.load()
|
||||
|
||||
def load(self):
|
||||
return self._launchctl("load")
|
||||
|
||||
def unload(self):
|
||||
return self._launchctl("unload")
|
||||
|
||||
def _launchctl(self, command):
|
||||
service_or_plist = self._plist.get_file() if command in [
|
||||
'load', 'unload'] else self._service if command in ['start', 'stop'] else ""
|
||||
|
||||
rc, out, err = self._module.run_command(
|
||||
'%s %s %s' % (self._launch, command, service_or_plist))
|
||||
|
||||
if rc != 0:
|
||||
msg = "Unable to %s '%s' (%s): '%s'" % (
|
||||
command, self._service, self._plist.get_file(), err)
|
||||
self._module.fail_json(msg=msg)
|
||||
|
||||
return (rc, out, err)
|
||||
|
||||
|
||||
class LaunchCtlStart(LaunchCtlTask):
|
||||
def __init__(self, module, service, plist):
|
||||
super(LaunchCtlStart, self).__init__(module, service, plist)
|
||||
|
||||
def runCommand(self):
|
||||
state, dummy, dummy, dummy = self.get_state()
|
||||
|
||||
if state == ServiceState.STOPPED or state == ServiceState.LOADED:
|
||||
self.reload()
|
||||
self.start()
|
||||
elif state == ServiceState.STARTED:
|
||||
# In case the service is already in started state but the
|
||||
# job definition was changed we need to unload/load the
|
||||
# service and start the service again.
|
||||
if self._plist.is_changed():
|
||||
self.reload()
|
||||
self.start()
|
||||
elif state == ServiceState.UNLOADED:
|
||||
self.load()
|
||||
self.start()
|
||||
elif state == ServiceState.UNKNOWN:
|
||||
# We are in an unknown state, let's try to reload the config
|
||||
# and start the service again.
|
||||
self.reload()
|
||||
self.start()
|
||||
|
||||
|
||||
class LaunchCtlStop(LaunchCtlTask):
|
||||
def __init__(self, module, service, plist):
|
||||
super(LaunchCtlStop, self).__init__(module, service, plist)
|
||||
|
||||
def runCommand(self):
|
||||
state, dummy, dummy, dummy = self.get_state()
|
||||
|
||||
if state == ServiceState.STOPPED:
|
||||
# In case the service is stopped and we might later decide
|
||||
# to start it, we need to reload the job definition by
|
||||
# forcing an unload and load first.
|
||||
# Afterwards we need to stop it as it might have been
|
||||
# started again (KeepAlive or RunAtLoad).
|
||||
if self._plist.is_changed():
|
||||
self.reload()
|
||||
self.stop()
|
||||
elif state == ServiceState.STARTED or state == ServiceState.LOADED:
|
||||
if self._plist.is_changed():
|
||||
self.reload()
|
||||
self.stop()
|
||||
elif state == ServiceState.UNKNOWN:
|
||||
# We are in an unknown state, let's try to reload the config
|
||||
# and stop the service gracefully.
|
||||
self.reload()
|
||||
self.stop()
|
||||
|
||||
|
||||
class LaunchCtlReload(LaunchCtlTask):
|
||||
def __init__(self, module, service, plist):
|
||||
super(LaunchCtlReload, self).__init__(module, service, plist)
|
||||
|
||||
def runCommand(self):
|
||||
state, dummy, dummy, dummy = self.get_state()
|
||||
|
||||
if state == ServiceState.UNLOADED:
|
||||
# launchd throws an error if we do an unload on an already
|
||||
# unloaded service.
|
||||
self.load()
|
||||
else:
|
||||
self.reload()
|
||||
|
||||
|
||||
class LaunchCtlUnload(LaunchCtlTask):
|
||||
def __init__(self, module, service, plist):
|
||||
super(LaunchCtlUnload, self).__init__(module, service, plist)
|
||||
|
||||
def runCommand(self):
|
||||
state, dummy, dummy, dummy = self.get_state()
|
||||
self.unload()
|
||||
|
||||
|
||||
class LaunchCtlRestart(LaunchCtlReload):
|
||||
def __init__(self, module, service, plist):
|
||||
super(LaunchCtlRestart, self).__init__(module, service, plist)
|
||||
|
||||
def runCommand(self):
|
||||
super(LaunchCtlRestart, self).runCommand()
|
||||
self.start()
|
||||
|
||||
|
||||
class LaunchCtlList(LaunchCtlTask):
|
||||
def __init__(self, module, service):
|
||||
super(LaunchCtlList, self).__init__(module, service, None)
|
||||
|
||||
def runCommand(self):
|
||||
# Do nothing, the list functionality is done by the
|
||||
# base class run method.
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
name=dict(type='str', required=True),
|
||||
state=dict(type='str', choices=['reloaded', 'restarted', 'started', 'stopped', 'unloaded']),
|
||||
enabled=dict(type='bool'),
|
||||
force_stop=dict(type='bool', default=False),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
required_one_of=[
|
||||
['state', 'enabled'],
|
||||
],
|
||||
)
|
||||
|
||||
service = module.params['name']
|
||||
action = module.params['state']
|
||||
rc = 0
|
||||
out = err = ''
|
||||
result = {
|
||||
'name': service,
|
||||
'changed': False,
|
||||
'status': {},
|
||||
}
|
||||
|
||||
# We will tailor the plist file in case one of the options
|
||||
# (enabled, force_stop) was specified.
|
||||
plist = Plist(module, service)
|
||||
result['changed'] = plist.is_changed()
|
||||
|
||||
# Gather information about the service to be controlled.
|
||||
state, pid, dummy, dummy = LaunchCtlList(module, service).run()
|
||||
result['status']['previous_state'] = ServiceState.to_string(state)
|
||||
result['status']['previous_pid'] = pid
|
||||
|
||||
# Map the actions to specific tasks
|
||||
tasks = {
|
||||
'started': LaunchCtlStart(module, service, plist),
|
||||
'stopped': LaunchCtlStop(module, service, plist),
|
||||
'restarted': LaunchCtlRestart(module, service, plist),
|
||||
'reloaded': LaunchCtlReload(module, service, plist),
|
||||
'unloaded': LaunchCtlUnload(module, service, plist)
|
||||
}
|
||||
|
||||
status_code = '0'
|
||||
# Run the requested task
|
||||
if not module.check_mode:
|
||||
state, pid, status_code, err = tasks[action].run()
|
||||
|
||||
result['status']['current_state'] = ServiceState.to_string(state)
|
||||
result['status']['current_pid'] = pid
|
||||
result['status']['status_code'] = status_code
|
||||
result['status']['error'] = err
|
||||
|
||||
if (result['status']['current_state'] != result['status']['previous_state'] or
|
||||
result['status']['current_pid'] != result['status']['previous_pid']):
|
||||
result['changed'] = True
|
||||
if module.check_mode:
|
||||
result['changed'] = True
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
3
tests/integration/targets/launchd/aliases
Normal file
3
tests/integration/targets/launchd/aliases
Normal file
|
@ -0,0 +1,3 @@
|
|||
shippable/posix/group1
|
||||
skip/freebsd
|
||||
skip/rhel
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import sys
|
||||
|
||||
if __name__ == '__main__':
|
||||
if sys.version_info[0] >= 3:
|
||||
import http.server
|
||||
import socketserver
|
||||
PORT = int(sys.argv[1])
|
||||
Handler = http.server.SimpleHTTPRequestHandler
|
||||
httpd = socketserver.TCPServer(("", PORT), Handler)
|
||||
httpd.serve_forever()
|
||||
else:
|
||||
import mimetypes
|
||||
mimetypes.init()
|
||||
mimetypes.add_type('application/json', '.json')
|
||||
import SimpleHTTPServer
|
||||
SimpleHTTPServer.test()
|
4
tests/integration/targets/launchd/meta/main.yml
Normal file
4
tests/integration/targets/launchd/meta/main.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
|
||||
dependencies:
|
||||
- prepare_tests
|
22
tests/integration/targets/launchd/tasks/main.yml
Normal file
22
tests/integration/targets/launchd/tasks/main.yml
Normal file
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
|
||||
- name: Test launchd module
|
||||
block:
|
||||
- name: Expect that launchctl exists
|
||||
stat:
|
||||
path: /bin/launchctl
|
||||
register: launchctl_check
|
||||
failed_when:
|
||||
- not launchctl_check.stat.exists
|
||||
|
||||
- name: Run tests
|
||||
include_tasks: test.yml
|
||||
with_items:
|
||||
- test_unknown
|
||||
- test_start_stop
|
||||
- test_restart
|
||||
- test_unload
|
||||
- test_reload
|
||||
- test_runatload
|
||||
|
||||
when: ansible_os_family == 'Darwin'
|
20
tests/integration/targets/launchd/tasks/setup.yml
Normal file
20
tests/integration/targets/launchd/tasks/setup.yml
Normal file
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
|
||||
- name: "[{{ item }}] Deploy test service configuration"
|
||||
template:
|
||||
src: "{{ launchd_service_name }}.plist.j2"
|
||||
dest: "{{ launchd_plist_location }}"
|
||||
become: yes
|
||||
|
||||
- name: install the test daemon script
|
||||
copy:
|
||||
src: ansible_test_service.py
|
||||
dest: /usr/local/sbin/ansible_test_service
|
||||
mode: '755'
|
||||
|
||||
- name: rewrite shebang in the test daemon script
|
||||
lineinfile:
|
||||
path: /usr/local/sbin/ansible_test_service
|
||||
line: "#!{{ ansible_python_interpreter | realpath }}"
|
||||
insertbefore: BOF
|
||||
firstmatch: yes
|
27
tests/integration/targets/launchd/tasks/teardown.yml
Normal file
27
tests/integration/targets/launchd/tasks/teardown.yml
Normal file
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
|
||||
- name: "[{{ item }}] Unload service"
|
||||
launchd:
|
||||
name: "{{ launchd_service_name }}"
|
||||
state: unloaded
|
||||
become: yes
|
||||
register: launchd_unloaded_result
|
||||
|
||||
- name: "[{{ item }}] Validation"
|
||||
assert:
|
||||
that:
|
||||
- launchd_unloaded_result is success
|
||||
- launchd_unloaded_result.status.current_state == 'unloaded'
|
||||
- launchd_unloaded_result.status.current_pid == '-'
|
||||
|
||||
- name: "[{{ item }}] Remove test service configuration"
|
||||
file:
|
||||
path: "{{ launchd_plist_location }}"
|
||||
state: absent
|
||||
become: yes
|
||||
|
||||
- name: "[{{ item }}] Remove test service server"
|
||||
file:
|
||||
path: "/usr/local/sbin/ansible_test_service"
|
||||
state: absent
|
||||
become: yes
|
8
tests/integration/targets/launchd/tasks/test.yml
Normal file
8
tests/integration/targets/launchd/tasks/test.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
|
||||
- name: "Running {{ item }}"
|
||||
block:
|
||||
- include_tasks: setup.yml
|
||||
- include_tasks: "tests/{{ item }}.yml"
|
||||
always:
|
||||
- include_tasks: teardown.yml
|
|
@ -0,0 +1,68 @@
|
|||
---
|
||||
|
||||
# -----------------------------------------------------------
|
||||
|
||||
- name: "[{{ item }}] Given a started service in check_mode"
|
||||
launchd:
|
||||
name: "{{ launchd_service_name }}"
|
||||
state: started
|
||||
become: yes
|
||||
register: "test_1_launchd_start_result_check_mode"
|
||||
check_mode: yes
|
||||
|
||||
- name: "[{{ item }}] Assert that everything work in check mode"
|
||||
assert:
|
||||
that:
|
||||
- test_1_launchd_start_result_check_mode is success
|
||||
- test_1_launchd_start_result_check_mode is changed
|
||||
|
||||
- name: "[{{ item }}] Given a started service..."
|
||||
launchd:
|
||||
name: "{{ launchd_service_name }}"
|
||||
state: started
|
||||
become: yes
|
||||
register: "test_1_launchd_start_result"
|
||||
|
||||
|
||||
- name: "[{{ item }}] The started service should run on port 21212"
|
||||
wait_for:
|
||||
port: 21212
|
||||
delay: 5
|
||||
timeout: 10
|
||||
|
||||
- name: "[{{ item }}] Deploy a new test service configuration with a new port 21213"
|
||||
template:
|
||||
src: "modified.{{ launchd_service_name }}.plist.j2"
|
||||
dest: "{{ launchd_plist_location }}"
|
||||
become: yes
|
||||
|
||||
- name: "[{{ item }}] When reloading the service..."
|
||||
launchd:
|
||||
name: "{{ launchd_service_name }}"
|
||||
state: reloaded
|
||||
become: yes
|
||||
register: "test_1_launchd_reload_result"
|
||||
|
||||
- name: "[{{ item }}] Validate that service was reloaded"
|
||||
assert:
|
||||
that:
|
||||
- test_1_launchd_reload_result is success
|
||||
- test_1_launchd_reload_result is changed
|
||||
- test_1_launchd_reload_result.status.previous_pid == test_1_launchd_start_result.status.current_pid
|
||||
- test_1_launchd_reload_result.status.previous_state == test_1_launchd_start_result.status.current_state
|
||||
- test_1_launchd_reload_result.status.current_state == 'stopped'
|
||||
- test_1_launchd_reload_result.status.current_pid == '-'
|
||||
|
||||
- name: "[{{ item }}] Start the service with the new configuration..."
|
||||
launchd:
|
||||
name: "{{ launchd_service_name }}"
|
||||
state: started
|
||||
become: yes
|
||||
register: "test_1_launchd_start_result"
|
||||
|
||||
|
||||
- name: "[{{ item }}] The started service should run on port 21213"
|
||||
wait_for:
|
||||
port: 21213
|
||||
delay: 5
|
||||
timeout: 10
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
|
||||
# -----------------------------------------------------------
|
||||
|
||||
- name: "[{{ item }}] Given a started service..."
|
||||
launchd:
|
||||
name: "{{ launchd_service_name }}"
|
||||
state: started
|
||||
become: yes
|
||||
register: "test_1_launchd_start_result"
|
||||
|
||||
|
||||
- name: "[{{ item }}] When restarting the service in check mode"
|
||||
launchd:
|
||||
name: "{{ launchd_service_name }}"
|
||||
state: restarted
|
||||
become: yes
|
||||
register: "test_1_launchd_restart_result_check_mode"
|
||||
check_mode: yes
|
||||
|
||||
- name: "[{{ item }}] Validate that service was restarted in check mode"
|
||||
assert:
|
||||
that:
|
||||
- test_1_launchd_restart_result_check_mode is success
|
||||
- test_1_launchd_restart_result_check_mode is changed
|
||||
|
||||
- name: "[{{ item }}] When restarting the service..."
|
||||
launchd:
|
||||
name: "{{ launchd_service_name }}"
|
||||
state: restarted
|
||||
become: yes
|
||||
register: "test_1_launchd_restart_result"
|
||||
|
||||
- name: "[{{ item }}] Validate that service was restarted"
|
||||
assert:
|
||||
that:
|
||||
- test_1_launchd_restart_result is success
|
||||
- test_1_launchd_restart_result is changed
|
||||
- test_1_launchd_restart_result.status.previous_pid == test_1_launchd_start_result.status.current_pid
|
||||
- test_1_launchd_restart_result.status.previous_state == test_1_launchd_start_result.status.current_state
|
||||
- test_1_launchd_restart_result.status.current_state == 'started'
|
||||
- test_1_launchd_restart_result.status.current_pid != '-'
|
||||
- test_1_launchd_restart_result.status.status_code == '0'
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
# -----------------------------------------------------------
|
||||
|
||||
- name: "[{{ item }}] Given a started service with RunAtLoad set to true..."
|
||||
launchd:
|
||||
name: "{{ launchd_service_name }}"
|
||||
state: started
|
||||
enabled: yes
|
||||
become: yes
|
||||
register: test_1_launchd_start_result
|
||||
|
||||
- name: "[{{ item }}] Validate that service was started"
|
||||
assert:
|
||||
that:
|
||||
- test_1_launchd_start_result is success
|
||||
- test_1_launchd_start_result is changed
|
||||
- test_1_launchd_start_result.status.previous_pid == '-'
|
||||
- test_1_launchd_start_result.status.previous_state == 'unloaded'
|
||||
- test_1_launchd_start_result.status.current_state == 'started'
|
||||
- test_1_launchd_start_result.status.current_pid != '-'
|
||||
- test_1_launchd_start_result.status.status_code == '0'
|
||||
|
||||
- name: "[{{ item }}] Validate that RunAtLoad is set to true"
|
||||
replace:
|
||||
path: "{{ launchd_plist_location }}"
|
||||
regexp: |
|
||||
\s+<key>RunAtLoad</key>
|
||||
\s+<true/>
|
||||
replace: found_run_at_load
|
||||
check_mode: yes
|
||||
register: contents_would_have
|
||||
failed_when: not contents_would_have is changed
|
|
@ -0,0 +1,112 @@
|
|||
---
|
||||
|
||||
# -----------------------------------------------------------
|
||||
|
||||
- name: "[{{ item }}] Given a started service in check mode"
|
||||
launchd:
|
||||
name: "{{ launchd_service_name }}"
|
||||
state: started
|
||||
become: yes
|
||||
register: "test_1_launchd_start_result_check_mode"
|
||||
check_mode: yes
|
||||
|
||||
|
||||
- name: "[{{ item }}] Validate that service was started in check mode"
|
||||
assert:
|
||||
that:
|
||||
- test_1_launchd_start_result_check_mode is success
|
||||
- test_1_launchd_start_result_check_mode is changed
|
||||
|
||||
|
||||
- name: "[{{ item }}] Given a started service..."
|
||||
launchd:
|
||||
name: "{{ launchd_service_name }}"
|
||||
state: started
|
||||
become: yes
|
||||
register: "test_1_launchd_start_result"
|
||||
|
||||
|
||||
- name: "[{{ item }}] Validate that service was started"
|
||||
assert:
|
||||
that:
|
||||
- test_1_launchd_start_result is success
|
||||
- test_1_launchd_start_result is changed
|
||||
- test_1_launchd_start_result.status.previous_pid == '-'
|
||||
- test_1_launchd_start_result.status.previous_state == 'unloaded'
|
||||
- test_1_launchd_start_result.status.current_state == 'started'
|
||||
- test_1_launchd_start_result.status.current_pid != '-'
|
||||
- test_1_launchd_start_result.status.status_code == '0'
|
||||
|
||||
# -----------------------------------------------------------
|
||||
|
||||
- name: "[{{ item }}] Given a stopped service..."
|
||||
launchd:
|
||||
name: "{{ launchd_service_name }}"
|
||||
state: stopped
|
||||
become: yes
|
||||
register: "test_2_launchd_stop_result"
|
||||
|
||||
- name: "[{{ item }}] Validate that service was stopped after it was started"
|
||||
assert:
|
||||
that:
|
||||
- test_2_launchd_stop_result is success
|
||||
- test_2_launchd_stop_result is changed
|
||||
- test_2_launchd_stop_result.status.previous_pid == test_1_launchd_start_result.status.current_pid
|
||||
- test_2_launchd_stop_result.status.previous_state == test_1_launchd_start_result.status.current_state
|
||||
- test_2_launchd_stop_result.status.current_state == 'stopped'
|
||||
- test_2_launchd_stop_result.status.current_pid == '-'
|
||||
|
||||
# -----------------------------------------------------------
|
||||
|
||||
- name: "[{{ item }}] Given a stopped service..."
|
||||
launchd:
|
||||
name: "{{ launchd_service_name }}"
|
||||
state: stopped
|
||||
become: yes
|
||||
register: "test_3_launchd_stop_result"
|
||||
|
||||
- name: "[{{ item }}] Validate that service can be stopped after being already stopped"
|
||||
assert:
|
||||
that:
|
||||
- test_3_launchd_stop_result is success
|
||||
- not test_3_launchd_stop_result is changed
|
||||
- test_3_launchd_stop_result.status.previous_pid == '-'
|
||||
- test_3_launchd_stop_result.status.previous_state == 'stopped'
|
||||
- test_3_launchd_stop_result.status.current_state == 'stopped'
|
||||
- test_3_launchd_stop_result.status.current_pid == '-'
|
||||
|
||||
# -----------------------------------------------------------
|
||||
|
||||
- name: "[{{ item }}] Given a started service..."
|
||||
launchd:
|
||||
name: "{{ launchd_service_name }}"
|
||||
state: started
|
||||
become: yes
|
||||
register: "test_4_launchd_start_result"
|
||||
|
||||
- name: "[{{ item }}] Validate that service was started..."
|
||||
assert:
|
||||
that:
|
||||
- test_4_launchd_start_result is success
|
||||
- test_4_launchd_start_result is changed
|
||||
- test_4_launchd_start_result.status.previous_pid == '-'
|
||||
- test_4_launchd_start_result.status.previous_state == 'stopped'
|
||||
- test_4_launchd_start_result.status.current_state == 'started'
|
||||
- test_4_launchd_start_result.status.current_pid != '-'
|
||||
- test_4_launchd_start_result.status.status_code == '0'
|
||||
|
||||
- name: "[{{ item }}] And when service is started again..."
|
||||
launchd:
|
||||
name: "{{ launchd_service_name }}"
|
||||
state: started
|
||||
become: yes
|
||||
register: "test_5_launchd_start_result"
|
||||
|
||||
- name: "[{{ item }}] Validate that service is still in the same state as before"
|
||||
assert:
|
||||
that:
|
||||
- test_5_launchd_start_result is success
|
||||
- not test_5_launchd_start_result is changed
|
||||
- test_5_launchd_start_result.status.previous_pid == test_4_launchd_start_result.status.current_pid
|
||||
- test_5_launchd_start_result.status.previous_state == test_4_launchd_start_result.status.current_state
|
||||
- test_5_launchd_start_result.status.status_code == '0'
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
|
||||
# -----------------------------------------------------------
|
||||
|
||||
- name: "[{{ item }}] Expect that an error occurs when an unknown service is used."
|
||||
launchd:
|
||||
name: com.acme.unknownservice
|
||||
state: started
|
||||
register: result
|
||||
failed_when:
|
||||
- not '"Unable to infer the path of com.acme.unknownservice service plist file and it was not found among active services" in result.msg'
|
|
@ -0,0 +1,62 @@
|
|||
---
|
||||
|
||||
# -----------------------------------------------------------
|
||||
|
||||
- name: "[{{ item }}] Given a started service..."
|
||||
launchd:
|
||||
name: "{{ launchd_service_name }}"
|
||||
state: started
|
||||
become: yes
|
||||
register: "test_1_launchd_start_result"
|
||||
|
||||
|
||||
- name: "[{{ item }}] When unloading the service in check mode"
|
||||
launchd:
|
||||
name: "{{ launchd_service_name }}"
|
||||
state: unloaded
|
||||
become: yes
|
||||
register: "test_1_launchd_unloaded_result_check_mode"
|
||||
check_mode: yes
|
||||
|
||||
- name: "[{{ item }}] Validate that service was unloaded in check mode"
|
||||
assert:
|
||||
that:
|
||||
- test_1_launchd_unloaded_result_check_mode is success
|
||||
- test_1_launchd_unloaded_result_check_mode is changed
|
||||
|
||||
|
||||
- name: "[{{ item }}] When unloading the service..."
|
||||
launchd:
|
||||
name: "{{ launchd_service_name }}"
|
||||
state: unloaded
|
||||
become: yes
|
||||
register: "test_1_launchd_unloaded_result"
|
||||
|
||||
- name: "[{{ item }}] Validate that service was unloaded"
|
||||
assert:
|
||||
that:
|
||||
- test_1_launchd_unloaded_result is success
|
||||
- test_1_launchd_unloaded_result is changed
|
||||
- test_1_launchd_unloaded_result.status.previous_pid == test_1_launchd_start_result.status.current_pid
|
||||
- test_1_launchd_unloaded_result.status.previous_state == test_1_launchd_start_result.status.current_state
|
||||
- test_1_launchd_unloaded_result.status.current_state == 'unloaded'
|
||||
- test_1_launchd_unloaded_result.status.current_pid == '-'
|
||||
|
||||
# -----------------------------------------------------------
|
||||
|
||||
- name: "[{{ item }}] Given an unloaded service on an unloaded service..."
|
||||
launchd:
|
||||
name: "{{ launchd_service_name }}"
|
||||
state: unloaded
|
||||
become: yes
|
||||
register: "test_2_launchd_unloaded_result"
|
||||
|
||||
- name: "[{{ item }}] Validate that service did not change and is still unloaded"
|
||||
assert:
|
||||
that:
|
||||
- test_2_launchd_unloaded_result is success
|
||||
- not test_2_launchd_unloaded_result is changed
|
||||
- test_2_launchd_unloaded_result.status.previous_pid == '-'
|
||||
- test_2_launchd_unloaded_result.status.previous_state == 'unloaded'
|
||||
- test_2_launchd_unloaded_result.status.current_state == 'unloaded'
|
||||
- test_2_launchd_unloaded_result.status.current_pid == '-'
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>{{ launchd_service_name }}</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/local/sbin/ansible_test_service</string>
|
||||
<string>21212</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>{{ launchd_service_name }}</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/local/sbin/ansible_test_service</string>
|
||||
<string>21213</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
4
tests/integration/targets/launchd/vars/main.yml
Normal file
4
tests/integration/targets/launchd/vars/main.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
|
||||
launchd_service_name: launchd.test.service
|
||||
launchd_plist_location: /Library/LaunchDaemons/{{ launchd_service_name }}.plist
|
|
@ -1235,6 +1235,7 @@ plugins/modules/system/java_keystore.py validate-modules:doc-missing-type
|
|||
plugins/modules/system/java_keystore.py validate-modules:parameter-type-not-in-doc
|
||||
plugins/modules/system/java_keystore.py validate-modules:undocumented-parameter
|
||||
plugins/modules/system/kernel_blacklist.py validate-modules:parameter-type-not-in-doc
|
||||
plugins/modules/system/launchd.py use-argspec-type-path # False positive
|
||||
plugins/modules/system/lbu.py validate-modules:doc-elements-mismatch
|
||||
plugins/modules/system/locale_gen.py validate-modules:parameter-type-not-in-doc
|
||||
plugins/modules/system/lvg.py pylint:blacklisted-name
|
||||
|
|
|
@ -1235,6 +1235,7 @@ plugins/modules/system/java_keystore.py validate-modules:doc-missing-type
|
|||
plugins/modules/system/java_keystore.py validate-modules:parameter-type-not-in-doc
|
||||
plugins/modules/system/java_keystore.py validate-modules:undocumented-parameter
|
||||
plugins/modules/system/kernel_blacklist.py validate-modules:parameter-type-not-in-doc
|
||||
plugins/modules/system/launchd.py use-argspec-type-path # False positive
|
||||
plugins/modules/system/lbu.py validate-modules:doc-elements-mismatch
|
||||
plugins/modules/system/locale_gen.py validate-modules:parameter-type-not-in-doc
|
||||
plugins/modules/system/lvg.py pylint:blacklisted-name
|
||||
|
|
|
@ -955,6 +955,7 @@ plugins/modules/system/interfaces_file.py validate-modules:parameter-type-not-in
|
|||
plugins/modules/system/java_cert.py pylint:blacklisted-name
|
||||
plugins/modules/system/java_keystore.py validate-modules:doc-missing-type
|
||||
plugins/modules/system/kernel_blacklist.py validate-modules:parameter-type-not-in-doc
|
||||
plugins/modules/system/launchd.py use-argspec-type-path # False positive
|
||||
plugins/modules/system/locale_gen.py validate-modules:parameter-type-not-in-doc
|
||||
plugins/modules/system/lvg.py pylint:blacklisted-name
|
||||
plugins/modules/system/lvol.py pylint:blacklisted-name
|
||||
|
|
Loading…
Reference in a new issue