From ee7e3fb39841a8ca94f64fbb000ca883b42bf146 Mon Sep 17 00:00:00 2001 From: git_admin Date: Sun, 3 May 2026 18:54:52 +0000 Subject: [PATCH] Tower: upload cetmix_tower_server_queue 18.0.2.0.0 (was 18.0.2.0.0, via marketplace) --- addons/cetmix_tower_server_queue/README.rst | 84 ++++ addons/cetmix_tower_server_queue/__init__.py | 1 + .../cetmix_tower_server_queue/__manifest__.py | 19 + .../i18n/cetmix_tower_server_queue.pot | 137 ++++++ addons/cetmix_tower_server_queue/i18n/it.po | 156 +++++++ .../models/__init__.py | 4 + .../models/cx_tower_command_log.py | 90 ++++ .../models/cx_tower_file.py | 184 ++++++++ .../models/cx_tower_server.py | 86 ++++ .../models/queue_job.py | 23 + .../cetmix_tower_server_queue/pyproject.toml | 3 + .../readme/CONFIGURE.md | 1 + .../readme/DESCRIPTION.md | 5 + .../readme/HISTORY.md | 3 + .../cetmix_tower_server_queue/readme/USAGE.md | 1 + .../readme/newsfragments/.gitkeep | 0 .../static/description/icon.png | Bin 0 -> 22128 bytes .../static/description/index.html | 441 ++++++++++++++++++ .../tests/__init__.py | 3 + .../tests/test_command.py | 145 ++++++ .../tests/test_command_log.py | 37 ++ .../tests/test_file.py | 201 ++++++++ .../views/cx_tower_command_log_view.xml | 16 + .../views/cx_tower_file_view.xml | 55 +++ 24 files changed, 1695 insertions(+) create mode 100644 addons/cetmix_tower_server_queue/README.rst create mode 100644 addons/cetmix_tower_server_queue/__init__.py create mode 100644 addons/cetmix_tower_server_queue/__manifest__.py create mode 100644 addons/cetmix_tower_server_queue/i18n/cetmix_tower_server_queue.pot create mode 100644 addons/cetmix_tower_server_queue/i18n/it.po create mode 100644 addons/cetmix_tower_server_queue/models/__init__.py create mode 100644 addons/cetmix_tower_server_queue/models/cx_tower_command_log.py create mode 100644 addons/cetmix_tower_server_queue/models/cx_tower_file.py create mode 100644 addons/cetmix_tower_server_queue/models/cx_tower_server.py create mode 100644 addons/cetmix_tower_server_queue/models/queue_job.py create mode 100644 addons/cetmix_tower_server_queue/pyproject.toml create mode 100644 addons/cetmix_tower_server_queue/readme/CONFIGURE.md create mode 100644 addons/cetmix_tower_server_queue/readme/DESCRIPTION.md create mode 100644 addons/cetmix_tower_server_queue/readme/HISTORY.md create mode 100644 addons/cetmix_tower_server_queue/readme/USAGE.md create mode 100644 addons/cetmix_tower_server_queue/readme/newsfragments/.gitkeep create mode 100644 addons/cetmix_tower_server_queue/static/description/icon.png create mode 100644 addons/cetmix_tower_server_queue/static/description/index.html create mode 100644 addons/cetmix_tower_server_queue/tests/__init__.py create mode 100644 addons/cetmix_tower_server_queue/tests/test_command.py create mode 100644 addons/cetmix_tower_server_queue/tests/test_command_log.py create mode 100644 addons/cetmix_tower_server_queue/tests/test_file.py create mode 100644 addons/cetmix_tower_server_queue/views/cx_tower_command_log_view.xml create mode 100644 addons/cetmix_tower_server_queue/views/cx_tower_file_view.xml diff --git a/addons/cetmix_tower_server_queue/README.rst b/addons/cetmix_tower_server_queue/README.rst new file mode 100644 index 0000000..12c2d07 --- /dev/null +++ b/addons/cetmix_tower_server_queue/README.rst @@ -0,0 +1,84 @@ +========================= +Cetmix Tower Server Queue +========================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:2e1ee4e42121b9fbd22081b4097aecc67b4988941177c5dc22711252011a95fa + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-cetmix%2Fcetmix--tower-lightgray.png?logo=github + :target: https://github.com/cetmix/cetmix-tower/tree/18.0/cetmix_tower_server_queue + :alt: cetmix/cetmix-tower + +|badge1| |badge2| |badge3| + +This module implements asynchronous task execution for `Cetmix +Tower `__. + +It requires the `queue_job `__ +module to be installed and configured in the Odoo instance. + +Please refer to the `official +documentation `__ for detailed information. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Please refer to the `official +documentation `__ for detailed configuration +instructions. + +Usage +===== + +Please refer to the `official +documentation `__ for detailed usage +instructions. + +Changelog +========= + +18.0.2.0.0 (2026-04-07) +----------------------- + +- Features: Jets! (4700) + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Cetmix + +Maintainers +----------- + +This module is part of the `cetmix/cetmix-tower `_ project on GitHub. + +You are welcome to contribute. diff --git a/addons/cetmix_tower_server_queue/__init__.py b/addons/cetmix_tower_server_queue/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/addons/cetmix_tower_server_queue/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/addons/cetmix_tower_server_queue/__manifest__.py b/addons/cetmix_tower_server_queue/__manifest__.py new file mode 100644 index 0000000..c116be1 --- /dev/null +++ b/addons/cetmix_tower_server_queue/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright (C) 2022 Cetmix OÜ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Cetmix Tower Server Queue", + "summary": "Cetmix Tower asynchronous task execution using 'queue_job'", + "version": "18.0.2.0.0", + "development_status": "Beta", + "category": "Productivity", + "website": "https://tower.cetmix.com", + "author": "Cetmix", + "license": "AGPL-3", + "installable": True, + "auto_install": True, + "depends": ["cetmix_tower_server", "queue_job"], + "data": [ + "views/cx_tower_command_log_view.xml", + "views/cx_tower_file_view.xml", + ], +} diff --git a/addons/cetmix_tower_server_queue/i18n/cetmix_tower_server_queue.pot b/addons/cetmix_tower_server_queue/i18n/cetmix_tower_server_queue.pot new file mode 100644 index 0000000..9ae3ace --- /dev/null +++ b/addons/cetmix_tower_server_queue/i18n/cetmix_tower_server_queue.pot @@ -0,0 +1,137 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * cetmix_tower_server_queue +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: cetmix_tower_server_queue +#: model:ir.model.fields,help:cetmix_tower_server_queue.field_cx_tower_command_log__command_status +msgid "" +"0 if command finished successfully.\n" +"-100 general error,\n" +"-101 not found,\n" +"-201 another instance of this command is running,\n" +"-202 no runner found for the command action,\n" +"-203 Python code execution failed\n" +"-205 plan line condition check failed\n" +"503 if SSH connection error occurred\n" +"601 if queue job failed" +msgstr "" + +#. module: cetmix_tower_server_queue +#: model:ir.model,name:cetmix_tower_server_queue.model_cx_tower_command_log +msgid "Cetmix Tower Command Log" +msgstr "" + +#. module: cetmix_tower_server_queue +#: model:ir.model,name:cetmix_tower_server_queue.model_cx_tower_file +msgid "Cetmix Tower File" +msgstr "" + +#. module: cetmix_tower_server_queue +#: model:ir.model,name:cetmix_tower_server_queue.model_cx_tower_server +msgid "Cetmix Tower Server" +msgstr "" + +#. module: cetmix_tower_server_queue +#: model_terms:ir.ui.view,arch_db:cetmix_tower_server_queue.cx_tower_file_view_form +msgid "Error" +msgstr "" + +#. module: cetmix_tower_server_queue +#: model:ir.model.fields,field_description:cetmix_tower_server_queue.field_cx_tower_command_log__command_status +msgid "Exit Code" +msgstr "" + +#. module: cetmix_tower_server_queue +#. odoo-python +#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0 +msgid "Failure" +msgstr "" + +#. module: cetmix_tower_server_queue +#. odoo-python +#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0 +msgid "File downloaded!" +msgstr "" + +#. module: cetmix_tower_server_queue +#: model:ir.model.fields,help:cetmix_tower_server_queue.field_cx_tower_file__is_being_processed +msgid "File is currently being processed" +msgstr "" + +#. module: cetmix_tower_server_queue +#. odoo-python +#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0 +msgid "File uploaded!" +msgstr "" + +#. module: cetmix_tower_server_queue +#. odoo-python +#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0 +msgid "File(s) %(name)s download failed: %(error)s" +msgstr "" + +#. module: cetmix_tower_server_queue +#. odoo-python +#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0 +msgid "File(s) %(name)s upload failed: %(error)s" +msgstr "" + +#. module: cetmix_tower_server_queue +#. odoo-python +#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0 +msgid "Files downloaded!" +msgstr "" + +#. module: cetmix_tower_server_queue +#. odoo-python +#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0 +msgid "Files uploaded!" +msgstr "" + +#. module: cetmix_tower_server_queue +#: model:ir.model.fields,field_description:cetmix_tower_server_queue.field_cx_tower_file__is_being_processed +msgid "Is Being Processed" +msgstr "" + +#. module: cetmix_tower_server_queue +#: model_terms:ir.ui.view,arch_db:cetmix_tower_server_queue.cx_tower_file_view_form +msgid "Processing" +msgstr "" + +#. module: cetmix_tower_server_queue +#: model:ir.model,name:cetmix_tower_server_queue.model_queue_job +#: model:ir.model.fields,field_description:cetmix_tower_server_queue.field_cx_tower_command_log__queue_job_id +msgid "Queue Job" +msgstr "" + +#. module: cetmix_tower_server_queue +#. odoo-python +#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0 +#: model_terms:ir.ui.view,arch_db:cetmix_tower_server_queue.cx_tower_file_view_form +msgid "Success" +msgstr "" + +#. module: cetmix_tower_server_queue +#. odoo-python +#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0 +msgid "The following files are already being processed: %(name)s" +msgstr "" + +#. module: cetmix_tower_server_queue +#. odoo-python +#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0 +msgid "" +"Unable to upload file '%(f)s'.\n" +"Upload operation is not supported for 'server' type files." +msgstr "" diff --git a/addons/cetmix_tower_server_queue/i18n/it.po b/addons/cetmix_tower_server_queue/i18n/it.po new file mode 100644 index 0000000..c893bb7 --- /dev/null +++ b/addons/cetmix_tower_server_queue/i18n/it.po @@ -0,0 +1,156 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * cetmix_tower_server_queue +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: it\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.3\n" + +#. module: cetmix_tower_server_queue +#: model:ir.model.fields,help:cetmix_tower_server_queue.field_cx_tower_command_log__command_status +msgid "" +"0 if command finished successfully.\n" +"-100 general error,\n" +"-101 not found,\n" +"-201 another instance of this command is running,\n" +"-202 no runner found for the command action,\n" +"-203 Python code execution failed\n" +"-205 plan line condition check failed\n" +"503 if SSH connection error occurred\n" +"601 if queue job failed" +msgstr "" +"0 se il comando è stato completato correttamente.\n" +"-100 errore generale,-101 non trovato,\n" +"-201 un'altra istanza di questo comando è in esecuzione,\n" +"-202 nessun runner trovato per l'azione del comando,\n" +"-203 esecuzione del codice Python non riuscita,\n" +"-205 controllo delle condizioni della riga del piano non riuscito,\n" +"503 se si è verificato un errore di connessione SSH,\n" +"601 se il processo in coda non è riuscito" + +#. module: cetmix_tower_server_queue +#: model:ir.model,name:cetmix_tower_server_queue.model_cx_tower_command_log +msgid "Cetmix Tower Command Log" +msgstr "Registro comandi Cetmix Tower" + +#. module: cetmix_tower_server_queue +#: model:ir.model,name:cetmix_tower_server_queue.model_cx_tower_file +msgid "Cetmix Tower File" +msgstr "File Cetmix Tower" + +#. module: cetmix_tower_server_queue +#: model:ir.model,name:cetmix_tower_server_queue.model_cx_tower_server +msgid "Cetmix Tower Server" +msgstr "Server Cetmix Tower" + +#. module: cetmix_tower_server_queue +#: model_terms:ir.ui.view,arch_db:cetmix_tower_server_queue.cx_tower_file_view_form +msgid "Error" +msgstr "Errore" + +#. module: cetmix_tower_server_queue +#: model:ir.model.fields,field_description:cetmix_tower_server_queue.field_cx_tower_command_log__command_status +msgid "Exit Code" +msgstr "Codice di uscita" + +#. module: cetmix_tower_server_queue +#. odoo-python +#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0 +#, python-format +msgid "Failure" +msgstr "Operazione non riuscita" + +#. module: cetmix_tower_server_queue +#. odoo-python +#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0 +#, python-format +msgid "File downloaded!" +msgstr "File scaricato!" + +#. module: cetmix_tower_server_queue +#: model:ir.model.fields,help:cetmix_tower_server_queue.field_cx_tower_file__is_being_processed +msgid "File is currently being processed" +msgstr "Il file è attualmente in elaborazione" + +#. module: cetmix_tower_server_queue +#. odoo-python +#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0 +#, python-format +msgid "File uploaded!" +msgstr "File caricato!" + +#. module: cetmix_tower_server_queue +#. odoo-python +#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0 +#, python-format +msgid "Files downloaded!" +msgstr "File scaricati!" + +#. module: cetmix_tower_server_queue +#. odoo-python +#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0 +#, python-format +msgid "Files uploaded!" +msgstr "File caricati!" + +#. module: cetmix_tower_server_queue +#: model:ir.model.fields,field_description:cetmix_tower_server_queue.field_cx_tower_file__is_being_processed +msgid "Is Being Processed" +msgstr "In elaborazione" + +#. module: cetmix_tower_server_queue +#: model_terms:ir.ui.view,arch_db:cetmix_tower_server_queue.cx_tower_file_view_form +msgid "Processing" +msgstr "Elaborazione" + +#. module: cetmix_tower_server_queue +#: model:ir.model,name:cetmix_tower_server_queue.model_queue_job +#: model:ir.model.fields,field_description:cetmix_tower_server_queue.field_cx_tower_command_log__queue_job_id +msgid "Queue Job" +msgstr "Lavoro in coda" + +#. module: cetmix_tower_server_queue +#. odoo-python +#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0 +#: model_terms:ir.ui.view,arch_db:cetmix_tower_server_queue.cx_tower_file_view_form +#, python-format +msgid "Success" +msgstr "Successo" + +#. module: cetmix_tower_server_queue +#. odoo-python +#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0 +#, python-format +msgid "The following files are already being processed: %(name)s" +msgstr "I seguenti file sono già in elaborazione: %(name)s" + +#. module: cetmix_tower_server_queue +#. odoo-python +#: code:addons/cetmix_tower_server_queue/models/cx_tower_file.py:0 +#, python-format +msgid "" +"Unable to upload file '%(f)s'.\n" +"Upload operation is not supported for 'server' type files." +msgstr "" +"Impossibile caricare il file '%(f)s'.\n" +"L'operazione di caricamento non è supportata per i file di tipo 'server'." + +#~ msgid "Display Name" +#~ msgstr "Nome visualizzato" + +#~ msgid "ID" +#~ msgstr "ID" + +#~ msgid "Last Modified on" +#~ msgstr "Ultima modifica il" diff --git a/addons/cetmix_tower_server_queue/models/__init__.py b/addons/cetmix_tower_server_queue/models/__init__.py new file mode 100644 index 0000000..4e40e4b --- /dev/null +++ b/addons/cetmix_tower_server_queue/models/__init__.py @@ -0,0 +1,4 @@ +from . import cx_tower_command_log +from . import cx_tower_server +from . import queue_job +from . import cx_tower_file diff --git a/addons/cetmix_tower_server_queue/models/cx_tower_command_log.py b/addons/cetmix_tower_server_queue/models/cx_tower_command_log.py new file mode 100644 index 0000000..9062038 --- /dev/null +++ b/addons/cetmix_tower_server_queue/models/cx_tower_command_log.py @@ -0,0 +1,90 @@ +# Copyright (C) 2025 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from psycopg2.errors import LockNotAvailable + +from odoo import fields, models, tools + +from odoo.addons.cetmix_tower_server.models.constants import ( + COMMAND_STOPPED, + COMMAND_TIMED_OUT, +) +from odoo.addons.queue_job.job import CANCELLED + +_logger = logging.getLogger(__name__) + + +class CxTowerCommandLog(models.Model): + _inherit = "cx.tower.command.log" + + queue_job_id = fields.Many2one( + "queue.job", + readonly=True, + groups="queue_job.group_queue_job_manager", + ) + + command_status = fields.Integer( + help="0 if command finished successfully.\n" + "-100 general error,\n" + "-101 not found,\n" + "-201 another instance of this command is running,\n" + "-202 no runner found for the command action,\n" + "-203 Python code execution failed\n" + "-205 plan line condition check failed\n" + "503 if SSH connection error occurred\n" + "601 if queue job failed" + ) + + def finish( + self, finish_date=None, status=None, response=None, error=None, **kwargs + ): + """Finish the command log + + Args: + finish_date (Datetime, optional): Command finish date. Defaults to None. + status (Integer, optional): Command status. Defaults to None. + response (Text, optional): Command response. Defaults to None. + error (Text, optional): Command error. Defaults to None. + """ + + # Filter out command logs that are already stopped + command_logs_to_process = self.filtered( + lambda log: log.command_status != COMMAND_STOPPED + ) + if not command_logs_to_process: + return + + # Lock and process each record individually + locked_logs = self.browse() + for command_log in command_logs_to_process: + try: + with self.env.cr.savepoint(), tools.mute_logger("odoo.sql_db"): + self.env.cr.execute( + f"SELECT command_status FROM {self._table} WHERE id = %s FOR UPDATE NOWAIT", # noqa: E501 + (command_log.id,), + ) + locked_logs |= command_log + except LockNotAvailable as e: + _logger.warning( + "Could not acquire lock on command log %s, skipping: %s", + command_log.id, + e, + ) + continue + + if not locked_logs: + return + + # Update the related queue job state if the command timed out + if status == COMMAND_TIMED_OUT: + for command_log in locked_logs: + if command_log.queue_job_id: + command_log.queue_job_id.sudo()._change_job_state( + CANCELLED, result=error + ) + + return super(CxTowerCommandLog, locked_logs).finish( + finish_date, status, response, error, **kwargs + ) diff --git a/addons/cetmix_tower_server_queue/models/cx_tower_file.py b/addons/cetmix_tower_server_queue/models/cx_tower_file.py new file mode 100644 index 0000000..327ffa1 --- /dev/null +++ b/addons/cetmix_tower_server_queue/models/cx_tower_file.py @@ -0,0 +1,184 @@ +# Copyright (C) 2025 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import _, fields, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + + +class CxTowerFile(models.Model): + _inherit = "cx.tower.file" + + is_being_processed = fields.Boolean( + copy=False, + help="File is currently being processed", + ) + + def _check_files_being_processed(self, raise_error): + """ + Check if any file in the recordset is being processed. + True if at least one file is already processing and raise_error is False. + False if no files are currently being processed. + The caller uses the boolean to decide whether to continue or abort. + """ + processing_files = self.filtered(lambda rec: rec.is_being_processed) + if processing_files: + if raise_error: + raise UserError( + _( + "The following files are already being processed: %(name)s", + name=", ".join(processing_files.mapped("name")), + ) + ) + else: + return True + return False + + def upload(self, raise_error=False): + """ + Trigger asynchronous upload via job queue. + """ + # Check if the file is already being processed + if self._check_files_being_processed(raise_error): + return + + self.write({"server_response": False, "is_being_processed": True}) + + # Enqueue the upload if not already in a queue job; + # otherwise, execute immediately + if not self.env.context.get("job_uuid"): + self.with_delay()._do_upload(raise_error=raise_error) + else: + self._do_upload(raise_error=raise_error) + + def download(self, raise_error=False): + """ + Trigger asynchronous download via job queue. + """ + + # Check if the file is already being processed + if self._check_files_being_processed(raise_error): + return + + self.write({"server_response": False, "is_being_processed": True}) + + # Enqueue the download if not already in a queue job; + # otherwise, execute immediately + if not self.env.context.get("job_uuid"): + self.with_delay()._do_download(raise_error=raise_error) + else: + self._do_download(raise_error=raise_error) + + def _do_upload(self, raise_error=True): + """ + Uploads the files within a job context and notifies the user on success. + Logs the error if an exception occurs; + failure state is managed by the parent method. + """ + try: + with self.env.cr.savepoint(): + result = super().upload(raise_error=raise_error) + single_msg = _("File uploaded!") + plural_msg = _("Files uploaded!") + self.env.user.notify_success( + message=single_msg if len(self) == 1 else plural_msg, + title=_("Success"), + # This notification should not be sticky + # to avoid blocking the user's screen + sticky=False, + ) + return result + except Exception as e: + if not raise_error: + self.env.user.notify_danger( + message=_( + "File(s) %(name)s upload failed: %(error)s", + name=", ".join(self.mapped("name")), + error=str(e), + ), + title=_("Failure"), + sticky=self.env["ir.config_parameter"] + .sudo() + .get_param("cetmix_tower_server.notification_type_error", "sticky") + == "sticky", + ) + _logger.error("File %s upload failed: %s", str(self), str(e)) + else: + raise + finally: + self.write({"is_being_processed": False}) + + def _do_download(self, raise_error=True): + """ + Downloads the files within a job context and notifies the user on success. + Logs the error if an exception occurs; + failure state is managed by the parent method. + """ + try: + with self.env.cr.savepoint(): + result = super().download(raise_error=raise_error) + single_msg = _("File downloaded!") + plural_msg = _("Files downloaded!") + self.env.user.notify_success( + message=single_msg if len(self) == 1 else plural_msg, + title=_("Success"), + # This notification should not be sticky + # to avoid blocking the user's screen + sticky=False, + ) + return result + except Exception as e: + if not raise_error: + self.env.user.notify_danger( + message=_( + "File(s) %(name)s download failed: %(error)s", + name=", ".join(self.mapped("name")), + error=str(e), + ), + title=_("Failure"), + sticky=self.env["ir.config_parameter"] + .sudo() + .get_param("cetmix_tower_server.notification_type_error", "sticky") + == "sticky", + ) + _logger.error("File %s download failed: %s", str(self), str(e)) + else: + raise + finally: + self.write({"is_being_processed": False}) + + def action_pull_from_server(self): + """ + Pull file from server without notification. + """ + tower_files = self.filtered(lambda file_: file_.source == "tower") + server_files = self - tower_files + + tower_files.action_get_current_server_code() + + server_files.download(raise_error=False) + + def action_push_to_server(self): + """ + Push the file to server without success notification. + """ + server_files = self.filtered(lambda file_: file_.source == "server") + if server_files: + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": _("Failure"), + "message": _( + "Unable to upload file '%(f)s'.\n" + "Upload operation is not supported for 'server' type files.", + f=", ".join(server_files.mapped("rendered_name")), + ), + "sticky": False, + }, + } + + self.upload(raise_error=False) diff --git a/addons/cetmix_tower_server_queue/models/cx_tower_server.py b/addons/cetmix_tower_server_queue/models/cx_tower_server.py new file mode 100644 index 0000000..a9a4b5a --- /dev/null +++ b/addons/cetmix_tower_server_queue/models/cx_tower_server.py @@ -0,0 +1,86 @@ +# Copyright (C) 2022 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class CxTowerServer(models.Model): + _inherit = "cx.tower.server" + + def _command_runner_wrapper( + self, + command, + log_record, + rendered_command_code, + sudo=None, + rendered_command_path=None, + ssh_connection=None, + **kwargs, + ): + # If the flight plan log has an entry on the parent flight plan log, + # it means that this flight plan was launched from another plan, + # this plan should be launched as a synchronous command to + # preserve the order of execution of commands with actions + # "Run Flight Plan", "Trigger Jet Action" and "Create Waypoint". + # Use runner only if command log record is provided. + if ( + log_record + and not log_record.plan_log_id.parent_flight_plan_log_id + and command.action + not in [ + "jet_action", + "create_waypoint", + ] + ): + job = self.with_delay()._queue_command_runner_wrapper( + command=command, + log_record=log_record, + rendered_command_code=rendered_command_code, + sudo=sudo, + rendered_command_path=rendered_command_path, + ssh_connection=None, # Always None for queued jobs + **kwargs, + ) + log_record.sudo().queue_job_id = job.db_record().id + + # Otherwise fallback to `super` to return the command output + else: + return super()._command_runner_wrapper( + command=command, + log_record=log_record, + rendered_command_code=rendered_command_code, + sudo=sudo, + rendered_command_path=rendered_command_path, + ssh_connection=ssh_connection, + **kwargs, + ) + + def _queue_command_runner_wrapper( + self, + command, + log_record, + rendered_command_code, + sudo=None, + rendered_command_path=None, + ssh_connection=None, + **kwargs, + ): + # avoid executing command if plan was stopped + log_record.invalidate_recordset(["plan_log_id"]) + plan_log_id = log_record.plan_log_id + if plan_log_id: + plan_log_id.invalidate_recordset(["is_stopped"]) + + # If plan was stopped, stop the command + if plan_log_id.is_stopped: + log_record.stop() + return + + return self._command_runner( + command=command, + log_record=log_record, + rendered_command_code=rendered_command_code, + sudo=sudo, + rendered_command_path=rendered_command_path, + ssh_connection=ssh_connection, + **kwargs, + ) diff --git a/addons/cetmix_tower_server_queue/models/queue_job.py b/addons/cetmix_tower_server_queue/models/queue_job.py new file mode 100644 index 0000000..7b66eea --- /dev/null +++ b/addons/cetmix_tower_server_queue/models/queue_job.py @@ -0,0 +1,23 @@ +# Copyright 2013-2020 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) +from odoo import models + + +class QueueJob(models.Model): + _inherit = "queue.job" + + QUEUE_JOB_ERROR = 601 + + def write(self, vals): + """ + Override write method to update command status + and write error information in the log record + """ + if vals.get("state") == "failed": + log_record = self.kwargs.get("log_record") + if log_record: + log_record.finish( + status=self.QUEUE_JOB_ERROR, + error=vals.get("exc_info"), + ) + return super().write(vals) diff --git a/addons/cetmix_tower_server_queue/pyproject.toml b/addons/cetmix_tower_server_queue/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/addons/cetmix_tower_server_queue/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/addons/cetmix_tower_server_queue/readme/CONFIGURE.md b/addons/cetmix_tower_server_queue/readme/CONFIGURE.md new file mode 100644 index 0000000..8c717e5 --- /dev/null +++ b/addons/cetmix_tower_server_queue/readme/CONFIGURE.md @@ -0,0 +1 @@ +Please refer to the [official documentation](https://cetmix.com/tower) for detailed configuration instructions. diff --git a/addons/cetmix_tower_server_queue/readme/DESCRIPTION.md b/addons/cetmix_tower_server_queue/readme/DESCRIPTION.md new file mode 100644 index 0000000..54e6fc3 --- /dev/null +++ b/addons/cetmix_tower_server_queue/readme/DESCRIPTION.md @@ -0,0 +1,5 @@ +This module implements asynchronous task execution for [Cetmix Tower](https://cetmix.com/tower). + +It requires the [queue_job](https://github.com/OCA/queue/queue_job) module to be installed and configured in the Odoo instance. + +Please refer to the [official documentation](https://cetmix.com/tower) for detailed information. diff --git a/addons/cetmix_tower_server_queue/readme/HISTORY.md b/addons/cetmix_tower_server_queue/readme/HISTORY.md new file mode 100644 index 0000000..b0cfcea --- /dev/null +++ b/addons/cetmix_tower_server_queue/readme/HISTORY.md @@ -0,0 +1,3 @@ +## 18.0.2.0.0 (2026-04-07) + +- Features: Jets! (4700) diff --git a/addons/cetmix_tower_server_queue/readme/USAGE.md b/addons/cetmix_tower_server_queue/readme/USAGE.md new file mode 100644 index 0000000..901f5a6 --- /dev/null +++ b/addons/cetmix_tower_server_queue/readme/USAGE.md @@ -0,0 +1 @@ +Please refer to the [official documentation](https://cetmix.com/tower) for detailed usage instructions. diff --git a/addons/cetmix_tower_server_queue/readme/newsfragments/.gitkeep b/addons/cetmix_tower_server_queue/readme/newsfragments/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/addons/cetmix_tower_server_queue/static/description/icon.png b/addons/cetmix_tower_server_queue/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2507f553896c442455b02ed5fa06b72ab398a990 GIT binary patch literal 22128 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 F{{#JqH_ZS5 literal 0 HcmV?d00001 diff --git a/addons/cetmix_tower_server_queue/static/description/index.html b/addons/cetmix_tower_server_queue/static/description/index.html new file mode 100644 index 0000000..4893443 --- /dev/null +++ b/addons/cetmix_tower_server_queue/static/description/index.html @@ -0,0 +1,441 @@ + + + + + +Cetmix Tower Server Queue + + + +
+

Cetmix Tower Server Queue

+ + +

Beta License: AGPL-3 cetmix/cetmix-tower

+

This module implements asynchronous task execution for Cetmix +Tower.

+

It requires the queue_job +module to be installed and configured in the Odoo instance.

+

Please refer to the official +documentation for detailed information.

+

Table of contents

+ +
+

Configuration

+

Please refer to the official +documentation for detailed configuration +instructions.

+
+
+

Usage

+

Please refer to the official +documentation for detailed usage +instructions.

+
+
+

Changelog

+
+

18.0.2.0.0 (2026-04-07)

+
    +
  • Features: Jets! (4700)
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Cetmix
  • +
+
+
+

Maintainers

+

This module is part of the cetmix/cetmix-tower project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/addons/cetmix_tower_server_queue/tests/__init__.py b/addons/cetmix_tower_server_queue/tests/__init__.py new file mode 100644 index 0000000..306c04b --- /dev/null +++ b/addons/cetmix_tower_server_queue/tests/__init__.py @@ -0,0 +1,3 @@ +from . import test_command +from . import test_command_log +from . import test_file diff --git a/addons/cetmix_tower_server_queue/tests/test_command.py b/addons/cetmix_tower_server_queue/tests/test_command.py new file mode 100644 index 0000000..2f043d9 --- /dev/null +++ b/addons/cetmix_tower_server_queue/tests/test_command.py @@ -0,0 +1,145 @@ +from datetime import timedelta +from unittest.mock import patch + +from odoo.fields import Datetime +from odoo.tools import mute_logger + +from odoo.addons.cetmix_tower_server.tests.common import TestTowerCommon + + +class TestTowerCommand(TestTowerCommon): + """Test suite for verifying zombie command detection and related + queue job cancellation. + + Tests in this class verify that commands which have been running + longer than the timeout are properly detected as zombies, and their + associated queue jobs are cancelled. + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + # Set command timeout to 10 seconds + cls.env["ir.config_parameter"].sudo().set_param( + "cetmix_tower_server.command_timeout", "10" + ) + # Set old time to 20 seconds ago (older than timeout) + # to simulate running command in past + now = Datetime.now() + cls.old_time = now - timedelta(seconds=20) + + def _patch_command_runner(self, command_type, runner_method): + """Helper to patch a command runner to simulate a zombie command. + + Args: + command_type: Type of command runner to patch ('ssh' or 'python_code') + runner_method: Original method to wrap + + Returns: + A context manager that applies the patch + """ + + def _wrapper(*args, **kwargs): + # Modify args to disable log record finishing + args = list(args) + if len(args) > 1: + args[1] = False # Set log_record to False + return runner_method(*args, **kwargs) + + return patch.object( + self.registry["cx.tower.server"], + f"_command_runner_{command_type}", + _wrapper, + ) + + def _verify_zombie_command_job_cancellation(self, command_action): + """Verify zombie command is detected and job is cancelled. + + Args: + command_action: Action type ('ssh_command' or 'python_code') + """ + # check zombie command logs + domain = [ + ("is_running", "=", True), + ("start_date", "=", self.old_time), + ("command_action", "=", command_action), + ] + zombie_command_logs = self.env["cx.tower.command.log"].search(domain) + + self.assertEqual( + len(zombie_command_logs), 1, "Zombie command log should be created" + ) + self.assertTrue( + zombie_command_logs.queue_job_id, + "Zombie command log should have queue job", + ) + + job = zombie_command_logs.queue_job_id + self.assertTrue(job.exists(), "Zombie command job should exist") + + self.assertEqual(job.state, "pending", "Zombie command job should be pending") + + # run process to kill zombie command + self.server_test_1._check_zombie_commands() + + # check that command log is cancelled + self.assertEqual( + job.state, "cancelled", "Zombie command job should be cancelled" + ) + + def test_check_zombie_ssh_command_queue(self): + """ + Test that zombie ssh command is killed and job is cancelled + """ + # Create test commands + ssh_command = self.Command.create( + { + "name": "Test SSH Command", + "code": "ls -la", + "action": "ssh_command", + } + ) + + # patch command runner to not finish log record + cx_tower_server_obj = self.registry["cx.tower.server"] + _command_runner_ssh_super = cx_tower_server_obj._command_runner_ssh + + with self._patch_command_runner("ssh", _command_runner_ssh_super): + # run zombie command with log creation in past + self.server_test_1.run_command( + ssh_command, log={"start_date": self.old_time} + ) + + # check zombie command logs + self._verify_zombie_command_job_cancellation("ssh_command") + + @mute_logger("py.warnings") + def test_check_zombie_python_command_queue(self): + """ + Test that zombie python command is killed and job is cancelled + """ + # Create test commands + python_command = self.Command.create( + { + "name": "Test Python Command", + "code": "print('test')", + "action": "python_code", + } + ) + + # patch command runner to not finish log record + cx_tower_server_obj = self.registry["cx.tower.server"] + _command_runner_python_code_super = ( + cx_tower_server_obj._command_runner_python_code + ) + + with self._patch_command_runner( + "python_code", _command_runner_python_code_super + ): + # run zombie command with log creation in past + self.server_test_1.run_command( + python_command, log={"start_date": self.old_time} + ) + + # check zombie command logs + self._verify_zombie_command_job_cancellation("python_code") diff --git a/addons/cetmix_tower_server_queue/tests/test_command_log.py b/addons/cetmix_tower_server_queue/tests/test_command_log.py new file mode 100644 index 0000000..5bef9eb --- /dev/null +++ b/addons/cetmix_tower_server_queue/tests/test_command_log.py @@ -0,0 +1,37 @@ +from odoo.addons.cetmix_tower_server.tests.common import TestTowerCommon +from odoo.addons.queue_job.job import Job + + +class TestTowerCommand(TestTowerCommon): + """ + Test cases for command log state on queue_job failure + """ + + def test_command_log_state_on_job_fail(self): + command = self.env["cx.tower.command"].create( + { + "name": "Test Command", + "action": "ssh_command", + "code": "echo 'Hello World'", + } + ) + self.assertTrue(command.id, "Command should be created successfully") + + self.server_test_1.run_command(command=command) + command_log = self.env["cx.tower.command.log"].search( + [("command_id", "=", command.id)], order="id desc", limit=1 + ) + self.assertTrue(command_log, "Command log should be created") + + job = command_log.queue_job_id + self.assertTrue(job, "Queue job should be associated with command log") + + job_obj = Job.load(self.env, job.uuid) + job_obj.set_failed() + job_obj.store() + self.assertEqual(job.state, "failed", "Job should be in failed state") + self.assertEqual( + command_log.command_status, + self.env["queue.job"].QUEUE_JOB_ERROR, + "Command log should be in failed state", + ) diff --git a/addons/cetmix_tower_server_queue/tests/test_file.py b/addons/cetmix_tower_server_queue/tests/test_file.py new file mode 100644 index 0000000..c04a229 --- /dev/null +++ b/addons/cetmix_tower_server_queue/tests/test_file.py @@ -0,0 +1,201 @@ +from odoo import exceptions + +from odoo.addons.cetmix_tower_server.tests.common import TestTowerCommon +from odoo.addons.queue_job.tests.common import trap_jobs + + +class TestCxTowerFileQueue(TestTowerCommon): + def setUp(self): + super().setUp() + self.file_template = self.FileTemplate.create( + { + "name": "Test", + "file_name": "test.txt", + "server_dir": "/var/tmp", + "code": "Hello, world!", + } + ) + + def test_async_upload_operations(self): + """Test that upload operations are processed asynchronously""" + # Create unique files specifically for this test + upload_file = self.File.create( + { + "source": "tower", + "template_id": self.file_template.id, + "server_id": self.server_test_1.id, + "name": "upload_test_1", + "auto_sync": False, + } + ) + + upload_file_2 = self.File.create( + { + "name": "upload_test_2", + "source": "server", + "server_id": self.server_test_1.id, + "server_dir": "/var/tmp", + "auto_sync": False, + } + ) + + with trap_jobs() as trap: + upload_file.upload() + upload_file_2.upload() + + self.assertEqual(len(trap.enqueued_jobs), 2) + + upload_file.write({"server_response": "ok", "is_being_processed": False}) + upload_file_2.write({"server_response": "ok", "is_being_processed": False}) + + # Refresh records to get updated values + upload_file.invalidate_recordset() + upload_file_2.invalidate_recordset() + + # Verify the expected state + self.assertEqual(upload_file.server_response, "ok") + self.assertFalse(upload_file.is_being_processed) + + self.assertEqual(upload_file_2.server_response, "ok") + self.assertFalse(upload_file_2.is_being_processed) + + def test_async_download_operations(self): + """Test that download operations are processed asynchronously""" + # Create unique files specifically for this test + download_file = self.File.create( + { + "source": "tower", + "template_id": self.file_template.id, + "server_id": self.server_test_1.id, + "name": "download_test_1", + "auto_sync": False, + } + ) + + download_file_2 = self.File.create( + { + "name": "download_test_2", + "source": "server", + "server_id": self.server_test_1.id, + "server_dir": "/var/tmp", + "auto_sync": False, + } + ) + + with trap_jobs() as trap: + download_file.download() + download_file_2.download() + + # Verify jobs were created + self.assertEqual(len(trap.enqueued_jobs), 2) + + download_file.write({"server_response": "ok", "is_being_processed": False}) + download_file_2.write( + {"server_response": "ok", "is_being_processed": False} + ) + + # Refresh records to get updated values + download_file.invalidate_recordset() + download_file_2.invalidate_recordset() + + # Verify the expected state + self.assertEqual(download_file.server_response, "ok") + self.assertFalse(download_file.is_being_processed) + + self.assertEqual(download_file_2.server_response, "ok") + self.assertFalse(download_file_2.is_being_processed) + + def test_upload_error_handling(self): + """Test error handling in async upload operations""" + error_file = self.File.create( + { + "source": "tower", + "template_id": self.file_template.id, + "server_id": self.server_test_1.id, + "name": "error_handling_test", + "auto_sync": False, + } + ) + + # Set context to force the mock in ssh_upload_file to raise error + error_context = {"raise_upload_error": "Forced upload error"} + + with trap_jobs() as trap: + # This will trigger job creation but the job would fail if executed + error_file.with_context(**error_context).upload(raise_error=True) + + # Verify job was created + self.assertEqual(len(trap.enqueued_jobs), 1) + + # Simulate what would happen if the job executed and failed + error_file.write({"server_response": "error", "is_being_processed": False}) + error_file.invalidate_recordset() + + self.assertEqual(error_file.server_response, "error") + self.assertFalse(error_file.is_being_processed) + + def test_download_error_handling(self): + """Test error handling in async download operations""" + error_file = self.File.create( + { + "source": "server", + "server_id": self.server_test_1.id, + "server_dir": "/var/tmp", + "name": "download_error_test", + } + ) + + # Set context to force the mock in ssh_download_file to raise error + error_context = {"raise_download_error": "Forced download error"} + + with trap_jobs() as trap: + # This will trigger job creation but the job would fail if executed + error_file.with_context(**error_context).download(raise_error=True) + + # Verify job was created + self.assertEqual(len(trap.enqueued_jobs), 1) + + # Simulate what would happen if the job executed and failed + error_file.write({"server_response": "error", "is_being_processed": False}) + error_file.invalidate_recordset() + + self.assertEqual(error_file.server_response, "error") + self.assertFalse(error_file.is_being_processed) + + def test_already_processing_check(self): + """Test that files being processed cannot be processed again""" + processing_file = self.File.create( + { + "source": "tower", + "template_id": self.file_template.id, + "server_id": self.server_test_1.id, + "name": "processing_test_file", + "is_being_processed": True, + } + ) + + self.assertTrue(processing_file.is_being_processed) + + # Test with raising error + with self.assertRaises(exceptions.UserError): + processing_file.upload(raise_error=True) + + # Test without raising error - should not create job + with trap_jobs() as trap: + processing_file.upload(raise_error=False) + # No job should be created since file is already being processed + self.assertEqual(len(trap.enqueued_jobs), 0) + + # Verify still marked as processing + self.assertTrue(processing_file.is_being_processed) + + # Same tests for download + with self.assertRaises(exceptions.UserError): + processing_file.download(raise_error=True) + + with trap_jobs() as trap: + processing_file.download(raise_error=False) + # No job should be created + self.assertEqual(len(trap.enqueued_jobs), 0) + + self.assertTrue(processing_file.is_being_processed) diff --git a/addons/cetmix_tower_server_queue/views/cx_tower_command_log_view.xml b/addons/cetmix_tower_server_queue/views/cx_tower_command_log_view.xml new file mode 100644 index 0000000..0840695 --- /dev/null +++ b/addons/cetmix_tower_server_queue/views/cx_tower_command_log_view.xml @@ -0,0 +1,16 @@ + + + + cx.tower.command.log.view.form + cx.tower.command.log + + + + + + + + diff --git a/addons/cetmix_tower_server_queue/views/cx_tower_file_view.xml b/addons/cetmix_tower_server_queue/views/cx_tower_file_view.xml new file mode 100644 index 0000000..296093f --- /dev/null +++ b/addons/cetmix_tower_server_queue/views/cx_tower_file_view.xml @@ -0,0 +1,55 @@ + + + + cx.tower.file.view.form + cx.tower.file + + + + + + + + + + + + + + cx.tower.queue.file.view.list + cx.tower.file + + + + + + + + + is_being_processed + + + not is_being_processed and server_response == 'ok' + + + not is_being_processed and server_response not in ('ok', False) + + + + +