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

261 lines
9.1 KiB
Python

# Copyright (C) 2024 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
_logger = logging.getLogger(__name__)
class CxTowerJetRequest(models.Model):
"""
Requests for jets. Issued when there is a jet needed in a specific
state on a server.
Eg. jet "Application" needs a jet "Database" to be in state "Running"
to be able to start.
It looks for an existing jet in the required state and if not found,
creates a jet request.
During the request processing, Tower will try to find and existing jet and
bring it to the required state. Or create a new one if not found.
When a request is finalized, it will report the result to the request issuer
using the callback function.
"""
_name = "cx.tower.jet.request"
_description = "Cetmix Tower Jet Request"
server_id = fields.Many2one(
comodel_name="cx.tower.server",
required=True,
ondelete="cascade",
copy=False,
help="Server where the jet is requested",
)
jet_id = fields.Many2one(
comodel_name="cx.tower.jet",
ondelete="cascade",
string="Serviced by Jet",
copy=False,
help="Jet that is requested",
)
jet_template_id = fields.Many2one(
comodel_name="cx.tower.jet.template",
required=True,
string="Requested Template",
ondelete="cascade",
copy=False,
help="Template of the jet that is requested. "
"Used to create a new jet if not found.",
)
state_requested_id = fields.Many2one(
comodel_name="cx.tower.jet.state",
ondelete="cascade",
copy=False,
help="State of the jet that is requested",
)
requested_by_jet_id = fields.Many2one(
comodel_name="cx.tower.jet",
ondelete="cascade",
string="Requested by Jet",
copy=False,
help="Jet that is requesting the jet",
)
for_dependency_id = fields.Many2one(
comodel_name="cx.tower.jet.dependency",
ondelete="cascade",
copy=False,
help="Dependency for which request is created",
)
state = fields.Selection(
selection=[
("new", "New"),
("processing", "Processing"),
("success", "Success"),
("failed", "Failed"),
],
default="new",
required=True,
copy=False,
)
@api.model
def _create_request(
self,
server,
jet=None,
jet_template=None,
state=None,
requested_by_jet=None,
for_dependency=None,
):
"""
Create a new jet request.
Args:
server (cx.tower.server()): Server to create the request on
jet (cx.tower.jet()): Jet to create the request for
jet_template (cx.tower.jet.template()): Template to create the request for
state (cx.tower.jet.state()): State to create the request for
requested_by_jet (cx.tower.jet()): Jet that is requesting the jet
for_dependency (cx.tower.jet.dependency()): Dependency for which request
is created
Returns:
cx.tower.jet.request(): A jet request for the jet
"""
# Must have either jet or jet template
if not jet and not jet_template:
raise ValidationError(
_("Either a jet or a jet template must be provided to create a request")
)
# Set jet template from the jet if not provided
if not jet_template and jet:
jet.ensure_one()
jet_template = jet.jet_template_id
request = self.env["cx.tower.jet.request"].create(
{
"server_id": server.id,
"jet_id": jet.id if jet else None,
"jet_template_id": jet_template.id if jet_template else None,
"state_requested_id": state.id if state else None,
"requested_by_jet_id": requested_by_jet.id
if requested_by_jet
else None,
"for_dependency_id": for_dependency.id if for_dependency else None,
}
)
# Step 1. Use the existing jet if provided explicitly
if jet:
if jet.server_id != server:
raise ValidationError(
_(
"Jet %(jet)s is not on server %(server)s",
jet=jet.name,
server=server.name,
)
)
if jet.state_id == state and not jet._is_busy():
_logger.info(
"Jet %s is available and not busy, finalizing request", jet.name
)
request._finalize(failed=False)
elif jet.target_state_id == state:
_logger.info(
"Jet %s is transitioning to the target state, "
"waiting for it to finish",
jet.name,
)
jet._serve_jet_request(jet_request=request)
else:
_logger.info(
"Jet %s is not available or busy, triggering jet to "
"bring itself to the required state",
jet.name,
)
jet._serve_jet_request(jet_request=request)
return request
# Step 2. Try to pick any of the existing jets from the template
available_jets = jet_template.jet_ids.filtered(
lambda j: j.server_id == server and j._accepts_new_links()
)
for available_jet in available_jets:
# Finalize the request instantly if the jet state
# matches and jet is not busy
if available_jet.state_id == state and not available_jet._is_busy():
_logger.info(
"Jet %s is available and not busy, finalizing request",
available_jet.name,
)
request.jet_id = available_jet
request._finalize(failed=False)
return request
# Step 3. Jet is available, and is not busy, but not in the required state
transitioning_jets = available_jets.filtered(
lambda j: j.target_state_id == state
)
if transitioning_jets:
_logger.info(
"Jet %s is transitioning to the target state, "
"waiting for it to finish",
transitioning_jets[0].name,
)
# Trigger the jet to bring itself to the required state
request.jet_id = transitioning_jets[0]
return request
# Step 4. Jet is available, and is not busy, but not in the required state
not_busy_jets = available_jets.filtered(lambda j: not j._is_busy())
if not_busy_jets:
# Pick the first available jet
not_busy_jet = not_busy_jets[0]
_logger.info(
"Jet %s is available and not busy, but not in the required state,"
" triggering jet to bring itself to the required state",
not_busy_jet.name,
)
# Trigger the jet to bring itself to the required state
request.jet_id = not_busy_jet
not_busy_jet._serve_jet_request(jet_request=request)
return request
# Step 5. Jet is not available, or is busy and not transitioning
# to the required state - create a new jet
# TODO: Add an option to wait for the jet to become available
if jet_template:
jet_template.ensure_one()
_logger.info("Creating new jet using template %s", jet_template.name)
jet = jet_template.create_jet(server)
if jet:
_logger.info("Created new jet %s", jet.name)
request.jet_id = jet
if jet.state_id == state:
request._finalize(failed=False)
else:
# Trigger the jet to bring itself to the required state
jet._serve_jet_request(jet_request=request)
else:
_logger.error(
"Failed to create new jet using template %s", jet_template.name
)
request._finalize(failed=True)
_logger.info("Jet request creation finished")
return request
def _finalize(self, failed=False):
"""
Finalize a jet request.
Args:
failed (bool): Whether the request failed
"""
self.ensure_one()
# 1. Update the state of the request
self.write(
{
"state": "success" if not failed else "failed",
}
)
# 2. Notify the jet that issued the request
if self.requested_by_jet_id:
self.requested_by_jet_id._finalize_jet_request(self)
# 3. Remove the link to the jet that was handling the request
if self.jet_id and self.jet_id.served_jet_request_id == self:
# Unlink the jet from the request
self.jet_id.sudo().write({"served_jet_request_id": False})