Last active
July 21, 2019 22:10
-
-
Save aikomastboom/8859290 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python | |
# -*- coding: utf-8 -*- | |
DOCUMENTATION = """ | |
--- | |
author: Aiko Mastboom | |
module: pm2.py | |
short_description: nodeJS pm2 wrapper | |
description: | |
- This module helps managing processes using pm2 on hosts | |
options: | |
command: | |
required: true | |
choices: [ start, stop, restart ] | |
description: | |
- C(started)/C(stopped) are idempotent actions that will not run | |
commands unless necessary. C(restarted) will always bounce the | |
service. | |
persist: | |
required: false | |
default: false | |
description: | |
- C(persist) will always dump the current pm2 state | |
json: | |
description: | |
- The processes to manage. | |
default: processes.json | |
required: false | |
use_nave: | |
description: | |
- Is nave used to select node version https://github.com/isaacs/nave | |
default: false | |
required: false | |
node_version: | |
description: | |
- version of node nave has installed | |
default: None | |
required: only when use_nave = true | |
executable: | |
description: | |
- path to pm2 executable (useful when pm2 is not on the path or something) | |
default: pm2 | |
required: false | |
""" | |
EXAMPLES = ''' | |
# Example playbook | |
- local_action: pm2 start | |
- action: pm2.py command=start use_nave=true node_version=0.10.21 json=bower-registry.json | |
''' | |
import os | |
import tempfile | |
import subprocess | |
def run_command(module, args, check_rc=False, close_fds=False, executable=None, | |
data=None, binary_data=False, path_prefix=None, cwd=None): | |
''' | |
Execute a command, returns rc, stdout, and stderr. | |
args is the command to run | |
If args is a list, the command will be run with shell=False. | |
Otherwise, the command will be run with shell=True when args is a string. | |
Other arguments: | |
- check_rc (boolean) Whether to call fail_json in case of | |
non zero RC. Default is False. | |
- close_fds (boolean) See documentation for subprocess.Popen(). | |
Default is False. | |
- executable (string) See documentation for subprocess.Popen(). | |
Default is None. | |
''' | |
if isinstance(args, list): | |
shell = False | |
elif isinstance(args, basestring): | |
shell = True | |
else: | |
msg = "Argument 'args' to run_command must be list or string" | |
module.fail_json(rc=257, cmd=args, msg=msg) | |
rc = 0 | |
msg = None | |
st_in = None | |
# Set a temporary env path if a prefix is passed | |
env = os.environ | |
if path_prefix: | |
env['PATH'] = "%s:%s" % (path_prefix, env['PATH']) | |
if data: | |
st_in = subprocess.PIPE | |
try: | |
if path_prefix is not None: | |
cmd = subprocess.Popen(args, | |
executable=executable, | |
shell=shell, | |
close_fds=close_fds, | |
stdin=st_in, | |
stdout=subprocess.PIPE, | |
stderr=subprocess.PIPE, | |
cwd=cwd, | |
env=env) | |
else: | |
cmd = subprocess.Popen(args, | |
executable=executable, | |
shell=shell, | |
close_fds=close_fds, | |
stdin=st_in, | |
stdout=subprocess.PIPE, | |
stderr=subprocess.PIPE, | |
cwd=cwd) | |
if data: | |
if not binary_data: | |
data += '\\n' | |
out, err = cmd.communicate(input=data) | |
rc = cmd.returncode | |
except (OSError, IOError), e: | |
module.fail_json(rc=e.errno, msg=str(e), cmd=args) | |
except: | |
module.fail_json(rc=257, msg=traceback.format_exc(), cmd=args) | |
if rc != 0 and check_rc: | |
msg = err.rstrip() | |
module.fail_json(cmd=args, rc=rc, stdout=out, stderr=err, msg=msg) | |
return (rc, out, err) | |
def main(): | |
default_pm2 = 'pm2' | |
module = AnsibleModule( | |
argument_spec=dict( | |
command=dict(required=True, | |
choices=['start', 'stop', 'restart', 'dump']), | |
executable=dict(required=False, default=default_pm2), | |
persist=dict(required=False, choices=BOOLEANS), | |
use_nave=dict(required=False, choices=BOOLEANS), | |
node_version=dict(required=False, default=None), | |
json=dict(required=False, default=None) | |
), | |
supports_check_mode=False | |
) | |
command = module.params.get('command') | |
executable = module.params.get('executable') | |
persist = module.params.get('persist') | |
use_nave = module.params.get('use_nave') | |
node_version = module.params.get('node_version') | |
json = module.params.get('json') | |
if use_nave: | |
if not node_version: | |
return module.fail_json(msg='node_version required when using nave') | |
if module.check_mode: | |
def check_if_system_state_would_be_changed(): | |
return False | |
# Check if any changes would be made by don't actually make those changes | |
module.exit_json(changed=check_if_system_state_would_be_changed()) | |
env = os.environ | |
path_prefix = '' | |
if executable != default_pm2: | |
# assuming node to be in same folder as pm2 | |
path_prefix = os.path.dirname(executable) | |
if use_nave: | |
path_prefix = env['HOME'] + '/.nave/installed/' + node_version + '/bin' | |
# TODO AM: duplicated line, remove when https://github.com/Unitech/pm2/issues/281 is fixed and released. | |
run_command(module, | |
[executable, 'jlist', '-s'], | |
path_prefix=path_prefix) | |
rc, json_out, err = run_command(module, | |
[executable, 'jlist', '-s'], | |
path_prefix=path_prefix) | |
jlist_out = module.from_json(json_out) | |
filename = 'processes.json' | |
processes = [] | |
if not os.path.exists(filename): | |
filename = '' | |
if json and os.path.exists(json): | |
filename = json | |
if filename: | |
infile = open(filename, 'rb') | |
block = infile.read() | |
infile.close() | |
processes = module.from_json(block) | |
status = {} | |
changed = False | |
commands = [] | |
rcs = [] | |
outs = [] | |
errs = [] | |
started = [] | |
if isinstance(processes, dict): | |
processes = [processes] | |
for proc in processes: | |
if 'name' in proc: | |
name = proc['name'] | |
status[name] = {} | |
status[name]['process'] = proc | |
for running in jlist_out: | |
if name == running['name']: | |
status[name]['status'] = running['pm2_env']['status'] | |
status[name]['pm_id'] = running['pm_id'] | |
if not 'status' in status[name]: | |
status[name]['status'] = 'missing' | |
if command == 'start': | |
if status[name]['status'] == 'missing': | |
tmp_fd, tmp_file_path = tempfile.mkstemp(suffix='.json') | |
file = os.fdopen(tmp_fd, 'w') | |
file.write(module.jsonify(status[name]['process'])) | |
file.close() | |
args = [executable, 'start', tmp_file_path, '-m'] | |
cwd = os.path.dirname(json) or env['PWD'] | |
rc, out, err = run_command(module, args, path_prefix=path_prefix, cwd=cwd) | |
os.unlink(tmp_file_path) | |
if rc == 0: | |
changed = True | |
else: | |
return module.fail_json(msg=command + ' '.join(args) + ' failed ' + str(rc)) | |
commands.append(args) | |
started.append(status[name]['process']) | |
rcs.append(rc) | |
outs.append(out) | |
errs.append(err) | |
elif 'stopped' == status[name]['status']: | |
args = [executable, 'restart', str(status[name]['pm_id']), '-m'] | |
rc, out, err = run_command(module, args, path_prefix=path_prefix) | |
if rc == 0: | |
changed = True | |
else: | |
return module.fail_json(msg='starting ' + ' '.join(args) + ' failed ' + str(rc)) | |
commands.append(args) | |
rcs.append(rc) | |
outs.append(out) | |
errs.append(err) | |
if command == 'restart': | |
if 'missing' == status[name]['status']: | |
tmp_fd, tmp_file_path = tempfile.mkstemp(suffix='.json') | |
file = os.fdopen(tmp_fd, 'w') | |
file.write(module.jsonify(status[name]['process'])) | |
file.close() | |
args = [executable, 'start', tmp_file_path, '-m'] | |
cwd = os.path.dirname(json) or env['PWD'] | |
rc, out, err = run_command(module, args, path_prefix=path_prefix, cwd=cwd) | |
os.unlink(tmp_file_path) | |
if rc == 0: | |
changed = True | |
else: | |
return module.fail_json(msg=command + ' '.join(args) + ' failed ' + str(rc)) | |
commands.append(args) | |
started.append(status[name]['process']) | |
rcs.append(rc) | |
outs.append(out) | |
errs.append(err) | |
elif 'stopped' == status[name]['status'] or \ | |
'online' == status[name]['status']: | |
args = [executable, 'restart', str(status[name]['pm_id']), '-m'] | |
rc, out, err = run_command(module, args, path_prefix=path_prefix) | |
if rc == 0: | |
changed = True | |
else: | |
return module.fail_json(msg=command + ' '.join(args) + ' failed ' + str(rc)) | |
commands.append(args) | |
rcs.append(rc) | |
outs.append(out) | |
errs.append(err) | |
if command == 'stop': | |
if status[name]['status'] == 'online': | |
args = [executable, 'stop', str(status[name]['pm_id']), '-m'] | |
rc, out, err = run_command(module, args, path_prefix=path_prefix) | |
if rc == 0: | |
changed = True | |
else: | |
return module.fail_json(msg=command + ' '.join(args) + ' failed ' + str(rc)) | |
commands.append(args) | |
rcs.append(rc) | |
outs.append(out) | |
errs.append(err) | |
if persist: | |
args = [executable, 'dump'] | |
rc, out, err = run_command(module, args, path_prefix=path_prefix) | |
commands.append(args) | |
rcs.append(rc) | |
outs.append(out) | |
errs.append(err) | |
module.exit_json(changed=changed, commands=commands, started=started, | |
rcs=rcs, outs=outs, errs=errs) | |
from ansible.module_utils.basic import * | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I'd like to use this as a basis for a pm2 library in ansible. Any objection?