Ajoute le store d’intrants et la CLI Life-NOC
This commit is contained in:
parent
601bad94b0
commit
a45f41bf03
9 changed files with 275 additions and 5 deletions
|
|
@ -9,3 +9,12 @@
|
||||||
state: reloaded
|
state: reloaded
|
||||||
when: life_noc_reload_icinga | bool
|
when: life_noc_reload_icinga | bool
|
||||||
listen: validate and reload icinga
|
listen: validate and reload icinga
|
||||||
|
|
||||||
|
- name: reload systemd
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
daemon_reload: true
|
||||||
|
|
||||||
|
- name: restart life-noc input api
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: life-noc-input-api
|
||||||
|
state: restarted
|
||||||
|
|
|
||||||
|
|
@ -221,3 +221,28 @@
|
||||||
when:
|
when:
|
||||||
- life_noc_bpm_deploy_enabled | bool
|
- life_noc_bpm_deploy_enabled | bool
|
||||||
- life_noc_bpm_destination_dir is defined
|
- life_noc_bpm_destination_dir is defined
|
||||||
|
|
||||||
|
- name: install api dependencies for inputs service
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name:
|
||||||
|
- python3-fastapi
|
||||||
|
- python3-uvicorn
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
|
||||||
|
- name: deploy life-noc input api systemd unit
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: life-noc-input-api.service.j2
|
||||||
|
dest: /etc/systemd/system/life-noc-input-api.service
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
notify:
|
||||||
|
- reload systemd
|
||||||
|
- restart life-noc input api
|
||||||
|
|
||||||
|
- name: enable and start life-noc input api
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: life-noc-input-api
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Life-NOC Input API
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
WorkingDirectory={{ life_noc_project_root }}
|
||||||
|
ExecStart=/usr/bin/python3 -m uvicorn scripts.life_noc_input_api:app --host 127.0.0.1 --port 8787
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
revue-quotidienne-life-noc:
|
revue-quotidienne-life-noc:
|
||||||
value: "2026-03-14"
|
value: '2026-03-14'
|
||||||
captured_at: "2026-03-14T08:00:00Z"
|
captured_at: '2026-03-14T08:00:00Z'
|
||||||
origin: manual
|
origin: manual
|
||||||
|
|
||||||
revue-hebdomadaire-priorites:
|
revue-hebdomadaire-priorites:
|
||||||
value: "2026-03-10"
|
value: '2026-03-14'
|
||||||
captured_at: "2026-03-14T08:00:00Z"
|
captured_at: '2026-03-14T23:16:41Z'
|
||||||
origin: manual
|
origin: manual
|
||||||
|
|
|
||||||
0
lib/__init__.py
Normal file
0
lib/__init__.py
Normal file
70
lib/life_noc_inputs.py
Normal file
70
lib/life_noc_inputs.py
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
INPUTS_DIR = Path("data/inputs")
|
||||||
|
|
||||||
|
|
||||||
|
def utc_now_iso() -> str:
|
||||||
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
|
||||||
|
def utc_today_date() -> str:
|
||||||
|
return datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
|
||||||
|
def domain_file(domain: str) -> Path:
|
||||||
|
return INPUTS_DIR / f"{domain}.yaml"
|
||||||
|
|
||||||
|
|
||||||
|
def load_domain(domain: str) -> dict:
|
||||||
|
path = domain_file(domain)
|
||||||
|
if not path.exists():
|
||||||
|
return {}
|
||||||
|
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||||
|
if data is None:
|
||||||
|
return {}
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError(f"Fichier invalide: {path}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def save_domain(domain: str, data: dict) -> Path:
|
||||||
|
path = domain_file(domain)
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(
|
||||||
|
yaml.safe_dump(data, sort_keys=False, allow_unicode=True),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def list_items(domain: str) -> list[str]:
|
||||||
|
return list(load_domain(domain).keys())
|
||||||
|
|
||||||
|
|
||||||
|
def get_item(domain: str, item_key: str) -> dict:
|
||||||
|
data = load_domain(domain)
|
||||||
|
if item_key not in data:
|
||||||
|
raise KeyError(item_key)
|
||||||
|
item = data[item_key]
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
raise ValueError(f"Entrée invalide pour {item_key}")
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def set_item(domain: str, item_key: str, value: str, origin: str = "manual") -> dict:
|
||||||
|
data = load_domain(domain)
|
||||||
|
data[item_key] = {
|
||||||
|
"value": value,
|
||||||
|
"captured_at": utc_now_iso(),
|
||||||
|
"origin": origin,
|
||||||
|
}
|
||||||
|
save_domain(domain, data)
|
||||||
|
return data[item_key]
|
||||||
|
|
||||||
|
|
||||||
|
def complete_item(domain: str, item_key: str, origin: str = "manual") -> dict:
|
||||||
|
return set_item(domain, item_key, utc_today_date(), origin=origin)
|
||||||
0
revue-hebdomadaire-priorites
Normal file
0
revue-hebdomadaire-priorites
Normal file
91
scripts/life_noc_input.py
Executable file
91
scripts/life_noc_input.py
Executable file
|
|
@ -0,0 +1,91 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
if str(ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from lib.life_noc_inputs import list_items, get_item, set_item, complete_item
|
||||||
|
|
||||||
|
def cmd_set(args: argparse.Namespace) -> int:
|
||||||
|
set_item(args.domain, args.item_key, args.value, origin=args.origin)
|
||||||
|
print(f"Mis à jour: {args.domain} -> {args.item_key}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_complete(args: argparse.Namespace) -> int:
|
||||||
|
complete_item(args.domain, args.item_key, origin=args.origin)
|
||||||
|
print(f"Complété: {args.domain} -> {args.item_key}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_get(args: argparse.Namespace) -> int:
|
||||||
|
try:
|
||||||
|
item = get_item(args.domain, args.item_key)
|
||||||
|
except KeyError:
|
||||||
|
print(f"Item introuvable: {args.item_key}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(
|
||||||
|
yaml.safe_dump(
|
||||||
|
{args.item_key: item},
|
||||||
|
sort_keys=False,
|
||||||
|
allow_unicode=True,
|
||||||
|
).strip()
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_list(args: argparse.Namespace) -> int:
|
||||||
|
for key in list_items(args.domain):
|
||||||
|
print(key)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(description="Gestion des intrants Life-NOC")
|
||||||
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
p_set = sub.add_parser("set", help="Met à jour un intrant")
|
||||||
|
p_set.add_argument("domain")
|
||||||
|
p_set.add_argument("item_key")
|
||||||
|
p_set.add_argument("value")
|
||||||
|
p_set.add_argument("--origin", default="manual")
|
||||||
|
p_set.set_defaults(func=cmd_set)
|
||||||
|
|
||||||
|
p_complete = sub.add_parser("complete", help="Marque un item comme complété maintenant")
|
||||||
|
p_complete.add_argument("domain")
|
||||||
|
p_complete.add_argument("item_key")
|
||||||
|
p_complete.add_argument("--origin", default="manual")
|
||||||
|
p_complete.set_defaults(func=cmd_complete)
|
||||||
|
|
||||||
|
p_get = sub.add_parser("get", help="Lit un intrant")
|
||||||
|
p_get.add_argument("domain")
|
||||||
|
p_get.add_argument("item_key")
|
||||||
|
p_get.set_defaults(func=cmd_get)
|
||||||
|
|
||||||
|
p_list = sub.add_parser("list", help="Liste les intrants d’un domaine")
|
||||||
|
p_list.add_argument("domain")
|
||||||
|
p_list.set_defaults(func=cmd_list)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = build_parser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
try:
|
||||||
|
return args.func(args)
|
||||||
|
except ValueError as exc:
|
||||||
|
print(f"Erreur: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
61
scripts/life_noc_input_api.py
Normal file
61
scripts/life_noc_input_api.py
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
if str(ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from lib.life_noc_inputs import list_items, get_item, set_item, complete_item # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
class SetInputRequest(BaseModel):
|
||||||
|
value: str
|
||||||
|
origin: str = "manual"
|
||||||
|
|
||||||
|
|
||||||
|
class CompleteInputRequest(BaseModel):
|
||||||
|
origin: str = "manual"
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(title="Life-NOC Input API", version="1.0.0")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
def health() -> dict:
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/inputs/{domain}")
|
||||||
|
def api_list_inputs(domain: str) -> dict:
|
||||||
|
return {"domain": domain, "items": list_items(domain)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/inputs/{domain}/{item_key}")
|
||||||
|
def api_get_input(domain: str, item_key: str) -> dict:
|
||||||
|
try:
|
||||||
|
item = get_item(domain, item_key)
|
||||||
|
except KeyError:
|
||||||
|
raise HTTPException(status_code=404, detail="item introuvable")
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc))
|
||||||
|
return {"domain": domain, "item_key": item_key, **item}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/inputs/{domain}/{item_key}")
|
||||||
|
def api_set_input(domain: str, item_key: str, req: SetInputRequest) -> dict:
|
||||||
|
item = set_item(domain, item_key, req.value, origin=req.origin)
|
||||||
|
return {"domain": domain, "item_key": item_key, **item}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/inputs/{domain}/{item_key}/complete")
|
||||||
|
def api_complete_input(domain: str, item_key: str, req: CompleteInputRequest | None = None) -> dict:
|
||||||
|
origin = "manual" if req is None else req.origin
|
||||||
|
item = complete_item(domain, item_key, origin=origin)
|
||||||
|
return {"domain": domain, "item_key": item_key, **item}
|
||||||
Loading…
Reference in a new issue