granny-devops 0.17.2__tar.gz → 0.17.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. {granny_devops-0.17.2 → granny_devops-0.17.3}/PKG-INFO +1 -1
  2. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/__init__.py +1 -1
  3. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cli/dns.py +21 -0
  4. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/dns/base.py +12 -0
  5. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/dns/cloudflare.py +60 -0
  6. {granny_devops-0.17.2 → granny_devops-0.17.3}/pyproject.toml +1 -1
  7. {granny_devops-0.17.2 → granny_devops-0.17.3}/.gitignore +0 -0
  8. {granny_devops-0.17.2 → granny_devops-0.17.3}/LICENSE +0 -0
  9. {granny_devops-0.17.2 → granny_devops-0.17.3}/README.md +0 -0
  10. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/analyze/__init__.py +0 -0
  11. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/analyze/costs.py +0 -0
  12. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/analyze/credits.py +0 -0
  13. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/analyze/gpu_pricing.py +0 -0
  14. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/analyze/gpus.py +0 -0
  15. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/analyze/kubernetes.py +0 -0
  16. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/analyze/lambdas.py +0 -0
  17. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/analyze/vpcs.py +0 -0
  18. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/authentik/__init__.py +0 -0
  19. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/authentik/client.py +0 -0
  20. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/authentik/provision.py +0 -0
  21. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/aws/__init__.py +0 -0
  22. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/aws/bedrock.py +0 -0
  23. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/aws/quota.py +0 -0
  24. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/azure/__init__.py +0 -0
  25. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/azure/_client.py +0 -0
  26. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/azure/account.py +0 -0
  27. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/azure/deployment.py +0 -0
  28. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/azure/openai.py +0 -0
  29. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/azure/quota.py +0 -0
  30. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/azure/resourcegroup.py +0 -0
  31. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/azure/role.py +0 -0
  32. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/azure/sku.py +0 -0
  33. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/azure/vm.py +0 -0
  34. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/azure/webapp.py +0 -0
  35. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cdn/__init__.py +0 -0
  36. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cdn/bunny.py +0 -0
  37. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cli/__init__.py +0 -0
  38. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cli/analyze.py +0 -0
  39. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cli/authentik.py +0 -0
  40. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cli/aws.py +0 -0
  41. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cli/azure.py +0 -0
  42. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cli/cdn.py +0 -0
  43. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cli/cloudflare.py +0 -0
  44. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cli/create.py +0 -0
  45. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cli/credentials.py +0 -0
  46. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cli/docker.py +0 -0
  47. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cli/edge.py +0 -0
  48. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cli/elk.py +0 -0
  49. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cli/email.py +0 -0
  50. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cli/indexing.py +0 -0
  51. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cli/main.py +0 -0
  52. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cli/oauth.py +0 -0
  53. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cli/serverless.py +0 -0
  54. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cli/storage.py +0 -0
  55. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cli/zitadel.py +0 -0
  56. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cloudflare/__init__.py +0 -0
  57. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cloudflare/d1.py +0 -0
  58. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cloudflare/r2.py +0 -0
  59. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/cloudflare/workers.py +0 -0
  60. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/create/__init__.py +0 -0
  61. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/create/auto_certificate.py +0 -0
  62. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/create/cloudfront-security-headers.js +0 -0
  63. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/create/letsencrypt_cert.py +0 -0
  64. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/create/manage-dns.sh +0 -0
  65. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/create/manage_mailjet_contacts.py +0 -0
  66. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/create/registrars.py +0 -0
  67. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/create/setup_aws_cloudfront.py +0 -0
  68. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/create/setup_bunny_edge_script.py +0 -0
  69. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/create/setup_bunny_storage.py +0 -0
  70. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/create/setup_cognito_identity_pool.py +0 -0
  71. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/create/setup_hetzner_bunny.py +0 -0
  72. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/create/setup_mailjet_dns.py +0 -0
  73. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/create/setup_private_cdn.py +0 -0
  74. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/create/setup_s3_website.py +0 -0
  75. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/create/setup_scaleway_container.py +0 -0
  76. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/create/setup_scaleway_faas.py +0 -0
  77. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/create/setup_workmail.py +0 -0
  78. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/create/www-redirect-function.js +0 -0
  79. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/credentials/__init__.py +0 -0
  80. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/credentials/secrets.py +0 -0
  81. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/dns/__init__.py +0 -0
  82. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/dns/bunny.py +0 -0
  83. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/dns/cloudns.py +0 -0
  84. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/dns/desec.py +0 -0
  85. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/dns/factory.py +0 -0
  86. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/dns/hetzner.py +0 -0
  87. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/dns/inwx.py +0 -0
  88. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/dns/manual.py +0 -0
  89. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/dns/records.py +0 -0
  90. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/docker/__init__.py +0 -0
  91. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/docker/build_base.py +0 -0
  92. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/edge/__init__.py +0 -0
  93. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/edge/bunny.py +0 -0
  94. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/elk/__init__.py +0 -0
  95. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/elk/client.py +0 -0
  96. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/email/__init__.py +0 -0
  97. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/email/mailjet.py +0 -0
  98. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/email/mailjet_contacts.py +0 -0
  99. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/email/ses_forwarding.py +0 -0
  100. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/email/stalwart.py +0 -0
  101. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/email/workmail.py +0 -0
  102. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/indexing/__init__.py +0 -0
  103. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/indexing/google.py +0 -0
  104. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/indexing/indexnow.py +0 -0
  105. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/report.py +0 -0
  106. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/serverless/__init__.py +0 -0
  107. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/serverless/scaleway.py +0 -0
  108. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/storage/__init__.py +0 -0
  109. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/storage/aws.py +0 -0
  110. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/storage/bunny.py +0 -0
  111. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/storage/hetzner.py +0 -0
  112. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/zitadel/__init__.py +0 -0
  113. {granny_devops-0.17.2 → granny_devops-0.17.3}/granny/zitadel/client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: granny-devops
3
- Version: 0.17.2
3
+ Version: 0.17.3
4
4
  Summary: Cloud tools collection -- AWS infrastructure, CDN, and DevOps automation
5
5
  Author-email: Martin Wieser <martin.wieser@pseekoo.com>
6
6
  License: MIT License
@@ -1,6 +1,6 @@
1
1
  """Granny -- Cloud tools collection for AWS infrastructure and DevOps automation."""
2
2
 
3
- __version__ = "0.17.2"
3
+ __version__ = "0.17.3"
4
4
  __all__ = [
5
5
  "get_secret",
6
6
  "load_secrets_into_env",
@@ -145,6 +145,27 @@ def dns() -> None:
145
145
  """Provider-agnostic DNS record management."""
146
146
 
147
147
 
148
+ @dns.command("create-zone")
149
+ @click.argument("zone")
150
+ @provider_option
151
+ def create_zone_cmd(zone: str, provider: str | None) -> None:
152
+ """Create zone ZONE at the provider (idempotent).
153
+
154
+ Prints the assigned nameservers — point the registrar's NS records at
155
+ them to activate the zone. Cloudflare requires a token with the
156
+ account-level ``Zone: Create`` permission.
157
+ """
158
+ zone = zone.strip().rstrip(".").lower()
159
+ if not zone or "." not in zone:
160
+ raise click.UsageError(f"ZONE must be a registrable domain, got {zone!r}")
161
+ dns_provider = _provider_instance(provider)
162
+ try:
163
+ meta = dns_provider.create_zone(zone)
164
+ except NotImplementedError as exc:
165
+ raise click.ClickException(str(exc))
166
+ click.echo(json.dumps(meta, indent=2))
167
+
168
+
148
169
  @dns.command("list")
149
170
  @provider_option
150
171
  @domain_option
@@ -33,6 +33,18 @@ class DNSProvider(ABC):
33
33
  f"{type(self).__name__} does not expose nameservers via API"
34
34
  )
35
35
 
36
+ def create_zone(self, zone_name: str) -> dict:
37
+ """Create ``zone_name`` at the provider (idempotent where supported).
38
+
39
+ Returns provider metadata for the zone — at minimum ``id``, ``name``,
40
+ ``status``, and ``nameservers`` (the pair the registrar must delegate
41
+ to). Providers whose API has no zone-creation endpoint (or where zones
42
+ are registrar-managed) keep this default.
43
+ """
44
+ raise NotImplementedError(
45
+ f"{type(self).__name__} does not support zone creation via API"
46
+ )
47
+
36
48
  @abstractmethod
37
49
  def list_records(
38
50
  self,
@@ -71,6 +71,66 @@ class CloudflareDNSProvider(DNSProvider):
71
71
  return []
72
72
  return list(result[0].get("name_servers") or [])
73
73
 
74
+ def _account_id(self) -> str:
75
+ """Resolve the account id the token operates on.
76
+
77
+ ``GET /accounts`` needs the Account Settings read scope, which
78
+ zone-scoped tokens usually lack — fall back to the ``account`` stamped
79
+ on any zone the token can already see.
80
+ """
81
+ try:
82
+ body = self._request("GET", "/accounts", params={"per_page": 1})
83
+ result = body.get("result") or []
84
+ if result:
85
+ return result[0]["id"]
86
+ except RuntimeError:
87
+ pass
88
+ body = self._request("GET", "/zones", params={"per_page": 1})
89
+ result = body.get("result") or []
90
+ if not result:
91
+ raise RuntimeError(
92
+ "Cloudflare: cannot resolve account id — token sees no "
93
+ "accounts and no zones"
94
+ )
95
+ return result[0]["account"]["id"]
96
+
97
+ @staticmethod
98
+ def _zone_meta(zone: dict) -> dict:
99
+ return {
100
+ "id": zone.get("id"),
101
+ "name": zone.get("name"),
102
+ "status": zone.get("status"),
103
+ "nameservers": list(zone.get("name_servers") or []),
104
+ }
105
+
106
+ def create_zone(self, zone_name: str) -> dict:
107
+ """Create ``zone_name`` in the token's account (idempotent).
108
+
109
+ Requires the account-level ``Zone: Create`` token permission; without
110
+ it Cloudflare answers 403 ``com.cloudflare.api.account.zone.create``.
111
+ If the zone already exists in this account, returns it unchanged.
112
+ """
113
+ existing = self._request("GET", "/zones", params={"name": zone_name})
114
+ result = existing.get("result") or []
115
+ if result:
116
+ logger.info("Cloudflare: zone %s already exists", zone_name)
117
+ return self._zone_meta(result[0])
118
+ body = self._request(
119
+ "POST",
120
+ "/zones",
121
+ json={
122
+ "name": zone_name,
123
+ "account": {"id": self._account_id()},
124
+ "type": "full",
125
+ },
126
+ )
127
+ zone = body.get("result") or {}
128
+ self._zone_cache[zone_name] = zone.get("id", "")
129
+ logger.info(
130
+ "Cloudflare: created zone %s (status %s)", zone_name, zone.get("status")
131
+ )
132
+ return self._zone_meta(zone)
133
+
74
134
  def _fqdn(self, zone_id: str, name: str) -> str:
75
135
  # Cloudflare's record API uses full FQDN for `name`. The caller passes
76
136
  # us a short name relative to the zone, so we need the zone name to
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "granny-devops"
3
- version = "0.17.2"
3
+ version = "0.17.3"
4
4
  description = "Cloud tools collection -- AWS infrastructure, CDN, and DevOps automation"
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
File without changes
File without changes