cloud-bridge-client 0.0.1__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.
@@ -0,0 +1,89 @@
1
+ """
2
+ cloud_bridge_client — Python client library for the Cloud Bridge API.
3
+
4
+ Quick start::
5
+
6
+ from cloud_bridge_client import AwsClient, AWSCredentials
7
+
8
+ client = AwsClient(
9
+ credentials=AWSCredentials(
10
+ access_key_id="AKIA...",
11
+ secret_access_key="...",
12
+ region="eu-central-1",
13
+ zone="eu-central-1a",
14
+ ),
15
+ base_url="http://my-server:8080",
16
+ )
17
+
18
+ client.deploy(config="./cluster.yaml")
19
+ client.preview(config="./cluster.yaml")
20
+ client.destroy(
21
+ cluster_name="my-cluster",
22
+ stateless_resources=True,
23
+ config_file=True,
24
+ )
25
+ """
26
+
27
+ __version__ = "0.0.1"
28
+
29
+ from .auth import AuthManager
30
+ from .client import (
31
+ DEFAULT_BASE_URL,
32
+ AwsClient,
33
+ AzureClient,
34
+ GcpClient,
35
+ OpenStackClient,
36
+ )
37
+ from .credentials import (
38
+ AWSCredentials,
39
+ AzureCredentials,
40
+ CloudProviderCredentials,
41
+ GCPCredentials,
42
+ OpenStackCredentials,
43
+ Provider,
44
+ )
45
+ from .exceptions import (
46
+ ApiError,
47
+ AuthenticationError,
48
+ CloudBridgeClientError,
49
+ ConfigError,
50
+ FlavorError,
51
+ ImageError,
52
+ NetworkError,
53
+ ProviderConnectionError,
54
+ ProviderMismatchError,
55
+ ProviderNotFoundError,
56
+ ServerConnectionError,
57
+ StackOperationError,
58
+ ValidationError,
59
+ VolumeError,
60
+ )
61
+
62
+ __all__ = [
63
+ "DEFAULT_BASE_URL",
64
+ "AWSCredentials",
65
+ "ApiError",
66
+ "AuthManager",
67
+ "AuthenticationError",
68
+ "AwsClient",
69
+ "AzureClient",
70
+ "AzureCredentials",
71
+ "CloudBridgeClientError",
72
+ "CloudProviderCredentials",
73
+ "ConfigError",
74
+ "FlavorError",
75
+ "GCPCredentials",
76
+ "GcpClient",
77
+ "ImageError",
78
+ "NetworkError",
79
+ "OpenStackClient",
80
+ "OpenStackCredentials",
81
+ "Provider",
82
+ "ProviderConnectionError",
83
+ "ProviderMismatchError",
84
+ "ProviderNotFoundError",
85
+ "ServerConnectionError",
86
+ "StackOperationError",
87
+ "ValidationError",
88
+ "VolumeError",
89
+ ]
@@ -0,0 +1,39 @@
1
+ from .credentials import CloudProviderCredentials
2
+
3
+
4
+ class AuthManager:
5
+ """
6
+ Responsible for building the authentication header from the credential
7
+ object supplied at client initialisation time.
8
+
9
+ The Cloud Bridge API uses a single ``X-API-Key`` header whose value is a
10
+ colon-separated string encoding the provider type and all necessary
11
+ credentials::
12
+
13
+ aws:ACCESS_KEY_ID:SECRET_ACCESS_KEY:REGION:ZONE
14
+ gcp:PROJECT:REGION:ZONE:ACCESS_TOKEN
15
+ openstack:AUTH_URL:APP_CREDS_ID:APP_CREDS_SECRET
16
+ azure:SUBSCRIPTION_ID:TENANT_ID:CLIENT_ID:CLIENT_SECRET:LOCATION:RG
17
+
18
+ Every credential dataclass exposes a ``to_api_key()`` method that returns
19
+ the correctly formatted string. AuthManager simply calls that method
20
+ and wraps the result in the expected header dict.
21
+ """
22
+
23
+ _HEADER_NAME = "X-API-Key"
24
+
25
+ def __init__(self, credentials: CloudProviderCredentials) -> None:
26
+ self._credentials = credentials
27
+
28
+ @property
29
+ def provider(self) -> str:
30
+ """Return the lowercase provider name ("aws", "gcp", ...)."""
31
+ return self._credentials.provider.value
32
+
33
+ def get_headers(self) -> dict[str, str]:
34
+ """Return the auth header dict ready to be merged into a request."""
35
+ return {self._HEADER_NAME: self._credentials.to_api_key()}
36
+
37
+ def update_credentials(self, credentials: CloudProviderCredentials) -> None:
38
+ """Hot-swap credentials without creating a new client instance."""
39
+ self._credentials = credentials
@@ -0,0 +1,422 @@
1
+ """
2
+ cloud_bridge_client — provider-specific client classes.
3
+
4
+ Usage::
5
+
6
+ from cloud_bridge_client import AwsClient, GcpClient
7
+ from cloud_bridge_client import AWSCredentials, GCPCredentials
8
+
9
+ aws = AwsClient(
10
+ credentials=AWSCredentials(
11
+ access_key_id="AKIA...",
12
+ secret_access_key="...",
13
+ region="eu-central-1",
14
+ zone="eu-central-1a",
15
+ ),
16
+ base_url="http://my-cloud-bridge-server:8080",
17
+ )
18
+
19
+ aws.deploy(config="./infra/cluster.yaml")
20
+ aws.preview(config="./infra/cluster.yaml")
21
+ aws.preview_destroy(cluster_name="my-cluster")
22
+ aws.destroy(cluster_name="my-cluster", stateless_resources=True, config_file=True)
23
+ aws.ping()
24
+ """
25
+
26
+ from typing import Any, cast
27
+
28
+ from .auth import AuthManager
29
+ from .credentials import (
30
+ AWSCredentials,
31
+ AzureCredentials,
32
+ GCPCredentials,
33
+ OpenStackCredentials,
34
+ )
35
+ from .http import ConfigInput, HttpClient
36
+
37
+ DEFAULT_BASE_URL = "http://localhost:8080"
38
+
39
+ _PROVIDERS_PATH = "/api/v1/cloud/providers"
40
+ _DEPLOY_PATH = "/api/v1/cloud/stack/deploy"
41
+ _PREVIEW_PATH = "/api/v1/cloud/stack/preview"
42
+ _PREVIEW_DESTROY_PATH = "/api/v1/cloud/stack/preview-destroy"
43
+ _CANCEL_PATH = "/api/v1/cloud/stack/cancel"
44
+ _DESTROY_PATH = "/api/v1/cloud/stack/destroy"
45
+ _VOLUMES_PATH = "/api/v1/cloud/stack/volumes"
46
+ _STATUS_PATH = "/api/v1/cloud/stack/status"
47
+
48
+
49
+ class _BaseClient:
50
+ """
51
+ Internal base — wires credentials → AuthManager → HttpClient
52
+ and exposes all cloud operations shared by all providers.
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ credentials: AWSCredentials | GCPCredentials | AzureCredentials | OpenStackCredentials,
58
+ base_url: str = DEFAULT_BASE_URL,
59
+ timeout: int = 600,
60
+ ) -> None:
61
+ self._credentials = credentials
62
+ self._auth = AuthManager(credentials)
63
+ self._http = HttpClient(base_url=base_url, auth=self._auth, timeout=timeout)
64
+
65
+ # ------------------------------------------------------------------
66
+ # Core operations
67
+ # ------------------------------------------------------------------
68
+
69
+ def deploy(
70
+ self,
71
+ config: ConfigInput,
72
+ *,
73
+ auto_select: bool = False,
74
+ request_id: str | None = None,
75
+ ) -> Any:
76
+ """
77
+ Deploy the infrastructure described in *config*.
78
+
79
+ Parameters
80
+ ----------
81
+ config:
82
+ Path to a YAML config file (str / pathlib.Path) — must exist
83
+ on disk. Alternatively, pass raw YAML content as ``bytes``,
84
+ or a plain ``dict`` that will be serialised to YAML.
85
+ auto_select:
86
+ If ``True``, automatically select a flavor when the config
87
+ does not specify one.
88
+ request_id:
89
+ Optional request tracing ID (UUID v4). Generated automatically
90
+ by the server if absent.
91
+ """
92
+ return self._http.post_file(
93
+ _DEPLOY_PATH,
94
+ config,
95
+ auto_select=auto_select,
96
+ request_id=request_id,
97
+ )
98
+
99
+ def preview(
100
+ self,
101
+ config: ConfigInput,
102
+ *,
103
+ auto_select: bool = False,
104
+ request_id: str | None = None,
105
+ ) -> Any:
106
+ """
107
+ Dry-run: show what would change without touching real infrastructure.
108
+
109
+ Parameters
110
+ ----------
111
+ config:
112
+ Path to a YAML config file (str / pathlib.Path) — must exist
113
+ on disk. Alternatively, pass raw YAML content as ``bytes``,
114
+ or a plain ``dict`` that will be serialised to YAML.
115
+ auto_select:
116
+ If ``True``, automatically select a flavor when the config
117
+ does not specify one.
118
+ request_id:
119
+ Optional request tracing ID (UUID v4).
120
+ """
121
+ return self._http.post_file(
122
+ _PREVIEW_PATH,
123
+ config,
124
+ auto_select=auto_select,
125
+ request_id=request_id,
126
+ )
127
+
128
+ def preview_destroy(self, cluster_name: str) -> Any:
129
+ """Dry-run: list resources that *would* be deleted."""
130
+ return self._http.post_json(_PREVIEW_DESTROY_PATH, {"cluster_name": cluster_name})
131
+
132
+ def cancel(self, cluster_name: str) -> Any:
133
+ """
134
+ Cancel an in-progress stack update or clear a stale lock.
135
+
136
+ Parameters
137
+ ----------
138
+ cluster_name:
139
+ Name of the cluster whose update should be cancelled.
140
+ """
141
+ return self._http.post_json(_CANCEL_PATH, {"cluster_name": cluster_name})
142
+
143
+ def destroy(
144
+ self,
145
+ cluster_name: str,
146
+ *,
147
+ stateless_resources: bool = False,
148
+ stateful_resources: bool = False,
149
+ config_file: bool = False,
150
+ confirm_code: str | None = None,
151
+ request_id: str | None = None,
152
+ ) -> Any:
153
+ """
154
+ Destroy stack resources. Supports a two-phase confirmation flow.
155
+
156
+ **Phase 1** — call with at least one of ``stateless_resources``,
157
+ ``stateful_resources``, or ``config_file`` set to ``True``.
158
+ Returns a confirmation response containing a ``code`` that expires.
159
+
160
+ **Phase 2** — call again with ``confirm_code`` set to the code from
161
+ phase 1. This triggers the actual destruction and returns a job ID.
162
+
163
+ Parameters
164
+ ----------
165
+ cluster_name:
166
+ Name of the cluster to destroy.
167
+ stateless_resources:
168
+ Delete stateless resources (compute, networks, etc.).
169
+ stateful_resources:
170
+ Delete stateful resources (volumes, databases).
171
+ Requires ``stateless_resources=True``.
172
+ config_file:
173
+ Delete the Pulumi config file.
174
+ confirm_code:
175
+ Confirmation code from a previous phase-1 call.
176
+ request_id:
177
+ Optional request tracing ID (UUID v4).
178
+
179
+ Returns
180
+ -------
181
+ dict
182
+ Phase 1: ``{"data": {"warning": str, "code": str, "expire_at": str, "volumes": [...]}}``
183
+ Phase 2: ``{"data": {"job_id": str, "operation": str, "provider": str, "cluster_name": str}}``
184
+ """
185
+ body: dict[str, Any] = {"cluster_name": cluster_name}
186
+ params: dict[str, Any] = {}
187
+ if confirm_code is not None:
188
+ params["confirm_code"] = confirm_code
189
+ else:
190
+ params["stateless_resources"] = stateless_resources
191
+ params["stateful_resources"] = stateful_resources
192
+ params["config_file"] = config_file
193
+ return self._http.post_json(_DESTROY_PATH, body, request_id=request_id, params=params)
194
+
195
+ # ------------------------------------------------------------------
196
+ # Info / utility
197
+ # ------------------------------------------------------------------
198
+
199
+ def providers(self) -> list[str]:
200
+ """
201
+ Return the list of available cloud provider identifiers.
202
+
203
+ Returns
204
+ -------
205
+ list[str]
206
+ e.g. ``["aws", "gcp", "openstack"]``
207
+ """
208
+ resp = self._http.get(_PROVIDERS_PATH)
209
+ return resp.get("data", {}).get("providers", [])
210
+
211
+ def volumes(self, cluster_name: str) -> Any:
212
+ """
213
+ Return volume fate info for a stack.
214
+
215
+ Shows what will happen to each volume when the stack is destroyed.
216
+
217
+ Parameters
218
+ ----------
219
+ cluster_name:
220
+ Name of the cluster.
221
+ """
222
+ return self._http.get(_VOLUMES_PATH, params={"cluster_name": cluster_name})
223
+
224
+ def status(self, cluster_name: str) -> Any:
225
+ """
226
+ Return current outputs and last-update summary for the named stack.
227
+
228
+ Parameters
229
+ ----------
230
+ cluster_name:
231
+ Name of the cluster.
232
+ """
233
+ return self._http.get(_STATUS_PATH, params={"cluster_name": cluster_name})
234
+
235
+ def ping(self) -> bool:
236
+ """Return ``True`` if the Cloud Bridge API server is reachable."""
237
+ return self._http.ping()
238
+
239
+ def update_credentials(
240
+ self,
241
+ credentials: AWSCredentials | GCPCredentials | AzureCredentials | OpenStackCredentials,
242
+ ) -> None:
243
+ """Hot-swap credentials without creating a new client instance."""
244
+ self._credentials = credentials
245
+ self._auth.update_credentials(credentials)
246
+
247
+
248
+ # ---------------------------------------------------------------------------
249
+ # Public provider-specific clients
250
+ # ---------------------------------------------------------------------------
251
+
252
+
253
+ class AwsClient(_BaseClient):
254
+ """
255
+ Cloud Bridge client for **Amazon Web Services**.
256
+
257
+ Parameters
258
+ ----------
259
+ credentials:
260
+ :class:`AWSCredentials` instance.
261
+ base_url:
262
+ Base URL of the Cloud Bridge API server (default: ``http://localhost:8080``).
263
+ timeout:
264
+ Request timeout in seconds (default: 600).
265
+
266
+ Example
267
+ -------
268
+ ::
269
+
270
+ from cloud_bridge_client import AwsClient, AWSCredentials
271
+
272
+ client = AwsClient(
273
+ credentials=AWSCredentials(
274
+ access_key_id="AKIA...",
275
+ secret_access_key="...",
276
+ region="eu-central-1",
277
+ zone="eu-central-1a",
278
+ ),
279
+ )
280
+ client.deploy(config="cluster.yaml")
281
+ """
282
+
283
+ def __init__(
284
+ self,
285
+ credentials: AWSCredentials,
286
+ base_url: str = DEFAULT_BASE_URL,
287
+ timeout: int = 600,
288
+ ) -> None:
289
+ super().__init__(credentials, base_url, timeout)
290
+
291
+ def __repr__(self) -> str:
292
+ creds = cast(AWSCredentials, self._credentials)
293
+ return f"AwsClient(region={creds.region!r}, base_url={self._http._base_url!r})"
294
+
295
+
296
+ class GcpClient(_BaseClient):
297
+ """
298
+ Cloud Bridge client for **Google Cloud Platform**.
299
+
300
+ Parameters
301
+ ----------
302
+ credentials:
303
+ :class:`GCPCredentials` instance.
304
+ base_url:
305
+ Base URL of the Cloud Bridge API server (default: ``http://localhost:8080``).
306
+ timeout:
307
+ Request timeout in seconds (default: 600).
308
+
309
+ Example
310
+ -------
311
+ ::
312
+
313
+ from cloud_bridge_client import GcpClient, GCPCredentials
314
+
315
+ client = GcpClient(
316
+ credentials=GCPCredentials(
317
+ project="my-project",
318
+ region="us-central1",
319
+ zone="us-central1-a",
320
+ service_account_email="sa@my-project.iam.gserviceaccount.com",
321
+ private_key="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
322
+ ),
323
+ )
324
+ client.deploy(config="cluster.yaml")
325
+ """
326
+
327
+ def __init__(
328
+ self,
329
+ credentials: GCPCredentials,
330
+ base_url: str = DEFAULT_BASE_URL,
331
+ timeout: int = 600,
332
+ ) -> None:
333
+ super().__init__(credentials, base_url, timeout)
334
+
335
+ def __repr__(self) -> str:
336
+ creds = cast(GCPCredentials, self._credentials)
337
+ return f"GcpClient(project={creds.project!r}, base_url={self._http._base_url!r})"
338
+
339
+
340
+ class AzureClient(_BaseClient):
341
+ """
342
+ Cloud Bridge client for **Microsoft Azure**.
343
+
344
+ Parameters
345
+ ----------
346
+ credentials:
347
+ :class:`AzureCredentials` instance.
348
+ base_url:
349
+ Base URL of the Cloud Bridge API server (default: ``http://localhost:8080``).
350
+ timeout:
351
+ Request timeout in seconds (default: 600).
352
+
353
+ Example
354
+ -------
355
+ ::
356
+
357
+ from cloud_bridge_client import AzureClient, AzureCredentials
358
+
359
+ client = AzureClient(
360
+ credentials=AzureCredentials(
361
+ subscription_id="...",
362
+ tenant_id="...",
363
+ client_id="...",
364
+ client_secret="...",
365
+ ),
366
+ )
367
+ client.deploy(config="cluster.yaml")
368
+ """
369
+
370
+ def __init__(
371
+ self,
372
+ credentials: AzureCredentials,
373
+ base_url: str = DEFAULT_BASE_URL,
374
+ timeout: int = 600,
375
+ ) -> None:
376
+ super().__init__(credentials, base_url, timeout)
377
+
378
+ def __repr__(self) -> str:
379
+ creds = cast(AzureCredentials, self._credentials)
380
+ return f"AzureClient(subscription_id={creds.subscription_id!r}, base_url={self._http._base_url!r})"
381
+
382
+
383
+ class OpenStackClient(_BaseClient):
384
+ """
385
+ Cloud Bridge client for **OpenStack**.
386
+
387
+ Parameters
388
+ ----------
389
+ credentials:
390
+ :class:`OpenStackCredentials` instance.
391
+ base_url:
392
+ Base URL of the Cloud Bridge API server (default: ``http://localhost:8080``).
393
+ timeout:
394
+ Request timeout in seconds (default: 600).
395
+
396
+ Example
397
+ -------
398
+ ::
399
+
400
+ from cloud_bridge_client import OpenStackClient, OpenStackCredentials
401
+
402
+ client = OpenStackClient(
403
+ credentials=OpenStackCredentials(
404
+ auth_url="https://my-openstack:5000/v3",
405
+ application_credential_id="...",
406
+ application_credential_secret="...",
407
+ ),
408
+ )
409
+ client.deploy(config="cluster.yaml")
410
+ """
411
+
412
+ def __init__(
413
+ self,
414
+ credentials: OpenStackCredentials,
415
+ base_url: str = DEFAULT_BASE_URL,
416
+ timeout: int = 600,
417
+ ) -> None:
418
+ super().__init__(credentials, base_url, timeout)
419
+
420
+ def __repr__(self) -> str:
421
+ creds = cast(OpenStackCredentials, self._credentials)
422
+ return f"OpenStackClient(auth_url={creds.auth_url!r}, base_url={self._http._base_url!r})"
@@ -0,0 +1,87 @@
1
+ from dataclasses import dataclass, field
2
+ from enum import StrEnum
3
+
4
+
5
+ class Provider(StrEnum):
6
+ AWS = "aws"
7
+ GCP = "gcp"
8
+ AZURE = "azure"
9
+ OPENSTACK = "openstack"
10
+
11
+
12
+ @dataclass
13
+ class AWSCredentials:
14
+ """Credentials for Amazon Web Services."""
15
+
16
+ access_key_id: str
17
+ secret_access_key: str
18
+ region: str = "eu-central-1"
19
+ zone: str = ""
20
+
21
+ #: Which provider enum value this maps to (read-only).
22
+ provider: Provider = field(default=Provider.AWS, init=False, repr=False)
23
+
24
+ def to_api_key(self) -> str:
25
+ """Encode as ``aws:ACCESS_KEY_ID:SECRET_ACCESS_KEY:REGION:ZONE``."""
26
+ return f"aws:{self.access_key_id}:{self.secret_access_key}:{self.region}:{self.zone}"
27
+
28
+
29
+ @dataclass
30
+ class GCPCredentials:
31
+ """Credentials for Google Cloud Platform."""
32
+
33
+ project: str
34
+ region: str
35
+ zone: str
36
+ service_account_email: str
37
+ private_key: str
38
+ # access_token: str | None = None
39
+
40
+ provider: Provider = field(default=Provider.GCP, init=False, repr=False)
41
+
42
+ def to_api_key(self) -> str:
43
+ """Encode as ``gcp:PROJECT:REGION:ZONE:SERVICE_ACCOUNT:PRIVATE_KEY``."""
44
+ key_for_header = self.private_key.replace("\n", "\\n")
45
+ return f"gcp:{self.project}:{self.region}:{self.zone}:{self.service_account_email}:{key_for_header}"
46
+
47
+
48
+ @dataclass
49
+ class OpenStackCredentials:
50
+ """Credentials for OpenStack."""
51
+
52
+ auth_url: str
53
+ application_credential_id: str
54
+ application_credential_secret: str
55
+ region_name: str = "RegionOne"
56
+
57
+ provider: Provider = field(default=Provider.OPENSTACK, init=False, repr=False)
58
+
59
+ def to_api_key(self) -> str:
60
+ """Encode as ``openstack:AUTH_URL:APP_CREDS_ID:APP_CREDS_SECRET``."""
61
+ return f"openstack:{self.auth_url}:{self.application_credential_id}:{self.application_credential_secret}"
62
+
63
+
64
+ @dataclass
65
+ class AzureCredentials:
66
+ """Credentials for Microsoft Azure."""
67
+
68
+ subscription_id: str
69
+ tenant_id: str
70
+ client_id: str
71
+ client_secret: str
72
+ location: str = "east's"
73
+ resource_group: str = "universal-infra-rg"
74
+
75
+ provider: Provider = field(default=Provider.AZURE, init=False, repr=False)
76
+
77
+ def to_api_key(self) -> str:
78
+ """Encode as ``azure:SUB:TENANT:CLIENT_ID:CLIENT_SECRET:LOCATION:RG``."""
79
+ return (
80
+ f"azure:{self.subscription_id}:{self.tenant_id}:"
81
+ f"{self.client_id}:{self.client_secret}:"
82
+ f"{self.location}:{self.resource_group}"
83
+ )
84
+
85
+
86
+ # Convenience union type used throughout the library.
87
+ CloudProviderCredentials = AWSCredentials | GCPCredentials | AzureCredentials | OpenStackCredentials