mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
homebrew: Add support for services functions (#8329)
* Homebrew: Add support for services functions Fixes #8286. Add a homebrew.services module for starting and stopping services that are attached to homebrew packages. * Address python version compatibility * Addressing reviewer comments * Addressing sanity logs * Address str format issues * Fixing Python 2.7 syntax issues * Test alias, BOTMETA, grammar * Attempt to fix brew in tests * Address comments by russoz * Fixing more dumb typos * Actually uninstall black * Update version_added in plugins/modules/homebrew_services.py Co-authored-by: Felix Fontein <felix@fontein.de> --------- Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
parent
229ed6dad9
commit
2963004991
6 changed files with 394 additions and 0 deletions
5
.github/BOTMETA.yml
vendored
5
.github/BOTMETA.yml
vendored
|
@ -648,6 +648,11 @@ files:
|
|||
labels: homebrew_ macos
|
||||
maintainers: $team_macos
|
||||
notify: chris-short
|
||||
$modules/homebrew_services.py:
|
||||
ignore: ryansb
|
||||
keywords: brew cask services darwin homebrew macosx macports osx
|
||||
labels: homebrew_ macos
|
||||
maintainers: $team_macos kitizz
|
||||
$modules/homectl.py:
|
||||
maintainers: jameslivulpi
|
||||
$modules/honeybadger_deployment.py:
|
||||
|
|
|
@ -113,3 +113,30 @@ class HomebrewValidate(object):
|
|||
return isinstance(
|
||||
package, string_types
|
||||
) and not cls.INVALID_PACKAGE_REGEX.search(package)
|
||||
|
||||
|
||||
def parse_brew_path(module):
|
||||
# type: (...) -> str
|
||||
"""Attempt to find the Homebrew executable path.
|
||||
|
||||
Requires:
|
||||
- module has a `path` parameter
|
||||
- path is a valid path string for the target OS. Otherwise, module.fail_json()
|
||||
is called with msg="Invalid_path: <path>".
|
||||
"""
|
||||
path = module.params["path"]
|
||||
if not HomebrewValidate.valid_path(path):
|
||||
module.fail_json(msg="Invalid path: {0}".format(path))
|
||||
|
||||
if isinstance(path, string_types):
|
||||
paths = path.split(":")
|
||||
elif isinstance(path, list):
|
||||
paths = path
|
||||
else:
|
||||
module.fail_json(msg="Invalid path: {0}".format(path))
|
||||
|
||||
brew_path = module.get_bin_path("brew", required=True, opt_dirs=paths)
|
||||
if not HomebrewValidate.valid_brew_path(brew_path):
|
||||
module.fail_json(msg="Invalid brew path: {0}".format(brew_path))
|
||||
|
||||
return brew_path
|
||||
|
|
256
plugins/modules/homebrew_services.py
Normal file
256
plugins/modules/homebrew_services.py
Normal file
|
@ -0,0 +1,256 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2013, Andrew Dunham <andrew@du.nham.ca>
|
||||
# Copyright (c) 2013, Daniel Jaouen <dcj24@cornell.edu>
|
||||
# Copyright (c) 2015, Indrajit Raychaudhuri <irc+code@indrajit.com>
|
||||
# Copyright (c) 2024, Kit Ham <kitizz.devside@gmail.com>
|
||||
#
|
||||
# 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 absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = """
|
||||
---
|
||||
module: homebrew_services
|
||||
author:
|
||||
- "Kit Ham (@kitizz)"
|
||||
requirements:
|
||||
- homebrew must already be installed on the target system
|
||||
short_description: Services manager for Homebrew
|
||||
version_added: 9.3.0
|
||||
description:
|
||||
- Manages daemons and services via Homebrew.
|
||||
extends_documentation_fragment:
|
||||
- community.general.attributes
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: none
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- An installed homebrew package whose service is to be updated.
|
||||
aliases: [ 'formula' ]
|
||||
type: str
|
||||
required: true
|
||||
path:
|
||||
description:
|
||||
- "A V(:) separated list of paths to search for C(brew) executable.
|
||||
Since a package (I(formula) in homebrew parlance) location is prefixed relative to the actual path of C(brew) command,
|
||||
providing an alternative C(brew) path enables managing different set of packages in an alternative location in the system."
|
||||
default: '/usr/local/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin'
|
||||
type: path
|
||||
state:
|
||||
description:
|
||||
- State of the package's service.
|
||||
choices: [ 'present', 'absent', 'restarted' ]
|
||||
default: present
|
||||
type: str
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Install foo package
|
||||
community.general.homebrew:
|
||||
name: foo
|
||||
state: present
|
||||
|
||||
- name: Start the foo service (equivalent to `brew services start foo`)
|
||||
community.general.homebrew_service:
|
||||
name: foo
|
||||
state: present
|
||||
|
||||
- name: Restart the foo service (equivalent to `brew services restart foo`)
|
||||
community.general.homebrew_service:
|
||||
name: foo
|
||||
state: restarted
|
||||
|
||||
- name: Remove the foo service (equivalent to `brew services stop foo`)
|
||||
community.general.homebrew_service:
|
||||
name: foo
|
||||
service_state: absent
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
pid:
|
||||
description:
|
||||
- If the service is now running, this is the PID of the service, otherwise -1.
|
||||
returned: success
|
||||
type: int
|
||||
sample: 1234
|
||||
running:
|
||||
description:
|
||||
- Whether the service is running after running this command.
|
||||
returned: success
|
||||
type: bool
|
||||
sample: true
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.homebrew import (
|
||||
HomebrewValidate,
|
||||
parse_brew_path,
|
||||
)
|
||||
|
||||
if sys.version_info < (3, 5):
|
||||
from collections import namedtuple
|
||||
|
||||
# Stores validated arguments for an instance of an action.
|
||||
# See DOCUMENTATION string for argument-specific information.
|
||||
HomebrewServiceArgs = namedtuple(
|
||||
"HomebrewServiceArgs", ["name", "state", "brew_path"]
|
||||
)
|
||||
|
||||
# Stores the state of a Homebrew service.
|
||||
HomebrewServiceState = namedtuple("HomebrewServiceState", ["running", "pid"])
|
||||
|
||||
else:
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
# Stores validated arguments for an instance of an action.
|
||||
# See DOCUMENTATION string for argument-specific information.
|
||||
HomebrewServiceArgs = NamedTuple(
|
||||
"HomebrewServiceArgs", [("name", str), ("state", str), ("brew_path", str)]
|
||||
)
|
||||
|
||||
# Stores the state of a Homebrew service.
|
||||
HomebrewServiceState = NamedTuple(
|
||||
"HomebrewServiceState", [("running", bool), ("pid", Optional[int])]
|
||||
)
|
||||
|
||||
|
||||
def _brew_service_state(args, module):
|
||||
# type: (HomebrewServiceArgs, AnsibleModule) -> HomebrewServiceState
|
||||
cmd = [args.brew_path, "services", "info", args.name, "--json"]
|
||||
rc, stdout, stderr = module.run_command(cmd, check_rc=True)
|
||||
|
||||
try:
|
||||
data = json.loads(stdout)[0]
|
||||
except json.JSONDecodeError:
|
||||
module.fail_json(msg="Failed to parse JSON output:\n{0}".format(stdout))
|
||||
|
||||
return HomebrewServiceState(running=data["status"] == "started", pid=data["pid"])
|
||||
|
||||
|
||||
def _exit_with_state(args, module, changed=False, message=None):
|
||||
# type: (HomebrewServiceArgs, AnsibleModule, bool, Optional[str]) -> None
|
||||
state = _brew_service_state(args, module)
|
||||
if message is None:
|
||||
message = (
|
||||
"Running: {state.running}, Changed: {changed}, PID: {state.pid}".format(
|
||||
state=state, changed=changed
|
||||
)
|
||||
)
|
||||
module.exit_json(msg=message, pid=state.pid, running=state.running, changed=changed)
|
||||
|
||||
|
||||
def validate_and_load_arguments(module):
|
||||
# type: (AnsibleModule) -> HomebrewServiceArgs
|
||||
"""Reuse the Homebrew module's validation logic to validate these arguments."""
|
||||
package = module.params["name"] # type: ignore
|
||||
if not HomebrewValidate.valid_package(package):
|
||||
module.fail_json(msg="Invalid package name: {0}".format(package))
|
||||
|
||||
state = module.params["state"] # type: ignore
|
||||
if state not in ["present", "absent", "restarted"]:
|
||||
module.fail_json(msg="Invalid state: {0}".format(state))
|
||||
|
||||
brew_path = parse_brew_path(module)
|
||||
|
||||
return HomebrewServiceArgs(name=package, state=state, brew_path=brew_path)
|
||||
|
||||
|
||||
def start_service(args, module):
|
||||
# type: (HomebrewServiceArgs, AnsibleModule) -> None
|
||||
"""Start the requested brew service if it is not already running."""
|
||||
state = _brew_service_state(args, module)
|
||||
if state.running:
|
||||
# Nothing to do, return early.
|
||||
_exit_with_state(args, module, changed=False, message="Service already running")
|
||||
|
||||
if module.check_mode:
|
||||
_exit_with_state(args, module, changed=True, message="Service would be started")
|
||||
|
||||
start_cmd = [args.brew_path, "services", "start", args.name]
|
||||
rc, stdout, stderr = module.run_command(start_cmd, check_rc=True)
|
||||
|
||||
_exit_with_state(args, module, changed=True)
|
||||
|
||||
|
||||
def stop_service(args, module):
|
||||
# type: (HomebrewServiceArgs, AnsibleModule) -> None
|
||||
"""Stop the requested brew service if it is running."""
|
||||
state = _brew_service_state(args, module)
|
||||
if not state.running:
|
||||
# Nothing to do, return early.
|
||||
_exit_with_state(args, module, changed=False, message="Service already stopped")
|
||||
|
||||
if module.check_mode:
|
||||
_exit_with_state(args, module, changed=True, message="Service would be stopped")
|
||||
|
||||
stop_cmd = [args.brew_path, "services", "stop", args.name]
|
||||
rc, stdout, stderr = module.run_command(stop_cmd, check_rc=True)
|
||||
|
||||
_exit_with_state(args, module, changed=True)
|
||||
|
||||
|
||||
def restart_service(args, module):
|
||||
# type: (HomebrewServiceArgs, AnsibleModule) -> None
|
||||
"""Restart the requested brew service. This always results in a change."""
|
||||
if module.check_mode:
|
||||
_exit_with_state(
|
||||
args, module, changed=True, message="Service would be restarted"
|
||||
)
|
||||
|
||||
restart_cmd = [args.brew_path, "services", "restart", args.name]
|
||||
rc, stdout, stderr = module.run_command(restart_cmd, check_rc=True)
|
||||
|
||||
_exit_with_state(args, module, changed=True)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
name=dict(
|
||||
aliases=["formula"],
|
||||
required=True,
|
||||
type="str",
|
||||
),
|
||||
state=dict(
|
||||
choices=["present", "absent", "restarted"],
|
||||
default="present",
|
||||
),
|
||||
path=dict(
|
||||
default="/usr/local/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin",
|
||||
type="path",
|
||||
),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
module.run_command_environ_update = dict(
|
||||
LANG="C", LC_ALL="C", LC_MESSAGES="C", LC_CTYPE="C"
|
||||
)
|
||||
|
||||
# Pre-validate arguments.
|
||||
service_args = validate_and_load_arguments(module)
|
||||
|
||||
# Choose logic based on the desired state.
|
||||
if service_args.state == "present":
|
||||
start_service(service_args, module)
|
||||
elif service_args.state == "absent":
|
||||
stop_service(service_args, module)
|
||||
elif service_args.state == "restarted":
|
||||
restart_service(service_args, module)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
9
tests/integration/targets/homebrew_services/aliases
Normal file
9
tests/integration/targets/homebrew_services/aliases
Normal file
|
@ -0,0 +1,9 @@
|
|||
# Copyright (c) Ansible Project
|
||||
# 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
|
||||
|
||||
azp/posix/1
|
||||
skip/aix
|
||||
skip/freebsd
|
||||
skip/rhel
|
||||
skip/docker
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
# Copyright (c) Ansible Project
|
||||
# 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
|
||||
|
||||
- name: uninstall black
|
||||
community.general.homebrew:
|
||||
name: black
|
||||
state: absent
|
||||
become: true
|
||||
become_user: "{{ brew_stat.stat.pw_name }}"
|
86
tests/integration/targets/homebrew_services/tasks/main.yml
Normal file
86
tests/integration/targets/homebrew_services/tasks/main.yml
Normal file
|
@ -0,0 +1,86 @@
|
|||
---
|
||||
# Copyright (c) Ansible Project
|
||||
# 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
|
||||
|
||||
# Don't run this test for non-MacOS systems.
|
||||
- meta: end_play
|
||||
when: ansible_facts.distribution != 'MacOSX'
|
||||
|
||||
- name: MACOS | Find brew binary
|
||||
command: which brew
|
||||
register: brew_which
|
||||
|
||||
- name: MACOS | Get owner of brew binary
|
||||
stat:
|
||||
path: "{{ brew_which.stdout }}"
|
||||
register: brew_stat
|
||||
|
||||
- name: Homebrew Services test block
|
||||
become: true
|
||||
become_user: "{{ brew_stat.stat.pw_name }}"
|
||||
block:
|
||||
- name: MACOS | Install black
|
||||
community.general.homebrew:
|
||||
name: black
|
||||
state: present
|
||||
register: install_result
|
||||
notify:
|
||||
- uninstall black
|
||||
|
||||
- name: Check the black service is installed
|
||||
assert:
|
||||
that:
|
||||
- install_result is success
|
||||
|
||||
- name: Start the black service
|
||||
community.general.homebrew_services:
|
||||
name: black
|
||||
state: present
|
||||
register: start_result
|
||||
environment:
|
||||
HOMEBREW_NO_ENV_HINTS: "1"
|
||||
|
||||
- name: Check the black service is running
|
||||
assert:
|
||||
that:
|
||||
- start_result is success
|
||||
|
||||
- name: Start the black service when already started
|
||||
community.general.homebrew_services:
|
||||
name: black
|
||||
state: present
|
||||
register: start_result
|
||||
environment:
|
||||
HOMEBREW_NO_ENV_HINTS: "1"
|
||||
|
||||
- name: Check for idempotency
|
||||
assert:
|
||||
that:
|
||||
- start_result.changed == 0
|
||||
|
||||
- name: Restart the black service
|
||||
community.general.homebrew_services:
|
||||
name: black
|
||||
state: restarted
|
||||
register: restart_result
|
||||
environment:
|
||||
HOMEBREW_NO_ENV_HINTS: "1"
|
||||
|
||||
- name: Check the black service is restarted
|
||||
assert:
|
||||
that:
|
||||
- restart_result is success
|
||||
|
||||
- name: Stop the black service
|
||||
community.general.homebrew_services:
|
||||
name: black
|
||||
state: present
|
||||
register: stop_result
|
||||
environment:
|
||||
HOMEBREW_NO_ENV_HINTS: "1"
|
||||
|
||||
- name: Check the black service is stopped
|
||||
assert:
|
||||
that:
|
||||
- stop_result is success
|
Loading…
Reference in a new issue