[NEW] maintenance_service_http_monitoring: create add-on
Some checks failed
pre-commit / pre-commit (pull_request) Failing after 1m32s
Some checks failed
pre-commit / pre-commit (pull_request) Failing after 1m32s
This commit is contained in:
@@ -0,0 +1,92 @@
|
|||||||
|
# Draft: Monitoring HTTP des services
|
||||||
|
|
||||||
|
## Contexte codebase
|
||||||
|
|
||||||
|
### Architecture des modules
|
||||||
|
```
|
||||||
|
maintenance_server_data (base, maintenance)
|
||||||
|
├── Définit: service, service.version, service.instance, backup.server
|
||||||
|
├── Extend maintenance.equipment: server_ip, service_ids (One2many), backup_*, etc.
|
||||||
|
└── Vues: service list, equipment form (onglet Services)
|
||||||
|
|
||||||
|
maintenance_server_ssh (base, maintenance)
|
||||||
|
├── Extend maintenance.equipment: server_domain, ssh_private_key_path
|
||||||
|
└── Méthode: get_ssh_connection()
|
||||||
|
|
||||||
|
maintenance_server_monitoring (base, maintenance, maintenance_server_ssh)
|
||||||
|
├── Extend maintenance.equipment: monitoring fields + test methods
|
||||||
|
├── Pattern MonitoringTest inner class (test_ok/warning/error)
|
||||||
|
├── Cron: toutes les 1 minute sur ALL equipment
|
||||||
|
├── Création auto de maintenance.request si error/warning
|
||||||
|
└── PAS de dépendance vers maintenance_server_data actuellement !
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modèle service.instance actuel (dans maintenance_server_data)
|
||||||
|
- equipment_id (Many2one → maintenance.equipment)
|
||||||
|
- service_id (Many2one → service, required)
|
||||||
|
- version_id (Many2one → service.version)
|
||||||
|
- service_url (Char) ← URL déjà existante, parfait pour les checks HTTP
|
||||||
|
|
||||||
|
### Pattern de création maintenance.request existant
|
||||||
|
- Vérifie si une request non-done existe déjà avant d'en créer une nouvelle
|
||||||
|
- Stocke la référence sur equipment: error_maintenance_request / warning_maintenance_request
|
||||||
|
- Assignation auto: employee_id, technician_user_id, maintenance_team_id
|
||||||
|
|
||||||
|
## Requirements confirmés (depuis les specs)
|
||||||
|
|
||||||
|
- [x] Modifier le module `maintenance_server_monitoring` (pas de nouveau module)
|
||||||
|
- [x] Ajouter `maintenance_server_data` aux dépendances (nécessaire pour accéder à service.instance)
|
||||||
|
- [x] Étendre `service.instance` avec: last_status_code (Integer), last_check_date (Datetime), status OK/KO (Boolean)
|
||||||
|
- [x] Nouvelle vue liste standalone pour service.instance (nom, version, URL, date check, status code, statut)
|
||||||
|
- [x] Paramètres module: fréquence vérification + durée mode maintenance
|
||||||
|
- [x] Mode maintenance sur equipment: bool tracked + bandeau
|
||||||
|
- [x] Si service KO → création maintenance.request (pas de doublon si toujours KO)
|
||||||
|
|
||||||
|
## Décision architecturale : NOUVEAU MODULE
|
||||||
|
|
||||||
|
**Nom**: `maintenance_service_http_monitoring`
|
||||||
|
**Raison**: Séparation des préoccupations — monitoring infra (SSH/ping) vs monitoring applicatif (HTTP)
|
||||||
|
**Dépendances**: `base`, `maintenance`, `maintenance_server_data`
|
||||||
|
**PAS de dépendance** vers `maintenance_server_ssh` ni `maintenance_server_monitoring`
|
||||||
|
|
||||||
|
## Décisions techniques
|
||||||
|
|
||||||
|
- Paramètres via res.config.settings + ir.config_parameter (pattern standard Odoo)
|
||||||
|
- _inherit = 'service.instance' dans le nouveau module pour étendre le modèle
|
||||||
|
- Cron dédié pour les checks HTTP (fréquence configurable)
|
||||||
|
- _inherit = 'maintenance.equipment' pour ajouter maintenance_mode + http_maintenance_request
|
||||||
|
- mail.thread déjà hérité par maintenance.equipment dans Odoo base → tracking fonctionne
|
||||||
|
|
||||||
|
## Décisions utilisateur (interview)
|
||||||
|
|
||||||
|
1. **Mode maintenance**: HTTP uniquement — le monitoring existant (ping, SSH, mémoire, disque) continue normalement
|
||||||
|
2. **Lien maintenance.request**: Sur maintenance.equipment — nouveau champ `http_maintenance_request` (Many2one). Si plusieurs services KO sur un même equipment, UNE seule request qui liste les services KO.
|
||||||
|
3. **Menu service list**: Sous Maintenance > principal, même niveau que Équipements et Demandes
|
||||||
|
4. **Tests**: OUI — setup pytest-odoo + tests unitaires pour la logique HTTP check et création maintenance requests
|
||||||
|
|
||||||
|
## Scope boundaries
|
||||||
|
|
||||||
|
### IN
|
||||||
|
- NOUVEAU module `maintenance_service_http_monitoring`
|
||||||
|
- Étendre service.instance avec champs monitoring HTTP
|
||||||
|
- Cron dédié pour checks HTTP (fréquence configurable)
|
||||||
|
- Paramètres module (fréquence + durée mode maintenance)
|
||||||
|
- Mode maintenance sur equipment (bool tracked, bandeau, HTTP checks only)
|
||||||
|
- Création maintenance.request si service KO (référence sur equipment)
|
||||||
|
- Nouvelle vue liste services avec colonnes monitoring
|
||||||
|
- Setup pytest-odoo + tests unitaires
|
||||||
|
|
||||||
|
### OUT
|
||||||
|
- Aucune modification de `maintenance_server_monitoring` ni de `maintenance_server_ssh`
|
||||||
|
- Pas de notification email (juste la maintenance.request)
|
||||||
|
- Pas de dashboard / reporting
|
||||||
|
- Pas de tracking sur service.instance (pas de mail.thread sur ce modèle)
|
||||||
|
|
||||||
|
## Décisions Metis (post-gap-analysis)
|
||||||
|
|
||||||
|
- Services orphelins (sans equipment_id): N'existent pas en pratique, filtrés par sécurité
|
||||||
|
- Récupération service KO→OK: Rien d'automatique, close manuelle de la maintenance.request
|
||||||
|
- Définition KO: Tout échec = KO (timeout, DNS, SSL, connexion refusée, code != 200). Log le détail.
|
||||||
|
- Timeout HTTP: Hardcodé (constante, comme les seuils existants dans maintenance_server_monitoring)
|
||||||
|
- `requests` library: Déclarer dans external_dependencies.python
|
||||||
|
- Chaque requests.get() DOIT avoir timeout= (pylintrc enforce external-request-timeout)
|
||||||
File diff suppressed because it is too large
Load Diff
58
maintenance_service_http_monitoring/README.rst
Normal file
58
maintenance_service_http_monitoring/README.rst
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
======================================
|
||||||
|
maintenance_service_http_monitoring
|
||||||
|
======================================
|
||||||
|
|
||||||
|
Module that allows monitoring of different services by checking their HTTP 200 responses:
|
||||||
|
|
||||||
|
- Uses the `maintenance_server_data` service for each device.
|
||||||
|
|
||||||
|
If the service has a URL, a request is made.
|
||||||
|
- Adds maintenance mode for a device:
|
||||||
|
|
||||||
|
- Allows disabling HTTP checks for a specified time (defined in the cron job).
|
||||||
|
|
||||||
|
## Logic:
|
||||||
|
If a request fails and a maintenance task has already been created for the same day,
|
||||||
|
no new task is added.
|
||||||
|
|
||||||
|
The default values for the cron jobs are located in `data/cron.xml`.
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
Use Odoo normal module installation procedure to install
|
||||||
|
``maintenance_service_http_monitoring``.
|
||||||
|
|
||||||
|
Known issues / Roadmap
|
||||||
|
======================
|
||||||
|
|
||||||
|
None yet.
|
||||||
|
|
||||||
|
Bug Tracker
|
||||||
|
===========
|
||||||
|
|
||||||
|
Bugs are tracked on `our issues website <https://github.com/elabore-coop/maintenance-tools/issues>`_. In case of
|
||||||
|
trouble, please check there if your issue has already been
|
||||||
|
reported. If you spotted it first, help us smashing it by providing a
|
||||||
|
detailed and welcomed feedback.
|
||||||
|
|
||||||
|
Credits
|
||||||
|
=======
|
||||||
|
|
||||||
|
Contributors
|
||||||
|
------------
|
||||||
|
|
||||||
|
* Boris Gallet
|
||||||
|
|
||||||
|
Funders
|
||||||
|
-------
|
||||||
|
|
||||||
|
The development of this module has been financially supported by:
|
||||||
|
* Elabore (https://elabore.coop)
|
||||||
|
|
||||||
|
|
||||||
|
Maintainer
|
||||||
|
----------
|
||||||
|
|
||||||
|
This module is maintained by Elabore.
|
||||||
1
maintenance_service_http_monitoring/__init__.py
Normal file
1
maintenance_service_http_monitoring/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
16
maintenance_service_http_monitoring/__manifest__.py
Normal file
16
maintenance_service_http_monitoring/__manifest__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "maintenance_service_http_monitoring",
|
||||||
|
"version": "16.0.1.0.0",
|
||||||
|
"author": "Elabore",
|
||||||
|
"license": "AGPL-3",
|
||||||
|
"category": "Tools",
|
||||||
|
"summary": "Monitor HTTP availability of services",
|
||||||
|
"depends": ["base", "maintenance", "maintenance_server_data"],
|
||||||
|
"external_dependencies": {"python": ["requests"]},
|
||||||
|
"data": [
|
||||||
|
"data/cron.xml",
|
||||||
|
"views/service_instance_views.xml",
|
||||||
|
"views/maintenance_equipment_views.xml"
|
||||||
|
],
|
||||||
|
"installable": True
|
||||||
|
}
|
||||||
23
maintenance_service_http_monitoring/data/cron.xml
Normal file
23
maintenance_service_http_monitoring/data/cron.xml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="ir_cron_http_service_monitoring" model="ir.cron">
|
||||||
|
<field name="name">HTTP Service Monitoring : check all services</field>
|
||||||
|
<field name="model_id" ref="maintenance_server_data.model_service_instance"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">model.cron_check_http_services()</field>
|
||||||
|
<field name="interval_number">15</field>
|
||||||
|
<field name="interval_type">minutes</field>
|
||||||
|
<field name="numbercall">-1</field>
|
||||||
|
<field name="doall">False</field>
|
||||||
|
</record>
|
||||||
|
<record id="ir_cron_maintenance_mode_expiry" model="ir.cron">
|
||||||
|
<field name="name">HTTP Service Monitoring : deactivate expired maintenance mode</field>
|
||||||
|
<field name="model_id" ref="maintenance_server_data.model_maintenance_equipment"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">model.cron_deactivate_expired_maintenance_mode()</field>
|
||||||
|
<field name="interval_number">15</field>
|
||||||
|
<field name="interval_type">minutes</field>
|
||||||
|
<field name="numbercall">-1</field>
|
||||||
|
<field name="doall">False</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
2
maintenance_service_http_monitoring/models/__init__.py
Normal file
2
maintenance_service_http_monitoring/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from . import service_instance
|
||||||
|
from . import maintenance_equipment
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
from odoo import models, fields, api
|
||||||
|
|
||||||
|
class MaintenanceEquipment(models.Model):
|
||||||
|
_inherit = 'maintenance.equipment'
|
||||||
|
|
||||||
|
maintenance_mode = fields.Boolean(
|
||||||
|
string="Maintenance Mode",
|
||||||
|
default=False,
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
maintenance_mode_start = fields.Datetime(
|
||||||
|
string="Maintenance Mode Start",
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
maintenance_mode_end = fields.Datetime(
|
||||||
|
string="Maintenance Mode End",
|
||||||
|
readonly=True,
|
||||||
|
help="Computed from start + configured duration",
|
||||||
|
)
|
||||||
|
http_maintenance_request = fields.Many2one(
|
||||||
|
'maintenance.request',
|
||||||
|
string="HTTP Maintenance Request",
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_activate_maintenance_mode(self):
|
||||||
|
for rec in self:
|
||||||
|
duration = int(self.env['ir.config_parameter'].sudo().get_param(
|
||||||
|
'maintenance_service_http_monitoring.maintenance_mode_duration', 4))
|
||||||
|
now = fields.Datetime.now()
|
||||||
|
rec.write({
|
||||||
|
'maintenance_mode': True,
|
||||||
|
'maintenance_mode_start': now,
|
||||||
|
'maintenance_mode_end': now + timedelta(hours=duration),
|
||||||
|
})
|
||||||
|
|
||||||
|
def action_deactivate_maintenance_mode(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.write({
|
||||||
|
'maintenance_mode': False,
|
||||||
|
'maintenance_mode_start': False,
|
||||||
|
'maintenance_mode_end': False,
|
||||||
|
})
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def cron_deactivate_expired_maintenance_mode(self):
|
||||||
|
now = fields.Datetime.now()
|
||||||
|
expired = self.search([
|
||||||
|
('maintenance_mode', '=', True),
|
||||||
|
('maintenance_mode_end', '<=', now),
|
||||||
|
])
|
||||||
|
expired.action_deactivate_maintenance_mode()
|
||||||
|
|
||||||
|
def create_http_maintenance_request(self, ko_services):
|
||||||
|
self.ensure_one()
|
||||||
|
today = fields.Date.context_today(self)
|
||||||
|
name = f"[HTTP KO] {self.name}"
|
||||||
|
domain = [
|
||||||
|
('name', '=', name),
|
||||||
|
('equipment_id', '=', self.id),
|
||||||
|
('maintenance_type', '=', 'corrective'),
|
||||||
|
('create_date', '>=', f"{today} 00:00:00"),
|
||||||
|
('create_date', '<=', f"{today} 23:59:59"),
|
||||||
|
]
|
||||||
|
existing = self.env['maintenance.request'].search(domain, limit=1)
|
||||||
|
# Check if a task with same name already exist for the day, if it’s the case : skip
|
||||||
|
if existing:
|
||||||
|
self.http_maintenance_request = existing.id
|
||||||
|
return existing
|
||||||
|
request = self.http_maintenance_request
|
||||||
|
if request and not request.stage_id.done:
|
||||||
|
return request
|
||||||
|
vals = {
|
||||||
|
'name': name,
|
||||||
|
'equipment_id': self.id,
|
||||||
|
'priority': '2',
|
||||||
|
'maintenance_type': 'corrective',
|
||||||
|
'description': self._build_ko_services_description(ko_services),
|
||||||
|
}
|
||||||
|
if self.employee_id:
|
||||||
|
vals['employee_id'] = self.employee_id.id
|
||||||
|
if self.technician_user_id:
|
||||||
|
vals['user_id'] = self.technician_user_id.id
|
||||||
|
if self.maintenance_team_id:
|
||||||
|
vals['maintenance_team_id'] = self.maintenance_team_id.id
|
||||||
|
else:
|
||||||
|
team = self.env['maintenance.team'].search([], limit=1)
|
||||||
|
if team:
|
||||||
|
vals['maintenance_team_id'] = team.id
|
||||||
|
request = self.env['maintenance.request'].create(vals)
|
||||||
|
self.http_maintenance_request = request.id
|
||||||
|
return request
|
||||||
|
|
||||||
|
def _build_ko_services_description(self, ko_services):
|
||||||
|
lines = [f"Service KO: {s.service_url or s.name}" for s in ko_services]
|
||||||
|
return '\n'.join(lines)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from odoo import models, fields, api
|
||||||
|
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
except ImportError:
|
||||||
|
requests = None
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
HTTP_CHECK_TIMEOUT = 10 # seconds
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_http_status(self):
|
||||||
|
for rec in self:
|
||||||
|
if not rec.service_url or not rec.equipment_id:
|
||||||
|
continue
|
||||||
|
equipment = rec.equipment_id
|
||||||
|
if getattr(equipment, 'maintenance_mode', False):
|
||||||
|
continue
|
||||||
|
status_ok = False
|
||||||
|
status_code = -1
|
||||||
|
now = fields.Datetime.now()
|
||||||
|
url = rec.service_url
|
||||||
|
if not url.lower().startswith("https://"):
|
||||||
|
url = "https://" + url.lstrip("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(f"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:
|
||||||
|
# Delegate maintenance.request creation to equipment
|
||||||
|
if hasattr(equipment, 'create_http_maintenance_request'):
|
||||||
|
equipment.create_http_maintenance_request([rec])
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def cron_check_http_services(self):
|
||||||
|
domain = [('service_url', '!=', False), ('equipment_id', '!=', False)]
|
||||||
|
services = self.search(domain)
|
||||||
|
for service in services:
|
||||||
|
equipment = service.equipment_id
|
||||||
|
if getattr(equipment, 'maintenance_mode', False):
|
||||||
|
continue
|
||||||
|
service.check_http_status()
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_equipment_form_http_monitoring" model="ir.ui.view">
|
||||||
|
<field name="name">maintenance.equipment.form.http.monitoring</field>
|
||||||
|
<field name="model">maintenance.equipment</field>
|
||||||
|
<field name="inherit_id" ref="maintenance.hr_equipment_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//sheet" position="before">
|
||||||
|
<div class="alert alert-warning" role="alert" attrs="{'invisible': [('maintenance_mode', '=', False)]}">
|
||||||
|
Mode maintenance actif — Vérifications HTTP désactivées.<br/>
|
||||||
|
Fin prévue : <field name="maintenance_mode_end" readonly="1"/>
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//header" position="inside">
|
||||||
|
<button name="action_activate_maintenance_mode" type="object" string="Activer le mode maintenance"
|
||||||
|
attrs="{'invisible': [('maintenance_mode', '=', True)]}" class="oe_highlight"/>
|
||||||
|
<button name="action_deactivate_maintenance_mode" type="object" string="Désactiver le mode maintenance"
|
||||||
|
attrs="{'invisible': [('maintenance_mode', '=', False)]}"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//notebook" position="inside">
|
||||||
|
<page string="HTTP Monitoring">
|
||||||
|
<group>
|
||||||
|
<field name="maintenance_mode"/>
|
||||||
|
<field name="maintenance_mode_start"/>
|
||||||
|
<field name="maintenance_mode_end"/>
|
||||||
|
<field name="http_maintenance_request" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<record id="view_equipment_tree_http_monitoring" model="ir.ui.view">
|
||||||
|
<field name="name">maintenance.equipment.tree.http.monitoring</field>
|
||||||
|
<field name="model">maintenance.equipment</field>
|
||||||
|
<field name="inherit_id" ref="maintenance.hr_equipment_view_tree"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//tree" position="inside">
|
||||||
|
<field name="maintenance_mode" optional="hide"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Inherit from base tree view to add HTTP monitoring fields -->
|
||||||
|
<record id="service_instance_http_monitoring_tree" model="ir.ui.view">
|
||||||
|
<field name="name">service.instance.http.monitoring.tree</field>
|
||||||
|
<field name="model">service.instance</field>
|
||||||
|
<field name="inherit_id" ref="maintenance_server_data.service_instance_view_tree"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree position="attributes">
|
||||||
|
<attribute name="decoration-danger">http_status_ok == False</attribute>
|
||||||
|
</tree>
|
||||||
|
<field name="version_id" position="after">
|
||||||
|
<field name="service_url"/>
|
||||||
|
<field name="last_http_check_date"/>
|
||||||
|
<field name="last_http_status_code"/>
|
||||||
|
<field name="http_status_ok"/>
|
||||||
|
</field>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
Reference in New Issue
Block a user