mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-02-04 07:51:50 +00:00
290 lines
9.9 KiB
Python
290 lines
9.9 KiB
Python
#!/usr/bin/python
|
|
|
|
# Copyright (c) 2012, Matt Wright <matt@nobien.net>
|
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
DOCUMENTATION = r"""
|
|
module: supervisorctl
|
|
short_description: Manage the state of a program or group of programs managed by C(supervisord)
|
|
description:
|
|
- Manage the state of a program or group of programs managed by C(supervisord).
|
|
extends_documentation_fragment:
|
|
- community.general.attributes
|
|
attributes:
|
|
check_mode:
|
|
support: full
|
|
diff_mode:
|
|
support: none
|
|
options:
|
|
name:
|
|
type: str
|
|
description:
|
|
- The name of the supervisord program or group to manage.
|
|
- The name is taken as group name when it ends with a colon V(:).
|
|
- If O(name=all), all programs and program groups are managed.
|
|
required: true
|
|
config:
|
|
type: path
|
|
description:
|
|
- The supervisor configuration file path.
|
|
server_url:
|
|
type: str
|
|
description:
|
|
- URL on which supervisord server is listening.
|
|
username:
|
|
type: str
|
|
description:
|
|
- Username to use for authentication.
|
|
password:
|
|
type: str
|
|
description:
|
|
- Password to use for authentication.
|
|
state:
|
|
type: str
|
|
description:
|
|
- The desired state of program/group.
|
|
required: true
|
|
choices: ["present", "started", "stopped", "restarted", "absent", "signalled"]
|
|
stop_before_removing:
|
|
type: bool
|
|
description:
|
|
- Use O(stop_before_removing=true) to stop the program/group before removing it.
|
|
default: false
|
|
version_added: 7.5.0
|
|
signal:
|
|
type: str
|
|
description:
|
|
- The signal to send to the program/group, when combined with the V(signalled) state. Required when l(state=signalled).
|
|
supervisorctl_path:
|
|
type: path
|
|
description:
|
|
- Path to C(supervisorctl) executable.
|
|
notes:
|
|
- When O(state=present), the module calls C(supervisorctl reread) then C(supervisorctl add) if the program/group does not
|
|
exist.
|
|
- When O(state=restarted), the module calls C(supervisorctl update) then calls C(supervisorctl restart).
|
|
- When O(state=absent), the module calls C(supervisorctl reread) then C(supervisorctl remove) to remove the target program/group.
|
|
If the program/group is still running, the action fails. If you want to stop the program/group before removing, use O(stop_before_removing=true).
|
|
requirements: ["supervisorctl"]
|
|
author:
|
|
- "Matt Wright (@mattupstate)"
|
|
- "Aaron Wang (@inetfuture) <inetfuture@gmail.com>"
|
|
"""
|
|
|
|
EXAMPLES = r"""
|
|
- name: Manage the state of program to be in started state
|
|
community.general.supervisorctl:
|
|
name: my_app
|
|
state: started
|
|
|
|
- name: Manage the state of program group to be in started state
|
|
community.general.supervisorctl:
|
|
name: 'my_apps:'
|
|
state: started
|
|
|
|
- name: Restart my_app, reading supervisorctl configuration from a specified file
|
|
community.general.supervisorctl:
|
|
name: my_app
|
|
state: restarted
|
|
config: /var/opt/my_project/supervisord.conf
|
|
|
|
- name: Restart my_app, connecting to supervisord with credentials and server URL
|
|
community.general.supervisorctl:
|
|
name: my_app
|
|
state: restarted
|
|
username: test
|
|
password: testpass
|
|
server_url: http://localhost:9001
|
|
|
|
- name: Send a signal to my_app via supervisorctl
|
|
community.general.supervisorctl:
|
|
name: my_app
|
|
state: signalled
|
|
signal: USR1
|
|
|
|
- name: Restart all programs and program groups
|
|
community.general.supervisorctl:
|
|
name: all
|
|
state: restarted
|
|
"""
|
|
|
|
|
|
import os
|
|
from ansible.module_utils.basic import AnsibleModule, is_executable
|
|
|
|
|
|
def main():
|
|
arg_spec = dict(
|
|
name=dict(type="str", required=True),
|
|
config=dict(type="path"),
|
|
server_url=dict(type="str"),
|
|
username=dict(type="str"),
|
|
password=dict(type="str", no_log=True),
|
|
supervisorctl_path=dict(type="path"),
|
|
state=dict(
|
|
type="str", required=True, choices=["present", "started", "restarted", "stopped", "absent", "signalled"]
|
|
),
|
|
stop_before_removing=dict(type="bool", default=False),
|
|
signal=dict(type="str"),
|
|
)
|
|
|
|
module = AnsibleModule(
|
|
argument_spec=arg_spec,
|
|
supports_check_mode=True,
|
|
required_if=[("state", "signalled", ["signal"])],
|
|
)
|
|
|
|
name = module.params["name"]
|
|
is_group = False
|
|
if name.endswith(":"):
|
|
is_group = True
|
|
name = name.rstrip(":")
|
|
state = module.params["state"]
|
|
stop_before_removing = module.params.get("stop_before_removing")
|
|
config = module.params.get("config")
|
|
server_url = module.params.get("server_url")
|
|
username = module.params.get("username")
|
|
password = module.params.get("password")
|
|
supervisorctl_path = module.params.get("supervisorctl_path")
|
|
signal = module.params.get("signal")
|
|
|
|
# we check error message for a pattern, so we need to make sure that's in C locale
|
|
module.run_command_environ_update = dict(LANG="C", LC_ALL="C", LC_MESSAGES="C", LC_CTYPE="C")
|
|
|
|
if supervisorctl_path:
|
|
if os.path.exists(supervisorctl_path) and is_executable(supervisorctl_path):
|
|
supervisorctl_args = [supervisorctl_path]
|
|
else:
|
|
module.fail_json(
|
|
msg=f"Provided path to supervisorctl does not exist or isn't executable: {supervisorctl_path}"
|
|
)
|
|
else:
|
|
supervisorctl_args = [module.get_bin_path("supervisorctl", True)]
|
|
|
|
if config:
|
|
supervisorctl_args.extend(["-c", config])
|
|
if server_url:
|
|
supervisorctl_args.extend(["-s", server_url])
|
|
if username:
|
|
supervisorctl_args.extend(["-u", username])
|
|
if password:
|
|
supervisorctl_args.extend(["-p", password])
|
|
|
|
def run_supervisorctl(cmd, name=None, **kwargs):
|
|
args = list(supervisorctl_args) # copy the master args
|
|
args.append(cmd)
|
|
if name:
|
|
args.append(name)
|
|
return module.run_command(args, **kwargs)
|
|
|
|
def get_matched_processes():
|
|
matched = []
|
|
rc, out, err = run_supervisorctl("status")
|
|
for line in out.splitlines():
|
|
# One status line may look like one of these two:
|
|
# process not in group:
|
|
# echo_date_lonely RUNNING pid 7680, uptime 13:22:18
|
|
# process in group:
|
|
# echo_date_group:echo_date_00 RUNNING pid 7681, uptime 13:22:18
|
|
fields = [field for field in line.split(" ") if field != ""]
|
|
process_name = fields[0]
|
|
status = fields[1]
|
|
|
|
if is_group:
|
|
# If there is ':', this process must be in a group.
|
|
if ":" in process_name:
|
|
group = process_name.split(":")[0]
|
|
if group != name:
|
|
continue
|
|
else:
|
|
continue
|
|
else:
|
|
if process_name != name and name != "all":
|
|
continue
|
|
|
|
matched.append((process_name, status))
|
|
return matched
|
|
|
|
def take_action_on_processes(processes, status_filter, action, expected_result, exit_module=True):
|
|
to_take_action_on = []
|
|
for process_name, status in processes:
|
|
if status_filter(status):
|
|
to_take_action_on.append(process_name)
|
|
|
|
if len(to_take_action_on) == 0:
|
|
if not exit_module:
|
|
return
|
|
module.exit_json(changed=False, name=name, state=state)
|
|
if module.check_mode:
|
|
if not exit_module:
|
|
return
|
|
module.exit_json(changed=True)
|
|
for process_name in to_take_action_on:
|
|
rc, out, err = run_supervisorctl(action, process_name, check_rc=True)
|
|
if f"{process_name}: {expected_result}" not in out:
|
|
module.fail_json(msg=out)
|
|
|
|
if exit_module:
|
|
module.exit_json(changed=True, name=name, state=state, affected=to_take_action_on)
|
|
|
|
if state == "restarted":
|
|
rc, out, err = run_supervisorctl("update", check_rc=True)
|
|
processes = get_matched_processes()
|
|
if len(processes) == 0:
|
|
module.fail_json(name=name, msg="ERROR (no such process)")
|
|
|
|
take_action_on_processes(processes, lambda s: True, "restart", "started")
|
|
|
|
processes = get_matched_processes()
|
|
|
|
if state == "absent":
|
|
if len(processes) == 0:
|
|
module.exit_json(changed=False, name=name, state=state)
|
|
|
|
if stop_before_removing:
|
|
take_action_on_processes(
|
|
processes, lambda s: s in ("RUNNING", "STARTING"), "stop", "stopped", exit_module=False
|
|
)
|
|
|
|
if module.check_mode:
|
|
module.exit_json(changed=True)
|
|
run_supervisorctl("reread", check_rc=True)
|
|
rc, out, err = run_supervisorctl("remove", name)
|
|
if f"{name}: removed process group" in out:
|
|
module.exit_json(changed=True, name=name, state=state)
|
|
else:
|
|
module.fail_json(msg=out, name=name, state=state)
|
|
|
|
if state == "present":
|
|
if len(processes) > 0:
|
|
module.exit_json(changed=False, name=name, state=state)
|
|
|
|
if module.check_mode:
|
|
module.exit_json(changed=True)
|
|
run_supervisorctl("reread", check_rc=True)
|
|
dummy, out, dummy = run_supervisorctl("add", name)
|
|
if f"{name}: added process group" in out:
|
|
module.exit_json(changed=True, name=name, state=state)
|
|
else:
|
|
module.fail_json(msg=out, name=name, state=state)
|
|
|
|
# from this point onwards, if there are no matching processes, module cannot go on.
|
|
if len(processes) == 0:
|
|
module.fail_json(name=name, msg="ERROR (no such process)")
|
|
|
|
if state == "started":
|
|
take_action_on_processes(processes, lambda s: s not in ("RUNNING", "STARTING"), "start", "started")
|
|
|
|
if state == "stopped":
|
|
take_action_on_processes(processes, lambda s: s in ("RUNNING", "STARTING"), "stop", "stopped")
|
|
|
|
if state == "signalled":
|
|
take_action_on_processes(processes, lambda s: s in ("RUNNING",), f"signal {signal}", "signalled")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|