261 lines
9.1 KiB
Python
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})
|