v.0.9 Beta
This commit is contained in:
parent
c9a3292bdd
commit
2b0972a22c
716
README.md
716
README.md
|
|
@ -1,199 +1,444 @@
|
|||
# Patriot API
|
||||
|
||||
The **Patriot API** is a multi-tenant REST API used to manage clients, zones, and users and automatically generate XML files in the format required by Patriot.
|
||||
The **Alarm24 Patriot API** is a REST API used to manage clients, zones, and users in the format required by Patriot.
|
||||
|
||||
Each API key has **its own fully isolated dataset**.
|
||||
---
|
||||
|
||||
## 🔐 Authentication
|
||||
|
||||
All requests require:
|
||||
Every request must include a authorization header:
|
||||
|
||||
```
|
||||
```http
|
||||
Authorization: Bearer <API_KEY>
|
||||
```
|
||||
|
||||
If the key is missing, invalid, disabled, or expired → **401 Unauthorized**
|
||||
If the key is missing, invalid, disabled, or expired → **401 Unauthorized**.
|
||||
|
||||
---
|
||||
|
||||
## 📘 Common Status Codes
|
||||
|
||||
| Status | Meaning |
|
||||
|--------|---------|
|
||||
| **200 OK** | Request successful (read or update) |
|
||||
| **201 Created** | Resource successfully created |
|
||||
|--------|------------------------------------------------|
|
||||
| **200 OK** | Request successful |
|
||||
| **201 Created** | Resource created |
|
||||
| **204 No Content** | Resource deleted |
|
||||
| **401 Unauthorized** | Invalid/missing API key |
|
||||
| **404 Not Found** | Client, zone, or user not found |
|
||||
| **409 Conflict** | Duplicate user number |
|
||||
| **422 Unprocessable Entity** | Validation failed |
|
||||
| **401 Unauthorized** | Missing/invalid/disabled/expired API key |
|
||||
| **404 Not Found** | Client/zone/user not found |
|
||||
| **409 Conflict** | Duplicate record, or client temporarily locked |
|
||||
| **422 Unprocessable Entity** | Validation failed, check request format |
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Required Headers
|
||||
|
||||
Some GET requests require client/user IDs in headers:
|
||||
Some GET endpoints require IDs in HTTP headers:
|
||||
|
||||
| Header | Purpose |
|
||||
|--------|---------|
|
||||
| **X-Client-Id** | Selects the client |
|
||||
| **X-User-No** | Selects a specific user |
|
||||
|
||||
All endpoints **also require** the `Authorization: Bearer` header.
|
||||
|
||||
---
|
||||
|
||||
## 🗄 Data Isolation & XML Generation
|
||||
|
||||
### Per-key Data Isolation
|
||||
|
||||
Each API key has its **own completely separate dataset**:
|
||||
|
||||
- Clients, zones, and users are not shared between API keys.
|
||||
- Two different keys can both have a client `1234`, but those are **separate clients** internally.
|
||||
|
||||
### XML Output Structure
|
||||
|
||||
For each API key, XML files for new clients are written to temporary storage before import:
|
||||
|
||||
```text
|
||||
out/clients/<key_name>/<client_id>.xml
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
out/clients/Alarm24Key/9998.xml
|
||||
out/clients/InstallerXKey/1234.xml
|
||||
```
|
||||
|
||||
### `<__id>` with Port Suffix
|
||||
|
||||
Each key in `keys.json` has a `port` field, e.g.:
|
||||
|
||||
```json
|
||||
{
|
||||
"key_name": "Alarm24",
|
||||
"key": "secure-api-key",
|
||||
"enabled": true,
|
||||
"valid_to": "2030-01-01T00:00:00Z",
|
||||
"port": "BASE11"
|
||||
}
|
||||
```
|
||||
|
||||
When generating XML, the `<__id>` element is:
|
||||
|
||||
```xml
|
||||
<__id>9998BASE11</__id>
|
||||
```
|
||||
|
||||
- `9998` = client ID
|
||||
- `BASE11` = `port` value from `keys.json`
|
||||
|
||||
The **XML filename** remains `9998.xml` (client id only).
|
||||
|
||||
### Extra XML Fields Injected Per Key
|
||||
|
||||
Some XML fields are **not provided by the API**, but are automatically injected from `keys.json` for each key:
|
||||
|
||||
Examples (exact names depend on your `keys.json`):
|
||||
|
||||
```xml
|
||||
<Installer>Installer Name</Installer>
|
||||
|
||||
<UseGlobCallOuts>True</UseGlobCallOuts>
|
||||
<ShowOnCallOuts>False</ShowOnCallOuts>
|
||||
<GlobCallOuts>TLF01BASE01</GlobCallOuts>
|
||||
|
||||
<UseGlobCallOuts2>True</UseGlobCallOuts2>
|
||||
<ShowOnCallOuts2>False</ShowOnCallOuts2>
|
||||
<GlobCallOuts2>TLF02BASE01</GlobCallOuts2>
|
||||
|
||||
<AltLookup>True</AltLookup>
|
||||
<AltAlarmNo>CID4BASE01</AltAlarmNo>
|
||||
<ConvertType>None</ConvertType>
|
||||
<SIGINTERPRET>SIADecimal</SIGINTERPRET>
|
||||
|
||||
<ClientGroupings>
|
||||
<ClientGrouping>
|
||||
<Description>Alarm24</Description>
|
||||
<GroupingTypeDescription>Alarm24 Tilgang</GroupingTypeDescription>
|
||||
<GroupingAllowMultiple>True</GroupingAllowMultiple>
|
||||
</ClientGrouping>
|
||||
<ClientGrouping>
|
||||
<Description>Import</Description>
|
||||
<GroupingTypeDescription>Østfold Sikkerhetsservice AS</GroupingTypeDescription>
|
||||
<GroupingAllowMultiple>True</GroupingAllowMultiple>
|
||||
</ClientGrouping>
|
||||
</ClientGroupings>
|
||||
```
|
||||
|
||||
These are **configured per key** and not exposed as API fields, and can therefore not be changed by the client.
|
||||
|
||||
### Auto-added Installer User (UserNo 199)
|
||||
|
||||
For each client, the XML will also contain an additional user (not managed via API):
|
||||
|
||||
```xml
|
||||
<User>
|
||||
<User_Name>Installer Name</User_Name>
|
||||
<Email>installer@email.com</Email>
|
||||
<UserNo>199</UserNo>
|
||||
<CallOrder>0</CallOrder>
|
||||
<Type>N</Type>
|
||||
</User>
|
||||
```
|
||||
|
||||
The actual `User_Name` and `Email` come from the API key configuration (`keys.json`).
|
||||
This is the clients installer company. Make sure the name and e-mail is identical to whats stored in Patriot, otherwise it will generate a new installer on first import.
|
||||
|
||||
---
|
||||
|
||||
# 📂 API Endpoints
|
||||
|
||||
## =======================
|
||||
Below are the available endpoints and request/response formats.
|
||||
|
||||
> **Note:** All examples assume you are also sending the correct `Authorization: Bearer <API_KEY>` header.
|
||||
|
||||
---
|
||||
|
||||
## 🔹 CLIENTS
|
||||
## =======================
|
||||
|
||||
## **GET /clients**
|
||||
Get full client info.
|
||||
### GET `/clients`
|
||||
|
||||
Requires header:
|
||||
Get full client info (info + zones + users).
|
||||
Also **regenerates XML** for this client if the XML file is missing (Only when the client is not present in DB, and is pending import).
|
||||
|
||||
**Headers:**
|
||||
|
||||
```http
|
||||
X-Client-Id: 9998
|
||||
Authorization: Bearer <API_KEY>
|
||||
```
|
||||
X-Client-Id: <client_id>
|
||||
|
||||
**Response (example):**
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": 9998,
|
||||
"info": { ... },
|
||||
"zones": { ... },
|
||||
"users": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **PUT /clients**
|
||||
Create or update a client (UPSERT).
|
||||
### PUT `/clients`
|
||||
|
||||
Returns:
|
||||
- `201 Created` if new
|
||||
- `200 OK` if updated
|
||||
Create or update a client (**UPSERT**).
|
||||
|
||||
Example JSON:
|
||||
- Returns **201 Created** on first creation.
|
||||
- Returns **200 OK** on subsequent updates.
|
||||
|
||||
```
|
||||
**Body (all supported fields):**
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": 1234,
|
||||
"client_id": 9998,
|
||||
"info": {
|
||||
"Name": "Customer Name",
|
||||
"Location": "Address line 1",
|
||||
"Name": "Ola Nordmann",
|
||||
"Alias": "",
|
||||
"Location": "Produksjonsveien 13",
|
||||
|
||||
"area_code": "1604",
|
||||
"area": "City",
|
||||
"AltLookup": true,
|
||||
"AltAlarmNo": "SIA1000101",
|
||||
"ConvertType": "None",
|
||||
"NoSigsMon": "None",
|
||||
"SinceDays": 0,
|
||||
"area": "Fredrikstad",
|
||||
|
||||
"BusPhone": "69310000",
|
||||
"Email": "post@ostsik.no",
|
||||
"OKPassword": "franzjager",
|
||||
"SpecRequest": "Dette skal gjøres ved alarm på denne kunden.",
|
||||
|
||||
"NoSigsMon": 1,
|
||||
"SinceDays": 1,
|
||||
"SinceHrs": 0,
|
||||
"SinceMins": 0,
|
||||
"SinceMins": 30,
|
||||
|
||||
"ResetNosigsIgnored": true,
|
||||
"ResetNosigsDays": 0,
|
||||
"ResetNosigsDays": 7,
|
||||
"ResetNosigsHrs": 0,
|
||||
"ResetNosigsMins": 0,
|
||||
"PanelName": "Panel Type",
|
||||
"PanelSite": "Panel location"
|
||||
|
||||
"InstallDateTime": "2023-02-20",
|
||||
|
||||
"PanelName": "Ajax",
|
||||
"PanelSite": "Stue",
|
||||
"KeypadLocation": "Inngang",
|
||||
"SPPage": "Ekstra informasjon som kan være relevant."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Some fields may be auto-defaulted if omitted, depending on implementation.
|
||||
|
||||
---
|
||||
|
||||
## **PATCH /clients**
|
||||
Partial update of a client.
|
||||
### PATCH `/clients`
|
||||
|
||||
Example updating only the location:
|
||||
Partial update of client info. Only the fields inside `info` that you send will be updated; the rest are left unchanged.
|
||||
|
||||
```
|
||||
**Example: update only the Location:**
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": 1234,
|
||||
"client_id": 9998,
|
||||
"info": {
|
||||
"Location": "New Street 99"
|
||||
"Location": "Ny gate 99"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
You can also update any combination of supported fields, for example:
|
||||
|
||||
## **DELETE /clients**
|
||||
Delete entire client and its XML file.
|
||||
|
||||
```
|
||||
```json
|
||||
{
|
||||
"client_id": 1234
|
||||
"client_id": 9998,
|
||||
"info": {
|
||||
"BusPhone": "12345678",
|
||||
"SPPage": "Oppdatert tekst"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# =======================
|
||||
# 🔹 ZONES
|
||||
# =======================
|
||||
### DELETE `/clients`
|
||||
|
||||
Delete an entire client and its XML file for the current API key.
|
||||
This does **not** touch clients of other API keys, even if they use the same `client_id`.
|
||||
|
||||
**Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": 9998
|
||||
}
|
||||
```
|
||||
|
||||
Response: **204 No Content** on success.
|
||||
|
||||
---
|
||||
|
||||
## 🔹 ZONES
|
||||
|
||||
### GET `/clients/zones`
|
||||
|
||||
## **GET /clients/zones**
|
||||
List all zones for a client.
|
||||
|
||||
Requires:
|
||||
**Headers:**
|
||||
|
||||
```http
|
||||
X-Client-Id: 9998
|
||||
Authorization: Bearer <API_KEY>
|
||||
```
|
||||
X-Client-Id: <client_id>
|
||||
|
||||
**Response example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": 9998,
|
||||
"zones": [
|
||||
{
|
||||
"Zone_No": 1,
|
||||
"Zone_area": "Inngang",
|
||||
"ModuleNo": 0
|
||||
},
|
||||
{
|
||||
"Zone_No": 2,
|
||||
"Zone_area": "Stue",
|
||||
"ModuleNo": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **PUT /clients/zones**
|
||||
Create or update a zone.
|
||||
### POST `/clients/zones/list`
|
||||
|
||||
Example:
|
||||
Alternative way to list zones using JSON body.
|
||||
|
||||
```
|
||||
**Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": 1234,
|
||||
"client_id": 9998
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### PUT `/clients/zones`
|
||||
|
||||
Create or update (UPSERT) a zone.
|
||||
|
||||
**Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": 9998,
|
||||
"zone_id": 1,
|
||||
"Zone_area": "Entrance",
|
||||
"Zone_area": "Inngang",
|
||||
"ModuleNo": 0
|
||||
}
|
||||
```
|
||||
|
||||
- `zone_id` must be between **1** and **999**.
|
||||
- Returns **201 Created** for both create and update (by design).
|
||||
|
||||
---
|
||||
|
||||
## **DELETE /clients/zones**
|
||||
Delete a zone.
|
||||
### DELETE `/clients/zones`
|
||||
|
||||
```
|
||||
Delete a specific zone for a client.
|
||||
|
||||
**Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": 1234,
|
||||
"client_id": 9998,
|
||||
"zone_id": 1
|
||||
}
|
||||
```
|
||||
|
||||
Response: **204 No Content** on success.
|
||||
|
||||
---
|
||||
|
||||
## 🔹 USERS
|
||||
|
||||
### GET `/clients/users`
|
||||
|
||||
List all users for a client.
|
||||
|
||||
**Headers:**
|
||||
|
||||
```http
|
||||
X-Client-Id: 9998
|
||||
Authorization: Bearer <API_KEY>
|
||||
```
|
||||
|
||||
**Response example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": 9998,
|
||||
"users": [
|
||||
{
|
||||
"User_Name": "User Number 1",
|
||||
"MobileNo": "+4712345678",
|
||||
"MobileNoOrder": 1,
|
||||
"Email": "user@email.com",
|
||||
"Type": "U",
|
||||
"UserNo": 1,
|
||||
"Instructions": "Optional",
|
||||
"CallOrder": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> Note: The installer user (UserNo 199) is **in XML only** and not exposed via this API.
|
||||
|
||||
---
|
||||
|
||||
### GET `/clients/users/single`
|
||||
|
||||
Get a single user by number.
|
||||
|
||||
**Headers:**
|
||||
|
||||
```http
|
||||
X-Client-Id: 9998
|
||||
X-User-No: 1
|
||||
Authorization: Bearer <API_KEY>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST `/clients/users/list`
|
||||
|
||||
List all users via JSON body.
|
||||
|
||||
**Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": 9998
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# =======================
|
||||
# 🔹 USERS
|
||||
# =======================
|
||||
### POST `/clients/users`
|
||||
|
||||
## **GET /clients/users**
|
||||
List all users for a client.
|
||||
|
||||
Requires:
|
||||
```
|
||||
X-Client-Id: <client_id>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **GET /clients/users/single**
|
||||
Get one user.
|
||||
|
||||
Headers required:
|
||||
|
||||
```
|
||||
X-Client-Id: <client_id>
|
||||
X-User-No: <user_no>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **POST /clients/users**
|
||||
Create a new user.
|
||||
|
||||
```
|
||||
**Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": 1234,
|
||||
"client_id": 9998,
|
||||
"user": {
|
||||
"User_Name": "User Number 1",
|
||||
"MobileNo": "+4712345678",
|
||||
|
|
@ -207,14 +452,34 @@ Create a new user.
|
|||
}
|
||||
```
|
||||
|
||||
If `UserNo` already exists → **409 Conflict**.
|
||||
|
||||
---
|
||||
|
||||
## **PUT /clients/users**
|
||||
Update a user (full replace).
|
||||
### POST `/clients/users/get`
|
||||
|
||||
```
|
||||
Get one specific user using JSON body.
|
||||
|
||||
**Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": 1234,
|
||||
"client_id": 9998,
|
||||
"user_no": 1
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### PUT `/clients/users`
|
||||
|
||||
Update/replace a user completely.
|
||||
|
||||
**Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": 9998,
|
||||
"user": {
|
||||
"User_Name": "Changed Name",
|
||||
"MobileNo": "+4798765432",
|
||||
|
|
@ -222,41 +487,250 @@ Update a user (full replace).
|
|||
"Email": "new@email.com",
|
||||
"Type": "U",
|
||||
"UserNo": 1,
|
||||
"Instructions": "Updated",
|
||||
"Instructions": "New instructions",
|
||||
"CallOrder": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
User is identified by `client_id` + `UserNo`.
|
||||
|
||||
---
|
||||
|
||||
## **DELETE /clients/users**
|
||||
Delete a user.
|
||||
### DELETE `/clients/users`
|
||||
|
||||
```
|
||||
Delete a specific user.
|
||||
|
||||
**Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": 1234,
|
||||
"client_id": 9998,
|
||||
"user_no": 1
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 🚀 Notes
|
||||
|
||||
### ✔ Every API key has completely isolated data
|
||||
Two different keys can both store client `1234`, but they are **stored separately**.
|
||||
|
||||
### ✔ XML files stored per key
|
||||
Example output directory:
|
||||
|
||||
```
|
||||
out/clients/KeyA/1234.xml
|
||||
out/clients/KeyB/1234.xml
|
||||
```
|
||||
|
||||
### ✔ Deleting a client only deletes it for that API key
|
||||
Response: **204 No Content** on success.
|
||||
|
||||
---
|
||||
|
||||
# 📄 End of README
|
||||
You can run the API as a **systemd service** (recommended) or screen/tmux process.
|
||||
|
||||
Copy "main.py" and "run_server.py" to (`/opt/patriot_api/`)
|
||||
|
||||
Example systemd unit (`/etc/systemd/system/patriot-api.service`):
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Patriot Client Registry API
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
# Run as this user
|
||||
User=root
|
||||
Group=root
|
||||
|
||||
WorkingDirectory=/opt/patriot_api
|
||||
|
||||
# Environment variables (optional overrides)
|
||||
#Environment=DATA_FILE=/opt/patriot_api/data.json
|
||||
Environment=KEY_FILE=/opt/patriot_api/keys.json
|
||||
Environment=XML_DIR=/opt/patriot_api/out/clients
|
||||
|
||||
# Start uvicorn from the venv
|
||||
ExecStart=/opt/patriot_api/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 8081 --proxy-headers
|
||||
|
||||
# Restart behavior
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
# Logging (stdout/stderr go to journalctl)
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable patriot-api.service
|
||||
sudo systemctl start patriot-api.service
|
||||
```
|
||||
|
||||
Logs will be available both in:
|
||||
|
||||
- `/opt/patriot_api/api_requests.log`
|
||||
- `journalctl -u patriot-api.service -f`
|
||||
|
||||
---
|
||||
|
||||
# 🔁 Companion Service: XML Combiner & SMB Uploader
|
||||
|
||||
In addition to the API, there is a separate Python script that:
|
||||
|
||||
1. Scans all per-client XML files from the API:
|
||||
```text
|
||||
/opt/patriot_api/out/clients/**/<client_id>.xml
|
||||
```
|
||||
2. Combines them into a single `clients.xml` file under:
|
||||
```text
|
||||
/opt/patriot_api/ready_for_import/clients.xml
|
||||
```
|
||||
3. Uploads this combined `clients.xml` to a Windows SMB share.
|
||||
4. Tracks import state per client in `client_state.json`.
|
||||
5. Logs missing or failed imports based on a results file.
|
||||
|
||||
The script is designed to run **as a daemon**, sleeping until ~1 minute before the next full hour, then performing a combine/upload run.
|
||||
|
||||
## Script Location & Paths
|
||||
|
||||
Example paths used in the script:
|
||||
|
||||
```python
|
||||
XML_ROOT_PATH = Path("/opt/patriot_api/out/clients") # per-client XMLs from API
|
||||
READY_DIR = Path("/opt/patriot_api/ready_for_import") # combined XML output
|
||||
COMBINED_FILENAME = "clients.xml"
|
||||
|
||||
LOG_FILE = "/opt/patriot_api/xml_combine.log"
|
||||
ERROR_LOG_FILE = "/opt/patriot_api/import_errors.log"
|
||||
|
||||
CLIENT_STATE_FILE = Path("/opt/patriot_api/client_state.json")
|
||||
```
|
||||
|
||||
### Client State Tracking
|
||||
|
||||
The script maintains a JSON file:
|
||||
|
||||
```text
|
||||
/opt/patriot_api/client_state.json
|
||||
```
|
||||
|
||||
After a successful combine+upload, every client `<__id>` in that batch is marked:
|
||||
|
||||
```json
|
||||
{
|
||||
"9998BASE11": {
|
||||
"status": "pending_import",
|
||||
"last_batch": "2025-01-01T12:55:00"
|
||||
},
|
||||
"1234BASE05": {
|
||||
"status": "pending_import",
|
||||
"last_batch": "2025-01-01T12:55:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is used for tracking what was included in which batch.
|
||||
|
||||
### SMB / Windows Share Upload
|
||||
|
||||
The script uploads `clients.xml` to an SMB share using the `pysmb` library.
|
||||
|
||||
Key config:
|
||||
|
||||
```python
|
||||
SMB_ENABLED = True
|
||||
SMB_SERVER_IP = "10.181.149.83"
|
||||
SMB_SERVER_NAME = "PATRIOT"
|
||||
SMB_SHARE_NAME = "api_import"
|
||||
|
||||
SMB_USERNAME = "administrator"
|
||||
SMB_PASSWORD = "...."
|
||||
SMB_DOMAIN = "WORKGROUP"
|
||||
|
||||
SMB_REMOTE_PATH = "clients.xml"
|
||||
SMB_RESULTS_FILENAME = "clients_Import_Results.txt"
|
||||
```
|
||||
|
||||
Upload behavior:
|
||||
|
||||
1. **Optional pre-upload check**:
|
||||
- Attempts to download existing `clients.xml` and `clients_Import_Results.txt` from the share.
|
||||
- Parses all `<__id>` values from the remote `clients.xml`.
|
||||
- For each `__id`, checks whether `clients_Import_Results.txt` contains a line with that id and the phrase `"Completed processing client"`.
|
||||
- Any ids missing a success line are logged into `import_errors.log` with timestamp.
|
||||
|
||||
2. **Rotation of existing clients.xml**:
|
||||
- If `clients.xml` already exists on the share, it is renamed with a timestamp, e.g.:
|
||||
`clients_20250101_120000.xml`
|
||||
|
||||
3. **Upload new clients.xml**:
|
||||
- The new combined XML is uploaded as `clients.xml`.
|
||||
|
||||
4. **Local cleanup**:
|
||||
- If upload succeeds, the per-client XMLs that were included in this batch are deleted from `out/clients/...`.
|
||||
- If upload fails, per-client XMLs are **not** deleted, so the batch can be retried later.
|
||||
|
||||
### Combining Logic
|
||||
|
||||
- The script looks for all `*.xml` files under `XML_ROOT_PATH` (recursively).
|
||||
- It parses each file and extracts `<Row>` elements with a `<__id>` child.
|
||||
- Rows are keyed by `__id`; if the same `__id` appears multiple times, the **last one wins** in the combined XML.
|
||||
- All rows are appended under a single root `<Clients>` element.
|
||||
- A limit `MAX_CLIENTS_PER_RUN` can be set (default 300).
|
||||
|
||||
### Scheduling
|
||||
|
||||
The script is written to run in an infinite loop:
|
||||
|
||||
```python
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
Where `main()` essentially:
|
||||
|
||||
1. Calls `sleep_until_next_run()` → sleeps until ~hh:59 before the next hour.
|
||||
2. Runs `combine_xml_once()`.
|
||||
3. Sleeps 1 second as a safety fallback.
|
||||
4. Repeats.
|
||||
|
||||
You can run it as a **systemd service** (recommended) or screen/tmux process.
|
||||
|
||||
Example systemd unit (`/etc/systemd/system/patriot-xml-combine.service`):
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Patriot XML Combiner & SMB Uploader
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=patriot
|
||||
WorkingDirectory=/opt/patriot_api
|
||||
ExecStart=/usr/bin/python3 /opt/patriot_api/combine_xml.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable patriot-xml-combine.service
|
||||
sudo systemctl start patriot-xml-combine.service
|
||||
```
|
||||
|
||||
Logs will be available both in:
|
||||
|
||||
- `/opt/patriot_api/xml_combine.log`
|
||||
- `journalctl -u patriot-xml-combine.service -f`
|
||||
|
||||
---
|
||||
|
||||
# ✅ Summary
|
||||
|
||||
- The REST API manages **clients, zones, and users**, per API key.
|
||||
- Each API key has **fully isolated data** and a dedicated XML output folder.
|
||||
- `<__id>` in XML is generated as `client_id + port` from `keys.json`.
|
||||
- Extra XML-only fields (installer info, callouts, client groupings, User 199) are injected per key, not via the API.
|
||||
- A companion script periodically **combines per-client XMLs** into one `clients.xml`, uploads it to a Windows SMB share, and tracks client import state.
|
||||
|
||||
This README describes all the endpoints and the companion XML pipeline required for integrating with Patriot.
|
||||
|
|
@ -5,7 +5,6 @@ import logging
|
|||
import os
|
||||
import copy
|
||||
import time
|
||||
import shutil
|
||||
import socket
|
||||
from datetime import datetime, timedelta
|
||||
from io import BytesIO
|
||||
|
|
|
|||
Loading…
Reference in New Issue