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.
- cloud_bridge_client/__init__.py +89 -0
- cloud_bridge_client/auth.py +39 -0
- cloud_bridge_client/client.py +422 -0
- cloud_bridge_client/credentials.py +87 -0
- cloud_bridge_client/exceptions.py +186 -0
- cloud_bridge_client/http.py +218 -0
- cloud_bridge_client-0.0.1.dist-info/METADATA +462 -0
- cloud_bridge_client-0.0.1.dist-info/RECORD +10 -0
- cloud_bridge_client-0.0.1.dist-info/WHEEL +5 -0
- cloud_bridge_client-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -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
|