Compare commits
10 Commits
e2cc746517
...
00e63e1a96
| Author | SHA1 | Date | |
|---|---|---|---|
| 00e63e1a96 | |||
| 10057b5f91 | |||
| 2cfd7ad06e | |||
| 6374ea50b7 | |||
| f58e06ddec | |||
| 30026a18bd | |||
| ea77ea1904 | |||
| cc454dff40 | |||
| cd10645056 | |||
| a652ea99d3 |
@@ -2,6 +2,7 @@
|
|||||||
venv
|
venv
|
||||||
books
|
books
|
||||||
.vscode
|
.vscode
|
||||||
|
important/
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
@@ -2,11 +2,9 @@
|
|||||||
|
|
||||||
[](https://github.com/DaanSelen/meshbook/actions/workflows/codeql.yaml)
|
[](https://github.com/DaanSelen/meshbook/actions/workflows/codeql.yaml)
|
||||||
|
|
||||||
> \[!NOTE]
|
> [!NOTE]
|
||||||
> 💬 If you experience issues or have suggestions, [submit an issue](https://github.com/DaanSelen/meshbook/issues) — I'll respond ASAP!
|
> 💬 If you experience issues or have suggestions, [submit an issue](https://github.com/DaanSelen/meshbook/issues) — I'll respond ASAP!
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Meshbook is a tool to **programmatically manage MeshCentral-managed machines**, inspired by tools like [Ansible](https://github.com/ansible/ansible).
|
Meshbook is a tool to **programmatically manage MeshCentral-managed machines**, inspired by tools like [Ansible](https://github.com/ansible/ansible).
|
||||||
|
|
||||||
## What problem does it solve?
|
## What problem does it solve?
|
||||||
@@ -17,14 +15,11 @@ Meshbook is designed to:
|
|||||||
* Allow configuration using simple and readable **YAML files** (like Ansible playbooks).
|
* Allow configuration using simple and readable **YAML files** (like Ansible playbooks).
|
||||||
* Simplify the use of **group-based** or **tag-based** device targeting.
|
* Simplify the use of **group-based** or **tag-based** device targeting.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏁 Quick Start
|
## 🏁 Quick Start
|
||||||
|
|
||||||
### ✅ Prerequisites
|
### ✅ Prerequisites
|
||||||
|
|
||||||
* Python 3.7+
|
* Python 3
|
||||||
* Git
|
|
||||||
* Access to a MeshCentral instance and credentials with:
|
* Access to a MeshCentral instance and credentials with:
|
||||||
|
|
||||||
* `Remote Commands`
|
* `Remote Commands`
|
||||||
@@ -33,8 +28,6 @@ Meshbook is designed to:
|
|||||||
|
|
||||||
A service account with access to the relevant device groups is recommended.
|
A service account with access to the relevant device groups is recommended.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🔧 Installation
|
### 🔧 Installation
|
||||||
|
|
||||||
#### Linux
|
#### Linux
|
||||||
@@ -45,7 +38,13 @@ cd ./meshbook
|
|||||||
python3 -m venv ./venv
|
python3 -m venv ./venv
|
||||||
source ./venv/bin/activate
|
source ./venv/bin/activate
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
cp ./templates/meshcentral.conf.template ./meshcentral.conf
|
cp ./templates/api.conf.template ./api.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, make sure to fill in the following file:
|
||||||
|
|
||||||
|
```
|
||||||
|
nano ./api.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Windows (PowerShell)
|
#### Windows (PowerShell)
|
||||||
@@ -56,13 +55,14 @@ cd .\meshbook
|
|||||||
python -m venv .\venv
|
python -m venv .\venv
|
||||||
.\venv\Scripts\activate
|
.\venv\Scripts\activate
|
||||||
pip install -r .\requirements.txt
|
pip install -r .\requirements.txt
|
||||||
cp .\templates\meshcentral.conf.template .\meshcentral.conf
|
cp .\templates\api.conf.template .\api.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
> 📌 Rename `meshcentral.conf.template` to `meshcentral.conf` and fill in your actual connection details.
|
Also here, make sure to fill in the `./api.conf` file.
|
||||||
> The URL must start with `wss://<MeshCentral-Host>`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> Meshbook will not work without a properly filled in `api.conf` file.
|
||||||
|
|
||||||
## 🚀 Running Meshbook
|
## 🚀 Running Meshbook
|
||||||
|
|
||||||
@@ -71,13 +71,13 @@ Once installed and configured, run a playbook like this:
|
|||||||
### Linux:
|
### Linux:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 meshbook.py -pb ./examples/echo_example.yaml
|
python3 meshbook.py -mb ./examples/echo_example.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
### Windows:
|
### Windows:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\venv\Scripts\python.exe .\meshbook.py -pb .\examples\echo_example.yaml
|
.\venv\Scripts\python.exe .\meshbook.py -mb .\examples\echo_example.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `--help` to explore available command-line options:
|
Use `--help` to explore available command-line options:
|
||||||
@@ -86,8 +86,6 @@ Use `--help` to explore available command-line options:
|
|||||||
python3 meshbook.py --help
|
python3 meshbook.py --help
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Creating Configurations
|
## 🛠️ Creating Configurations
|
||||||
|
|
||||||
Meshbook configurations are written in YAML. Below is an overview of supported fields.
|
Meshbook configurations are written in YAML. Below is an overview of supported fields.
|
||||||
@@ -95,7 +93,7 @@ Meshbook configurations are written in YAML. Below is an overview of supported f
|
|||||||
### ▶️ Group Targeting (Primary*)
|
### ▶️ Group Targeting (Primary*)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
---
|
|
||||||
name: My Configuration
|
name: My Configuration
|
||||||
group: "Dev Machines"
|
group: "Dev Machines"
|
||||||
powershell: true
|
powershell: true
|
||||||
@@ -145,7 +143,7 @@ Each task must include:
|
|||||||
* `name`: Description for human readability.
|
* `name`: Description for human readability.
|
||||||
* `command`: The actual shell or PowerShell command.
|
* `command`: The actual shell or PowerShell command.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🪟 Windows Client Notes
|
## 🪟 Windows Client Notes
|
||||||
|
|
||||||
@@ -153,7 +151,7 @@ Each task must include:
|
|||||||
* Ensure Windows commands are compatible (use `powershell: true` if needed).
|
* Ensure Windows commands are compatible (use `powershell: true` if needed).
|
||||||
* Examples are available in [`examples/windows`](./examples/windows).
|
* Examples are available in [`examples/windows`](./examples/windows).
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔎 OS & Tag Filtering
|
## 🔎 OS & Tag Filtering
|
||||||
|
|
||||||
@@ -177,12 +175,10 @@ target_tag: "Production"
|
|||||||
|
|
||||||
> ⚠️ Tag values are **case-sensitive**.
|
> ⚠️ Tag values are **case-sensitive**.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Example Playbook
|
## 📋 Example Playbook
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
---
|
|
||||||
name: Echo OS Info
|
name: Echo OS Info
|
||||||
group: "Dev"
|
group: "Dev"
|
||||||
target_os: "Linux"
|
target_os: "Linux"
|
||||||
@@ -198,7 +194,7 @@ Sample output:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"Task 1": {
|
"task 1": {
|
||||||
"task_name": "Show contents of os-release",
|
"task_name": "Show contents of os-release",
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
@@ -215,9 +211,7 @@ Sample output:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## ⚠ Blocking Commands Warning
|
||||||
|
|
||||||
## ⚠️ Blocking Commands Warning
|
|
||||||
|
|
||||||
Avoid using commands that **block indefinitely** — MeshCentral requires **non-blocking** execution.
|
Avoid using commands that **block indefinitely** — MeshCentral requires **non-blocking** execution.
|
||||||
|
|
||||||
@@ -225,7 +219,7 @@ Avoid using commands that **block indefinitely** — MeshCentral requires **non-
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
apt upgrade # Without -y
|
apt upgrade # Without -y
|
||||||
sleep infinity
|
sleep infinity # Will never return
|
||||||
ping 1.1.1.1 # Without -c
|
ping 1.1.1.1 # Without -c
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -233,10 +227,11 @@ ping 1.1.1.1 # Without -c
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
apt upgrade -y
|
apt upgrade -y
|
||||||
|
sleep 3s
|
||||||
ping 1.1.1.1 -c 1
|
ping 1.1.1.1 -c 1
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Check Python Environment
|
## 🧪 Check Python Environment
|
||||||
|
|
||||||
@@ -249,8 +244,6 @@ pip3 list
|
|||||||
|
|
||||||
The lists should match. If not, make sure the correct environment is activated.
|
The lists should match. If not, make sure the correct environment is activated.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📂 Project Structure (excerpt)
|
## 📂 Project Structure (excerpt)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -272,11 +265,9 @@ meshbook/
|
|||||||
├── os_categories.json
|
├── os_categories.json
|
||||||
├── requirements.txt
|
├── requirements.txt
|
||||||
├── templates/
|
├── templates/
|
||||||
│ └── config.conf.template
|
│ └── api.conf.template
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📄 License
|
## 📄 License
|
||||||
|
|
||||||
This project is licensed under the terms of the GPL3 License. See [LICENSE](./LICENSE).
|
This project is licensed under the terms of the GPL3 License. See [LICENSE](./LICENSE).
|
||||||
|
|||||||
+18
-10
@@ -103,10 +103,14 @@ async def main():
|
|||||||
meshbook["group"] = args.group
|
meshbook["group"] = args.group
|
||||||
if "device" in meshbook:
|
if "device" in meshbook:
|
||||||
del meshbook["device"]
|
del meshbook["device"]
|
||||||
|
if "devices" in meshbook:
|
||||||
|
del meshbook["devices"]
|
||||||
elif args.device != "":
|
elif args.device != "":
|
||||||
meshbook["device"] = args.device
|
meshbook["device"] = args.device
|
||||||
if "group" in meshbook:
|
if "group" in meshbook:
|
||||||
del meshbook["group"]
|
del meshbook["group"]
|
||||||
|
if "groups" in meshbook:
|
||||||
|
del meshbook["groups"]
|
||||||
|
|
||||||
'''
|
'''
|
||||||
The following section mainly displays used variables and first steps of the program to the Console.
|
The following section mainly displays used variables and first steps of the program to the Console.
|
||||||
@@ -166,8 +170,8 @@ async def main():
|
|||||||
"Target groups: " + Console.text_color.yellow + str(meshbook["groups"]) + Console.text_color.reset + ".")
|
"Target groups: " + Console.text_color.yellow + str(meshbook["groups"]) + Console.text_color.reset + ".")
|
||||||
|
|
||||||
# RUNNING PARAMETERS PRINTING
|
# RUNNING PARAMETERS PRINTING
|
||||||
Console.print_text(args.silent, "Grace: " + Console.text_color.yellow + str((not args.nograce))) # Negation of bool for correct explanation
|
Console.print_text(args.silent, "Grace: " + Console.text_color.yellow + str(not args.nograce) + Console.text_color.reset + ".") # Negation of bool for correct explanation
|
||||||
Console.print_text(args.silent, "Silent: " + Console.text_color.yellow + "False") # Can be pre-defined because if silent flag was passed then none of this would be printed.
|
Console.print_text(args.silent, "Silent: " + Console.text_color.yellow + "False" + Console.text_color.reset + ".") # Can be pre-defined because if silent flag was passed then none of this would be printed.
|
||||||
|
|
||||||
session = await init_connection(credentials)
|
session = await init_connection(credentials)
|
||||||
|
|
||||||
@@ -189,7 +193,7 @@ async def main():
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
group_list = await Transform.compile_group_list(session)
|
group_list = await Transform.compile_group_list(session)
|
||||||
compiled_device_list = await Utilities.gather_targets(args, meshbook, group_list, os_categories)
|
compiled_device_list = await Utilities.gather_targets(args.silent, meshbook, group_list, os_categories)
|
||||||
|
|
||||||
# Check if we have reachable targets on the MeshCentral host
|
# Check if we have reachable targets on the MeshCentral host
|
||||||
if "target_list" not in compiled_device_list or len(compiled_device_list["target_list"]) == 0:
|
if "target_list" not in compiled_device_list or len(compiled_device_list["target_list"]) == 0:
|
||||||
@@ -259,18 +263,22 @@ async def main():
|
|||||||
Console.print_text(args.silent, "Writing to file...")
|
Console.print_text(args.silent, "Writing to file...")
|
||||||
history.write_history(formatted_history)
|
history.write_history(formatted_history)
|
||||||
|
|
||||||
await session.close()
|
|
||||||
|
|
||||||
except OSError as message:
|
except OSError as message:
|
||||||
Console.print_text(args.silent,
|
Console.print_text(
|
||||||
Console.text_color.red + f'{message}')
|
args.silent,
|
||||||
|
Console.text_color.red + f'{message}'
|
||||||
|
)
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
Console.print_text(args.silent,
|
Console.print_text(
|
||||||
Console.text_color.red + "Received SIGINT, Aborting - (Tasks may still be running on targets).")
|
args.silent,
|
||||||
await session.close()
|
Console.text_color.red + "Received SIGINT, Aborting - (Tasks may still be running on targets)."
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
finally:
|
||||||
|
await session.close()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
|
|||||||
+36
-34
@@ -6,6 +6,8 @@ import os
|
|||||||
import shlex
|
import shlex
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from modules.console import Console
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Creation and compilation of the MeshCentral nodes list (list of all nodes available to the user in the configuration) is handled in the following section.
|
Creation and compilation of the MeshCentral nodes list (list of all nodes available to the user in the configuration) is handled in the following section.
|
||||||
'''
|
'''
|
||||||
@@ -48,7 +50,7 @@ class Utilities:
|
|||||||
return meshbook
|
return meshbook
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def gather_targets(args: argparse.Namespace,
|
async def gather_targets(silent: bool,
|
||||||
meshbook: dict,
|
meshbook: dict,
|
||||||
group_list: dict[str, list[dict]],
|
group_list: dict[str, list[dict]],
|
||||||
os_categories: dict) -> dict:
|
os_categories: dict) -> dict:
|
||||||
@@ -99,23 +101,28 @@ class Utilities:
|
|||||||
await process_group_helper(group_list[pseudo_target])
|
await process_group_helper(group_list[pseudo_target])
|
||||||
|
|
||||||
elif pseudo_target not in group_list:
|
elif pseudo_target not in group_list:
|
||||||
console.nice_print(
|
Console.print_text(
|
||||||
args,
|
silent,
|
||||||
console.text_color.yellow + "Targeted group not found on the MeshCentral server."
|
Console.text_color.yellow + "Targeted group not found on the MeshCentral server."
|
||||||
)
|
)
|
||||||
elif isinstance(pseudo_target, list):
|
elif isinstance(pseudo_target, list):
|
||||||
console.nice_print(
|
Console.print_text(
|
||||||
args,
|
silent,
|
||||||
console.text_color.yellow + "Please use groups (Notice the plural with 'S') for multiple groups."
|
Console.text_color.yellow + "Please use groups (Notice the plural with 'S') for multiple groups."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
console.nice_print(
|
Console.print_text(
|
||||||
args,
|
silent,
|
||||||
console.text_color.yellow + "The 'group' key is being used, but an unknown data type was found, please check your values."
|
Console.text_color.yellow + "The 'group' key is being used, but an unknown data type was found, please check your values."
|
||||||
)
|
)
|
||||||
|
|
||||||
case {"groups": pseudo_target}:
|
case {"groups": pseudo_target}:
|
||||||
if isinstance(pseudo_target, list):
|
if isinstance(pseudo_target, str) or (isinstance(pseudo_target, list) and len(pseudo_target) == 1):
|
||||||
|
Console.print_text(
|
||||||
|
silent,
|
||||||
|
Console.text_color.yellow + "The 'groups' key is being used, but only one group seems to be given. Did you mean 'group'?"
|
||||||
|
)
|
||||||
|
elif isinstance(pseudo_target, list):
|
||||||
for sub_group in pseudo_target:
|
for sub_group in pseudo_target:
|
||||||
sub_group = sub_group.lower()
|
sub_group = sub_group.lower()
|
||||||
if sub_group in group_list:
|
if sub_group in group_list:
|
||||||
@@ -123,44 +130,39 @@ class Utilities:
|
|||||||
elif isinstance(pseudo_target, str) and pseudo_target.lower() == "all":
|
elif isinstance(pseudo_target, str) and pseudo_target.lower() == "all":
|
||||||
for group in group_list.values():
|
for group in group_list.values():
|
||||||
await process_group_helper(group)
|
await process_group_helper(group)
|
||||||
elif isinstance(pseudo_target, str):
|
|
||||||
console.nice_print(
|
|
||||||
args,
|
|
||||||
console.text_color.yellow + "The 'groups' key is being used, but only one string is given. Did you mean 'group'?"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
console.nice_print(
|
Console.print_text(
|
||||||
args,
|
silent,
|
||||||
console.text_color.yellow + "The 'groups' key is being used, but an unknown data type was found, please check your values."
|
Console.text_color.yellow + "The 'groups' key is being used, but an unknown data type was found, please check your values."
|
||||||
)
|
)
|
||||||
|
|
||||||
case {"device": pseudo_target}:
|
case {"device": pseudo_target}:
|
||||||
if isinstance(pseudo_target, str):
|
if isinstance(pseudo_target, str):
|
||||||
await process_device_helper(pseudo_target)
|
await process_device_helper(pseudo_target)
|
||||||
elif isinstance(pseudo_target, list):
|
elif isinstance(pseudo_target, list):
|
||||||
console.nice_print(
|
Console.print_text(
|
||||||
args,
|
silent,
|
||||||
console.text_color.yellow + "Please use devices (Notice the plural with 'S') for multiple devices."
|
Console.text_color.yellow + "Please use devices (Notice the plural with 'S') for multiple devices."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
console.nice_print(
|
Console.print_text(
|
||||||
args,
|
silent,
|
||||||
console.text_color.yellow + "The 'device' key is being used, but an unknown data type was found, please check your values."
|
Console.text_color.yellow + "The 'device' key is being used, but an unknown data type was found, please check your values."
|
||||||
)
|
)
|
||||||
|
|
||||||
case {"devices": pseudo_target}:
|
case {"devices": pseudo_target}:
|
||||||
if isinstance(pseudo_target, list):
|
if isinstance(pseudo_target, str) or (isinstance(pseudo_target, list) and len(pseudo_target) == 1):
|
||||||
|
Console.print_text(
|
||||||
|
silent,
|
||||||
|
Console.text_color.yellow + "The 'devices' key is being used, but only one device seems to be given. Did you mean 'device'?"
|
||||||
|
)
|
||||||
|
elif isinstance(pseudo_target, list):
|
||||||
for sub_device in pseudo_target:
|
for sub_device in pseudo_target:
|
||||||
await process_device_helper(sub_device)
|
await process_device_helper(sub_device)
|
||||||
elif isinstance(pseudo_target, str):
|
|
||||||
console.nice_print(
|
|
||||||
args,
|
|
||||||
console.text_color.yellow + "The 'devices' key is being used, but only one string is given. Did you mean 'device'?"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
console.nice_print(
|
Console.print_text(
|
||||||
args,
|
silent,
|
||||||
console.text_color.yellow + "The 'devices' key is being used, but an unknown data type was found, please check your values."
|
Console.text_color.yellow + "The 'devices' key is being used, but an unknown data type was found, please check your values."
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"target_list": target_list, "offline_list": offline_list}
|
return {"target_list": target_list, "offline_list": offline_list}
|
||||||
|
|||||||
Reference in New Issue
Block a user