cloud-bridge-client 0.0.1__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.
@@ -0,0 +1,462 @@
1
+ Metadata-Version: 2.4
2
+ Name: cloud-bridge-client
3
+ Version: 0.0.1
4
+ Summary: Client for integration with Cloud Bridge
5
+ Author-email: Bohdan Kravets <kravetsbodj@email.com>, Artur Kochut <kochutworks@gmail.com>, Ihor Voskoboinikov <voskoboinikov777@gmail.com>, Vlad Bega <begavlad068@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/AuroraResourcing/cloud-bridge-client
8
+ Project-URL: Repository, https://github.com/AuroraResourcing/cloud-bridge-client
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.13
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: python-dotenv>=1.2.2
15
+ Requires-Dist: pyyaml>=6.0
16
+ Requires-Dist: requests>=2.28.0
17
+
18
+ # cloud-bridge-client
19
+
20
+ A lightweight, typed Python client for the [**Cloud Bridge**](https://github.com/AuroraResourcing/cloud-bridge) API — a multi-cloud infrastructure orchestration service.
21
+
22
+ Cloud Bridge lets you describe an entire cluster (networks, firewalls, nodes, volumes, NAT gateways, outputs, …) in a **single provider-agnostic YAML file** and deploy it to **AWS, GCP or OpenStack** through one uniform HTTP API. This library is the Python front-end to that API: you pass credentials and a config file, and you get back structured results and typed exceptions.
23
+
24
+ ```python
25
+ from cloud_bridge_client import AwsClient, AWSCredentials
26
+
27
+ client = AwsClient(
28
+ credentials=AWSCredentials(
29
+ access_key_id="AKIA...",
30
+ secret_access_key="...",
31
+ region="eu-central-1",
32
+ zone="eu-central-1a",
33
+ ),
34
+ )
35
+
36
+ client.preview(config="cluster.yaml") # dry-run
37
+ client.deploy(config="cluster.yaml") # provision infrastructure
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Table of contents
43
+
44
+ - [Features](#features)
45
+ - [Requirements](#requirements)
46
+ - [Installation](#installation)
47
+ - [Quick start](#quick-start)
48
+ - [Authentication & credentials](#authentication--credentials)
49
+ - [The cluster config file](#the-cluster-config-file)
50
+ - [Operations reference](#operations-reference)
51
+ - [Synchronous vs. asynchronous operations](#synchronous-vs-asynchronous-operations)
52
+ - [Destroy: the two-phase confirmation flow](#destroy-the-two-phase-confirmation-flow)
53
+ - [Error handling](#error-handling)
54
+ - [Full example](#full-example)
55
+ - [License](#license)
56
+
57
+ ---
58
+
59
+ ## Features
60
+
61
+ - **One client per provider** — `AwsClient`, `GcpClient`, `OpenStackClient` — with an identical method surface.
62
+ - **Provider-agnostic config** — the same `cluster.yaml` deploys to any supported cloud.
63
+ - **Typed credentials** — each provider has its own dataclass; no more juggling loose strings.
64
+ - **Rich exception hierarchy** — HTTP errors are mapped to specific, catchable exceptions (`AuthenticationError`, `FlavorError`, `ValidationError`, …) carrying the server's structured error payload.
65
+ - **Flexible config input** — pass a file path, raw YAML `bytes`, or a plain `dict`.
66
+ - **Two-phase destroy** — a built-in confirmation flow protects against accidental teardown.
67
+ - Zero-magic transport built on [`requests`](https://requests.readthedocs.io/); no async runtime required.
68
+
69
+ ## Requirements
70
+
71
+ - **Python 3.13+**
72
+ - A running **Cloud Bridge** server that the client can reach over HTTP.
73
+ - Valid cloud-provider credentials for whichever provider you target.
74
+
75
+ ## Installation
76
+
77
+ ```bash
78
+ pip install cloud-bridge-client
79
+ ```
80
+
81
+ Dependencies (`requests`, `pyyaml`, `python-dotenv`) are installed automatically.
82
+
83
+ ## Quick start
84
+
85
+ > **Server URL.** `base_url` is optional and defaults to `http://localhost:8080`. If your Cloud Bridge server lives elsewhere, pass `base_url=` to the provider client you are constructing (e.g. `AwsClient(credentials=..., base_url="http://my-server:8080")`). The `timeout` argument (seconds) is optional too and defaults to `600`.
86
+
87
+ ```python
88
+ from cloud_bridge_client import AwsClient, AWSCredentials
89
+
90
+ client = AwsClient(
91
+ credentials=AWSCredentials(
92
+ access_key_id="AKIA...",
93
+ secret_access_key="...",
94
+ region="eu-central-1",
95
+ zone="eu-central-1a",
96
+ ),
97
+ )
98
+
99
+ # Is the server reachable?
100
+ assert client.ping()
101
+
102
+ # What can this server provision?
103
+ print(client.providers()) # -> ["aws", "gcp", "openstack", ...]
104
+
105
+ # Dry-run first — see what *would* change, touching nothing.
106
+ print(client.preview(config="cluster.yaml"))
107
+
108
+ # Provision. Returns a job_id — the work runs asynchronously on the server.
109
+ result = client.deploy(config="cluster.yaml")
110
+ job_id = result["data"]["job_id"]
111
+
112
+ # Inspect a running/finished stack.
113
+ print(client.status(cluster_name="swarm-cluster"))
114
+ ```
115
+
116
+ ## Authentication & credentials
117
+
118
+ Every client is constructed with a **credentials object** for its provider. Under the hood the library encodes the credentials into a single `X-API-Key` header — you never build that header yourself.
119
+
120
+ Pick the client + credentials pair that matches your target cloud:
121
+
122
+ ### AWS
123
+
124
+ ```python
125
+ from cloud_bridge_client import AwsClient, AWSCredentials
126
+
127
+ client = AwsClient(
128
+ credentials=AWSCredentials(
129
+ access_key_id="AKIA...",
130
+ secret_access_key="...",
131
+ region="eu-central-1", # default: "eu-central-1"
132
+ zone="eu-central-1a", # default: ""
133
+ ),
134
+ )
135
+ ```
136
+
137
+ ### GCP
138
+
139
+ ```python
140
+ from cloud_bridge_client import GcpClient, GCPCredentials
141
+
142
+ client = GcpClient(
143
+ credentials=GCPCredentials(
144
+ project="my-project",
145
+ region="us-central1",
146
+ zone="us-central1-a",
147
+ service_account_email="sa@my-project.iam.gserviceaccount.com",
148
+ private_key="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
149
+ ),
150
+ )
151
+ ```
152
+
153
+ ### OpenStack
154
+
155
+ ```python
156
+ from cloud_bridge_client import OpenStackClient, OpenStackCredentials
157
+
158
+ client = OpenStackClient(
159
+ credentials=OpenStackCredentials(
160
+ auth_url="https://my-openstack:5000/v3",
161
+ application_credential_id="...",
162
+ application_credential_secret="...",
163
+ region_name="RegionOne", # default: "RegionOne"
164
+ ),
165
+ )
166
+ ```
167
+
168
+ > **Constructor parameters** (shared by every client):
169
+ > | Parameter | Type | Default | Meaning |
170
+ > |-----------|------|---------|---------|
171
+ > | `credentials` | provider credentials dataclass | *required* | Provider credentials. |
172
+ > | `base_url` | `str` | `http://localhost:8080` | Base URL of the Cloud Bridge server. |
173
+ > | `timeout` | `int` | `600` | Per-request timeout in seconds. |
174
+
175
+ ### Hot-swapping credentials
176
+
177
+ You can rotate credentials on a live client without recreating it:
178
+
179
+ ```python
180
+ client.update_credentials(AWSCredentials(access_key_id="AKIA-NEW...", secret_access_key="..."))
181
+ ```
182
+
183
+ ### Loading credentials from the environment
184
+
185
+ Credentials are plain dataclasses, so any config source works. A common pattern with `python-dotenv` (bundled as a dependency):
186
+
187
+ ```python
188
+ import os
189
+ from dotenv import load_dotenv
190
+ from cloud_bridge_client import AwsClient, AWSCredentials
191
+
192
+ load_dotenv()
193
+
194
+ client = AwsClient(
195
+ credentials=AWSCredentials(
196
+ access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
197
+ secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"],
198
+ region=os.environ["AWS_REGION"],
199
+ zone=os.environ["AWS_AVAILABILITY_ZONE"],
200
+ ),
201
+ )
202
+ ```
203
+
204
+ ## The cluster config file
205
+
206
+ `deploy` and `preview` take a **cluster config** describing the infrastructure you want. It is the same provider-agnostic YAML the Cloud Bridge server understands — networks, firewalls, NAT gateways, keypairs, volumes, nodes and outputs. The server resolves provider-specific details (image names, flavors, sources) from per-provider keys inside the file.
207
+
208
+ You can supply the config three ways:
209
+
210
+ ```python
211
+ # 1. A path to a file on disk (str or pathlib.Path) — must exist.
212
+ client.deploy(config="infra/cluster.yaml")
213
+
214
+ # 2. Raw YAML bytes already in memory.
215
+ client.deploy(config=b"name: swarm-cluster\nnodes: ...")
216
+
217
+ # 3. A plain dict — serialised to YAML for you.
218
+ client.deploy(config={"name": "swarm-cluster", "nodes": [...]})
219
+ ```
220
+
221
+ A minimal shape looks like:
222
+
223
+ ```yaml
224
+ name: swarm-cluster
225
+ description: Docker Swarm cluster on Fedora CoreOS
226
+
227
+ networks:
228
+ - create: true
229
+ name: swarm-internal-net
230
+ cidr: "10.10.0.0/24"
231
+ external_network: public
232
+
233
+ firewalls:
234
+ - name: swarm-cluster-sg
235
+ ingress:
236
+ - protocol: tcp
237
+ port: 22
238
+ cidr: "0.0.0.0/0"
239
+
240
+ nodes:
241
+ - name: swarm-manager
242
+ flavor:
243
+ name: t3.small
244
+ vcpus: 1
245
+ ram_gb: 3
246
+ architecture: x86_64
247
+ image:
248
+ name:
249
+ aws: fedora-coreos-44...x86_64
250
+ gcp: fedora-coreos-stable
251
+ openstack: Fedora-CoreOS-stable
252
+ user_data_format: ignition
253
+ public_ip: true
254
+
255
+ outputs:
256
+ - name: manager_public_ip
257
+ from: swarm-manager.public_ip
258
+ ```
259
+
260
+ > The config schema is owned and validated by the **Cloud Bridge server**, not by this client. See the server documentation (and the `universal_cluster.yaml` example shipped in this repository) for the full set of supported keys.
261
+
262
+ ### Automatic flavor selection
263
+
264
+ If your config does not pin a flavor for a node, pass `auto_select=True` and the server will pick one that satisfies the requested `vcpus` / `ram_gb` / `architecture`:
265
+
266
+ ```python
267
+ client.preview(config="cluster.yaml", auto_select=True)
268
+ client.deploy(config="cluster.yaml", auto_select=True)
269
+ ```
270
+
271
+ ## Operations reference
272
+
273
+ Every client exposes the same methods:
274
+
275
+ | Method | Kind | Description |
276
+ |--------|------|-------------|
277
+ | `ping()` | sync | Returns `True` if the server is reachable (never raises). |
278
+ | `providers()` | sync | Returns a `list[str]` of enabled provider identifiers. |
279
+ | `preview(config, *, auto_select=False, request_id=None)` | sync | Dry-run of a deploy — shows planned changes without touching real infra. |
280
+ | `deploy(config, *, auto_select=False, request_id=None)` | **async job** | Provisions the stack. Returns a `job_id`. |
281
+ | `status(cluster_name)` | sync | Current outputs + last-update summary for a stack. |
282
+ | `volumes(cluster_name)` | sync | What will happen to each volume when the stack is destroyed. |
283
+ | `preview_destroy(cluster_name)` | sync | Dry-run of a destroy — lists resources that *would* be deleted. |
284
+ | `destroy(cluster_name, *, ...)` | **async job** | Tears down a stack. Two-phase (see below). |
285
+ | `cancel(cluster_name)` | sync | Cancels an in-progress update / clears a stale lock. |
286
+ | `update_credentials(credentials)` | — | Hot-swaps credentials on the live client. |
287
+
288
+ ### Method signatures
289
+
290
+ ```python
291
+ def deploy(self, config, *, auto_select=False, request_id=None) -> Any
292
+ def preview(self, config, *, auto_select=False, request_id=None) -> Any
293
+ def preview_destroy(self, cluster_name: str) -> Any
294
+ def cancel(self, cluster_name: str) -> Any
295
+ def destroy(self, cluster_name, *, stateless_resources=False,
296
+ stateful_resources=False, config_file=False,
297
+ confirm_code=None, request_id=None) -> Any
298
+ def providers(self) -> list[str]
299
+ def volumes(self, cluster_name: str) -> Any
300
+ def status(self, cluster_name: str) -> Any
301
+ def ping(self) -> bool
302
+ def update_credentials(self, credentials) -> None
303
+ ```
304
+
305
+ Most methods return the server's parsed JSON envelope, shaped like:
306
+
307
+ ```json
308
+ { "success": true, "data": { ... } }
309
+ ```
310
+
311
+ `providers()` and `ping()` are the exceptions — they unwrap the useful value for you (a list and a bool respectively).
312
+
313
+ The optional `request_id` (a UUID v4) is echoed back through the server for request tracing; if you omit it, the server generates one.
314
+
315
+ ## Synchronous vs. asynchronous operations
316
+
317
+ The Cloud Bridge server runs long-lived infrastructure work on a background queue. This has a concrete consequence for the client:
318
+
319
+ - **`deploy` and `destroy` are asynchronous.** They return quickly with a `job_id`; the actual provisioning/teardown continues on the server afterwards.
320
+
321
+ ```python
322
+ result = client.deploy(config="cluster.yaml")
323
+ job_id = result["data"]["job_id"]
324
+ # -> {"data": {"job_id": "...", "operation": "deploy",
325
+ # "provider": "aws", "cluster_name": "swarm-cluster", "flavor": ...}}
326
+ ```
327
+
328
+ - **Everything else is synchronous** — `preview`, `preview_destroy`, `status`, `volumes`, `providers`, `cancel` all complete within the single HTTP call.
329
+
330
+ To follow the progress of a deploy/destroy job, poll `status(cluster_name=...)`. Because a deploy can take a long time, the default `timeout` is a generous **600 seconds** — tune it to your environment.
331
+
332
+ ## Destroy: the two-phase confirmation flow
333
+
334
+ Because destroying infrastructure is irreversible, `destroy()` uses a two-step handshake.
335
+
336
+ **Phase 1 — request confirmation.** Call with at least one of `stateless_resources`, `stateful_resources`, or `config_file` set to `True`. Nothing is deleted yet; the server returns a short-lived confirmation `code` and a warning describing exactly what will be removed.
337
+
338
+ ```python
339
+ resp = client.destroy(
340
+ cluster_name="swarm-cluster",
341
+ stateless_resources=True, # compute, networks, ...
342
+ config_file=True, # also remove the Pulumi config file
343
+ )
344
+ data = resp["data"]
345
+ print(data["warning"]) # human-readable warning
346
+ code = data["code"] # e.g. "a1b2c3d4" — expires after a short TTL
347
+ print(data["volumes"]) # volumes that would be affected
348
+ ```
349
+
350
+ **Phase 2 — confirm.** Call again with `confirm_code` set to the code from phase 1. This enqueues the actual teardown and returns a `job_id`.
351
+
352
+ ```python
353
+ resp = client.destroy(cluster_name="swarm-cluster", confirm_code=code)
354
+ job_id = resp["data"]["job_id"]
355
+ ```
356
+
357
+ Flag meanings:
358
+
359
+ | Flag | Deletes |
360
+ |------|---------|
361
+ | `stateless_resources=True` | Compute, networks, and other stateless resources. |
362
+ | `stateful_resources=True` | Volumes, databases (**requires** `stateless_resources=True`). |
363
+ | `config_file=True` | The server-side Pulumi config file for the stack. |
364
+
365
+ > The confirmation `code` expires. If it lapses, phase 2 fails and you must repeat phase 1.
366
+
367
+ ## Error handling
368
+
369
+ All exceptions inherit from `CloudBridgeClientError`, so you can catch broadly or narrowly. Non-2xx HTTP responses are mapped to specific subclasses of `ApiError`, each carrying the server's `status_code`, `message`, and structured `data` payload.
370
+
371
+ ```python
372
+ from cloud_bridge_client import (
373
+ AuthenticationError,
374
+ FlavorError,
375
+ ValidationError,
376
+ StackOperationError,
377
+ ServerConnectionError,
378
+ ApiError,
379
+ CloudBridgeClientError,
380
+ )
381
+
382
+ try:
383
+ client.deploy(config="cluster.yaml", auto_select=True)
384
+ except FlavorError as e:
385
+ # 400 — the requested flavor is invalid; server returns candidates in e.data
386
+ print("Available flavors per node:", e.data)
387
+ except ValidationError as e:
388
+ # 422 — payload rejected; e.data is {field: [messages]}
389
+ print("Validation failed:", e.data)
390
+ except AuthenticationError:
391
+ # 401 / 403 — bad credentials
392
+ print("Check your credentials.")
393
+ except ServerConnectionError:
394
+ # Could not reach the Cloud Bridge server at all
395
+ print("Server unreachable.")
396
+ except ApiError as e:
397
+ # Any other non-2xx response
398
+ print(f"HTTP {e.status_code}: {e.message}")
399
+ ```
400
+
401
+ ### Exception hierarchy
402
+
403
+ ```
404
+ CloudBridgeClientError # base for everything
405
+ ├── ServerConnectionError # cannot reach the Cloud Bridge server
406
+ ├── ConfigError # local config could not be read/parsed
407
+ ├── ProviderMismatchError # wrong client for the credential type
408
+ └── ApiError # any non-2xx HTTP response (has .status_code, .message, .data)
409
+ ├── AuthenticationError # 401 / 403
410
+ ├── ProviderNotFoundError # 404 (region/zone/stack not found)
411
+ ├── ProviderConnectionError # 400 — server could not reach the cloud provider
412
+ ├── ValidationError # 422 — request payload rejected
413
+ ├── StackOperationError # 409 — deploy/destroy/preview conflict
414
+ ├── FlavorError # 400 — flavor validation failed (candidates in .data)
415
+ ├── ImageError # 400 — image validation failed (candidates in .data)
416
+ ├── VolumeError # 400 — volume-type validation failed (candidates in .data)
417
+ └── NetworkError # 400 — network validation failed (detail in .data)
418
+ ```
419
+
420
+ Each `ApiError` renders nicely when printed — `str(e)` includes the status code, message, and a pretty-printed `data` payload when present, which makes `FlavorError` / `ImageError` / `VolumeError` immediately actionable.
421
+
422
+ ## Full example
423
+
424
+ An interactive, env-driven example lives in [`main.py`](./main.py). The essence:
425
+
426
+ ```python
427
+ import os
428
+ from pprint import pprint
429
+
430
+ from dotenv import load_dotenv
431
+ from cloud_bridge_client import AwsClient, AWSCredentials
432
+
433
+ load_dotenv()
434
+
435
+ client = AwsClient(
436
+ credentials=AWSCredentials(
437
+ access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
438
+ secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"],
439
+ region=os.environ["AWS_REGION"],
440
+ zone=os.environ["AWS_AVAILABILITY_ZONE"],
441
+ ),
442
+ )
443
+
444
+ CONFIG = "universal_cluster.yaml"
445
+ CLUSTER = "swarm-cluster"
446
+
447
+ pprint(client.preview(config=CONFIG, auto_select=True)) # dry-run
448
+ pprint(client.deploy(config=CONFIG)) # provision
449
+ pprint(client.status(cluster_name=CLUSTER)) # follow up
450
+
451
+ # Two-phase teardown
452
+ resp = client.destroy(cluster_name=CLUSTER, stateless_resources=True, config_file=True)
453
+ code = resp["data"]["code"]
454
+ if input(f"Confirm destroy? code={code} (y/n) ") == "y":
455
+ pprint(client.destroy(cluster_name=CLUSTER, confirm_code=code))
456
+ ```
457
+
458
+ A matching `.env.example` with the expected variable names is included in the repository.
459
+
460
+ ## License
461
+
462
+ MIT © Aurora Resourcing.