v.0.9 Beta
This commit is contained in:
parent
c9a3292bdd
commit
2b0972a22c
712
README.md
712
README.md
|
|
@ -1,199 +1,444 @@
|
||||||
# Patriot API
|
# 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
|
## 🔐 Authentication
|
||||||
|
|
||||||
All requests require:
|
Every request must include a authorization header:
|
||||||
|
|
||||||
```
|
```http
|
||||||
Authorization: Bearer <API_KEY>
|
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
|
## 📘 Common Status Codes
|
||||||
|
|
||||||
| Status | Meaning |
|
| Status | Meaning |
|
||||||
|--------|---------|
|
|--------|------------------------------------------------|
|
||||||
| **200 OK** | Request successful (read or update) |
|
| **200 OK** | Request successful |
|
||||||
| **201 Created** | Resource successfully created |
|
| **201 Created** | Resource created |
|
||||||
| **204 No Content** | Resource deleted |
|
| **204 No Content** | Resource deleted |
|
||||||
| **401 Unauthorized** | Invalid/missing API key |
|
| **401 Unauthorized** | Missing/invalid/disabled/expired API key |
|
||||||
| **404 Not Found** | Client, zone, or user not found |
|
| **404 Not Found** | Client/zone/user not found |
|
||||||
| **409 Conflict** | Duplicate user number |
|
| **409 Conflict** | Duplicate record, or client temporarily locked |
|
||||||
| **422 Unprocessable Entity** | Validation failed |
|
| **422 Unprocessable Entity** | Validation failed, check request format |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧩 Required Headers
|
## 🧩 Required Headers
|
||||||
|
|
||||||
Some GET requests require client/user IDs in headers:
|
Some GET endpoints require IDs in HTTP headers:
|
||||||
|
|
||||||
| Header | Purpose |
|
| Header | Purpose |
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
| **X-Client-Id** | Selects the client |
|
| **X-Client-Id** | Selects the client |
|
||||||
| **X-User-No** | Selects a specific user |
|
| **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
|
# 📂 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
|
## 🔹 CLIENTS
|
||||||
## =======================
|
|
||||||
|
|
||||||
## **GET /clients**
|
### GET `/clients`
|
||||||
Get full client info.
|
|
||||||
|
|
||||||
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**
|
### PUT `/clients`
|
||||||
Create or update a client (UPSERT).
|
|
||||||
|
|
||||||
Returns:
|
Create or update a client (**UPSERT**).
|
||||||
- `201 Created` if new
|
|
||||||
- `200 OK` if updated
|
|
||||||
|
|
||||||
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": {
|
"info": {
|
||||||
"Name": "Customer Name",
|
"Name": "Ola Nordmann",
|
||||||
"Location": "Address line 1",
|
"Alias": "",
|
||||||
|
"Location": "Produksjonsveien 13",
|
||||||
|
|
||||||
"area_code": "1604",
|
"area_code": "1604",
|
||||||
"area": "City",
|
"area": "Fredrikstad",
|
||||||
"AltLookup": true,
|
|
||||||
"AltAlarmNo": "SIA1000101",
|
"BusPhone": "69310000",
|
||||||
"ConvertType": "None",
|
"Email": "post@ostsik.no",
|
||||||
"NoSigsMon": "None",
|
"OKPassword": "franzjager",
|
||||||
"SinceDays": 0,
|
"SpecRequest": "Dette skal gjøres ved alarm på denne kunden.",
|
||||||
|
|
||||||
|
"NoSigsMon": 1,
|
||||||
|
"SinceDays": 1,
|
||||||
"SinceHrs": 0,
|
"SinceHrs": 0,
|
||||||
"SinceMins": 0,
|
"SinceMins": 30,
|
||||||
|
|
||||||
"ResetNosigsIgnored": true,
|
"ResetNosigsIgnored": true,
|
||||||
"ResetNosigsDays": 0,
|
"ResetNosigsDays": 7,
|
||||||
"ResetNosigsHrs": 0,
|
"ResetNosigsHrs": 0,
|
||||||
"ResetNosigsMins": 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**
|
### PATCH `/clients`
|
||||||
Partial update of a client.
|
|
||||||
|
|
||||||
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": {
|
"info": {
|
||||||
"Location": "New Street 99"
|
"Location": "Ny gate 99"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also update any combination of supported fields, for example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"client_id": 9998,
|
||||||
|
"info": {
|
||||||
|
"BusPhone": "12345678",
|
||||||
|
"SPPage": "Oppdatert tekst"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## **DELETE /clients**
|
### DELETE `/clients`
|
||||||
Delete entire client and its XML file.
|
|
||||||
|
|
||||||
```
|
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": 1234
|
"client_id": 9998
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Response: **204 No Content** on success.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# =======================
|
## 🔹 ZONES
|
||||||
# 🔹 ZONES
|
|
||||||
# =======================
|
### GET `/clients/zones`
|
||||||
|
|
||||||
## **GET /clients/zones**
|
|
||||||
List all zones for a client.
|
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**
|
### POST `/clients/zones/list`
|
||||||
Create or update a zone.
|
|
||||||
|
|
||||||
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_id": 1,
|
||||||
"Zone_area": "Entrance",
|
"Zone_area": "Inngang",
|
||||||
"ModuleNo": 0
|
"ModuleNo": 0
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- `zone_id` must be between **1** and **999**.
|
||||||
|
- Returns **201 Created** for both create and update (by design).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## **DELETE /clients/zones**
|
### DELETE `/clients/zones`
|
||||||
Delete a zone.
|
|
||||||
|
|
||||||
```
|
Delete a specific zone for a client.
|
||||||
|
|
||||||
|
**Body:**
|
||||||
|
|
||||||
|
```json
|
||||||
{
|
{
|
||||||
"client_id": 1234,
|
"client_id": 9998,
|
||||||
"zone_id": 1
|
"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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# =======================
|
### POST `/clients/users`
|
||||||
# 🔹 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.
|
Create a new user.
|
||||||
|
|
||||||
```
|
**Body:**
|
||||||
|
|
||||||
|
```json
|
||||||
{
|
{
|
||||||
"client_id": 1234,
|
"client_id": 9998,
|
||||||
"user": {
|
"user": {
|
||||||
"User_Name": "User Number 1",
|
"User_Name": "User Number 1",
|
||||||
"MobileNo": "+4712345678",
|
"MobileNo": "+4712345678",
|
||||||
|
|
@ -207,14 +452,34 @@ Create a new user.
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If `UserNo` already exists → **409 Conflict**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## **PUT /clients/users**
|
### POST `/clients/users/get`
|
||||||
Update a user (full replace).
|
|
||||||
|
|
||||||
```
|
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": {
|
||||||
"User_Name": "Changed Name",
|
"User_Name": "Changed Name",
|
||||||
"MobileNo": "+4798765432",
|
"MobileNo": "+4798765432",
|
||||||
|
|
@ -222,41 +487,250 @@ Update a user (full replace).
|
||||||
"Email": "new@email.com",
|
"Email": "new@email.com",
|
||||||
"Type": "U",
|
"Type": "U",
|
||||||
"UserNo": 1,
|
"UserNo": 1,
|
||||||
"Instructions": "Updated",
|
"Instructions": "New instructions",
|
||||||
"CallOrder": 0
|
"CallOrder": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
User is identified by `client_id` + `UserNo`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## **DELETE /clients/users**
|
### DELETE `/clients/users`
|
||||||
Delete a user.
|
|
||||||
|
|
||||||
```
|
Delete a specific user.
|
||||||
|
|
||||||
|
**Body:**
|
||||||
|
|
||||||
|
```json
|
||||||
{
|
{
|
||||||
"client_id": 1234,
|
"client_id": 9998,
|
||||||
"user_no": 1
|
"user_no": 1
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
Response: **204 No Content** on success.
|
||||||
|
|
||||||
# 🚀 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
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 📄 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 os
|
||||||
import copy
|
import copy
|
||||||
import time
|
import time
|
||||||
import shutil
|
|
||||||
import socket
|
import socket
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue