This commit is contained in:
Jokob-sk
2023-07-22 12:37:53 +10:00
parent bcea3e1a97
commit b1f946e1e6
24 changed files with 0 additions and 0 deletions

View File

@@ -1,529 +0,0 @@
## 📚 Docs for individual plugins
### Script based plugins
- [website_monitor (WEBMON)](https://github.com/jokob-sk/Pi.Alert/blob/main/front/plugins/website_monitor/)
- [dhcp_servers (DHCPSRVS)](https://github.com/jokob-sk/Pi.Alert/blob/main/front/plugins/dhcp_servers/)
- [dhcp_leases (DHCPLSS)](https://github.com/jokob-sk/Pi.Alert/blob/main/front/plugins/dhcp_leases/)
- [unifi_import (UNFIMP)](https://github.com/jokob-sk/Pi.Alert/blob/main/front/plugins/unifi_import/)
- [snmp_discovery (SNMPDSC)](https://github.com/jokob-sk/Pi.Alert/blob/main/front/plugins/snmp_discovery/)
- [undiscoverables (UNDIS)](https://github.com/jokob-sk/Pi.Alert/blob/main/front/plugins/undiscoverables/)
### SQL query based plugins
- [nmap_services (NMAPSERV)](https://github.com/jokob-sk/Pi.Alert/blob/main/front/plugins/nmap_services/)
### template based plugins
- [newdev_template (NEWDEV)](https://github.com/jokob-sk/Pi.Alert/blob/main/front/plugins/newdev_template/)
## 🌟 Create a custom plugin: Overview
| ![Screen 1][screen1] | ![Screen 2][screen2] | ![Screen 3][screen3] |
|----------------------|----------------------| ----------------------|
| ![Screen 4][screen4] | ![Screen 5][screen5] |
PiAlert comes with a plugin system to feed events from third-party scripts into the UI and then send notifications, if desired. The highlighted core functionality this plugin system supports, is:
* dynamic creation of a simple UI to interact with the discovered objects,
* filtering of displayed values in the Devices UI
* surface settings of plugins in the UI,
* different column types for reported values to e.g. link back to a device
* import objects into existing PiAlert database tables
> (Currently, update/overwriting of existing objects is not supported.)
Example use cases for plugins could be:
* Monitor a web service and alert me if it's down
* Import devices from dhcp.leases files instead/complementary to using PiHole or arp-scans
* Creating ad-hoc UI tables from existing data in the PiAlert database, e.g. to show all open ports on devices, to list devices that disconnected in the last hour, etc.
* Using other device discovery methods on the network and importing the data as new devices
* Creating a script to create FAKE devices based on user input via custom settings
* ...at this point the limitation is mostly the creativity than the capability (there might be edge cases and a need to support more form controls for user input off custom settings, but you probably get the idea)
If you wish to develop a plugin, please check the existing plugin structure. Once the settings are saved by the user they need to be removed from the `pialert.conf` file manually if you want to re-initialize them from the `config.json` of the plugin.
Again, please read the below carefully if you'd like to contribute with a plugin yourself. This documentation file might be outdated, so double-check the sample plugins as well.
## ⚠ Disclaimer
Follow the below very carefully and check example plugin(s) if you'd like to write one yourself. Plugin UI is not my priority right now, happy to approve PRs if you are interested in extending/improving the UI experience. Example improvements for the taking:
* Making the tables sortable/filterable
* Using the same approach to display table data as in the Devices section (solves above)
* Adding form controls supported to display the data (Currently supported ones are listed in the section "UI settings in database_column_definitions" below)
* ...
## ❗ Known issues:
These issues will be hopefully fixed with time, so please don't report them. Instead, if you know how, feel free to investigate and submit a PR to fix the below. Keep the PRs small as it's easier to approve them:
* Existing plugin objects sometimes not interpreted correctly and a new object is created instead, resulting in duplicate entries.
* Occasional (experienced twice) hanging of processing plugin script file.
UI displays outdated values until the API endpoints get refreshed.
## Plugin file structure overview
> Folder name must be the same as the code name value in: `"code_name": "<value>"`
> Unique prefix needs to be unique compared to the other settings prefixes, e.g.: the prefix `APPRISE` is already in use.
| File | Required (plugin type) | Description |
|----------------------|----------------------|----------------------|
| `config.json` | yes | Contains the plugin configuration (manifest) including the settings available to the user. |
| `script.py` | yes (script) | The Python script itself |
| `last_result.log` | yes (script) | The file used to interface between PiAlert and the plugin (script). |
| `script.log` | no | Logging output (recommended) |
| `README.md` | no | Any setup considerations or overview (recommended) |
More on specifics below.
### Column order and values
| Order | Represented Column | Required | Description |
|----------------------|----------------------|----------------------|----------------------|
| 0 | `Object_PrimaryID` | yes | The primary ID used to group Events under. |
| 1 | `Object_SecondaryID` | no | Optional secondary ID to create a relationship beween other entities, such as a MAC address |
| 2 | `DateTime` | yes | When the event occured in the format `2023-01-02 15:56:30` |
| 3 | `Watched_Value1` | yes | A value that is watched and users can receive notifications if it changed compared to the previously saved entry. For example IP address |
| 4 | `Watched_Value2` | no | As above |
| 5 | `Watched_Value3` | no | As above |
| 6 | `Watched_Value4` | no | As above |
| 7 | `Extra` | no | Any other data you want to pass and display in PiAlert and the notifications |
| 8 | `ForeignKey` | no | A foreign key that can be used to link to the parent object (usually a MAC address) |
# config.json structure
## Supported data sources
Currently, only 3 data sources are supported (valid `data_source` value).
- Script (`python-script`)
- SQL query on the PiAlert database (`pialert-db-query`)
- Template (`template`)
> 🔎Example
>```json
>"data_source": "pialert-db-query"
>```
Any of the above data sources have to return a "table" of the exact structure as outlined above.
### "data_source": "python-script"
If the `data_source` is set to `python-script` the `CMD` setting (that you specify in the `settings` array section in the `config.json`) needs to contain an executable Linux command, that generates a `last_result.log` file. This file needs to be stored in the same folder as the plugin.
The content of the `last_result.log` file needs to contain the columns as defined in the "Column order and values" section above. The order of columns can't be changed. After every scan it should contain only the results from the latest scan/execution.
- The format of the `last_result.log` is a `csv`-like file with the pipe `|` as a separator.
- 9 (nine) values need to be supplied, so every line needs to contain 8 pipe separators. Empty values are represented by `null`.
- Don't render "headers" for these "columns".
- Every scan result / event entry needs to be on a new line.
- You can find which "columns" need to be present, and if the value is required or optional, in the "Column order and values" section.
- The order of these "columns" can't be changed.
### 👍 Python script.py tips
The [Undicoverables plugins `script.py` file](https://github.com/jokob-sk/Pi.Alert/blob/main/front/plugins/undiscoverables/script.py) is a good and simple example to start with if you are considering creating a custom plugin. It uses the [`plugin_helper.py` library](https://github.com/jokob-sk/Pi.Alert/blob/main/front/plugins/plugin_helper.py) that significantly simplifies the creation of your custom script.
#### 🔎 last_result.log examples
Valid CSV:
```csv
https://www.google.com|null|2023-01-02 15:56:30|200|0.7898|null|null|null|null
https://www.duckduckgo.com|192.168.0.1|2023-01-02 15:56:30|200|0.9898|null|null|Best search engine|ff:ee:ff:11:ff:11
```
Invalid CSV with different errors on each line:
```csv
https://www.google.com|null|2023-01-02 15:56:30|200|0.7898||null|null|null
https://www.duckduckgo.com|null|2023-01-02 15:56:30|200|0.9898|null|null|Best search engine|
|https://www.duckduckgo.com|null|2023-01-02 15:56:30|200|0.9898|null|null|Best search engine|null
null|192.168.1.1|2023-01-02 15:56:30|200|0.9898|null|null|Best search engine
https://www.duckduckgo.com|192.168.1.1|2023-01-02 15:56:30|null|0.9898|null|null|Best search engine
https://www.google.com|null|2023-01-02 15:56:30|200|0.7898|||
https://www.google.com|null|2023-01-02 15:56:30|200|0.7898|
```
### "data_source": "pialert-db-query"
If the `data_source` is set to `pialert-db-query` the `CMD` setting needs to contain a SQL query rendering the columns as defined in the "Column order and values" section above. The order of columns is important.
This SQL query is executed on the `pialert.db` SQLite database file.
> 🔎Example
>
> SQL query example:
>
> ```SQL
> SELECT dv.dev_Name as Object_PrimaryID,
> cast(dv.dev_LastIP as VARCHAR(100)) || ':' || cast( SUBSTR(ns.Port ,0, INSTR(ns.Port , '/')) as VARCHAR(100)) as Object_SecondaryID,
> datetime() as DateTime,
> ns.Service as Watched_Value1,
> ns.State as Watched_Value2,
> 'null' as Watched_Value3,
> 'null' as Watched_Value4,
> ns.Extra as Extra,
> dv.dev_MAC as ForeignKey
> FROM
> (SELECT * FROM Nmap_Scan) ns
> LEFT JOIN
> (SELECT dev_Name, dev_MAC, dev_LastIP FROM Devices) dv
> ON ns.MAC = dv.dev_MAC
> ```
>
> Required `CMD` setting example with above query (you can set `"type": "label"` if you want it to make uneditable in the UI):
>
> ```json
> {
> "function": "CMD",
> "type": "text",
> "default_value":"SELECT dv.dev_Name as Object_PrimaryID, cast(dv.dev_LastIP as VARCHAR(100)) || ':' || cast( SUBSTR(ns.Port ,0, INSTR(ns.Port , '/')) as VARCHAR(100)) as Object_SecondaryID, datetime() as DateTime, ns.Service as Watched_Value1, ns.State as Watched_Value2, 'null' as Watched_Value3, 'null' as Watched_Value4, ns.Extra as Extra FROM (SELECT * FROM Nmap_Scan) ns LEFT JOIN (SELECT dev_Name, dev_MAC, dev_LastIP FROM Devices) dv ON ns.MAC = dv.dev_MAC",
> "options": [],
> "localized": ["name", "description"],
> "name" : [{
> "language_code":"en_us",
> "string" : "SQL to run"
> }],
> "description": [{
> "language_code":"en_us",
> "string" : "This SQL query is used to populate the coresponding UI tables under the Plugins section."
> }]
> }
> ```
### "data_source": "template"
Used to initialize internal settings. Check the `newdev_template` plugin for details.
## 🕳 Filters
Plugin entries can be filtered based on values entered into filter fields. The `txtMacFilter` textbox/field contains the Mac address of the currently viewed device or simply a Mac address that's available in the `mac` query string.
| Property | Required | Description |
|----------------------|----------------------|----------------------|
| `compare_column` | yes | Plugin column name that's value is used for comparison (**Left** side of the equation) |
| `compare_operator` | yes | JavaScript comparison operator |
| `compare_field_id` | yes | The `id` of a input text field containing a value is used for comparison (**Right** side of the equation)|
| `compare_js_template` | yes | JavaScript code used to convert left and right side of the equation. `{value}` is replaced with input values. |
| `compare_use_quotes` | yes | If `true` then the end result of the `compare_js_template` i swrapped in `"` quotes. Use to compare strings. |
> 🔎Example:
>
> ```json
> "data_filters": [
> {
> "compare_column" : "Object_PrimaryID",
> "compare_operator" : "==",
> "compare_field_id": "txtMacFilter",
> "compare_js_template": "'{value}'.toString()",
> "compare_use_quotes": true
> }
> ],
> ```
1. On the `pluginsCore.php` page is an input field with the `txtMacFilter` id:
```html
<input class="form-control" id="txtMacFilter" type="text" value="--">
```
2. This input field is initialized via the `&mac=` query string.
3. The app then proceeds to use this Mac value from this field and compares it to the value of the `Object_PrimaryID` database field. The `compare_operator` is `==`.
4. Both values, from the database field `Object_PrimaryID` and from the `txtMacFilter` are wrapped and evaluated with the `compare_js_template`, that is `'{value}.toString()'`.
5. `compare_use_quotes` is set to `true` so `'{value}'.toString()` is wrappe dinto `"` quotes.
6. This results in for example this code:
```javascript
// left part of teh expression coming from compare_column and right from the input field
// notice the added quotes ()") around the left and right part of teh expression
"eval('ac:82:ac:82:ac:82".toString()')" == "eval('ac:82:ac:82:ac:82".toString()')"
```
7. Filters are only applied if a filter is specified and the `txtMacFilter` is not `undefined` or empty (`--`).
### 🗺 Mapping the plugin results into a database table
PiAlert will take the results of the plugin execution and insert these results into a database table, if a plugin contains the property `"mapped_to_table"` in the `config.json` root. The mapping of the columns is defined in the `database_column_definitions` array.
This approach is used to implement the `DHCPLSS` plugin. The script parses all supplied "dhcp.leases" files, get's the results in the generic table format outlined in the "Column order and values" section above and takes individual values and inserts them into the `"DHCP_Leases"` database table in the PiAlert database. All this is achieved by:
1) Specifying the database table into which the results are inserted by defining `"mapped_to_table": "DHCP_Leases"` in the root of the `config.json` file as shown below:
```json
{
"code_name": "dhcp_leases",
"unique_prefix": "DHCPLSS",
...
"data_source": "python-script",
"localized": ["display_name", "description", "icon"],
"mapped_to_table": "DHCP_Leases",
...
}
```
2) Defining the target column with the `mapped_to_column` property for individual columns in the `database_column_definitions` array of the `config.json` file. For example in the `DHCPLSS` plugin, I needed to map the value of the `Object_PrimaryID` column returned by the plugin, to the `DHCP_MAC` column in the PiAlert database `DHCP_Leases` table. Notice the `"mapped_to_column": "DHCP_MAC"` key-value pair in the sample below.
```json
{
"column": "Object_PrimaryID",
"mapped_to_column": "DHCP_MAC",
"css_classes": "col-sm-2",
"show": true,
"type": "devicemac",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "MAC address"
}]
}
```
3) That's it. PiAlert takes care of the rest. It loops thru the objects discovered by the plugin, takes the results line, by line and inserts them into the database table specified in `"mapped_to_table"`. The columns are translated from the generic plugin columns to the target table via the `"mapped_to_column"` property in the column definitions.
#### params
The `params` array in the `config.json` is used to enable the user to change the parameters of the executed script. For example, the user wants to monitor a specific URL.
> 🔎 Example:
> Passing user-defined settings to a command. Let's say, you want to have a script, that is called with a user-defined parameter called `urls`:
>
> ```bash
> root@server# python3 /home/pi/pialert/front/plugins/website_monitor/script.py urls=https://google.com,https://duck.com
> ```
* You can allow the user to add URLs to a setting with the `function` property set to a custom name, such as `urls_to_check` (this is not a reserved name from the section "Supported settings `function` values" below).
* You specify the parameter `urls` in the `params` section of the `config.json` the following way (`WEBMON_` is the plugin prefix automatically added to all the settings):
```json
{
"params" : [
{
"name" : "urls",
"type" : "setting",
"value" : "WEBMON_urls_to_check"
}]
}
```
* Then you use this setting as an input parameter for your command in the `CMD` setting. Notice `urls={urls}` in the below json:
```json
{
"function": "CMD",
"type": "text",
"default_value":"python3 /home/pi/pialert/front/plugins/website_monitor/script.py urls={urls}",
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Command"
}],
"description": [{
"language_code":"en_us",
"string" : "Command to run"
}]
}
```
During script execution, the app will take the command `"python3 /home/pi/pialert/front/plugins/website_monitor/script.py urls={urls}"`, take the `{urls}` wildcard and replace it by with the value from the `WEBMON_urls_to_check` setting. This is because:
1) The app checks the `params` entries
2) It finds `"name" : "urls"`
3) Checks the type of the `urls` params and finds `"type" : "setting"`
4) Gets the setting name from `"value" : "WEBMON_urls_to_check"`
- IMPORTANT: in the `config.json` this setting is identified by `"function":"urls_to_check"`, not `"function":"WEBMON_urls_to_check"`
- You can also use a global setting, or a setting from a different plugin
5) The app gets the user defined value from the setting with the code name `WEBMON_urls_to_check`
- let's say the setting with the code name `WEBMON_urls_to_check` contains 2 values entered by the user:
- `WEBMON_urls_to_check=['https://google.com','https://duck.com']`
6) The app takes the value from `WEBMON_urls_to_check` and replaces the `{urls}` wildcard in the setting where `"function":"CMD"`, so you go from:
- `python3 /home/pi/pialert/front/plugins/website_monitor/script.py urls={urls}`
- to
- `python3 /home/pi/pialert/front/plugins/website_monitor/script.py urls=https://google.com,https://duck.com`
Below are some general additional notes, when defining `params`:
- `"name":"name_value"` - is used as a wildcard replacement in the `CMD` setting value by using curly brackets `{name_value}`. The wildcard is replaced by the result of the `"value" : "param_value"` and `"type":"type_value"` combo configuration below.
- `"type":"<sql|setting>"` - is used to specify the type of the params, currently only 2 supported (`sql`,`setting`).
- `"type":"sql"` - will execute the SQL query specified in the `value` property. The sql query needs to return only one column. The column is flattened and separated by commas (`,`), e.g: `SELECT dev_MAC from DEVICES` -> `Internet,74:ac:74:ac:74:ac,44:44:74:ac:74:ac`. This is then used to replace the wildcards in the `CMD` setting.
- `"type":"setting"` - The setting code name. A combination of the value from `unique_prefix` + `_` + `function` value, or otherwise the code name you can find in the Settings page under the Setting display name, e.g. `SCAN_CYCLE_MINUTES`.
- `"value" : "param_value"` - Needs to contain a setting code name or SQL query without wildcards.
> 🔎Example:
>
> ```json
> {
> "params" : [{
> "name" : "macs",
> "type" : "sql",
> "value" : "SELECT dev_MAC from DEVICES"
> },
> {
> "name" : "urls",
> "type" : "setting",
> "value" : "WEBMON_urls_to_check"
> },
> {
> "name" : "internet_ip",
> "type" : "setting",
> "value" : "WEBMON_SQL_internet_ip"
> }]
> }
> ```
#### Setting object structure
- `"function": "<see Supported settings function values>"` - What function the setting drives or a simple unique code name
- `"type": "<text|integer|boolean|password|readonly|integer.select|text.select|text.multiselect|list|integer.checkbox>"` - The form control used for the setting displayed in the Settings page and what values are accepted.
- `"localized"` - a list of properties on the current JSON level which need to be localized
- `"name"` and `"description"` - Displayed in the Settings page. An array of localized strings. (see Localized strings below).
##### Supported settings `function` values
You can have any `"function": "my_custom_name"` custom name, however, the ones listed below have a specific functionality attached to them. If you use a custom name, then the setting is mostly used as an input parameter for the `params` section.
- `RUN` - (required) Specifies when the service is executed
- Supported Options: "disabled", "once", "schedule" (if included then a `RUN_SCHD` setting needs to be specified), "always_after_scan", "on_new_device"
- `RUN_SCHD` - (required if you include the above `RUN` function) Cron-like scheduling used if the `RUN` setting set to `schedule`
- `CMD` - (required) What command should be executed.
- `API_SQL` - (optional) Generates a `table_` + code_name + `.json` file as per [API docs](https://github.com/jokob-sk/Pi.Alert/blob/main/docs/API.md).
- `RUN_TIMEOUT` - (optional) Max execution time of the script. If not specified a default value of 10 seconds is used to prevent hanging.
- `WATCH` - (optional) Which database columns are watched for changes for this particular plugin. If not specified no notifications are sent.
- `REPORT_ON` - (optional) Send a notification only on these statuses. Supported options are:
- `new` means a new unique (unique combination of PrimaryId and SecondaryId) object was discovered.
- `watched-changed` - means that selected `Watched_ValueN` columns changed
- `watched-not-changed` - reports even on events where selected `Watched_ValueN` did not change
> 🔎 Example:
>
> ```json
> {
> "function": "RUN",
> "type": "text.select",
> "default_value":"disabled",
> "options": ["disabled", "once", "schedule", "always_after_scan", "on_new_device"],
> "localized": ["name", "description"],
> "name" :[{
> "language_code":"en_us",
> "string" : "When to run"
> }],
> "description": [{
> "language_code":"en_us",
> "string" : "Enable a regular scan of your services. If you select <code>schedule</code> the scheduling settings from below are applied. If you select <code>once</code> the scan is run only once on start of the application (container) for the time specified in <a href=\"#WEBMON_RUN_TIMEOUT\"><code>WEBMON_RUN_TIMEOUT</code> setting</a>."
> }]
> }
> ```
##### 🌍Localized strings
- `"language_code":"<en_us|es_es|de_de>"` - code name of the language string. Only these three are currently supported. At least the `"language_code":"en_us"` variant has to be defined.
- `"string"` - The string to be displayed in the given language.
> 🔎 Example:
>
> ```json
>
> {
> "language_code":"en_us",
> "string" : "When to run"
> }
>
> ```
##### UI settings in database_column_definitions
The UI will adjust how columns are displayed in the UI based on the definition of the `database_column_definitions` object. These are the supported form controls and related functionality:
- Only columns with `"show": true` and also with at least an English translation will be shown in the UI.
- Supported types: `label`, `text`, `threshold`, `replace`, `deviceip`, `devicemac`, `url`. Check for details below, how columns behave based on the type.
- `label` makes a column display only
- `text` makes a column editable and a save icon is displayed next to it.
- See below for information on `threshold`, `replace`
- The `options` property is used in conjunction with these types:
- `threshold` - The `options` array contains objects from lowest `maximum` to highest with corresponding `hexColor` used for the value background color if it's less than the specified `maximum`, but more than the previous one in the `options` array
- `replace` - The `options` array contains objects with an `equals` property, that is compared to the "value" and if the values are the same, the string in `replacement` is displayed in the UI instead of the actual "value"
- `devicemac` - The value is considered to be a Mac address and a link pointing to the device with the given Mac address is generated.
- `deviceip` - The value is considered to be an IP address and a link pointing to the device with the given IP is generated. The IP is checked against the last detected IP addresses and translated into a Mac address that is then used for the link itself.
- `url` - The value is considered to be a URL so a link is generated.
```json
{
"column": "Watched_Value1",
"css_classes": "col-sm-2",
"show": true,
"type": "threshold",
"default_value":"",
"options": [
{
"maximum": 199,
"hexColor": "#792D86"
},
{
"maximum": 299,
"hexColor": "#5B862D"
},
{
"maximum": 399,
"hexColor": "#7D862D"
},
{
"maximum": 499,
"hexColor": "#BF6440"
},
{
"maximum": 599,
"hexColor": "#D33115"
}
],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Status code"
}]
},
{
"column": "Status",
"show": true,
"type": "replace",
"default_value":"",
"options": [
{
"equals": "watched-not-changed",
"replacement": "<i class='fa-solid fa-square-check'></i>"
},
{
"equals": "watched-changed",
"replacement": "<i class='fa-solid fa-triangle-exclamation'></i>"
},
{
"equals": "new",
"replacement": "<i class='fa-solid fa-circle-plus'></i>"
}
],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Status"
}]
}
```
[screen1]: https://raw.githubusercontent.com/jokob-sk/Pi.Alert/main/docs/img/plugins.png "Screen 1"
[screen2]: https://raw.githubusercontent.com/jokob-sk/Pi.Alert/main/docs/img/plugins_settings.png "Screen 2"
[screen3]: https://raw.githubusercontent.com/jokob-sk/Pi.Alert/main/docs/img/plugins_json_settings.png "Screen 3"
[screen4]: https://raw.githubusercontent.com/jokob-sk/Pi.Alert/main/docs/img/plugins_json_ui.png "Screen 4"

View File

@@ -1,53 +0,0 @@
## Overview
A plugin allowing for importing devices from DHCP.leases files.
### Usage
- Specify full paths of all `dhcp.leases` files you want to import and watch in the `DHCPLSS_paths_to_check`setting.
- Map the paths specified in the `DHCPLSS_paths_to_check`setting in your `docker-compose.yml` file.
#### Example:
`docker-compose.yml` excerpt:
```yaml
volumes:
...
# mapping different dhcp.leases files
- /first/location/dhcp.leases:/mnt/dhcp1.leases
- /second/location/dhcp.leases:/mnt/dhcp2.leases
...
```
`DHCPLSS_paths_to_check` Setting:
```python
DHCPLSS_paths_to_check = ['/mnt/dhcp1.leases','/mnt/dhcp2.leases']
```
### Notes
- No specific configuration needed.
- This plugin expects the dhcp.leases file(s) to be in the format of **dhcpd.leases** that is different to the format that PiHole uses.
[dhcpd.leases(5) - Linux man page]( https://linux.die.net/man/5/dhcpd.leases#:~:text=This%20database%20is%20a%20free,file%20is%20the%20current%20one.)
Example File Format: _(not all lines are required)_
```
lease 192.168.79.15 {
starts 0 2016/08/21 13:25:45;
ends 0 2016/08/21 19:25:45;
cltt 0 2016/08/21 13:25:45;
binding state active;
next binding state free;
rewind binding state free;
hardware ethernet 8c:1a:bf:11:00:ea;
uid "\001\214\032\277\021\000\352";
option agent.circuit-id 0:17;
option agent.remote-id c0:a8:9:5;
client-hostname "android-8182e21c852776e7";
}
```

View File

@@ -1,336 +0,0 @@
{
"code_name": "dhcp_leases",
"unique_prefix": "DHCPLSS",
"enabled": true,
"data_source": "python-script",
"data_filters": [
{
"compare_column" : "Object_PrimaryID",
"compare_operator" : "==",
"compare_field_id": "txtMacFilter",
"compare_js_template": "'{value}'.toString()",
"compare_use_quotes": true
}
],
"localized": ["display_name", "description", "icon"],
"mapped_to_table": "DHCP_Leases",
"display_name" : [{
"language_code":"en_us",
"string" : "DHCP Leases"
}],
"icon":[{
"language_code":"en_us",
"string" : "<i class=\"fa-solid fa-hourglass-half\"></i>"
}],
"description": [{
"language_code":"en_us",
"string" : "This plugin is to import devices from dhcp.leases files."
}],
"params" : [
{
"name" : "paths",
"type" : "setting",
"value" : "DHCPLSS_paths_to_check"
}],
"database_column_definitions":
[
{
"column": "Index",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "N/A"
}]
} ,
{
"column": "Plugin",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "N/A"
}]
},
{
"column": "Object_PrimaryID",
"mapped_to_column": "DHCP_MAC",
"css_classes": "col-sm-2",
"show": true,
"type": "devicemac",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "MAC address"
}]
},
{
"column": "Object_SecondaryID",
"mapped_to_column": "DHCP_IP",
"css_classes": "col-sm-2",
"show": true,
"type": "deviceip",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "IP"
}]
} ,
{
"column": "DateTimeCreated",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Created"
}]
},
{
"column": "DateTimeChanged",
"mapped_to_column": "DHCP_DateTime",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Changed"
}]
},
{
"column": "Watched_Value1",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Is active"
}]
},
{
"column": "Watched_Value2",
"mapped_to_column": "DHCP_Name",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Hostname"
}]
},
{
"column": "Watched_Value3",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Hardware"
}]
} ,
{
"column": "Watched_Value4",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "State"
}]
} ,
{
"column": "UserData",
"css_classes": "col-sm-2",
"show": false,
"type": "textboxsave",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Comments"
}]
},
{
"column": "Extra",
"css_classes": "col-sm-3",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "DHCP leases file"
}]
},
{
"column": "Status",
"css_classes": "col-sm-1",
"show": true,
"type": "replace",
"default_value":"",
"options": [
{
"equals": "watched-not-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-square-check'></i><div></div>"
},
{
"equals": "watched-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-triangle-exclamation'></i></div>"
},
{
"equals": "new",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-circle-plus'></i></div>"
}
],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Status"
}]
}
],
"settings":[
{
"function": "RUN",
"type": "text.select",
"default_value":"disabled",
"options": ["disabled", "once", "schedule", "always_after_scan", "on_new_device"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "When to run"
}],
"description": [{
"language_code":"en_us",
"string" : "Enable import of devices from <code>dhcp.leases</code> files. If you select <code>schedule</code> the scheduling settings from below are applied. If you select <code>once</code> the scan is run only once on start of the application (container) or after you update your settings."
}]
},
{
"function": "CMD",
"type": "text",
"default_value":"python3 /home/pi/pialert/front/plugins/dhcp_leases/script.py paths={paths}",
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Command"
}],
"description": [{
"language_code":"en_us",
"string" : "Command to run"
}]
},
{
"function": "paths_to_check",
"type": "list",
"default_value":["/mnt/dhcp1.leases", "/mnt/dhcp2.leases"],
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Files"
}],
"description": [{
"language_code":"en_us",
"string" : "Add all dhcp.leases mapped paths to watch. Enter full path within the container, e.g. <code>/mnt/dhcp2.leases</code>. You must map these files accordingly in your <code>docker-compose.yml</code> file."
}]
},
{
"function": "RUN_SCHD",
"type": "text",
"default_value":"0 2 * * *",
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Schedule"
}],
"description": [{
"language_code":"en_us",
"string" : "Only enabled if you select <code>schedule</code> in the <a href=\"#DHCPLSS_RUN\"><code>DHCPLSS_RUN</code> setting</a>. Make sure you enter the schedule in the correct cron-like format (e.g. validate at <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). For example entering <code>0 4 * * *</code> will run the scan after 4 am in the <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> you set above</a>. Will be run NEXT time the time passes."
}]
},
{
"function": "RUN_TIMEOUT",
"type": "integer",
"default_value":5,
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Run timeout"
},
{
"language_code":"de_de",
"string" : "Wartezeit"
}],
"description": [{
"language_code":"en_us",
"string" : "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
}]
},
{
"function": "WATCH",
"type": "text.multiselect",
"default_value":["Watched_Value1", "Watched_Value4"],
"options": ["Watched_Value1","Watched_Value2","Watched_Value3","Watched_Value4"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "Watched"
}] ,
"description":[{
"language_code":"en_us",
"string" : "Send a notification if selected values change. Use <code>CTRL + Click</code> to select/deselect. <ul> <li><code>Watched_Value1</code> is Active </li><li><code>Watched_Value2</code> is Hostname </li><li><code>Watched_Value3</code> is hardware </li><li><code>Watched_Value4</code> is State </li></ul>"
}]
},
{
"function": "REPORT_ON",
"type": "text.multiselect",
"default_value":["new","watched-changed"],
"options": ["new","watched-changed","watched-not-changed"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "Report on"
}] ,
"description":[{
"language_code":"en_us",
"string" : "Send a notification only on these statuses. <code>new</code> means a new unique (unique combination of PrimaryId and SecondaryId) object was discovered. <code>watched-changed</code> means that selected <code>Watched_ValueN</code> columns changed."
}]
}
]
}

View File

@@ -1,116 +0,0 @@
#!/usr/bin/env python
# Based on the work of https://github.com/leiweibau/Pi.Alert
from __future__ import unicode_literals
from time import sleep, time, strftime
import requests
import pathlib
import threading
import subprocess
import socket
import argparse
import io
import sys
from requests.packages.urllib3.exceptions import InsecureRequestWarning
import pwd
import os
from dhcp_leases import DhcpLeases
curPath = str(pathlib.Path(__file__).parent.resolve())
log_file = curPath + '/script.log'
last_run = curPath + '/last_result.log'
print(last_run)
# Workflow
def main():
last_run_logfile = open(last_run, 'a')
# empty file
last_run_logfile.write("")
parser = argparse.ArgumentParser(description='Import devices from dhcp.leases files')
parser.add_argument('paths', action="store", help="absolute dhcp.leases file paths to check separated by ','")
values = parser.parse_args()
# parse output
newEntries = []
if values.paths:
for path in values.paths.split('=')[1].split(','):
newEntries = get_entries(newEntries, path)
for e in newEntries:
# Insert list into the log
service_monitoring_log(e.primaryId, e.secondaryId, e.created, e.watched1, e.watched2, e.watched3, e.watched4, e.extra, e.foreignKey )
# -----------------------------------------------------------------------------
def service_monitoring_log(primaryId, secondaryId, created, watched1, watched2 = '', watched3 = '', watched4 = '', extra ='', foreignKey ='' ):
if watched1 == '':
watched1 = 'null'
if watched2 == '':
watched2 = 'null'
if watched3 == '':
watched3 = 'null'
if watched4 == '':
watched4 = 'null'
with open(last_run, 'a') as last_run_logfile:
# https://www.duckduckgo.com|192.168.0.1|2023-01-02 15:56:30|200|0.9898|null|null|Best search engine|null
last_run_logfile.write("{}|{}|{}|{}|{}|{}|{}|{}|{}\n".format(
primaryId,
secondaryId,
created,
watched1,
watched2,
watched3,
watched4,
extra,
foreignKey
)
)
# -----------------------------------------------------------------------------
def get_entries(newEntries, path):
leases = DhcpLeases(path)
leasesList = leases.get()
for lease in leasesList:
tmpPlugObj = plugin_object_class(lease.ethernet, lease.ip, lease.active, lease.hostname, lease.hardware, lease.binding_state, path)
newEntries.append(tmpPlugObj)
return newEntries
# -------------------------------------------------------------------
class plugin_object_class:
def __init__(self, primaryId = '',secondaryId = '', watched1 = '',watched2 = '',watched3 = '',watched4 = '',extra = '',foreignKey = ''):
self.pluginPref = ''
self.primaryId = primaryId
self.secondaryId = secondaryId
self.created = strftime("%Y-%m-%d %H:%M:%S")
self.changed = ''
self.watched1 = watched1
self.watched2 = watched2
self.watched3 = watched3
self.watched4 = watched4
self.status = ''
self.extra = extra
self.userData = ''
self.foreignKey = foreignKey
#===============================================================================
# BEGIN
#===============================================================================
if __name__ == '__main__':
main()

View File

@@ -1,11 +0,0 @@
## Overview
A simple sample plugin allowing for detecting rogue DHCP servers on the network.
### Usage
- No specific configuration needed.
### Notes
- No specific configuration needed.

View File

@@ -1,302 +0,0 @@
{
"code_name": "dhcp_servers",
"unique_prefix": "DHCPSRVS",
"enabled": true,
"data_source": "python-script",
"localized": ["display_name", "description", "icon"],
"display_name" : [{
"language_code":"en_us",
"string" : "Rogue DHCP"
}],
"icon":[{
"language_code":"en_us",
"string" : "<i class=\"fa-solid fa-skull-crossbones\"></i>"
}],
"description": [{
"language_code":"en_us",
"string" : "This plugin is to use NMAP to monitor for rogue DHCP servers."
}],
"params" : [],
"database_column_definitions":
[
{
"column": "Index",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "N/A"
}]
} ,
{
"column": "Plugin",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "N/A"
}]
},
{
"column": "Object_PrimaryID",
"css_classes": "col-sm-2",
"show": true,
"type": "deviceip",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Server Identifier"
}]
},
{
"column": "Object_SecondaryID",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Domain Name"
}]
} ,
{
"column": "DateTimeCreated",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Created"
}]
},
{
"column": "DateTimeChanged",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Changed"
}]
},
{
"column": "Watched_Value1",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Domain Name Server"
}]
},
{
"column": "Watched_Value2",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "IP Offered"
}]
},
{
"column": "Watched_Value3",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Interface"
}]
} ,
{
"column": "Watched_Value4",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Router"
}]
} ,
{
"column": "UserData",
"css_classes": "col-sm-2",
"show": true,
"type": "textboxsave",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Comments"
}]
},
{
"column": "Status",
"css_classes": "col-sm-1",
"show": true,
"type": "replace",
"default_value":"",
"options": [
{
"equals": "watched-not-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-square-check'></i><div></div>"
},
{
"equals": "watched-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-triangle-exclamation'></i></div>"
},
{
"equals": "new",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-circle-plus'></i></div>"
}
],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Status"
}]
},
{
"column": "Extra",
"css_classes": "col-sm-3",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Extra info"
}]
}
],
"settings":[
{
"function": "RUN",
"type": "text.select",
"default_value":"disabled",
"options": ["disabled", "once", "schedule", "always_after_scan", "on_new_device"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "When to run"
}],
"description": [{
"language_code":"en_us",
"string" : "Enable a regular scan of rogue DHCP servers. If you select <code>schedule</code> the scheduling settings from below are applied. If you select <code>once</code> the scan is run only once on start of the application (container) or after you update your settings."
}]
},
{
"function": "CMD",
"type": "text",
"default_value":"python3 /home/pi/pialert/front/plugins/dhcp_servers/script.py",
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Command"
}],
"description": [{
"language_code":"en_us",
"string" : "Command to run"
}]
},
{
"function": "RUN_SCHD",
"type": "text",
"default_value":"0 2 * * *",
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Schedule"
}],
"description": [{
"language_code":"en_us",
"string" : "Only enabled if you select <code>schedule</code> in the <a href=\"#DHCPSRVS_RUN\"><code>DHCPSRVS_RUN</code> setting</a>. Make sure you enter the schedule in the correct cron-like format (e.g. validate at <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). For example entering <code>0 4 * * *</code> will run the scan after 4 am in the <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> you set above</a>. Will be run NEXT time the time passes."
}]
},
{
"function": "RUN_TIMEOUT",
"type": "integer",
"default_value":5,
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Run timeout"
},
{
"language_code":"de_de",
"string" : "Wartezeit"
}],
"description": [{
"language_code":"en_us",
"string" : "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
}]
},
{
"function": "WATCH",
"type": "text.multiselect",
"default_value":["Watched_Value1"],
"options": ["Watched_Value1","Watched_Value2","Watched_Value3","Watched_Value4"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "Watched"
}] ,
"description":[{
"language_code":"en_us",
"string" : "Send a notification if selected values change. Use <code>CTRL + Click</code> to select/deselect. <ul> <li><code>Watched_Value1</code> is Domain Name Server</li><li><code>Watched_Value2</code> is IP Offered</li><li><code>Watched_Value3</code> is Interface </li><li><code>Watched_Value4</code> is Router </li></ul>"
}]
},
{
"function": "REPORT_ON",
"type": "text.multiselect",
"default_value":["new","watched-changed"],
"options": ["new","watched-changed","watched-not-changed"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "Report on"
}] ,
"description":[{
"language_code":"en_us",
"string" : "Send a notification only on these statuses. <code>new</code> means a new unique (unique combination of PrimaryId and SecondaryId) object was discovered. <code>watched-changed</code> means that selected <code>Watched_ValueN</code> columns changed."
}]
}
]
}

View File

@@ -1,133 +0,0 @@
#!/usr/bin/env python
# Based on the work of https://github.com/leiweibau/Pi.Alert
from __future__ import unicode_literals
from time import sleep, time, strftime
import requests
import pathlib
import threading
import subprocess
import socket
import argparse
import io
import sys
from requests.packages.urllib3.exceptions import InsecureRequestWarning
import pwd
import os
curPath = str(pathlib.Path(__file__).parent.resolve())
log_file = curPath + '/script.log'
last_run = curPath + '/last_result.log'
print(last_run)
# Workflow
def main():
last_run_logfile = open(last_run, 'a')
timeoutSec = 10
nmapArgs = ['sudo', 'nmap', '--script', 'broadcast-dhcp-discover']
# Execute N probes and insert in list
dhcp_probes = 1 # N probes
newLines = []
newLines.append(strftime("%Y-%m-%d %H:%M:%S"))
#dhcp_server_list_time = []
for _ in range(dhcp_probes):
output = subprocess.check_output (nmapArgs, universal_newlines=True, stderr=subprocess.STDOUT, timeout=(timeoutSec ))
newLines = newLines + output.split("\n")
# parse output
newEntries = []
duration = ""
for line in newLines:
if newEntries is None:
index = 0
else:
index = len(newEntries) - 1
if 'Response ' in line and ' of ' in line:
newEntries.append(plugin_object_class())
elif 'Server Identifier' in line :
newEntries[index].primaryId = line.split(':')[1].strip()
elif 'Domain Name' in line :
newEntries[index].secondaryId = line.split(':')[1].strip()
elif 'Domain Name Server' in line :
newEntries[index].watched1 = line.split(':')[1].strip()
elif 'IP Offered' in line :
newEntries[index].watched2 = line.split(':')[1].strip()
elif 'Interface' in line :
newEntries[index].watched3 = line.split(':')[1].strip()
elif 'Router' in line :
newEntries[index].watched4 = line.split(':')[1].strip()
newEntries[index].foreignKey = line.split(':')[1].strip()
elif ('IP Address Lease Time' in line or 'Subnet Mask' in line or 'Broadcast Address' in line) :
newVal = line.split(':')[1].strip()
if newEntries[index].extra == '':
newEntries[index].extra = newVal
else:
newEntries[index].extra = newEntries[index].extra + ',' + newVal
for e in newEntries:
# Insert list into the log
service_monitoring_log(e.primaryId, e.secondaryId, e.created, e.watched1, e.watched2, e.watched3, e.watched4, e.extra, e.foreignKey )
# -----------------------------------------------------------------------------
def service_monitoring_log(primaryId, secondaryId, created, watched1, watched2 = '', watched3 = '', watched4 = '', extra ='', foreignKey ='' ):
if watched1 == '':
watched1 = 'null'
if watched2 == '':
watched2 = 'null'
if watched3 == '':
watched3 = 'null'
if watched4 == '':
watched4 = 'null'
with open(last_run, 'a') as last_run_logfile:
# https://www.duckduckgo.com|192.168.0.1|2023-01-02 15:56:30|200|0.9898|null|null|Best search engine|null
last_run_logfile.write("{}|{}|{}|{}|{}|{}|{}|{}|{}\n".format(
primaryId,
secondaryId,
created,
watched1,
watched2,
watched3,
watched4,
extra,
foreignKey
)
)
# -------------------------------------------------------------------
class plugin_object_class:
def __init__(self, primaryId = '',secondaryId = '', watched1 = '',watched2 = '',watched3 = '',watched4 = '',extra = '',foreignKey = ''):
self.pluginPref = ''
self.primaryId = primaryId
self.secondaryId = secondaryId
self.created = strftime("%Y-%m-%d %H:%M:%S")
self.changed = ''
self.watched1 = watched1
self.watched2 = watched2
self.watched3 = watched3
self.watched4 = watched4
self.status = ''
self.extra = extra
self.userData = ''
self.foreignKey = foreignKey
#===============================================================================
# BEGIN
#===============================================================================
if __name__ == '__main__':
sys.exit(main())

View File

@@ -1,11 +0,0 @@
## Overview
A simple template-based plugin for new devices. You can change the default values for newly detected devices.
### Usage
- Head to **Settings** > **New Devices** to adjust the default values.
### Notes
- This plugin generates editable settings that are then used in the `device.py` script to initialize new values.

View File

@@ -1,535 +0,0 @@
{
"code_name": "Devices.new",
"template_type": "database-entry",
"unique_prefix": "NEWDEV",
"enabled": true,
"data_source": "template",
"localized": ["display_name", "description", "icon"],
"display_name": [{
"language_code": "en_us",
"string": "New Devices"
}],
"description": [{
"language_code": "en_us",
"string": "The template used for new devices."
}],
"icon": [
{
"language_code": "en_us",
"string": "<i class=\"fa fa-plus\"></i>"
}
],
"settings":[
{
"function": "dev_MAC",
"type": "readonly",
"maxLength": 50,
"default_value": "",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Device MAC"
}
],
"description": [
{
"language_code": "en_us",
"string": "The MAC address of the device. Uneditable - Autodetected."
}
]
},
{
"function": "dev_Name",
"type": "readonly",
"maxLength": 50,
"default_value": "(unknown)",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Device Name"
}
],
"description": [
{
"language_code": "en_us",
"string": "The name of the device. Uneditable as internal functionality is dependent on specific new device names."
}
]
},
{
"function": "dev_Owner",
"type": "string",
"maxLength": 30,
"default_value": "House",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Device Owner"
}
],
"description": [
{
"language_code": "en_us",
"string": "The owner of the device."
}
]
},
{
"function": "dev_DeviceType",
"type": "string",
"maxLength": 30,
"default_value": "",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Device Type"
}
],
"description": [
{
"language_code": "en_us",
"string": "The type of the device."
}
]
},
{
"function": "dev_Vendor",
"type": "readonly",
"maxLength": 250,
"default_value": "",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Device Vendor"
}
],
"description": [
{
"language_code": "en_us",
"string": "The vendor of the device. Uneditable - Autodetected."
}
]
},
{
"function": "dev_Favorite",
"type": "integer.checkbox",
"default_value": 0,
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Favorite Device"
}
],
"description": [
{
"language_code": "en_us",
"string": "Indicates whether the device is marked as a favorite."
}
]
},
{
"function": "dev_Group",
"type": "string",
"maxLength": 10,
"default_value": "",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Device Group"
}
],
"description": [
{
"language_code": "en_us",
"string": "The group to which the device belongs."
}
]
},
{
"function": "dev_Comments",
"type": "string",
"default_value": "",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Device Comments"
}
],
"description": [
{
"language_code": "en_us",
"string": "Additional comments or notes about the device."
}
]
},
{
"function": "dev_FirstConnection",
"type": "readonly",
"format": "date-time",
"default_value": "",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "First Connection"
}
],
"description": [
{
"language_code": "en_us",
"string": "The date and time of the first connection with the device. Uneditable - Autodetected."
}
]
},
{
"function": "dev_LastConnection",
"type": "readonly",
"format": "date-time",
"default_value": "",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Last Connection"
}
],
"description": [
{
"language_code": "en_us",
"string": "The date and time of the last connection with the device. Uneditable - Autodetected."
}
]
},
{
"function": "dev_LastIP",
"type": "readonly",
"maxLength": 50,
"default_value": "",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Last IP"
}
],
"description": [
{
"language_code": "en_us",
"string": "The last known IP address of the device. Uneditable - Autodetected."
}
]
},
{
"function": "dev_StaticIP",
"type": "integer.checkbox",
"default_value": 0,
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Static IP"
}
],
"description": [
{
"language_code": "en_us",
"string": "Indicates whether the device has a static IP address."
}
]
},
{
"function": "dev_ScanCycle",
"type": "integer.checkbox",
"default_value": 1,
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Scan Cycle"
}
],
"description": [
{
"language_code": "en_us",
"string": "The default value of the <code>Scan device</code> dropdown. Enable if newly discovered devices should be scanned."
}
]
},
{
"function": "dev_LogEvents",
"type": "integer.checkbox",
"default_value": 1,
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Log Events"
}
],
"description": [
{
"language_code": "en_us",
"string": "Indicates whether events related to the device shouldbe logged."
}]
},
{
"function": "dev_AlertEvents",
"type": "integer.checkbox",
"default_value": 1,
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Alert Events"
}
],
"description": [
{
"language_code": "en_us",
"string": "Indicates whether events related to the device should trigger alerts. The default value of the <code>Alert All Events</code> checkbox."
}
]
},
{
"function": "dev_AlertDeviceDown",
"type": "integer.checkbox",
"default_value": 0,
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Alert Device Down"
}
],
"description": [
{
"language_code": "en_us",
"string": "Indicates whether an alert should be triggered when the device goes down. The default value of the <code>Alert Down</code> checkbox."
}
]
},
{
"function": "dev_SkipRepeated",
"type": "integer",
"default_value": 0,
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Skip Repeated"
}
],
"description": [
{
"language_code": "en_us",
"string": "The default value of the <code>Skip repeated notifications for</code> dropdown. Enter number of <b>hours</b> for which repeated notifications should be ignored for. If you enter <code>0</code> then you get notified on all events."
}
]
},
{
"function": "dev_LastNotification",
"type": "readonly",
"format": "date-time",
"default_value": "",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Last Notification"
}
],
"description": [
{
"language_code": "en_us",
"string": "The date and time of the last notification sent for the device. Uneditable - Autodetected."
}
]
},
{
"function": "dev_PresentLastScan",
"type": "integer.checkbox",
"default_value": 1,
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Present Last Scan"
}
],
"description": [
{
"language_code": "en_us",
"string": "Indicates whether the device should be marked as present after detected in a scan."
}
]
},
{
"function": "dev_NewDevice",
"type": "integer.checkbox",
"default_value": true,
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "New Device"
}
],
"description": [
{
"language_code": "en_us",
"string": "Indicates whether the device is considered a new device. The default value of the <code>New Device</code> checkbox."
}
]
},
{
"function": "dev_Location",
"type": "string",
"maxLength": 250,
"default_value": "",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Device Location"
}
],
"description": [
{
"language_code": "en_us",
"string": "The location of the device."
}
]
},
{
"function": "dev_Archived",
"type": "integer.checkbox",
"default_value": 0,
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Archived"
}
],
"description": [
{
"language_code": "en_us",
"string": "Indicates whether the device is archived. The default value of the <code>Archived</code> checkbox."
}
]
},
{
"function": "dev_Network_Node_MAC_ADDR",
"type": "string",
"default_value": "",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Network Node MAC Address"
}
],
"description": [
{
"language_code": "en_us",
"string": "The MAC address of the network node."
}
]
},
{
"function": "dev_Network_Node_port",
"type": "readonly",
"default_value": 0,
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Network Node Port"
}
],
"description": [
{
"language_code": "en_us",
"string": "The port number of the network node. Uneditable."
}
]
},
{
"function": "dev_Icon",
"type": "string",
"default_value": "",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Device Icon"
}
],
"description": [
{
"language_code": "en_us",
"string": "The icon associated with the device. Check the <a href=\"https://github.com/jokob-sk/Pi.Alert/blob/main/docs/ICONS.md\" target=\"_blank\">documentation on icons</a> for more details."
}
]
}
],
"required": [
"dev_MAC",
"dev_Name",
"dev_Owner",
"dev_FirstConnection",
"dev_LastConnection",
"dev_LastIP",
"dev_StaticIP",
"dev_ScanCycle",
"dev_LogEvents",
"dev_AlertEvents",
"dev_AlertDeviceDown",
"dev_SkipRepeated",
"dev_LastNotification",
"dev_PresentLastScan",
"dev_NewDevice",
"dev_Location",
"dev_Archived",
"dev_Network_Node_MAC_ADDR",
"dev_Network_Node_port",
"dev_Icon"
],
"additionalProperties": false
}

View File

@@ -1,11 +0,0 @@
## Overview
This plugin shows all Services discovered by regular NMAP scans. It's also a sample plugin showcasing how to use a SQL Query to show existing data from the PiAlert database.
### Usage
- The sql query from the `NMAPSRV_CMD` setting is used to create source data for this plugin. Column order and values need to adhere to the ones specified in the [documentation](https://github.com/jokob-sk/Pi.Alert/tree/main/front/plugins).
### Notes
- N/A

View File

@@ -1,291 +0,0 @@
{
"code_name": "nmap_services",
"unique_prefix": "NMAPSRV",
"enabled": true,
"data_source": "pialert-db-query",
"data_filters": [
{
"compare_column" : "ForeignKey",
"compare_operator" : "==",
"compare_field_id": "txtMacFilter",
"compare_js_template": "'{value}.toString()'",
"compare_use_quotes": true
}
],
"localized": ["display_name", "description", "icon"],
"display_name" : [{
"language_code":"en_us",
"string" : "Services (NMAP)"
}],
"icon":[{
"language_code":"en_us",
"string" : "<i class=\"fa-solid fa-satellite-dish\"></i>"
}],
"description": [{
"language_code":"en_us",
"string" : "This plugin shows all services discovered by NMAP scans."
}],
"params" : [],
"database_column_definitions":
[
{
"column": "Index",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "N/A"
}]
} ,
{
"column": "Plugin",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "N/A"
}]
},
{
"column": "Object_PrimaryID",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Device name"
}]
},
{
"column": "Object_SecondaryID",
"css_classes": "col-sm-2",
"show": true,
"type": "url",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Ip and Port"
}]
} ,
{
"column": "DateTimeCreated",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Created"
}]
},
{
"column": "DateTimeChanged",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Changed"
}]
},
{
"column": "Watched_Value1",
"css_classes": "col-sm-1",
"show": true,
"type": "label",
"default_value":"",
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Service"
}]
},
{
"column": "Watched_Value2",
"css_classes": "col-sm-1",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "State"
}]
},
{
"column": "Watched_Value3",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "N/A"
}]
} ,
{
"column": "Watched_Value4",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "N/A"
}]
} ,
{
"column": "Extra",
"css_classes": "col-sm-3",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Extra"
}]
},
{
"column": "ForeignKey",
"css_classes": "col-sm-2",
"show": true,
"type": "devicemac",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "MAC"
}]
},
{
"column": "Status",
"css_classes": "col-sm-1",
"show": true,
"type": "replace",
"default_value":"",
"options": [
{
"equals": "watched-not-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-square-check'></i><div></div>"
},
{
"equals": "watched-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-triangle-exclamation'></i></div>"
},
{
"equals": "new",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-circle-plus'></i></div>"
}
],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Status"
}]
}
],
"settings":[
{
"function": "RUN",
"type": "text.select",
"default_value":"disabled",
"options": ["disabled", "once", "schedule", "always_after_scan", "on_new_device"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "When to run"
}],
"description": [{
"language_code":"en_us",
"string" : "Specify when the SQL query is executed."
}]
},
{
"function": "CMD",
"type": "text",
"default_value":"SELECT ns.MAC as Object_PrimaryID, cast('http://' || dv.dev_LastIP as VARCHAR(100)) || ':' || cast( SUBSTR(ns.Port ,0, INSTR(ns.Port , '/')) as VARCHAR(100)) as Object_SecondaryID, datetime() as DateTime, ns.Service as Watched_Value1, ns.State as Watched_Value2, dv.dev_Name as Watched_Value3, 'null' as Watched_Value4, ns.Extra as Extra, ns.MAC as ForeignKey FROM (SELECT * FROM Nmap_Scan) ns left JOIN (SELECT dev_Name, dev_MAC, dev_LastIP FROM Devices) dv ON ns.MAC = dv.dev_MAC",
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "SQL to run"
}],
"description": [{
"language_code":"en_us",
"string" : "This SQL query is used to populate the coresponding UI tables under the Plugins section."
}]
},
{
"function": "RUN_SCHD",
"type": "text",
"default_value":"0 2 * * *",
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Schedule"
}],
"description": [{
"language_code":"en_us",
"string" : "Only enabled if you select <code>schedule</code> in the <a href=\"#NMAPSRV_RUN\"><code>NMAPSRV_RUN</code> setting</a>. Make sure you enter the schedule in the correct cron-like format (e.g. validate at <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). For example entering <code>0 4 * * *</code> will run the scan after 4 am in the <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> you set above</a>. Will be run NEXT time the time passes."
}]
},
{
"function": "WATCH",
"type": "text.multiselect",
"default_value":["Watched_Value1"],
"options": ["Watched_Value1","Watched_Value2","Watched_Value3","Watched_Value4"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "Watched"
}] ,
"description":[{
"language_code":"en_us",
"string" : "Send a notification if selected values change. Use <code>CTRL + Click</code> to select/deselect. <ul> <li><code>Watched_Value1</code> is service type (e.g.: http, ssh)</li><li><code>Watched_Value2</code> is Status (open or closed)</li><li><code>Watched_Value3</code> unused </li><li><code>Watched_Value4</code> unused </li></ul>"
}]
},
{
"function": "REPORT_ON",
"type": "text.multiselect",
"default_value":["new","watched-changed"],
"options": ["new","watched-changed","watched-not-changed"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "Report on"
}] ,
"description":[{
"language_code":"en_us",
"string" : "Send a notification only on these statuses. <code>new</code> means a new unique (unique combination of PrimaryId and SecondaryId) object was discovered. <code>watched-changed</code> means that selected <code>Watched_ValueN</code> columns changed."
}]
}
]
}

View File

@@ -1,96 +0,0 @@
from time import strftime
# -------------------------------------------------------------------
class Plugin_Object:
"""
Plugin_Object class to manage one object introduced by the plugin
An object typically is a device but could also be a website or something
else that is monitored by the plugin.
"""
def __init__(
self,
primaryId="",
secondaryId="",
watched1="",
watched2="",
watched3="",
watched4="",
extra="",
foreignKey="",
):
self.pluginPref = ""
self.primaryId = primaryId
self.secondaryId = secondaryId
self.created = strftime("%Y-%m-%d %H:%M:%S")
self.changed = ""
self.watched1 = watched1
self.watched2 = watched2
self.watched3 = watched3
self.watched4 = watched4
self.status = ""
self.extra = extra
self.userData = ""
self.foreignKey = foreignKey
def write(self):
"""
write the object details as a string in the
format required to write the result file
"""
line = "{}|{}|{}|{}|{}|{}|{}|{}|{}\n".format(
self.primaryId,
self.secondaryId,
self.created,
self.watched1,
self.watched2,
self.watched3,
self.watched4,
self.extra,
self.foreignKey,
)
return line
class Plugin_Objects:
"""
Plugin_Objects is the class that manages and holds all the objects created by the plugin.
It contains a list of Plugin_Object instances.
And can write the required result file.
"""
def __init__(self, result_file):
self.result_file = result_file
self.objects = []
def add_object(
self,
primaryId="",
secondaryId="",
watched1="",
watched2="",
watched3="",
watched4="",
extra="",
foreignKey="",
):
self.objects.append(
Plugin_Object(
primaryId,
secondaryId,
watched1,
watched2,
watched3,
watched4,
extra,
foreignKey,
)
)
def write_result_file(self):
# print ("writing file: "+self.result_file)
with open(self.result_file, mode="w") as fp:
for obj in self.objects:
fp.write(obj.write())
fp.close()

View File

@@ -1,39 +0,0 @@
## Overview
A plugin for importing devices from an SNMP enabled router or switch. Using SNMP offers an efficient way to discover IPv4 devices across one or more networks/subnets/vlans.
### Usage
Specify the following settings in the Settings section of PiAlert:
- `SNMPDSC_routers` - A list of `snmpwalk` commands to execute against IP addresses of roputers/switches with SNMP turned on. For example:
- `snmpwalk -v 2c -c public -OXsq 192.168.1.1 .1.3.6.1.2.1.3.1.1.2`
- `snmpwalk -v 2c -c public -Oxsq 192.168.1.1 .1.3.6.1.2.1.3.1.1.2` (note: lower case `x`)
### Setup Cisco IOS
Enable IOS SNMP service and restrict to selected (internal) IP/Subnet.
````
! Add standard ip access-list 10
ip access-list standard 10
permit 192.168.1.0 0.0.0.255
permit host 192.168.2.10
!
! Enable IOS snmp server with Read Only community 'mysnmpcommunitysecret' name.
! Restrict connections to access-list 10
snmp-server community mysnmpcommunitysecret RO 10
````
Confirm SNMP enabled
````
show snmp
````
### Notes
- Only IPv4 supported.
- The SNMP OID `.1.1.1.3.6.1.2.1.3.1.1.2` is specifically for devices IPv4 ARP table. This OID has been tested on Cisco ISRs and other L3 devices. Support may vary between other vendors / devices.
- Expected output (ingestion) in format `iso.3.6.1.2.1.3.1.1.2.3.1.192.168.1.2 "6C 6C 6C 6C 6C 6C "`.

View File

@@ -1,337 +0,0 @@
{
"code_name": "snmp_discovery",
"unique_prefix": "SNMPDSC",
"enabled": true,
"data_source": "pyton-script",
"data_filters": [
{
"compare_column" : "Object_PrimaryID",
"compare_operator" : "==",
"compare_field_id": "txtMacFilter",
"compare_js_template": "'{value}'.toString()",
"compare_use_quotes": true
}
],
"localized": ["display_name", "description", "icon"],
"mapped_to_table": "DHCP_Leases",
"display_name" : [{
"language_code":"en_us",
"string" : "SNMP discovery"
}],
"icon":[{
"language_code":"en_us",
"string" : "<i class=\"fa-solid fa-s\"></i>"
}],
"description": [{
"language_code":"en_us",
"string" : "This plugin is used to discover devices via the arp table(s) of a RFC1213 compliant router or switch."
}],
"params" : [
{
"name" : "routers",
"type" : "setting",
"value" : "SNMPDSC_routers"
}
],
"database_column_definitions":
[
{
"column": "Index",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "N/A"
}]
} ,
{
"column": "Plugin",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "N/A"
}]
},
{
"column": "Object_PrimaryID",
"mapped_to_column": "DHCP_MAC",
"css_classes": "col-sm-2",
"show": true,
"type": "devicemac",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "MAC address"
}]
},
{
"column": "Object_SecondaryID",
"mapped_to_column": "DHCP_IP",
"css_classes": "col-sm-2",
"show": true,
"type": "deviceip",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "IP"
}]
} ,
{
"column": "DateTimeCreated",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Created"
}]
},
{
"column": "DateTimeChanged",
"mapped_to_column": "DHCP_DateTime",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Changed"
}]
},
{
"column": "Watched_Value1",
"mapped_to_column": "DHCP_Name",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"(unknown)",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Hostname"
}]
},
{
"column": "Watched_Value2",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Router IP"
}]
},
{
"column": "Watched_Value3",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Type"
}]
} ,
{
"column": "Watched_Value4",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Network"
}]
} ,
{
"column": "UserData",
"css_classes": "col-sm-2",
"show": false,
"type": "textboxsave",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Comments"
}]
},
{
"column": "Extra",
"css_classes": "col-sm-3",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "RAW output"
}]
},
{
"column": "Status",
"css_classes": "col-sm-1",
"show": true,
"type": "replace",
"default_value":"",
"options": [
{
"equals": "watched-not-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-square-check'></i><div></div>"
},
{
"equals": "watched-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-triangle-exclamation'></i></div>"
},
{
"equals": "new",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-circle-plus'></i></div>"
}
],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Status"
}]
}
],
"settings":[
{
"function": "RUN",
"type": "text.select",
"default_value":"disabled",
"options": ["disabled", "once", "schedule", "always_after_scan", "on_new_device"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "When to run"
}],
"description": [{
"language_code":"en_us",
"string" : "Enable import of devices from a SNMP enabled device. If you select <code>schedule</code> the scheduling settings from below are applied. If you select <code>once</code> the scan is run only once on start of the application (container) or after you update your settings."
}]
},
{
"function": "CMD",
"type": "text",
"default_value":"python3 /home/pi/pialert/front/plugins/snmp_discovery/script.py routers={s-quote}{routers}{s-quote}",
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Command"
}],
"description": [{
"language_code":"en_us",
"string" : "Command to run. Not recommended to change."
}]
},
{
"function": "routers",
"type": "list",
"default_value":["snmpwalk -v 2c -c public -OXsq 192.168.1.1 .1.3.6.1.2.1.3.1.1.2"],
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Routers"
}],
"description": [{
"language_code":"en_us",
"string" : "A list of <code>snmpwalk</code> commands to execute against IP addresses of roputers/switches with SNMP turned on. <br/> <br/> Example with the router on the IP <code>192.168.1.1</code>: <br/> <code>snmpwalk -v 2c -c public -OXsq 192.168.1.1 .1.3.6.1.2.1.3.1.1.2</code> <br/><br/> Only IPv4 supported. Authentication is not supported. More info on the plugin <a href='https://github.com/jokob-sk/Pi.Alert/tree/main/front/plugins/snmp_discovery' target='_blank'>here</a>."
}]
},
{
"function": "RUN_SCHD",
"type": "text",
"default_value":"0 2 * * *",
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Schedule"
}],
"description": [{
"language_code":"en_us",
"string" : "Only enabled if you select <code>schedule</code> in the <a href=\"#SNMPDSC_RUN\"><code>SNMPDSC_RUN</code> setting</a>. Make sure you enter the schedule in the correct cron-like format (e.g. validate at <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). For example entering <code>0 4 * * *</code> will run the scan after 4 am in the <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> you set above</a>. Will be run NEXT time the time passes."
}]
},
{
"function": "RUN_TIMEOUT",
"type": "integer",
"default_value":5,
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Run timeout"
},
{
"language_code":"de_de",
"string" : "Wartezeit"
}],
"description": [{
"language_code":"en_us",
"string" : "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
}]
},
{
"function": "WATCH",
"type": "text.multiselect",
"default_value":["Watched_Value1"],
"options": ["Watched_Value1","Watched_Value2","Watched_Value3","Watched_Value4"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "Watched"
}] ,
"description":[{
"language_code":"en_us",
"string" : "Send a notification if selected values change. Use <code>CTRL + Click</code> to select/deselect. <ul> <li><code>Watched_Value1</code> is Hostname (not discoverable) </li><li><code>Watched_Value2</code> is Router IP </li><li><code>Watched_Value3</code> is not used </li><li><code>Watched_Value4</code> is not used </li></ul>"
}]
},
{
"function": "REPORT_ON",
"type": "text.multiselect",
"default_value":["new","watched-changed"],
"options": ["new","watched-changed","watched-not-changed"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "Report on"
}] ,
"description":[{
"language_code":"en_us",
"string" : "Send a notification only on these statuses. <code>new</code> means a new unique (unique combination of PrimaryId and SecondaryId) object was discovered. <code>watched-changed</code> means that selected <code>Watched_ValueN</code> columns changed."
}]
}
]
}

View File

@@ -1,172 +0,0 @@
#!/usr/bin/env python
# Example call
# python3 /home/pi/pialert/front/plugins/snmp_discovery/script.py routers='snmpwalk -v 2c -c public -OXsq 192.168.1.1 .1.3.6.1.2.1.3.1.1.2'
from __future__ import unicode_literals
from time import sleep, time, strftime
import requests
from requests import Request, Session, packages
import pathlib
import threading
import subprocess
import socket
import json
import argparse
import io
import sys
from requests.packages.urllib3.exceptions import InsecureRequestWarning
import pwd
import os
curPath = str(pathlib.Path(__file__).parent.resolve())
log_file = curPath + '/script.log'
last_run = curPath + '/last_result.log'
# Workflow
def main():
# init global variables
global ROUTERS
# empty file
open(last_run , 'w').close()
last_run_logfile = open(last_run, 'a')
parser = argparse.ArgumentParser(description='This plugin is used to discover devices via the arp table(s) of a RFC1213 compliant router or switch.')
parser.add_argument('routers', action="store", help="IP(s) of routers, separated by comma (,) if passing multiple")
values = parser.parse_args()
# parse output
newEntries = []
if values.routers:
ROUTERS = values.routers.split('=')[1].replace('\'','')
newEntries = get_entries(newEntries)
for e in newEntries:
# Insert list into the log
service_monitoring_log(e.primaryId, e.secondaryId, e.created, e.watched1, e.watched2, e.watched3, e.watched4, e.extra, e.foreignKey )
# -----------------------------------------------------------------------------
def get_entries(newEntries):
routers = []
if ',' in ROUTERS:
# multiple
routers = ROUTERS.split(',')
else:
# only one
routers.append(ROUTERS)
for router in routers:
# snmpwalk -v 2c -c public -OXsq 192.168.1.1 .1.3.6.1.2.1.3.1.1.2
print(router)
timeoutSec = 10
snmpwalkArgs = router.split(' ')
# Execute N probes and insert in list
probes = 1 # N probes
newLines = []
for _ in range(probes):
output = subprocess.check_output (snmpwalkArgs, universal_newlines=True, stderr=subprocess.STDOUT, timeout=(timeoutSec ))
newLines = newLines + output.split("\n")
# Process outputs
# Sample: iso.3.6.1.2.1.3.1.1.2.3.1.192.168.1.2 "6C 6C 6C 6C 6C 6C "
with open(log_file, 'a') as run_logfile:
for line in newLines:
# debug
run_logfile.write(line)
tmpSplt = line.split('"')
if len(tmpSplt) == 3:
ipStr = tmpSplt[0].split('.')[-4:] # Get the last 4 elements to extract the IP
macStr = tmpSplt[1].strip().split(' ') # Remove leading/trailing spaces from MAC
if 'iso.' in line and len(ipStr) == 4:
macAddress = ':'.join(macStr)
ipAddress = '.'.join(ipStr)
tmpEntry = plugin_object_class(
macAddress,
ipAddress,
watched1='(unknown)',
watched2=snmpwalkArgs[6], # router IP
extra=line
)
newEntries.append(tmpEntry)
return newEntries
# -------------------------------------------------------------------
class plugin_object_class:
def __init__(self, primaryId = '',secondaryId = '', watched1 = '',watched2 = '',watched3 = '',watched4 = '',extra = '',foreignKey = ''):
self.pluginPref = ''
self.primaryId = primaryId
self.secondaryId = secondaryId
self.created = strftime("%Y-%m-%d %H:%M:%S")
self.changed = ''
self.watched1 = watched1
self.watched2 = watched2
self.watched3 = watched3
self.watched4 = watched4
self.status = ''
self.extra = extra
self.userData = ''
self.foreignKey = foreignKey
# -----------------------------------------------------------------------------
def service_monitoring_log(primaryId, secondaryId, created, watched1, watched2 = 'null', watched3 = 'null', watched4 = 'null', extra ='null', foreignKey ='null' ):
if watched1 == '':
watched1 = 'null'
if watched2 == '':
watched2 = 'null'
if watched3 == '':
watched3 = 'null'
if watched4 == '':
watched4 = 'null'
if extra == '':
extra = 'null'
if foreignKey == '':
foreignKey = 'null'
with open(last_run, 'a') as last_run_logfile:
last_run_logfile.write("{}|{}|{}|{}|{}|{}|{}|{}|{}\n".format(
primaryId,
secondaryId,
created,
watched1,
watched2,
watched3,
watched4,
extra,
foreignKey
)
)
#===============================================================================
# BEGIN
#===============================================================================
if __name__ == '__main__':
main()

View File

@@ -1,27 +0,0 @@
## Overview
A plugin allowing for importing Un-Discoverable devices from the settings page.
The main usecase is to add dumb network gear like unmanaged hubs and switches to the network view.
There might be other usecases, please let me know.
### Usage
- Go to settings and find Un-Discoverabe Devices in the list of plugins.
- Enable the plugin by changing the RUN parameter from disabled to `once` or `always_after_scan`.
- Add the name of your device to the list. (remove the sample entry first)
- SAVE
- wait for the next scan to finish
#### Examples:
Settings:
![settings](https://github.com/Data-Monkey/Pi.Alert/assets/7224371/52883307-19a5-4602-b13a-9825461f6cc4)
resulting in these devices:
![devices](https://github.com/Data-Monkey/Pi.Alert/assets/7224371/9f7659e7-75a8-4ae9-9f5f-781bdbcbc949)
Allowing Un-Discoverable devices like hubs, switches or APs to be added to the network view.
![network](https://github.com/Data-Monkey/Pi.Alert/assets/7224371/b5ccc3b3-f5fd-4f5b-b0f0-e4e637c6da33)
### Known Limitations
- Un-Discoverable Devices always show as offline. That is expected as they can not be discovered by Pi.Alert.
- All IPs are set to 0.0.0.0 therefore the "Random MAC" icon might show up.

View File

@@ -1,217 +0,0 @@
{
"code_name": "undiscoverables",
"unique_prefix": "UNDIS",
"enabled": true,
"data_source": "python-script",
"mapped_to_table": "DHCP_Leases",
"localized": ["display_name", "description", "icon"],
"display_name": [
{
"language_code": "en_us",
"string": "Un-Discoverable Devices"
}
],
"icon": [
{
"language_code": "en_us",
"string": "<i class=\"fa-solid fa-binoculars\"></i>"
}
],
"description": [
{
"language_code": "en_us",
"string": "This plugin is to import undiscoverable devices from a file."
}
],
"params" : [
{
"name" : "devices",
"type" : "setting",
"value" : "UNDIS_devices_to_import"
}],
"settings": [
{
"function": "RUN",
"type": "text.select",
"default_value":"disabled",
"options": ["disabled", "once", "always_after_scan"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "When to run"
}],
"description": [{
"language_code":"en_us",
"string" : "When enabled, ONCE is the preferred option. It runs at startup and after every save of the config here.<br> Changes will only show in the devices <b> after the next scan!</b>"
}]
},
{
"function": "CMD",
"type": "text",
"default_value": "python3 /home/pi/pialert/front/plugins/undiscoverables/script.py devices={devices}",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Command"
}
],
"description": [
{
"language_code": "en_us",
"string": "Command to run. This can not be changed"
}
]
},
{
"function": "RUN_TIMEOUT",
"type": "integer",
"default_value": 10,
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Run timeout"
}
],
"description": [
{
"language_code": "en_us",
"string": "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
}
]
},
{
"function": "WATCH",
"type": "lable",
"default_value": [],
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Watched"
}
],
"description": [
{
"language_code": "en_us",
"string": "Undiscoverable Devices can not change their status, no watch is enabled."
}
]
},
{
"function": "REPORT_ON",
"type": "lable",
"default_value": [],
"options": ["new", "watched-changed", "watched-not-changed"],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Report on"
}
],
"description": [
{
"language_code": "en_us",
"string": "No notifications will be sent."
}
]
},
{
"function": "devices_to_import",
"type": "list",
"default_value":["dummy_router"],
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "UnDiscoverable Devices"
}],
"description": [{
"language_code":"en_us",
"string" : "Devices to be added to the devices list."
}]
}
],
"database_column_definitions":
[
{
"column": "Watched_Value1",
"mapped_to_column": "DHCP_Name",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Device Name"
}]
},
{
"column": "Object_PrimaryID",
"mapped_to_column": "DHCP_MAC",
"css_classes": "col-sm-2",
"show": true,
"type": "devicemac",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "MAC address"
}]
},
{
"column": "Object_SecondaryID",
"mapped_to_column": "DHCP_IP",
"css_classes": "col-sm-2",
"show": true,
"type": "deviceip",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "IP"
}]
} ,
{
"column": "DateTimeCreated",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Created"
}]
},
{
"column": "DateTimeChanged",
"mapped_to_column": "DHCP_DateTime",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Changed"
}]
}
]
}

View File

@@ -1,48 +0,0 @@
#!/usr/bin/env python
# test script by running python script.py devices=test,dummy
import os
import pathlib
import argparse
import sys
sys.path.append("/home/pi/pialert/front/plugins")
from plugin_helper import Plugin_Objects
CUR_PATH = str(pathlib.Path(__file__).parent.resolve())
LOG_FILE = os.path.join(CUR_PATH , 'script.log')
RESULT_FILE = os.path.join(CUR_PATH , 'last_result.log')
def main():
# the script expects a parameter in the format of devices=device1,device2,...
parser = argparse.ArgumentParser(description='Import devices from settings')
parser.add_argument('devices', action="store", help="list of device names separated by ','")
values = parser.parse_args()
UNDIS_devices = Plugin_Objects( RESULT_FILE )
if values.devices:
for fake_dev in values.devices.split('=')[1].split(','):
UNDIS_devices.add_object(
primaryId=fake_dev, # MAC (Device Name)
secondaryId="0.0.0.0", # IP Address (always 0.0.0.0)
watched1=fake_dev, # Device Name
watched2="",
watched3="",
watched4="",
extra="",
foreignKey="")
UNDIS_devices.write_result_file()
return 0
#===============================================================================
# BEGIN
#===============================================================================
if __name__ == '__main__':
main()

View File

@@ -1,19 +0,0 @@
## Overview
A plugin allowing for importing devices from a UniFi controller.
### Usage
Specify the following settings in the Settings section of PiAlert:
- `UNFIMP_username` - Username used to login into the UNIFI controller.
- `UNFIMP_password` - Password used to login into the UNIFI controller.
- `UNFIMP_host` - Host url or IP address where the UNIFI controller is hosted (excluding http://)
- `UNFIMP_sites` - Name of the sites (usually 'default', check the URL in your UniFi controller UI if unsure. The site id is in the following part of the URL: `https://192.168.1.1:8443/manage/site/this-is-the-site-id/settings/`).
- `UNFIMP_protocol` - https:// or http://
- `UNFIMP_port` - Usually 8443
### Notes
- Currently only used to import devices, not their status, type or network map.
- It is recommend to create a read-only user in your UniFi controller

View File

@@ -1,457 +0,0 @@
{
"code_name": "unifi_import",
"unique_prefix": "UNFIMP",
"enabled": true,
"data_source": "python-script",
"data_filters": [
{
"compare_column" : "Object_PrimaryID",
"compare_operator" : "==",
"compare_field_id": "txtMacFilter",
"compare_js_template": "'{value}'.toString()",
"compare_use_quotes": true
}
],
"localized": ["display_name", "description", "icon"],
"mapped_to_table": "DHCP_Leases",
"display_name" : [{
"language_code":"en_us",
"string" : "UniFi import"
}],
"icon":[{
"language_code":"en_us",
"string" : "<i class=\"fa-solid fa-upload\"></i>"
}],
"description": [{
"language_code":"en_us",
"string" : "This plugin is used to import devices from an UNIFI controller."
}],
"params" : [
{
"name" : "username",
"type" : "setting",
"value" : "UNFIMP_username"
},
{
"name" : "password",
"type" : "setting",
"value" : "UNFIMP_password"
},
{
"name" : "host",
"type" : "setting",
"value" : "UNFIMP_host"
},
{
"name" : "sites",
"type" : "setting",
"value" : "UNFIMP_sites"
},
{
"name" : "protocol",
"type" : "setting",
"value" : "UNFIMP_protocol"
},
{
"name" : "port",
"type" : "setting",
"value" : "UNFIMP_port"
},
{
"name" : "version",
"type" : "setting",
"value" : "UNFIMP_version"
}
],
"database_column_definitions":
[
{
"column": "Index",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "N/A"
}]
} ,
{
"column": "Plugin",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "N/A"
}]
},
{
"column": "Object_PrimaryID",
"mapped_to_column": "DHCP_MAC",
"css_classes": "col-sm-2",
"show": true,
"type": "devicemac",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "MAC address"
}]
},
{
"column": "Object_SecondaryID",
"mapped_to_column": "DHCP_IP",
"css_classes": "col-sm-2",
"show": true,
"type": "deviceip",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "IP"
}]
} ,
{
"column": "DateTimeCreated",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Created"
}]
},
{
"column": "DateTimeChanged",
"mapped_to_column": "DHCP_DateTime",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Changed"
}]
},
{
"column": "Watched_Value1",
"mapped_to_column": "DHCP_Name",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Hostname"
}]
},
{
"column": "Watched_Value2",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Vendor"
}]
},
{
"column": "Watched_Value3",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Type"
}]
} ,
{
"column": "Watched_Value4",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Online?"
}]
} ,
{
"column": "UserData",
"css_classes": "col-sm-2",
"show": false,
"type": "textboxsave",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Comments"
}]
},
{
"column": "Extra",
"css_classes": "col-sm-3",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Network"
}]
},
{
"column": "Status",
"css_classes": "col-sm-1",
"show": true,
"type": "replace",
"default_value":"",
"options": [
{
"equals": "watched-not-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-square-check'></i><div></div>"
},
{
"equals": "watched-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-triangle-exclamation'></i></div>"
},
{
"equals": "new",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-circle-plus'></i></div>"
}
],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Status"
}]
}
],
"settings":[
{
"function": "RUN",
"type": "text.select",
"default_value":"disabled",
"options": ["disabled", "once", "schedule", "always_after_scan", "on_new_device"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "When to run"
}],
"description": [{
"language_code":"en_us",
"string" : "Enable import of devices from a UNIFI controller. If you select <code>schedule</code> the scheduling settings from below are applied. If you select <code>once</code> the scan is run only once on start of the application (container) or after you update your settings."
}]
},
{
"function": "CMD",
"type": "text",
"default_value":"python3 /home/pi/pialert/front/plugins/unifi_import/script.py username={username} password={password} host={host} sites={sites} protocol={protocol} port={port} version={version}",
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Command"
}],
"description": [{
"language_code":"en_us",
"string" : "Command to run. Not recommended to change."
}]
},
{
"function": "username",
"type": "text",
"default_value":"",
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Username"
}],
"description": [{
"language_code":"en_us",
"string" : "The username used to login into your UNIFI controller. It is recommended to create a read-only user account."
}]
},
{
"function": "password",
"type": "password",
"default_value":"",
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Password"
}],
"description": [{
"language_code":"en_us",
"string" : "The password used to login into your UNIFI controller."
}]
},
{
"function": "protocol",
"type": "text.select",
"default_value":"https://",
"options": ["https://", "http://"],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Protocol"
}],
"description": [{
"language_code":"en_us",
"string" : "The protocol to use to access the controller."
}]
},
{
"function": "host",
"type": "text",
"default_value":"192.168.1.1",
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Host"
}],
"description": [{
"language_code":"en_us",
"string" : "The host (IP) where the UNIFI controller is runnig. Do NOT include the protocol (e.g. <code>https://</code>)"
}]
},
{
"function": "port",
"type": "text",
"default_value":"8443",
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Port number"
}],
"description": [{
"language_code":"en_us",
"string" : "The port number where the UNIFI controller is runnig. Usually it is <code>8443</code>."
}]
},
{
"function": "version",
"type": "text",
"default_value":"",
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "API version"
}],
"description": [{
"language_code":"en_us",
"string" : "The base version of the Unify controller API. Supported values as of time of writing are <code>v4|v5|unifiOS|UDMP-unifiOS</code>."
}]
},
{
"function": "sites",
"type": "list",
"default_value":["default"],
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "UNIFI sites"
}],
"description": [{
"language_code":"en_us",
"string" : "The sites you want to connect to. Usually it is only one and the name is <code>default</code>. Check the URL in your UniFi controller UI if unsure."
}]
},
{
"function": "RUN_SCHD",
"type": "text",
"default_value":"0 2 * * *",
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Schedule"
}],
"description": [{
"language_code":"en_us",
"string" : "Only enabled if you select <code>schedule</code> in the <a href=\"#UNFIMP_RUN\"><code>UNFIMP_RUN</code> setting</a>. Make sure you enter the schedule in the correct cron-like format (e.g. validate at <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). For example entering <code>0 4 * * *</code> will run the scan after 4 am in the <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> you set above</a>. Will be run NEXT time the time passes."
}]
},
{
"function": "RUN_TIMEOUT",
"type": "integer",
"default_value":5,
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Run timeout"
},
{
"language_code":"de_de",
"string" : "Wartezeit"
}],
"description": [{
"language_code":"en_us",
"string" : "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
}]
},
{
"function": "WATCH",
"type": "text.multiselect",
"default_value":["Watched_Value1", "Watched_Value4"],
"options": ["Watched_Value1","Watched_Value2","Watched_Value3","Watched_Value4"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "Watched"
}] ,
"description":[{
"language_code":"en_us",
"string" : "Send a notification if selected values change. Use <code>CTRL + Click</code> to select/deselect. <ul> <li><code>Watched_Value1</code> is Hostname </li><li><code>Watched_Value2</code> is Vendor </li><li><code>Watched_Value3</code> is Type </li><li><code>Watched_Value4</code> is Network </li></ul>"
}]
},
{
"function": "REPORT_ON",
"type": "text.multiselect",
"default_value":["new","watched-changed"],
"options": ["new","watched-changed","watched-not-changed"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "Report on"
}] ,
"description":[{
"language_code":"en_us",
"string" : "Send a notification only on these statuses. <code>new</code> means a new unique (unique combination of PrimaryId and SecondaryId) object was discovered. <code>watched-changed</code> means that selected <code>Watched_ValueN</code> columns changed."
}]
}
]
}

View File

@@ -1,245 +0,0 @@
#!/usr/bin/env python
# Inspired by https://github.com/stevehoek/Pi.Alert
# Example call
# python3 /home/pi/pialert/front/plugins/unifi_import/script.py username=pialert password=passw0rd host=192.168.1.1 site=default protocol=https:// port=8443
from __future__ import unicode_literals
from time import sleep, time, strftime
import requests
from requests import Request, Session, packages
import pathlib
import threading
import subprocess
import socket
import json
import argparse
import io
import sys
from requests.packages.urllib3.exceptions import InsecureRequestWarning
import pwd
import os
from pyunifi.controller import Controller
curPath = str(pathlib.Path(__file__).parent.resolve())
log_file = curPath + '/script.log'
last_run = curPath + '/last_result.log'
# Workflow
def main():
# init global variables
global UNIFI_USERNAME, UNIFI_PASSWORD, UNIFI_HOST
global UNIFI_SITES, PORT, PROTOCOL, VERSION
last_run_logfile = open(last_run, 'a')
# empty file
last_run_logfile.write("")
parser = argparse.ArgumentParser(description='Import devices from an UNIFI controller')
parser.add_argument('username', action="store", help="Username used to login into the UNIFI controller")
parser.add_argument('password', action="store", help="Password used to login into the UNIFI controller")
parser.add_argument('host', action="store", help="Host url or IP address where the UNIFI controller is hosted (excluding http://)")
parser.add_argument('sites', action="store", help="Name of the sites (usually 'default', check the URL in your UniFi controller UI). Separated by comma (,) if passing multiple sites")
parser.add_argument('protocol', action="store", help="https:// or http://")
parser.add_argument('port', action="store", help="Usually 8443")
parser.add_argument('version', action="store", help="The base version of the controller API [v4|v5|unifiOS|UDMP-unifiOS]")
values = parser.parse_args()
# parse output
newEntries = []
if values.username and values.password and values.host and values.sites:
UNIFI_USERNAME = values.username.split('=')[1]
UNIFI_PASSWORD = values.password.split('=')[1]
UNIFI_HOST = values.host.split('=')[1]
UNIFI_SITES = values.sites.split('=')[1]
PROTOCOL = values.protocol.split('=')[1]
PORT = values.port.split('=')[1]
VERSION = values.version.split('=')[1]
newEntries = get_entries(newEntries)
for e in newEntries:
# Insert list into the log
service_monitoring_log(e.primaryId, e.secondaryId, e.created, e.watched1, e.watched2, e.watched3, e.watched4, e.extra, e.foreignKey )
# -----------------------------------------------------------------------------
def get_entries(newEntries):
sites = []
if ',' in UNIFI_SITES:
sites = UNIFI_SITES.split(',')
else:
sites.append(UNIFI_SITES)
for site in sites:
c = Controller(UNIFI_HOST, UNIFI_USERNAME, UNIFI_PASSWORD, version=VERSION, ssl_verify=False, site_id=site )
for ap in c.get_aps():
# print(f'{json.dumps(ap)}')
deviceType = ''
if (ap['type'] == 'udm'):
deviceType = 'Router'
elif (ap['type'] == 'usg'):
deviceType = 'Router'
elif (ap['type'] == 'usw'):
deviceType = 'Switch'
elif (ap['type'] == 'uap'):
deviceType = 'AP'
name = get_unifi_val(ap, 'name')
hostName = get_unifi_val(ap, 'hostname')
if name == 'null' and hostName != 'null':
name = hostName
tmpPlugObj = plugin_object_class(
ap['mac'],
get_unifi_val(ap, 'ip'),
name,
'Ubiquiti Networks Inc.',
deviceType,
ap['state'],
get_unifi_val(ap, 'connection_network_name')
)
newEntries.append(tmpPlugObj)
# print(f'>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
for cl in c.get_clients():
# print(f'{json.dumps(cl)}')
name = get_unifi_val(cl, 'name')
hostName = get_unifi_val(cl, 'hostname')
if name == 'null' and hostName != 'null':
name = hostName
tmpPlugObj = plugin_object_class(
cl['mac'],
get_unifi_val(cl, 'ip'),
name,
get_unifi_val(cl, 'oui'),
'Other',
1,
get_unifi_val(cl, 'connection_network_name')
)
# print(f'>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
for us in c.get_clients():
# print(f'{json.dumps(us)}')
name = get_unifi_val(us, 'name')
hostName = get_unifi_val(us, 'hostname')
if name == 'null' and hostName != 'null':
name = hostName
tmpPlugObj = plugin_object_class(
us['mac'],
get_unifi_val(us, 'ip'),
name,
get_unifi_val(us, 'oui'),
'Other',
1,
get_unifi_val(us, 'connection_network_name')
)
newEntries.append(tmpPlugObj)
return newEntries
# -----------------------------------------------------------------------------
def get_unifi_val(obj, key):
res = ''
if key in obj:
res = obj[key]
if res not in ['','None']:
return res
if obj.get(key) is not None:
res = obj.get(key)
if res not in ['','None']:
return res
return 'null'
# -------------------------------------------------------------------
class plugin_object_class:
def __init__(self, primaryId = '',secondaryId = '', watched1 = '',watched2 = '',watched3 = '',watched4 = '',extra = '',foreignKey = ''):
self.pluginPref = ''
self.primaryId = primaryId
self.secondaryId = secondaryId
self.created = strftime("%Y-%m-%d %H:%M:%S")
self.changed = ''
self.watched1 = watched1
self.watched2 = watched2
self.watched3 = watched3
self.watched4 = watched4
self.status = ''
self.extra = extra
self.userData = ''
self.foreignKey = foreignKey
# -----------------------------------------------------------------------------
def service_monitoring_log(primaryId, secondaryId, created, watched1, watched2 = 'null', watched3 = 'null', watched4 = 'null', extra ='null', foreignKey ='null' ):
if watched1 == '':
watched1 = 'null'
if watched2 == '':
watched2 = 'null'
if watched3 == '':
watched3 = 'null'
if watched4 == '':
watched4 = 'null'
if extra == '':
extra = 'null'
if foreignKey == '':
foreignKey = 'null'
with open(last_run, 'a') as last_run_logfile:
last_run_logfile.write("{}|{}|{}|{}|{}|{}|{}|{}|{}\n".format(
primaryId,
secondaryId,
created,
watched1,
watched2,
watched3,
watched4,
extra,
foreignKey
)
)
#===============================================================================
# BEGIN
#===============================================================================
if __name__ == '__main__':
main()

View File

@@ -1,12 +0,0 @@
## Overview
A simple sample plugin allowing for monitoring web services or urls. The status code corresponds to the commonly used [HTTP response status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status).
### Usage
- The user can specify which services (websites) to monitor via the `WEBMON_urls_to_check` setting.
### Notes
- Setting `(WEBMON_)SQL_internet_ip` is not used and specified for demonstration purposes only.
- Parameters `macs` and `internet_ip` in the `config.json` file are not used and specified for demonstration purposes only.

View File

@@ -1,383 +0,0 @@
{
"code_name": "website_monitor",
"unique_prefix": "WEBMON",
"enabled": true,
"data_source": "python-script",
"localized": ["display_name", "description", "icon"],
"display_name" : [{
"language_code":"en_us",
"string" : "Website monitor"
}],
"icon":[{
"language_code":"en_us",
"string" : "<i class=\"fa-solid fa-globe\"></i>"
}],
"description": [{
"language_code":"en_us",
"string" : "This plugin is to monitor status changes of services or websites."
}],
"params" : [{
"name" : "macs",
"type" : "sql",
"value" : "SELECT dev_MAC from DEVICES"
},
{
"name" : "urls",
"type" : "setting",
"value" : "WEBMON_urls_to_check"
},
{
"name" : "internet_ip",
"type" : "setting",
"value" : "WEBMON_SQL_internet_ip"
}],
"database_column_definitions":
[
{
"column": "Index",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "N/A"
}]
} ,
{
"column": "Plugin",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "N/A"
}]
},
{
"column": "Object_PrimaryID",
"css_classes": "col-sm-2",
"show": true,
"type": "url",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Monitored URL"
}]
},
{
"column": "Object_SecondaryID",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "N/A"
}]
} ,
{
"column": "DateTimeCreated",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Created"
}]
},
{
"column": "DateTimeChanged",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Changed"
}]
},
{
"column": "Watched_Value1",
"css_classes": "col-sm-2",
"show": true,
"type": "threshold",
"default_value":"",
"options": [
{
"maximum": 199,
"hexColor": "#792D86"
},
{
"maximum": 299,
"hexColor": "#5B862D"
},
{
"maximum": 399,
"hexColor": "#7D862D"
},
{
"maximum": 499,
"hexColor": "#BF6440"
},
{
"maximum": 599,
"hexColor": "#D33115"
}
],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Status code"
}]
},
{
"column": "Watched_Value2",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Latency"
}]
},
{
"column": "Watched_Value3",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "N/A"
}]
} ,
{
"column": "Watched_Value4",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "N/A"
}]
} ,
{
"column": "UserData",
"css_classes": "col-sm-2",
"show": true,
"type": "textboxsave",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Comments"
}]
},
{
"column": "Status",
"css_classes": "col-sm-1",
"show": true,
"type": "replace",
"default_value":"",
"options": [
{
"equals": "watched-not-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-square-check'></i><div></div>"
},
{
"equals": "watched-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-triangle-exclamation'></i></div>"
},
{
"equals": "new",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-circle-plus'></i></div>"
}
],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Status"
}]
},
{
"column": "Extra",
"css_classes": "col-sm-3",
"show": false,
"type": "label",
"default_value":"",
"options": [],
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "Extra"
}]
}
],
"settings":[
{
"function": "RUN",
"type": "text.select",
"default_value":"disabled",
"options": ["disabled", "once", "schedule", "always_after_scan", "on_new_device"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "When to run"
}],
"description": [{
"language_code":"en_us",
"string" : "Enable a regular scan of your services. If you select <code>schedule</code> the scheduling settings from below are applied. If you select <code>once</code> the scan is run only once on start of the application (container) for the time specified in <a href=\"#WEBMON_RUN_TIMEOUT\"><code>WEBMON_RUN_TIMEOUT</code> setting</a>."
}]
},
{
"function": "CMD",
"type": "text",
"default_value":"python3 /home/pi/pialert/front/plugins/website_monitor/script.py urls={urls}",
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Command"
}],
"description": [{
"language_code":"en_us",
"string" : "Command to run"
}]
},
{
"function": "RUN_SCHD",
"type": "text",
"default_value":"0 2 * * *",
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Schedule"
}],
"description": [{
"language_code":"en_us",
"string" : "Only enabled if you select <code>schedule</code> in the <a href=\"#WEBMON_RUN\"><code>WEBMON_RUN</code> setting</a>. Make sure you enter the schedule in the correct cron-like format (e.g. validate at <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). For example entering <code>0 4 * * *</code> will run the scan after 4 am in the <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> you set above</a>. Will be run NEXT time the time passes."
}]
},
{
"function": "API_SQL",
"type": "text",
"default_value":"SELECT * FROM plugin_website_monitor",
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "API endpoint (not implemented)"
}],
"description": [{
"language_code":"en_us",
"string" : "You can specify a custom SQL query which will generate a JSON file and then expose it via the <a href=\"/api/plugin_website_monitor.json\" target=\"_blank\"><code>plugin_website_monitor.json</code> file endpoint</a>."
}]
},
{
"function": "RUN_TIMEOUT",
"type": "integer",
"default_value":5,
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Run timeout"
},
{
"language_code":"de_de",
"string" : "Wartezeit"
}],
"description": [{
"language_code":"en_us",
"string" : "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
}]
},
{
"function": "WATCH",
"type": "text.multiselect",
"default_value":["Watched_Value1"],
"options": ["Watched_Value1","Watched_Value2","Watched_Value3","Watched_Value4"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "Watched"
}] ,
"description":[{
"language_code":"en_us",
"string" : "Send a notification if selected values change. Use <code>CTRL + Click</code> to select/deselect. <ul> <li><code>Watched_Value1</code> is response status code (e.g.: 200, 404)</li><li><code>Watched_Value2</code> is Latency (not recommended)</li><li><code>Watched_Value3</code> unused </li><li><code>Watched_Value4</code> unused </li></ul>"
}]
},
{
"function": "REPORT_ON",
"type": "text.multiselect",
"default_value":["new","watched-changed"],
"options": ["new","watched-changed","watched-not-changed"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "Report on"
}] ,
"description":[{
"language_code":"en_us",
"string" : "Send a notification only on these statuses. <code>new</code> means a new unique (unique combination of PrimaryId and SecondaryId) object was discovered. <code>watched-changed</code> means that selected <code>Watched_ValueN</code> columns changed."
}]
},
{
"function": "urls_to_check",
"type": "list",
"default_value":["https://google.com", "https://duck.com"],
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Arguments"
}],
"description": [{
"language_code":"en_us",
"string" : "Services to watch. Enter full URL, e.g. <code>https://google.com</code>. The values from this setting will be used to replace the <code>{urls}</code> wildcard in the <code>WEBMON_CMD</code> setting."
}]
},
{
"function": "SQL_internet_ip",
"type": "readonly",
"default_value":"SELECT dev_LastIP FROM Devices WHERE dev_MAC = 'Internet'",
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Helper variable"
}],
"description": [{
"language_code":"en_us",
"string" : "Unused setting - for demonstration only. Getting the IP address of the Router / Internet. "
}]
}
]
}

View File

@@ -1,148 +0,0 @@
#!/usr/bin/env python
# Based on the work of https://github.com/leiweibau/Pi.Alert
# python3 /home/pi/pialert/front/plugins/website_monitor/script.py urls=http://google.com,http://bing.com
from __future__ import unicode_literals
from time import sleep, time, strftime
import requests
import pathlib
import argparse
import io
#import smtplib
import sys
#from smtp_config import sender, password, receivers, host, port
from requests.packages.urllib3.exceptions import InsecureRequestWarning
import pwd
import os
curPath = str(pathlib.Path(__file__).parent.resolve())
log_file = curPath + '/script.log'
last_run = curPath + '/last_result.log'
print(last_run)
# Workflow
def main():
parser = argparse.ArgumentParser(description='Simple URL monitoring tool')
parser.add_argument('urls', action="store", help="urls to check separated by ','")
values = parser.parse_args()
if values.urls:
with open(last_run, 'w') as last_run_logfile:
# empty file
last_run_logfile.write("")
service_monitoring(values.urls.split('=')[1].split(','))
else:
return
# -----------------------------------------------------------------------------
def service_monitoring_log(site, status, latency):
# global monitor_logfile
# Log status message to log file
with open(log_file, 'a') as monitor_logfile:
monitor_logfile.write("{} | {} | {} | {}\n".format(strftime("%Y-%m-%d %H:%M:%S"),
site,
status,
latency,
)
)
with open(last_run, 'a') as last_run_logfile:
# https://www.duckduckgo.com|192.168.0.1|2023-01-02 15:56:30|200|0.9898|null|null|Best search engine|null
last_run_logfile.write("{}|{}|{}|{}|{}|{}|{}|{}|{}\n".format(
site,
'null',
strftime("%Y-%m-%d %H:%M:%S"),
status,
latency,
'null',
'null',
'null',
'null',
)
)
# -----------------------------------------------------------------------------
def check_services_health(site):
# Enable self signed SSL
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
"""Send GET request to input site and return status code"""
try:
resp = requests.get(site, verify=False, timeout=10)
latency = resp.elapsed
latency_str = str(latency)
latency_str_seconds = latency_str.split(":")
format_latency_str = latency_str_seconds[2]
if format_latency_str[0] == "0" and format_latency_str[1] != "." :
format_latency_str = format_latency_str[1:]
return resp.status_code, format_latency_str
except requests.exceptions.SSLError:
pass
except:
latency = "99999"
return 503, latency
# -----------------------------------------------------------------------------
def get_username():
return pwd.getpwuid(os.getuid())[0]
# -----------------------------------------------------------------------------
def service_monitoring(urls):
# Empty Log and write new header
print("Prepare Services Monitoring")
print("... Prepare Logfile")
with open(log_file, 'w') as monitor_logfile:
monitor_logfile.write("Pi.Alert [Prototype]:\n---------------------------------------------------------\n")
monitor_logfile.write("Current User: %s \n\n" % get_username())
monitor_logfile.write("Monitor Web-Services\n")
monitor_logfile.write("Timestamp: " + strftime("%Y-%m-%d %H:%M:%S") + "\n")
monitor_logfile.close()
print("... Get Services List")
sites = urls
print("Start Services Monitoring")
with open(log_file, 'a') as monitor_logfile:
monitor_logfile.write("\nStart Services Monitoring\n\n| Timestamp | URL | StatusCode | ResponseTime |\n-----------------------------------------------\n")
monitor_logfile.close()
while sites:
for site in sites:
status,latency = check_services_health(site)
scantime = strftime("%Y-%m-%d %H:%M:%S")
# Debugging
# print("{} - {} STATUS: {} ResponseTime: {}".format(strftime("%Y-%m-%d %H:%M:%S"),
# site,
# status,
# latency)
# )
# Write Logfile
service_monitoring_log(site, status, latency)
sys.stdout.flush()
break
else:
with open(log_file, 'a') as monitor_logfile:
monitor_logfile.write("\n\nNo site(s) to monitor!")
monitor_logfile.close()
#===============================================================================
# BEGIN
#===============================================================================
if __name__ == '__main__':
sys.exit(main())