python-rundeck 0.1.0__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,79 @@
1
+ """
2
+ Gestion du stockage des clés (/storage/keys).
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any
8
+
9
+ from rundeck.base import RundeckObject, RundeckObjectManager
10
+
11
+
12
+ class StorageKey(RundeckObject):
13
+ """Ressource de stockage (fichier ou dossier)."""
14
+
15
+ _id_attr = "path"
16
+ _repr_attr = "name"
17
+
18
+
19
+ class StorageKeyManager(RundeckObjectManager[StorageKey]):
20
+ """Manager pour les clés/ressources du storage."""
21
+
22
+ _path = "/storage/keys"
23
+ _obj_cls = StorageKey
24
+
25
+ def _dir_path(self, path: str | None = None) -> str:
26
+ base = self._build_path()
27
+ if path:
28
+ cleaned = path.strip("/")
29
+ return f"{base}/{cleaned}/"
30
+ return base
31
+
32
+ def _file_path(self, path: str) -> str:
33
+ base = self._build_path()
34
+ cleaned = path.strip("/")
35
+ return f"{base}/{cleaned}"
36
+
37
+ def list(self, path: str | None = None) -> list[StorageKey]:
38
+ """Liste les ressources (fichiers/dossiers) sous le chemin donné."""
39
+ target = self._dir_path(path)
40
+ result = self.rd.http_get(target)
41
+ resources = []
42
+ if isinstance(result, dict):
43
+ resources = result.get("resources") or []
44
+ return self._wrap_list(resources)
45
+
46
+ def get_metadata(self, path: str) -> StorageKey:
47
+ """Récupère les métadonnées d'une clé/fichier."""
48
+ target = self._file_path(path)
49
+ result = self.rd.http_get(target)
50
+ if isinstance(result, dict):
51
+ return self._wrap(result)
52
+ return self._wrap({"path": path, "raw": result})
53
+
54
+ def get_content(self, path: str, accept: str = "application/pgp-keys") -> bytes:
55
+ """Récupère le contenu d'une clé (ex: clé publique)."""
56
+ target = self._file_path(path)
57
+ headers = {"Accept": accept} if accept else None
58
+ response = self.rd.http_get(target, headers=headers, raw=True)
59
+ return response.content
60
+
61
+ def create(self, path: str, content: bytes | str, content_type: str) -> Any:
62
+ """Crée une nouvelle clé/fichier à l'emplacement donné."""
63
+ target = self._file_path(path)
64
+ headers = {"Content-Type": content_type, "Accept": "application/json"}
65
+ return self.rd.http_post(target, data=content, headers=headers)
66
+
67
+ def update(self, path: str, content: bytes | str, content_type: str) -> Any:
68
+ """Met à jour une clé/fichier existant."""
69
+ target = self._file_path(path)
70
+ headers = {"Content-Type": content_type, "Accept": "application/json"}
71
+ return self.rd.http_put(target, data=content, headers=headers)
72
+
73
+ def delete(self, path: str) -> None:
74
+ """Supprime une clé/fichier."""
75
+ target = self._file_path(path)
76
+ self.rd.http_delete(target)
77
+
78
+
79
+ __all__ = ["StorageKey", "StorageKeyManager"]
@@ -0,0 +1,37 @@
1
+ """
2
+ Gestion des métriques Rundeck
3
+ """
4
+
5
+ from typing import Any
6
+
7
+ from rundeck.base import RundeckObjectManager
8
+
9
+
10
+ class MetricsManager(RundeckObjectManager):
11
+ """Manager pour les endpoints de métriques."""
12
+
13
+ _path = "/metrics"
14
+
15
+ def list(self) -> dict[str, Any]:
16
+ """Liste les endpoints de métriques disponibles."""
17
+ path = self._build_path()
18
+ return self.rd.http_get(path)
19
+
20
+ def data(self) -> dict[str, Any]:
21
+ """Récupère les données de métriques."""
22
+ path = self._build_path("metrics")
23
+ return self.rd.http_get(path)
24
+
25
+ def healthcheck(self) -> dict[str, Any]:
26
+ """Récupère les résultats des health checks."""
27
+ path = self._build_path("healthcheck")
28
+ return self.rd.http_get(path)
29
+
30
+ def ping(self) -> str:
31
+ """Ping du serveur via l'endpoint métriques."""
32
+ path = self._build_path("ping")
33
+ response = self.rd.http_get(path, raw=True)
34
+ return response.text
35
+
36
+
37
+ __all__ = ["MetricsManager"]
@@ -0,0 +1,38 @@
1
+ """
2
+ Gestion des plugins installés (/plugin/list).
3
+ """
4
+
5
+ from rundeck.base import RundeckObject, RundeckObjectManager
6
+
7
+
8
+ class Plugin(RundeckObject):
9
+ """Représente un plugin Rundeck installé."""
10
+
11
+ _id_attr = "id"
12
+ _repr_attr = "name"
13
+
14
+
15
+ class PluginManager(RundeckObjectManager[Plugin]):
16
+ """Manager pour lister les plugins installés."""
17
+
18
+ _path = "/plugin"
19
+ _obj_cls = Plugin
20
+
21
+ def list(self) -> list[Plugin]:
22
+ """Liste les plugins installés sur l'instance."""
23
+ path = self._build_path("list")
24
+ return self._list(path=path)
25
+
26
+ def detail(self, service: str, provider: str) -> Plugin:
27
+ """Détails d'un plugin spécifique (service/provider)."""
28
+ path = self._build_path(f"detail/{service}/{provider}")
29
+ result = self.rd.http_get(path)
30
+ # Certains retours ne contiennent pas le champ service ; on le re-injecte
31
+ # pour cohérence.
32
+ if isinstance(result, dict):
33
+ result.setdefault("service", service)
34
+ result.setdefault("provider", provider)
35
+ return self._wrap(result)
36
+
37
+
38
+ __all__ = ["Plugin", "PluginManager"]
@@ -0,0 +1,421 @@
1
+ """
2
+ Gestion des projets Rundeck
3
+ """
4
+
5
+ from typing import Any
6
+
7
+ from rundeck.base import RundeckObject, RundeckObjectManager
8
+ from rundeck.v1.objects.adhoc import AdhocManager
9
+ from rundeck.v1.objects.executions import ExecutionManager
10
+ from rundeck.v1.objects.jobs import JobManager
11
+ from rundeck.v1.objects.scm import ProjectSCMManager
12
+ from rundeck.v1.objects.webhooks import ProjectWebhookManager
13
+
14
+
15
+ class Project(RundeckObject):
16
+ """Représente un projet Rundeck."""
17
+
18
+ _id_attr = "name"
19
+ _repr_attr = "name"
20
+
21
+ @property
22
+ def jobs(self) -> JobManager:
23
+ """Accès aux jobs du projet via un manager parenté."""
24
+ return JobManager(self.rd, parent=self)
25
+
26
+ @property
27
+ def executions(self) -> ExecutionManager:
28
+ """Accès aux exécutions du projet via un manager parenté."""
29
+ return ExecutionManager(self.rd, parent=self)
30
+
31
+ @property
32
+ def config(self) -> "ProjectConfigManager":
33
+ """Accès à la configuration du projet (clé/valeur)."""
34
+ return ProjectConfigManager(self.rd, parent=self)
35
+
36
+ @property
37
+ def resources(self) -> "ProjectResourcesManager":
38
+ """Accès aux ressources/nodes du projet."""
39
+ return ProjectResourcesManager(self.rd, parent=self)
40
+
41
+ @property
42
+ def sources(self) -> "ProjectSourcesManager":
43
+ """Accès aux sources de ressources du projet."""
44
+ return ProjectSourcesManager(self.rd, parent=self)
45
+
46
+ @property
47
+ def acl(self) -> "ProjectACLManager":
48
+ """Accès aux ACL du projet."""
49
+ return ProjectACLManager(self.rd, parent=self)
50
+
51
+ @property
52
+ def archive(self) -> "ProjectArchiveManager":
53
+ """Accès aux exports/imports d'archive projet."""
54
+ return ProjectArchiveManager(self.rd, parent=self)
55
+
56
+ @property
57
+ def readme(self) -> "ProjectReadmeManager":
58
+ """Accès aux fichiers readme/motd du projet."""
59
+ return ProjectReadmeManager(self.rd, parent=self)
60
+
61
+ @property
62
+ def scm(self) -> ProjectSCMManager:
63
+ """Accès aux opérations SCM du projet (import/export)."""
64
+ return ProjectSCMManager(self.rd, parent=self)
65
+
66
+ @property
67
+ def webhooks(self) -> ProjectWebhookManager:
68
+ """Accès aux webhooks du projet."""
69
+ return ProjectWebhookManager(self.rd, parent=self)
70
+
71
+ @property
72
+ def adhoc(self) -> AdhocManager:
73
+ """Accès aux commandes/scripts AdHoc du projet."""
74
+ return AdhocManager(self.rd, parent=self)
75
+
76
+
77
+ class ProjectManager(RundeckObjectManager[Project]):
78
+ """Manager pour les projets."""
79
+
80
+ _path = "/projects"
81
+ _obj_cls = Project
82
+
83
+ def list(self) -> list[Project]:
84
+ """Liste tous les projets."""
85
+ return self._list()
86
+
87
+ def get(self, name: str) -> Project:
88
+ """Récupère un projet par son nom."""
89
+ path = f"/project/{name}"
90
+ return self._get(name, path=path)
91
+
92
+ def create(self, name: str, config: dict[str, Any] | None = None) -> Project:
93
+ """Crée un projet (optionnellement avec configuration)."""
94
+ payload: dict[str, Any] = {"name": name}
95
+ if config:
96
+ payload["config"] = config
97
+ path = self._build_path()
98
+ return self._create(json=payload, path=path)
99
+
100
+ def delete(self, name: str) -> None:
101
+ """Supprime un projet par son nom."""
102
+ path = f"/project/{name}"
103
+ self._delete(name, path=path)
104
+
105
+
106
+ class ProjectConfig(RundeckObject):
107
+ """Placeholder, non utilisé pour wrapping (retours dict)."""
108
+
109
+
110
+ class ProjectConfigManager(RundeckObjectManager[ProjectConfig]):
111
+ """Manager pour la configuration d'un projet (clé/valeur)."""
112
+
113
+ _path = "/project/{parent}/config"
114
+ _obj_cls = ProjectConfig
115
+
116
+ @property
117
+ def keys(self) -> "ProjectConfigKeysManager":
118
+ """Opérations clé/valeur (GET/PUT/POST/DELETE)."""
119
+ return ProjectConfigKeysManager(self.rd, parent=self.parent)
120
+
121
+ def get(self) -> dict[str, str]:
122
+ """Récupère toute la configuration du projet parent."""
123
+ path = self._build_path()
124
+ return self.rd.http_get(path)
125
+
126
+ def replace(self, config: dict[str, str]) -> dict[str, str]:
127
+ """Remplace toute la configuration du projet."""
128
+ path = self._build_path()
129
+ return self.rd.http_put(path, json=config)
130
+
131
+
132
+ class ProjectConfigKeysManager(RundeckObjectManager[ProjectConfig]):
133
+ """Opérations par clé sur la configuration."""
134
+
135
+ _path = "/project/{parent}/config"
136
+ _obj_cls = ProjectConfig
137
+
138
+ def get(self, key: str) -> dict[str, str]:
139
+ """Récupère une propriété de configuration."""
140
+ path = self._build_path(key)
141
+ return self.rd.http_get(path)
142
+
143
+ def set(self, key: str, value: str) -> dict[str, str]:
144
+ """Définit une propriété de configuration."""
145
+ path = self._build_path(key)
146
+ return self.rd.http_put(path, json={"value": value})
147
+
148
+ def update(self, config: dict[str, str]) -> dict[str, str]:
149
+ """Met à jour plusieurs propriétés en une fois."""
150
+ path = self._build_path()
151
+ return self.rd.http_post(path, json=config)
152
+
153
+ def delete(self, key: str) -> None:
154
+ """Supprime une propriété de configuration."""
155
+ path = self._build_path(key)
156
+ self.rd.http_delete(path)
157
+
158
+
159
+ class ProjectACLManager(RundeckObjectManager):
160
+ """Manager pour les ACL d'un projet (/project/{parent}/acl)."""
161
+
162
+ _path = "/project/{parent}/acl"
163
+
164
+ def list(self) -> dict[str, Any]:
165
+ """Liste les politiques ACL du projet."""
166
+ path = self._build_path("")
167
+ return self.rd.http_get(path)
168
+
169
+ def get(self, policy_name: str) -> str:
170
+ """Récupère une politique ACL projet (contenu texte)."""
171
+ path = self._build_path(policy_name)
172
+ response = self.rd.http_get(path, raw=True)
173
+ return response.text
174
+
175
+ def create(self, policy_name: str, content: str) -> dict[str, Any]:
176
+ """Crée une politique ACL projet."""
177
+ path = self._build_path(policy_name)
178
+ headers = {"Content-Type": "application/yaml"}
179
+ return self.rd.http_post(path, data=content, headers=headers)
180
+
181
+ def update(self, policy_name: str, content: str) -> dict[str, Any]:
182
+ """Met à jour une politique ACL projet."""
183
+ path = self._build_path(policy_name)
184
+ headers = {"Content-Type": "application/yaml"}
185
+ return self.rd.http_put(path, data=content, headers=headers)
186
+
187
+ def delete(self, policy_name: str) -> None:
188
+ """Supprime une politique ACL projet."""
189
+ path = self._build_path(policy_name)
190
+ self.rd.http_delete(path)
191
+
192
+
193
+ class ProjectResourcesManager(RundeckObjectManager):
194
+ """Manager pour les ressources (nodes) d'un projet."""
195
+
196
+ _path = "/project/{parent}/resources"
197
+ _obj_cls = RundeckObject # non utilisé, on retourne les données brutes
198
+
199
+ def list(self, format: str | None = "json", **filters: Any) -> Any:
200
+ """Liste les ressources du projet (avec filtres de nodes éventuels)."""
201
+ params: dict[str, Any] = {}
202
+ if format:
203
+ params["format"] = format
204
+ params.update(filters)
205
+ path = self._build_path()
206
+ return self.rd.http_get(path, params=params or None)
207
+
208
+ def get(self, name: str, format: str | None = "json") -> Any:
209
+ """Récupère une ressource spécifique par son nom."""
210
+ params: dict[str, Any] = {"format": format} if format else None
211
+ path = (
212
+ f"/project/{self.parent.id}/resource/{name}"
213
+ if self.parent
214
+ else f"/resource/{name}"
215
+ )
216
+ return self.rd.http_get(path, params=params)
217
+
218
+
219
+ class ProjectSourcesManager(RundeckObjectManager):
220
+ """Manager pour les sources de ressources d'un projet."""
221
+
222
+ _path = "/project/{parent}/sources"
223
+ _obj_cls = RundeckObject # on retourne les dicts tels quels
224
+
225
+ def list(self) -> Any:
226
+ """Liste les sources de ressources du projet."""
227
+ path = self._build_path()
228
+ return self.rd.http_get(path)
229
+
230
+ def get(self, index: int) -> Any:
231
+ """Récupère une source spécifique par son index."""
232
+ path = self._build_path(f"{index}")
233
+ return self.rd.http_get(path)
234
+
235
+ def list_resources(self, index: int, accept: str | None = None) -> Any:
236
+ """Liste les ressources d'une source donnée."""
237
+ path = self._build_path(f"{index}/resources")
238
+ headers = {"Accept": accept} if accept else None
239
+ return self.rd.http_get(path, headers=headers)
240
+
241
+ def update_resources(
242
+ self,
243
+ index: int,
244
+ content: Any,
245
+ content_type: str = "application/json",
246
+ accept: str | None = None,
247
+ ) -> Any:
248
+ """Met à jour les ressources d'une source via POST."""
249
+ path = self._build_path(f"{index}/resources")
250
+ headers: dict[str, str] = {"Content-Type": content_type}
251
+ if accept:
252
+ headers["Accept"] = accept
253
+ return self.rd.http_post(path, data=content, headers=headers)
254
+
255
+
256
+ class ProjectReadmeManager(RundeckObjectManager):
257
+ """Gestion des fichiers readme.md / motd.md d'un projet."""
258
+
259
+ _path = "/project/{parent}"
260
+
261
+ def _get_file(self, filename: str, accept: str | None = "text/plain") -> Any:
262
+ headers = {"Accept": accept} if accept else None
263
+ path = self._build_path(filename)
264
+ return self.rd.http_get(path, headers=headers)
265
+
266
+ def _put_file(
267
+ self, filename: str, content: Any, content_type: str = "text/plain"
268
+ ) -> Any:
269
+ path = self._build_path(filename)
270
+ headers = {"Content-Type": content_type}
271
+ if content_type == "application/json" and isinstance(content, str):
272
+ payload = {"contents": content}
273
+ return self.rd.http_put(path, json=payload, headers=headers)
274
+ return self.rd.http_put(path, data=content, headers=headers)
275
+
276
+ def _delete_file(self, filename: str) -> None:
277
+ path = self._build_path(filename)
278
+ self.rd.http_delete(path)
279
+
280
+ def get_readme(self, accept: str | None = "text/plain") -> Any:
281
+ """Récupère le readme du projet (texte ou JSON selon l'Accept)."""
282
+ return self._get_file("readme.md", accept=accept)
283
+
284
+ def update_readme(self, content: Any, content_type: str = "text/plain") -> Any:
285
+ """Crée ou met à jour le readme du projet."""
286
+ return self._put_file("readme.md", content=content, content_type=content_type)
287
+
288
+ def delete_readme(self) -> None:
289
+ """Supprime le readme du projet."""
290
+ self._delete_file("readme.md")
291
+
292
+ def get_motd(self, accept: str | None = "text/plain") -> Any:
293
+ """Récupère le motd (Message of the Day) du projet."""
294
+ return self._get_file("motd.md", accept=accept)
295
+
296
+ def update_motd(self, content: Any, content_type: str = "text/plain") -> Any:
297
+ """Crée ou met à jour le motd du projet."""
298
+ return self._put_file("motd.md", content=content, content_type=content_type)
299
+
300
+ def delete_motd(self) -> None:
301
+ """Supprime le motd du projet."""
302
+ self._delete_file("motd.md")
303
+
304
+
305
+ class ProjectArchiveManager(RundeckObjectManager):
306
+ """Manager pour l'export/import d'un projet complet."""
307
+
308
+ _path = "/project/{parent}"
309
+
310
+ def export(
311
+ self,
312
+ execution_ids: list[str] | str | None = None,
313
+ export_all: bool | None = None,
314
+ export_jobs: bool | None = None,
315
+ export_executions: bool | None = None,
316
+ export_configs: bool | None = None,
317
+ export_readmes: bool | None = None,
318
+ export_acls: bool | None = None,
319
+ export_scm: bool | None = None,
320
+ export_webhooks: bool | None = None,
321
+ whkIncludeAuthTokens: bool | None = None,
322
+ **components: Any,
323
+ ) -> Any:
324
+ """
325
+ Exporte une archive ZIP du projet (synchrone).
326
+
327
+ Retourne la réponse brute (zip) pour laisser l'appelant gérer le flux.
328
+ """
329
+ params: dict[str, Any] = {}
330
+ if execution_ids:
331
+ if isinstance(execution_ids, list):
332
+ params["executionIds"] = ",".join(execution_ids)
333
+ else:
334
+ params["executionIds"] = execution_ids
335
+ if export_all is not None:
336
+ params["exportAll"] = export_all
337
+ if export_jobs is not None:
338
+ params["exportJobs"] = export_jobs
339
+ if export_executions is not None:
340
+ params["exportExecutions"] = export_executions
341
+ if export_configs is not None:
342
+ params["exportConfigs"] = export_configs
343
+ if export_readmes is not None:
344
+ params["exportReadmes"] = export_readmes
345
+ if export_acls is not None:
346
+ params["exportAcls"] = export_acls
347
+ if export_scm is not None:
348
+ params["exportScm"] = export_scm
349
+ if export_webhooks is not None:
350
+ params["exportWebhooks"] = export_webhooks
351
+ if whkIncludeAuthTokens is not None:
352
+ params["whkIncludeAuthTokens"] = whkIncludeAuthTokens
353
+ if components:
354
+ params.update(components)
355
+ path = self._build_path("export")
356
+ return self.rd.http_get(path, params=params or None, raw=True)
357
+
358
+ def export_async(self, **params: Any) -> Any:
359
+ """Lance une export async, retourne le token."""
360
+ path = self._build_path("export/async")
361
+ return self.rd.http_get(path, params=params or None)
362
+
363
+ def export_status(self, token: str) -> Any:
364
+ """Vérifie le statut d'un export async."""
365
+ path = self._build_path(f"export/status/{token}")
366
+ return self.rd.http_get(path)
367
+
368
+ def export_download(self, token: str) -> Any:
369
+ """Télécharge l'archive d'un export async prêt."""
370
+ path = self._build_path(f"export/download/{token}")
371
+ return self.rd.http_get(path, raw=True)
372
+
373
+ def import_archive(
374
+ self,
375
+ content: Any,
376
+ async_import: bool | None = None,
377
+ jobUuidOption: str | None = None,
378
+ importExecutions: bool | None = None,
379
+ importConfig: bool | None = None,
380
+ importACL: bool | None = None,
381
+ importScm: bool | None = None,
382
+ importWebhooks: bool | None = None,
383
+ whkRegenAuthTokens: bool | None = None,
384
+ importNodesSources: bool | None = None,
385
+ **component_options: Any,
386
+ ) -> Any:
387
+ """
388
+ Importe une archive ZIP dans le projet.
389
+ """
390
+ params: dict[str, Any] = {}
391
+ if async_import is not None:
392
+ params["asyncImport"] = async_import
393
+ if jobUuidOption:
394
+ params["jobUuidOption"] = jobUuidOption
395
+ if importExecutions is not None:
396
+ params["importExecutions"] = importExecutions
397
+ if importConfig is not None:
398
+ params["importConfig"] = importConfig
399
+ if importACL is not None:
400
+ params["importACL"] = importACL
401
+ if importScm is not None:
402
+ params["importScm"] = importScm
403
+ if importWebhooks is not None:
404
+ params["importWebhooks"] = importWebhooks
405
+ if whkRegenAuthTokens is not None:
406
+ params["whkRegenAuthTokens"] = whkRegenAuthTokens
407
+ if importNodesSources is not None:
408
+ params["importNodesSources"] = importNodesSources
409
+ if component_options:
410
+ params.update(component_options)
411
+
412
+ path = self._build_path("import")
413
+ headers = {"Content-Type": "application/zip"}
414
+ return self.rd.http_put(
415
+ path, params=params or None, data=content, headers=headers
416
+ )
417
+
418
+ def import_status(self) -> Any:
419
+ """Statut d'un import asynchrone."""
420
+ path = self._build_path("import/status")
421
+ return self.rd.http_get(path)
@@ -0,0 +1,50 @@
1
+ """
2
+ Gestion des endpoints /scheduler
3
+ """
4
+
5
+ from typing import Any
6
+
7
+ from rundeck.base import RundeckObjectManager
8
+
9
+
10
+ class SchedulerManager(RundeckObjectManager):
11
+ """Manager pour les opérations du scheduler (/scheduler/*)."""
12
+
13
+ _path = "/scheduler"
14
+
15
+ def takeover(
16
+ self,
17
+ server_uuid: str | None = None,
18
+ all_servers: bool = False,
19
+ project: str | None = None,
20
+ job_id: str | None = None,
21
+ ) -> dict[str, Any]:
22
+ """
23
+ Reprend le planning des jobs en mode cluster.
24
+
25
+ Args:
26
+ server_uuid: UUID du serveur cible.
27
+ all_servers: True pour reprendre tous les serveurs.
28
+ project: Projet concerné (optionnel).
29
+ job_id: Job spécifique (optionnel).
30
+
31
+ Returns:
32
+ Résultat de la reprise.
33
+ """
34
+ path = self._build_path("takeover")
35
+
36
+ data: dict[str, Any] = {}
37
+ if all_servers:
38
+ data["server"] = {"all": True}
39
+ elif server_uuid:
40
+ data["server"] = {"uuid": server_uuid}
41
+
42
+ if project:
43
+ data["project"] = project
44
+ if job_id:
45
+ data["job"] = {"id": job_id}
46
+
47
+ return self.rd.http_put(path, json=data)
48
+
49
+
50
+ __all__ = ["SchedulerManager"]