granny-devops 0.4.0__py3-none-any.whl

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 (68) hide show
  1. granny/__init__.py +19 -0
  2. granny/analyze/__init__.py +6 -0
  3. granny/analyze/lambdas.py +59 -0
  4. granny/analyze/vpcs.py +57 -0
  5. granny/cdn/__init__.py +9 -0
  6. granny/cdn/bunny.py +231 -0
  7. granny/cli/__init__.py +0 -0
  8. granny/cli/analyze.py +66 -0
  9. granny/cli/cdn.py +210 -0
  10. granny/cli/create.py +94 -0
  11. granny/cli/credentials.py +99 -0
  12. granny/cli/dns.py +290 -0
  13. granny/cli/docker.py +165 -0
  14. granny/cli/edge.py +106 -0
  15. granny/cli/email.py +224 -0
  16. granny/cli/main.py +98 -0
  17. granny/cli/serverless.py +278 -0
  18. granny/cli/storage.py +249 -0
  19. granny/create/__init__.py +4 -0
  20. granny/create/auto_certificate.py +1899 -0
  21. granny/create/cloudfront-security-headers.js +53 -0
  22. granny/create/manage-dns.sh +321 -0
  23. granny/create/manage_mailjet_contacts.py +619 -0
  24. granny/create/registrars.py +363 -0
  25. granny/create/setup_aws_cloudfront.py +2808 -0
  26. granny/create/setup_bunny_edge_script.py +923 -0
  27. granny/create/setup_bunny_storage.py +1719 -0
  28. granny/create/setup_cognito_identity_pool.py +740 -0
  29. granny/create/setup_hetzner_bunny.py +1482 -0
  30. granny/create/setup_mailjet_dns.py +1103 -0
  31. granny/create/setup_private_cdn.py +547 -0
  32. granny/create/setup_s3_website.py +1512 -0
  33. granny/create/setup_scaleway_faas.py +1165 -0
  34. granny/create/setup_workmail.py +1217 -0
  35. granny/create/www-redirect-function.js +17 -0
  36. granny/credentials/__init__.py +15 -0
  37. granny/credentials/secrets.py +403 -0
  38. granny/dns/__init__.py +22 -0
  39. granny/dns/base.py +113 -0
  40. granny/dns/bunny.py +150 -0
  41. granny/dns/cloudflare.py +192 -0
  42. granny/dns/cloudns.py +162 -0
  43. granny/dns/desec.py +152 -0
  44. granny/dns/factory.py +72 -0
  45. granny/dns/hetzner.py +165 -0
  46. granny/dns/manual.py +64 -0
  47. granny/dns/records.py +29 -0
  48. granny/docker/__init__.py +5 -0
  49. granny/docker/build_base.py +204 -0
  50. granny/edge/__init__.py +5 -0
  51. granny/edge/bunny.py +147 -0
  52. granny/email/__init__.py +7 -0
  53. granny/email/mailjet.py +119 -0
  54. granny/email/mailjet_contacts.py +115 -0
  55. granny/email/ses_forwarding.py +281 -0
  56. granny/email/workmail.py +145 -0
  57. granny/report.py +128 -0
  58. granny/serverless/__init__.py +5 -0
  59. granny/serverless/scaleway.py +264 -0
  60. granny/storage/__init__.py +7 -0
  61. granny/storage/aws.py +113 -0
  62. granny/storage/bunny.py +98 -0
  63. granny/storage/hetzner.py +118 -0
  64. granny_devops-0.4.0.dist-info/METADATA +445 -0
  65. granny_devops-0.4.0.dist-info/RECORD +68 -0
  66. granny_devops-0.4.0.dist-info/WHEEL +4 -0
  67. granny_devops-0.4.0.dist-info/entry_points.txt +2 -0
  68. granny_devops-0.4.0.dist-info/licenses/LICENSE +21 -0
granny/cli/storage.py ADDED
@@ -0,0 +1,249 @@
1
+ """``granny storage`` — object storage management across providers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+
8
+ @click.group()
9
+ def storage() -> None:
10
+ """Object storage management (Bunny, Hetzner S3, AWS S3)."""
11
+
12
+
13
+ # -- Bunny Storage subgroup ---------------------------------------------------
14
+
15
+
16
+ @storage.group("bunny")
17
+ def bunny_storage() -> None:
18
+ """Bunny Storage zone management."""
19
+
20
+
21
+ @bunny_storage.command("list")
22
+ @click.option("--customer", "-c", default=None, help="Bunny customer profile.")
23
+ def bunny_list(customer: str | None) -> None:
24
+ """List all Bunny storage zones."""
25
+ from granny.storage.bunny import BunnyStorageClient
26
+
27
+ c = BunnyStorageClient(customer=customer)
28
+ zones = c.list_storage_zones()
29
+ if not zones:
30
+ click.echo("No storage zones found.")
31
+ return
32
+ for z in zones:
33
+ click.echo(
34
+ f" {z['Id']:<10} {z['Name']:<30} "
35
+ f"Region={z.get('Region', '?')} "
36
+ f"Files={z.get('FilesStored', 0)}"
37
+ )
38
+
39
+
40
+ @bunny_storage.command("create")
41
+ @click.argument("name")
42
+ @click.option(
43
+ "--region",
44
+ default="DE",
45
+ show_default=True,
46
+ type=click.Choice(
47
+ ["DE", "UK", "NY", "LA", "SG", "SE", "BR", "JH", "SYD"],
48
+ case_sensitive=False,
49
+ ),
50
+ )
51
+ @click.option("--customer", "-c", default=None)
52
+ def bunny_create(name: str, region: str, customer: str | None) -> None:
53
+ """Create a Bunny storage zone."""
54
+ from granny.storage.bunny import BunnyStorageClient
55
+
56
+ c = BunnyStorageClient(customer=customer)
57
+ zone = c.create_storage_zone(name, region=region.upper())
58
+ click.echo(f"Storage zone created: ID={zone['Id']} Name={zone['Name']}")
59
+ click.echo(f"Password: {zone.get('Password', 'N/A')}")
60
+ click.echo(f"Hostname: {zone.get('StorageHostname', 'N/A')}")
61
+
62
+
63
+ @bunny_storage.command("upload")
64
+ @click.argument("zone_name")
65
+ @click.argument("local_path", type=click.Path(exists=True))
66
+ @click.option("--remote-path", default=None, help="Remote path (default: filename).")
67
+ @click.option("--password", envvar="BUNNY_STORAGE_PASSWORD", required=True,
68
+ help="Storage zone password.")
69
+ @click.option("--hostname", default="storage.bunnycdn.com", show_default=True,
70
+ help="Storage endpoint hostname.")
71
+ @click.option("--content-type", default=None, help="Override Content-Type.")
72
+ def bunny_upload(
73
+ zone_name: str,
74
+ local_path: str,
75
+ remote_path: str | None,
76
+ password: str,
77
+ hostname: str,
78
+ content_type: str | None,
79
+ ) -> None:
80
+ """Upload a single file to a Bunny storage zone."""
81
+ import os
82
+
83
+ from granny.storage.bunny import BunnyStorageClient
84
+
85
+ if remote_path is None:
86
+ remote_path = os.path.basename(local_path)
87
+
88
+ if content_type is None:
89
+ content_type = _guess_content_type(local_path)
90
+
91
+ c = BunnyStorageClient()
92
+ with open(local_path, "rb") as f:
93
+ c.upload_file(hostname, password, zone_name, remote_path, f.read(), content_type)
94
+ click.echo(f"Uploaded {local_path} -> {zone_name}/{remote_path}")
95
+
96
+
97
+ @bunny_storage.command("upload-dir")
98
+ @click.argument("zone_name")
99
+ @click.argument("directory", type=click.Path(exists=True, file_okay=False))
100
+ @click.option("--password", envvar="BUNNY_STORAGE_PASSWORD", required=True,
101
+ help="Storage zone password.")
102
+ @click.option("--hostname", default="storage.bunnycdn.com", show_default=True,
103
+ help="Storage endpoint hostname.")
104
+ @click.option("--cache-control", default="public, max-age=31536000, immutable",
105
+ show_default=True,
106
+ help="Cache-Control for non-HTML files (hashed assets).")
107
+ @click.option("--html-cache-control", default="no-cache, no-store, must-revalidate",
108
+ show_default=True,
109
+ help="Cache-Control for *.html (so SPA entry is never stale).")
110
+ def bunny_upload_dir(
111
+ zone_name: str,
112
+ directory: str,
113
+ password: str,
114
+ hostname: str,
115
+ cache_control: str,
116
+ html_cache_control: str,
117
+ ) -> None:
118
+ """Upload an entire directory to a Bunny storage zone.
119
+
120
+ Recursively uploads all files, preserving directory structure.
121
+ Content-Type is auto-detected from file extensions.
122
+ HTML files get ``--html-cache-control`` (default: no-cache) so SPA entry
123
+ reloads fetch the latest asset hashes; everything else gets ``--cache-control``
124
+ (default: immutable long-cache).
125
+ """
126
+ import os
127
+
128
+ from granny.storage.bunny import BunnyStorageClient
129
+
130
+ c = BunnyStorageClient()
131
+ uploaded = 0
132
+ errors = 0
133
+
134
+ for root, _dirs, files in os.walk(directory):
135
+ for fname in files:
136
+ local_path = os.path.join(root, fname)
137
+ remote_path = os.path.relpath(local_path, directory).replace("\\", "/")
138
+ ct = _guess_content_type(local_path)
139
+ cc = html_cache_control if fname.lower().endswith(".html") else cache_control
140
+
141
+ try:
142
+ with open(local_path, "rb") as f:
143
+ c.upload_file(
144
+ hostname, password, zone_name, remote_path,
145
+ f.read(), ct, cache_control=cc,
146
+ )
147
+ uploaded += 1
148
+ if uploaded % 10 == 0:
149
+ click.echo(f" Uploaded {uploaded} files...")
150
+ except Exception as e:
151
+ click.echo(f" ERROR: {remote_path}: {e}", err=True)
152
+ errors += 1
153
+
154
+ click.echo(f"Done: {uploaded} uploaded, {errors} errors.")
155
+ if errors:
156
+ raise SystemExit(1)
157
+
158
+
159
+ def _guess_content_type(path: str) -> str:
160
+ """Guess Content-Type from file extension."""
161
+ ext = path.rsplit(".", 1)[-1].lower() if "." in path else ""
162
+ return {
163
+ "html": "text/html; charset=utf-8",
164
+ "css": "text/css; charset=utf-8",
165
+ "js": "application/javascript; charset=utf-8",
166
+ "json": "application/json; charset=utf-8",
167
+ "png": "image/png",
168
+ "jpg": "image/jpeg",
169
+ "jpeg": "image/jpeg",
170
+ "gif": "image/gif",
171
+ "svg": "image/svg+xml",
172
+ "webp": "image/webp",
173
+ "ico": "image/x-icon",
174
+ "woff": "font/woff",
175
+ "woff2": "font/woff2",
176
+ "ttf": "font/ttf",
177
+ "eot": "application/vnd.ms-fontobject",
178
+ "xml": "application/xml",
179
+ "txt": "text/plain",
180
+ "map": "application/json",
181
+ }.get(ext, "application/octet-stream")
182
+
183
+
184
+ # -- Hetzner S3 subgroup ------------------------------------------------------
185
+
186
+
187
+ @storage.group("hetzner")
188
+ def hetzner_storage() -> None:
189
+ """Hetzner S3-compatible object storage."""
190
+
191
+
192
+ @hetzner_storage.command("create")
193
+ @click.argument("bucket_name")
194
+ @click.option(
195
+ "--region",
196
+ default="fsn1",
197
+ show_default=True,
198
+ type=click.Choice(["fsn1", "nbg1", "hel1"]),
199
+ )
200
+ @click.option("--public/--private", default=True, show_default=True)
201
+ @click.option("--cors", is_flag=True, help="Enable CORS for web access.")
202
+ def hetzner_create(
203
+ bucket_name: str, region: str, public: bool, cors: bool
204
+ ) -> None:
205
+ """Create a Hetzner S3 bucket."""
206
+ from granny.storage.hetzner import HetznerS3Client
207
+
208
+ c = HetznerS3Client(region=region)
209
+ c.create_bucket(bucket_name)
210
+ if public:
211
+ c.set_bucket_policy_public(bucket_name)
212
+ if cors:
213
+ c.set_cors_config(bucket_name)
214
+ click.echo(f"Bucket created: {c.get_bucket_url(bucket_name)}")
215
+
216
+
217
+ # -- AWS S3 subgroup -----------------------------------------------------------
218
+
219
+
220
+ @storage.group("aws")
221
+ def aws_storage() -> None:
222
+ """AWS S3 bucket management."""
223
+
224
+
225
+ @aws_storage.command("create")
226
+ @click.argument("bucket_name")
227
+ @click.option("--region", default=None, help="AWS region (default from profile).")
228
+ @click.option("--profile", "aws_profile", default=None, help="AWS profile name.")
229
+ @click.option(
230
+ "--website/--no-website",
231
+ default=False,
232
+ help="Enable static website hosting.",
233
+ )
234
+ def aws_create(
235
+ bucket_name: str,
236
+ region: str | None,
237
+ aws_profile: str | None,
238
+ website: bool,
239
+ ) -> None:
240
+ """Create an AWS S3 bucket."""
241
+ from granny.storage.aws import AWSS3Client
242
+
243
+ c = AWSS3Client(aws_profile=aws_profile, region=region)
244
+ c.create_bucket(bucket_name)
245
+ if website:
246
+ c.enable_website_hosting(bucket_name)
247
+ click.echo(f"Website endpoint: {c.get_website_endpoint(bucket_name)}")
248
+ else:
249
+ click.echo(f"Bucket created: {bucket_name} in {c.region}")
@@ -0,0 +1,4 @@
1
+ """Provisioning scripts invoked by `granny create <thing>`.
2
+
3
+ Each module exposes an argparse-based ``main()`` that the CLI dispatches to.
4
+ """