blob: 8911608702914b4ad0adf0d09000b0d6bccf2d3a [file] [log] [blame]
Ilya Kharinaa03d4e2017-04-18 17:25:53 +04001import logging
2
3import salt.exceptions
4
5import requests
6from requests.compat import urljoin
7
8LOG = logging.getLogger(__name__)
9
10
Ilya Kharin9be40682017-05-02 16:43:51 +040011# Project
12
Ilya Kharinaa03d4e2017-04-18 17:25:53 +040013def get_project(name):
14 session, make_url = get_session()
15 resp = session.get(make_url("/api/18/project/{}".format(name)))
16 status_code = resp.status_code
17 if status_code == 200:
18 return resp.json()
19 elif status_code == 404:
20 return None
21 raise salt.exceptions.SaltInvocationError(
Ilya Kharin5f4e9062017-07-05 00:54:56 +040022 "Could not retrieve information about project {} from Rundeck {}: "
23 "{}/{}".format(name, make_url.base_url, status_code, resp.text))
Ilya Kharinaa03d4e2017-04-18 17:25:53 +040024
25
26def create_project(name, params):
27 session, make_url = get_session()
28 config = create_project_config(name, params)
Ilya Kharinaa03d4e2017-04-18 17:25:53 +040029 resp = session.post(
30 make_url("/api/18/projects"),
31 json={
32 'name': name,
33 'config': config,
34 },
35 allow_redirects=False,
36 )
37 if resp.status_code == 201:
38 return resp.json()
Ilya Kharinaa03d4e2017-04-18 17:25:53 +040039
40
Ilya Kharinaa03d4e2017-04-18 17:25:53 +040041def update_project_config(name, project, config):
42 session, make_url = get_session()
43 resp = session.put(
44 make_url("/api/18/project/{}/config".format(name)),
45 json=config,
46 allow_redirects=False,
47 )
48 if resp.status_code == 201:
49 return resp.json()
Ilya Kharinaa03d4e2017-04-18 17:25:53 +040050
51
52def delete_project(name):
53 session, make_url = get_session()
54 resp = session.delete(make_url("/api/18/project/{}".format(name)))
55 status_code = resp.status_code
56 if status_code != 204:
57 raise salt.exceptions.SaltInvocationError(
Ilya Kharin5f4e9062017-07-05 00:54:56 +040058 "Could not remove project {} from Rundeck {}: {}/{}"
59 .format(name, make_url.base_url, status_code, resp.text))
Ilya Kharinaa03d4e2017-04-18 17:25:53 +040060
61
Ilya Kharin9be40682017-05-02 16:43:51 +040062# SCM
63
64def get_plugin(project_name, integration):
65 session, make_url = get_session()
66 resp = session.get(make_url("/api/18/project/{}/scm/{}/config"
67 .format(project_name, integration)))
68 if resp.status_code == 200:
69 return True, resp.json()
70 elif resp.status_code == 404:
71 return True, None
72 return False, (
Ilya Kharin5f4e9062017-07-05 00:54:56 +040073 "Could not get config for the {} plugin of the {} project: {}"
74 .format(integration, project_name, resp.text))
Ilya Kharin9be40682017-05-02 16:43:51 +040075
76
77def get_plugin_status(project_name, integration):
78 def get_plugin(plugins, plugin_type):
79 for plugin in plugins:
80 if plugin['type'] == plugin_type:
81 return plugin
Ilya Kharin5f4e9062017-07-05 00:54:56 +040082 LOG.debug(
83 "Could not find the %s integration among available plugins of "
84 "the %s projects: %s", integration, project_name, plugins)
Ilya Kharin9be40682017-05-02 16:43:51 +040085 raise salt.exceptions.SaltInvocationError(
Ilya Kharin5f4e9062017-07-05 00:54:56 +040086 "Could not find status for the {}/{} plugin of the {} project, "
87 "this integration is not available in your deployment."
Ilya Kharin9be40682017-05-02 16:43:51 +040088 .format(integration, plugin_type, project_name))
89
90 session, make_url = get_session()
91 resp = session.get(make_url("/api/18/project/{}/scm/{}/plugins"
92 .format(project_name, integration)))
93 if resp.status_code == 200:
94 plugin_type = "git-{}".format(integration)
95 status = get_plugin(resp.json()['plugins'], plugin_type)
96 return True, status
97 return False, (
Ilya Kharin5f4e9062017-07-05 00:54:56 +040098 "Could not get status for the {} plugin of the {} project: {}"
99 .format(integration, project_name, resp.text))
Ilya Kharin9be40682017-05-02 16:43:51 +0400100
101
102def get_plugin_state(project_name, integration):
103 session, make_url = get_session()
104 resp = session.get(make_url("/api/18/project/{}/scm/{}/status"
105 .format(project_name, integration)))
106 if resp.status_code == 200:
107 return True, resp.json()
108 return False, (
Ilya Kharin5f4e9062017-07-05 00:54:56 +0400109 "Could not get state for the {} plugin of the {} project: {}"
110 .format(integration, project_name, resp.text))
Ilya Kharin9be40682017-05-02 16:43:51 +0400111
112
113def disable_plugin(project_name, integration):
114 session, make_url = get_session()
115 resp = session.post(make_url(
116 "/api/15/project/{}/scm/{}/plugin/git-{}/disable"
117 .format(project_name, integration, integration)))
118 if resp.status_code == 200:
119 msg = resp.json()
120 return True, msg['message']
121 return False, (
Ilya Kharin5f4e9062017-07-05 00:54:56 +0400122 "Could not disable the {} plugin for the {} project: {}/{}"
123 .format(integration, project_name, resp.status_code, resp.text))
Ilya Kharin9be40682017-05-02 16:43:51 +0400124
125
126def enable_plugin(project_name, integration):
127 session, make_url = get_session()
128 resp = session.post(make_url(
129 "/api/15/project/{}/scm/{}/plugin/git-{}/enable"
130 .format(project_name, integration, integration)))
131 if resp.status_code == 200:
132 msg = resp.json()
133 return True, msg['message']
134 return False, (
Ilya Kharin5f4e9062017-07-05 00:54:56 +0400135 "Could not enable the {} plugin for the {} project: {}/{}"
136 .format(integration, project_name, resp.status_code, resp.text))
Ilya Kharin9be40682017-05-02 16:43:51 +0400137
138
139# SCM Import
140
141def setup_scm_import(project_name, params):
142 session, make_url = get_session()
143 config = create_scm_import_config(project_name, params)
144 resp = session.post(
145 make_url("/api/15/project/{}/scm/import/plugin/git-import/setup"
146 .format(project_name)),
147 json={
148 'config': config,
149 },
150 allow_redirects=False,
151 )
152 if resp.status_code == 200:
153 return True, resp.json()
154 return False, (
155 "Could not configure SCM Import for the {} project: {}/{}"
156 .format(project_name, resp.status_code, resp.text))
157
158
159def update_scm_import_config(project_name, plugin, config):
160 session, make_url = get_session()
161 resp = session.post(
162 make_url("/api/15/project/{}/scm/import/plugin/git-import/setup"
163 .format(project_name)),
164 json={
165 'config': config,
166 },
167 allow_redirects=False,
168 )
169 if resp.status_code == 200:
170 return True, resp.json()
171 return False, (
172 "Could not update SCM Import for the {} project: {}/{}"
173 .format(project_name, resp.status_code, resp.text))
174
175
176def perform_scm_import_tracking(project_name, plugin, params):
177 format = plugin['config']['format']
178 file_pattern = params.get('file_pattern')
179 if not file_pattern:
180 file_pattern = DEFAULT_FILE_PATTERNS[format]
181
182 session, make_url = get_session()
183 resp = session.post(
184 make_url("/api/15/project/{}/scm/import/action/initialize-tracking"
185 .format(project_name)),
186 json={
187 'input': {
188 'filePattern': file_pattern,
189 'useFilePattern': 'true',
190 },
191 'jobs': [],
192 'items': [],
193 'deleted': [],
194 },
195 allow_redirects=False,
196 )
197 if resp.status_code == 200:
198 return True, resp.json()
199 return False, (
200 "Could not update SCM Import for the {} project: {}/{}"
201 .format(project_name, resp.status_code, resp.text))
202
203DEFAULT_FILE_PATTERNS = {
204 'yaml': r'.*\.yaml',
205 'xml': r'.*\.xml',
206}
207
208
209def perform_scm_import_pull(project_name, plugin, params):
210 session, make_url = get_session()
211 resp = session.post(
212 make_url("/api/15/project/{}/scm/import/action/remote-pull"
213 .format(project_name)),
214 json={
215 'input': {},
216 'jobs': [],
217 'items': [],
218 'deleted': [],
219 },
220 allow_redirects=False,
221 )
222 if resp.status_code == 200:
223 return True, resp.json()
224 return False, (
225 "Could not pull remote changes for the {} project: {}/{}"
226 .format(project_name, resp.status_code, resp.text))
227
228
229def perform_scm_import(project_name, plugin, params):
230 session, make_url = get_session()
231 ok, inputs = get_plugin_action_inputs(
232 project_name, 'import', 'import-all')
233 if not ok:
234 return False, inputs
235 items = list(item['itemId'] for item in inputs['importItems'])
236 resp = session.post(
237 make_url("/api/15/project/{}/scm/import/action/import-all"
238 .format(project_name)),
239 json={
240 'input': {},
241 'jobs': [],
242 'items': items,
243 'deleted': [],
244 },
245 allow_redirects=False,
246 )
247 if resp.status_code == 200:
248 return True, resp.json()
249 return False, (
250 "Could not import jobs for the {} project: {}/{}"
251 .format(project_name, resp.status_code, resp.text))
252
253
Ilya Kharin28ffe0c2017-06-08 03:29:42 +0400254# Key Store
255
256def get_secret_metadata(path):
257 session, make_url = get_session()
258 resp = session.get(
259 make_url("/api/11/storage/keys/{}".format(path)),
260 allow_redirects=False,
261 )
262 if resp.status_code == 200:
263 return True, resp.json()
264 elif resp.status_code == 404:
265 return True, None
266 return False, (
267 "Could not retrieve metadata for the {} secret key: {}/{}"
268 .format(path, resp.status_code, resp.text))
269
270
271def upload_secret(path, type, content, update=False):
272 session, make_url = get_session()
273 session.headers['Content-Type'] = SECRET_CONTENT_TYPE[type]
274 method = session.put if update else session.post
275 resp = method(
276 make_url("/api/11/storage/keys/{}".format(path)),
277 data=content,
278 allow_redirects=False,
279 )
280 if resp.status_code in (200, 201):
281 return True, resp.json()
282 return False, (
283 "Could not create or update the {} secret key with the type {}: {}/{}"
284 .format(path, type, resp.status_code, resp.text))
285
286SECRET_CONTENT_TYPE = {
287 "private": "application/octet-stream",
288 "public": "application/pgp-keys",
289 "password": "application/x-rundeck-data-password",
290}
291
292
293def delete_secret(path):
294 session, make_url = get_session()
295 resp = session.delete(
296 make_url("/api/11/storage/keys/{}".format(path)),
297 allow_redirects=False,
298 )
299 if resp.status_code == 204:
300 return True, None
301 return False, (
302 "Could not delete the {} secret key: {}/{}"
303 .format(path, resp.status_code, resp.text))
304
305
Ilya Kharin9be40682017-05-02 16:43:51 +0400306# Utils
307
308def create_project_config(project_name, params, config=None):
309 config = dict(config) if config else {}
310 if params['description']:
311 config['project.description'] = params['description']
312 else:
313 config.pop('project.description', None)
314 config.update({
315 'resources.source.1.config.file':
316 "/var/rundeck/projects/{}/etc/resources.yaml".format(project_name),
317 'resources.source.1.config.format': 'resourceyaml',
318 'resources.source.1.config.generateFileAutomatically': 'true',
319 'resources.source.1.config.includeServerNode': 'false',
320 'resources.source.1.config.requireFileExists': 'false',
321 'project.ssh-keypath': '/var/rundeck/.ssh/id_rsa',
322 'resources.source.1.type': 'file',
323 })
324 return config
325
326
327def create_scm_import_config(project_name, params, config=None):
328 config = dict(config) if config else {}
329
330 format = params.get('format', 'yaml')
331 if format not in DEFAULT_FILE_PATTERNS:
332 supported_formats = DEFAULT_FILE_PATTERNS.keys()
333 raise salt.exceptions.SaltInvocationError(
334 "Unsupported format {} for the {} SCM import module, should be {}"
335 .format(format, project_name, ','.join(supported_formats)))
336
337 config.update({
338 'dir': "/var/rundeck/projects/{}/scm".format(project_name),
339 'url': params['address'],
340 'branch': params.get('branch', 'master'),
341 'fetchAutomatically': 'true',
342 'format': format,
343 'pathTemplate': params.get(
344 'path_template', '${job.group}${job.name}.${config.format}'),
345 'importUuidBehavior': params.get('import_uuid_behavior', 'remove'),
346 'strictHostKeyChecking': 'yes',
347 })
348 return config
349
350
351def get_plugin_action_inputs(project_name, integration, action):
352 session, make_url = get_session()
353 resp = session.get(
354 make_url("/api/18/project/cicd/scm/import/action/import-all/input"))
355 if resp.status_code == 200:
356 return True, resp.json()
357 return False, (
358 "Could not get inputs for the {} action for the {} project: {}/{}"
359 .format(action, project_name, resp.status_code, resp.text))
360
361
362
Ilya Kharinaa03d4e2017-04-18 17:25:53 +0400363def get_session():
364 def make_url(url):
365 return urljoin(make_url.base_url, url)
366
367 rundeck_url = __salt__['config.get']('rundeck.url')
Ilya Kharinaa03d4e2017-04-18 17:25:53 +0400368 api_token = __salt__['config.get']('rundeck.api_token')
369 username = __salt__['config.get']('rundeck.username')
370 password = __salt__['config.get']('rundeck.password')
371
Ilya Kharinc61d92c2017-07-05 00:07:05 +0400372 if not rundeck_url:
373 raise salt.exceptions.SaltInvocationError(
374 "The 'rundeck.url' parameter have to be set as non-empty value in "
375 "the minion's configuration file.")
376 elif not (api_token or username and password):
377 raise salt.exceptions.SaltInvocationError(
378 "Either the 'rundeck.api_token' parameter or a pair of "
379 "'rundeck.username' and 'rundeck.password' parameters have to be "
380 "set as non-empty values in the minion's configuration file.")
381
382 make_url.base_url = rundeck_url
383
Ilya Kharinaa03d4e2017-04-18 17:25:53 +0400384 session = requests.Session()
385
386 if api_token:
387 session.headers.update({
Ilya Kharinaa03d4e2017-04-18 17:25:53 +0400388 'X-Rundeck-Auth-Token': api_token,
389 })
390 else:
391 resp = session.post(make_url('/j_security_check'),
392 data={
393 'j_username': username,
394 'j_password': password,
395 },
396 )
397 if (resp.status_code != 200 or
398 '/user/error' in resp.url or
399 '/user/login' in resp.url):
400 raise salt.exceptions.SaltInvocationError(
Vnaumov3daf8ba2017-10-12 13:41:51 +0300401 "Username/password authorization failed in Rundeck {} for "
Ilya Kharinaa03d4e2017-04-18 17:25:53 +0400402 "user {}".format(rundeck_url, username))
403 session.params.update({
404 'format': 'json',
405 })
406 return session, make_url