12 Commits
v1.4.0 ... main

Author SHA1 Message Date
e710dadfcd chore: bump libmeshctrl 2026-02-19 09:01:01 +01:00
Daan Selen
00e63e1a96 chore: remove old notice 2026-02-06 17:42:02 +01:00
Daan Selen
10057b5f91 correct text 2026-02-06 17:41:31 +01:00
Daan Selen
2cfd7ad06e chore: add caution notice 2026-02-06 17:40:35 +01:00
Daan Selen
6374ea50b7 docs: update README 2026-02-06 17:38:50 +01:00
f58e06ddec chore: edit readme 2026-01-29 16:48:44 +01:00
30026a18bd fix: KeyError due to previous commit 2026-01-15 13:40:21 +01:00
ea77ea1904 chore: add del to groups and devices 2026-01-15 13:38:31 +01:00
cc454dff40 chore: add more ends of sentences 2026-01-06 08:48:33 +01:00
cd10645056 chore: catch wrong keywords better 2026-01-02 15:54:04 +01:00
a652ea99d3 chore: fix apparent missing reference 2026-01-02 15:01:15 +01:00
e2cc746517 chore: add newline after content of meshbook in file 2026-01-02 09:40:37 +01:00
6 changed files with 85 additions and 83 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
venv
books
.vscode
important/
# Byte-compiled / optimized / DLL files
__pycache__/

View File

@@ -2,11 +2,9 @@
[![CodeQL Advanced](https://github.com/DaanSelen/meshbook/actions/workflows/codeql.yaml/badge.svg)](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!
---
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?
@@ -17,14 +15,11 @@ Meshbook is designed to:
* Allow configuration using simple and readable **YAML files** (like Ansible playbooks).
* Simplify the use of **group-based** or **tag-based** device targeting.
---
## 🏁 Quick Start
### ✅ Prerequisites
* Python 3.7+
* Git
* Python 3
* Access to a MeshCentral instance and credentials with:
* `Remote Commands`
@@ -33,8 +28,6 @@ Meshbook is designed to:
A service account with access to the relevant device groups is recommended.
---
### 🔧 Installation
#### Linux
@@ -45,7 +38,13 @@ cd ./meshbook
python3 -m venv ./venv
source ./venv/bin/activate
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)
@@ -56,13 +55,14 @@ cd .\meshbook
python -m venv .\venv
.\venv\Scripts\activate
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.
> The URL must start with `wss://<MeshCentral-Host>`.
Also here, make sure to fill in the `./api.conf` file.
---
> [!CAUTION]
> Meshbook will not work without a properly filled in `api.conf` file.
## 🚀 Running Meshbook
@@ -71,13 +71,13 @@ Once installed and configured, run a playbook like this:
### Linux:
```bash
python3 meshbook.py -pb ./examples/echo_example.yaml
python3 meshbook.py -mb ./examples/echo_example.yaml
```
### Windows:
```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:
@@ -86,8 +86,6 @@ Use `--help` to explore available command-line options:
python3 meshbook.py --help
```
---
## 🛠️ Creating Configurations
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*)
```yaml
---
name: My Configuration
group: "Dev Machines"
powershell: true
@@ -145,7 +143,7 @@ Each task must include:
* `name`: Description for human readability.
* `command`: The actual shell or PowerShell command.
---
## 🪟 Windows Client Notes
@@ -153,7 +151,7 @@ Each task must include:
* Ensure Windows commands are compatible (use `powershell: true` if needed).
* Examples are available in [`examples/windows`](./examples/windows).
---
## 🔎 OS & Tag Filtering
@@ -177,12 +175,10 @@ target_tag: "Production"
> ⚠️ Tag values are **case-sensitive**.
---
## 📋 Example Playbook
```yaml
---
name: Echo OS Info
group: "Dev"
target_os: "Linux"
@@ -198,7 +194,7 @@ Sample output:
```json
{
"Task 1": {
"task 1": {
"task_name": "Show contents of os-release",
"data": [
{
@@ -215,9 +211,7 @@ Sample output:
}
```
---
## ⚠️ Blocking Commands Warning
## ⚠ Blocking Commands Warning
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
apt upgrade # Without -y
sleep infinity
sleep infinity # Will never return
ping 1.1.1.1 # Without -c
```
@@ -233,10 +227,11 @@ ping 1.1.1.1 # Without -c
```bash
apt upgrade -y
sleep 3s
ping 1.1.1.1 -c 1
```
---
## 🧪 Check Python Environment
@@ -249,8 +244,6 @@ pip3 list
The lists should match. If not, make sure the correct environment is activated.
---
## 📂 Project Structure (excerpt)
```bash
@@ -272,11 +265,9 @@ meshbook/
├── os_categories.json
├── requirements.txt
├── templates/
│ └── config.conf.template
│ └── api.conf.template
```
---
## 📄 License
This project is licensed under the terms of the GPL3 License. See [LICENSE](./LICENSE).

View File

@@ -102,11 +102,15 @@ async def main():
if args.group != "":
meshbook["group"] = args.group
if "device" in meshbook:
del meshbook["device"]
del meshbook["device"]
if "devices" in meshbook:
del meshbook["devices"]
elif args.device != "":
meshbook["device"] = args.device
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.
@@ -166,8 +170,8 @@ async def main():
"Target groups: " + Console.text_color.yellow + str(meshbook["groups"]) + Console.text_color.reset + ".")
# 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, "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, "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" + 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)
@@ -189,7 +193,7 @@ async def main():
'''
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
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...")
history.write_history(formatted_history)
await session.close()
except OSError as message:
Console.print_text(args.silent,
Console.text_color.red + f'{message}')
Console.print_text(
args.silent,
Console.text_color.red + f'{message}'
)
except asyncio.CancelledError:
Console.print_text(args.silent,
Console.text_color.red + "Received SIGINT, Aborting - (Tasks may still be running on targets).")
await session.close()
Console.print_text(
args.silent,
Console.text_color.red + "Received SIGINT, Aborting - (Tasks may still be running on targets)."
)
raise
finally:
await session.close()
if __name__ == "__main__":
try:
asyncio.run(main())

View File

@@ -45,4 +45,4 @@ class History():
stitched_file = f"{self.history_directory}/meshbook_run_{datetime.now().strftime('%Y_%m_%d_%H_%M_%S')}.log"
with open(stitched_file, "x") as f:
f.write(history)
f.write(history + "\n")

View File

@@ -6,6 +6,8 @@ import os
import shlex
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.
'''
@@ -48,7 +50,7 @@ class Utilities:
return meshbook
@staticmethod
async def gather_targets(args: argparse.Namespace,
async def gather_targets(silent: bool,
meshbook: dict,
group_list: dict[str, list[dict]],
os_categories: dict) -> dict:
@@ -99,23 +101,28 @@ class Utilities:
await process_group_helper(group_list[pseudo_target])
elif pseudo_target not in group_list:
console.nice_print(
args,
console.text_color.yellow + "Targeted group not found on the MeshCentral server."
Console.print_text(
silent,
Console.text_color.yellow + "Targeted group not found on the MeshCentral server."
)
elif isinstance(pseudo_target, list):
console.nice_print(
args,
console.text_color.yellow + "Please use groups (Notice the plural with 'S') for multiple groups."
Console.print_text(
silent,
Console.text_color.yellow + "Please use groups (Notice the plural with 'S') for multiple groups."
)
else:
console.nice_print(
args,
console.text_color.yellow + "The 'group' key is being used, but an unknown data type was found, please check your values."
Console.print_text(
silent,
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}:
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:
sub_group = sub_group.lower()
if sub_group in group_list:
@@ -123,44 +130,39 @@ class Utilities:
elif isinstance(pseudo_target, str) and pseudo_target.lower() == "all":
for group in group_list.values():
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:
console.nice_print(
args,
console.text_color.yellow + "The 'groups' key is being used, but an unknown data type was found, please check your values."
Console.print_text(
silent,
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}:
if isinstance(pseudo_target, str):
await process_device_helper(pseudo_target)
elif isinstance(pseudo_target, list):
console.nice_print(
args,
console.text_color.yellow + "Please use devices (Notice the plural with 'S') for multiple devices."
Console.print_text(
silent,
Console.text_color.yellow + "Please use devices (Notice the plural with 'S') for multiple devices."
)
else:
console.nice_print(
args,
console.text_color.yellow + "The 'device' key is being used, but an unknown data type was found, please check your values."
Console.print_text(
silent,
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}:
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:
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:
console.nice_print(
args,
console.text_color.yellow + "The 'devices' key is being used, but an unknown data type was found, please check your values."
Console.print_text(
silent,
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}

View File

@@ -1,4 +1,4 @@
colorama==0.4.6
pyyaml==6.0.3
libmeshctrl==1.3.2
libmeshctrl==1.3.3
pyotp==2.9.0