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
Obachan OpenBao OBACHAN_BAO_ADDR, OBACHAN_BAO_TOKEN
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

# Provision mail/calendar OAuth clients to a dashboard env and Obachan OpenBao
granny oauth provision-productivity --env-file ../dashboard/.deploy.env \
  --obachan-env staging --obachan-app dashboard \
  --google-client-json client_secret.json \
  --azure-client-id "$AZURE_CLIENT_ID" --azure-client-secret "$AZURE_CLIENT_SECRET"

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
Productivity OAuth Google Cloud and Microsoft Entra mail/calendar client provisioning

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
  oauth/        Google/Microsoft productivity OAuth provisioning
  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.1.tar.gz (294.5 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.1-py3-none-any.whl (355.6 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for granny_devops-0.18.1.tar.gz
Algorithm Hash digest
SHA256 ca6258676352cbc6248bd2faf6a51a1d08519cabb04ed83f4e2f0f478002ad99
MD5 5f5481d31b60feb563bd8f56b148df01
BLAKE2b-256 62682d7835986c47458337fbb96d090a0a918868aa611483abae054ef4653e19

See more details on using hashes here.

File details

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

File metadata

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

File hashes

Hashes for granny_devops-0.18.1-py3-none-any.whl
Algorithm Hash digest
SHA256 13ad827b03bc1b39d330791287c07526e966fe6a82393ebeebe1006af371bdc1
MD5 ae8d1e3723d81e50794d12c1e80211be
BLAKE2b-256 39109d746c98d064849f90f76157bbb263dbc4bd8e72d4979c09ec3edbc74639

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

This release

0.18.1 This release

2 files

0.18.0

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