Wipe addons/: full reset for clean re-upload
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
from . import cx_tower_command_log
|
||||
from . import cx_tower_server
|
||||
from . import queue_job
|
||||
from . import cx_tower_file
|
||||
@@ -1,82 +0,0 @@
|
||||
# Copyright (C) 2025 Cetmix OÜ
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
|
||||
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
|
||||
|
||||
# Avoid finishing the command log multiple times at the same time
|
||||
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 IN %s FOR UPDATE NOWAIT", # noqa: E501
|
||||
(tuple(command_logs_to_process.ids),),
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.error(
|
||||
"Could not acquire lock on command logs %s, skipping finish: %s",
|
||||
command_logs_to_process.ids,
|
||||
e,
|
||||
)
|
||||
return
|
||||
|
||||
# Update the related queue job state if the command timed out
|
||||
if status == COMMAND_TIMED_OUT:
|
||||
for command_log in command_logs_to_process:
|
||||
if command_log.queue_job_id:
|
||||
command_log.queue_job_id.sudo()._change_job_state(
|
||||
CANCELLED, result=error
|
||||
)
|
||||
|
||||
return super(CxTowerCommandLog, command_logs_to_process).finish(
|
||||
finish_date, status, response, error, **kwargs
|
||||
)
|
||||
@@ -1,184 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,86 +0,0 @@
|
||||
# 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=ssh_connection,
|
||||
**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,
|
||||
)
|
||||
@@ -1,23 +0,0 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user