Skip to content

Instantly share code, notes, and snippets.

@aikomastboom
Last active July 21, 2019 22:10
Show Gist options
  • Save aikomastboom/8859290 to your computer and use it in GitHub Desktop.
Save aikomastboom/8859290 to your computer and use it in GitHub Desktop.
#!/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()
@gamename
Copy link

I'd like to use this as a basis for a pm2 library in ansible. Any objection?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment