diff --git a/addons/cetmix_tower_server/models/cx_tower_command.py b/addons/cetmix_tower_server/models/cx_tower_command.py new file mode 100644 index 0000000..fa72ac1 --- /dev/null +++ b/addons/cetmix_tower_server/models/cx_tower_command.py @@ -0,0 +1,550 @@ +# 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"
{key}: {value['help']}custom_values: {_('Flight plan custom values')}tldextract.extract() to parse domains. "
+ "Check tldextract for more information."
+ ),
+ },
+ "dns": {
+ "import": dns,
+ "help": _(
+ "Python 'dnspython' library. "
+ "Documentation."
+ "dns.resolver: "
+ "wrapped dnspython. Use "
+ 'dns.resolver.resolve(hostname, "A") for '
+ "DNS lookups.dns.reversename: wrapped dnspython. "
+ 'Use dns.reversename.from_address("8.8.8.8")'
+ " to build and reverse PTR records.dns.exception: wrapped dnspython. "
+ "Catch "
+ "dns.exception.DNSException to handle "
+ "DNS-related errors.env['cx.tower.server']"),
+ },
+ "tower_commands": {
+ "import": self.env["cx.tower.command"],
+ "help": _("A helper shortcut to env['cx.tower.command']"),
+ },
+ "tower_plans": {
+ "import": self.env["cx.tower.plan"],
+ "help": _("A helper shortcut to env['cx.tower.plan']"),
+ },
+ }
+
+ 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:
+ {
+