Files
odoo-addons/addons/cetmix_tower_server/models/cx_tower_plan_log.py

428 lines
14 KiB
Python

# Copyright (C) 2022 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from .constants import PLAN_IS_EMPTY, PLAN_STOPPED
class CxTowerPlanLog(models.Model):
"""Flight Plan Log"""
_name = "cx.tower.plan.log"
_description = "Cetmix Tower Flight Plan Log"
_order = "start_date desc, id desc"
active = fields.Boolean(default=True)
name = fields.Char(compute="_compute_name", compute_sudo=True, store=True)
label = fields.Char(
help="Custom label. Can be used for search/tracking",
index="trigram",
unaccent=False,
)
server_id = fields.Many2one(
comodel_name="cx.tower.server", required=True, index=True, ondelete="cascade"
)
plan_id = fields.Many2one(
string="Flight Plan",
comodel_name="cx.tower.plan",
required=True,
index=True,
ondelete="cascade",
)
access_level = fields.Selection(
related="plan_id.access_level",
readonly=True,
store=True,
index=True,
)
# -- Time
start_date = fields.Datetime(string="Started")
finish_date = fields.Datetime(string="Finished")
duration = fields.Float(
help="Time consumed for execution, seconds",
compute="_compute_duration",
store=True,
)
duration_current = fields.Float(
string="Duration, sec",
compute="_compute_duration_current",
help="For how long a flight plan is already running",
)
# -- Commands
is_running = fields.Boolean(
help="Plan is being executed right now", compute="_compute_duration", store=True
)
is_stopped = fields.Boolean(
string="Stopped", default=False, help="Flight plan was stopped by user"
)
plan_line_executed_id = fields.Many2one(
comodel_name="cx.tower.plan.line",
help="Flight Plan line that is being currently executed",
)
command_log_ids = fields.One2many(
comodel_name="cx.tower.command.log", inverse_name="plan_log_id", auto_join=True
)
plan_status = fields.Integer(
string="Status",
help="0 if plan is finished successfully. \n"
"-301 if another instance of this flight plan is running, \n"
"-302 if plan is empty, \n"
"-303 if plan reference is missing, \n"
"-304 if plan line reference is missing, \n"
"-306 if plan is not compatible with server,\n"
"-308 if plan is stopped by user",
)
custom_message = fields.Text(
help="Custom message to be displayed in the plan log",
)
parent_flight_plan_log_id = fields.Many2one(
"cx.tower.plan.log", string="Main Log", ondelete="cascade"
)
scheduled_task_id = fields.Many2one(
"cx.tower.scheduled.task",
ondelete="set null",
help="Scheduled task that triggered this flight plan",
)
variable_values = fields.Json(
default={},
help="Custom variable values passed to the flight plan",
)
@api.depends("server_id.name", "name")
def _compute_name(self):
for rec in self:
rec.name = ": ".join((rec.server_id.name, rec.plan_id.name)) # type: ignore
@api.depends("start_date", "finish_date")
def _compute_duration(self):
for plan_log in self:
# Not started yet
if not plan_log.start_date:
continue
# If plan is finished, compute duration
if plan_log.finish_date:
plan_log.update(
{
"duration": (
plan_log.finish_date - plan_log.start_date
).total_seconds(),
"is_running": False,
}
)
continue
# If plan is running, set is_running to True
plan_log.is_running = True
@api.depends("is_running")
def _compute_duration_current(self):
"""Shows relative time between now() and start time for running plans,
and computed duration for finished ones.
"""
now = fields.Datetime.now()
for plan_log in self:
if plan_log.is_running:
plan_log.duration_current = (now - plan_log.start_date).total_seconds()
else:
plan_log.duration_current = plan_log.duration
def start(self, server, plan, start_date=None, **kwargs):
"""
Runs plan on server.
Creates initial log records for each command that cannot be executed until
it finds the first executable command.
Args:
server (cx.tower.server()) server.
plan (cx.tower.plan()) Flight Plan.
start_date (datetime) flight plan start date time.
**kwargs (dict): optional values
Following keys are supported but not limited to:
- "plan_log": {values passed to flightplan logger}
- "log": {values passed to logger}
- "key": {values passed to key parser}
- "no_command_log" (bool): If True, no logs will be recorded for
non-executable lines.
- "variable_values", dict(): custom variable values
in the format of `{variable_reference: variable_value}`
eg `{'odoo_version': '16.0'}`
Will be applied only if user has write access to the server.
Returns:
cx.tower.plan.log(): New flightplan log record.
"""
def get_executable_line(plan, server, variable_values=None):
"""
Generator to get each line and check if it's executable.
"""
for line in plan.line_ids:
yield (
line,
line._is_executable_line(server, variable_values=variable_values),
)
vals = {
"server_id": server.id,
"plan_id": plan.id,
"is_running": True,
"start_date": start_date or fields.Datetime.now(),
}
# Extract and apply plan log kwargs
plan_log_kwargs = kwargs.get("plan_log")
if plan_log_kwargs:
vals.update(plan_log_kwargs)
# Extract and apply variable values
variable_values = kwargs.get("variable_values")
if variable_values:
vals["variable_values"] = variable_values
plan_log = self.sudo().create(vals)
# Process each line until the first executable one is found
for line, is_executable in get_executable_line(
plan, server, variable_values=variable_values
):
if is_executable:
line._run(server, plan_log, **kwargs)
break
else:
if self._context.get("no_command_log"):
continue
line._skip(
server,
plan_log,
log={"variable_values": dict(variable_values or {})},
)
break
else:
plan_log.finish(plan_status=PLAN_IS_EMPTY)
return plan_log
def stop(self):
"""
Force stop this plan log (and currently running command if possible).
"""
user_name = self.env.user.name
for log in self:
if not log.is_running:
continue
# Finish plan log
log.finish(
plan_status=PLAN_STOPPED,
custom_message=_("Stopped by user %(user)s", user=user_name),
is_stopped=True,
)
# Stop running command
running_cmd_logs = log.command_log_ids.filtered(lambda c: c.is_running)
running_cmd_logs.stop()
def action_stop(self):
"""
Action to stop the running plans.
"""
self.stop()
if len(self) > 1: # more than one plan is running
title = _("Flight Plans Stopped")
message = ", ".join([plan.name for plan in self])
else:
title = _("Flight Plan Stopped")
message = self.name
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": title,
"message": message,
"sticky": False,
"next": {
"type": "ir.actions.act_window_close",
},
},
}
def finish(self, plan_status, **kwargs):
"""Finish plan execution
Args:
plan_status (Integer) plan execution code
**kwargs (dict): optional values
"""
values = {
"is_running": False,
"plan_status": plan_status,
"finish_date": fields.Datetime.now(),
}
self.ensure_one()
# Apply kwargs
if kwargs:
values.update(kwargs)
self.sudo().write(values)
# Call hook
self._plan_finished()
# Check if we were deleting a server
if (
self.server_id._is_being_deleted()
and self.server_id.plan_delete_id == self.plan_id
):
if plan_status == 0:
# And finally delete the server
self.with_context(server_force_delete=True).server_id.unlink()
else:
# Set deletion error if flightplan failed
self.server_id.status = "delete_error"
def record(self, server, plan, status, start_date=None, finish_date=None, **kwargs):
"""
Record plan log without running it.
Args:
server (cx.tower.server()) server.
plan (cx.tower.plan()) Flight Plan.
status (int) plan execution code
start_date (datetime) flight plan start date time.
finish_date (datetime) flight plan finish date time.
**kwargs (dict): optional values
Following keys are supported but not limited to:
- "plan_log": {values passed to flightplan logger}
- "log": {values passed to logger}
- "key": {values passed to key parser}
- "no_command_log" (bool): If True, no logs will be recorded for
non-executable lines.
Returns:
cx.tower.plan.log(): New flightplan log record.
"""
default_date = fields.Datetime.now()
vals = {
"server_id": server.id,
"plan_id": plan.id,
"start_date": start_date or default_date,
"finish_date": finish_date or default_date,
"plan_status": status,
}
# Extract and apply plan log kwargs
plan_log_kwargs = kwargs.get("plan_log")
if plan_log_kwargs:
vals.update(plan_log_kwargs)
plan_log = self.sudo().create(vals)
plan_log._plan_finished()
return plan_log
def _plan_finished(self):
"""Triggered when flightplan in finished
Inherit to implement your own hooks
Returns:
bool: True if event was handled
"""
self.ensure_one()
# Do not notify if a plan that was run from another plan has been executed
if self.parent_flight_plan_log_id:
return True
# Check if notifications are enabled
ICP_sudo = self.env["ir.config_parameter"].sudo()
notification_type_success = ICP_sudo.get_param(
"cetmix_tower_server.notification_type_success"
)
notification_type_error = ICP_sudo.get_param(
"cetmix_tower_server.notification_type_error"
)
# Prepare notifications
if not notification_type_success and not notification_type_error:
return True
# Use context timestamp to avoid timezone issues
context_timestamp = fields.Datetime.context_timestamp(
self, fields.Datetime.now()
)
# Action for button
action = self.env["ir.actions.act_window"]._for_xml_id(
"cetmix_tower_server.action_cx_tower_plan_log"
)
context = self.env.context.copy()
params = dict(context.get("params") or {})
params["button_name"] = _("View Log")
context["params"] = params
# Add record id and context to the action
action.update(
{
"context": context,
"res_id": self.id,
"views": [(False, "form")],
}
)
# Send notification
if self.plan_status == 0 and notification_type_success:
# Success notification
self.create_uid.notify_success(
message=_(
"%(timestamp)s<br/>" "Flight Plan '%(name)s' finished successfully",
name=self.plan_id.name,
timestamp=context_timestamp,
),
title=self.server_id.name,
sticky=notification_type_success == "sticky",
action=action,
)
# Error notification
if self.plan_status != 0 and notification_type_error:
self.create_uid.notify_danger(
message=_(
"%(timestamp)s<br/>"
"Flight Plan '%(name)s'"
" finished with error",
name=self.plan_id.name,
timestamp=context_timestamp,
),
title=self.server_id.name,
sticky=notification_type_error == "sticky",
action=action,
)
return True
def _plan_command_finished(self, command_log):
"""This function is triggered when a command from this log is finished.
Next action is triggered based on command status (ak exit code)
Args:
command_log (cx.tower.command.log()): Command log object
"""
self.ensure_one()
# Prevent scheduling further actions if this log was stopped
if self.is_stopped:
return
# Update plan log variable values from command log
# Overwrite with command log values (last command's values take precedence)
self.variable_values = command_log.variable_values
# Get next line to execute
self.plan_id._run_next_action(command_log) # type: ignore