diff --git a/lib/ansible/modules/web_infrastructure/gunicorn.py b/lib/ansible/modules/web_infrastructure/gunicorn.py new file mode 100644 index 00000000000..ee85428ed79 --- /dev/null +++ b/lib/ansible/modules/web_infrastructure/gunicorn.py @@ -0,0 +1,237 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Alejandro Gomez +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +ANSIBLE_METADATA = {'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: gunicorn +version_added: "2.4" +short_description: Run gunicorn with various settings. +description: + - Starts gunicorn with the parameters specified. Common settings for gunicorn + configuration are supported. For additional configuration use a config file + See U(https://gunicorn-docs.readthedocs.io/en/latest/settings.html) for more + options. It's recommended to always use the chdir option to avoid problems + with the location of the app. +requirements: [gunicorn] +author: + - "Alejandro Gomez (@agmezr)" +options: + app: + required: true + aliases: ['name'] + description: + - The app module. A name refers to a WSGI callable that should be found in the specified module. + venv: + aliases: ['virtualenv'] + description: + - 'Path to the virtualenv directory.' + config: + description: + - 'Path to the gunicorn configuration file.' + chdir: + description: + - 'Chdir to specified directory before apps loading.' + pid: + description: + - 'A filename to use for the PID file. If not set and not found on the configuration file a tmp + pid file will be created to check a successful run of gunicorn.' + worker: + choices: ['sync', 'eventlet', 'gevent', 'tornado ', 'gthread', 'gaiohttp'] + description: + - 'The type of workers to use. The default class (sync) should handle most “normal” types of workloads.' + user: + description: + - 'Switch worker processes to run as this user.' +notes: + - If not specified on config file, a temporary error log will be created on /tmp dir. + Please make sure you have write access in /tmp dir. Not needed but will help you to + identify any problem with configuration. +''' + +EXAMPLES = ''' +- name: simple gunicorn run example + gunicorn: + app: 'wsgi' + chdir: '/workspace/example' + +- name: run gunicorn on a virtualenv + gunicorn: + app: 'wsgi' + chdir: '/workspace/example' + venv: '/workspace/example/venv' + +- name: run gunicorn with a config file + gunicorn: + app: 'wsgi' + chdir: '/workspace/example' + conf: '/workspace/example/gunicorn.cfg' + +- name: run gunicorn as ansible user with specified pid and config file + gunicorn: + app: 'wsgi' + chdir: '/workspace/example' + conf: '/workspace/example/gunicorn.cfg' + venv: '/workspace/example/venv' + pid: '/workspace/example/gunicorn.pid' + user: 'ansible' +''' + +RETURN = ''' +gunicorn: + description: process id of gunicorn + returned: changed + type: string + sample: "1234" +''' + +import os +import time + +# import ansible utils +from ansible.module_utils.basic import AnsibleModule + + +def search_existing_config(config, option): + ''' search in config file for specified option ''' + if config and os.path.isfile(config): + data_config = None + with open(config, 'r') as f: + for line in f: + if option in line: + return line + return None + + +def remove_tmp_file(file_path): + ''' remove temporary files ''' + if os.path.isfile(file_path): + os.remove(file_path) + + +def main(): + + # available gunicorn options on module + gunicorn_options = { + 'config': '-c', + 'chdir': '--chdir', + 'worker': '-k', + 'user': '-u', + } + + # temporary files in case no option provided + tmp_error_log = '/tmp/gunicorn.temp.error.log' + tmp_pid_file = '/tmp/gunicorn.temp.pid' + + # remove temp file if exists + remove_tmp_file(tmp_pid_file) + remove_tmp_file(tmp_error_log) + + module = AnsibleModule( + argument_spec=dict( + app=dict(required=True, type='str', aliases=['name']), + venv=dict(required=False, type='path', default=None, aliases=['virtualenv']), + config=dict(required=False, default=None, type='path', aliases=['conf']), + chdir=dict(required=False, type='path', default=None), + pid=dict(required=False, type='path', default=None), + user=dict(required=False, type='str'), + worker=dict(required=False, + type='str', + choices=['sync', 'eventlet', 'gevent', 'tornado ', 'gthread', 'gaiohttp'] + ), + ) + ) + + # obtain app name and venv + params = module.params + app = params['app'] + venv = params['venv'] + pid = params['pid'] + + # use venv path if exists + if venv: + gunicorn_command = "/".join((venv, 'bin', 'gunicorn')) + else: + gunicorn_command = 'gunicorn' + + # to daemonize the process + options = ["-D"] + + # fill options + for option in gunicorn_options: + param = params[option] + if param: + options.append(gunicorn_options[option]) + options.append(param) + + error_log = search_existing_config(params['config'], 'errorlog') + if not error_log: + # place error log somewhere in case of fail + options.append("--error-logfile") + options.append(tmp_error_log) + + pid_file = search_existing_config(params['config'], 'pid') + if not params['pid'] and not pid_file: + pid = tmp_pid_file + + # add option for pid file if not found on config file + if not pid_file: + options.append('--pid') + options.append(pid) + + # put args together + args = [gunicorn_command] + options + [app] + rc, out, err = module.run_command(args, use_unsafe_shell=False, encoding=None) + + if not err: + # wait for gunicorn to dump to log + time.sleep(0.5) + if os.path.isfile(pid): + with open(pid, 'r') as f: + result = f.readline().strip() + + if not params['pid']: + os.remove(pid) + + module.exit_json(changed=True, pid=result, debug=" ".join(args)) + else: + # if user defined own error log, check that + if error_log: + error = 'Please check your {0}'.format(error_log.strip()) + else: + if os.path.isfile(tmp_error_log): + with open(tmp_error_log, 'r') as f: + error = f.read() + # delete tmp log + os.remove(tmp_error_log) + else: + error = "Log not found" + + module.fail_json(msg='Failed to start gunicorn. {0}'.format(error), error=err) + + else: + module.fail_json(msg='Failed to start gunicorn {0}'.format(err), error=err) + +if __name__ == '__main__': + main()