-
Notifications
You must be signed in to change notification settings - Fork 36
/
jenkins_hooks.py
executable file
·313 lines (259 loc) · 10.1 KB
/
jenkins_hooks.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
#!/usr/bin/python
import grp
import hashlib
import os
import pwd
import shutil
import subprocess
import sys
from charmhelpers.core.hookenv import (
Hooks,
UnregisteredHookError,
config,
remote_unit,
related_units,
relation_get,
relation_set,
relation_ids,
unit_get,
open_port,
log,
DEBUG,
INFO,
)
from charmhelpers.fetch import (
apt_install,
apt_update,
)
from charmhelpers.core.host import (
service_restart,
service_start,
service_stop,
)
from charmhelpers.payload.execd import execd_preinstall
from charmhelpers.core.templating import render
from jenkins_utils import (
JENKINS_HOME,
JENKINS_USERS,
TEMPLATES_DIR,
add_node,
del_node,
get_jenkins_password,
setup_source,
install_from_bundle,
install_from_remote_deb,
install_jenkins_plugins,
)
hooks = Hooks()
@hooks.hook('install')
def install():
execd_preinstall('hooks/install.d')
if config('release') == 'bundle':
install_from_bundle()
elif config('release').startswith('http'):
install_from_remote_deb(config('release'))
else:
# Only setup the source if jenkins is not already installed i.e. makes
# the config 'release' immutable so you can't change source once
# deployed.
setup_source(config('release'))
config_changed()
open_port(8080)
@hooks.hook('config-changed')
def config_changed():
apt_update()
# Re-run whenever called to pickup any updates
log("Installing/upgrading jenkins.", level=DEBUG)
apt_install(['jenkins', 'default-jre-headless', 'pwgen'], fatal=True)
# Always run - even if config has not changed, its safe
log("Configuring user for jenkins.", level=DEBUG)
# Check to see if password provided
admin_passwd = config('password')
if not admin_passwd:
# Generate a random one for security. User can then override using juju
# set.
admin_passwd = subprocess.check_output(['pwgen', '-N1', '15'])
admin_passwd = admin_passwd.strip()
passwd_file = os.path.join(JENKINS_HOME, '.admin_password')
with open(passwd_file, 'w+') as fd:
fd.write(admin_passwd)
os.chmod(passwd_file, 0600)
jenkins_uid = pwd.getpwnam('jenkins').pw_uid
jenkins_gid = grp.getgrnam('jenkins').gr_gid
nogroup_gid = grp.getgrnam('nogroup').gr_gid
# Generate Salt and Hash Password for Jenkins
salt = subprocess.check_output(['pwgen', '-N1', '6']).strip()
csum = hashlib.sha256("%s{%s}" % (admin_passwd, salt)).hexdigest()
salty_password = "%s:%s" % (salt, csum)
admin_username = config('username')
admin_user_home = os.path.join(JENKINS_USERS, admin_username)
if not os.path.isdir(admin_user_home):
os.makedirs(admin_user_home, 0o0700)
os.chown(JENKINS_USERS, jenkins_uid, nogroup_gid)
os.chown(admin_user_home, jenkins_uid, nogroup_gid)
# NOTE: overwriting will destroy any data added by jenkins or via the ui
admin_user_config = os.path.join(admin_user_home, 'config.xml')
with open(os.path.join(TEMPLATES_DIR, 'user-config.xml')) as src_fd:
with open(admin_user_config, 'w') as dst_fd:
lines = src_fd.readlines()
for line in lines:
kvs = {'__USERNAME__': admin_username,
'__PASSWORD__': salty_password}
for key, val in kvs.iteritems():
if key in line:
line = line.replace(key, val)
dst_fd.write(line)
os.chown(admin_user_config, jenkins_uid, nogroup_gid)
# Only run on first invocation otherwise we blast
# any configuration changes made
jenkins_bootstrap_flag = '/var/lib/jenkins/config.bootstrapped'
if not os.path.exists(jenkins_bootstrap_flag):
log("Bootstrapping secure initial configuration in Jenkins.",
level=DEBUG)
dst = os.path.join(JENKINS_HOME, 'config.xml')
context = {'master_executors': config('master-executors')}
render('jenkins-config.xml', dst, context, owner='jenkins',
group='nogroup')
# Touch
with open(jenkins_bootstrap_flag, 'w'):
pass
log("Stopping jenkins for plugin update(s)", level=DEBUG)
service_stop('jenkins')
install_jenkins_plugins(jenkins_uid, jenkins_gid)
log("Starting jenkins to pickup configuration changes", level=DEBUG)
service_start('jenkins')
apt_install(['python-jenkins'], fatal=True)
tools = config('tools')
if tools:
log("Installing tools.", level=DEBUG)
apt_install(tools.split(), fatal=True)
@hooks.hook('start')
def start():
service_start('jenkins')
@hooks.hook('stop')
def stop():
service_stop('jenkins')
@hooks.hook('upgrade-charm')
def upgrade_charm():
log("Upgrading charm.", level=DEBUG)
config_changed()
@hooks.hook('master-relation-joined')
def master_relation_joined():
HOSTNAME = unit_get('private-address')
log("Setting url relation to http://%s:8080" % (HOSTNAME), level=DEBUG)
relation_set(url="http://%s:8080" % (HOSTNAME))
@hooks.hook('master-relation-changed')
def master_relation_changed():
password = get_jenkins_password()
# Once we have the password, export credentials to the slave so it can
# download slave-agent.jnlp from the master.
username = config('username')
relation_set(username=username)
relation_set(password=password)
required_settings = ['executors', 'labels', 'slavehost']
settings = relation_get()
missing = [s for s in required_settings if s not in settings]
if missing:
log("Not all required relation settings received yet (missing=%s) - "
"skipping" % (', '.join(missing)), level=INFO)
return
slavehost = settings['slavehost']
executors = settings['executors']
labels = settings['labels']
# Double check to see if this has happened yet
if "x%s" % (slavehost) == "x":
log("Slave host not yet defined - skipping", level=INFO)
return
log("Adding slave with hostname %s." % (slavehost), level=DEBUG)
add_node(slavehost, executors, labels, username, password)
log("Node slave %s added." % (slavehost), level=DEBUG)
@hooks.hook('master-relation-departed')
def master_relation_departed():
# Slave hostname is derived from unit name so
# this is pretty safe
slavehost = remote_unit()
log("Deleting slave with hostname %s." % (slavehost), level=DEBUG)
del_node(slavehost, config('username'), config('password'))
@hooks.hook('master-relation-broken')
def master_relation_broken():
password = get_jenkins_password()
for member in relation_ids():
member = member.replace('/', '-')
log("Removing node %s from Jenkins master." % (member), level=DEBUG)
del_node(member, config('username'), password)
@hooks.hook('website-relation-joined')
def website_relation_joined():
hostname = unit_get('private-address')
log("Setting website URL to %s:8080" % (hostname), level=DEBUG)
relation_set(port=8080, hostname=hostname)
@hooks.hook('extension-relation-joined')
def extension_relation_joined():
log("Updating extension interface with up-to-date data.")
# Fish out the current zuul address from any relation we have.
zuul_address = ""
for rid in relation_ids('zuul'):
for unit in related_units(rid):
zuul_address = relation_get(
rid=rid, unit=unit, attribute='private-address')
for rid in relation_ids('extension'):
r_settings = {
'admin_username': config('username'),
'admin_password': get_jenkins_password(),
'jenkins_url': 'http://%s:8080' % unit_get('private-address'),
'jenkins-admin-user': config('jenkins-admin-user'),
'jenkins-token': config('jenkins-token')
}
relation_set(relation_id=rid, relation_settings=r_settings)
if zuul_address:
relation_set(relation_id=rid, zuul_address=zuul_address)
@hooks.hook('extension-relation-changed')
def extension_relation_changed():
# extension subordinates may request the principle service install
# specified jenkins plugins
if relation_get('required_plugins'):
log("Installing required plugins as requested by jenkins-extension "
"subordinate.")
jenkins_uid = pwd.getpwnam('jenkins').pw_uid
jenkins_gid = grp.getgrnam('jenkins').gr_gid
install_jenkins_plugins(
jenkins_uid, jenkins_gid,
plugins=relation_get('required_plugins'))
ZUUL_CONFIG_SNIPPET = """
<hudson.plugins.gearman.GearmanPluginConfig>
<enablePlugin>true</enablePlugin>
<host>{}</host>
<port>4730</port>
</hudson.plugins.gearman.GearmanPluginConfig>
"""
@hooks.hook('zuul-relation-joined')
def zuul_relation_joined():
log("Installing and configuring gearman-plugin for Zuul communication")
# zuul relation requires we install the required plugins and set the
# address of the remote zuul/gearman service in the plugin setting.
required_plugins = (
"credentials ssh-credentials ssh-agent gearman-plugin git-client git")
# Grab jenkins uid and gid.
jenkins_uid = pwd.getpwnam('jenkins').pw_uid
jenkins_gid = grp.getgrnam('jenkins').gr_gid
log("Installing and configuring gearman-plugin for Zuul communication")
install_jenkins_plugins(
jenkins_uid, jenkins_gid, plugins=required_plugins)
# Generate plugin config with address of remote unit.
zuul_config = ZUUL_CONFIG_SNIPPET.format(relation_get('private-address'))
config_path = os.path.join(
JENKINS_HOME, "hudson.plugins.gearman.GearmanPluginConfig.xml")
with open(config_path, 'w') as f:
f.write(zuul_config)
# Change permission of config file.
nogroup_gid = grp.getgrnam('nogroup').gr_gid
os.chown(config_path, jenkins_uid, nogroup_gid)
# Restart jenkins so changes will take efect.
service_restart('jenkins')
# Trigger the extension hook to update it with zuul relation data, if its
# coded to do so.
hooks.execute(['extension-relation-joined'])
if __name__ == '__main__':
try:
hooks.execute(sys.argv)
except UnregisteredHookError as e:
log('Unknown hook {} - skipping.'.format(e), level=INFO)