551 lines
19 KiB
Python
551 lines
19 KiB
Python
# Copyright (C) 2022 Cetmix OÜ
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
from types import SimpleNamespace
|
|
|
|
from dns import exception, resolver, reversename
|
|
from pytz import timezone
|
|
|
|
from odoo import _, api, fields, models, tools
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools import ormcache
|
|
from odoo.tools.float_utils import float_compare
|
|
from odoo.tools.safe_eval import wrap_module
|
|
|
|
from .constants import DEFAULT_PYTHON_CODE, DEFAULT_PYTHON_CODE_HELP
|
|
|
|
requests = wrap_module(__import__("requests"), ["post", "get", "delete", "request"])
|
|
json = wrap_module(__import__("json"), ["dumps"])
|
|
hashlib = wrap_module(
|
|
__import__("hashlib"),
|
|
[
|
|
"sha1",
|
|
"sha224",
|
|
"sha256",
|
|
"sha384",
|
|
"sha512",
|
|
"sha3_224",
|
|
"sha3_256",
|
|
"sha3_384",
|
|
"sha3_512",
|
|
"shake_128",
|
|
"shake_256",
|
|
"blake2b",
|
|
"blake2s",
|
|
"md5",
|
|
"new",
|
|
],
|
|
)
|
|
hmac = wrap_module(
|
|
__import__("hmac"),
|
|
["new", "compare_digest"],
|
|
)
|
|
tldextract = wrap_module(__import__("tldextract"), ["extract"])
|
|
dns_resolver = wrap_module(resolver, ["resolve", "query"])
|
|
dns_reversename = wrap_module(reversename, ["from_address", "to_address"])
|
|
dns_exception = wrap_module(exception, ["DNSException"])
|
|
|
|
|
|
dns = SimpleNamespace(
|
|
resolver=dns_resolver,
|
|
reversename=dns_reversename,
|
|
exception=dns_exception,
|
|
)
|
|
|
|
|
|
class CxTowerCommand(models.Model):
|
|
"""Command to run on a server"""
|
|
|
|
_name = "cx.tower.command"
|
|
_inherit = [
|
|
"cx.tower.template.mixin",
|
|
"cx.tower.reference.mixin",
|
|
"cx.tower.access.mixin",
|
|
"cx.tower.access.role.mixin",
|
|
"cx.tower.key.mixin",
|
|
"cx.tower.tag.mixin",
|
|
]
|
|
_description = "Cetmix Tower Command"
|
|
_order = "name"
|
|
|
|
active = fields.Boolean(default=True)
|
|
allow_parallel_run = fields.Boolean(
|
|
help="If enabled, multiple instances of the same command "
|
|
"can be run on the same server at the same time.\n"
|
|
"Otherwise, ANOTHER_COMMAND_RUNNING status will be returned if another"
|
|
" instance of the same command is already running"
|
|
)
|
|
server_ids = fields.Many2many(
|
|
comodel_name="cx.tower.server",
|
|
relation="cx_tower_server_command_rel",
|
|
column1="command_id",
|
|
column2="server_id",
|
|
string="Servers",
|
|
help="Servers on which the command will be run.\n"
|
|
"If empty, command can be run on all servers",
|
|
)
|
|
tag_ids = fields.Many2many(
|
|
relation="cx_tower_command_tag_rel",
|
|
column1="command_id",
|
|
column2="tag_id",
|
|
)
|
|
os_ids = fields.Many2many(
|
|
comodel_name="cx.tower.os",
|
|
relation="cx_tower_os_command_rel",
|
|
column1="command_id",
|
|
column2="os_id",
|
|
string="OSes",
|
|
)
|
|
note = fields.Text()
|
|
|
|
action = fields.Selection(
|
|
selection=lambda self: self._selection_action(),
|
|
required=True,
|
|
default=lambda self: self._selection_action()[0][0],
|
|
)
|
|
path = fields.Char(
|
|
string="Default Path",
|
|
help="Location where command will be run. "
|
|
"You can use {{ variables }} in path",
|
|
)
|
|
file_template_id = fields.Many2one(
|
|
comodel_name="cx.tower.file.template",
|
|
help="This template will be used to create or update the pushed file",
|
|
)
|
|
template_code = fields.Text(
|
|
string="Template Code",
|
|
related="file_template_id.code",
|
|
readonly=True,
|
|
help="Code of the associated file template",
|
|
)
|
|
flight_plan_line_ids = fields.One2many(
|
|
comodel_name="cx.tower.plan.line",
|
|
related="flight_plan_id.line_ids",
|
|
readonly=True,
|
|
help="Lines of the associated flight plan",
|
|
)
|
|
code = fields.Text(
|
|
compute="_compute_code",
|
|
store=True,
|
|
readonly=False,
|
|
)
|
|
command_help = fields.Html(
|
|
compute="_compute_command_help",
|
|
compute_sudo=True,
|
|
)
|
|
flight_plan_id = fields.Many2one(
|
|
comodel_name="cx.tower.plan",
|
|
help="Flight plan run by the command",
|
|
)
|
|
flight_plan_used_ids = fields.Many2many(
|
|
comodel_name="cx.tower.plan",
|
|
help="Flight plan this command is used in",
|
|
relation="cx_tower_command_flight_plan_used_id_rel",
|
|
column1="command_id",
|
|
column2="plan_id",
|
|
store=True,
|
|
copy=False,
|
|
)
|
|
flight_plan_used_ids_count = fields.Integer(
|
|
compute="_compute_flight_plan_used_ids_count",
|
|
help="Flight plan this command is used in",
|
|
)
|
|
server_status = fields.Selection(
|
|
selection=lambda self: self.env["cx.tower.server"]._selection_status(),
|
|
help="Set the following status if command finishes with success. "
|
|
"Leave 'Undefined' if you don't need to update the status",
|
|
)
|
|
no_split_for_sudo = fields.Boolean(
|
|
string="No Split for sudo",
|
|
help="If enabled, do not split command on '&&' when using sudo."
|
|
"Prepend sudo once to the whole command.",
|
|
)
|
|
variable_ids = fields.Many2many(
|
|
comodel_name="cx.tower.variable",
|
|
relation="cx_tower_command_variable_rel",
|
|
column1="command_id",
|
|
column2="variable_id",
|
|
)
|
|
|
|
if_file_exists = fields.Selection(
|
|
selection=[
|
|
("skip", "Skip"),
|
|
("overwrite", "Overwrite"),
|
|
("raise", "Raise Error"),
|
|
],
|
|
default="skip",
|
|
help="What to do if file already exists on the server.\n"
|
|
"- Skip: Do not create or update the file.\n"
|
|
"- Overwrite: Replace the existing file with the new one.\n"
|
|
"- Raise Error: Raise an error if the file already exists.",
|
|
)
|
|
disconnect_file = fields.Boolean(
|
|
string="Disconnect from Template",
|
|
help=(
|
|
"If enabled, disconnects the file from its template "
|
|
"after running the command.\n"
|
|
),
|
|
)
|
|
|
|
# ---- Access. Add relation for mixin fields
|
|
user_ids = fields.Many2many(
|
|
relation="cx_tower_command_user_rel",
|
|
)
|
|
manager_ids = fields.Many2many(
|
|
relation="cx_tower_command_manager_rel",
|
|
)
|
|
|
|
@classmethod
|
|
def _get_depends_fields(cls):
|
|
"""
|
|
Define dependent fields for computing `variable_ids` in command-related models.
|
|
|
|
This implementation specifies that the fields `code` and `path`
|
|
are used to determine the variables associated with a command.
|
|
|
|
Returns:
|
|
list: A list of field names (str) representing the dependencies.
|
|
|
|
Example:
|
|
The following fields trigger recomputation of `variable_ids`:
|
|
- `code`: The command's script or running logic.
|
|
- `path`: The default running path for the command.
|
|
"""
|
|
return ["code", "path"]
|
|
|
|
# -- Selection
|
|
def _selection_action(self):
|
|
"""Actions that can be run by a command.
|
|
|
|
Returns:
|
|
List of tuples: available options.
|
|
"""
|
|
return [
|
|
("ssh_command", "SSH command"),
|
|
("python_code", "Run Python code"),
|
|
("file_using_template", "Create file using template"),
|
|
("plan", "Run flight plan"),
|
|
]
|
|
|
|
# -- Defaults
|
|
def _get_default_python_code(self):
|
|
"""
|
|
Default python command code
|
|
"""
|
|
return DEFAULT_PYTHON_CODE
|
|
|
|
def _get_default_python_code_help(self):
|
|
"""
|
|
Default python code help
|
|
"""
|
|
|
|
# Available libraries are Odoo objects + Python libraries
|
|
available_libraries = self._get_python_command_odoo_objects()
|
|
available_libraries.update(self._get_python_command_libraries())
|
|
help_text_fragments = []
|
|
for key, value in available_libraries.items():
|
|
help_text_fragments.append(f"<li><code>{key}</code>: {value['help']}</li>")
|
|
|
|
help_text_fragments.append(
|
|
f"<li><code>custom_values</code>: {_('Flight plan custom values')}</li>"
|
|
)
|
|
|
|
help_text = "<ul>" + "".join(help_text_fragments) + "</ul>"
|
|
return f"{DEFAULT_PYTHON_CODE_HELP}{help_text}"
|
|
|
|
# -- Computes
|
|
@api.depends("action")
|
|
def _compute_code(self):
|
|
"""
|
|
Compute default code
|
|
"""
|
|
default_python_code = self._get_default_python_code()
|
|
for command in self:
|
|
if command.action == "python_code":
|
|
command.code = default_python_code
|
|
continue
|
|
command.code = False
|
|
|
|
@api.depends("action")
|
|
def _compute_command_help(self):
|
|
"""
|
|
Compute command help
|
|
"""
|
|
default_python_code_help = self._get_default_python_code_help()
|
|
for command in self:
|
|
if command.action == "python_code":
|
|
command.command_help = default_python_code_help
|
|
else:
|
|
command.command_help = False
|
|
|
|
@api.depends("flight_plan_used_ids")
|
|
def _compute_flight_plan_used_ids_count(self):
|
|
"""
|
|
Compute flight plan ids count
|
|
"""
|
|
for command in self:
|
|
command.flight_plan_used_ids_count = len(command.flight_plan_used_ids)
|
|
|
|
def action_open_command_logs(self):
|
|
"""
|
|
Open current current command log records
|
|
"""
|
|
action = self.env["ir.actions.actions"]._for_xml_id(
|
|
"cetmix_tower_server.action_cx_tower_command_log"
|
|
)
|
|
action["domain"] = [("command_id", "=", self.id)]
|
|
return action
|
|
|
|
def action_open_plans(self):
|
|
"""
|
|
Open plans this command is used in
|
|
"""
|
|
action = self.env["ir.actions.actions"]._for_xml_id(
|
|
"cetmix_tower_server.action_cx_tower_plan"
|
|
)
|
|
action["domain"] = [("id", "in", self.flight_plan_used_ids.ids)]
|
|
return action
|
|
|
|
def _check_server_compatibility(self, server):
|
|
"""Check if the command is compatible with the server
|
|
Args:
|
|
server (cx.tower.server()): Server object
|
|
|
|
Returns:
|
|
bool: True if the command is compatible with the server, False otherwise
|
|
"""
|
|
self.ensure_one()
|
|
return not self.server_ids or server.id in self.server_ids.ids
|
|
|
|
# -- Business logic
|
|
@ormcache()
|
|
@api.model
|
|
def _get_python_command_libraries(self):
|
|
"""
|
|
Get available python imports. Use this method to import python libraries.
|
|
Please be advised, that this method is cached.
|
|
If you need to use a non-cached import, eg for Odoo objects,
|
|
use the `_get_python_command_odoo_objects` method instead.
|
|
|
|
|
|
Returns:
|
|
dict: Available libraries:
|
|
{"<library_name>": {
|
|
"import": <library_import>,
|
|
"help": <library_help_html>
|
|
}}
|
|
"""
|
|
python_libraries = {
|
|
"time": {
|
|
"import": tools.safe_eval.time,
|
|
"help": _("Python 'time' library"),
|
|
},
|
|
"datetime": {
|
|
"import": tools.safe_eval.datetime,
|
|
"help": _("Python 'datetime' library"),
|
|
},
|
|
"dateutil": {
|
|
"import": tools.safe_eval.dateutil,
|
|
"help": _("Python 'dateutil' library"),
|
|
},
|
|
"timezone": {
|
|
"import": timezone,
|
|
"help": _("Python 'timezone' library"),
|
|
},
|
|
"requests": {
|
|
"import": requests,
|
|
"help": _(
|
|
"Python 'requests' library. Available methods: 'post', 'get',"
|
|
" 'delete', 'request'"
|
|
),
|
|
},
|
|
"json": {
|
|
"import": json,
|
|
"help": _("Python 'json' library. Available methods: 'dumps'"),
|
|
},
|
|
"float_compare": {
|
|
"import": float_compare,
|
|
"help": _("Float compare. Odoo helper function to compare floats."),
|
|
},
|
|
"UserError": {
|
|
"import": UserError,
|
|
"help": _("UserError. Helper to raise UserError."),
|
|
},
|
|
"hashlib": {
|
|
"import": hashlib,
|
|
"help": _(
|
|
"Python 'hashlib' library. "
|
|
"<a href='https://docs.python.org/3/library/hashlib.html'"
|
|
" target='_blank'>Documentation</a>. "
|
|
"Available methods: 'sha1', 'sha224', "
|
|
"'sha256', 'sha384',"
|
|
" 'sha512', 'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512', "
|
|
"'shake_128', 'shake_256',"
|
|
" 'blake2b', 'blake2s', 'md5', 'new'"
|
|
),
|
|
},
|
|
"hmac": {
|
|
"import": hmac,
|
|
"help": _(
|
|
"Python 'hmac' library. "
|
|
"<a href='https://docs.python.org/3/library/hmac.html'"
|
|
" target='_blank'>Documentation</a>. "
|
|
"Use 'new' to create HMAC objects. "
|
|
"Available methods on the HMAC *object*: 'update', 'copy',"
|
|
" 'digest', 'hexdigest'. "
|
|
" Module-level function: 'compare_digest'."
|
|
),
|
|
},
|
|
"tldextract": {
|
|
"import": tldextract,
|
|
"help": _(
|
|
"Python 'tldextract' library. Use "
|
|
"<code>tldextract.extract()</code> to parse domains. "
|
|
"Check <a href='https://github.com/john-kurkowski/tldextract'"
|
|
" target='_blank'>tldextract</a> for more information."
|
|
),
|
|
},
|
|
"dns": {
|
|
"import": dns,
|
|
"help": _(
|
|
"Python 'dnspython' library. "
|
|
"<a href='https://dnspython.readthedocs.io'"
|
|
" target='_blank'>Documentation</a>."
|
|
"<ul><li><code>dns.resolver</code>: "
|
|
"wrapped dnspython. Use "
|
|
'<code>dns.resolver.resolve(hostname, "A")</code> for '
|
|
"DNS lookups.</li>"
|
|
"<li><code>dns.reversename</code>: wrapped dnspython. "
|
|
'Use <code>dns.reversename.from_address("8.8.8.8")</code>'
|
|
" to build and reverse PTR records.</li>"
|
|
"<li><code>dns.exception</code>: wrapped dnspython. "
|
|
"Catch "
|
|
"<code>dns.exception.DNSException</code> to handle "
|
|
"DNS-related errors.</li>"
|
|
"</ul>"
|
|
),
|
|
},
|
|
}
|
|
custom_python_libraries = self._custom_python_libraries()
|
|
for libraries in custom_python_libraries.values():
|
|
python_libraries.update(libraries)
|
|
return python_libraries
|
|
|
|
def _get_python_command_odoo_objects(self, server=None):
|
|
"""
|
|
This method is used to import Odoo objects.
|
|
Because Odoo objects can be records, this method is not cached.
|
|
Use this method to import Odoo objects that are not cached.
|
|
If you need to import some static objects, use the
|
|
`_get_python_command_libraries` method instead.
|
|
|
|
Args:
|
|
server: Server to get the Odoo objects for.
|
|
|
|
Returns:
|
|
dict: Available Odoo objects:
|
|
{"<object_name>": {
|
|
"import": <object_import>,
|
|
"help": <object_help_html>
|
|
}}
|
|
"""
|
|
return {
|
|
"uid": {"import": self._uid, "help": _("Current Odoo user ID")},
|
|
"user": {"import": self.env.user, "help": _("Current Odoo user")},
|
|
"env": {"import": self.env, "help": _("Odoo Environment")},
|
|
"server": {
|
|
"import": server,
|
|
"help": _("Current Cetmix Tower server this command is running on"),
|
|
},
|
|
"tower": {
|
|
"import": self.env["cetmix.tower"],
|
|
"help": _(
|
|
"Cetmix Tower "
|
|
"<a href='https://cetmix.com/tower/documentation/odoo_automation'"
|
|
" target='_blank'>helper class</a> shortcut"
|
|
),
|
|
},
|
|
"tower_servers": {
|
|
"import": self.env["cx.tower.server"],
|
|
"help": _("A helper shortcut to <code>env['cx.tower.server']</code>"),
|
|
},
|
|
"tower_commands": {
|
|
"import": self.env["cx.tower.command"],
|
|
"help": _("A helper shortcut to <code>env['cx.tower.command']</code>"),
|
|
},
|
|
"tower_plans": {
|
|
"import": self.env["cx.tower.plan"],
|
|
"help": _("A helper shortcut to <code>env['cx.tower.plan']</code>"),
|
|
},
|
|
}
|
|
|
|
def _custom_python_libraries(self):
|
|
"""
|
|
This function is designed to be used in custom modules
|
|
extending Cetmix Tower to add custom python libraries
|
|
to the evaluation context.
|
|
|
|
Returns:
|
|
Dict: Custom python libraries.
|
|
|
|
The following format is used:
|
|
{
|
|
<module_name>: {"<library_name>": {
|
|
"import": <library_import>,
|
|
"help": <library_help_html>
|
|
}
|
|
}
|
|
|
|
Where:
|
|
|
|
<module_name> Odoo module technical name.
|
|
<library_name> is the name of the library how it will be used in the code.
|
|
<library_import> is the library to import.
|
|
<library_help_html> is the help text for the library shown in the "Help" tab.
|
|
|
|
Example:
|
|
|
|
```python
|
|
# Custom module extending Cetmix Tower
|
|
custom_python_libraries = super()._custom_python_libraries()
|
|
custom_python_libraries.update({
|
|
"cetmix_tower_aws": {
|
|
"boto3": {
|
|
"import": boto3,
|
|
"help": "Python 'boto3' library. "
|
|
"<a href='https://boto3.amazonaws.com/v1/documentation/api/latest/index.html'"
|
|
" target='_blank'>Documentation</a>."
|
|
},
|
|
"custom_library_name": {
|
|
"import": custom_library_import,
|
|
"help": "Custom library help text"
|
|
}
|
|
}
|
|
})
|
|
return custom_python_libraries
|
|
|
|
```
|
|
"""
|
|
return {}
|
|
|
|
def _get_python_command_eval_context(self, server=None, **kwargs):
|
|
"""
|
|
Get the evaluation context for the python command.
|
|
This method is used to get the evaluation context for the python command.
|
|
|
|
Args:
|
|
server: Server to get the evaluation context for.
|
|
|
|
Returns:
|
|
dict: Evaluation context for the python command.
|
|
"""
|
|
|
|
# Get the Odoo objects first
|
|
imports = self._get_python_command_odoo_objects(server=server)
|
|
|
|
# Update with the libraries
|
|
imports.update(self._get_python_command_libraries())
|
|
eval_context = {key: value["import"] for key, value in imports.items()}
|
|
|
|
eval_context["custom_values"] = kwargs.get("variable_values", {})
|
|
return eval_context
|