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
|
||||
when: life_noc_reload_icinga | bool
|
||||
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:
|
||||
- life_noc_bpm_deploy_enabled | bool
|
||||
- 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:
|
||||
value: "2026-03-14"
|
||||
captured_at: "2026-03-14T08:00:00Z"
|
||||
value: '2026-03-14'
|
||||
captured_at: '2026-03-14T08:00:00Z'
|
||||
origin: manual
|
||||
|
||||
revue-hebdomadaire-priorites:
|
||||
value: "2026-03-10"
|
||||
captured_at: "2026-03-14T08:00:00Z"
|
||||
value: '2026-03-14'
|
||||
captured_at: '2026-03-14T23:16:41Z'
|
||||
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