Merge ~dannf/charms/+source/scalebot-jenkins:go-stop into ~dannf/charms/+source/scalebot-jenkins:master
- Git
- lp:~dannf/charms/+source/scalebot-jenkins
- go-stop
- Merge into master
Status: | Superseded |
---|---|
Proposed branch: | ~dannf/charms/+source/scalebot-jenkins:go-stop |
Merge into: | ~dannf/charms/+source/scalebot-jenkins:master |
Diff against target: |
1007 lines (+919/-2) 14 files modified
.flake8 (+2/-0) .pre-commit-config.yaml (+10/-0) README.devel (+12/-0) README.md (+92/-0) actions.yaml (+2/-0) actions/refreshjobs (+6/-0) bin/jobsync.py (+100/-0) bin/pull-and-reload-jobs (+18/-0) config.yaml (+47/-0) files/known_hosts.lp (+2/-0) layer.yaml (+22/-1) lib/charms/layer/scalebot_jenkins.py (+44/-0) metadata.yaml (+12/-1) reactive/scalebot-jenkins.py (+550/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
dann frazier | Pending | ||
Review via email:
|
Commit message
Description of the change
Unmerged commits
- 13888a4... by dann frazier
-
Block unnecessary reactions when the charm is stopping
If a user removes the scalebot application (`juju remove-
application` ), juju
will trigger the stop hook. We provide a stop hook which destroys the lab
controller, freeing up resources in the cloud. However, the charm continues
to react to other events, which can lead to it *re*-bootstrapping a
controller. This can lead to orphaned resources in the cloud.Introduce a new pair of states[*], "scalebot.go" and "scalebot.stop", which
are intended to be mutually exclusive. The charm will start out and remain
in the "scalebot.go" state until the stop hook is called, at which point
we'll transition to the "scalebot.stop" state. Add guards to prevent
various reactions from executing in "scalebot.stop".[*] The "state" API has been apparently deprecated for a "flag" API for
some time. We should switch over to the "flag" API - but I'll leave
that for another time. - 79a5200... by dann frazier
-
Fix typo and simplify log message
The main goal here is to correct spelling of "bootstrapping", but I also
decided to shorten the message because I'm not sure the second sentence
was adding to the first. - 5f7aec1... by dann frazier
-
Fix reference-
before- assignment issue Seen in debug-log:
unit-scalebot-0: 12:01:15 WARNING unit.scalebot/
0.stop UnboundLocalError: local variable 'modeldefaultsfile' referenced before assignment
unit-scalebot-0: 12:01:16 ERROR juju.worker.uniter. operation hook "stop" (via explicit, bespoke hook script) failed: exit status 1 - fe90de4... by dann frazier
-
Add focal support
This charm deploys fine on Ubuntu 20.04 ('focal'), so designate it to
be a supported series. - 5e2ad36... by dann frazier
-
Remove the python3-maas-client dependency
The scalebot-jenkins charm itself does not know anything about MAAS, it is
cloud-agnostic by design. If a specific scalebot config needs to talk to
a MAAS directly, it should install the necessary dependencies itself
(e.g. using scalebot.d/init). python3-maas-client is no longer available
in Ubuntu 20.04 ('focal'), so this is necessary to support that series. - 1bd653d... by Taihsiang Ho
-
Merge branch 'mr-remote-lxd' into master
- 1454aa4... by Taihsiang Ho
-
refactor(
config. yaml): rm useless lxd parameters - 676404c... by Taihsiang Ho
-
feat: integrate juju yaml of remote lxd
By integrating the clouds.yaml and credentials.yaml for remote lxd, we could
merge the bootstraping methods of remote lxd and maas clouds.BREAKING CHANGE: LP merge request: #391092
- 72c3e58... by Taihsiang Ho
-
feat(log): provide more explicit failure messages for debugging of importjenkins
- ca70db6... by Taihsiang Ho
-
style(pre-commit): style fix
Preview Diff
1 | diff --git a/.flake8 b/.flake8 |
2 | new file mode 100644 |
3 | index 0000000..b5039a2 |
4 | --- /dev/null |
5 | +++ b/.flake8 |
6 | @@ -0,0 +1,2 @@ |
7 | +[flake8] |
8 | +ignore = W503, E501 |
9 | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml |
10 | new file mode 100644 |
11 | index 0000000..5f7fa9a |
12 | --- /dev/null |
13 | +++ b/.pre-commit-config.yaml |
14 | @@ -0,0 +1,10 @@ |
15 | +repos: |
16 | + - repo: https://github.com/psf/black |
17 | + rev: stable |
18 | + hooks: |
19 | + - id: black |
20 | + language_version: python3.8 |
21 | + - repo: https://gitlab.com/pycqa/flake8 |
22 | + rev: master |
23 | + hooks: |
24 | + - id: flake8 |
25 | diff --git a/README.devel b/README.devel |
26 | new file mode 100644 |
27 | index 0000000..fbaa6ff |
28 | --- /dev/null |
29 | +++ b/README.devel |
30 | @@ -0,0 +1,12 @@ |
31 | +# Pushing changes to the charm store |
32 | +``` |
33 | +$ charm build |
34 | +$ charm push /tmp/charm-builds/scalebot-jenkins cs:~ce-hyperscale/scalebot-jenkins |
35 | +``` |
36 | +This will return a URL with a new version, e.g.: |
37 | +url: cs:~ce-hyperscale/scalebot-jenkins-7 |
38 | +Use that new URL in the following commands: |
39 | +``` |
40 | +$ charm release <url> |
41 | +$ charm grant <url> everyone |
42 | +``` |
43 | diff --git a/README.md b/README.md |
44 | new file mode 100644 |
45 | index 0000000..43e5e2f |
46 | --- /dev/null |
47 | +++ b/README.md |
48 | @@ -0,0 +1,92 @@ |
49 | +# What is ScaleBot? |
50 | +ScaleBot is a framework for running scheduled jobs against a cloud. It is built upon the Jenkins CI and Juju modeling tools. |
51 | + |
52 | +# QuickStart (using Amazon Web Services) |
53 | +Clone a local copy of the sample ScaleBot configuration repository: |
54 | +``` |
55 | +$ git clone git://git.launchpad.net/~scalebot-team/+git/scalebot-config |
56 | +``` |
57 | +Make a copy of the aws credentials template, and add your account keys: |
58 | +``` |
59 | +$ cd scalebot-config |
60 | +$ cp labs/aws/credentials.yaml.template labs/aws/credentials.yaml |
61 | +$ vi labs/aws/credentials.yaml |
62 | +``` |
63 | +Deploy the ScaleBot bundle. In this example, we'll deploy to local LXD containers: |
64 | +``` |
65 | +$ juju bootstrap localhost |
66 | +Creating Juju controller "localhost-localhost" on localhost/localhost |
67 | +Looking for packaged Juju agent version 2.2.4 for amd64 |
68 | +To configure your system to better support LXD containers, please see: https://github.com/lxc/lxd/blob/master/doc/production-setup.md |
69 | +Launching controller instance(s) on localhost/localhost... |
70 | + - juju-689661-0 (arch=amd64) |
71 | +Fetching Juju GUI 2.9.2 |
72 | +Waiting for address |
73 | +Attempting to connect to 10.98.200.35:22 |
74 | +Bootstrap agent now started |
75 | +Contacting Juju controller at 10.98.200.35 to verify accessibility... |
76 | +Bootstrap complete, "localhost-localhost" controller now available. |
77 | +Controller machines are in the "controller" model. |
78 | +Initial model "default" added. |
79 | +$ juju deploy bundle.yaml |
80 | +Deploying charm "cs:xenial/jenkins-4" |
81 | +Deploying charm "cs:~ce-hyperscale/scalebot-jenkins-3" |
82 | +Related "jenkins:extension" and "scalebot:extension" |
83 | +Deploy of bundle completed. |
84 | +``` |
85 | +Once deployment has completed, use ```juju status``` to find the IP of your scalebot jenkins server. You can connect to it at http://ip-of-server:8080. The sample configuration includes an example "Hello World" job. Building this job will deploy a node in the Lab cloud and run ```echo "Hello World"``` on it. |
86 | + |
87 | +# Architecture |
88 | +ScaleBot is deployed from a Juju charm that builds a running instance of Jenkins CI, initialized with a git repository that provides job definitions and a target cloud to run those jobs against. We refer to this target cloud as a "lab". ScaleBot will use the target cloud ("lab") configuration to automatically setup an *internal* Juju controller which Jenkins jobs can use to deploy systems and workloads as necessary within the lab. |
89 | + |
90 | +<!-- To edit this image, modify the following image and select |
91 | + "Publish to the web..." |
92 | + https://docs.google.com/drawings/d/1BPKUyUMAy8PfAf81397eAKD8shFQT3D9uuqDW4yfRCg/edit --> |
93 | + |
94 | +![alt text](https://docs.google.com/drawings/d/e/2PACX-1vS0RwIg_CyMTKzJlnRJTf9wrH4e4v3W8wbMsI4RwEcB1FuvclU8Q7X4NPVOLSzYWtGBcXERMVI-PY7p/pub?w=935&h=527 "ScaleBot Diagram A") |
95 | + |
96 | +Of course, since scalebot-jenkins is a charm itself, it also is deployed in a cloud. This can just be a LXD cloud though, as the controller and Jenkins server can easily run in containers. The important thing is that the jenkins server in the ScaleBot cloud is on a network that can access the Lab cloud. |
97 | + |
98 | +<!-- To edit this image, modify the following image and select |
99 | + "Publish to the web..." |
100 | + https://docs.google.com/drawings/d/1mb8oea6fmZtTmnC-gpMZgmsGnsaMPaO4XELZv0jNRFo/edit --> |
101 | + |
102 | +![alt text](https://docs.google.com/drawings/d/e/2PACX-1vRNzqlTKIU57N-cdSA1L8M-20zxUbwMxteOAQEs321NrER-EkSEhbb1P-qZc8V_p-dUq1lTmsTl5IHq/pub?w=1016&h=508 "ScaleBot Diagram B") |
103 | + |
104 | +# Configuration Repository |
105 | +The scalebot-jenkins charm accepts configuration that tells ScaleBot where to fetch Jenkins job and Lab Cloud definitions. This data must be in a git repository with the following layout: |
106 | + |
107 | +``` |
108 | +jobs/<jobfoo>.yaml |
109 | +jobs/<jobbar>.yaml |
110 | +... |
111 | +jobs/<jobbaz>.yaml |
112 | + |
113 | +labs/<labname>/<labname>.yaml |
114 | +labs/<labname>/clouds.yaml |
115 | +labs/<labname>/credentials.yaml |
116 | +labs/<labname>/model-defaults.yaml |
117 | +scalebot.d/init |
118 | +scalebot.d/refresh |
119 | +``` |
120 | + |
121 | +Jobs must be in the YAML format used by [Jenkins Job Builder](https://docs.openstack.org/infra/jenkins-job-builder), as that is the tool used to load them into Jenkins. The clouds.yaml, credentials.yaml, and model-defaults.yaml files are passed directly to Juju when bootsrapping the Lab controller, so see [Juju documentation](http://jujucharms.com/docs) for details on their contents. |
122 | + |
123 | +You may also want to store code in this repository and call it from your jobs. |
124 | + |
125 | + |
126 | +# Job Environment |
127 | +ScaleBot will automatically create an empty model prior to building a jenkins job. This model will have a unique name, based on the $BUILD_ID. Jobs can assume that this model exists and is in-focus prior to executing, so your build code can just begin deploying charms immediately on startup. |
128 | + |
129 | +Since it is expected that builds will deploy systems using Juju, the python [bindings for juju](https://github.com/juju/python-libjuju) are pre-installed on the system. |
130 | + |
131 | +## Environment Variables |
132 | +In addition to the environment variables set by Jenkins, the following variable is also available: |
133 | + |
134 | +**$SCALEBOT_REPO** will be set to the path of the locally cloned copy of the Scalebot configuration repository. |
135 | + |
136 | +## scalebot.d/init hook |
137 | +If it exists, ```$SCALEBOT_REPO/scalebot.d/init``` will be executed each time the configuration repository is refreshed. You can use this to install any additional dependencies needed for your jobs. |
138 | + |
139 | +## scalebot.d/refresh hook |
140 | +If it exists, ```$SCALEBOT_REPO/scalebot.d/refresh``` will be executed after the repository is updated, and before loading jobs. This is useful in case something job yaml files need to be dynamically generated. |
141 | diff --git a/actions.yaml b/actions.yaml |
142 | new file mode 100644 |
143 | index 0000000..6d6949f |
144 | --- /dev/null |
145 | +++ b/actions.yaml |
146 | @@ -0,0 +1,2 @@ |
147 | +refreshjobs: |
148 | + descripton: Perform a Git Pull and Refresh the Jenkins Jobs if any. |
149 | diff --git a/actions/refreshjobs b/actions/refreshjobs |
150 | new file mode 100755 |
151 | index 0000000..21e3870 |
152 | --- /dev/null |
153 | +++ b/actions/refreshjobs |
154 | @@ -0,0 +1,6 @@ |
155 | +#!/bin/sh |
156 | + |
157 | +set -e |
158 | +set -x |
159 | + |
160 | +su - jenkins -c "${JUJU_CHARM_DIR}/bin/pull-and-reload-jobs" |
161 | diff --git a/bin/jobsync.py b/bin/jobsync.py |
162 | new file mode 100755 |
163 | index 0000000..108c8ac |
164 | --- /dev/null |
165 | +++ b/bin/jobsync.py |
166 | @@ -0,0 +1,100 @@ |
167 | +#!/usr/bin/env python3 |
168 | +import argparse |
169 | +import os |
170 | +import logging |
171 | +import subprocess |
172 | +import sys |
173 | + |
174 | +SCALEBOT_HOME = "/srv/scalebot" |
175 | +SCALEBOT_REPO = os.path.join(SCALEBOT_HOME, "repo") |
176 | + |
177 | + |
178 | +def syncjobs(JOBSET): |
179 | + jjb = "/usr/local/bin/jenkins-jobs" |
180 | + # Load jenkinsjobs config |
181 | + config_ini = "/etc/jenkins_jobs/jenkins_jobs.ini" |
182 | + # common directory to add jjb Macro |
183 | + COMMON = SCALEBOT_REPO + "/jobs/common" |
184 | + cmd = [jjb, "--flush-cache", "--conf", config_ini, "update"] |
185 | + flag = [] |
186 | + if os.path.exists(COMMON): |
187 | + jobs = "{}/jobs/{}:{}".format(SCALEBOT_REPO, JOBSET, COMMON) |
188 | + flag = ["-r", jobs] |
189 | + else: |
190 | + jobs = "{}/jobs/{}".format(SCALEBOT_REPO, JOBSET) |
191 | + flag = [jobs] |
192 | + subprocess.run(cmd + flag, check=True) |
193 | + |
194 | + |
195 | +def main(): |
196 | + |
197 | + parser = argparse.ArgumentParser() |
198 | + |
199 | + parser.add_argument( |
200 | + "-s", |
201 | + "--syncjobs", |
202 | + help=( |
203 | + "This will import jenkins jobs via yaml from \ |
204 | + $SCALEBOT_REPO/jobs/" |
205 | + ), |
206 | + required=False, |
207 | + action="store_true", |
208 | + ) |
209 | + parser.add_argument( |
210 | + "-d", |
211 | + "--deletejobs", |
212 | + help=( |
213 | + "This will Delete all jobs from Hudson, useful \ |
214 | + or refreshing job list after branch change." |
215 | + ), |
216 | + required=False, |
217 | + action="store_true", |
218 | + ) |
219 | + parser.add_argument( |
220 | + "-j", |
221 | + "--jobset", |
222 | + help=( |
223 | + "Name of the desired jobset from which to import \ |
224 | + $SCALEBOT_REPO/jobs/$JOBSET" |
225 | + ), |
226 | + required=True, |
227 | + type=str, |
228 | + ) |
229 | + |
230 | + args = parser.parse_args() |
231 | + |
232 | + # If no args specified upon calling tool, dump help and exit |
233 | + if len(sys.argv[1:]) == 0: |
234 | + parser.print_help() |
235 | + parser.exit() |
236 | + |
237 | + try: |
238 | + if args.deletejobs: |
239 | + # Delete the jobs completely from Jenkins |
240 | + logger.info("Removing Jenkins Jobs from Hudson") |
241 | + cmd = subprocess.Popen( |
242 | + ["jenkins-jobs", "delete-all"], |
243 | + stdin=subprocess.PIPE, |
244 | + stdout=subprocess.PIPE, |
245 | + ) |
246 | + cmd.communicate(input="Y".encode()) |
247 | + logger.info("Hudson Jobs Deleted") |
248 | + |
249 | + if args.syncjobs: |
250 | + JOBSET = args.jobset |
251 | + logger.info("Importing Jenkins Jobs") |
252 | + syncjobs(JOBSET) |
253 | + |
254 | + logger.info("Import Jobs Complete") |
255 | + |
256 | + except Exception as e: |
257 | + logger.info("Problem with job import") |
258 | + logger.info(e) |
259 | + return "-1" |
260 | + |
261 | + |
262 | +logging.basicConfig(level=logging.INFO) |
263 | +logger = logging.getLogger(__name__) |
264 | + |
265 | +if __name__ == "__main__": |
266 | + sys.exit(main()) |
267 | diff --git a/bin/pull-and-reload-jobs b/bin/pull-and-reload-jobs |
268 | new file mode 100755 |
269 | index 0000000..330f824 |
270 | --- /dev/null |
271 | +++ b/bin/pull-and-reload-jobs |
272 | @@ -0,0 +1,18 @@ |
273 | +#!/bin/sh |
274 | + |
275 | +# Pull the git repo and refresh jobs |
276 | + |
277 | +set -e |
278 | +set -x |
279 | + |
280 | +[ -n "$SCALEBOT_HOME" ] |
281 | +[ -n "$SCALEBOT_JOBSET" ] |
282 | +[ -n "$SCALEBOT_REPO" ] |
283 | + |
284 | +git -C ${SCALEBOT_REPO} pull --ff-only |
285 | +refresh="${SCALEBOT_REPO}/scalebot.d/refresh" |
286 | +if [ -x "$refresh" ]; then |
287 | + $refresh |
288 | +fi |
289 | +jobsync="${SCALEBOT_HOME}/bin/jobsync.py" |
290 | +${jobsync} -j ${SCALEBOT_JOBSET} -s |
291 | diff --git a/config.yaml b/config.yaml |
292 | new file mode 100644 |
293 | index 0000000..98588c7 |
294 | --- /dev/null |
295 | +++ b/config.yaml |
296 | @@ -0,0 +1,47 @@ |
297 | +options: |
298 | + scalebot_private_ssh_key: |
299 | + description: A null-passphrase SSH private key with access to $scalebot_repo |
300 | + type: "string" |
301 | + default: |
302 | + scalebot_config_repo: |
303 | + description: URI to a ScaleBot config git repository |
304 | + type: "string" |
305 | + default: git://git.launchpad.net/~scalebot-team/+git/scalebot-config |
306 | + scalebot_config_branch: |
307 | + description: The branch of $scalebot_repo to checkout |
308 | + type: "string" |
309 | + default: master |
310 | + scalebot_juju_clouds: |
311 | + description: Juju cloud config YAML for target cloud |
312 | + type: "string" |
313 | + default: |
314 | + scalebot_juju_credentials: |
315 | + description: Credentials YAML for Juju cloud defined in scalebot_juju_clouds |
316 | + type: "string" |
317 | + default: |
318 | + scalebot_juju_model_defaults: |
319 | + description: Juju model defaults configuration YAML |
320 | + type: "string" |
321 | + default: |
322 | + scalebot_juju_bootstrap_constraints: |
323 | + description: Bootstrap constraints for Juju controller |
324 | + type: "string" |
325 | + default: |
326 | + scalebot_lab_type: |
327 | + description: Bootstrap lab type for Juju controller |
328 | + type: "string" |
329 | + default: maas |
330 | + scalebot_jobset: |
331 | + description: Desired jobset to import jenkins jobs from |
332 | + type: "string" |
333 | + default: |
334 | + scalebot_production: |
335 | + description: Is this a production (vs. a development) deployment? When set to true, tests will find SCALEBOT_PRODUCTION=1 in their environment. This gives tests an option to behave differently in production vs. development environments. |
336 | + type: "boolean" |
337 | + default: false |
338 | + install_sources: |
339 | + default: | |
340 | + - ppa:juju/stable |
341 | + install_keys: |
342 | + default: | |
343 | + - null |
344 | diff --git a/files/known_hosts.lp b/files/known_hosts.lp |
345 | new file mode 100644 |
346 | index 0000000..6c51a1e |
347 | --- /dev/null |
348 | +++ b/files/known_hosts.lp |
349 | @@ -0,0 +1,2 @@ |
350 | +|1|dHt6eHIAjzK73nzI0mQEboOUmsg=|dngXa3BXrbOi2e8ATMU9VUD6A9E= ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDEFREwBD2ye2Xrc2SVcUmmJ44MF1BCB3W11NTaiqzVj7XZnQmgWZk9UadHVY2wBXvelcDO51MPN5ozJjFAknw09rP7XMRJMlAOLSIVoU6DRF1u1j8kJVY+dfiDHheS7+siADnrmb8HGn2xQQ6EJDjAXrw1x58x5eZjQ0PFWdI+pRTdYGvWkpHdXKFO6a9/lDx4uo9MCnePEGi/QnkCmKqLCBUlYNZYRiB8nVee2tMF0mjV8xk1rJ+/UP+897+FXFR9w/B1EPRjiQ35ZNQZKPP4isxPtyMuCQkZY7ckWr5YsylNfvNcyGDnO1XazZhJ71rzDpi1RmnFXBW5i+2dm2y7 |
351 | +|1|CWLqG45/xn+VEnXUvak6ST5YuiQ=|ZwtFngsyHwu3XLlLeu/JGjQijis= ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDEFREwBD2ye2Xrc2SVcUmmJ44MF1BCB3W11NTaiqzVj7XZnQmgWZk9UadHVY2wBXvelcDO51MPN5ozJjFAknw09rP7XMRJMlAOLSIVoU6DRF1u1j8kJVY+dfiDHheS7+siADnrmb8HGn2xQQ6EJDjAXrw1x58x5eZjQ0PFWdI+pRTdYGvWkpHdXKFO6a9/lDx4uo9MCnePEGi/QnkCmKqLCBUlYNZYRiB8nVee2tMF0mjV8xk1rJ+/UP+897+FXFR9w/B1EPRjiQ35ZNQZKPP4isxPtyMuCQkZY7ckWr5YsylNfvNcyGDnO1XazZhJ71rzDpi1RmnFXBW5i+2dm2y7 |
352 | diff --git a/layer.yaml b/layer.yaml |
353 | index 7623e14..413895f 100644 |
354 | --- a/layer.yaml |
355 | +++ b/layer.yaml |
356 | @@ -1 +1,22 @@ |
357 | -includes: ['layer:jenkins-charm'] |
358 | +includes: |
359 | + - layer:basic |
360 | + - layer:apt |
361 | + - interface:jenkins-extension |
362 | +repo: git+ssh://git.launchpad.net/~ce-hyperscale/charms/+source/scalebot-jenkins |
363 | +options: |
364 | + basic: |
365 | + use_venv: false |
366 | + python_packages: |
367 | + - python-jenkins==0.4.15 |
368 | + - jenkins-job-builder |
369 | + - async-timeout |
370 | + - juju |
371 | + packages: |
372 | + - libssl-dev |
373 | + - libffi-dev |
374 | + - python3-cffi |
375 | + - python3-nacl |
376 | + apt: |
377 | + packages: |
378 | + - git |
379 | + - juju |
380 | diff --git a/lib/charms/layer/scalebot_jenkins.py b/lib/charms/layer/scalebot_jenkins.py |
381 | new file mode 100644 |
382 | index 0000000..6a2d81f |
383 | --- /dev/null |
384 | +++ b/lib/charms/layer/scalebot_jenkins.py |
385 | @@ -0,0 +1,44 @@ |
386 | +from subprocess import CalledProcessError |
387 | +from subprocess import check_call |
388 | + |
389 | + |
390 | +__all__ = ["get_bootstrap_failure_hint"] |
391 | + |
392 | + |
393 | +def get_bootstrap_failure_hint(msg): |
394 | + """ |
395 | + Check input message and return a guess of the root cause of error. |
396 | + |
397 | + Parameters |
398 | + ---------- |
399 | + msg : string |
400 | + A message string. It is often stdout or stderr returned by a subprocess command. |
401 | + |
402 | + Returns |
403 | + ------- |
404 | + string |
405 | + The description of possible failure root cause. |
406 | + |
407 | + """ |
408 | + rtn_msg_prefix = "Failed for:" |
409 | + if ( |
410 | + "cannot start bootstrap instance: failed to acquire node" in msg |
411 | + and "No such tag" in msg |
412 | + ): |
413 | + return f"{rtn_msg_prefix} not finding nodes with specified tags. You may want to check your bootstrap constraint." |
414 | + elif "bootstrap instance started but did not change to Deployed state" in msg: |
415 | + return f"{rtn_msg_prefix} bootstrap instance started but did not change to Deployed state." |
416 | + elif "failed to acquire node: No available machine matches constraints.": |
417 | + return f"{rtn_msg_prefix} failed to acquire node: No available machine matches constraints." |
418 | + else: |
419 | + return f"{rtn_msg_prefix} unknown reason." |
420 | + |
421 | + |
422 | +def if_controller(): |
423 | + cmd = ["/bin/su", "-", "jenkins", "-c", "juju controllers"] |
424 | + try: |
425 | + check_call(cmd) |
426 | + return True |
427 | + |
428 | + except CalledProcessError: |
429 | + return False |
430 | diff --git a/metadata.yaml b/metadata.yaml |
431 | index b57c212..7451207 100644 |
432 | --- a/metadata.yaml |
433 | +++ b/metadata.yaml |
434 | @@ -1,5 +1,16 @@ |
435 | name: scalebot-jenkins |
436 | summary: Automation Server for ScaleBot |
437 | -maintainer: dann frazier <dann.frazier@canonical.com> |
438 | +maintainer: canonical-hyperscale-team@lists.canonical.com |
439 | description: | |
440 | Automates jobs in a ScaleBot Lab. |
441 | +tags: |
442 | + - ops |
443 | +series: |
444 | + - focal |
445 | + - bionic |
446 | +subordinate: true |
447 | +requires: |
448 | + extension: |
449 | + interface: jenkins-extension |
450 | + scope: container |
451 | + |
452 | diff --git a/reactive/scalebot-jenkins.py b/reactive/scalebot-jenkins.py |
453 | new file mode 100644 |
454 | index 0000000..9a8a2b6 |
455 | --- /dev/null |
456 | +++ b/reactive/scalebot-jenkins.py |
457 | @@ -0,0 +1,550 @@ |
458 | +import os |
459 | +import shutil |
460 | +import stat |
461 | +import subprocess |
462 | +import tempfile |
463 | +import requests |
464 | +import jenkins |
465 | +import io |
466 | +import yaml |
467 | + |
468 | +from subprocess import CalledProcessError |
469 | +from charmhelpers.core import hookenv |
470 | +from charmhelpers.core.host import service_restart |
471 | +from charmhelpers.core.hookenv import ( |
472 | + log, |
473 | + status_set, |
474 | + ERROR, |
475 | + WARNING, |
476 | + DEBUG, |
477 | +) |
478 | +from charms.reactive import ( |
479 | + hook, |
480 | + when, |
481 | + when_all, |
482 | + when_none, |
483 | + when_not, |
484 | + set_state, |
485 | + get_flags, |
486 | + remove_state, |
487 | +) |
488 | +from charms.reactive.helpers import data_changed |
489 | +from charms.layer import scalebot_jenkins |
490 | + |
491 | +SCALEBOT_URL = "http://localhost:8080" |
492 | +SCALEBOT_HOME = "/srv/scalebot" |
493 | +SCALEBOT_REPO = os.path.join(SCALEBOT_HOME, "repo") |
494 | +PYTHONPATH = os.path.join(SCALEBOT_REPO, "lib") |
495 | + |
496 | + |
497 | +def _run_cmd_yaml(args): |
498 | + """Runs a command, assuming it will send some valid YAML data to stdout, |
499 | + and parses its output. All the output will be loaded in memory. |
500 | + """ |
501 | + |
502 | + proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
503 | + try: |
504 | + rstdout, rstderr = proc.communicate(timeout=15) |
505 | + except subprocess.TimeoutExpired as exc: |
506 | + proc.kill() |
507 | + raise exc |
508 | + else: |
509 | + if proc.returncode != 0: |
510 | + raise Exception("Command error: " + str(rstderr)) |
511 | + return yaml.load(io.BytesIO(rstdout), Loader=yaml.SafeLoader) |
512 | + |
513 | + |
514 | +@when_not("scalebot.jjb.workaround.installed") |
515 | +def scalebot_install_jjb_workaround(): |
516 | + cfgfile = "/etc/default/jenkins" |
517 | + tmpfile = "%s.scalebot.tmp" % (cfgfile) |
518 | + |
519 | + snippet = [ |
520 | + "# ScaleBot Workaround for https://storyboard.openstack.org/#!/story/2006489\n", |
521 | + 'JAVA_ARGS+=" -Dhudson.security.csrf.DefaultCrumbIssuer.EXCLUDE_SESSION_ID=true"\n', |
522 | + ] |
523 | + |
524 | + with open(cfgfile, "r") as f: |
525 | + cfg = f.readlines() |
526 | + with open(tmpfile, "w+") as f: |
527 | + for line in cfg: |
528 | + if line in snippet: |
529 | + continue |
530 | + f.write("%s" % (line)) |
531 | + f.writelines(snippet) |
532 | + shutil.copymode(cfgfile, tmpfile) |
533 | + os.rename(tmpfile, cfgfile) |
534 | + |
535 | + # Do a full service restart for Jenkins. We can't do a simple safe |
536 | + # restart yet because we need this workaround to connect to the API. |
537 | + # We only can do this on deployment, otherwise jobs can be disrupted. |
538 | + service_restart("jenkins") |
539 | + |
540 | + set_state("scalebot.jjb.workaround.installed") |
541 | + |
542 | + |
543 | +@when_all("scalebot.go", "extension.available") |
544 | +def configure_jenkins_jobbuilder(extension): |
545 | + remove_state("scalebot.configured.jobbuilder") |
546 | + status_set("maintenance", "Configuring JobBuilder") |
547 | + if not os.path.isdir("/etc/jenkins_jobs"): |
548 | + os.mkdir("/etc/jenkins_jobs") |
549 | + |
550 | + jj_ini = "/etc/jenkins_jobs/jenkins_jobs.ini" |
551 | + if os.path.isfile(jj_ini): |
552 | + os.remove(jj_ini) |
553 | + |
554 | + config = extension.get_connection_info() |
555 | + username = config["admin_username"] |
556 | + password = config["admin_password"] |
557 | + |
558 | + jenkinsini = open(jj_ini, "w") |
559 | + jenkinsini.write( |
560 | + """[jenkins]\nuser=%s\n |
561 | +password=%s |
562 | +url=%s |
563 | +""" |
564 | + % (username, password, SCALEBOT_URL) |
565 | + ) |
566 | + jenkinsini.close() |
567 | + |
568 | + status_set( |
569 | + "maintenance", "Configured JobBuilder. Waiting for Scalebot Juju controller." |
570 | + ) |
571 | + |
572 | + log(scalebot_jenkins.if_controller(), DEBUG) |
573 | + |
574 | + if scalebot_jenkins.if_controller(): |
575 | + status_set( |
576 | + "maintenance", "Configured JobBuilder and found Scalebot Juju controllers." |
577 | + ) |
578 | + set_state("scalebot.configured.jobbuilder") |
579 | + log("scalebot.configured.jobbuilder is set.") |
580 | + |
581 | + flags = get_flags() |
582 | + log(f"current flags: {flags}", DEBUG) |
583 | + else: |
584 | + status_set( |
585 | + "waiting", "Configured JobBuilder. Waiting for Scalebot Juju controller." |
586 | + ) |
587 | + |
588 | + |
589 | +@when("extension.changed") |
590 | +def unconfigure_jobbuilder(): |
591 | + remove_state("scalebot.configured.jobbuilder") |
592 | + |
593 | + |
594 | +@hook("config-changed") |
595 | +def configure_scalebot_env(): |
596 | + config = hookenv.config() |
597 | + jobset = config.get("scalebot_jobset") |
598 | + sbprofile = "/etc/profile.d/scalebot.sh" |
599 | + |
600 | + with open(sbprofile, "w") as f: |
601 | + f.write("#Export Scalebot required paths\n") |
602 | + f.write("export PYTHONPATH=%s\n" % (PYTHONPATH)) |
603 | + f.write('export SCALEBOT_HOME="%s"\n' % (SCALEBOT_HOME)) |
604 | + f.write('export SCALEBOT_REPO="%s"\n' % (SCALEBOT_REPO)) |
605 | + f.write('export SCALEBOT_JOBSET="%s"\n' % (jobset)) |
606 | + prod = int(config.get("scalebot_production")) |
607 | + f.write("export SCALEBOT_PRODUCTION=%d\n" % (prod)) |
608 | + |
609 | + # We need to restart to get this environment. |
610 | + remove_state("scalebot.configured.env") |
611 | + set_state("scalebot.written.env") |
612 | + |
613 | + |
614 | +def scalebot_link_files_into_home(): |
615 | + # Link files used outside of charm context into $SCALEBOT_HOME |
616 | + src = os.path.join(hookenv.charm_dir(), "bin") |
617 | + dest = os.path.join(SCALEBOT_HOME, "bin") |
618 | + if os.path.islink(dest): |
619 | + os.unlink(dest) |
620 | + os.symlink(src, dest) |
621 | + |
622 | + |
623 | +def scalebot_git_config_check(): |
624 | + required_configs = ["scalebot_config_repo", "scalebot_config_branch"] |
625 | + missing_configs = [] |
626 | + config = hookenv.config() |
627 | + for c in required_configs: |
628 | + if not config.get(c): |
629 | + missing_configs.append(c) |
630 | + if len(missing_configs) > 0: |
631 | + status_set( |
632 | + "blocked", |
633 | + "Unable to continue due to missing configs: %s" |
634 | + % ", ".join(missing_configs), |
635 | + ) |
636 | + return False |
637 | + return True |
638 | + |
639 | + |
640 | +@when_all("scalebot.go", "apt.installed.git") |
641 | +def scalebot_git_clone(): |
642 | + if not scalebot_git_config_check(): |
643 | + return |
644 | + |
645 | + config = hookenv.config() |
646 | + repo = config.get("scalebot_config_repo") |
647 | + branch = config.get("scalebot_config_branch") |
648 | + ssh_key = config.get("scalebot_private_ssh_key") |
649 | + |
650 | + if not data_changed("scalebot.git.config", [repo, branch, ssh_key]): |
651 | + log("Repo config has not changed, nothing to do.") |
652 | + return True |
653 | + |
654 | + log("Scalebot repo config has changed, (re-)cloning.") |
655 | + remove_state("scalebot.configured.repo") |
656 | + status_set("maintenance", "Cloning Scalebot repository") |
657 | + |
658 | + try: |
659 | + shutil.rmtree(SCALEBOT_HOME) |
660 | + except FileNotFoundError: |
661 | + pass |
662 | + |
663 | + os.makedirs(SCALEBOT_HOME) |
664 | + shutil.chown(SCALEBOT_HOME, user="jenkins", group="jenkins") |
665 | + scalebot_link_files_into_home() |
666 | + |
667 | + known_hosts = os.path.join(hookenv.charm_dir(), "files", "known_hosts.lp") |
668 | + |
669 | + git_ssh = "" |
670 | + keypath = os.path.join(SCALEBOT_HOME, "key") |
671 | + if ssh_key: |
672 | + log("Writing ssh key to %s" % (keypath)) |
673 | + with os.fdopen( |
674 | + os.open(keypath, os.O_WRONLY | os.O_CREAT, mode=0o600), "w" |
675 | + ) as keyfile: |
676 | + shutil.chown(keypath, user="jenkins", group="jenkins") |
677 | + keyfile.write(ssh_key) |
678 | + ssh_cmd = "/usr/bin/ssh -v -i %s -o UserKnownHostsFile=%s" % ( |
679 | + keypath, |
680 | + known_hosts, |
681 | + ) |
682 | + git_ssh = '-c core.sshCommand="%s"' % (ssh_cmd) |
683 | + else: |
684 | + try: |
685 | + os.unlink(keypath) |
686 | + except FileNotFoundError: |
687 | + pass |
688 | + |
689 | + git_cmd = "/usr/bin/git clone %s -b %s %s %s" % ( |
690 | + git_ssh, |
691 | + branch, |
692 | + repo, |
693 | + os.path.join(SCALEBOT_HOME, "repo"), |
694 | + ) |
695 | + |
696 | + su_git_cmd = ["/bin/su", "-", "jenkins", "-c", git_cmd] |
697 | + log("Beginning git clone") |
698 | + log(su_git_cmd) |
699 | + subprocess.run(su_git_cmd, check=True) |
700 | + |
701 | + set_state("scalebot.configured.repo") |
702 | + remove_state("scalebot.repo.init") |
703 | + |
704 | + |
705 | +@when_not("scalebot.repo.init", "scalebot.stop") |
706 | +def scalebot_repo_init(): |
707 | + init_path = os.path.join(SCALEBOT_REPO, "scalebot.d", "init") |
708 | + if os.path.exists(init_path): |
709 | + status_set("maintenance", "Executing %s" % (init_path)) |
710 | + subprocess.check_call(init_path) |
711 | + set_state("scalebot.repo.init") |
712 | + |
713 | + |
714 | +@hook("config-changed") |
715 | +def scalebot_git_reconfig(): |
716 | + config = hookenv.config() |
717 | + required_configs = [ |
718 | + "scalebot_private_ssh_key", |
719 | + "scalebot_config_repo", |
720 | + "scalebot_config_branch", |
721 | + ] |
722 | + if any(config.changed(key) for key in required_configs): |
723 | + scalebot_git_clone() |
724 | + |
725 | + |
726 | +@when_all( |
727 | + "scalebot.configured.repo", |
728 | + "scalebot.configured.jobbuilder", |
729 | + "scalebot.configured.juju", |
730 | + "scalebot.go", |
731 | +) |
732 | +def scalebot_ready(): |
733 | + status_set("active", "ScaleBot is Ready") |
734 | + log("Congratulations!! Scalebot is ready. Go testing go!!") |
735 | + |
736 | + |
737 | +@when_all( |
738 | + "scalebot.configured.repo", |
739 | + "scalebot.configured.jobbuilder", |
740 | + "scalebot.configured.env", |
741 | + "scalebot.go", |
742 | + "scalebot.jjb.workaround.installed", |
743 | + "scalebot.jenkins-http-ready", |
744 | +) |
745 | +def importjenkins(): |
746 | + cmd = os.path.join(hookenv.charm_dir(), "actions", "refreshjobs") |
747 | + log(cmd, DEBUG) |
748 | + try: |
749 | + subprocess.run( |
750 | + [cmd], check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, |
751 | + ) |
752 | + except CalledProcessError as e: |
753 | + msg_stdout = e.stdout.decode() |
754 | + log(msg_stdout, level=ERROR) |
755 | + |
756 | + |
757 | +@hook("config-changed") |
758 | +def scalebot_juju_reconfig(): |
759 | + config = hookenv.config() |
760 | + required_configs = [ |
761 | + "scalebot_juju_clouds", |
762 | + "scalebot_juju_credentials", |
763 | + "scalebot_juju_model_defaults", |
764 | + ] |
765 | + if any(config.changed(key) for key in required_configs): |
766 | + scalebot_configure_juju() |
767 | + |
768 | + |
769 | +@when_all("apt.installed.juju", "scalebot.go") |
770 | +def scalebot_configure_juju(): |
771 | + config = hookenv.config() |
772 | + |
773 | + clouds = config.get("scalebot_juju_clouds") |
774 | + creds = config.get("scalebot_juju_credentials") |
775 | + modeldefaults = config.get("scalebot_juju_model_defaults") |
776 | + bootstrapconstraints = config.get("scalebot_juju_bootstrap_constraints") |
777 | + |
778 | + if ( |
779 | + not data_changed("scalebot.juju.config", [clouds, creds]) |
780 | + and scalebot_jenkins.if_controller() |
781 | + ): |
782 | + log( |
783 | + "Juju controller is available and its config has not changed, nothing to do." |
784 | + ) |
785 | + |
786 | + return True |
787 | + |
788 | + if data_changed("scalebot.juju.config", [clouds, creds]): |
789 | + log("Juju config has changed, (re-)bootstrapping.") |
790 | + elif not scalebot_jenkins.if_controller(): |
791 | + log("Scalebot Juju controller is not ready, (re-)bootstrapping.") |
792 | + else: |
793 | + log("Scalebot Juju controller is going to be deployed, (re-)bootstrapping.") |
794 | + |
795 | + remove_state("scalebot.configured.juju") |
796 | + status_set("maintenance", "Bootstrapping Juju controller") |
797 | + |
798 | + # In case we're reconfiguring, destroy any pre-existing |
799 | + # controller before re-bootstrapping |
800 | + subprocess.run( |
801 | + ["/bin/su", "-", "jenkins", "-c", "juju destroy-controller jenkins -y"], |
802 | + check=False, |
803 | + ) |
804 | + |
805 | + scalebot_configure_juju_for_lab(clouds, creds, modeldefaults, bootstrapconstraints) |
806 | + |
807 | + |
808 | +def scalebot_configure_juju_for_lab(clouds, creds, modeldefaults, bootstrapconstraints): |
809 | + if clouds: |
810 | + cloudsfile = tempfile.NamedTemporaryFile(mode="w", delete=False) |
811 | + cloudsfile.write(clouds) |
812 | + # Make it readable by user jenkins |
813 | + os.chmod( |
814 | + cloudsfile.name, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH |
815 | + ) |
816 | + cloudsfile.close() |
817 | + curr_clouds = _run_cmd_yaml( |
818 | + ["/bin/su", "-", "jenkins", "-c", "juju list-clouds --format yaml"] |
819 | + ) |
820 | + if curr_clouds and ("scalebot" in curr_clouds): |
821 | + command = "juju update-cloud --client scalebot -f %s" % (cloudsfile.name) |
822 | + else: |
823 | + command = "juju add-cloud --client scalebot %s" % (cloudsfile.name) |
824 | + subprocess.run(["/bin/su", "-", "jenkins", "-c", command], check=True) |
825 | + os.unlink(cloudsfile.name) |
826 | + |
827 | + if creds: |
828 | + credsfile = tempfile.NamedTemporaryFile(mode="w", delete=False) |
829 | + credsfile.write(creds) |
830 | + # Make it readable by user jenkins |
831 | + os.chmod( |
832 | + credsfile.name, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH |
833 | + ) |
834 | + credsfile.close() |
835 | + curr_creds = _run_cmd_yaml( |
836 | + ["/bin/su", "-", "jenkins", "-c", "juju show-credentials --format yaml"] |
837 | + ) |
838 | + if ( |
839 | + curr_creds |
840 | + and ("client-credentials" in curr_creds) |
841 | + and ("scalebot" in curr_creds["client-credentials"]) |
842 | + ): |
843 | + command = "juju update-credential --client -f %s scalebot" % ( |
844 | + credsfile.name |
845 | + ) |
846 | + else: |
847 | + command = "juju add-credential --client -f %s scalebot" % (credsfile.name) |
848 | + subprocess.run(["/bin/su", "-", "jenkins", "-c", command], check=True) |
849 | + os.unlink(credsfile.name) |
850 | + |
851 | + modeldefaultsparam = "" |
852 | + if modeldefaults: |
853 | + modeldefaultsfile = tempfile.NamedTemporaryFile(mode="w", delete=False) |
854 | + modeldefaultsfile.write(modeldefaults) |
855 | + os.chmod( |
856 | + modeldefaultsfile.name, |
857 | + stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH, |
858 | + ) |
859 | + modeldefaultsfile.close() |
860 | + modeldefaultsparam = " --model-default %s" % (modeldefaultsfile.name) |
861 | + |
862 | + bconstraints = "" |
863 | + if bootstrapconstraints: |
864 | + bconstraints = ' --bootstrap-constraints "%s"' % (bootstrapconstraints) |
865 | + |
866 | + try: |
867 | + subprocess.run( |
868 | + [ |
869 | + "/bin/su", |
870 | + "-", |
871 | + "jenkins", |
872 | + "-c", |
873 | + "juju bootstrap --debug scalebot jenkins" |
874 | + + modeldefaultsparam |
875 | + + bconstraints, |
876 | + ], |
877 | + check=True, |
878 | + stdout=subprocess.PIPE, |
879 | + stderr=subprocess.STDOUT, |
880 | + ) |
881 | + |
882 | + if modeldefaults: |
883 | + log(f"Removing {modeldefaultsfile.name}", DEBUG) |
884 | + os.unlink(modeldefaultsfile.name) |
885 | + log(f"Removed {modeldefaultsfile.name}", DEBUG) |
886 | + |
887 | + log( |
888 | + "Bootstrapping of Scalebot Juju controller looks good." |
889 | + ) |
890 | + |
891 | + except CalledProcessError as e: |
892 | + msg_stdout = e.stdout.decode() |
893 | + log(msg_stdout, level=ERROR) |
894 | + |
895 | + msg_guess = scalebot_jenkins.get_bootstrap_failure_hint(msg_stdout) |
896 | + msg = f"Fail to bootstrap a controller. It may be because: {msg_guess}" |
897 | + log(msg, level=ERROR) |
898 | + |
899 | + # fail to bootstrap a controller is devastating |
900 | + # by setting status as "blocked" to help stop the deployment process |
901 | + status_set("blocked", msg) |
902 | + |
903 | + if scalebot_jenkins.if_controller(): |
904 | + set_state("scalebot.configured.juju") |
905 | + |
906 | + |
907 | +@when_not("scalebot.stop") |
908 | +def scalebot_start(): |
909 | + set_state("scalebot.go") |
910 | + |
911 | + |
912 | +@hook("stop") |
913 | +def scalebot_stop(): |
914 | + set_state("scalebot.stop") |
915 | + remove_state("scalebot.go") |
916 | + log("Stopping Scalebot.") |
917 | + status_set("maintenance", "Stopping ScaleBot") |
918 | + |
919 | + |
920 | +@when("scalebot.stop") |
921 | +def scalebot_juju_destroy_controller(): |
922 | + status_set("maintenance", "Destroying Juju controller") |
923 | + subprocess.run( |
924 | + [ |
925 | + "/bin/su", |
926 | + "-", |
927 | + "jenkins", |
928 | + "-c", |
929 | + "juju destroy-controller --destroy-all-models -y jenkins", |
930 | + ] |
931 | + ) |
932 | + |
933 | + |
934 | +@when_not("scalebot.jenkins-http-ready") |
935 | +def jenkins_http_ready(): |
936 | + rc = 500 |
937 | + try: |
938 | + rc = requests.get(SCALEBOT_URL, timeout=5).status_code |
939 | + except ( |
940 | + requests.HTTPError, |
941 | + requests.exceptions.Timeout, |
942 | + requests.exceptions.ConnectionError, |
943 | + ): |
944 | + pass |
945 | + |
946 | + # Valid status codes (we can get redirects, login forms, etc.) |
947 | + if rc in (200, 300, 301, 302, 403): |
948 | + set_state("scalebot.jenkins-http-ready") |
949 | + |
950 | + |
951 | +# Count the number of jobs currently running on Jenkins and lock it for new |
952 | +# jobs if zero. This is required to prevent a race condition if new jobs are |
953 | +# started between running this script and restarting the server. |
954 | + |
955 | +JK_RESTART_CHECK_SCRIPT = """ |
956 | +import jenkins.model.* |
957 | +import hudson.model.* |
958 | + |
959 | +lst = Jenkins.instance.getView('All').getBuilds().findAll() { |
960 | + it.getResult().equals(null) |
961 | +} |
962 | +if (lst.size() == 0) { |
963 | + Jenkins.instance.doQuietDown(); |
964 | +} |
965 | +println(lst.size()); |
966 | +""" |
967 | + |
968 | + |
969 | +@when_none("scalebot.configured.env", "scalebot.stop") |
970 | +@when_all( |
971 | + "scalebot.go", |
972 | + "scalebot.jenkins-http-ready", |
973 | + "scalebot.jjb.workaround.installed", |
974 | + "scalebot.written.env", |
975 | +) |
976 | +@when_all("extension.available", "scalebot.go") |
977 | +def safe_jenkins_service_restart(extension): |
978 | + """Restart Jenkins service if no jobs are running. |
979 | + |
980 | + Environment variables required by some jobs will not be available until |
981 | + we do a full service restart (i.e. finishing Java process and starting |
982 | + it again instead of just reloading Jenkins, as done by safe-restart). |
983 | + |
984 | + Tests can be lost if we just restart so we need to get the number of |
985 | + running jobs (using the Rest API), restart the service and ensure that |
986 | + Jenkins is back online before proceeding. |
987 | + """ |
988 | + |
989 | + config = extension.get_connection_info() |
990 | + try: |
991 | + server = jenkins.Jenkins( |
992 | + SCALEBOT_URL, |
993 | + username=config["admin_username"], |
994 | + password=config["admin_password"], |
995 | + timeout=10, |
996 | + ) |
997 | + ret = server.run_script(JK_RESTART_CHECK_SCRIPT) |
998 | + if ret and int(ret) == 0: |
999 | + log("Restarting Jenkins service") |
1000 | + service_restart("jenkins") |
1001 | + remove_state("scalebot.jenkins-http-ready") |
1002 | + set_state("scalebot.configured.env") |
1003 | + else: |
1004 | + log("Jenkins is running some jobs, will restart later") |
1005 | + |
1006 | + except jenkins.JenkinsException as exc: |
1007 | + log("Jenkins API exception:" + str(exc), level=WARNING) |