2024-02-02 23:39:28 +01:00
|
|
|
#!/usr/bin/python
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
2024-02-03 02:53:17 +01:00
|
|
|
# Copyright (c) 2024, Ralf Langebrake <ralf@langebrake.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
|
2024-02-02 23:39:28 +01:00
|
|
|
|
|
|
|
__metaclass__ = type
|
|
|
|
|
|
|
|
DOCUMENTATION = r'''
|
|
|
|
---
|
|
|
|
module: artisan
|
|
|
|
|
|
|
|
short_description: Laravel command line interface
|
|
|
|
|
2024-02-11 15:05:28 +01:00
|
|
|
version_added: 8.4.0
|
|
|
|
|
2024-02-02 23:39:28 +01:00
|
|
|
description:
|
|
|
|
- >
|
2024-02-03 02:53:17 +01:00
|
|
|
Artisan is the command line interface included with Laravel, the
|
|
|
|
PHP web application framework for artisans. The module was heavily
|
2024-02-11 15:05:50 +01:00
|
|
|
inspired by M(community.general.composer), which is typically used together with artisan.
|
2024-02-02 23:39:28 +01:00
|
|
|
|
|
|
|
extends_documentation_fragment:
|
|
|
|
- community.general.attributes
|
|
|
|
|
|
|
|
attributes:
|
|
|
|
check_mode:
|
|
|
|
support: full
|
|
|
|
diff_mode:
|
|
|
|
support: none
|
|
|
|
|
|
|
|
options:
|
|
|
|
working_dir:
|
|
|
|
description:
|
|
|
|
- Directory of your project that contains the Artisan executable.
|
|
|
|
required: true
|
|
|
|
type: path
|
|
|
|
command:
|
|
|
|
description:
|
|
|
|
- Artisan command like "migrate", "clear-compiled" or "app:custom-command".
|
|
|
|
required: true
|
|
|
|
type: str
|
|
|
|
options:
|
|
|
|
description:
|
|
|
|
- Command options like seed or force without leading hyphens.
|
|
|
|
required: false
|
2024-02-03 02:53:17 +01:00
|
|
|
elements: str
|
|
|
|
default: []
|
2024-02-02 23:39:28 +01:00
|
|
|
type: list
|
|
|
|
args:
|
|
|
|
description:
|
|
|
|
- Command arguments, mainly in custom commands.
|
|
|
|
required: false
|
2024-02-03 02:53:17 +01:00
|
|
|
elements: str
|
|
|
|
default: []
|
2024-02-02 23:39:28 +01:00
|
|
|
type: list
|
|
|
|
php_path:
|
|
|
|
description:
|
|
|
|
- Path to the PHP executable on the remote host if PHP is not included in PATH.
|
|
|
|
required: false
|
|
|
|
type: path
|
|
|
|
|
|
|
|
requirements:
|
|
|
|
- Laravel
|
|
|
|
- PHP
|
|
|
|
|
|
|
|
notes:
|
|
|
|
- Always appended in each execution are --no-ansi, --no-interaction and --force if available.
|
|
|
|
- Ansible Artisan is intended primarily, but not exclusively, for production deployments
|
|
|
|
|
|
|
|
author:
|
|
|
|
- Ralf Langebrake (@codebarista)
|
|
|
|
'''
|
|
|
|
|
|
|
|
EXAMPLES = r'''
|
|
|
|
# php artisan down --secret="t0pS3cRet" --refresh=60 --retry=60 --no-ansi --no-interaction
|
|
|
|
- name: Put the application into maintenance / demo mode
|
|
|
|
community.general.artisan:
|
|
|
|
working_dir: "{{ project_src }}"
|
|
|
|
command: down
|
|
|
|
options:
|
|
|
|
- secret="ToPs3cReT"
|
|
|
|
- refresh=60
|
|
|
|
- retry=60
|
|
|
|
|
|
|
|
# php artisan storage:link --no-ansi --no-interaction --force
|
|
|
|
- name: Connect the public/storage link to storage/app/public
|
|
|
|
community.general.artisan:
|
|
|
|
working_dir: "{{ project_src }}"
|
|
|
|
command: storage:link
|
|
|
|
|
|
|
|
# php artisan app:custom-command --no-ansi --no-interaction maybe/a/path
|
|
|
|
- name: Run custom command with arguments
|
|
|
|
community.general.artisan:
|
|
|
|
working_dir: "{{ project_src }}"
|
|
|
|
php_path: /usr/local/bin/php
|
|
|
|
command: app:custom-command
|
|
|
|
arguments: maybe/a/path
|
|
|
|
changed_when: "'modified' in app_custom_command.stdout"
|
|
|
|
register: app_custom_command
|
|
|
|
|
|
|
|
# php artisan migrate --database=sqlite --isolated=true --seed --force
|
|
|
|
- name: Run migrations and seed database
|
|
|
|
community.general.artisan:
|
|
|
|
working_dir: "{{ project_src }}"
|
|
|
|
command: "{{ item }}"
|
|
|
|
options:
|
|
|
|
- database=sqlite
|
|
|
|
- isolated=true
|
|
|
|
loop:
|
|
|
|
- migrate
|
|
|
|
- seed
|
|
|
|
|
|
|
|
# php artisan up --no-ansi --no-interaction
|
|
|
|
- name: Clear caches and bring the application out of maintenance mode
|
|
|
|
community.general.artisan:
|
|
|
|
working_dir: "{{ project_src }}"
|
|
|
|
command: "{{ item }}"
|
|
|
|
loop:
|
|
|
|
- cache:clear
|
|
|
|
- event:clear
|
|
|
|
- route:clear
|
|
|
|
- view:clear
|
|
|
|
- up
|
|
|
|
'''
|
|
|
|
|
2024-02-03 02:53:17 +01:00
|
|
|
import os
|
|
|
|
import re
|
|
|
|
|
|
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
|
|
|
2024-02-02 23:39:28 +01:00
|
|
|
|
|
|
|
def parse(out):
|
|
|
|
# remove any unicode strings
|
|
|
|
pattern = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]')
|
|
|
|
|
|
|
|
# split and trim lines
|
|
|
|
lines = list(filter(None, out.splitlines()))
|
|
|
|
message = stderr = lines[0]
|
|
|
|
|
|
|
|
# second line has the exception message
|
|
|
|
if len(lines) > 1:
|
|
|
|
stderr = lines[1]
|
|
|
|
|
|
|
|
# message, stderr, stderr_lines
|
|
|
|
return [
|
|
|
|
re.sub(pattern, '', message).strip(),
|
|
|
|
re.sub(pattern, '', stderr).strip(),
|
|
|
|
list(map(str.strip, lines))
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def has_changed(message):
|
|
|
|
# message fragments that Laravel gives us
|
|
|
|
artisan_messages = [
|
|
|
|
'No publishable', # resources for tag
|
|
|
|
'Nothing to', # migrate,...
|
|
|
|
'already', # up, down, exists,...
|
|
|
|
'Not modified',
|
|
|
|
'No changes'
|
|
|
|
]
|
|
|
|
|
|
|
|
for info in artisan_messages:
|
|
|
|
if info in message:
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def get_help(module, command):
|
|
|
|
# get all available options and arguments from an artisan command using artisan help to json
|
|
|
|
rc, out, err = artisan_command(module, 'help %s' % command, options='--format=json --no-ansi')
|
|
|
|
|
|
|
|
# no command, no help, no json
|
|
|
|
if rc != 0:
|
|
|
|
module.fail_json(msg='%s is not part of the command definitions.' % command)
|
|
|
|
|
|
|
|
return module.from_json(out)
|
|
|
|
|
|
|
|
|
|
|
|
def get_options(module, command_help):
|
|
|
|
# set available command options
|
|
|
|
available_options = command_help['definition']['options']
|
|
|
|
|
|
|
|
# merge default options
|
|
|
|
command_options = module.params['options'] + [
|
|
|
|
'no-interaction',
|
|
|
|
'no-ansi',
|
|
|
|
'force',
|
|
|
|
]
|
|
|
|
|
|
|
|
options = []
|
|
|
|
|
|
|
|
# check availability and format options
|
|
|
|
for option in command_options:
|
|
|
|
option = list(map(str.strip, option.lstrip('-').split('=')))
|
|
|
|
if option[0] in available_options:
|
|
|
|
options.append('--%s' % '='.join(option))
|
|
|
|
|
|
|
|
# return the unique options turnkey ready as a string
|
|
|
|
return ' '.join(list(set(options))).strip()
|
|
|
|
|
|
|
|
|
|
|
|
def get_arguments(module, command_help):
|
|
|
|
# set available command arguments
|
|
|
|
available_arguments = command_help['definition']['arguments']
|
|
|
|
|
|
|
|
# mostly we have no arguments
|
|
|
|
arguments = ''
|
|
|
|
|
|
|
|
# if arguments expected for command, join from params
|
|
|
|
if len(available_arguments) > 0:
|
|
|
|
arguments = ' '.join(module.params['args'])
|
|
|
|
|
|
|
|
# return arguments turnkey ready as a string
|
|
|
|
return arguments.strip()
|
|
|
|
|
|
|
|
|
|
|
|
def get_artisan(module):
|
|
|
|
artisan = '%s/artisan' % os.path.abspath(module.params['working_dir'])
|
|
|
|
|
|
|
|
# check artisan executable path
|
|
|
|
if not os.path.isfile(artisan):
|
|
|
|
module.fail_json(msg='The artisan executable is not present.', artisan=artisan)
|
|
|
|
|
|
|
|
return artisan
|
|
|
|
|
|
|
|
|
|
|
|
def get_php(module):
|
|
|
|
# in most cases, PHP is available by default
|
|
|
|
if module.params['php_path'] is None:
|
|
|
|
php = module.get_bin_path('php', True, ['/usr/local/bin'])
|
|
|
|
else:
|
|
|
|
php = os.path.abspath(module.params['php_path'])
|
|
|
|
|
|
|
|
# check php executable path
|
|
|
|
if not os.path.isfile(php):
|
|
|
|
module.fail_json(msg='The php executable is not present.', php=php)
|
|
|
|
|
|
|
|
return php
|
|
|
|
|
|
|
|
|
|
|
|
def artisan_command(module, command, options, arguments=''):
|
|
|
|
# this is the artisan executable in the project root
|
|
|
|
artisan = get_artisan(module)
|
|
|
|
|
|
|
|
# PHP is essential for successful execution
|
|
|
|
php = get_php(module)
|
|
|
|
|
|
|
|
# combine the entire command
|
|
|
|
command = '%s %s %s %s %s' % (php, artisan, command, options, arguments)
|
|
|
|
|
|
|
|
return module.run_command(command.strip())
|
|
|
|
|
|
|
|
|
|
|
|
def run_module():
|
|
|
|
# available arguments a user can pass to the module
|
|
|
|
module_args = dict(
|
|
|
|
working_dir=dict(type='path', required=True),
|
|
|
|
command=dict(type='str', required=True),
|
|
|
|
php_path=dict(type='path'),
|
2024-02-03 03:07:58 +01:00
|
|
|
options=dict(type='list', elements='str', default=[]),
|
|
|
|
args=dict(type='list', elements='str', default=[]),
|
2024-02-02 23:39:28 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
# instantiate the AnsibleModule object
|
|
|
|
module = AnsibleModule(
|
|
|
|
argument_spec=module_args,
|
|
|
|
supports_check_mode=True
|
|
|
|
)
|
|
|
|
|
|
|
|
# define the artisan command
|
|
|
|
cmd = module.params['command'].strip()
|
|
|
|
|
|
|
|
# get command info from help
|
|
|
|
hlp = get_help(module, cmd)
|
|
|
|
|
|
|
|
# merge default and param options
|
|
|
|
opts = get_options(module, hlp)
|
|
|
|
|
|
|
|
# merge param arguments
|
|
|
|
args = get_arguments(module, hlp)
|
|
|
|
|
|
|
|
# seed the result dict in the object
|
|
|
|
result = dict(
|
|
|
|
stdout='%s %s %s' % (cmd, opts, args),
|
|
|
|
artisan=get_artisan(module),
|
|
|
|
msg='Running check mode',
|
|
|
|
changed=False,
|
|
|
|
)
|
|
|
|
|
|
|
|
# in check mode just return the current state
|
|
|
|
if module.check_mode:
|
|
|
|
module.exit_json(**result)
|
|
|
|
|
|
|
|
# do what it needs to do
|
|
|
|
rc, out, err = artisan_command(module, cmd, opts, args)
|
|
|
|
|
|
|
|
# try to extract the most meaningful output possible
|
|
|
|
message, stderr, lines = parse(out)
|
|
|
|
|
|
|
|
# set first stdout line as message
|
|
|
|
result['msg'] = message.replace('INFO', '').strip()
|
|
|
|
result['stdout'] = message
|
|
|
|
|
|
|
|
# if necessary, add an error message to output
|
|
|
|
if rc != 0:
|
|
|
|
module.fail_json(stderr=stderr, stderr_lines=lines[1:3], **result)
|
|
|
|
|
|
|
|
# set changed status
|
|
|
|
result['changed'] = has_changed(message)
|
|
|
|
|
|
|
|
# in the event of a success simply pass the key/value results
|
|
|
|
module.exit_json(**result)
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
run_module()
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|