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.
- python_rundeck-0.1.0.dist-info/METADATA +454 -0
- python_rundeck-0.1.0.dist-info/RECORD +28 -0
- python_rundeck-0.1.0.dist-info/WHEEL +4 -0
- python_rundeck-0.1.0.dist-info/licenses/LICENSE +21 -0
- rundeck/__init__.py +39 -0
- rundeck/base.py +231 -0
- rundeck/client.py +341 -0
- rundeck/config.py +166 -0
- rundeck/const.py +45 -0
- rundeck/exceptions.py +164 -0
- rundeck/py.typed +1 -0
- rundeck/v1/__init__.py +1 -0
- rundeck/v1/objects/__init__.py +1 -0
- rundeck/v1/objects/adhoc.py +93 -0
- rundeck/v1/objects/config_management.py +51 -0
- rundeck/v1/objects/executions.py +327 -0
- rundeck/v1/objects/features.py +44 -0
- rundeck/v1/objects/jobs.py +329 -0
- rundeck/v1/objects/key_storage.py +79 -0
- rundeck/v1/objects/metrics.py +37 -0
- rundeck/v1/objects/plugins.py +38 -0
- rundeck/v1/objects/projects.py +421 -0
- rundeck/v1/objects/scheduler.py +50 -0
- rundeck/v1/objects/scm.py +229 -0
- rundeck/v1/objects/system.py +140 -0
- rundeck/v1/objects/tokens.py +102 -0
- rundeck/v1/objects/users.py +137 -0
- rundeck/v1/objects/webhooks.py +104 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gestion des exécutions Rundeck
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from rundeck.base import RundeckObject, RundeckObjectManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Execution(RundeckObject):
|
|
11
|
+
"""Représente une exécution Rundeck"""
|
|
12
|
+
|
|
13
|
+
_id_attr = "id"
|
|
14
|
+
|
|
15
|
+
def abort(self, asUser: str | None = None) -> dict[str, Any]:
|
|
16
|
+
"""
|
|
17
|
+
Annule l'exécution
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
asUser: Annuler en tant qu'utilisateur
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Statut de l'annulation
|
|
24
|
+
"""
|
|
25
|
+
path = f"/execution/{self.id}/abort"
|
|
26
|
+
|
|
27
|
+
data = {}
|
|
28
|
+
if asUser:
|
|
29
|
+
data["asUser"] = asUser
|
|
30
|
+
|
|
31
|
+
return self.manager.rd.http_get(path, params=data)
|
|
32
|
+
|
|
33
|
+
def get_output(
|
|
34
|
+
self,
|
|
35
|
+
offset: int = 0,
|
|
36
|
+
lastlines: int | None = None,
|
|
37
|
+
lastmod: int | None = None,
|
|
38
|
+
maxlines: int | None = None,
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
"""
|
|
41
|
+
Récupère la sortie de l'exécution
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
offset: Offset dans la sortie
|
|
45
|
+
lastlines: Nombre de dernières lignes
|
|
46
|
+
lastmod: Timestamp de dernière modification
|
|
47
|
+
maxlines: Nombre maximum de lignes
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Sortie de l'exécution
|
|
51
|
+
"""
|
|
52
|
+
path = f"/execution/{self.id}/output"
|
|
53
|
+
|
|
54
|
+
params: dict[str, Any] = {"offset": offset}
|
|
55
|
+
if lastlines:
|
|
56
|
+
params["lastlines"] = lastlines
|
|
57
|
+
if lastmod:
|
|
58
|
+
params["lastmod"] = lastmod
|
|
59
|
+
if maxlines:
|
|
60
|
+
params["maxlines"] = maxlines
|
|
61
|
+
|
|
62
|
+
return self.manager.rd.http_get(path, params=params)
|
|
63
|
+
|
|
64
|
+
def get_state(self) -> dict[str, Any]:
|
|
65
|
+
"""
|
|
66
|
+
Récupère l'état de l'exécution
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
État de l'exécution
|
|
70
|
+
"""
|
|
71
|
+
path = f"/execution/{self.id}/state"
|
|
72
|
+
return self.manager.rd.http_get(path)
|
|
73
|
+
|
|
74
|
+
def delete(self) -> None:
|
|
75
|
+
"""Supprime l'exécution"""
|
|
76
|
+
self.manager.delete(self.id)
|
|
77
|
+
|
|
78
|
+
def is_running(self) -> bool:
|
|
79
|
+
"""
|
|
80
|
+
Vérifie si l'exécution est en cours
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True si en cours, False sinon
|
|
84
|
+
"""
|
|
85
|
+
return self._attrs.get("status") == "running"
|
|
86
|
+
|
|
87
|
+
def is_succeeded(self) -> bool:
|
|
88
|
+
"""
|
|
89
|
+
Vérifie si l'exécution a réussi
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
True si réussie, False sinon
|
|
93
|
+
"""
|
|
94
|
+
return self._attrs.get("status") == "succeeded"
|
|
95
|
+
|
|
96
|
+
def is_failed(self) -> bool:
|
|
97
|
+
"""
|
|
98
|
+
Vérifie si l'exécution a échoué
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
True si échouée, False sinon
|
|
102
|
+
"""
|
|
103
|
+
return self._attrs.get("status") in ("failed", "failed-with-retry")
|
|
104
|
+
|
|
105
|
+
def is_aborted(self) -> bool:
|
|
106
|
+
"""
|
|
107
|
+
Vérifie si l'exécution a été annulée
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
True si annulée, False sinon
|
|
111
|
+
"""
|
|
112
|
+
return self._attrs.get("status") == "aborted"
|
|
113
|
+
|
|
114
|
+
def refresh(self) -> "Execution":
|
|
115
|
+
"""
|
|
116
|
+
Actualise les données de l'exécution
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Exécution actualisée
|
|
120
|
+
"""
|
|
121
|
+
updated = self.manager.get(self.id)
|
|
122
|
+
self._attrs = updated._attrs
|
|
123
|
+
return self
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class ExecutionManager(RundeckObjectManager):
|
|
127
|
+
"""Manager pour les exécutions"""
|
|
128
|
+
|
|
129
|
+
_path = "/executions"
|
|
130
|
+
_obj_cls = Execution
|
|
131
|
+
|
|
132
|
+
def list(
|
|
133
|
+
self,
|
|
134
|
+
project: str | None = None,
|
|
135
|
+
status: str | None = None,
|
|
136
|
+
max: int | None = None,
|
|
137
|
+
offset: int | None = None,
|
|
138
|
+
**kwargs: Any,
|
|
139
|
+
) -> "list[Execution]":
|
|
140
|
+
"""
|
|
141
|
+
Liste les exécutions
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
project: Filtre par projet
|
|
145
|
+
status: Filtre par statut
|
|
146
|
+
max: Nombre maximum de résultats
|
|
147
|
+
offset: Offset de pagination
|
|
148
|
+
**kwargs: Autres paramètres de filtrage
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Liste d'exécutions
|
|
152
|
+
"""
|
|
153
|
+
project_name = project or (
|
|
154
|
+
self.parent.id if getattr(self, "parent", None) else None
|
|
155
|
+
)
|
|
156
|
+
if project_name:
|
|
157
|
+
path = f"/project/{project_name}/executions"
|
|
158
|
+
else:
|
|
159
|
+
path = self._build_path()
|
|
160
|
+
|
|
161
|
+
params: dict[str, Any] = {}
|
|
162
|
+
if status:
|
|
163
|
+
params["status"] = status
|
|
164
|
+
if max:
|
|
165
|
+
params["max"] = max
|
|
166
|
+
if offset:
|
|
167
|
+
params["offset"] = offset
|
|
168
|
+
params.update(kwargs)
|
|
169
|
+
|
|
170
|
+
return self._list(path=path, params=params or None)
|
|
171
|
+
|
|
172
|
+
def running(self, project: str | None = None) -> "list[Execution]":
|
|
173
|
+
"""
|
|
174
|
+
Liste les exécutions en cours
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
project: Filtre par projet (ou '*' pour tous)
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Liste d'exécutions en cours
|
|
181
|
+
"""
|
|
182
|
+
project_name = project or (
|
|
183
|
+
self.parent.id if getattr(self, "parent", None) else None
|
|
184
|
+
)
|
|
185
|
+
if project_name:
|
|
186
|
+
path = f"/project/{project_name}/executions/running"
|
|
187
|
+
else:
|
|
188
|
+
path = "/project/*/executions/running"
|
|
189
|
+
|
|
190
|
+
return self._list(path=path)
|
|
191
|
+
|
|
192
|
+
def get(self, id: str) -> Execution:
|
|
193
|
+
"""
|
|
194
|
+
Récupère une exécution par son ID
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
id: ID de l'exécution
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Exécution
|
|
201
|
+
"""
|
|
202
|
+
path = f"/execution/{id}"
|
|
203
|
+
return self._get(id, path=path)
|
|
204
|
+
|
|
205
|
+
def delete(self, id: str) -> None:
|
|
206
|
+
"""
|
|
207
|
+
Supprime une exécution
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
id: ID de l'exécution
|
|
211
|
+
"""
|
|
212
|
+
path = f"/execution/{id}"
|
|
213
|
+
self._delete(id, path=path)
|
|
214
|
+
|
|
215
|
+
def bulk_delete(self, ids: "list[str]") -> dict[str, Any]:
|
|
216
|
+
"""
|
|
217
|
+
Supprime plusieurs exécutions
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
ids: Liste d'IDs d'exécutions
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Résultat de la suppression
|
|
224
|
+
"""
|
|
225
|
+
path = self._build_path("delete")
|
|
226
|
+
data = {"ids": ids}
|
|
227
|
+
return self.rd.http_post(path, json=data)
|
|
228
|
+
|
|
229
|
+
def query(
|
|
230
|
+
self,
|
|
231
|
+
project: str,
|
|
232
|
+
statusFilter: str | None = None,
|
|
233
|
+
abortedbyFilter: str | None = None,
|
|
234
|
+
userFilter: str | None = None,
|
|
235
|
+
recentFilter: str | None = None,
|
|
236
|
+
olderFilter: str | None = None,
|
|
237
|
+
begin: str | None = None,
|
|
238
|
+
end: str | None = None,
|
|
239
|
+
adhoc: bool | None = None,
|
|
240
|
+
jobIdListFilter: "list[str] | None" = None,
|
|
241
|
+
excludeJobIdListFilter: "list[str] | None" = None,
|
|
242
|
+
jobListFilter: "list[str] | None" = None,
|
|
243
|
+
excludeJobListFilter: "list[str] | None" = None,
|
|
244
|
+
groupPath: str | None = None,
|
|
245
|
+
groupPathExact: str | None = None,
|
|
246
|
+
excludeGroupPath: str | None = None,
|
|
247
|
+
excludeGroupPathExact: str | None = None,
|
|
248
|
+
jobExactFilter: str | None = None,
|
|
249
|
+
excludeJobExactFilter: str | None = None,
|
|
250
|
+
max: int | None = None,
|
|
251
|
+
offset: int | None = None,
|
|
252
|
+
) -> "list[Execution]":
|
|
253
|
+
"""
|
|
254
|
+
Requête avancée sur les exécutions
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
project: Nom du projet
|
|
258
|
+
statusFilter: Filtre par statut
|
|
259
|
+
abortedbyFilter: Filtre par utilisateur ayant annulé
|
|
260
|
+
userFilter: Filtre par utilisateur
|
|
261
|
+
recentFilter: Filtre par période récente
|
|
262
|
+
olderFilter: Filtre par période ancienne
|
|
263
|
+
begin: Date de début
|
|
264
|
+
end: Date de fin
|
|
265
|
+
adhoc: Exécutions adhoc uniquement
|
|
266
|
+
jobIdListFilter: Liste d'IDs de jobs
|
|
267
|
+
excludeJobIdListFilter: Exclure les IDs de jobs
|
|
268
|
+
jobListFilter: Liste de noms de jobs
|
|
269
|
+
excludeJobListFilter: Exclure les noms de jobs
|
|
270
|
+
groupPath: Chemin de groupe
|
|
271
|
+
groupPathExact: Chemin de groupe exact
|
|
272
|
+
excludeGroupPath: Exclure le chemin de groupe
|
|
273
|
+
excludeGroupPathExact: Exclure le chemin de groupe exact
|
|
274
|
+
jobExactFilter: Nom exact du job
|
|
275
|
+
excludeJobExactFilter: Exclure le nom exact du job
|
|
276
|
+
max: Nombre maximum de résultats
|
|
277
|
+
offset: Offset de pagination
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Liste d'exécutions
|
|
281
|
+
"""
|
|
282
|
+
path = f"/project/{project}/executions"
|
|
283
|
+
|
|
284
|
+
params: dict[str, Any] = {}
|
|
285
|
+
if statusFilter:
|
|
286
|
+
params["statusFilter"] = statusFilter
|
|
287
|
+
if abortedbyFilter:
|
|
288
|
+
params["abortedbyFilter"] = abortedbyFilter
|
|
289
|
+
if userFilter:
|
|
290
|
+
params["userFilter"] = userFilter
|
|
291
|
+
if recentFilter:
|
|
292
|
+
params["recentFilter"] = recentFilter
|
|
293
|
+
if olderFilter:
|
|
294
|
+
params["olderFilter"] = olderFilter
|
|
295
|
+
if begin:
|
|
296
|
+
params["begin"] = begin
|
|
297
|
+
if end:
|
|
298
|
+
params["end"] = end
|
|
299
|
+
if adhoc is not None:
|
|
300
|
+
params["adhoc"] = "true" if adhoc else "false"
|
|
301
|
+
if jobIdListFilter:
|
|
302
|
+
params["jobIdListFilter"] = ",".join(jobIdListFilter)
|
|
303
|
+
if excludeJobIdListFilter:
|
|
304
|
+
params["excludeJobIdListFilter"] = ",".join(excludeJobIdListFilter)
|
|
305
|
+
if jobListFilter:
|
|
306
|
+
params["jobListFilter"] = ",".join(jobListFilter)
|
|
307
|
+
if excludeJobListFilter:
|
|
308
|
+
params["excludeJobListFilter"] = ",".join(excludeJobListFilter)
|
|
309
|
+
if groupPath:
|
|
310
|
+
params["groupPath"] = groupPath
|
|
311
|
+
if groupPathExact:
|
|
312
|
+
params["groupPathExact"] = groupPathExact
|
|
313
|
+
if excludeGroupPath:
|
|
314
|
+
params["excludeGroupPath"] = excludeGroupPath
|
|
315
|
+
if excludeGroupPathExact:
|
|
316
|
+
params["excludeGroupPathExact"] = excludeGroupPathExact
|
|
317
|
+
if jobExactFilter:
|
|
318
|
+
params["jobExactFilter"] = jobExactFilter
|
|
319
|
+
if excludeJobExactFilter:
|
|
320
|
+
params["excludeJobExactFilter"] = excludeJobExactFilter
|
|
321
|
+
if max:
|
|
322
|
+
params["max"] = max
|
|
323
|
+
if offset:
|
|
324
|
+
params["offset"] = offset
|
|
325
|
+
|
|
326
|
+
result = self.rd.http_list(path, params=params)
|
|
327
|
+
return [self._obj_cls(self, item) for item in result]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gestion des system features (/feature).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from rundeck.base import RundeckObject, RundeckObjectManager
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Feature(RundeckObject):
|
|
9
|
+
"""Représente un feature flag système."""
|
|
10
|
+
|
|
11
|
+
_id_attr = "name"
|
|
12
|
+
_repr_attr = "name"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FeatureManager(RundeckObjectManager[Feature]):
|
|
16
|
+
"""Manager pour interroger les features système."""
|
|
17
|
+
|
|
18
|
+
_path = "/feature"
|
|
19
|
+
_obj_cls = Feature
|
|
20
|
+
|
|
21
|
+
def list(self) -> list[Feature]:
|
|
22
|
+
"""Liste le statut de tous les features système."""
|
|
23
|
+
base = self._build_path()
|
|
24
|
+
path = f"{base.rstrip('/')}/"
|
|
25
|
+
result = self.rd.http_get(path)
|
|
26
|
+
items = []
|
|
27
|
+
if isinstance(result, dict):
|
|
28
|
+
for name, value in result.items():
|
|
29
|
+
if isinstance(value, dict):
|
|
30
|
+
items.append({"name": name, **(value or {})})
|
|
31
|
+
else:
|
|
32
|
+
items.append({"name": name, "enabled": bool(value)})
|
|
33
|
+
return self._wrap_list(items)
|
|
34
|
+
|
|
35
|
+
def get(self, name: str) -> Feature:
|
|
36
|
+
"""Récupère le statut d'un feature spécifique."""
|
|
37
|
+
path = self._build_path(name)
|
|
38
|
+
data = self.rd.http_get(path)
|
|
39
|
+
if isinstance(data, dict):
|
|
40
|
+
data.setdefault("name", name)
|
|
41
|
+
return self._wrap(data)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
__all__ = ["Feature", "FeatureManager"]
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gestion des jobs Rundeck
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from rundeck.base import RundeckObject, RundeckObjectManager
|
|
8
|
+
from rundeck.v1.objects.executions import Execution
|
|
9
|
+
from rundeck.v1.objects.scm import JobSCMManager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Job(RundeckObject):
|
|
13
|
+
"""Représente un job Rundeck."""
|
|
14
|
+
|
|
15
|
+
_id_attr = "id"
|
|
16
|
+
_repr_attr = "name"
|
|
17
|
+
|
|
18
|
+
def _job_path(self, suffix: str | None = None) -> str:
|
|
19
|
+
"""Construit le chemin de base /job/{id} avec suffixe optionnel."""
|
|
20
|
+
base = f"/job/{self.id}"
|
|
21
|
+
if suffix:
|
|
22
|
+
return f"{base}/{suffix.lstrip('/')}"
|
|
23
|
+
return base
|
|
24
|
+
|
|
25
|
+
def run(self, **kwargs: Any) -> Execution:
|
|
26
|
+
"""
|
|
27
|
+
Exécute le job et retourne l'exécution.
|
|
28
|
+
"""
|
|
29
|
+
path = self._job_path("run")
|
|
30
|
+
result = self.rd.http_post(path, json=kwargs or None)
|
|
31
|
+
# Wrap avec le manager d'exécutions existant.
|
|
32
|
+
return Execution(self.manager.rd.executions, result)
|
|
33
|
+
|
|
34
|
+
def retry(self, execution_id: str, **kwargs: Any) -> Execution:
|
|
35
|
+
"""
|
|
36
|
+
Relance une exécution (retry).
|
|
37
|
+
"""
|
|
38
|
+
path = self._job_path(f"retry/{execution_id}")
|
|
39
|
+
result = self.rd.http_post(path, json=kwargs or None)
|
|
40
|
+
return Execution(self.manager.rd.executions, result)
|
|
41
|
+
|
|
42
|
+
def delete(self) -> None:
|
|
43
|
+
"""Supprime le job courant."""
|
|
44
|
+
self.manager.delete(self.id)
|
|
45
|
+
|
|
46
|
+
def definition(self, format: str = "json") -> Any:
|
|
47
|
+
"""Récupère la définition du job dans le format demandé."""
|
|
48
|
+
path = self._job_path()
|
|
49
|
+
return self.rd.http_get(path, params={"format": format})
|
|
50
|
+
|
|
51
|
+
def enable_execution(self) -> Any:
|
|
52
|
+
"""Active l'exécution pour ce job."""
|
|
53
|
+
path = self._job_path("execution/enable")
|
|
54
|
+
return self.rd.http_post(path)
|
|
55
|
+
|
|
56
|
+
def disable_execution(self) -> Any:
|
|
57
|
+
"""Désactive l'exécution pour ce job."""
|
|
58
|
+
path = self._job_path("execution/disable")
|
|
59
|
+
return self.rd.http_post(path)
|
|
60
|
+
|
|
61
|
+
def enable_schedule(self) -> Any:
|
|
62
|
+
"""Active la planification pour ce job."""
|
|
63
|
+
path = self._job_path("schedule/enable")
|
|
64
|
+
return self.rd.http_post(path)
|
|
65
|
+
|
|
66
|
+
def disable_schedule(self) -> Any:
|
|
67
|
+
"""Désactive la planification pour ce job."""
|
|
68
|
+
path = self._job_path("schedule/disable")
|
|
69
|
+
return self.rd.http_post(path)
|
|
70
|
+
|
|
71
|
+
def info(self) -> dict[str, Any]:
|
|
72
|
+
"""Récupère les métadonnées du job."""
|
|
73
|
+
path = self._job_path("info")
|
|
74
|
+
return self.rd.http_get(path)
|
|
75
|
+
|
|
76
|
+
def workflow(self) -> dict[str, Any]:
|
|
77
|
+
"""Récupère le workflow du job."""
|
|
78
|
+
path = self._job_path("workflow")
|
|
79
|
+
return self.rd.http_get(path)
|
|
80
|
+
|
|
81
|
+
def meta(self, meta: str | None = None) -> list[dict[str, Any]]:
|
|
82
|
+
"""
|
|
83
|
+
Récupère les métadonnées UI du job (endpoint /job/{id}/meta).
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
meta: Liste de métadonnées à inclure (ex: "name,description")
|
|
87
|
+
ou "*" pour tout.
|
|
88
|
+
"""
|
|
89
|
+
params = {"meta": meta} if meta else None
|
|
90
|
+
path = self._job_path("meta")
|
|
91
|
+
return self.rd.http_get(path, params=params)
|
|
92
|
+
|
|
93
|
+
def tags(self) -> list[str]:
|
|
94
|
+
"""Retourne les tags du job (commercial, endpoint /job/{id}/tags)."""
|
|
95
|
+
path = self._job_path("tags")
|
|
96
|
+
return self.rd.http_get(path)
|
|
97
|
+
|
|
98
|
+
def upload_option_file(
|
|
99
|
+
self,
|
|
100
|
+
option_name: str,
|
|
101
|
+
content: bytes | str,
|
|
102
|
+
file_name: str | None = None,
|
|
103
|
+
) -> dict[str, Any]:
|
|
104
|
+
"""
|
|
105
|
+
Upload d'un fichier pour une option (single file).
|
|
106
|
+
"""
|
|
107
|
+
path = self._job_path("input/file")
|
|
108
|
+
params = {"optionName": option_name}
|
|
109
|
+
if file_name:
|
|
110
|
+
params["fileName"] = file_name
|
|
111
|
+
response = self.rd.http_post(
|
|
112
|
+
path,
|
|
113
|
+
params=params,
|
|
114
|
+
data=content,
|
|
115
|
+
headers={
|
|
116
|
+
"Content-Type": "application/octet-stream",
|
|
117
|
+
"Accept": "application/json",
|
|
118
|
+
},
|
|
119
|
+
raw=True,
|
|
120
|
+
)
|
|
121
|
+
return response.json()
|
|
122
|
+
|
|
123
|
+
def upload_option_files(
|
|
124
|
+
self, files: dict[str, tuple[str, bytes | Any]]
|
|
125
|
+
) -> dict[str, Any]:
|
|
126
|
+
"""
|
|
127
|
+
Upload multi-fichiers pour des options (multipart/form-data).
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
files: mapping option_name -> (file_name, content)
|
|
131
|
+
"""
|
|
132
|
+
multipart: dict[str, tuple[str, bytes | Any]] = {
|
|
133
|
+
f"option.{name}": (fname, data) for name, (fname, data) in files.items()
|
|
134
|
+
}
|
|
135
|
+
path = self._job_path("input/file")
|
|
136
|
+
response = self.rd.http_post(
|
|
137
|
+
path,
|
|
138
|
+
files=multipart,
|
|
139
|
+
headers={"Accept": "application/json"},
|
|
140
|
+
raw=True,
|
|
141
|
+
)
|
|
142
|
+
return response.json()
|
|
143
|
+
|
|
144
|
+
def list_uploaded_files(
|
|
145
|
+
self,
|
|
146
|
+
max: int | None = None,
|
|
147
|
+
offset: int | None = None,
|
|
148
|
+
file_state: str | None = None,
|
|
149
|
+
) -> dict[str, Any]:
|
|
150
|
+
"""
|
|
151
|
+
Liste les fichiers uploadés pour ce job.
|
|
152
|
+
"""
|
|
153
|
+
params: dict[str, Any] = {}
|
|
154
|
+
if max is not None:
|
|
155
|
+
params["max"] = max
|
|
156
|
+
if offset is not None:
|
|
157
|
+
params["offset"] = offset
|
|
158
|
+
if file_state:
|
|
159
|
+
params["fileState"] = file_state
|
|
160
|
+
path = f"/job/{self.id}/input/files"
|
|
161
|
+
return self.rd.http_get(path, params=params or None)
|
|
162
|
+
|
|
163
|
+
def forecast(
|
|
164
|
+
self, time: str | None = None, max: int | None = None
|
|
165
|
+
) -> dict[str, Any]:
|
|
166
|
+
"""Prévision du planning du job (scheduler/forecast)."""
|
|
167
|
+
params: dict[str, Any] = {}
|
|
168
|
+
if time:
|
|
169
|
+
params["time"] = time
|
|
170
|
+
if max:
|
|
171
|
+
params["max"] = max
|
|
172
|
+
path = f"/scheduler/{self._job_path('forecast').lstrip('/')}"
|
|
173
|
+
return self.rd.http_get(path, params=params)
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def scm(self) -> JobSCMManager:
|
|
177
|
+
"""Accès aux opérations SCM pour ce job."""
|
|
178
|
+
return JobSCMManager(self.rd, parent=self)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class JobBulkManager(RundeckObjectManager[Job]):
|
|
182
|
+
"""Opérations bulk sur les jobs (/jobs/*)."""
|
|
183
|
+
|
|
184
|
+
_path = "/jobs"
|
|
185
|
+
_obj_cls = Job
|
|
186
|
+
|
|
187
|
+
def enable_execution(self, ids: "list[str]") -> Any:
|
|
188
|
+
"""Active l'exécution pour plusieurs jobs."""
|
|
189
|
+
data = {"ids": ids}
|
|
190
|
+
path = self._build_path("execution/enable")
|
|
191
|
+
return self.rd.http_put(path, json=data)
|
|
192
|
+
|
|
193
|
+
def disable_execution(self, ids: "list[str]") -> Any:
|
|
194
|
+
"""Désactive l'exécution pour plusieurs jobs."""
|
|
195
|
+
data = {"ids": ids}
|
|
196
|
+
path = self._build_path("execution/disable")
|
|
197
|
+
return self.rd.http_put(path, json=data)
|
|
198
|
+
|
|
199
|
+
def enable_schedule(self, ids: "list[str]") -> Any:
|
|
200
|
+
"""Active la planification pour plusieurs jobs."""
|
|
201
|
+
data = {"ids": ids}
|
|
202
|
+
path = self._build_path("schedule/enable")
|
|
203
|
+
return self.rd.http_post(path, json=data)
|
|
204
|
+
|
|
205
|
+
def disable_schedule(self, ids: "list[str]") -> Any:
|
|
206
|
+
"""Désactive la planification pour plusieurs jobs."""
|
|
207
|
+
data = {"ids": ids}
|
|
208
|
+
path = self._build_path("schedule/disable")
|
|
209
|
+
return self.rd.http_post(path, json=data)
|
|
210
|
+
|
|
211
|
+
def delete(self, ids: "list[str]") -> Any:
|
|
212
|
+
"""Supprime plusieurs jobs en une requête."""
|
|
213
|
+
data = {"ids": ids}
|
|
214
|
+
path = self._build_path("delete")
|
|
215
|
+
return self.rd.http_delete(path, json=data)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class JobManager(RundeckObjectManager[Job]):
|
|
219
|
+
"""Manager pour les jobs."""
|
|
220
|
+
|
|
221
|
+
_path = "/project/{parent}/jobs"
|
|
222
|
+
_obj_cls = Job
|
|
223
|
+
|
|
224
|
+
def __init__(self, rd: Any, parent: RundeckObject | None = None) -> None:
|
|
225
|
+
super().__init__(rd, parent)
|
|
226
|
+
self.bulk = JobBulkManager(rd)
|
|
227
|
+
|
|
228
|
+
def __getattr__(self, name: str) -> Any:
|
|
229
|
+
# Alias pour permettre rd.jobs.import(...) malgré le mot-clé Python.
|
|
230
|
+
if name == "import":
|
|
231
|
+
return self.import_jobs
|
|
232
|
+
return super().__getattribute__(name)
|
|
233
|
+
|
|
234
|
+
def list(
|
|
235
|
+
self,
|
|
236
|
+
project: str | None = None,
|
|
237
|
+
**filters: Any,
|
|
238
|
+
) -> "list[Job]":
|
|
239
|
+
"""Liste les jobs d'un projet."""
|
|
240
|
+
project_name = project or (self.parent.id if self.parent else None)
|
|
241
|
+
# Avec un parent Project, on réutilise le manager parenté ; sinon on exige
|
|
242
|
+
# le nom du projet.
|
|
243
|
+
if not project_name:
|
|
244
|
+
raise ValueError("Le paramètre project est requis")
|
|
245
|
+
|
|
246
|
+
# Si le manager est parenté (project.jobs), _build_path() résout le
|
|
247
|
+
# placeholder {parent}.
|
|
248
|
+
# Sinon, on formate le chemin collection à partir du nom de projet fourni.
|
|
249
|
+
path = (
|
|
250
|
+
self._build_path()
|
|
251
|
+
if self.parent and not project
|
|
252
|
+
else self._path.format(parent=project_name)
|
|
253
|
+
)
|
|
254
|
+
return self._list(path=path, params=filters)
|
|
255
|
+
|
|
256
|
+
def get(self, id: str) -> "Job":
|
|
257
|
+
"""Récupère un job par identifiant."""
|
|
258
|
+
path = f"/job/{id}"
|
|
259
|
+
return self._get(id, path=path)
|
|
260
|
+
|
|
261
|
+
def export(
|
|
262
|
+
self,
|
|
263
|
+
project: str | None = None,
|
|
264
|
+
format: str = "json",
|
|
265
|
+
idlist: str | None = None,
|
|
266
|
+
groupPath: str | None = None,
|
|
267
|
+
jobFilter: str | None = None,
|
|
268
|
+
) -> Any:
|
|
269
|
+
"""Exporte les jobs d'un projet (json/xml/yaml)."""
|
|
270
|
+
project_name = project or (self.parent.id if self.parent else None)
|
|
271
|
+
if not project_name:
|
|
272
|
+
raise ValueError("Le paramètre project est requis")
|
|
273
|
+
|
|
274
|
+
params: dict[str, Any] = {"format": format} if format else {}
|
|
275
|
+
if idlist:
|
|
276
|
+
params["idlist"] = idlist
|
|
277
|
+
if groupPath:
|
|
278
|
+
params["groupPath"] = groupPath
|
|
279
|
+
if jobFilter:
|
|
280
|
+
params["jobFilter"] = jobFilter
|
|
281
|
+
|
|
282
|
+
path = (
|
|
283
|
+
self._build_path("export")
|
|
284
|
+
if self.parent and not project
|
|
285
|
+
else f"/project/{project_name}/jobs/export"
|
|
286
|
+
)
|
|
287
|
+
return self.rd.http_get(path, params=params or None)
|
|
288
|
+
|
|
289
|
+
def import_jobs(
|
|
290
|
+
self,
|
|
291
|
+
project: str | None = None,
|
|
292
|
+
content: Any = None,
|
|
293
|
+
fileformat: str = "json",
|
|
294
|
+
dupeOption: str | None = None,
|
|
295
|
+
uuidOption: str | None = None,
|
|
296
|
+
content_type: str = "application/octet-stream",
|
|
297
|
+
) -> Any:
|
|
298
|
+
"""Importe des jobs (json/xml/yaml) dans un projet."""
|
|
299
|
+
project_name = project or (self.parent.id if self.parent else None)
|
|
300
|
+
if not project_name:
|
|
301
|
+
raise ValueError("Le paramètre project est requis")
|
|
302
|
+
|
|
303
|
+
params: dict[str, Any] = {"fileformat": fileformat} if fileformat else {}
|
|
304
|
+
if dupeOption:
|
|
305
|
+
params["dupeOption"] = dupeOption
|
|
306
|
+
if uuidOption:
|
|
307
|
+
params["uuidOption"] = uuidOption
|
|
308
|
+
|
|
309
|
+
path = (
|
|
310
|
+
self._build_path("import")
|
|
311
|
+
if self.parent and not project
|
|
312
|
+
else f"/project/{project_name}/jobs/import"
|
|
313
|
+
)
|
|
314
|
+
return self.rd.http_post(
|
|
315
|
+
path,
|
|
316
|
+
params=params or None,
|
|
317
|
+
data=content,
|
|
318
|
+
headers={"Content-Type": content_type},
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def delete(self, id: str) -> None:
|
|
322
|
+
"""Supprime un job par identifiant."""
|
|
323
|
+
path = f"/job/{id}"
|
|
324
|
+
self._delete(id, path=path)
|
|
325
|
+
|
|
326
|
+
def get_uploaded_file_info(self, file_id: str) -> dict[str, Any]:
|
|
327
|
+
"""Récupère les métadonnées d'un fichier uploadé (par ID)."""
|
|
328
|
+
path = f"/jobs/file/{file_id}"
|
|
329
|
+
return self.rd.http_get(path)
|