import logging from datetime import timedelta from odoo import api, fields, models try: import requests except ImportError: requests = None _logger = logging.getLogger(__name__) HTTP_CHECK_TIMEOUT = 20 # seconds HTTP_KO_CONFIRMATION_DELAY = timedelta(minutes=10) class ServiceInstance(models.Model): _inherit = "service.instance" last_http_status_code = fields.Integer( string="Last HTTP Status Code", readonly=True, default=0, ) last_http_check_date = fields.Datetime( string="Last HTTP Check Date", readonly=True, ) http_status_ok = fields.Boolean( string="HTTP Status OK", readonly=True, default=True, ) http_maintenance_request = fields.Many2one( "maintenance.request", string="HTTP Maintenance Request", readonly=True, ) http_first_ko_at = fields.Datetime( string="First HTTP KO Check (current streak)", readonly=True, ) def check_http_status(self): """ Perform HTTP check for each record and return the KO recordset. Writes last_http_status_code, last_http_check_date and http_status_ok on every checked record. Does NOT create maintenance.request — the cron only opens one once the service has been continuously KO for at least HTTP_KO_CONFIRMATION_DELAY, to avoid flagging transient outages (e.g. a short server overload) as real incidents. """ ko_records = self.browse() for rec in self: if not rec.service_url or not rec.equipment_id: continue if rec.equipment_id.maintenance_mode: continue status_ok = False status_code = -1 now = fields.Datetime.now() url = rec.service_url if not url.lower().startswith("https://"): url = "https://" + url.removeprefix("http://").removeprefix("HTTP://") try: response = requests.get(url, timeout=HTTP_CHECK_TIMEOUT) status_code = response.status_code status_ok = status_code == 200 except requests.exceptions.RequestException as e: _logger.warning("HTTP check failed for %s: %s", rec.service_url, e) rec.write( { "last_http_status_code": status_code, "last_http_check_date": now, "http_status_ok": status_ok, } ) if not status_ok: ko_records |= rec return ko_records def _close_http_maintenance_request(self): """ Close the open maintenance.request for each recovered service. Moves the request to the first done stage, posts a chatter note as OdooBot, and clears http_maintenance_request on the service instance. """ done_stage = self.env["maintenance.stage"].search( [("done", "=", True)], limit=1 ) if not done_stage: return odoobot = self.env.ref("base.partner_root") for rec in self: request = rec.http_maintenance_request if not request or request.stage_id.done: continue request.sudo().write({"stage_id": done_stage.id}) request.sudo().message_post( body=( f"Service {rec.service_url} is back online. " "This request has been automatically closed by the monitoring cron." ), author_id=odoobot.id, message_type="comment", subtype_xmlid="mail.mt_note", ) rec.http_maintenance_request = False @api.model def cron_check_http_services(self): """ Check all active services with a URL. A service must be continuously KO for at least HTTP_KO_CONFIRMATION_DELAY before a maintenance.request is created — this tolerates transient outages (e.g. a temporary server overload) regardless of how often this cron runs. Services that had an open request and are now OK are auto-resolved. """ now = fields.Datetime.now() domain = [ ("active", "=", True), ("service_url", "!=", False), ("equipment_id", "!=", False), ] services = self.search(domain).filtered( lambda s: not s.equipment_id.maintenance_mode ) # Snapshot services that currently have an open request before the check services_with_open_request = services.filtered( lambda s: s.http_maintenance_request and not s.http_maintenance_request.stage_id.done ) ko_services = services.check_http_status() # Auto-resolve services that recovered recovered = services_with_open_request.filtered(lambda s: s.http_status_ok) if recovered: recovered._close_http_maintenance_request() ok_services = services - ko_services if ok_services: ok_services.write({"http_first_ko_at": False}) new_ko = ko_services.filtered(lambda s: not s.http_first_ko_at) if new_ko: new_ko.write({"http_first_ko_at": now}) confirmed_ko = ko_services.filtered( lambda s: now - s.http_first_ko_at >= HTTP_KO_CONFIRMATION_DELAY ) for service in confirmed_ko: service.equipment_id.create_http_maintenance_request(service)