Add initial skills: caldav, netbird, ourgroceries, planka

This commit is contained in:
Bobby
2026-04-01 14:29:17 +01:00
commit cd5694aa13
7 changed files with 1121 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
---
name: netbird
description: Manage NetBird private mesh networks via the REST API. Use when Steve wants to interact with his self-hosted NetBird instance at https://net.dcglab.co.uk for tasks like listing peers, managing groups, creating setup keys, configuring policies, managing routes, or checking network status. Triggers on phrases like "netbird", "check my netbird", "list peers", "create setup key", "manage netbird groups", "netbird policy", "netbird routes".
---
# NetBird Skill
Manage your self-hosted NetBird mesh network via the REST API.
## Configuration
The skill expects a `NETBIRD_API_TOKEN` environment variable containing your Personal Access Token (PAT). Create one in your NetBird dashboard under User settings.
Base URL: `https://net.dcglab.co.uk/api`
## Quick Start
Use the provided Python script for common operations:
```bash
# List all peers
python3 scripts/netbird_cli.py peers list
# List all groups
python3 scripts/netbird_cli.py groups list
# Create a setup key
python3 scripts/netbird_cli.py setup-keys create --name "server-key" --type reusable --expires 86400
# List policies
python3 scripts/netbird_cli.py policies list
```
## Available Resources
- **API Reference**: See [references/api-reference.md](references/api-reference.md) for detailed endpoint documentation
## Common Operations
### Peers
- List, get details, update, delete peers
- Check connection status, IP addresses, groups
### Groups
- Create, list, update, delete groups
- Manage peer memberships
### Setup Keys
- Create reusable or one-off keys
- Set expiration and auto-assign groups
- Revoke keys
### Policies
- Create network access policies
- Define rules with sources, destinations, ports, protocols
- Enable/disable policies
### Routes
- Create network routes
- Assign to peer groups
### DNS
- Manage nameserver groups
- Configure DNS settings
### Users
- List users and service users
- Manage user roles and groups
## Authentication
All API requests require a Bearer token header:
```
Authorization: Bearer <token>
```
For self-hosted instances, create a Personal Access Token (PAT) in the NetBird dashboard under User settings → Personal Access Tokens.
## API Patterns
- All endpoints return JSON
- IDs are string-based (e.g., `chacbco6lnnbn6cg5s90`)
- Dates are ISO 8601 format
- Boolean fields use JSON true/false
+137
View File
@@ -0,0 +1,137 @@
# NetBird API Reference
Base URL: `https://net.dcglab.co.uk/api`
## Authentication
Header: `Authorization: Bearer <token>`
## Endpoints
### Peers
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/peers` | List all peers |
| GET | `/api/peers/{peerId}` | Get peer details |
| PUT | `/api/peers/{peerId}` | Update peer |
| DELETE | `/api/peers/{peerId}` | Delete peer |
**Peer Object Fields:**
- `id` - Unique identifier
- `name` - Peer name
- `ip` - NetBird IP address
- `connection_ip` - Public connection IP
- `connected` - Boolean connection status
- `last_seen` - ISO timestamp
- `os` - Operating system
- `hostname` - System hostname
- `groups` - Array of group objects
- `ssh_enabled` - Boolean SSH access
- `user_id` - Owner user ID
- `dns_label` - DNS name
- `approval_required` - Boolean pending approval
### Groups
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/groups` | List all groups |
| POST | `/api/groups` | Create group |
| GET | `/api/groups/{groupId}` | Get group details |
| PUT | `/api/groups/{groupId}` | Update group |
| DELETE | `/api/groups/{groupId}` | Delete group |
**Group Object Fields:**
- `id` - Unique identifier
- `name` - Group name
- `peers_count` - Number of peers
- `resources_count` - Number of resources
- `issued` - Creation source (api, etc.)
### Setup Keys
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/setup-keys` | List all setup keys |
| POST | `/api/setup-keys` | Create setup key |
| GET | `/api/setup-keys/{keyId}` | Get key details |
| PUT | `/api/setup-keys/{keyId}` | Update key |
| DELETE | `/api/setup-keys/{keyId}` | Delete key |
**Create Setup Key Body:**
```json
{
"name": "string",
"type": "one-off" | "reusable",
"expires_in": 86400,
"auto_groups": ["group-id-1", "group-id-2"]
}
```
### Policies
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/policies` | List all policies |
| POST | `/api/policies` | Create policy |
| GET | `/api/policies/{policyId}` | Get policy details |
| PUT | `/api/policies/{policyId}` | Update policy |
| DELETE | `/api/policies/{policyId}` | Delete policy |
**Policy Rule Fields:**
- `name` - Rule name
- `description` - Rule description
- `enabled` - Boolean status
- `action` - "accept" or "drop"
- `protocol` - "tcp", "udp", "icmp", "all"
- `ports` - Array of port strings (e.g., ["80", "443"])
- `sources` - Array of source group IDs
- `destinations` - Array of destination group IDs
- `bidirectional` - Boolean
### Routes
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/routes` | List all routes |
| POST | `/api/routes` | Create route |
| GET | `/api/routes/{routeId}` | Get route details |
| PUT | `/api/routes/{routeId}` | Update route |
| DELETE | `/api/routes/{routeId}` | Delete route |
### DNS
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/dns/nameservers` | List nameserver groups |
| POST | `/api/dns/nameservers` | Create nameserver group |
| GET | `/api/dns/nameservers/{nsgroupId}` | Get nameserver group |
| PUT | `/api/dns/nameservers/{nsgroupId}` | Update nameserver group |
| DELETE | `/api/dns/nameservers/{nsgroupId}` | Delete nameserver group |
| GET | `/api/dns/settings` | Get DNS settings |
| PUT | `/api/dns/settings` | Update DNS settings |
### Users
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/users` | List all users |
| POST | `/api/users` | Create service user or invite |
| GET | `/api/users/{userId}` | Get user details |
| PUT | `/api/users/{userId}` | Update user |
| DELETE | `/api/users/{userId}` | Delete user |
| POST | `/api/users/{userId}/invite` | Resend invitation |
| POST | `/api/users/{userId}/approve` | Approve pending user |
| DELETE | `/api/users/{userId}/reject` | Reject pending user |
## Common Patterns
### Filter by Name
Many list endpoints support `?name=` query parameter for exact match filtering.
### Service Users
Set `is_service_user: true` when creating users for automation/API access.
### Auto Groups
When creating setup keys or users, specify `auto_groups` to automatically assign peers to groups.
+391
View File
@@ -0,0 +1,391 @@
#!/usr/bin/env python3
"""
NetBird CLI - Simple command-line interface for NetBird API
"""
import os
import sys
import json
import argparse
import urllib.request
import urllib.error
import urllib.parse
from typing import Optional, Dict, Any, List
BASE_URL = "https://net.dcglab.co.uk/api"
def get_token() -> str:
"""Get API token from environment."""
token = os.environ.get("NETBIRD_API_TOKEN")
if not token:
print("Error: NETBIRD_API_TOKEN environment variable not set", file=sys.stderr)
print("Create a Personal Access Token in your NetBird dashboard:", file=sys.stderr)
print(" User settings → Personal Access Tokens", file=sys.stderr)
sys.exit(1)
return token
def api_request(
method: str,
endpoint: str,
data: Optional[Dict] = None,
params: Optional[Dict] = None,
) -> Any:
"""Make an API request to NetBird."""
token = get_token()
url = f"{BASE_URL}{endpoint}"
if params:
query = "&".join(f"{k}={urllib.parse.quote(str(v))}" for k, v in params.items())
url = f"{url}?{query}"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
req = urllib.request.Request(
url,
method=method,
headers=headers,
)
if data:
req.data = json.dumps(data).encode("utf-8")
try:
with urllib.request.urlopen(req) as response:
if response.status == 204:
return None
return json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as e:
error_body = e.read().decode("utf-8")
print(f"HTTP Error {e.code}: {e.reason}", file=sys.stderr)
try:
error_json = json.loads(error_body)
print(json.dumps(error_json, indent=2), file=sys.stderr)
except:
print(error_body, file=sys.stderr)
sys.exit(1)
except urllib.error.URLError as e:
print(f"Connection error: {e.reason}", file=sys.stderr)
sys.exit(1)
def format_peer(peer: Dict) -> str:
"""Format peer for display."""
status = "🟢" if peer.get("connected") else "🔴"
name = peer.get("name", "unknown")
ip = peer.get("ip", "N/A")
hostname = peer.get("hostname", "N/A")
os_info = peer.get("os", "N/A")
return f"{status} {name} ({hostname}) - IP: {ip} - OS: {os_info}"
def format_group(group: Dict) -> str:
"""Format group for display."""
name = group.get("name", "unknown")
gid = group.get("id", "N/A")[:8]
peers = group.get("peers_count", 0)
return f"{name} ({gid}...) - {peers} peers"
def format_policy(policy: Dict) -> str:
"""Format policy for display."""
name = policy.get("name", "unknown")
enabled = "" if policy.get("enabled") else ""
rules_count = len(policy.get("rules", []))
return f"{enabled} {name} - {rules_count} rules"
def format_setup_key(key: Dict) -> str:
"""Format setup key for display."""
name = key.get("name", "unknown")
key_type = key.get("type", "unknown")
revoked = "🚫 REVOKED" if key.get("revoked") else ""
auto_groups = ", ".join(key.get("auto_groups", [])) or "none"
return f"{name} ({key_type}) - Auto-groups: {auto_groups} {revoked}"
def format_route(route: Dict) -> str:
"""Format route for display."""
desc = route.get("description", "unknown")
network = route.get("network", "N/A")
enabled = "" if route.get("enabled") else ""
return f"{enabled} {desc} - {network}"
# ===== PEERS =====
def list_peers():
"""List all peers."""
peers = api_request("GET", "/peers")
if not peers:
print("No peers found.")
return
print(f"Found {len(peers)} peer(s):\n")
for peer in peers:
print(format_peer(peer))
def get_peer(peer_id: str):
"""Get peer details."""
peer = api_request("GET", f"/peers/{peer_id}")
print(json.dumps(peer, indent=2))
def delete_peer(peer_id: str):
"""Delete a peer."""
api_request("DELETE", f"/peers/{peer_id}")
print(f"Peer {peer_id} deleted.")
# ===== GROUPS =====
def list_groups():
"""List all groups."""
groups = api_request("GET", "/groups")
if not groups:
print("No groups found.")
return
print(f"Found {len(groups)} group(s):\n")
for group in groups:
print(format_group(group))
def get_group(group_id: str):
"""Get group details."""
group = api_request("GET", f"/groups/{group_id}")
print(json.dumps(group, indent=2))
def create_group(name: str, peers: Optional[List[str]] = None):
"""Create a new group."""
data = {"name": name}
if peers:
data["peers"] = peers
group = api_request("POST", "/groups", data=data)
print(f"Group created: {group.get('name')} (ID: {group.get('id')})")
def delete_group(group_id: str):
"""Delete a group."""
api_request("DELETE", f"/groups/{group_id}")
print(f"Group {group_id} deleted.")
# ===== SETUP KEYS =====
def list_setup_keys():
"""List all setup keys."""
keys = api_request("GET", "/setup-keys")
if not keys:
print("No setup keys found.")
return
print(f"Found {len(keys)} setup key(s):\n")
for key in keys:
print(format_setup_key(key))
def create_setup_key(name: str, key_type: str, expires: int, auto_groups: Optional[List[str]] = None):
"""Create a new setup key."""
data = {
"name": name,
"type": key_type,
"expires_in": expires,
"auto_groups": auto_groups or []
}
key = api_request("POST", "/setup-keys", data=data)
print(f"Setup key created: {key.get('name')}")
print(f"Key: {key.get('key')}")
print(f"ID: {key.get('id')}")
def revoke_setup_key(key_id: str):
"""Revoke a setup key."""
api_request("PUT", f"/setup-keys/{key_id}", data={"revoked": True, "auto_groups": []})
print(f"Setup key {key_id} revoked.")
# ===== POLICIES =====
def list_policies():
"""List all policies."""
policies = api_request("GET", "/policies")
if not policies:
print("No policies found.")
return
print(f"Found {len(policies)} policy(ies):\n")
for policy in policies:
print(format_policy(policy))
def get_policy(policy_id: str):
"""Get policy details."""
policy = api_request("GET", f"/policies/{policy_id}")
print(json.dumps(policy, indent=2))
def delete_policy(policy_id: str):
"""Delete a policy."""
api_request("DELETE", f"/policies/{policy_id}")
print(f"Policy {policy_id} deleted.")
# ===== ROUTES =====
def list_routes():
"""List all routes."""
routes = api_request("GET", "/routes")
if not routes:
print("No routes found.")
return
print(f"Found {len(routes)} route(s):\n")
for route in routes:
print(format_route(route))
def get_route(route_id: str):
"""Get route details."""
route = api_request("GET", f"/routes/{route_id}")
print(json.dumps(route, indent=2))
# ===== USERS =====
def list_users():
"""List all users."""
users = api_request("GET", "/users")
if not users:
print("No users found.")
return
print(f"Found {len(users)} user(s):\n")
for user in users:
name = user.get("name", user.get("email", "unknown"))
role = user.get("role", "unknown")
service = "[service]" if user.get("is_service_user") else ""
print(f"{name} ({role}) {service}")
def main():
parser = argparse.ArgumentParser(description="NetBird CLI")
subparsers = parser.add_subparsers(dest="resource", help="Resource type")
# Peers
peers_parser = subparsers.add_parser("peers", help="Manage peers")
peers_sub = peers_parser.add_subparsers(dest="action")
peers_sub.add_parser("list", help="List all peers")
peers_get = peers_sub.add_parser("get", help="Get peer details")
peers_get.add_argument("peer_id", help="Peer ID")
peers_del = peers_sub.add_parser("delete", help="Delete a peer")
peers_del.add_argument("peer_id", help="Peer ID")
# Groups
groups_parser = subparsers.add_parser("groups", help="Manage groups")
groups_sub = groups_parser.add_subparsers(dest="action")
groups_sub.add_parser("list", help="List all groups")
groups_get = groups_sub.add_parser("get", help="Get group details")
groups_get.add_argument("group_id", help="Group ID")
groups_create = groups_sub.add_parser("create", help="Create a group")
groups_create.add_argument("name", help="Group name")
groups_create.add_argument("--peers", nargs="+", help="Peer IDs to add")
groups_del = groups_sub.add_parser("delete", help="Delete a group")
groups_del.add_argument("group_id", help="Group ID")
# Setup Keys
keys_parser = subparsers.add_parser("setup-keys", help="Manage setup keys")
keys_sub = keys_parser.add_subparsers(dest="action")
keys_sub.add_parser("list", help="List all setup keys")
keys_create = keys_sub.add_parser("create", help="Create a setup key")
keys_create.add_argument("--name", required=True, help="Key name")
keys_create.add_argument("--type", choices=["one-off", "reusable"], default="reusable", help="Key type")
keys_create.add_argument("--expires", type=int, default=86400, help="Expiration in seconds (default: 86400 = 24h)")
keys_create.add_argument("--auto-groups", nargs="+", help="Group IDs to auto-assign")
keys_revoke = keys_sub.add_parser("revoke", help="Revoke a setup key")
keys_revoke.add_argument("key_id", help="Key ID")
# Policies
policies_parser = subparsers.add_parser("policies", help="Manage policies")
policies_sub = policies_parser.add_subparsers(dest="action")
policies_sub.add_parser("list", help="List all policies")
policies_get = policies_sub.add_parser("get", help="Get policy details")
policies_get.add_argument("policy_id", help="Policy ID")
policies_del = policies_sub.add_parser("delete", help="Delete a policy")
policies_del.add_argument("policy_id", help="Policy ID")
# Routes
routes_parser = subparsers.add_parser("routes", help="Manage routes")
routes_sub = routes_parser.add_subparsers(dest="action")
routes_sub.add_parser("list", help="List all routes")
routes_get = routes_sub.add_parser("get", help="Get route details")
routes_get.add_argument("route_id", help="Route ID")
# Users
users_parser = subparsers.add_parser("users", help="Manage users")
users_sub = users_parser.add_subparsers(dest="action")
users_sub.add_parser("list", help="List all users")
args = parser.parse_args()
if not args.resource:
parser.print_help()
sys.exit(1)
# Handle commands
if args.resource == "peers":
if args.action == "list":
list_peers()
elif args.action == "get":
get_peer(args.peer_id)
elif args.action == "delete":
delete_peer(args.peer_id)
elif args.resource == "groups":
if args.action == "list":
list_groups()
elif args.action == "get":
get_group(args.group_id)
elif args.action == "create":
create_group(args.name, args.peers)
elif args.action == "delete":
delete_group(args.group_id)
elif args.resource == "setup-keys":
if args.action == "list":
list_setup_keys()
elif args.action == "create":
create_setup_key(args.name, args.type, args.expires, args.auto_groups)
elif args.action == "revoke":
revoke_setup_key(args.key_id)
elif args.resource == "policies":
if args.action == "list":
list_policies()
elif args.action == "get":
get_policy(args.policy_id)
elif args.action == "delete":
delete_policy(args.policy_id)
elif args.resource == "routes":
if args.action == "list":
list_routes()
elif args.action == "get":
get_route(args.route_id)
elif args.resource == "users":
if args.action == "list":
list_users()
if __name__ == "__main__":
main()