exoscale-connector 0.2.0__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.
Files changed (148) hide show
  1. exoscale_connector-0.2.0/.github/workflows/ci.yml +29 -0
  2. exoscale_connector-0.2.0/.github/workflows/release.yml +51 -0
  3. exoscale_connector-0.2.0/.gitignore +81 -0
  4. exoscale_connector-0.2.0/LICENSE +21 -0
  5. exoscale_connector-0.2.0/PKG-INFO +150 -0
  6. exoscale_connector-0.2.0/README.md +125 -0
  7. exoscale_connector-0.2.0/docs/asset-types/README.md +72 -0
  8. exoscale_connector-0.2.0/docs/asset-types/anti-affinity-group.md +65 -0
  9. exoscale_connector-0.2.0/docs/asset-types/api-key.md +88 -0
  10. exoscale_connector-0.2.0/docs/asset-types/block-volume-snapshot.md +96 -0
  11. exoscale_connector-0.2.0/docs/asset-types/block-volume.md +118 -0
  12. exoscale_connector-0.2.0/docs/asset-types/dbaas.md +160 -0
  13. exoscale_connector-0.2.0/docs/asset-types/dns.md +112 -0
  14. exoscale_connector-0.2.0/docs/asset-types/elastic-ip.md +100 -0
  15. exoscale_connector-0.2.0/docs/asset-types/iam-role.md +149 -0
  16. exoscale_connector-0.2.0/docs/asset-types/iam-user.md +62 -0
  17. exoscale_connector-0.2.0/docs/asset-types/instance-pool.md +108 -0
  18. exoscale_connector-0.2.0/docs/asset-types/instance-type.md +43 -0
  19. exoscale_connector-0.2.0/docs/asset-types/instance.md +136 -0
  20. exoscale_connector-0.2.0/docs/asset-types/load-balancer.md +152 -0
  21. exoscale_connector-0.2.0/docs/asset-types/object-storage.md +137 -0
  22. exoscale_connector-0.2.0/docs/asset-types/private-network.md +83 -0
  23. exoscale_connector-0.2.0/docs/asset-types/security-group.md +119 -0
  24. exoscale_connector-0.2.0/docs/asset-types/sks.md +184 -0
  25. exoscale_connector-0.2.0/docs/asset-types/snapshot.md +102 -0
  26. exoscale_connector-0.2.0/docs/asset-types/ssh-key.md +83 -0
  27. exoscale_connector-0.2.0/docs/asset-types/template.md +48 -0
  28. exoscale_connector-0.2.0/docs/asset-types/zone.md +37 -0
  29. exoscale_connector-0.2.0/docs/developer-guide.md +217 -0
  30. exoscale_connector-0.2.0/docs/iam-policy-cookbook.md +198 -0
  31. exoscale_connector-0.2.0/docs/live-test-plan.md +389 -0
  32. exoscale_connector-0.2.0/docs/live-test-results.md +329 -0
  33. exoscale_connector-0.2.0/docs/roadmap.md +116 -0
  34. exoscale_connector-0.2.0/docs/user-guide.md +184 -0
  35. exoscale_connector-0.2.0/pyproject.toml +90 -0
  36. exoscale_connector-0.2.0/src/exoscale_connector/__init__.py +48 -0
  37. exoscale_connector-0.2.0/src/exoscale_connector/auth.py +78 -0
  38. exoscale_connector-0.2.0/src/exoscale_connector/cli/__init__.py +7 -0
  39. exoscale_connector-0.2.0/src/exoscale_connector/cli/_base.py +338 -0
  40. exoscale_connector-0.2.0/src/exoscale_connector/cli/anti_affinity_group.py +25 -0
  41. exoscale_connector-0.2.0/src/exoscale_connector/cli/api_key.py +25 -0
  42. exoscale_connector-0.2.0/src/exoscale_connector/cli/block_volume.py +27 -0
  43. exoscale_connector-0.2.0/src/exoscale_connector/cli/block_volume_snapshot.py +33 -0
  44. exoscale_connector-0.2.0/src/exoscale_connector/cli/dbaas.py +97 -0
  45. exoscale_connector-0.2.0/src/exoscale_connector/cli/dns.py +42 -0
  46. exoscale_connector-0.2.0/src/exoscale_connector/cli/elastic_ip.py +25 -0
  47. exoscale_connector-0.2.0/src/exoscale_connector/cli/iam_role.py +25 -0
  48. exoscale_connector-0.2.0/src/exoscale_connector/cli/iam_user.py +25 -0
  49. exoscale_connector-0.2.0/src/exoscale_connector/cli/instance.py +25 -0
  50. exoscale_connector-0.2.0/src/exoscale_connector/cli/instance_pool.py +25 -0
  51. exoscale_connector-0.2.0/src/exoscale_connector/cli/instance_type.py +25 -0
  52. exoscale_connector-0.2.0/src/exoscale_connector/cli/load_balancer.py +25 -0
  53. exoscale_connector-0.2.0/src/exoscale_connector/cli/main.py +85 -0
  54. exoscale_connector-0.2.0/src/exoscale_connector/cli/object_storage.py +160 -0
  55. exoscale_connector-0.2.0/src/exoscale_connector/cli/private_network.py +25 -0
  56. exoscale_connector-0.2.0/src/exoscale_connector/cli/security_group.py +25 -0
  57. exoscale_connector-0.2.0/src/exoscale_connector/cli/sks.py +43 -0
  58. exoscale_connector-0.2.0/src/exoscale_connector/cli/snapshot.py +34 -0
  59. exoscale_connector-0.2.0/src/exoscale_connector/cli/ssh_key.py +25 -0
  60. exoscale_connector-0.2.0/src/exoscale_connector/cli/template.py +31 -0
  61. exoscale_connector-0.2.0/src/exoscale_connector/cli/zone.py +23 -0
  62. exoscale_connector-0.2.0/src/exoscale_connector/client.py +262 -0
  63. exoscale_connector-0.2.0/src/exoscale_connector/config.py +110 -0
  64. exoscale_connector-0.2.0/src/exoscale_connector/errors.py +76 -0
  65. exoscale_connector-0.2.0/src/exoscale_connector/iam_expr.py +80 -0
  66. exoscale_connector-0.2.0/src/exoscale_connector/models.py +79 -0
  67. exoscale_connector-0.2.0/src/exoscale_connector/py.typed +0 -0
  68. exoscale_connector-0.2.0/src/exoscale_connector/resources/__init__.py +8 -0
  69. exoscale_connector-0.2.0/src/exoscale_connector/resources/_base.py +255 -0
  70. exoscale_connector-0.2.0/src/exoscale_connector/resources/_reverse_dns.py +85 -0
  71. exoscale_connector-0.2.0/src/exoscale_connector/resources/anti_affinity_group.py +39 -0
  72. exoscale_connector-0.2.0/src/exoscale_connector/resources/api_key.py +59 -0
  73. exoscale_connector-0.2.0/src/exoscale_connector/resources/block_volume.py +159 -0
  74. exoscale_connector-0.2.0/src/exoscale_connector/resources/block_volume_snapshot.py +95 -0
  75. exoscale_connector-0.2.0/src/exoscale_connector/resources/dbaas.py +383 -0
  76. exoscale_connector-0.2.0/src/exoscale_connector/resources/dns.py +206 -0
  77. exoscale_connector-0.2.0/src/exoscale_connector/resources/elastic_ip.py +50 -0
  78. exoscale_connector-0.2.0/src/exoscale_connector/resources/iam_role.py +237 -0
  79. exoscale_connector-0.2.0/src/exoscale_connector/resources/iam_user.py +44 -0
  80. exoscale_connector-0.2.0/src/exoscale_connector/resources/instance.py +146 -0
  81. exoscale_connector-0.2.0/src/exoscale_connector/resources/instance_pool.py +68 -0
  82. exoscale_connector-0.2.0/src/exoscale_connector/resources/instance_type.py +49 -0
  83. exoscale_connector-0.2.0/src/exoscale_connector/resources/load_balancer.py +144 -0
  84. exoscale_connector-0.2.0/src/exoscale_connector/resources/object_storage.py +390 -0
  85. exoscale_connector-0.2.0/src/exoscale_connector/resources/private_network.py +31 -0
  86. exoscale_connector-0.2.0/src/exoscale_connector/resources/security_group.py +93 -0
  87. exoscale_connector-0.2.0/src/exoscale_connector/resources/sks.py +237 -0
  88. exoscale_connector-0.2.0/src/exoscale_connector/resources/snapshot.py +94 -0
  89. exoscale_connector-0.2.0/src/exoscale_connector/resources/ssh_key.py +44 -0
  90. exoscale_connector-0.2.0/src/exoscale_connector/resources/template.py +82 -0
  91. exoscale_connector-0.2.0/src/exoscale_connector/resources/zone.py +39 -0
  92. exoscale_connector-0.2.0/src/exoscale_connector/wait.py +53 -0
  93. exoscale_connector-0.2.0/tests/conftest.py +36 -0
  94. exoscale_connector-0.2.0/tests/integration/__init__.py +1 -0
  95. exoscale_connector-0.2.0/tests/integration/_fixtures.py +165 -0
  96. exoscale_connector-0.2.0/tests/integration/_recorder.py +85 -0
  97. exoscale_connector-0.2.0/tests/integration/conftest.py +190 -0
  98. exoscale_connector-0.2.0/tests/integration/test_smoke.py +106 -0
  99. exoscale_connector-0.2.0/tests/integration/test_tier_1.py +290 -0
  100. exoscale_connector-0.2.0/tests/integration/test_tier_2.py +168 -0
  101. exoscale_connector-0.2.0/tests/integration/test_tier_3.py +329 -0
  102. exoscale_connector-0.2.0/tests/integration/test_tier_4.py +305 -0
  103. exoscale_connector-0.2.0/tests/recorded/README.md +17 -0
  104. exoscale_connector-0.2.0/tests/recorded/live-20260610t210654z-hm5r27.jsonl +71 -0
  105. exoscale_connector-0.2.0/tests/recorded/live-20260610t211130z-qint3r.jsonl +39 -0
  106. exoscale_connector-0.2.0/tests/recorded/live-20260610t212438z-zc5fk2.jsonl +22 -0
  107. exoscale_connector-0.2.0/tests/recorded/live-20260610t212710z-vha6f5.jsonl +149 -0
  108. exoscale_connector-0.2.0/tests/recorded/live-20260610t213124z-eumxat.jsonl +83 -0
  109. exoscale_connector-0.2.0/tests/recorded/live-20260610t213339z-bxntit.jsonl +83 -0
  110. exoscale_connector-0.2.0/tests/recorded/live-20260610t213541z-6912dm.jsonl +64 -0
  111. exoscale_connector-0.2.0/tests/recorded/live-20260610t213656z-ufn85e.jsonl +334 -0
  112. exoscale_connector-0.2.0/tests/recorded/live-20260610t214915z-ohlx0g.jsonl +43 -0
  113. exoscale_connector-0.2.0/tests/recorded/live-20260610t215040z-3424x5.jsonl +92 -0
  114. exoscale_connector-0.2.0/tests/recorded/live-20260610t215314z-myjv55.jsonl +40 -0
  115. exoscale_connector-0.2.0/tests/recorded/live-20260610t215606z-hf3cnq.jsonl +136 -0
  116. exoscale_connector-0.2.0/tests/unit/test_anti_affinity_group.py +103 -0
  117. exoscale_connector-0.2.0/tests/unit/test_api_key.py +132 -0
  118. exoscale_connector-0.2.0/tests/unit/test_auth.py +39 -0
  119. exoscale_connector-0.2.0/tests/unit/test_block_volume.py +193 -0
  120. exoscale_connector-0.2.0/tests/unit/test_block_volume_snapshot.py +152 -0
  121. exoscale_connector-0.2.0/tests/unit/test_cli.py +192 -0
  122. exoscale_connector-0.2.0/tests/unit/test_cli_main.py +83 -0
  123. exoscale_connector-0.2.0/tests/unit/test_client.py +214 -0
  124. exoscale_connector-0.2.0/tests/unit/test_config.py +34 -0
  125. exoscale_connector-0.2.0/tests/unit/test_dbaas.py +333 -0
  126. exoscale_connector-0.2.0/tests/unit/test_dns.py +161 -0
  127. exoscale_connector-0.2.0/tests/unit/test_elastic_ip.py +131 -0
  128. exoscale_connector-0.2.0/tests/unit/test_ensure.py +89 -0
  129. exoscale_connector-0.2.0/tests/unit/test_iam_expr.py +54 -0
  130. exoscale_connector-0.2.0/tests/unit/test_iam_role.py +332 -0
  131. exoscale_connector-0.2.0/tests/unit/test_iam_user.py +119 -0
  132. exoscale_connector-0.2.0/tests/unit/test_instance.py +142 -0
  133. exoscale_connector-0.2.0/tests/unit/test_instance_pool.py +127 -0
  134. exoscale_connector-0.2.0/tests/unit/test_instance_type.py +32 -0
  135. exoscale_connector-0.2.0/tests/unit/test_load_balancer.py +173 -0
  136. exoscale_connector-0.2.0/tests/unit/test_models.py +42 -0
  137. exoscale_connector-0.2.0/tests/unit/test_object_storage.py +353 -0
  138. exoscale_connector-0.2.0/tests/unit/test_private_network.py +131 -0
  139. exoscale_connector-0.2.0/tests/unit/test_recorded_replay.py +58 -0
  140. exoscale_connector-0.2.0/tests/unit/test_recorder.py +67 -0
  141. exoscale_connector-0.2.0/tests/unit/test_reverse_dns.py +95 -0
  142. exoscale_connector-0.2.0/tests/unit/test_security_group.py +86 -0
  143. exoscale_connector-0.2.0/tests/unit/test_sks.py +181 -0
  144. exoscale_connector-0.2.0/tests/unit/test_snapshot.py +152 -0
  145. exoscale_connector-0.2.0/tests/unit/test_ssh_key.py +137 -0
  146. exoscale_connector-0.2.0/tests/unit/test_template.py +49 -0
  147. exoscale_connector-0.2.0/tests/unit/test_wait.py +43 -0
  148. exoscale_connector-0.2.0/tests/unit/test_zone.py +18 -0
@@ -0,0 +1,29 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ # Lower and upper bounds of the supported range (requires-python >=3.9).
14
+ python-version: ["3.9", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v6
17
+ - uses: actions/setup-python@v6
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - name: Install package with dev dependencies
21
+ run: pip install -e '.[dev]'
22
+ - name: Lint
23
+ run: ruff check src tests
24
+ - name: Type-check
25
+ run: mypy src
26
+ - name: Unit tests
27
+ run: pytest tests/unit -q
28
+ # Integration tiers (tests/integration) need live tenant credentials and
29
+ # are deliberately NOT run in CI — see docs/live-test-plan.md.
@@ -0,0 +1,51 @@
1
+ name: Release
2
+
3
+ # Publishes to PyPI on version tags (v0.2.0, v1.0.0, ...).
4
+ #
5
+ # INERT UNTIL CONFIGURED: this uses PyPI "trusted publishing" (no API token in
6
+ # the repo). Before the first release, register this repository + workflow as a
7
+ # trusted publisher for the 'exoscale-connector' project on pypi.org, and create
8
+ # the 'pypi' environment in the GitHub repo settings.
9
+
10
+ on:
11
+ push:
12
+ tags: ["v*"]
13
+
14
+ jobs:
15
+ build:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v6
19
+ - uses: actions/setup-python@v6
20
+ with:
21
+ python-version: "3.12"
22
+ - name: Run the full check suite before building
23
+ run: |
24
+ pip install -e '.[dev]'
25
+ ruff check src tests
26
+ mypy src
27
+ pytest tests/unit -q
28
+ - name: Build sdist and wheel
29
+ run: |
30
+ pip install build
31
+ python -m build
32
+ - uses: actions/upload-artifact@v7
33
+ with:
34
+ name: dist
35
+ path: dist/
36
+
37
+ publish:
38
+ needs: build
39
+ runs-on: ubuntu-latest
40
+ environment: pypi
41
+ permissions:
42
+ id-token: write # required for PyPI trusted publishing
43
+ steps:
44
+ - uses: actions/download-artifact@v8
45
+ with:
46
+ name: dist
47
+ path: dist/
48
+ # Pinned to a commit SHA: this third-party action receives the OIDC
49
+ # token that authorizes publishing; a floating branch ref would let a
50
+ # compromised upstream publish on our behalf. Bump deliberately.
51
+ - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 as of 2026-02-18
@@ -0,0 +1,81 @@
1
+ # Python build / cache
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ .eggs/
8
+
9
+ # Local virtualenvs
10
+ .venv/
11
+ venv/
12
+ env/
13
+
14
+ # Test / tooling caches
15
+ .pytest_cache/
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+ .coverage
19
+ coverage.xml
20
+ htmlcov/
21
+
22
+ # Secrets & credentials — NEVER commit
23
+ # App reads credentials from environment variables only; injected at runtime.
24
+ .env
25
+ .env.*
26
+ *.env
27
+ secrets.json
28
+ secrets.yaml
29
+ secrets.yml
30
+ credentials
31
+ credentials.json
32
+ *.token
33
+ *.secret
34
+
35
+ # Exoscale / cloud credentials
36
+ # EXOSCALE_API_KEY / EXOSCALE_API_SECRET must come from the environment, not files.
37
+ exoscale.toml
38
+ .exoscale/
39
+ # Object Storage (SOS) uses boto3 — keep AWS-style shared creds out of the repo
40
+ .aws/
41
+ .boto
42
+
43
+ # Keys & certificates
44
+ *.pem
45
+ *.key
46
+ *.p12
47
+ *.pfx
48
+ *.crt
49
+ *.cer
50
+ # SSH keypairs (the ssh-key asset type can generate ephemeral ed25519 keys)
51
+ id_rsa
52
+ id_rsa.pub
53
+ id_ed25519
54
+ id_ed25519.pub
55
+ id_dsa
56
+ *.ppk
57
+
58
+ # Kubernetes — SKS generate_kubeconfig() output must never be committed
59
+ *.kubeconfig
60
+ kubeconfig
61
+ .kube/
62
+
63
+ # Terraform / IaC state & vars (this connector is built for IaC pipelines)
64
+ *.tfvars
65
+ *.tfstate
66
+ *.tfstate.*
67
+ .terraform/
68
+
69
+ # IDE / editor
70
+ .idea/
71
+ .vscode/
72
+ *.swp
73
+ *.swo
74
+ *~
75
+
76
+ # OS
77
+ .DS_Store
78
+ Thumbs.db
79
+
80
+ # Logs
81
+ *.log
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Raphael Lang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: exoscale-connector
3
+ Version: 0.2.0
4
+ Summary: A clean, typed, reusable Python connector for the Exoscale APIv2 — no CLI tool required.
5
+ Author: Raphael Lang
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: api,cloud,connector,exoscale,iac
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Typing :: Typed
12
+ Requires-Python: >=3.9
13
+ Requires-Dist: pydantic<3,>=2.5
14
+ Requires-Dist: requests>=2.28
15
+ Provides-Extra: dev
16
+ Requires-Dist: cryptography>=40; extra == 'dev'
17
+ Requires-Dist: mypy>=1.8; extra == 'dev'
18
+ Requires-Dist: pytest>=7.4; extra == 'dev'
19
+ Requires-Dist: responses>=0.24; extra == 'dev'
20
+ Requires-Dist: ruff>=0.4; extra == 'dev'
21
+ Requires-Dist: types-requests; extra == 'dev'
22
+ Provides-Extra: sos
23
+ Requires-Dist: boto3>=1.28; extra == 'sos'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # exoscale-connector
27
+
28
+ A clean, typed, reusable Python connector for the **Exoscale APIv2**. It talks to
29
+ the HTTP API directly — **no `exo` CLI and no Ansible required** — so it can be
30
+ dropped into any project that needs to read or manage Exoscale resources
31
+ programmatically.
32
+
33
+ - **Typed** — every request/response is a [pydantic](https://docs.pydantic.dev) v2
34
+ model, so you get validation and editor autocompletion.
35
+ - **One module per asset type** — `security-group`, `instance`, `elastic-ip`,
36
+ `dns`, `dbaas`, `sks`, … each with a small, uniform client.
37
+ - **Library + CLI** — import it, or use the per-asset command-line tools
38
+ (also namespaced under one `exoscale-connector` binary, with `--output table`).
39
+ - **Built for automation** — idempotent `ensure()` (get-or-create by name),
40
+ `wait_for_state` polling, and label-filtered listing make provisioning
41
+ scripts re-runnable by construction.
42
+ - **Self-contained** — runtime deps are just `requests` + `pydantic`; copy the
43
+ package into another repo and it keeps working.
44
+ - **Secret-safe** — credentials come only from the environment; nothing is
45
+ hardcoded or read from disk, and credentials are masked in `repr()`/log
46
+ output.
47
+
48
+ ## Relationship to the official Exoscale SDK
49
+
50
+ Exoscale publishes an official, actively maintained Python SDK —
51
+ [`python-exoscale`](https://github.com/exoscale/python-exoscale) (the `exoscale`
52
+ package on PyPI); if you want the vendor-supported, batteries-included bindings,
53
+ use that. This connector is a smaller, opinionated alternative built for one
54
+ goal — a **drop-in, IaC-ready APIv2 client that is as easy to use as possible**:
55
+ where the official SDK splits into a high-level interface that grew up around the
56
+ now-retired APIv1 and a lower-level, OpenAPI-generated `exoscale.api.v2.Client`,
57
+ this project gives *every* asset type the same uniform, pydantic-typed client
58
+ plus a matching per-asset CLI, talks only to APIv2, reads credentials only from
59
+ the environment, polls async operations to completion, backs every asset type
60
+ with a live test that has run end-to-end against a real account, depends on just
61
+ `requests` + `pydantic`, and can be vendored by copying one folder.
62
+
63
+ ## Install
64
+
65
+ ```bash
66
+ pip install -e ".[dev]" # from this folder, for development
67
+ # or, once published / vendored:
68
+ pip install exoscale-connector
69
+ ```
70
+
71
+ Object Storage (S3-compatible) support pulls in `boto3`:
72
+
73
+ ```bash
74
+ pip install "exoscale-connector[sos]"
75
+ ```
76
+
77
+ ## Quickstart (library)
78
+
79
+ ```python
80
+ from exoscale_connector import ExoscaleClient
81
+ from exoscale_connector.resources.security_group import (
82
+ SecurityGroupClient, SecurityGroupRule,
83
+ )
84
+
85
+ # Credentials from EXOSCALE_API_KEY / EXOSCALE_API_SECRET in the environment.
86
+ client = ExoscaleClient.from_env(zone="de-fra-1")
87
+ sg = SecurityGroupClient(client)
88
+
89
+ for group in sg.list():
90
+ print(group.id, group.name)
91
+
92
+ group = sg.create({"name": "web", "description": "public web tier"})
93
+ sg.add_rule(group.id, SecurityGroupRule(
94
+ flow_direction="ingress", protocol="tcp",
95
+ start_port=443, end_port=443, network="0.0.0.0/0",
96
+ ))
97
+ ```
98
+
99
+ ## Quickstart (CLI)
100
+
101
+ ```bash
102
+ export EXOSCALE_API_KEY=... EXOSCALE_API_SECRET=... EXOSCALE_ZONE=de-fra-1
103
+
104
+ exoscale-security-group list
105
+ exoscale-security-group get --id <uuid>
106
+ exoscale-security-group create --json '{"name": "web"}'
107
+ exoscale-security-group delete --id <uuid>
108
+ ```
109
+
110
+ > In practice, inject the credentials with your secret-management tooling rather
111
+ > than exporting them by hand. The connector only reads environment variables, so
112
+ > any injector works (HashiCorp Vault, Infisical, Doppler, …), e.g.
113
+ > `<vault-cli> run -- exoscale-security-group list`.
114
+
115
+ ## Documentation
116
+
117
+ - **[User / operator guide](docs/user-guide.md)** — installing, authenticating,
118
+ zones, and the common commands shared by every asset type.
119
+ - **[Asset type reference](docs/asset-types/README.md)** — one page per asset
120
+ type with model schema, CLI subcommands, library snippets, gotchas, and a
121
+ runnable end-to-end example backed by a passing live test.
122
+ - **[IAM policy cookbook](docs/iam-policy-cookbook.md)** — helper constructors
123
+ and copy-paste recipes for IAM role policies (the one area with real depth).
124
+ - **[Developer guide](docs/developer-guide.md)** — architecture, how to add a
125
+ new asset type, and the testing strategy.
126
+ - **[Live test plan](docs/live-test-plan.md)** — tiered per-asset live-test
127
+ design (safety rails, naming prefix, cleanup invariants, cost model).
128
+ - **[Live test results](docs/live-test-results.md)** — run log of every live
129
+ test executed against a real Exoscale tenant, plus the bugs each tier
130
+ surfaced and how they were fixed.
131
+
132
+ Every asset type the connector supports has a live test that has actually run
133
+ end-to-end against a real account; the gotchas in the asset-type pages are
134
+ empirical, not theoretical.
135
+
136
+ ## Maintenance & support
137
+
138
+ This is a personal project, maintained on a **best-effort, occasional basis** —
139
+ not full-time and not on a fixed schedule. It's shared because it may be useful
140
+ to others, not as a supported product. Issues and pull requests are welcome and
141
+ will be looked at when time allows, but there is **no guaranteed response time or
142
+ release cadence**. The API surface it tracks can drift; if you depend on it,
143
+ pin a version and don't hesitate to fork and adapt it — that's encouraged.
144
+
145
+ ## License
146
+
147
+ Released under the [MIT License](LICENSE) — free to use, modify, and
148
+ redistribute, including commercially. Provided **as-is, without warranty of any
149
+ kind**; use entirely at your own risk. The only condition is that the copyright
150
+ and permission notice are kept in copies.
@@ -0,0 +1,125 @@
1
+ # exoscale-connector
2
+
3
+ A clean, typed, reusable Python connector for the **Exoscale APIv2**. It talks to
4
+ the HTTP API directly — **no `exo` CLI and no Ansible required** — so it can be
5
+ dropped into any project that needs to read or manage Exoscale resources
6
+ programmatically.
7
+
8
+ - **Typed** — every request/response is a [pydantic](https://docs.pydantic.dev) v2
9
+ model, so you get validation and editor autocompletion.
10
+ - **One module per asset type** — `security-group`, `instance`, `elastic-ip`,
11
+ `dns`, `dbaas`, `sks`, … each with a small, uniform client.
12
+ - **Library + CLI** — import it, or use the per-asset command-line tools
13
+ (also namespaced under one `exoscale-connector` binary, with `--output table`).
14
+ - **Built for automation** — idempotent `ensure()` (get-or-create by name),
15
+ `wait_for_state` polling, and label-filtered listing make provisioning
16
+ scripts re-runnable by construction.
17
+ - **Self-contained** — runtime deps are just `requests` + `pydantic`; copy the
18
+ package into another repo and it keeps working.
19
+ - **Secret-safe** — credentials come only from the environment; nothing is
20
+ hardcoded or read from disk, and credentials are masked in `repr()`/log
21
+ output.
22
+
23
+ ## Relationship to the official Exoscale SDK
24
+
25
+ Exoscale publishes an official, actively maintained Python SDK —
26
+ [`python-exoscale`](https://github.com/exoscale/python-exoscale) (the `exoscale`
27
+ package on PyPI); if you want the vendor-supported, batteries-included bindings,
28
+ use that. This connector is a smaller, opinionated alternative built for one
29
+ goal — a **drop-in, IaC-ready APIv2 client that is as easy to use as possible**:
30
+ where the official SDK splits into a high-level interface that grew up around the
31
+ now-retired APIv1 and a lower-level, OpenAPI-generated `exoscale.api.v2.Client`,
32
+ this project gives *every* asset type the same uniform, pydantic-typed client
33
+ plus a matching per-asset CLI, talks only to APIv2, reads credentials only from
34
+ the environment, polls async operations to completion, backs every asset type
35
+ with a live test that has run end-to-end against a real account, depends on just
36
+ `requests` + `pydantic`, and can be vendored by copying one folder.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install -e ".[dev]" # from this folder, for development
42
+ # or, once published / vendored:
43
+ pip install exoscale-connector
44
+ ```
45
+
46
+ Object Storage (S3-compatible) support pulls in `boto3`:
47
+
48
+ ```bash
49
+ pip install "exoscale-connector[sos]"
50
+ ```
51
+
52
+ ## Quickstart (library)
53
+
54
+ ```python
55
+ from exoscale_connector import ExoscaleClient
56
+ from exoscale_connector.resources.security_group import (
57
+ SecurityGroupClient, SecurityGroupRule,
58
+ )
59
+
60
+ # Credentials from EXOSCALE_API_KEY / EXOSCALE_API_SECRET in the environment.
61
+ client = ExoscaleClient.from_env(zone="de-fra-1")
62
+ sg = SecurityGroupClient(client)
63
+
64
+ for group in sg.list():
65
+ print(group.id, group.name)
66
+
67
+ group = sg.create({"name": "web", "description": "public web tier"})
68
+ sg.add_rule(group.id, SecurityGroupRule(
69
+ flow_direction="ingress", protocol="tcp",
70
+ start_port=443, end_port=443, network="0.0.0.0/0",
71
+ ))
72
+ ```
73
+
74
+ ## Quickstart (CLI)
75
+
76
+ ```bash
77
+ export EXOSCALE_API_KEY=... EXOSCALE_API_SECRET=... EXOSCALE_ZONE=de-fra-1
78
+
79
+ exoscale-security-group list
80
+ exoscale-security-group get --id <uuid>
81
+ exoscale-security-group create --json '{"name": "web"}'
82
+ exoscale-security-group delete --id <uuid>
83
+ ```
84
+
85
+ > In practice, inject the credentials with your secret-management tooling rather
86
+ > than exporting them by hand. The connector only reads environment variables, so
87
+ > any injector works (HashiCorp Vault, Infisical, Doppler, …), e.g.
88
+ > `<vault-cli> run -- exoscale-security-group list`.
89
+
90
+ ## Documentation
91
+
92
+ - **[User / operator guide](docs/user-guide.md)** — installing, authenticating,
93
+ zones, and the common commands shared by every asset type.
94
+ - **[Asset type reference](docs/asset-types/README.md)** — one page per asset
95
+ type with model schema, CLI subcommands, library snippets, gotchas, and a
96
+ runnable end-to-end example backed by a passing live test.
97
+ - **[IAM policy cookbook](docs/iam-policy-cookbook.md)** — helper constructors
98
+ and copy-paste recipes for IAM role policies (the one area with real depth).
99
+ - **[Developer guide](docs/developer-guide.md)** — architecture, how to add a
100
+ new asset type, and the testing strategy.
101
+ - **[Live test plan](docs/live-test-plan.md)** — tiered per-asset live-test
102
+ design (safety rails, naming prefix, cleanup invariants, cost model).
103
+ - **[Live test results](docs/live-test-results.md)** — run log of every live
104
+ test executed against a real Exoscale tenant, plus the bugs each tier
105
+ surfaced and how they were fixed.
106
+
107
+ Every asset type the connector supports has a live test that has actually run
108
+ end-to-end against a real account; the gotchas in the asset-type pages are
109
+ empirical, not theoretical.
110
+
111
+ ## Maintenance & support
112
+
113
+ This is a personal project, maintained on a **best-effort, occasional basis** —
114
+ not full-time and not on a fixed schedule. It's shared because it may be useful
115
+ to others, not as a supported product. Issues and pull requests are welcome and
116
+ will be looked at when time allows, but there is **no guaranteed response time or
117
+ release cadence**. The API surface it tracks can drift; if you depend on it,
118
+ pin a version and don't hesitate to fork and adapt it — that's encouraged.
119
+
120
+ ## License
121
+
122
+ Released under the [MIT License](LICENSE) — free to use, modify, and
123
+ redistribute, including commercially. Provided **as-is, without warranty of any
124
+ kind**; use entirely at your own risk. The only condition is that the copyright
125
+ and permission notice are kept in copies.
@@ -0,0 +1,72 @@
1
+ # Asset type reference
2
+
3
+ One page per asset type the connector supports. Every page has the same six
4
+ sections — **Overview**, **Model**, **CLI**, **Library**, **Gotchas**,
5
+ **End-to-end example** — and is backed by a passing
6
+ [live test](../live-test-plan.md).
7
+
8
+ If something on a page contradicts the live behaviour, the live test is the
9
+ source of truth — open an issue and the page will be corrected.
10
+
11
+ ## Capability matrix
12
+
13
+ | Asset type | CLI binary | Live-tested | Tier |
14
+ |---|---|---|---|
15
+ | [security-group (+rules)](security-group.md) | `exoscale-security-group` | ✅ | 1 |
16
+ | [private-network](private-network.md) | `exoscale-private-network` | ✅ | 1 |
17
+ | [anti-affinity-group](anti-affinity-group.md) | `exoscale-anti-affinity-group` | ✅ | 1 |
18
+ | [ssh-key](ssh-key.md) | `exoscale-ssh-key` | ✅ | 1 |
19
+ | [iam-role](iam-role.md) | `exoscale-iam-role` | ✅ | 1 |
20
+ | [iam-user](iam-user.md) | `exoscale-iam-user` | read-only | — |
21
+ | [api-key](api-key.md) | `exoscale-api-key` | ✅ (gated) | 1 (opt-in, `EXOSCALE_TEST_TIER_1_API_KEY=1`) |
22
+ | [dns (domain + records)](dns.md) | `exoscale-dns` | ✅ | 1 |
23
+ | [elastic-ip](elastic-ip.md) | `exoscale-elastic-ip` | ✅ | 2 |
24
+ | [object-storage bucket](object-storage.md) | `exoscale-bucket` | ✅ | 2 |
25
+ | [block-volume](block-volume.md) | `exoscale-block-volume` | ✅ create/snapshot/delete (Tier 2); attach/detach (Tier 3); resize endpoint+method verified, size-change assertion self-skips on tenant quota | 2/3 |
26
+ | [block-volume-snapshot](block-volume-snapshot.md) | `exoscale-block-volume-snapshot` | ✅ | 2 |
27
+ | [instance (+lifecycle)](instance.md) | `exoscale-instance` | ✅ | 3 |
28
+ | [instance-pool (+scale)](instance-pool.md) | `exoscale-instance-pool` | ✅ | 3 |
29
+ | [snapshot (compute)](snapshot.md) | `exoscale-snapshot` | ✅ create/list/get/export/delete | 3 |
30
+ | [load-balancer (+services)](load-balancer.md) | `exoscale-load-balancer` | ✅ | 4 |
31
+ | [dbaas](dbaas.md) | `exoscale-dbaas` | ✅ | 4 |
32
+ | [sks (cluster + nodepool)](sks.md) | `exoscale-sks` | ✅ | 4 |
33
+ | [zone](zone.md) | `exoscale-zone` | pending (smoke test added) | 0 |
34
+ | [template](template.md) | `exoscale-template` | pending (smoke test added) | 0 |
35
+ | [instance-type](instance-type.md) | `exoscale-instance-type` | pending (smoke test added) | 0 |
36
+
37
+ Instance scale, reverse DNS, SOS objects, DBaaS users/update, and `ensure()`
38
+ were all live-verified in the 2026-06-10 extensions validation run (see
39
+ [live-test-results.md](../live-test-results.md)). Three spec-vs-reality
40
+ divergences were found and fixed during that run.
41
+
42
+ ## Page template
43
+
44
+ ```
45
+ # <asset-type>
46
+ Overview — one paragraph.
47
+ ## Model
48
+ Field table from the pydantic model.
49
+ ## CLI
50
+ Every subcommand with a copy-pasteable example invocation.
51
+ ## Library
52
+ Python snippet for each operation.
53
+ ## Gotchas
54
+ Caveats verified by the live test (e.g. unit-of-measure quirks,
55
+ required-but-undocumented fields, quota constraints).
56
+ ## End-to-end example
57
+ The full lifecycle distilled from the corresponding live test.
58
+ ```
59
+
60
+ ## Conventions used on every page
61
+
62
+ - **Authentication** is always env-based: `EXOSCALE_API_KEY` /
63
+ `EXOSCALE_API_SECRET` / `EXOSCALE_ZONE`. Inject with your secret manager
64
+ (HashiCorp Vault, Infisical, Doppler, …); the connector reads only env vars.
65
+ - **JSON output** from CLIs goes to stdout; errors to stderr; exit 0 on
66
+ success, 1 on API/connector error, 2 on argparse error.
67
+ - **All resources** are pydantic v2 models with snake_case attributes that
68
+ auto-map to the API's kebab-case JSON keys (e.g. `flow_direction` ↔
69
+ `flow-direction`). Unknown server fields pass through (`extra="allow"`),
70
+ so the connector keeps working when the API adds fields ahead of the model.
71
+ - **Async operations** are awaited by default — pass `wait=False` to return
72
+ the operation object without polling.
@@ -0,0 +1,65 @@
1
+ # anti-affinity-group
2
+
3
+ A scheduling hint telling Exoscale's placement engine that the instances
4
+ assigned to this group should be spread across distinct physical hosts. Used
5
+ to maximise availability for replica sets (e.g. a 3-node etcd cluster, or an
6
+ HA database).
7
+
8
+ ## Model
9
+
10
+ ```python
11
+ class AntiAffinityGroup(ExoscaleModel):
12
+ id: Optional[str]
13
+ name: Optional[str]
14
+ description: Optional[str]
15
+ instances: Optional[List[Reference]] # members; populated on detail responses
16
+ ```
17
+
18
+ ## CLI
19
+
20
+ ```bash
21
+ exoscale-anti-affinity-group list
22
+ exoscale-anti-affinity-group get --id <uuid>
23
+ exoscale-anti-affinity-group find --name <name>
24
+ exoscale-anti-affinity-group create --json '{"name": "etcd-aag", "description": "etcd replica anti-affinity"}'
25
+ exoscale-anti-affinity-group delete --id <uuid>
26
+ ```
27
+
28
+ ## Library
29
+
30
+ ```python
31
+ from exoscale_connector import ExoscaleClient
32
+ from exoscale_connector.resources.anti_affinity_group import AntiAffinityGroupClient
33
+
34
+ aag = AntiAffinityGroupClient(ExoscaleClient.from_env(zone="de-fra-1"))
35
+
36
+ group = aag.create({"name": "etcd-aag", "description": "etcd anti-affinity"})
37
+ fetched = aag.get(group.id)
38
+ aag.delete(group.id)
39
+ ```
40
+
41
+ ## Gotchas
42
+
43
+ - **No `update` endpoint.** The API does not support modifying an AAG in
44
+ place — `update()` is intentionally not exposed on this client. To change
45
+ anything, delete and recreate.
46
+ - **Members are read-only here.** Instances are assigned via the
47
+ *instance* create/update endpoint by including the AAG id in the
48
+ instance's `anti-affinity-groups` array.
49
+
50
+ ## End-to-end example
51
+
52
+ Distilled from
53
+ [`tests/integration/test_tier_1.py::test_anti_affinity_group_lifecycle`](../../tests/integration/test_tier_1.py):
54
+
55
+ ```python
56
+ from exoscale_connector import ExoscaleClient
57
+ from exoscale_connector.resources.anti_affinity_group import AntiAffinityGroupClient
58
+
59
+ aag = AntiAffinityGroupClient(ExoscaleClient.from_env(zone="de-fra-1"))
60
+
61
+ group = aag.create({"name": "etcd-aag", "description": "tier-1 smoke"})
62
+ assert aag.get(group.id).name == "etcd-aag"
63
+ assert aag.find_by_name("etcd-aag").id == group.id
64
+ aag.delete(group.id)
65
+ ```
@@ -0,0 +1,88 @@
1
+ # api-key
2
+
3
+ A scoped credential bound to an IAM role at creation time. The role
4
+ determines what the key can do; the key's name/identifier on the wire is
5
+ its **key id** (an API-generated string), and the **secret is returned
6
+ exactly once** on the create response — there is no way to re-fetch it.
7
+
8
+ ## Model
9
+
10
+ ```python
11
+ class ApiKey(ExoscaleModel):
12
+ key: Optional[str] # API key id (the public part, used in URL paths)
13
+ name: Optional[str]
14
+ role_id: Optional[str] # bound role's id
15
+ role: Optional[Reference]
16
+ secret: Optional[str] # PRESENT ONLY ON CREATE — never returned later
17
+ ```
18
+
19
+ ## CLI
20
+
21
+ ```bash
22
+ exoscale-api-key list
23
+ exoscale-api-key get --id <key-id>
24
+ exoscale-api-key find --name <name>
25
+ exoscale-api-key create --json '{"name": "ci-bot", "role-id": "<role-uuid>"}'
26
+ exoscale-api-key delete --id <key-id>
27
+ ```
28
+
29
+ ## Library
30
+
31
+ ```python
32
+ from exoscale_connector import ExoscaleClient
33
+ from exoscale_connector.resources.api_key import ApiKeyClient
34
+
35
+ keys = ApiKeyClient(ExoscaleClient.from_env(zone="de-fra-1"))
36
+
37
+ created = keys.create({"name": "ci-bot", "role-id": "<role-uuid>"})
38
+ # Capture the secret IMMEDIATELY — it is not retrievable later.
39
+ secret = created.secret
40
+ key_id = created.key
41
+
42
+ # Listing / fetching never returns the secret.
43
+ for existing in keys.list():
44
+ print(existing.key, existing.name)
45
+
46
+ keys.delete(key_id)
47
+ ```
48
+
49
+ ## Gotchas
50
+
51
+ - **`secret` is one-shot.** The first response after `create` is the only
52
+ time the secret is visible. Persist it to a vault immediately; never log it.
53
+ As a guard, `repr()` of an `ApiKey` masks the secret — but it is still
54
+ present in `model_dump()` and in the CLI's `create` JSON output (that is
55
+ your one chance to capture it).
56
+ - **`id_field = "key"`** — the path token is the `key` field, not a uuid in
57
+ `id`. `keys.get("EXO...")` calls `GET /api-key/EXO...`.
58
+ - **`update` not exposed.** The API does not allow rotating a key in place —
59
+ delete and create a new one.
60
+ - **Tier 1 live test for create is gated separately** (off by default) so
61
+ the standard Tier 1 run never produces a stray secret-bearing response.
62
+ Enable with `EXOSCALE_TEST_TIER_1_API_KEY=1`.
63
+
64
+ ## End-to-end example
65
+
66
+ ```python
67
+ from exoscale_connector import ExoscaleClient
68
+ from exoscale_connector.resources.api_key import ApiKeyClient
69
+ from exoscale_connector.resources.iam_role import IAMPolicy, IAMRole, IAMRoleClient
70
+
71
+ client = ExoscaleClient.from_env(zone="de-fra-1")
72
+ roles = IAMRoleClient(client)
73
+ keys = ApiKeyClient(client)
74
+
75
+ # Bind to a minimal role created just for this key.
76
+ role = roles.create(IAMRole(
77
+ name="ci-bot-role",
78
+ policy=IAMPolicy(default_service_strategy="deny", services={}),
79
+ ))
80
+
81
+ created = keys.create({"name": "ci-bot", "role-id": role.id})
82
+ assert created.secret, "secret must be returned exactly once"
83
+ # Hand `created.key` / `created.secret` to your vault here.
84
+
85
+ # Teardown
86
+ keys.delete(created.key)
87
+ roles.delete(role.id)
88
+ ```