mirror of
https://github.com/DaanSelen/meshbook.git
synced 2026-02-21 08:52:09 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e710dadfcd | |||
|
|
00e63e1a96 | ||
|
|
10057b5f91 | ||
|
|
2cfd7ad06e | ||
|
|
6374ea50b7 | ||
| f58e06ddec | |||
| 30026a18bd | |||
| ea77ea1904 | |||
| cc454dff40 | |||
| cd10645056 | |||
| a652ea99d3 | |||
| e2cc746517 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
venv
|
||||
books
|
||||
.vscode
|
||||
important/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
||||
61
README.md
61
README.md
@@ -2,11 +2,9 @@
|
||||
|
||||
[](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).
|
||||
|
||||
32
meshbook.py
32
meshbook.py
@@ -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())
|
||||
|
||||
@@ -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")
|
||||
@@ -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}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
colorama==0.4.6
|
||||
pyyaml==6.0.3
|
||||
libmeshctrl==1.3.2
|
||||
libmeshctrl==1.3.3
|
||||
pyotp==2.9.0
|
||||
|
||||
Reference in New Issue
Block a user