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.
- cloud_bridge_client-0.0.1/PKG-INFO +462 -0
- cloud_bridge_client-0.0.1/README.md +445 -0
- cloud_bridge_client-0.0.1/pyproject.toml +99 -0
- cloud_bridge_client-0.0.1/setup.cfg +4 -0
- cloud_bridge_client-0.0.1/src/cloud_bridge_client/__init__.py +89 -0
- cloud_bridge_client-0.0.1/src/cloud_bridge_client/auth.py +39 -0
- cloud_bridge_client-0.0.1/src/cloud_bridge_client/client.py +422 -0
- cloud_bridge_client-0.0.1/src/cloud_bridge_client/credentials.py +87 -0
- cloud_bridge_client-0.0.1/src/cloud_bridge_client/exceptions.py +186 -0
- cloud_bridge_client-0.0.1/src/cloud_bridge_client/http.py +218 -0
- cloud_bridge_client-0.0.1/src/cloud_bridge_client.egg-info/PKG-INFO +462 -0
- cloud_bridge_client-0.0.1/src/cloud_bridge_client.egg-info/SOURCES.txt +13 -0
- cloud_bridge_client-0.0.1/src/cloud_bridge_client.egg-info/dependency_links.txt +1 -0
- cloud_bridge_client-0.0.1/src/cloud_bridge_client.egg-info/requires.txt +3 -0
- cloud_bridge_client-0.0.1/src/cloud_bridge_client.egg-info/top_level.txt +1 -0
|
@@ -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.
|