Skip to main content

Granny

One CLI for the cloud chores you'd otherwise do across six tabs.

granny is a pragmatic, multi-provider DevOps toolkit with a strong bias toward European cloud infrastructure. It wraps the parts of provider APIs you actually use day-to-day — Bunny pull zones, Cloudflare DNS, Hetzner S3, Scaleway functions, Mailjet sender setup, INWX zones, AWS Lambda inventory — behind a single command:

granny dns add api.example.com --type A --value 203.0.113.4 --provider hetzner
granny cdn purge 12345
granny storage bunny upload my-zone ./dist
granny credentials status

What it is

  • Pragmatic, not exhaustive. Granny implements the parts of each provider API the maintainers needed in production. It will not cover every endpoint of every service — and it will not pretend to. Add what you need; the contributor guide explains how.
  • Multi-cloud, with a European tilt. First-class support for Bunny, Cloudflare, Hetzner, deSEC, ClouDNS, INWX, Scaleway, and Mailjet alongside AWS S3 / Lambda / WorkMail. The cloud world doesn't end at AWS, and granny doesn't pretend it does.
  • Secrets done right. Every credential goes through one resolver chain — environment variable first, optional Vaultwarden vault second, never hardcoded. .env and .deploy.env are auto-loaded; vault support is a one-line activation when you want it.
  • Library or CLI. Every CLI subcommand is a thin shim over a small Python module. Import granny.dns.cloudflare, granny.cdn.bunny, granny.cloudflare.d1 directly when you need to script something the CLI doesn't expose yet.
  • Small, composable, no plugin system. The whole package is one pip install away. No daemon, no service, no opinionated framework. Drop it into your CI image and call it a day.

Install

pip install granny-devops
# or
uv add granny-devops

Optional extras enable additional providers:

pip install "granny-devops[gcp]"     # GCP for granny analyze (gpus|credits|costs)
pip install "granny-devops[azure]"   # Azure for granny analyze (gpus|credits|costs)
pip install "granny-devops[cdn]"     # Cloudflare, Hetzner DNS

Tagged releases land on pypi.org and the public GitLab PyPI registry in parallel. If PyPI is propagating slowly, fall back to the registry:

pip install --extra-index-url https://gitlab.com/api/v4/projects/81189862/packages/pypi/simple granny-devops

For Vaultwarden (Locke) credential resolution, install separately:

pip install "locke @ git+https://gitlab.com/martin-wieser/locke.git#subdirectory=python"

Configure

Granny reads secrets from environment variables, falling back to a Vaultwarden vault when Locke is installed. Copy the example and fill in what you need:

cp .env.example .env

Common keys:

Provider Variables
Bunny BUNNY_API_KEY (+ BUNNY_API_KEY_<CUSTOMER> for multi-account)
Airtable AIRTABLE_API_KEY
Cloudflare CLOUDFLARE_API_TOKEN
Hetzner HETZNER_S3_ACCESS_KEY, HETZNER_S3_SECRET_KEY, HETZNER_DNS_API_TOKEN
Scaleway SCW_ACCESS_KEY, SCW_SECRET_KEY, SCW_DEFAULT_PROJECT_ID
Knative FaaS API OBACHAN_API_KEY, OBACHAN_API_BASE_URL
Mailjet MAILJET_API_KEY, MAILJET_SECRET_KEY
deSEC DESEC_API_TOKEN
ClouDNS CLOUDNS_AUTH_ID/_PASSWORD (or _SUB_AUTH_ID/_SUB_AUTH_USER)
INWX INWX_USERNAME, INWX_PASSWORD, INWX_SHARED_SECRET (only with 2FA)
Docker Hub DOCKER_HUB_USER, DOCKER_HUB_TOKEN
Elasticsearch / Kibana ELASTICSEARCH_URL, ELASTICSEARCH_API_KEY or ELASTICSEARCH_USERNAME + ELASTICSEARCH_PASSWORD

Set only the ones you need. Use granny credentials status to verify what's configured at any time.

Use

granny --help                          # see every command group
granny <group> --help                  # drill into one

The CLI is organized by capability, not by provider. You pick the provider per command:

# DNS — same command, any of seven providers
granny dns list example.com --provider cloudflare
granny dns add www.example.com --type CNAME --value example.com --provider bunny
granny dns nameservers example.com --provider inwx

# CDN — Bunny pull zones
granny cdn list-zones
granny cdn purge 12345
granny cdn ssl www.example.com --dns01

# Object storage — three providers, one verb set
granny storage bunny create my-assets --region DE
granny storage hetzner create my-bucket --region fsn1 --public
granny storage aws create my-website --website

# AWS inventory (read-only)
granny analyze vpcs --json-output
granny analyze lambdas --region us-east-1 eu-west-1

# AWS Bedrock -- foundation models (read-only; on-demand access auto-enables on first invoke)
granny aws bedrock list-models --profile lularge --region eu-central-1 --by-provider Anthropic
granny aws bedrock check-access --model-id anthropic.claude-sonnet-4-20250514-v1:0 --profile lularge --region eu-central-1

# Cross-cloud GPU / credit / cost inventory (AWS + GCP + Azure)
granny analyze gpus                                # running GPUs everywhere
granny analyze gpus --filter h100,h200             # find Hopper clusters
granny analyze gpus --include-reserved             # also capacity blocks / RIs
granny analyze gpus --provider aws --profile prod --profile dev
granny analyze credits                             # available balances
granny analyze costs                               # MTD + month-end forecast

# AWS Capacity Blocks for ML -- discover available H100/H200/A100 blocks
granny analyze capacity-blocks --instance-type p5.48xlarge --hours 24
granny analyze capacity-blocks --instance-type p5e.48xlarge --count 2 --hours 168

# Kubernetes service architecture inventory via kubectl
# Maps Services to Ingress hosts, EndpointSlices/Endpoints, and owning workloads.
# Refuses writable Kubernetes identities by default; use a read-only kubeconfig.
granny analyze k8s-services --context prod-cluster --namespace platform
granny analyze k8s-services --context prod-cluster --json-output
granny analyze k8s-services --context prod-cluster --mermaid > architecture.mmd

# Cloudflare account resources (Workers, D1, R2, KV)
granny cloudflare d1 create my-app
granny cloudflare r2 create my-app-media
granny cloudflare site provision my-app --secret-from-vault MAILJET_API_KEY

# Airtable endpoint maintenance
granny airtable bases
granny airtable tables --base app123
granny airtable find-text --base app123 --table Config --contains old.example.com
granny airtable replace-text --base app123 --table Config \
  --field Endpoint --old old.example.com --new new.example.com       # dry run
granny airtable replace-text --base app123 --table Config \
  --field Endpoint --old old.example.com --new new.example.com --apply --confirm

# Multi-arch Docker builds with deterministic tags
granny docker build-base --image myapp-base --hash-file requirements.txt

# Scaleway FaaS
granny serverless deploy my-fn --source-dir ./dist --namespace my-app
granny serverless delete-function my-fn --namespace my-app --yes

# Knative FaaS control API (`granny faas` is an alias)
granny knative list
granny knative deploy my-fn --image registry.example.com/my-fn:latest \
  --env LOG_LEVEL=info --min-scale 0 --max-scale 5
granny knative scale my-fn --max-scale 10
granny knative logs my-fn --tail 100
granny knative invoke my-fn --method POST --path /process --body '{"job": 42}'
granny knative remove my-fn --yes

# Mailjet + WorkMail
granny email mailjet setup-dns example.com
granny email workmail create-user example.com --email user@example.com

# One-shot infrastructure provisioning (dispatches granny/create/*.py)
granny create s3-website example.com --help
granny create scaleway-container --name my-app --port 3000
granny create mailjet-dns example.com
granny create letsencrypt-cert \
  --domain '*.example.com' --domain example.com \
  --dns inwx --zone example.com \
  --email ops@example.com --out-dir ./certs   # real LE cert via ACME dns-01, any granny DNS provider

# Authentik admin (provider + application + group plumbing)
granny authentik provision-oauth-app my-app \
  --name "My App" \
  --redirect-uri https://app.example.com/auth/callback \
  --launch-url https://app.example.com \
  --group my-app-admins
granny authentik list providers
granny authentik rotate-secret my-oauth-provider
granny authentik delete-app my-app --name "My App"     # name-confirmed application delete
granny authentik add-user-to-group user@example.com     # defaults to dash_admins
granny authentik api GET /api/v3/core/users/me/        # generic escape hatch

# Zitadel admin (self-hosted OIDC server — same shape as authentik)
#   ZITADEL_DOMAIN  (e.g. https://id.example.com) — plain config, NOT a secret;
#     set it via env or .env/.deploy.env (granny auto-loads those). Not vaulted.
#   Plus one machine credential:
#     ZITADEL_API_TOKEN  (a PAT, used as a bearer), OR
#     ZITADEL_SA_KEY / ZITADEL_SA_KEY_FILE  (service-account key JSON; JWT-profile auth)
#   Secret vault keys (with the [vault] extra): granny/infra/zitadel-api-token, -sa-key
#   Mint a PAT reproducibly:  granny zitadel create-pat iam-admin --expiration 2027-06-27T00:00:00Z
granny zitadel create-project platform
granny zitadel create-oidc-app platform my-app \
  --redirect-uri https://app.example.com/oidc/callback
granny zitadel set-redirect-uris --project platform --app my-app \
  --redirect-uri https://app.example.com/oidc/callback \
  --redirect-uri https://app.example.com/oidc/callback2   # merges by default; --replace to set exactly
granny zitadel rotate-secret platform my-app
granny zitadel create-user alice --email alice@example.com --password 's3cret'
granny zitadel ensure-role platform admin                 # Zitadel uses project roles, not groups
granny zitadel grant-user-role platform alice admin       # ...granted to users
granny zitadel enforce-mfa                                 # forceMfa + passkey + TOTP
granny zitadel list users                                  # also: projects | apps --project p | roles --project p

# Elasticsearch / Kibana users
granny elk add-user user@example.com \
  --email user@example.com \
  --full-name "Example User" \
  --role kibana_admin \
  --generate-password

# Azure -- subscriptions, ARM deployments, OpenAI, App Service, VM sizes
granny azure account whoami                                  # signed-in identity
granny azure account list                                    # subscriptions
granny azure deployment list --subscription <id> --all-groups
granny azure group create --subscription <id> --name <rg> --location swedencentral
granny azure openai accounts --subscription <id>
granny azure openai account create --subscription <id> --resource-group <rg> \
    --name <acct> --location swedencentral                   # create an Azure OpenAI account
granny azure openai keys --subscription <id> --resource-group <rg> --account <acct>
granny azure openai deployments --subscription <id> --kind OpenAI --with-usage  # + created_at & last-used (Azure Monitor)
granny azure openai deployment create --subscription <id> --resource-group <rg> \
    --account <acct> --name gpt-5.4 --model gpt-5.4 --model-version 2026-03-05 \
    --sku GlobalStandard --capacity 50                       # create a model deployment
granny azure webapp list --subscription <id>
granny azure vm-sizes list --subscription <id> --location polandcentral
granny azure vm list --subscription <id>                      # running VMs + power state
granny azure vm deallocate --subscription <id> --resource-group <rg> --name <vm>
granny azure vm delete --subscription <id> --resource-group <rg> --name <vm> --dry-run
granny azure role list --subscription <id> --principal-id <oid>
granny azure role assign --subscription <id> --role-name Reader --dry-run

# Search-engine indexing (IndexNow + Google Indexing API)
granny indexing generate-key                                 # mint an IndexNow key
granny indexing submit https://example.com/new-page          # notify every engine
granny indexing indexnow https://example.com/p1 https://example.com/p2
granny indexing google https://example.com/job-posting --action URL_UPDATED

Capability matrix

Capability Providers
DNS Cloudflare, Bunny, Hetzner, deSEC, ClouDNS, INWX, manual
CDN Bunny
Edge scripting Bunny
Object storage AWS S3, Bunny Storage, Hetzner S3
Serverless functions Scaleway FaaS, Scaleway Containers, Knative FaaS control API
Workers / KV / D1 / R2 Cloudflare
Record search and endpoint maintenance Airtable
Email send infra Mailjet, AWS SES, AWS WorkMail
AWS inventory VPCs, Lambdas
Cross-cloud inventory GPU instances + reservations, credit balances, MTD spend + forecast (AWS, GCP, Azure)
SSL automation Bunny, Cloudflare, ACM, Let's Encrypt (ACME dns-01 over any granny DNS provider)
SSO / IdP Authentik (provider, application, group, and user operations)
Observability admin Elasticsearch / Kibana native-user management
Azure ops Identity, subscriptions, ARM deployments, Cognitive Services (Azure OpenAI), App Service, VM sizes, VM inventory/deallocate/delete, RBAC role assignment, compute quota
Search indexing IndexNow (Bing/Yandex/Seznam/Naver/Yep), Google Indexing API

As a library

Every CLI command is a thin wrapper. Import the underlying module when the CLI doesn't have what you need:

from granny.dns.factory import get_provider
from granny.cloudflare.d1 import D1Client
from granny.credentials import get_secret, load_secrets_into_env

# Multi-provider DNS
dns = get_provider("inwx")
zone = dns.get_zone_id("example.com")
dns.upsert_record(zone, "_acme-challenge", "TXT", "abc-token", ttl=300)

# Cloudflare D1 directly
db = D1Client().create_database("my-app", primary_location_hint="weur")

# Bulk-load registered secrets into os.environ for downstream tools
load_secrets_into_env()

Project layout

granny/
  cli/           Click command groups (granny <group> <verb>)
  airtable/     Airtable record search and endpoint maintenance
  analyze/      Cross-cloud inventory (AWS, GCP, Azure)
  authentik/    Authentik admin
  azure/        Azure account, ARM deployments, Cognitive, App Service, VMs, RBAC roles, quota
  cdn/          Bunny CDN
  cloudflare/   Cloudflare Workers / D1 / R2 / KV
  create/       Standalone setup scripts (granny create <name>)
  credentials/  Env + vault secret resolution
  dns/          Provider-agnostic DNS CRUD
  docker/       Multi-arch image builds
  edge/         Bunny Edge Scripting
  elk/          Elasticsearch / Kibana security user management
  email/        Mailjet, WorkMail, SES forwarding
  indexing/     IndexNow + Google Indexing API
  knative/      Knative FaaS control API client
  serverless/   Scaleway FaaS
  storage/      Object storage (AWS / Bunny / Hetzner)

Where to look next

  • CHANGELOG.md — release history and notable changes.
  • Project_Guidelines.md — contributor toolchain, conventions, extension checklists, release flow.
  • AGENTS.md — instructions for AI coding agents working in the repository (Claude Code reads CLAUDE.md which imports this).
  • .env.example — full list of supported environment variables.
  • granny --help and granny <group> --help — authoritative command docs, always in sync with the installed version.

License

MIT — see LICENSE. Built and maintained by Martin Wieser.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

granny_devops-0.18.0.tar.gz (291.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

granny_devops-0.18.0-py3-none-any.whl (351.9 kB view details)

Uploaded Python 3

File details

Details for the file granny_devops-0.18.0.tar.gz.

File metadata

  • Download URL: granny_devops-0.18.0.tar.gz
  • Upload date:
  • Size: 291.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.14

File hashes

Hashes for granny_devops-0.18.0.tar.gz
Algorithm Hash digest
SHA256 d6d475ac9a0f0135150504f4f5a7861a8e3a431a6f2c3b8bdac233bee81dde7d
MD5 f5d4d45f4edcb90acfdd6910d93cc64d
BLAKE2b-256 93460a23f973f562826cb93b8a30d7a5a0202586736b450ec1f5ffc70555bff9

See more details on using hashes here.

File details

Details for the file granny_devops-0.18.0-py3-none-any.whl.

File metadata

  • Download URL: granny_devops-0.18.0-py3-none-any.whl
  • Upload date:
  • Size: 351.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.14

File hashes

Hashes for granny_devops-0.18.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c0c49bc63bebda62c50d7a46a9897e256fede6753d0d1bf28db2797f0139cae3
MD5 e2a9bcf9270ca4f83cbaf18dd5fa1075
BLAKE2b-256 a814544a66c6af2fb87c360e5fa2d0e9f3820fabf68442d60e6b02e76b60051d

See more details on using hashes here.

Release history Release notifications | RSS feed

0.19.3

2 files

0.19.2

2 files

0.19.1

2 files

0.19.0

2 files

0.18.1

2 files

This release

0.18.0 This release

2 files

0.17.4

2 files

0.17.3

2 files

0.17.2

2 files

0.17.1

2 files

0.17.0

2 files

0.16.0

2 files

0.15.0

2 files

0.14.0

2 files

0.13.0

2 files

0.12.1

2 files

0.12.0

2 files

0.11.0

2 files

0.10.0

2 files

0.9.3

2 files

0.9.2

2 files

0.9.1

2 files

0.9.0

2 files

0.8.0

2 files

0.7.0

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page