blob: 5d9c7a395be2c22df50550a94d9cdfb35946da37 [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(
22 "Could not retrieve information about project {} from Rundeck {}: {}"
23 .format(name, make_url.base_url, status_code))
24
25
26def create_project(name, params):
27 session, make_url = get_session()
28 config = create_project_config(name, params)
29 LOG.debug("create_project: %s", name)
30 LOG.warning("create_project.config: %s/%s", name, config)
31 resp = session.post(
32 make_url("/api/18/projects"),
33 json={
34 'name': name,
35 'config': config,
36 },
37 allow_redirects=False,
38 )
39 if resp.status_code == 201:
40 return resp.json()
41 LOG.debug("create_project: %s", name)
42
43
Ilya Kharinaa03d4e2017-04-18 17:25:53 +040044def update_project_config(name, project, config):
45 session, make_url = get_session()
46 resp = session.put(
47 make_url("/api/18/project/{}/config".format(name)),
48 json=config,
49 allow_redirects=False,
50 )
51 if resp.status_code == 201:
52 return resp.json()
53 LOG.debug("update_project: %s", name)
54
55
56def delete_project(name):
57 session, make_url = get_session()
58 resp = session.delete(make_url("/api/18/project/{}".format(name)))
59 status_code = resp.status_code
60 if status_code != 204:
61 raise salt.exceptions.SaltInvocationError(
62 "Could not remove project {} from Rundeck {}: {}"
63 .format(name, make_url.base_url, status_code))
64
65
Ilya Kharin9be40682017-05-02 16:43:51 +040066# SCM
67
68def get_plugin(project_name, integration):
69 session, make_url = get_session()
70 resp = session.get(make_url("/api/18/project/{}/scm/{}/config"
71 .format(project_name, integration)))
72 if resp.status_code == 200:
73 return True, resp.json()
74 elif resp.status_code == 404:
75 return True, None
76 return False, (
77 "Could not get config for the {} plugin of the {} project"
78 .format(integration, project_name))
79
80
81def get_plugin_status(project_name, integration):
82 def get_plugin(plugins, plugin_type):
83 for plugin in plugins:
84 if plugin['type'] == plugin_type:
85 return plugin
86 raise salt.exceptions.SaltInvocationError(
87 "Could not find status for the {}/{} plugin of the {} project"
88 .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, (
98 "Could not get status for the {} plugin of the {} project"
99 .format(integration, project_name))
100
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, (
109 "Could not get state for the {} plugin of the {} project"
110 .format(integration, project_name))
111
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, (
122 "Could not disable the {} plugin for the {} project: {}"
123 .format(integration, project_name, resp.status_code))
124
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, (
135 "Could not enable the {} plugin for the {} project: {}"
136 .format(integration, project_name, resp.status_code))
137
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')
368 make_url.base_url = rundeck_url
369
370 api_token = __salt__['config.get']('rundeck.api_token')
371 username = __salt__['config.get']('rundeck.username')
372 password = __salt__['config.get']('rundeck.password')
373
374 session = requests.Session()
375
376 if api_token:
377 session.headers.update({
378 'Content-Type': 'application/json',
379 'X-Rundeck-Auth-Token': api_token,
380 })
381 else:
382 resp = session.post(make_url('/j_security_check'),
383 data={
384 'j_username': username,
385 'j_password': password,
386 },
387 )
388 if (resp.status_code != 200 or
389 '/user/error' in resp.url or
390 '/user/login' in resp.url):
391 raise salt.exceptions.SaltInvocationError(
392 "Username/passowrd authorization failed in Rundeck {} for "
393 "user {}".format(rundeck_url, username))
394 session.params.update({
395 'format': 'json',
396 })
397 return session, make_url