Merge lp:~hloeung/jenkins-agent-charm/reactive-rewrite into lp:jenkins-agent-charm

Proposed by Haw Loeung
Status: Merged
Approved by: Tom Haddon
Approved revision: 51
Merged at revision: 22
Proposed branch: lp:~hloeung/jenkins-agent-charm/reactive-rewrite
Merge into: lp:jenkins-agent-charm
Diff against target: 1152 lines (+673/-311)
25 files modified
.bzrignore (+11/-0)
Makefile (+30/-0)
README.md (+5/-0)
files/jenkins-slave-sudoers (+5/-0)
hooks/configure-slave (+0/-27)
hooks/install.d/add_sudoers (+0/-18)
hooks/install.d/canonical_ci_utils.py (+0/-91)
hooks/nrpe-external-master-relation-changed (+0/-7)
hooks/slave-relation-changed (+0/-19)
hooks/slave-relation-departed (+0/-3)
hooks/slave-relation-joined (+0/-20)
hooks/start (+0/-3)
hooks/stop (+0/-3)
layer.yaml (+5/-0)
metadata.yaml (+5/-1)
reactive/jenkins_slave.py (+228/-118)
requirements.txt (+1/-0)
revision (+0/-1)
templates/jenkins-slave-default (+8/-0)
tests/functional/requirements.txt (+6/-0)
tests/functional/test_jenkins_slave.py (+50/-0)
tests/unit/files/jenkins-slave-default (+59/-0)
tests/unit/requirements.txt (+5/-0)
tests/unit/test_jenkins_slave.py (+218/-0)
tox.ini (+37/-0)
To merge this branch: bzr merge lp:~hloeung/jenkins-agent-charm/reactive-rewrite
Reviewer Review Type Date Requested Status
Tom Haddon Approve
Haw Loeung Needs Resubmitting
Canonical IS Reviewers Pending
Review via email: mp+363649@code.launchpad.net

Commit message

Charm rewrite using Python and the Reactive framework

To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Revision history for this message
Tom Haddon (mthaddon) wrote :

Some comments and questions inline.

review: Needs Fixing
Revision history for this message
Tom Haddon (mthaddon) wrote :

Missed one other thing as well.

Revision history for this message
Tom Haddon (mthaddon) wrote :

Some comments inline with suggestions for testing.

Revision history for this message
Tom Haddon (mthaddon) wrote :

Er, we we obviously *do* want the max-complexity check

Revision history for this message
Tom Haddon (mthaddon) wrote :

A few comments inline

Revision history for this message
Haw Loeung (hloeung) :
review: Needs Resubmitting
50. By Haw Loeung

Added basic functional test

51. By Haw Loeung

Fixed to use the module itself instead of sys.modules

Revision history for this message
Tom Haddon (mthaddon) wrote :

LGTM, thanks

review: Approve
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision 22

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file '.bzrignore'
2--- .bzrignore 1970-01-01 00:00:00 +0000
3+++ .bzrignore 2019-03-01 05:39:53 +0000
4@@ -0,0 +1,11 @@
5+*.pyc
6+*.swp
7+*~
8+.coverage
9+.pytest_cache/
10+.tox/
11+.unit-state.db
12+__pycache__/
13+builds/
14+deps/
15+revision
16
17=== added file 'Makefile'
18--- Makefile 1970-01-01 00:00:00 +0000
19+++ Makefile 2019-03-01 05:39:53 +0000
20@@ -0,0 +1,30 @@
21+help:
22+ @echo "This project supports the following targets"
23+ @echo ""
24+ @echo " make help - show this text"
25+ @echo " make lint - run flake8"
26+ @echo " make test - run the functional test and unittests"
27+ @echo " make unittest - run the the unittest"
28+ @echo " make functionaltest - run the functional tests"
29+ @echo " make clean - remove unneeded files"
30+ @echo ""
31+
32+lint:
33+ @echo "Running flake8"
34+ @tox -e lint
35+
36+test: unittest functionaltest lint
37+
38+unittest:
39+ @tox -e unit
40+
41+functionaltest:
42+ @tox -e functional
43+
44+clean:
45+ @echo "Cleaning files"
46+ @if [ -d ./.tox ] ; then rm -r ./.tox ; fi
47+ @if [ -d ./.pytest_cache ] ; then rm -r ./.pytest_cache ; fi
48+
49+# The targets below don't depend on a file
50+.PHONY: lint test unittest functionaltest clean help
51
52=== modified file 'README.md'
53--- README.md 2014-05-15 16:06:29 +0000
54+++ README.md 2019-03-01 05:39:53 +0000
55@@ -21,3 +21,8 @@
56 juju deploy --to <special-mabine-number> jenkins-slave ppc-slave
57
58 See the Jenkins charm for more details.
59+
60+
61+# Notes
62+
63+We can't use interface:jenkins-slave yet as it's not fully implemented.
64
65=== added directory 'actions'
66=== added file 'files/jenkins-slave-sudoers'
67--- files/jenkins-slave-sudoers 1970-01-01 00:00:00 +0000
68+++ files/jenkins-slave-sudoers 2019-03-01 05:39:53 +0000
69@@ -0,0 +1,5 @@
70+# Created automatically during charm installation
71+# Any manual changes to it will be lost
72+
73+# User rules for jenkins
74+jenkins ALL=(ALL) NOPASSWD: ALL
75
76=== removed symlink 'hooks/config-changed'
77=== target was u'install'
78=== removed file 'hooks/configure-slave'
79--- hooks/configure-slave 2016-06-03 02:54:52 +0000
80+++ hooks/configure-slave 1970-01-01 00:00:00 +0000
81@@ -1,27 +0,0 @@
82-#!/bin/bash
83-
84-set -e
85-
86-# Grab the jenkins master url as a passed in parameter
87-url="$1"
88-
89-# Set the slave hostname to match the juju unit
90-slavehost="$(echo ${JUJU_UNIT_NAME} | sed s,/,-,)"
91-
92-# Set the slave and url fields
93-juju-log "Configuring jenkins-slave with ${url}..."
94-sed -i -e "s!^JENKINS_HOSTNAME.*!JENKINS_HOSTNAME=${slavehost}!" \
95- -e "s!^#*JENKINS_URL.*!JENKINS_URL=${url}!" \
96- /etc/default/jenkins-slave
97-
98-# Startup the jenkins-slave service
99-# This is called in the install, config-changed and upgrade-charm paths
100-# It needs to be tolerant of a running or stopped jenkins-slave
101-status="$(service jenkins-slave status || true)"
102-if echo "${status}" | egrep -q "stop|inactive"; then
103- juju-log "Starting jenkins-slave..."
104- service jenkins-slave start
105-else
106- juju-log "Restarting jenkins-slave..."
107- service jenkins-slave restart
108-fi
109
110=== removed symlink 'hooks/install.d/03_nrpe_relation_changed'
111=== target was u'canonical_ci_utils.py'
112=== removed file 'hooks/install.d/add_sudoers'
113--- hooks/install.d/add_sudoers 2015-08-13 18:04:16 +0000
114+++ hooks/install.d/add_sudoers 1970-01-01 00:00:00 +0000
115@@ -1,18 +0,0 @@
116-#!/bin/bash
117-
118-set -eu
119-
120-temp_sudoers="$(mktemp /tmp/jenkins.XXXXXX)"
121-
122-cat > "$temp_sudoers" << EOF
123-# Created automatically during charm installation
124-# Any manual changes to it will be lost
125-
126-# User rules for jenkins
127-jenkins ALL=(ALL) NOPASSWD: ALL
128-EOF
129-
130-visudo -c -f "$temp_sudoers"
131-install -m 440 "$temp_sudoers" /etc/sudoers.d/jenkins
132-rm "$temp_sudoers"
133-visudo -c
134
135=== removed file 'hooks/install.d/canonical_ci_utils.py'
136--- hooks/install.d/canonical_ci_utils.py 2015-12-03 10:40:55 +0000
137+++ hooks/install.d/canonical_ci_utils.py 1970-01-01 00:00:00 +0000
138@@ -1,91 +0,0 @@
139-#!/usr/bin/python
140-
141-import os
142-import sys
143-
144-from charmhelpers.canonical_ci import nrpe
145-
146-
147-from charmhelpers.core.hookenv import (
148- config,
149- local_unit,
150- log,
151- relation_ids,
152- unit_get,
153- ERROR,
154- INFO,
155-)
156-
157-from charmhelpers.core.host import (
158- service_reload,
159-)
160-
161-# Constants set in jenkins/hooks/install.
162-JENKINS_HOME = '/var/lib/jenkins'
163-JENKINS_USER = 'jenkins'
164-JENKINS_GROUP = 'nogroup'
165-JENKINS_PORT = 8080
166-
167-# Local constants
168-NRPE_CHECK_PS = "/etc/nagios/nrpe.d/check_jenkins_slave_ps.cfg"
169-
170-
171-def update_nrpe_config():
172- if not relation_ids('nrpe-external-master'):
173- log('No relation to an nrpe-external-master, not configuring '
174- 'nagios.')
175- return
176- with open(NRPE_CHECK_PS, "w") as conf:
177- log("Writing config: %s." % NRPE_CHECK_PS, INFO)
178- conf.write(nrpe.CONF_HEADER)
179- conf.write(
180- "command[check_jenkins_slave_ps]="
181- "/usr/lib/nagios/plugins/check_procs -c 1:1 -a slave.jar")
182-
183- # config
184- unit_name = local_unit().replace('/', '-')
185- n_hostname = "%s-%s" % (config("nagios_context"), unit_name)
186- n_servicegroup = config("nagios_servicegroups") if config("nagios_servicegroups") else config("nagios_context")
187- service_file = (
188- '/var/lib/nagios/export/service__%s_check_jenkins_slave.cfg' %
189- n_hostname)
190-
191- # There is a race between this code and the nrpe_external_master install
192- # hook. Both need to write files to /var/lib/nagios/export, but only one
193- # needs to create it. So tolerate failure if the directory exists.
194- try:
195- os.makedirs(os.path.dirname(service_file))
196- except os.error:
197- pass
198-
199- with open(service_file, "w") as conf:
200- log("Writing config: %s." % conf, INFO)
201- conf.write(nrpe.CONF_HEADER)
202- conf.write(nrpe.NRPE_SERVICE_ENTRY % {
203- 'nagios_hostname': n_hostname,
204- 'check_name': 'check_jenkins_slave_ps',
205- 'nagios_servicegroup': n_servicegroup,
206- })
207-
208- # reboot
209- if os.path.isfile('/etc/init.d/nagios-nrpe-server'):
210- service_reload('nagios-nrpe-server')
211-
212-
213-hooks = {
214- '03_nrpe_relation_changed': update_nrpe_config
215-}
216-
217-
218-def main(hook):
219- try:
220- hooks[hook]()
221- except KeyError:
222- e = 'Invalid install.d hook: %s.' % hook
223- log(e, ERROR)
224- raise Exception(e)
225-
226-
227-if __name__ == '__main__':
228- if os.path.islink(sys.argv[0]):
229- main(os.path.basename(sys.argv[0]))
230
231=== removed file 'hooks/nrpe-external-master-relation-changed'
232--- hooks/nrpe-external-master-relation-changed 2015-08-19 20:00:15 +0000
233+++ hooks/nrpe-external-master-relation-changed 1970-01-01 00:00:00 +0000
234@@ -1,7 +0,0 @@
235-#!/bin/sh
236-set -e
237-
238-home=`dirname $0`
239-
240-juju-log "Running install hook once more to pick up new NRPE relation changes"
241-exec $home/install
242
243=== removed symlink 'hooks/slave-relation-broken'
244=== target was u'slave-relation-departed'
245=== removed file 'hooks/slave-relation-changed'
246--- hooks/slave-relation-changed 2015-08-05 13:20:44 +0000
247+++ hooks/slave-relation-changed 1970-01-01 00:00:00 +0000
248@@ -1,19 +0,0 @@
249-#!/bin/bash
250-
251-set -e
252-
253-# Setup connection to master instance once set
254-url="$(relation-get url)"
255-
256-if [ "x$url" = "x" ]; then
257- juju-log "Master hasn't exported its url yet, exiting..."
258- exit 0
259-fi
260-
261-master_url="$(config-get master_url)"
262-if [ -n "${master_url}" ]; then
263- juju-log "Config option 'master_url' is set. Can't use slave relation."
264- exit 0
265-fi
266-
267-hooks/configure-slave "${url}"
268
269=== removed file 'hooks/slave-relation-departed'
270--- hooks/slave-relation-departed 2012-07-27 11:23:24 +0000
271+++ hooks/slave-relation-departed 1970-01-01 00:00:00 +0000
272@@ -1,3 +0,0 @@
273-#!/bin/bash
274-
275-stop jenkins-slave || true
276
277=== removed file 'hooks/slave-relation-joined'
278--- hooks/slave-relation-joined 2015-08-04 16:46:21 +0000
279+++ hooks/slave-relation-joined 1970-01-01 00:00:00 +0000
280@@ -1,20 +0,0 @@
281-#!/bin/bash
282-
283-set -e
284-
285-# Set the slave hostname to match the juju unit
286-# in the jenkins master instance
287-slavehost=`echo ${JUJU_UNIT_NAME} | sed s,/,-,`
288-noexecutors=`cat /proc/cpuinfo | grep processor | wc -l`
289-config_labels=`config-get labels`
290-labels=`uname -p`
291-
292-if [ -n "$config_labels" ]; then
293- labels=$config_labels
294-fi
295-
296-# Set all relations
297-relation-set executors=$noexecutors
298-relation-set labels="$labels"
299-relation-set slavehost=$slavehost
300-relation-set slaveaddress=`unit-get private-address`
301
302=== removed file 'hooks/start'
303--- hooks/start 2011-08-02 08:53:54 +0000
304+++ hooks/start 1970-01-01 00:00:00 +0000
305@@ -1,3 +0,0 @@
306-#!/bin/bash
307-
308-start jenkins-slave || true
309
310=== removed file 'hooks/stop'
311--- hooks/stop 2011-08-02 08:53:54 +0000
312+++ hooks/stop 1970-01-01 00:00:00 +0000
313@@ -1,3 +0,0 @@
314-#!/bin/bash
315-
316-stop jenkins-slave
317
318=== removed symlink 'hooks/upgrade-charm'
319=== target was u'install'
320=== added file 'layer.yaml'
321--- layer.yaml 1970-01-01 00:00:00 +0000
322+++ layer.yaml 2019-03-01 05:39:53 +0000
323@@ -0,0 +1,5 @@
324+includes:
325+ - layer:basic
326+ - layer:apt
327+ - layer:nagios
328+repo: lp:jenkins-slave-charm
329
330=== added directory 'lib'
331=== modified file 'metadata.yaml'
332--- metadata.yaml 2019-02-18 00:41:09 +0000
333+++ metadata.yaml 2019-03-01 05:39:53 +0000
334@@ -9,8 +9,12 @@
335 This charm provides support for jenkins slaves
336 .
337 https://launchpad.net/jenkins-slave-charm
338-categories:
339+tags:
340 - applications
341+series:
342+ - bionic
343+ - xenial
344+ - trusty
345 provides:
346 slave:
347 interface: jenkins-slave
348
349=== added directory 'reactive'
350=== renamed file 'hooks/install' => 'reactive/jenkins_slave.py'
351--- hooks/install 2016-06-14 01:04:43 +0000
352+++ reactive/jenkins_slave.py 2019-03-01 05:39:53 +0000
353@@ -1,121 +1,231 @@
354-#!/bin/bash
355-
356-set -eu
357-
358-
359-install_exec_d () {
360- if [[ -d exec.d ]]; then
361- shopt -s nullglob
362- for f in exec.d/*/charm-pre-install; do
363- [[ -x "$f" ]] || continue
364- ${SHELL} -c "$f"|| {
365- ## bail out if anyone fails
366- juju-log -l ERROR "$f: returned exit_status=$? "
367- }
368- done
369- shopt -u nullglob
370- fi
371-}
372-
373-# Get rid of the legacy jenkins-slave package, including config files.
374-clean_up_old_package () {
375- juju-log "Removing the old jenkin-slave package... (obsoleted by this charm)"
376- dpkg --purge jenkins-slave
377- juju-log "Removing the old jenkin-slave package... done."
378-}
379-
380-# Install the slave if it is not installed already.
381-install_slave () {
382- juju-log "Installing jenkins-slave..."
383- juju-log "Installing jenkins-slave (dependencies)..."
384- apt-get -y install -qq wget adduser default-jre-headless
385+import os
386+
387+from charmhelpers.core import hookenv, host, templating, unitdata
388+from charmhelpers.contrib.charmsupport import nrpe
389+from charmhelpers.fetch import apt_purge
390+from charms import apt, reactive
391+
392+
393+@reactive.hook('upgrade-charm')
394+def upgrade_charm():
395+ hookenv.status_set('maintenance', 'forcing reconfiguration on upgrade-charm')
396+ reactive.clear_flag('jenkins-slave.active')
397+ reactive.clear_flag('jenkins-slave.installed')
398+
399+
400+@reactive.when_not('jenkins-slave.installed')
401+def install():
402+ hookenv.status_set('maintenance', 'installing jenkins-slave')
403+ reactive.clear_flag('jenkins-slave.active')
404+
405+ config = hookenv.config()
406+
407+ hookenv.log('Adding jenkins-slave dependencies to be installed')
408+ packages = ['wget', 'default-jre-headless']
409+
410+ # Install extra packages needed by the slave.
411+ tools = config.get('tools')
412+ if tools:
413+ hookenv.log('Adding jenkins-slave additional tools to be installed: {}'.format(tools))
414+ for package in tools.split():
415+ packages.append(package)
416+ apt.queue_install(packages)
417+ if not apt.install_queued():
418+ return # apt layer already set blocked state.
419+
420+ # Get rid of the legacy jenkins-slave package, including config files.
421+ hookenv.log('Removing the old jenkins-slave package... (obsoleted by this charm)')
422+ apt_purge(['jenkins-slave'])
423
424 # Create jenkins user if it doesn't exist.
425- if ! id jenkins > /dev/null 2>&1 ; then
426- juju-log "Installing jenkins-slave (user account)..."
427- adduser --system --home /var/lib/jenkins --group \
428- --disabled-password --quiet --shell /bin/bash \
429- jenkins
430- else
431- juju-log "Installing jenkins-slave (user account already exists)..."
432- fi
433- juju-log "Installing jenkins-slave (directories)..."
434+ if host.user_exists('jenkins'):
435+ hookenv.log('Installing jenkins-slave (user account already exists)...')
436+ else:
437+ hookenv.log('Installing jenkins-slave (user account)...')
438+ host.adduser(username='jenkins', system_user=True, home_dir='/var/lib/jenkins')
439+
440 # And ensure required directories exist and are set up.
441- mkdir -p /var/lib/jenkins
442- chown -R jenkins:jenkins /var/lib/jenkins || true
443- mkdir -p /var/log/jenkins
444- chown -R jenkins:jenkins /var/log/jenkins || true
445- juju-log "Installing jenkins-slave (common files)..."
446- install -m 0555 files/download-slave.sh /usr/local/sbin/download-slave.sh
447- # XXX obviously we lose conffile handling here...
448- install -m 0444 files/jenkins-slave-default /etc/default/jenkins-slave
449- install -m 0444 files/jenkins-slave-logrotate-config /etc/logrotate.d/jenkins-slave
450-
451- distro=$(source /etc/lsb-release ; echo $DISTRIB_CODENAME)
452- case $distro in
453- xenial)
454- # LTS or bust!
455- juju-log "Installing jenkins-slave (system unit)..."
456- install -m 0444 files/jenkins-slave-systemd-config /lib/systemd/system/jenkins-slave.service
457- systemctl enable jenkins-slave
458- ;;
459- *)
460- # Probably an LTS, and it's not, then too bad.
461- juju-log "Installing jenkins-slave (upstart job)..."
462- install -m 0444 files/jenkins-slave-upstart-config /etc/init/jenkins-slave.conf
463- ;;
464- esac
465- juju-log "Installing jenkins-slave... done."
466-}
467-
468-
469-# Install extra packages needed by the slave.
470-install_tools () {
471- juju-log "Installing tools..."
472- apt-get -y install -qq $(config-get tools)
473-}
474-
475-
476-# Configure slave
477-set_up_slave () {
478- # If a master_url value is specified, use that to configure the slave.
479- master_url="$(config-get master_url)"
480- if [ -n "${master_url}" ]; then
481- juju-log "Using 'master_url' to configure the slave."
482- hooks/configure-slave "${master_url}"
483- else
484- juju-log "No 'master_url' set; not configuring slave at this time."
485- fi
486-}
487-
488-# Execute any hook overlay which may be provided
489-# by forks of this charm.
490-install_extra_hooks () {
491- # XXX for canonical_ci_utils.py
492- apt-get -y install -qq python-yaml
493-
494- juju-log "Installing hooks..."
495- if [[ -d hooks/install.d ]]
496- then
497- for i in $(ls -1 hooks/install.d/*)
498- do
499- if [[ -x "$i" ]]
500- then
501- ./$i
502- fi
503- done
504- else
505- juju-log "No extra hooks found."
506- fi
507-}
508-
509-
510-apt-get update -qq
511-install_exec_d
512-clean_up_old_package
513-install_slave
514-install_tools
515-set_up_slave
516-install_extra_hooks
517-
518-exit 0
519+ hookenv.log('Installing jenkins-slave (directories)...')
520+ host.mkdir('/var/lib/jenkins', owner='jenkins', group='jenkins')
521+ host.mkdir('/var/log/jenkins', owner='jenkins', group='jenkins')
522+
523+ hookenv.log('Installing jenkins-slave (common files)...')
524+ write_default_conf()
525+ file_to_units('files/download-slave.sh', '/usr/local/sbin/download-slave.sh')
526+ file_to_units('files/jenkins-slave-logrotate-config', '/etc/logrotate.d/jenkins-slave')
527+
528+ if host.lsb_release()['DISTRIB_CODENAME'] == 'trusty':
529+ hookenv.log('Installing jenkins-slave (upstart job)...')
530+ file_to_units('files/jenkins-slave-upstart-config', '/etc/init/jenkins-slave.conf')
531+ else:
532+ hookenv.log('Installing jenkins-slave (system unit)...')
533+ file_to_units('files/jenkins-slave-systemd-config', '/lib/systemd/system/jenkins-slave.service')
534+ host.service('enable', 'jenkins-slave')
535+
536+ hookenv.log('Installing jenkins-slave... done.')
537+ reactive.clear_flag('jenkins-slave.blocked')
538+ reactive.clear_flag('jenkins-slave.configured')
539+ reactive.set_flag('jenkins-slave.installed')
540+
541+
542+@reactive.when('config.changed')
543+def config_changed():
544+ reactive.clear_flag('jenkins-slave.blocked')
545+ reactive.clear_flag('jenkins-slave.configured')
546+ reactive.clear_flag('nagios-nrpe.configured')
547+
548+
549+@reactive.when('jenkins-slave.installed')
550+@reactive.when_not('jenkins-slave.configured')
551+@reactive.when_not('jenkins-slave.blocked')
552+def configure_jenkins_slave():
553+ hookenv.status_set('maintenance', 'configuring jenkins-slave')
554+ reactive.clear_flag('jenkins-slave.active')
555+
556+ config = hookenv.config()
557+ kv = unitdata.kv()
558+
559+ if config.get('master_url'):
560+ hookenv.log("Using 'master_url' to configure the slave.")
561+ write_default_conf(config.get('master_url'))
562+ elif kv.get('url'):
563+ hookenv.log("Using url from relation as 'master_url'")
564+ write_default_conf(kv.get('url'))
565+ else:
566+ hookenv.log("No 'master_url' set; not configuring slave at this time.")
567+ hookenv.status_set('blocked', "requires either slave relation or 'master_url'")
568+ reactive.set_flag('jenkins-slave.blocked')
569+ return
570+
571+ file_to_units('files/jenkins-slave-sudoers', '/etc/sudoers.d/jenkins', perms=0o440)
572+
573+ reactive.clear_flag('nagios-nrpe.configured')
574+ reactive.set_flag('jenkins-slave.configured')
575+
576+
577+@reactive.when('jenkins-slave.blocked')
578+def blocked_on_jenkins_url():
579+ reactive.clear_flag('jenkins-slave.active')
580+
581+
582+@reactive.when('jenkins-slave.configured')
583+@reactive.when('nrpe-external-master.available')
584+@reactive.when('jenkins-slave.active')
585+@reactive.when_not('nagios-nrpe.configured')
586+def configure_nagios(nagios):
587+ hookenv.status_set('maintenance', 'setting up NRPE checks')
588+
589+ # Use charmhelpers.contrib.charmsupport's nrpe to determine hostname
590+ hostname = nrpe.get_nagios_hostname()
591+ nrpe_setup = nrpe.NRPE(hostname=hostname, primary=True)
592+
593+ cmd = '/usr/lib/nagios/plugins/check_procs -c 1:1 -a slave.jar'
594+ nrpe_setup.add_check('jenkins_slave_ps', 'Jenkins Slave Process', cmd)
595+
596+ nrpe_setup.write()
597+ reactive.set_flag('nagios-nrpe.configured')
598+
599+
600+@reactive.when('jenkins-slave.configured')
601+@reactive.when_not('jenkins-slave.active')
602+def set_active():
603+ # Startup the jenkins-slave service. This is called in the
604+ # install, config-changed and upgrade-charm paths. It needs to be
605+ # tolerant of a running or stopped jenkins-slave.
606+ if host.service_running('jenkins-slave'):
607+ hookenv.log('Restarting jenkins-slave...')
608+ host.service_restart('jenkins-slave')
609+ else:
610+ hookenv.log('Starting jenkins-slave...')
611+ host.service_start('jenkins-slave')
612+
613+ hookenv.status_set('active', 'ready')
614+ reactive.set_flag('jenkins-slave.active')
615+
616+
617+# We can't use interface:jenkins-slave yet as it's not implemented.
618+@reactive.hook('slave-relation-joined', 'slave-relation-changed')
619+def slave_relation_changed():
620+ reactive.set_flag('slave-relation.available')
621+ reactive.clear_flag('jenkins-slave.blocked')
622+ reactive.clear_flag('jenkins-slave.configured')
623+ reactive.clear_flag('slave-relation.configured')
624+
625+
626+@reactive.hook('slave-relation-departed', 'slave-relation-broken')
627+def slave_relation_removed():
628+ kv = unitdata.kv()
629+ kv.set('url', None)
630+ reactive.clear_flag('slave-relation.available')
631+
632+
633+@reactive.when('slave-relation.available')
634+@reactive.when_not('slave-relation.configured')
635+def slave_relation():
636+ hookenv.status_set('maintenance', 'setting up jenkins via slave relation')
637+ config = hookenv.config()
638+ kv = unitdata.kv()
639+
640+ if config.get('master_url'):
641+ hookenv.log("Config option 'master_url' is set. Can't use slave relation.")
642+ reactive.set_flag('slave-relation.configured')
643+ return
644+
645+ url = hookenv.relation_get('url')
646+ if url:
647+ kv.set('url', url)
648+ write_default_conf(url)
649+ else:
650+ hookenv.log("Master hasn't exported its url yet, exiting...")
651+ return
652+
653+ reactive.clear_flag('jenkins-slave.active')
654+
655+ # Set the slave hostname to match the juju unit
656+ # in the jenkins master instance
657+ slave_host = hookenv.local_unit().replace('/', '-')
658+ slave_address = hookenv.unit_private_ip()
659+ noexecutors = os.cpu_count()
660+ config_labels = config.get('labels')
661+
662+ if config_labels:
663+ labels = config_labels
664+ else:
665+ labels = os.uname()[4]
666+
667+ # Set all relations
668+ hookenv.relation_set(executors=noexecutors)
669+ hookenv.relation_set(labels=labels)
670+ hookenv.relation_set(slavehost=slave_host)
671+ hookenv.relation_set(slaveaddress=slave_address)
672+ reactive.set_flag('slave-relation.configured')
673+
674+
675+def file_to_units(local_path, unit_path, perms=None, owner='root', group='root'):
676+ """ copy a file from the charm onto our unit(s) """
677+ file_perms = perms
678+ if not perms:
679+ # Let's try manually work it out
680+ if local_path[-3:] == '.py' or local_path[-3:] == '.sh':
681+ file_perms = 0o755
682+ else:
683+ file_perms = 0o644
684+
685+ with open(local_path, 'r') as fh:
686+ host.write_file(
687+ path=unit_path,
688+ content=fh.read().encode(),
689+ owner=owner,
690+ group=group,
691+ perms=file_perms,
692+ )
693+
694+
695+def write_default_conf(master_url=None, owner='root', group='root',
696+ conf_path='/etc/default/jenkins-slave'):
697+ templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
698+ slave_host = hookenv.local_unit().replace('/', '-')
699+ templating.render('jenkins-slave-default', conf_path,
700+ {'master_url': master_url, 'slave_host': slave_host},
701+ owner=owner, group=group, perms=0o444,
702+ templates_dir=templates_dir)
703
704=== added file 'requirements.txt'
705--- requirements.txt 1970-01-01 00:00:00 +0000
706+++ requirements.txt 2019-03-01 05:39:53 +0000
707@@ -0,0 +1,1 @@
708+# Include python requirements here
709
710=== removed file 'revision'
711--- revision 2015-08-04 16:46:21 +0000
712+++ revision 1970-01-01 00:00:00 +0000
713@@ -1,1 +0,0 @@
714-9
715
716=== added directory 'templates'
717=== renamed file 'files/jenkins-slave-default' => 'templates/jenkins-slave-default'
718--- files/jenkins-slave-default 2016-06-03 03:37:31 +0000
719+++ templates/jenkins-slave-default 2019-03-01 05:39:53 +0000
720@@ -32,12 +32,20 @@
721 # URL of jenkins server to connect to
722 # Not specifying this parameter will stop the slave
723 # job from running.
724+{% if master_url %}
725+JENKINS_URL="{{ master_url }}"
726+{% else %}
727 #JENKINS_URL=""
728+{% endif %}
729
730 # Name of slave configuration to use at JENKINS_URL
731 # Override if it need to be something other than the
732 # hostname of the server the slave is running on.
733+{% if slave_host %}
734+JENKINS_HOSTNAME="{{ slave_host }}"
735+{% else %}
736 JENKINS_HOSTNAME="$(hostname)"
737+{% endif %}
738
739 # Log file location for use in Debian init script
740 JENKINS_SLAVE_LOG=/var/log/jenkins/$NAME.log
741
742=== added directory 'tests'
743=== added directory 'tests/functional'
744=== added file 'tests/functional/requirements.txt'
745--- tests/functional/requirements.txt 1970-01-01 00:00:00 +0000
746+++ tests/functional/requirements.txt 2019-03-01 05:39:53 +0000
747@@ -0,0 +1,6 @@
748+flake8
749+juju
750+mock
751+pytest
752+pytest-asyncio
753+requests
754
755=== added file 'tests/functional/test_jenkins_slave.py'
756--- tests/functional/test_jenkins_slave.py 1970-01-01 00:00:00 +0000
757+++ tests/functional/test_jenkins_slave.py 2019-03-01 05:39:53 +0000
758@@ -0,0 +1,50 @@
759+import os
760+import pytest
761+from juju.model import Model
762+
763+# Treat tests as coroutines
764+pytestmark = pytest.mark.asyncio
765+
766+series = ['bionic']
767+juju_repository = os.getenv('JUJU_REPOSITORY', '.').rstrip('/')
768+
769+
770+@pytest.fixture
771+async def model():
772+ model = Model()
773+ await model.connect_current()
774+ yield model
775+ await model.disconnect()
776+
777+
778+@pytest.fixture
779+async def apps(model):
780+ apps = []
781+ for entry in series:
782+ app = model.applications['jenkins-slave-{}'.format(entry)]
783+ apps.append(app)
784+ return apps
785+
786+
787+@pytest.fixture
788+async def units(apps):
789+ units = []
790+ for app in apps:
791+ units.extend(app.units)
792+ return units
793+
794+
795+@pytest.mark.parametrize('series', series)
796+async def test_jenkins_slave_deploy(model, series):
797+ # Starts a deploy for each series
798+ await model.deploy('{}/builds/jenkins-slave'.format(juju_repository),
799+ series=series,
800+ application_name='jenkins-slave-{}'.format(series))
801+ assert True
802+
803+
804+async def test_jenkins_slave_status(apps, model):
805+ # Verifies status for all deployed series of the charm
806+ for app in apps:
807+ await model.block_until(lambda: app.status == 'active')
808+ assert True
809
810=== added directory 'tests/unit'
811=== added directory 'tests/unit/files'
812=== added file 'tests/unit/files/jenkins-slave-default'
813--- tests/unit/files/jenkins-slave-default 1970-01-01 00:00:00 +0000
814+++ tests/unit/files/jenkins-slave-default 2019-03-01 05:39:53 +0000
815@@ -0,0 +1,59 @@
816+#
817+# This file is managed by Juju. Attempt no changes here.
818+#
819+
820+# defaults for jenkins-slave component of the jenkins continuous integration
821+# system
822+
823+# pulled in from the init script; makes things easier.
824+NAME=jenkins-slave
825+
826+# location of java
827+JAVA=/usr/bin/java
828+
829+# arguments to pass to java - optional
830+#JAVA_ARGS="-Xmx256m"
831+
832+# for daemon to use
833+PIDFILE=/var/run/jenkins/$NAME.pid
834+
835+# user id to be invoked as (otherwise will run as root; not wise!)
836+JENKINS_USER=jenkins
837+
838+# location of jenkins arch indep files
839+JENKINS_ROOT=/usr/share/jenkins
840+
841+# jenkins home location
842+JENKINS_HOME=/var/lib/jenkins
843+
844+# jenkins /run location
845+JENKINS_RUN=/var/run/jenkins
846+
847+# URL of jenkins server to connect to
848+# Not specifying this parameter will stop the slave
849+# job from running.
850+
851+#JENKINS_URL=""
852+
853+
854+# Name of slave configuration to use at JENKINS_URL
855+# Override if it need to be something other than the
856+# hostname of the server the slave is running on.
857+
858+JENKINS_HOSTNAME="jenkins-slave-3"
859+
860+
861+# Log file location for use in Debian init script
862+JENKINS_SLAVE_LOG=/var/log/jenkins/$NAME.log
863+
864+# OS LIMITS SETUP
865+# comment this out to observe /etc/security/limits.conf
866+# this is on by default because http://github.com/feniix/hudson/commit/d13c08ea8f5a3fa730ba174305e6429b74853927
867+# reported that Ubuntu's PAM configuration doesn't include pam_limits.so, and as a result the # of file
868+# descriptors are forced to 1024 regardless of /etc/security/limits.confa
869+# NOTE - Ubuntu Users - this is not used by the upstart configuration - please use an upstart overrides file
870+# to change the OS limits setup.
871+MAXOPENFILES=8192
872+
873+# Arguments to pass to jenkins slave on startup
874+JENKINS_ARGS="-jnlpUrl $JENKINS_URL/computer/$JENKINS_HOSTNAME/slave-agent.jnlp"
875\ No newline at end of file
876
877=== added file 'tests/unit/files/somefile'
878=== added file 'tests/unit/files/somefile.py'
879=== added file 'tests/unit/requirements.txt'
880--- tests/unit/requirements.txt 1970-01-01 00:00:00 +0000
881+++ tests/unit/requirements.txt 2019-03-01 05:39:53 +0000
882@@ -0,0 +1,5 @@
883+charmhelpers
884+charms.reactive
885+mock
886+pytest
887+pytest-cov
888
889=== added file 'tests/unit/test_jenkins_slave.py'
890--- tests/unit/test_jenkins_slave.py 1970-01-01 00:00:00 +0000
891+++ tests/unit/test_jenkins_slave.py 2019-03-01 05:39:53 +0000
892@@ -0,0 +1,218 @@
893+import grp
894+import os
895+import pwd
896+import shutil
897+import sys
898+import tempfile
899+import unittest
900+from unittest import mock
901+
902+sys.modules['charms.apt'] = mock.MagicMock()
903+from charms import apt # NOQA: E402
904+
905+# Add path to where our reactive layer lives and import.
906+sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
907+from reactive.jenkins_slave import (
908+ config_changed,
909+ install,
910+ configure_jenkins_slave,
911+ file_to_units,
912+ write_default_conf,
913+) # NOQA: E402
914+
915+
916+INITIAL_CONF = 'tests/unit/files/jenkins-slave-default'
917+
918+
919+class TestSetDefaultConf(unittest.TestCase):
920+ def setUp(self):
921+ self.tmpdir = tempfile.mkdtemp(prefix='charm-unittests-')
922+ temp_file = tempfile.NamedTemporaryFile(delete=False, dir=self.tmpdir)
923+ with open(INITIAL_CONF, 'rb') as f:
924+ conf = f.read().decode('utf-8')
925+ temp_file.write(conf.encode())
926+ temp_file.close()
927+ self.conf_file = temp_file.name
928+ self.user = pwd.getpwuid(os.getuid()).pw_name
929+ self.group = grp.getgrgid(os.getgid()).gr_name
930+ self.charm_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
931+
932+ # charmhelpers is getting difficult to test against, as it writes
933+ # to system directories even for things that should be idempotent,
934+ # like accessing config options.
935+ patcher = mock.patch('charmhelpers.core.hookenv.charm_dir')
936+ self.mock_charm_dir = patcher.start()
937+ self.addCleanup(patcher.stop)
938+ self.mock_charm_dir.return_value = self.charm_dir
939+
940+ patcher = mock.patch('charmhelpers.core.hookenv.config')
941+ self.mock_config = patcher.start()
942+ self.addCleanup(patcher.stop)
943+ self.mock_config.return_value = {'tools': []}
944+
945+ patcher = mock.patch('charmhelpers.core.hookenv.local_unit')
946+ self.mock_local_unit = patcher.start()
947+ self.addCleanup(patcher.stop)
948+ self.mock_local_unit.return_value = 'mock-jenkins-slave/0'
949+
950+ patcher = mock.patch('charmhelpers.core.hookenv.log')
951+ self.mock_log = patcher.start()
952+ self.addCleanup(patcher.stop)
953+ self.mock_log.return_value = ''
954+
955+ def tearDown(self):
956+ shutil.rmtree(self.tmpdir)
957+
958+ @mock.patch('charms.reactive.clear_flag')
959+ def test_hook_config_changed(self, clear_flag):
960+ config_changed()
961+ expected = [mock.call('jenkins-slave.blocked'),
962+ mock.call('jenkins-slave.configured'),
963+ mock.call('nagios-nrpe.configured')]
964+ self.assertEqual(clear_flag.call_args_list, expected)
965+
966+ @mock.patch('charmhelpers.core.host.adduser')
967+ @mock.patch('charmhelpers.core.host.mkdir')
968+ @mock.patch('charmhelpers.core.host.service')
969+ @mock.patch('reactive.jenkins_slave.apt_purge')
970+ @mock.patch('reactive.jenkins_slave.file_to_units')
971+ @mock.patch('reactive.jenkins_slave.write_default_conf')
972+ def test_hook_install(self, write_default_conf, file_to_units, apt_purge, service, mkdir, adduser):
973+ install()
974+ expected = [mock.call(home_dir='/var/lib/jenkins', system_user=True, username='jenkins')]
975+ self.assertEqual(adduser.call_args_list, expected)
976+ expected = [mock.call('/var/lib/jenkins', group='jenkins', owner='jenkins'),
977+ mock.call('/var/log/jenkins', group='jenkins', owner='jenkins')]
978+ self.assertEqual(mkdir.call_args_list, expected)
979+ self.assertEqual(service.call_args_list, [mock.call('enable', 'jenkins-slave')])
980+ self.assertEqual(apt_purge.call_args_list, [mock.call(['jenkins-slave'])])
981+ expected = [mock.call('files/download-slave.sh', '/usr/local/sbin/download-slave.sh'),
982+ mock.call('files/jenkins-slave-logrotate-config', '/etc/logrotate.d/jenkins-slave'),
983+ mock.call('files/jenkins-slave-systemd-config', '/lib/systemd/system/jenkins-slave.service')]
984+ self.assertEqual(file_to_units.call_args_list, expected)
985+ self.assertEqual(write_default_conf.call_args_list, [mock.call()])
986+ expected = [mock.call.queue_install(['wget', 'default-jre-headless']),
987+ mock.call.install_queued()]
988+ self.assertEqual(apt.method_calls, expected)
989+
990+ @mock.patch('charms.reactive.clear_flag')
991+ @mock.patch('charms.reactive.set_flag')
992+ @mock.patch('charmhelpers.core.hookenv.config')
993+ @mock.patch('charmhelpers.core.unitdata.kv')
994+ @mock.patch('reactive.jenkins_slave.write_default_conf')
995+ @mock.patch('reactive.jenkins_slave.file_to_units')
996+ def test_configure_jenkins_slave_no_url(self, file_to_units, write_default_conf, unitdata_kv, config,
997+ set_flag, clear_flag):
998+ config.return_value = {}
999+ unitdata_kv.return_value = {}
1000+ configure_jenkins_slave()
1001+ self.assertEqual(write_default_conf.call_args_list, [])
1002+ self.assertEqual(set_flag.call_args_list, [mock.call('jenkins-slave.blocked')])
1003+ self.assertEqual(clear_flag.call_args_list, [mock.call('jenkins-slave.active')])
1004+
1005+ @mock.patch('charms.reactive.clear_flag')
1006+ @mock.patch('charms.reactive.set_flag')
1007+ @mock.patch('charmhelpers.core.hookenv.config')
1008+ @mock.patch('charmhelpers.core.unitdata.kv')
1009+ @mock.patch('reactive.jenkins_slave.write_default_conf')
1010+ @mock.patch('reactive.jenkins_slave.file_to_units')
1011+ def test_configure_jenkins_slave_master_url(self, file_to_units, write_default_conf, unitdata_kv, config,
1012+ set_flag, clear_flag):
1013+ config.return_value = {'master_url': 'http://10.1.1.1:8080'}
1014+ unitdata_kv.return_value = {}
1015+ configure_jenkins_slave()
1016+ self.assertEqual(write_default_conf.call_args_list, [mock.call('http://10.1.1.1:8080')])
1017+ self.assertEqual(set_flag.call_args_list, [mock.call('jenkins-slave.configured')])
1018+ expected = [mock.call('jenkins-slave.active'), mock.call('nagios-nrpe.configured')]
1019+ self.assertEqual(clear_flag.call_args_list, expected)
1020+
1021+ @mock.patch('charms.reactive.clear_flag')
1022+ @mock.patch('charms.reactive.set_flag')
1023+ @mock.patch('charmhelpers.core.hookenv.config')
1024+ @mock.patch('charmhelpers.core.unitdata.kv')
1025+ @mock.patch('reactive.jenkins_slave.write_default_conf')
1026+ @mock.patch('reactive.jenkins_slave.file_to_units')
1027+ def test_configure_jenkins_slave_relation_url(self, file_to_units, write_default_conf, unitdata_kv, config,
1028+ set_flag, clear_flag):
1029+ config.return_value = {}
1030+ unitdata_kv.return_value = {'url': 'http://10.22.22.22:8080'}
1031+ configure_jenkins_slave()
1032+ print(write_default_conf.call_args_list)
1033+ self.assertEqual(write_default_conf.call_args_list, [mock.call('http://10.22.22.22:8080')])
1034+ self.assertEqual(set_flag.call_args_list, [mock.call('jenkins-slave.configured')])
1035+ expected = [mock.call('jenkins-slave.active'), mock.call('nagios-nrpe.configured')]
1036+ self.assertEqual(clear_flag.call_args_list, expected)
1037+
1038+ def test_write_default_conf_update(self):
1039+ write_default_conf('http://10.1.1.1:8080', self.user, self.group, self.conf_file)
1040+ self.assertTrue(conf_match(self.conf_file, 'JENKINS_URL', 'http://10.1.1.1:8080'))
1041+
1042+ def test_write_default_conf_reset(self):
1043+ write_default_conf(None, self.user, self.group, self.conf_file)
1044+ self.assertTrue(conf_match(self.conf_file, '#JENKINS_URL', ''))
1045+
1046+ def test_file_to_units_executable_sh(self):
1047+ source = os.path.join(self.charm_dir, 'files/download-slave.sh')
1048+ dest = os.path.join(self.tmpdir, os.path.basename(source))
1049+ file_to_units(source, dest, owner=self.user, group=self.group)
1050+ with open(dest, 'rb') as fh:
1051+ want = fh.read().decode('utf-8')
1052+ self.assertTrue(conf_equals(source, want))
1053+ self.assertEqual(pwd.getpwuid(os.stat(dest).st_uid).pw_name, self.user)
1054+ self.assertEqual(grp.getgrgid(os.stat(dest).st_gid).gr_name, self.group)
1055+ self.assertTrue(os.access(dest, os.X_OK))
1056+
1057+ def test_file_to_units_executable_py(self):
1058+ source = os.path.join(self.charm_dir, 'tests/unit/files/somefile.py')
1059+ dest = os.path.join(self.tmpdir, os.path.basename(source))
1060+ file_to_units(source, dest, owner=self.user, group=self.group)
1061+ with open(dest, 'rb') as fh:
1062+ want = fh.read().decode('utf-8')
1063+ self.assertTrue(conf_equals(source, want))
1064+ self.assertEqual(pwd.getpwuid(os.stat(dest).st_uid).pw_name, self.user)
1065+ self.assertEqual(grp.getgrgid(os.stat(dest).st_gid).gr_name, self.group)
1066+ self.assertTrue(os.access(dest, os.X_OK))
1067+
1068+ def test_file_to_units_non_executable(self):
1069+ source = os.path.join(self.charm_dir, 'files/jenkins-slave-logrotate-config')
1070+ dest = os.path.join(self.tmpdir, os.path.basename(source))
1071+ file_to_units(source, dest, owner=self.user, group=self.group)
1072+ with open(dest, 'rb') as fh:
1073+ want = fh.read().decode('utf-8')
1074+ self.assertTrue(conf_equals(dest, want))
1075+ self.assertEqual(pwd.getpwuid(os.stat(dest).st_uid).pw_name, self.user)
1076+ self.assertEqual(grp.getgrgid(os.stat(dest).st_gid).gr_name, self.group)
1077+ self.assertFalse(os.access(dest, os.X_OK))
1078+
1079+ def test_file_to_units_non_executable_x_on_disk(self):
1080+ source = os.path.join(self.charm_dir, 'tests/unit/files/somefile')
1081+ dest = os.path.join(self.tmpdir, os.path.basename(source))
1082+ file_to_units(source, dest, owner=self.user, group=self.group)
1083+ with open(dest, 'rb') as fh:
1084+ want = fh.read().decode('utf-8')
1085+ self.assertTrue(conf_equals(source, want))
1086+ self.assertEqual(pwd.getpwuid(os.stat(dest).st_uid).pw_name, self.user)
1087+ self.assertEqual(grp.getgrgid(os.stat(dest).st_gid).gr_name, self.group)
1088+ self.assertFalse(os.access(dest, os.X_OK))
1089+
1090+
1091+def conf_equals(conf_file, want):
1092+ with open(conf_file, 'rb') as conf:
1093+ got = conf.read().decode('utf-8')
1094+ if got == want:
1095+ return True
1096+ print('{}\n != \n{}'.format(got, want))
1097+ return False
1098+
1099+
1100+def conf_match(conf_file, key, value):
1101+ with open(conf_file, 'rb') as conf:
1102+ for line in conf.readlines():
1103+ line = line.decode('utf-8').rstrip('\n')
1104+ if line == '{}="{}"'.format(key, value):
1105+ return True
1106+ return False
1107+
1108+
1109+if __name__ == '__main__':
1110+ unittest.main()
1111
1112=== added file 'tox.ini'
1113--- tox.ini 1970-01-01 00:00:00 +0000
1114+++ tox.ini 2019-03-01 05:39:53 +0000
1115@@ -0,0 +1,37 @@
1116+[tox]
1117+skipsdist=True
1118+envlist = unit, functional
1119+skip_missing_interpreters = True
1120+
1121+[testenv]
1122+basepython = python3
1123+setenv =
1124+ PYTHONPATH = .
1125+
1126+[testenv:unit]
1127+commands = pytest -v --ignore {toxinidir}/tests/functional --cov=lib --cov=reactive --cov=actions --cov-report=term
1128+deps = -r{toxinidir}/tests/unit/requirements.txt
1129+ -r{toxinidir}/requirements.txt
1130+setenv = PYTHONPATH={toxinidir}/lib
1131+
1132+[testenv:functional]
1133+passenv =
1134+ HOME
1135+ JUJU_REPOSITORY
1136+ PATH
1137+commands = pytest -v --ignore {toxinidir}/tests/unit
1138+deps = -r{toxinidir}/tests/functional/requirements.txt
1139+ -r{toxinidir}/requirements.txt
1140+
1141+[testenv:lint]
1142+commands = flake8
1143+deps = flake8
1144+
1145+[flake8]
1146+exclude =
1147+ .git,
1148+ __pycache__,
1149+ .tox,
1150+ hooks/install.d/,
1151+max-line-length = 120
1152+max-complexity = 10

Subscribers

People subscribed via source and target branches