Merge ~kzapalowicz/snappy-hwe-snaps/+git/jenkins-jobs:fix/infrastructure-is-missing-file into ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-ap:master
- Git
- lp:~kzapalowicz/snappy-hwe-snaps/+git/jenkins-jobs
- fix/infrastructure-is-missing-file
- Merge into master
Status: | Superseded |
---|---|
Proposed branch: | ~kzapalowicz/snappy-hwe-snaps/+git/jenkins-jobs:fix/infrastructure-is-missing-file |
Merge into: | ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-ap:master |
Diff against target: |
4384 lines (+4018/-0) (has conflicts) 58 files modified
.ci_tests_disabled (+0/-0) README.md (+56/-0) docker/Dockerfile (+59/-0) docker/initial-setup.groovy (+34/-0) docker/jenkins.sh (+24/-0) docker/plugins.sh (+28/-0) docker/plugins.txt (+29/-0) jobs/image/common-job-prepare.sh (+7/-0) jobs/image/image-build-worker.sh (+27/-0) jobs/image/image-build-worker.yaml (+15/-0) jobs/image/image-project-jobs.yaml (+4/-0) jobs/infrastructure/common-job-prepare.sh (+14/-0) jobs/infrastructure/credentials-0-ssh.sh (+58/-0) jobs/infrastructure/credentials-0-ssh.yaml (+29/-0) jobs/infrastructure/credentials-1-launchpad.py (+86/-0) jobs/infrastructure/credentials-1-launchpad.yaml (+21/-0) jobs/infrastructure/credentials-2-launchpad-plugin.sh (+108/-0) jobs/infrastructure/credentials-2-launchpad-plugin.yaml (+24/-0) jobs/infrastructure/deploy-jenkins-jobs.sh (+58/-0) jobs/infrastructure/deploy-jenkins-jobs.yaml (+24/-0) jobs/infrastructure/infrastructure-jobs.yaml (+8/-0) jobs/infrastructure/prepare-0-install.sh (+47/-0) jobs/infrastructure/prepare-0-install.yaml (+27/-0) jobs/snap/common-job-prepare.sh (+24/-0) jobs/snap/snap-automerger.sh (+27/-0) jobs/snap/snap-automerger.yaml (+22/-0) jobs/snap/snap-build-update-chroot.sh (+28/-0) jobs/snap/snap-build-update-chroot.yaml (+29/-0) jobs/snap/snap-build-worker.sh (+147/-0) jobs/snap/snap-build-worker.yaml (+72/-0) jobs/snap/snap-build.yaml (+91/-0) jobs/snap/snap-cleanup.sh (+38/-0) jobs/snap/snap-cleanup.yaml (+32/-0) jobs/snap/snap-nightly.yaml (+41/-0) jobs/snap/snap-project-jobs.yaml (+13/-0) jobs/snap/snap-release.sh (+170/-0) jobs/snap/snap-release.yaml (+58/-0) jobs/snap/snap-test.sh (+121/-0) jobs/snap/snap-test.yaml (+64/-0) jobs/snap/snap-trigger-ci.sh (+29/-0) jobs/snap/snap-trigger-ci.yaml (+22/-0) jobs/snap/snap-update-mp.sh (+30/-0) jobs/snap/snap-update-mp.yaml (+31/-0) local.conf (+11/-0) local.yaml (+60/-0) run-tests.sh (+4/-0) tools/automerge-mps.py (+152/-0) tools/build-rootfs-create (+26/-0) tools/common.sh (+93/-0) tools/delete-ci-repo.py (+45/-0) tools/hardware-test.sh (+112/-0) tools/se_utils/__init__.py (+122/-0) tools/shyaml (+454/-0) tools/snapbuild.sh (+192/-0) tools/test-snap.sh (+100/-0) tools/trigger-ci.py (+270/-0) tools/trigger-lp-build.py (+230/-0) tools/vote-on-merge-proposal.py (+271/-0) Conflict in README.md Conflict in run-tests.sh |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Snappy HWE Team | Pending | ||
Review via email: mp+328106@code.launchpad.net |
This proposal has been superseded by a proposal from 2017-07-26.
Commit message
Description of the change
jobs/infrastruc
Unmerged commits
- 4fb056a... by =?utf-8?q?Konrad_Zapa=C5=82owicz?= <email address hidden>
-
jobs/infrastruc
ture: add missing file - a7a4e17... by System Enablement CI Bot <email address hidden>
-
Merge remote tracking branch debug-perm
Merge-Proposal: https:/
/code.launchpad .net/~alfonsosa nchezbeato/ snappy- hwe-snaps/ +git/jenkins- jobs/+merge/ 326212 Author: Alfonso Sanchez-Beato <email address hidden>
automerge: do not fail due to lack of e-mail
If the registrant of the MP had not defined an e-mail in the launchpad
account, we were failing to merge. - dd90414... by System Enablement CI Bot <email address hidden>
-
Merge remote tracking branch master
Merge-Proposal: https:/
/code.launchpad .net/~jasonlo/ snappy- hwe-snaps/ +git/jenkins- jobs/+merge/ 325971 Author: lo <email address hidden>
Create a system-
enablement- image-build- worker item on Jenkins to build the final image tarball. - 9d2a88b... by lo
-
Rebase to top of master.
- ff946e1... by lo
-
Remove output-dir from yaml and verify build worker script.
- de2c800... by lo
-
Verify description to be more general.
- 54b8a81... by lo
-
Use quote on variables.
- bede38b... by lo
-
Verify according to image build document.
- 7e81583... by lo
-
Add outdir parameter to indicate output file directory. To avoid frequently deploying after build.sh done some FIXME in the future, we keep one build.sh command in image-build-worker on Jenkins.
- 56b9a84... by lo
-
To expand build.sh from brando as an example of image build process.
Preview Diff
1 | diff --git a/.ci_tests_disabled b/.ci_tests_disabled | |||
2 | 0 | new file mode 100644 | 0 | new file mode 100644 |
3 | index 0000000..e69de29 | |||
4 | --- /dev/null | |||
5 | +++ b/.ci_tests_disabled | |||
6 | diff --git a/README.md b/README.md | |||
7 | index 1a49a98..c4af3d3 100644 | |||
8 | --- a/README.md | |||
9 | +++ b/README.md | |||
10 | @@ -1,3 +1,4 @@ | |||
11 | 1 | <<<<<<< README.md | ||
12 | 1 | # wifi-ap | 2 | # wifi-ap |
13 | 2 | 3 | ||
14 | 3 | This snap provided WiFi AP functionality out of the box. | 4 | This snap provided WiFi AP functionality out of the box. |
15 | @@ -81,3 +82,58 @@ snap. | |||
16 | 81 | If you want to see more verbose debugging output of spread run | 82 | If you want to see more verbose debugging output of spread run |
17 | 82 | 83 | ||
18 | 83 | $ ./run-tests --debug | 84 | $ ./run-tests --debug |
19 | 85 | ======= | ||
20 | 86 | # Jenkins Jobs | ||
21 | 87 | |||
22 | 88 | This is a repository collecting a set of jenkins job definitions | ||
23 | 89 | used to build a contineous integrate pipeline for snap based | ||
24 | 90 | components. | ||
25 | 91 | |||
26 | 92 | ## Required Jenkins Plugins | ||
27 | 93 | |||
28 | 94 | * build-name-setter | ||
29 | 95 | * conditional-buildstep | ||
30 | 96 | * description-setter | ||
31 | 97 | * dynamic-axis | ||
32 | 98 | * matrix-combinations-parameter | ||
33 | 99 | * matrix-project | ||
34 | 100 | * nodelabelparameter | ||
35 | 101 | * parameterized-trigger | ||
36 | 102 | * rebuild | ||
37 | 103 | * timestamper | ||
38 | 104 | * ws-cleanup | ||
39 | 105 | |||
40 | 106 | ## Test locally | ||
41 | 107 | |||
42 | 108 | Build the docker container which comes with this repository: | ||
43 | 109 | |||
44 | 110 | ``` | ||
45 | 111 | $ cd docker | ||
46 | 112 | $ sudo docker build -t se-jenkins . | ||
47 | 113 | ``` | ||
48 | 114 | |||
49 | 115 | Spawn up a docker container with jenkins | ||
50 | 116 | |||
51 | 117 | ``` | ||
52 | 118 | $ sudo docker run -p 8080:8080 -p 50000:50000 --name jenkins se-jenkins | ||
53 | 119 | ``` | ||
54 | 120 | |||
55 | 121 | You can now login into jenkins on http://localhost:8080 with user 'system-enablement-ci-bot' | ||
56 | 122 | and password 'jenkins'. | ||
57 | 123 | |||
58 | 124 | Install jenkins-job-builder on your host via | ||
59 | 125 | |||
60 | 126 | ``` | ||
61 | 127 | $ sudo apt install python3-pip | ||
62 | 128 | $ pip3 install jenkins-job-builder | ||
63 | 129 | ``` | ||
64 | 130 | |||
65 | 131 | Now you should have everything setup and ready for a first deployment: | ||
66 | 132 | |||
67 | 133 | ``` | ||
68 | 134 | $ jenkins-jobs --conf local.conf update local.yaml:jobs/infrastructure:jobs/snap:jobs/image | ||
69 | 135 | ``` | ||
70 | 136 | |||
71 | 137 | This will create all configured jobs on the jenkins instance or update | ||
72 | 138 | them if already available. | ||
73 | 139 | >>>>>>> README.md | ||
74 | diff --git a/docker/Dockerfile b/docker/Dockerfile | |||
75 | 84 | new file mode 100644 | 140 | new file mode 100644 |
76 | index 0000000..a898d1e | |||
77 | --- /dev/null | |||
78 | +++ b/docker/Dockerfile | |||
79 | @@ -0,0 +1,59 @@ | |||
80 | 1 | FROM ubuntu:latest | ||
81 | 2 | |||
82 | 3 | ENV DEBIAN_FRONTEND noninteractive | ||
83 | 4 | ENV INITRD No | ||
84 | 5 | ENV LANG en_US.UTF-8 | ||
85 | 6 | |||
86 | 7 | RUN apt-get update && \ | ||
87 | 8 | apt-get upgrade -y && \ | ||
88 | 9 | apt-get install -y --no-install-recommends \ | ||
89 | 10 | vim.tiny wget curl sudo net-tools pwgen \ | ||
90 | 11 | git-core logrotate software-properties-common locales openssh-client && \ | ||
91 | 12 | locale-gen en_US en_US.UTF-8 && \ | ||
92 | 13 | apt-get clean && \ | ||
93 | 14 | rm -rf /var/lib/apt/lists/* | ||
94 | 15 | |||
95 | 16 | RUN apt-get update && \ | ||
96 | 17 | apt-get install --no-install-recommends -y openjdk-8-jre-headless && \ | ||
97 | 18 | apt-get clean && \ | ||
98 | 19 | rm -rf /var/lib/apt/lists/* | ||
99 | 20 | |||
100 | 21 | RUN wget -qO - http://pkg.jenkins-ci.org/debian/jenkins-ci.org.key | sudo apt-key add - && \ | ||
101 | 22 | echo 'deb http://pkg.jenkins-ci.org/debian binary/' \ | ||
102 | 23 | | tee /etc/apt/sources.list.d/jenkins.list && \ | ||
103 | 24 | apt-get update && \ | ||
104 | 25 | apt-get install --no-install-recommends -y jenkins && \ | ||
105 | 26 | apt-get clean && \ | ||
106 | 27 | rm -rf /var/lib/apt/lists/* && \ | ||
107 | 28 | update-rc.d -f jenkins disable | ||
108 | 29 | |||
109 | 30 | RUN usermod -d /var/jenkins jenkins | ||
110 | 31 | |||
111 | 32 | # Passthrough all sudo requests without requiring a password for our jenkins user | ||
112 | 33 | RUN echo "jenkins ALL = NOPASSWD: ALL" > /etc/sudoers.d/jenkins | ||
113 | 34 | |||
114 | 35 | RUN apt-get update && apt-get install --yes unzip zip wget | ||
115 | 36 | |||
116 | 37 | ENV JENKINS_UC https://updates.jenkins-ci.org | ||
117 | 38 | ENV JENKINS_HOME /var/jenkins | ||
118 | 39 | ENV COPY_REFERENCE_FILE_LOG $JENKINS_HOME/copy_reference_file.log | ||
119 | 40 | ENV JENKINS_USER system-enablement-ci-bot | ||
120 | 41 | ENV JENKINS_PASS jenkins | ||
121 | 42 | |||
122 | 43 | COPY plugins.sh /usr/local/bin/plugins.sh | ||
123 | 44 | RUN chmod +x /usr/local/bin/plugins.sh | ||
124 | 45 | COPY plugins.txt /tmp/plugins.txt | ||
125 | 46 | RUN /usr/local/bin/plugins.sh /tmp/plugins.txt | ||
126 | 47 | |||
127 | 48 | COPY jenkins.sh /usr/local/bin/jenkins.sh | ||
128 | 49 | RUN chmod +x /usr/local/bin/jenkins.sh | ||
129 | 50 | |||
130 | 51 | COPY initial-setup.groovy /usr/share/jenkins/ref/init.groovy.d/ | ||
131 | 52 | |||
132 | 53 | ENV JAVA_OPTS -Djenkins.install.runSetupWizard=false | ||
133 | 54 | |||
134 | 55 | EXPOSE 8080 50000 | ||
135 | 56 | |||
136 | 57 | USER jenkins | ||
137 | 58 | |||
138 | 59 | CMD ["/usr/local/bin/jenkins.sh"] | ||
139 | diff --git a/docker/initial-setup.groovy b/docker/initial-setup.groovy | |||
140 | 0 | new file mode 100644 | 60 | new file mode 100644 |
141 | index 0000000..5a9598d | |||
142 | --- /dev/null | |||
143 | +++ b/docker/initial-setup.groovy | |||
144 | @@ -0,0 +1,34 @@ | |||
145 | 1 | import jenkins.model.* | ||
146 | 2 | import jenkins.security.* | ||
147 | 3 | import hudson.security.* | ||
148 | 4 | |||
149 | 5 | def env = System.getenv() | ||
150 | 6 | |||
151 | 7 | def jenkins = Jenkins.getInstance() | ||
152 | 8 | |||
153 | 9 | jenkins.setLabelString("snap build test release misc monitor") | ||
154 | 10 | |||
155 | 11 | jenkins.setSecurityRealm(new HudsonPrivateSecurityRealm(false)) | ||
156 | 12 | jenkins.setAuthorizationStrategy(new GlobalMatrixAuthorizationStrategy()) | ||
157 | 13 | |||
158 | 14 | def user = jenkins.getSecurityRealm().createAccount(env.JENKINS_USER, env.JENKINS_PASS) | ||
159 | 15 | ApiTokenProperty t = user.getProperty(ApiTokenProperty.class) | ||
160 | 16 | def token = t.getApiTokenInsecure() | ||
161 | 17 | |||
162 | 18 | println "" | ||
163 | 19 | println "########################################################################" | ||
164 | 20 | println "API token for user " + env.JENKINS_USER + " is " + token | ||
165 | 21 | println "########################################################################" | ||
166 | 22 | println "" | ||
167 | 23 | |||
168 | 24 | user.save() | ||
169 | 25 | |||
170 | 26 | jenkins.getAuthorizationStrategy().add(Jenkins.ADMINISTER, env.JENKINS_USER) | ||
171 | 27 | jenkins.save() | ||
172 | 28 | |||
173 | 29 | for (slave in jenkins.model.Jenkins.instance.slaves) { | ||
174 | 30 | println "Slave: " + slave.getNodeName() + "\n"; | ||
175 | 31 | println "Label: " + slave.getLabelString() + "\n\n"; | ||
176 | 32 | slave.setLabelString("snap build test monitor release misc") | ||
177 | 33 | } | ||
178 | 34 | jenkins.save() | ||
179 | diff --git a/docker/jenkins.sh b/docker/jenkins.sh | |||
180 | 0 | new file mode 100644 | 35 | new file mode 100644 |
181 | index 0000000..5d095be | |||
182 | --- /dev/null | |||
183 | +++ b/docker/jenkins.sh | |||
184 | @@ -0,0 +1,24 @@ | |||
185 | 1 | #! /bin/bash | ||
186 | 2 | |||
187 | 3 | set -e | ||
188 | 4 | |||
189 | 5 | # Copy files from /usr/share/jenkins/ref into $JENKINS_HOME | ||
190 | 6 | # So the initial JENKINS-HOME is set with expected content. | ||
191 | 7 | # Don't override, as this is just a reference setup, and use from UI | ||
192 | 8 | # can then change this, upgrade plugins, etc. | ||
193 | 9 | copy_reference_file() { | ||
194 | 10 | f=${1%/} | ||
195 | 11 | rel=${f:23} | ||
196 | 12 | dir=$(dirname ${f}) | ||
197 | 13 | if [[ ! -e $JENKINS_HOME/${rel} ]] | ||
198 | 14 | then | ||
199 | 15 | mkdir -p $JENKINS_HOME/${dir:23} | ||
200 | 16 | cp -r /usr/share/jenkins/ref/${rel} $JENKINS_HOME/${rel}; | ||
201 | 17 | # pin plugins on initial copy | ||
202 | 18 | [[ ${rel} == plugins/*.jpi ]] && touch $JENKINS_HOME/${rel}.pinned | ||
203 | 19 | fi; | ||
204 | 20 | } | ||
205 | 21 | export -f copy_reference_file | ||
206 | 22 | find /usr/share/jenkins/ref/ -type f -exec bash -c "copy_reference_file '{}'" \; | ||
207 | 23 | |||
208 | 24 | exec /usr/bin/java $JAVA_OPTS -jar /usr/share/jenkins/jenkins.war | ||
209 | diff --git a/docker/plugins.sh b/docker/plugins.sh | |||
210 | 0 | new file mode 100755 | 25 | new file mode 100755 |
211 | index 0000000..ec8e8e5 | |||
212 | --- /dev/null | |||
213 | +++ b/docker/plugins.sh | |||
214 | @@ -0,0 +1,28 @@ | |||
215 | 1 | #! /bin/bash | ||
216 | 2 | |||
217 | 3 | # Parse a support-core plugin -style txt file as specification for jenkins plugins to be installed | ||
218 | 4 | # in the reference directory, so user can define a derived Docker image with just : | ||
219 | 5 | # | ||
220 | 6 | # FROM jenkins | ||
221 | 7 | # COPY plugins.txt /plugins.txt | ||
222 | 8 | # RUN /usr/local/bin/plugins.sh /plugins.txt | ||
223 | 9 | # | ||
224 | 10 | |||
225 | 11 | set -e | ||
226 | 12 | |||
227 | 13 | REF=/usr/share/jenkins/ref/plugins | ||
228 | 14 | mkdir -p $REF | ||
229 | 15 | |||
230 | 16 | while read spec || [ -n "$spec" ]; do | ||
231 | 17 | plugin=(${spec//:/ }); | ||
232 | 18 | [[ ${plugin[0]} =~ ^# ]] && continue | ||
233 | 19 | [[ ${plugin[0]} =~ ^\s*$ ]] && continue | ||
234 | 20 | [[ -z ${plugin[1]} ]] && plugin[1]="latest" | ||
235 | 21 | echo "Downloading ${plugin[0]}:${plugin[1]}" | ||
236 | 22 | |||
237 | 23 | if [ -z "$JENKINS_UC_DOWNLOAD" ]; then | ||
238 | 24 | JENKINS_UC_DOWNLOAD=$JENKINS_UC/download | ||
239 | 25 | fi | ||
240 | 26 | curl -sSL -f ${JENKINS_UC_DOWNLOAD}/plugins/${plugin[0]}/${plugin[1]}/${plugin[0]}.hpi -o $REF/${plugin[0]}.jpi | ||
241 | 27 | unzip -qqt $REF/${plugin[0]}.jpi | ||
242 | 28 | done < $1 | ||
243 | diff --git a/docker/plugins.txt b/docker/plugins.txt | |||
244 | 0 | new file mode 100644 | 29 | new file mode 100644 |
245 | index 0000000..5020d33 | |||
246 | --- /dev/null | |||
247 | +++ b/docker/plugins.txt | |||
248 | @@ -0,0 +1,29 @@ | |||
249 | 1 | build-name-setter | ||
250 | 2 | conditional-buildstep | ||
251 | 3 | description-setter | ||
252 | 4 | dynamic-axis | ||
253 | 5 | matrix-combinations-parameter | ||
254 | 6 | matrix-project | ||
255 | 7 | nodelabelparameter | ||
256 | 8 | parameterized-trigger | ||
257 | 9 | rebuild | ||
258 | 10 | timestamper | ||
259 | 11 | ws-cleanup | ||
260 | 12 | maven-plugin | ||
261 | 13 | token-macro | ||
262 | 14 | junit | ||
263 | 15 | script-security | ||
264 | 16 | jquery | ||
265 | 17 | workflow-basic-steps | ||
266 | 18 | structs | ||
267 | 19 | javadoc | ||
268 | 20 | workflow-api | ||
269 | 21 | workflow-step-api | ||
270 | 22 | mailer | ||
271 | 23 | matrix-auth | ||
272 | 24 | scm-api | ||
273 | 25 | display-url-api | ||
274 | 26 | scm-api | ||
275 | 27 | icon-shim | ||
276 | 28 | resource-disposer | ||
277 | 29 | run-condition | ||
278 | diff --git a/jobs/image/common-job-prepare.sh b/jobs/image/common-job-prepare.sh | |||
279 | 0 | new file mode 100644 | 30 | new file mode 100644 |
280 | index 0000000..5feaa8d | |||
281 | --- /dev/null | |||
282 | +++ b/jobs/image/common-job-prepare.sh | |||
283 | @@ -0,0 +1,7 @@ | |||
284 | 1 | cat << EOF > $WORKSPACE/.build_env | ||
285 | 2 | BOT_USERNAME={bot_username} | ||
286 | 3 | LAUNCHPAD_PROJECT={launchpad_project} | ||
287 | 4 | LAUNCHPAD_TEAM={launchpad_team} | ||
288 | 5 | BUILD_SCRIPTS=$WORKSPACE/jenkins-jobs | ||
289 | 6 | BUILD_ID=$BUILD_ID | ||
290 | 7 | EOF | ||
291 | diff --git a/jobs/image/image-build-worker.sh b/jobs/image/image-build-worker.sh | |||
292 | 0 | new file mode 100644 | 8 | new file mode 100644 |
293 | index 0000000..161c5e4 | |||
294 | --- /dev/null | |||
295 | +++ b/jobs/image/image-build-worker.sh | |||
296 | @@ -0,0 +1,27 @@ | |||
297 | 1 | #!/bin/bash | ||
298 | 2 | # | ||
299 | 3 | # Copyright (C) 2017 Canonical Ltd | ||
300 | 4 | # | ||
301 | 5 | # This program is free software: you can redistribute it and/or modify | ||
302 | 6 | # it under the terms of the GNU General Public License version 3 as | ||
303 | 7 | # published by the Free Software Foundation. | ||
304 | 8 | # | ||
305 | 9 | # This program is distributed in the hope that it will be useful, | ||
306 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
307 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
308 | 12 | # GNU General Public License for more details. | ||
309 | 13 | # | ||
310 | 14 | # You should have received a copy of the GNU General Public License | ||
311 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
312 | 16 | |||
313 | 17 | set -ex | ||
314 | 18 | |||
315 | 19 | . "$WORKSPACE/.build_env" | ||
316 | 20 | |||
317 | 21 | rm -rf "$WORKSPACE"/image-builds | ||
318 | 22 | |||
319 | 23 | git clone git+ssh://git.launchpad.net/~$LAUNCHPAD_TEAM/$LAUNCHPAD_PROJECT/+git/image-builds | ||
320 | 24 | cd image-builds | ||
321 | 25 | |||
322 | 26 | # Project specific image build process | ||
323 | 27 | ./build.sh | ||
324 | diff --git a/jobs/image/image-build-worker.yaml b/jobs/image/image-build-worker.yaml | |||
325 | 0 | new file mode 100644 | 28 | new file mode 100644 |
326 | index 0000000..f34faf3 | |||
327 | --- /dev/null | |||
328 | +++ b/jobs/image/image-build-worker.yaml | |||
329 | @@ -0,0 +1,15 @@ | |||
330 | 1 | - job-template: | ||
331 | 2 | name: '{name}-image-build-worker' | ||
332 | 3 | project-type: freestyle | ||
333 | 4 | defaults: global | ||
334 | 5 | description: "Build an image." | ||
335 | 6 | display-name: "{name}-image-build-worker" | ||
336 | 7 | concurrent: true | ||
337 | 8 | node: snap && build | ||
338 | 9 | builders: | ||
339 | 10 | - shell: | ||
340 | 11 | !include-raw: | ||
341 | 12 | - common-job-prepare.sh | ||
342 | 13 | - shell: | ||
343 | 14 | !include-raw-escape: | ||
344 | 15 | - image-build-worker.sh | ||
345 | diff --git a/jobs/image/image-project-jobs.yaml b/jobs/image/image-project-jobs.yaml | |||
346 | 0 | new file mode 100644 | 16 | new file mode 100644 |
347 | index 0000000..0b517c7 | |||
348 | --- /dev/null | |||
349 | +++ b/jobs/image/image-project-jobs.yaml | |||
350 | @@ -0,0 +1,4 @@ | |||
351 | 1 | - job-group: | ||
352 | 2 | name: image-project-jobs | ||
353 | 3 | jobs: | ||
354 | 4 | - '{name}-image-build-worker' | ||
355 | diff --git a/jobs/infrastructure/common-job-prepare.sh b/jobs/infrastructure/common-job-prepare.sh | |||
356 | 0 | new file mode 100644 | 5 | new file mode 100644 |
357 | index 0000000..3dbc9fd | |||
358 | --- /dev/null | |||
359 | +++ b/jobs/infrastructure/common-job-prepare.sh | |||
360 | @@ -0,0 +1,14 @@ | |||
361 | 1 | JENKINS_JOBS_GIT_REPO="{jobs-git-repo}" | ||
362 | 2 | JENKINS_JOBS_GIT_REPO_BRANCH="{jobs-git-repo-branch}" | ||
363 | 3 | |||
364 | 4 | if [ -n "${{CLEANUP_WORKSPACE}}" ] && [ "${{CLEANUP_WORKSPACE}}" -eq 1 ]; then | ||
365 | 5 | rm -rf ${{WORKSPACE}}/* | ||
366 | 6 | fi | ||
367 | 7 | |||
368 | 8 | cat << EOF > $WORKSPACE/.build_env | ||
369 | 9 | JENKINS_JOBS_REPO="{jobs-git-repo}" | ||
370 | 10 | JENKINS_JOBS_BRANCH="{jobs-git-repo-branch}" | ||
371 | 11 | JENKINS_CONFIG_REPO="{config-git-repo}" | ||
372 | 12 | JENKINS_CONFIG_BRANCH="{config-git-repo-branch}" | ||
373 | 13 | JENKINS_INSTANCE="{jenkins-instance}" | ||
374 | 14 | EOF | ||
375 | diff --git a/jobs/infrastructure/credentials-0-ssh.sh b/jobs/infrastructure/credentials-0-ssh.sh | |||
376 | 0 | new file mode 100644 | 15 | new file mode 100644 |
377 | index 0000000..d72fd49 | |||
378 | --- /dev/null | |||
379 | +++ b/jobs/infrastructure/credentials-0-ssh.sh | |||
380 | @@ -0,0 +1,58 @@ | |||
381 | 1 | #!/bin/sh -ex | ||
382 | 2 | # | ||
383 | 3 | # Copyright (C) 2016 Canonical Ltd | ||
384 | 4 | # | ||
385 | 5 | # This program is free software: you can redistribute it and/or modify | ||
386 | 6 | # it under the terms of the GNU General Public License version 3 as | ||
387 | 7 | # published by the Free Software Foundation. | ||
388 | 8 | # | ||
389 | 9 | # This program is distributed in the hope that it will be useful, | ||
390 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
391 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
392 | 12 | # GNU General Public License for more details. | ||
393 | 13 | # | ||
394 | 14 | # You should have received a copy of the GNU General Public License | ||
395 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
396 | 16 | |||
397 | 17 | # put it in $JENKINS_HOME so it doesn't get purged with the workspace | ||
398 | 18 | SSH_PATH="${{JENKINS_HOME}}/.ssh/" | ||
399 | 19 | |||
400 | 20 | # this is so that this key's only used with git | ||
401 | 21 | SSH_KEY_PATH="${{SSH_PATH}}/git.launchpad.net/{bot_username}" | ||
402 | 22 | |||
403 | 23 | if [ ! -d "${{SSH_KEY_PATH}}" ]; then | ||
404 | 24 | mkdir -p "${{SSH_KEY_PATH}}" | ||
405 | 25 | fi | ||
406 | 26 | |||
407 | 27 | # don't care about host keys, but use the one id key for launchpad | ||
408 | 28 | # TODO: use http://pad.lv/p/canonical-sshebang instead | ||
409 | 29 | cat > "${{SSH_PATH}}/config" << EOF | ||
410 | 30 | Host * | ||
411 | 31 | StrictHostKeyChecking no | ||
412 | 32 | |||
413 | 33 | Host git.launchpad.net | ||
414 | 34 | User {bot_username} | ||
415 | 35 | IdentityFile ~/.ssh/%h/%r/id_rsa | ||
416 | 36 | EOF | ||
417 | 37 | |||
418 | 38 | # only use keys if both were uploaded, otherwise fail | ||
419 | 39 | if [ -f "keys/id_rsa" -o -f "keys/id_rsa.pub" ]; then | ||
420 | 40 | [ -f "keys/id_rsa" -a -f "keys/id_rsa.pub" ] || \ | ||
421 | 41 | (echo "ERROR: You need to upload both keys, or none of them"; exit 1) | ||
422 | 42 | mv "keys/id_rsa" "keys/id_rsa.pub" "${{SSH_KEY_PATH}}/" | ||
423 | 43 | chmod 600 ${{SSH_KEY_PATH}}/* | ||
424 | 44 | fi | ||
425 | 45 | |||
426 | 46 | # generate the keypair if none was uploaded or pre-existing | ||
427 | 47 | if [ ! -f "${{SSH_KEY_PATH}}/id_rsa" ]; then | ||
428 | 48 | ssh-keygen -f "${{SSH_KEY_PATH}}/id_rsa" | ||
429 | 49 | fi | ||
430 | 50 | |||
431 | 51 | # display the public key | ||
432 | 52 | echo "The public key for this jenkins user is:" | ||
433 | 53 | echo "----------" | ||
434 | 54 | cat "${{SSH_KEY_PATH}}/id_rsa.pub" | ||
435 | 55 | echo "----------" | ||
436 | 56 | |||
437 | 57 | # Probe ssh public ssh key for relevant hosts and add it to our known_hosts file | ||
438 | 58 | ssh-keyscan -t rsa,dsa git.launchpad.net >> ${{SSH_PATH}}/known_hosts | ||
439 | diff --git a/jobs/infrastructure/credentials-0-ssh.yaml b/jobs/infrastructure/credentials-0-ssh.yaml | |||
440 | 0 | new file mode 100644 | 59 | new file mode 100644 |
441 | index 0000000..add42d4 | |||
442 | --- /dev/null | |||
443 | +++ b/jobs/infrastructure/credentials-0-ssh.yaml | |||
444 | @@ -0,0 +1,29 @@ | |||
445 | 1 | - job-template: | ||
446 | 2 | name: '{name}-credentials-0-ssh' | ||
447 | 3 | project-type: matrix | ||
448 | 4 | description: | | ||
449 | 5 | This job will generate or store the supplied RSA keypair | ||
450 | 6 | and display the public key for use with Launchpad. | ||
451 | 7 | properties: | ||
452 | 8 | - build-discarder: | ||
453 | 9 | num-to-keep: 1 | ||
454 | 10 | - rebuild | ||
455 | 11 | parameters: | ||
456 | 12 | - matrix-combinations: | ||
457 | 13 | name: nodes | ||
458 | 14 | - file: | ||
459 | 15 | name: 'keys/id_rsa' | ||
460 | 16 | description: Private RSA key | ||
461 | 17 | - file: | ||
462 | 18 | name: 'keys/id_rsa.pub' | ||
463 | 19 | description: Public RSA key | ||
464 | 20 | axes: | ||
465 | 21 | - axis: | ||
466 | 22 | type: slave | ||
467 | 23 | name: node | ||
468 | 24 | values: '{obj:build_slaves}' | ||
469 | 25 | wrappers: | ||
470 | 26 | - timestamps | ||
471 | 27 | builders: | ||
472 | 28 | - shell: | ||
473 | 29 | !include-raw: credentials-0-ssh.sh | ||
474 | diff --git a/jobs/infrastructure/credentials-1-launchpad.py b/jobs/infrastructure/credentials-1-launchpad.py | |||
475 | 0 | new file mode 100644 | 30 | new file mode 100644 |
476 | index 0000000..59dd706 | |||
477 | --- /dev/null | |||
478 | +++ b/jobs/infrastructure/credentials-1-launchpad.py | |||
479 | @@ -0,0 +1,86 @@ | |||
480 | 1 | #!/usr/bin/env python | ||
481 | 2 | # | ||
482 | 3 | # Copyright (C) 2016 Canonical Ltd | ||
483 | 4 | # | ||
484 | 5 | # This program is free software: you can redistribute it and/or modify | ||
485 | 6 | # it under the terms of the GNU General Public License version 3 as | ||
486 | 7 | # published by the Free Software Foundation. | ||
487 | 8 | # | ||
488 | 9 | # This program is distributed in the hope that it will be useful, | ||
489 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
490 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
491 | 12 | # GNU General Public License for more details. | ||
492 | 13 | # | ||
493 | 14 | # You should have received a copy of the GNU General Public License | ||
494 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
495 | 16 | |||
496 | 17 | import sys | ||
497 | 18 | import time | ||
498 | 19 | |||
499 | 20 | from launchpadlib.credentials import RequestTokenAuthorizationEngine | ||
500 | 21 | from launchpadlib.credentials import UnencryptedFileCredentialStore | ||
501 | 22 | from launchpadlib.launchpad import Launchpad | ||
502 | 23 | from lazr.restfulclient.errors import HTTPError | ||
503 | 24 | |||
504 | 25 | |||
505 | 26 | ACCESS_TOKEN_POLL_TIME = 1 | ||
506 | 27 | WAITING_FOR_USER = """Open this link: | ||
507 | 28 | {{}} | ||
508 | 29 | to authorize this program to access Launchpad on your behalf. | ||
509 | 30 | Waiting to hear from Launchpad about your decision. . . .""" | ||
510 | 31 | |||
511 | 32 | |||
512 | 33 | class AuthorizeRequestTokenWithConsole(RequestTokenAuthorizationEngine): | ||
513 | 34 | """Authorize a token in a server environment (with no browser). | ||
514 | 35 | |||
515 | 36 | Print a link for the user to copy-and-paste into his/her browser | ||
516 | 37 | for authentication. | ||
517 | 38 | """ | ||
518 | 39 | |||
519 | 40 | def __init__(self, *args, **kwargs): | ||
520 | 41 | # as implemented in AuthorizeRequestTokenWithBrowser | ||
521 | 42 | kwargs['consumer_name'] = None | ||
522 | 43 | kwargs.pop('allow_access_levels', None) | ||
523 | 44 | super(AuthorizeRequestTokenWithConsole, self).__init__(*args, **kwargs) | ||
524 | 45 | |||
525 | 46 | def make_end_user_authorize_token(self, credentials, request_token): | ||
526 | 47 | """Ask the end-user to authorize the token in their browser. | ||
527 | 48 | |||
528 | 49 | """ | ||
529 | 50 | authorization_url = self.authorization_url(request_token) | ||
530 | 51 | print(WAITING_FOR_USER.format(authorization_url)) | ||
531 | 52 | # if we don't flush we may not see the message | ||
532 | 53 | sys.stdout.flush() | ||
533 | 54 | while credentials.access_token is None: | ||
534 | 55 | time.sleep(ACCESS_TOKEN_POLL_TIME) | ||
535 | 56 | try: | ||
536 | 57 | credentials.exchange_request_token_for_access_token( | ||
537 | 58 | self.web_root) | ||
538 | 59 | break | ||
539 | 60 | except HTTPError as e: | ||
540 | 61 | if e.response.status == 403: | ||
541 | 62 | # The user decided not to authorize this | ||
542 | 63 | # application. | ||
543 | 64 | raise e | ||
544 | 65 | elif e.response.status == 401: | ||
545 | 66 | # The user has not made a decision yet. | ||
546 | 67 | pass | ||
547 | 68 | else: | ||
548 | 69 | # There was an error accessing the server. | ||
549 | 70 | raise e | ||
550 | 71 | |||
551 | 72 | |||
552 | 73 | def get_launchpad(cred_path, launchpadlib_dir=None): | ||
553 | 74 | """ return a launchpad API class. In case launchpadlib_dir is | ||
554 | 75 | specified used that directory to store launchpadlib cache instead of | ||
555 | 76 | the default """ | ||
556 | 77 | store = UnencryptedFileCredentialStore(cred_path) | ||
557 | 78 | authorization_engine = AuthorizeRequestTokenWithConsole( | ||
558 | 79 | 'production', 'ci-jenkins-slave') | ||
559 | 80 | return Launchpad.login_with('ci-jenkins-slave', 'production', | ||
560 | 81 | credential_store=store, | ||
561 | 82 | authorization_engine=authorization_engine, | ||
562 | 83 | launchpadlib_dir=launchpadlib_dir) | ||
563 | 84 | |||
564 | 85 | if __name__ == '__main__': | ||
565 | 86 | get_launchpad("{credentials_path}") | ||
566 | diff --git a/jobs/infrastructure/credentials-1-launchpad.yaml b/jobs/infrastructure/credentials-1-launchpad.yaml | |||
567 | 0 | new file mode 100644 | 87 | new file mode 100644 |
568 | index 0000000..d1c4968 | |||
569 | --- /dev/null | |||
570 | +++ b/jobs/infrastructure/credentials-1-launchpad.yaml | |||
571 | @@ -0,0 +1,21 @@ | |||
572 | 1 | - job-template: | ||
573 | 2 | name: '{name}-credentials-1-launchpad' | ||
574 | 3 | project-type: matrix | ||
575 | 4 | description: This job creates or validates OAuth tokens to allow launchpadlib to work. | ||
576 | 5 | properties: | ||
577 | 6 | - build-discarder: | ||
578 | 7 | num-to-keep: 1 | ||
579 | 8 | - rebuild | ||
580 | 9 | parameters: | ||
581 | 10 | - matrix-combinations: | ||
582 | 11 | name: nodes | ||
583 | 12 | axes: | ||
584 | 13 | - axis: | ||
585 | 14 | type: slave | ||
586 | 15 | name: node | ||
587 | 16 | values: '{obj:build_slaves}' | ||
588 | 17 | wrappers: | ||
589 | 18 | - timestamps | ||
590 | 19 | builders: | ||
591 | 20 | - shell: | ||
592 | 21 | !include-raw: credentials-1-launchpad.py | ||
593 | diff --git a/jobs/infrastructure/credentials-2-launchpad-plugin.sh b/jobs/infrastructure/credentials-2-launchpad-plugin.sh | |||
594 | 0 | new file mode 100644 | 22 | new file mode 100644 |
595 | index 0000000..d47164f | |||
596 | --- /dev/null | |||
597 | +++ b/jobs/infrastructure/credentials-2-launchpad-plugin.sh | |||
598 | @@ -0,0 +1,108 @@ | |||
599 | 1 | #!/bin/bash -ex | ||
600 | 2 | # | ||
601 | 3 | # Copyright (C) 2016 Canonical Ltd | ||
602 | 4 | # | ||
603 | 5 | # This program is free software: you can redistribute it and/or modify | ||
604 | 6 | # it under the terms of the GNU General Public License version 3 as | ||
605 | 7 | # published by the Free Software Foundation. | ||
606 | 8 | # | ||
607 | 9 | # This program is distributed in the hope that it will be useful, | ||
608 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
609 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
610 | 12 | # GNU General Public License for more details. | ||
611 | 13 | # | ||
612 | 14 | # You should have received a copy of the GNU General Public License | ||
613 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
614 | 16 | |||
615 | 17 | # Apply the configuration file. | ||
616 | 18 | CONFIG_DIR="${{JENKINS_HOME}}/.jlp" | ||
617 | 19 | CONFIG_PATH="${{CONFIG_DIR}}/jlp.config" | ||
618 | 20 | |||
619 | 21 | if [ ! -d "${{CONFIG_DIR}}" ]; then | ||
620 | 22 | mkdir -p "${{CONFIG_DIR}}" | ||
621 | 23 | fi | ||
622 | 24 | |||
623 | 25 | cat > "${{CONFIG_PATH}}" << EOF | ||
624 | 26 | #You must explicitely allow users to trigger the jobs on your jenkins | ||
625 | 27 | #Otherwise anybody can run arbitrary code on your jenkins servers. | ||
626 | 28 | allowed_users: [{allowed_users}] | ||
627 | 29 | |||
628 | 30 | #path to your credentials file. The first time you run one of these scripts, | ||
629 | 31 | #launchpad will ask you to authenticate (via a provided URL). Once you do so | ||
630 | 32 | #(in launchpad) you won't need to do this again. | ||
631 | 33 | #If your jenkins "lives" in /var/lib/jenkins you probably don't need to change | ||
632 | 34 | #this | ||
633 | 35 | credential_store_path: {credentials_path} | ||
634 | 36 | |||
635 | 37 | # When doing a dput into ppa (in autoland.py) a new changelog entry is | ||
636 | 38 | # generated. DEBEMAIL and DEBFULLNAME are used to generate the entry correctly. | ||
637 | 39 | # Please note that the gpg keys of the user specified here must be available | ||
638 | 40 | # on the host where autoland.py is running | ||
639 | 41 | DEBEMAIL: | ||
640 | 42 | DEBFULLNAME: | ||
641 | 43 | |||
642 | 44 | #user and password for accessing jenkins. This is needed as we need to find | ||
643 | 45 | #out if a job is being published to public jenkins or not. The user needs to be | ||
644 | 46 | #able to see the job configuration | ||
645 | 47 | jenkins_user: "{bot_username}" | ||
646 | 48 | jenkins_password: "${{jenkins_api_token}}" | ||
647 | 49 | |||
648 | 50 | #Actual URL of your jenkins (e.g. the jenkins backend URL) | ||
649 | 51 | jenkins_url: "{backend_url}" | ||
650 | 52 | |||
651 | 53 | #Proxy URL of your jenkins (e.g. the URL accessed by users) | ||
652 | 54 | jenkins_proxy_url: "${{JENKINS_URL}}" | ||
653 | 55 | |||
654 | 56 | #Token to pass when triggering a jenkins build (leave blank for none) | ||
655 | 57 | jenkins_build_token: "BUILD_ME" | ||
656 | 58 | |||
657 | 59 | # console output from the following jobs will not be printed to the | ||
658 | 60 | # affected merge proposal (in the "Executed test runs:" section) | ||
659 | 61 | jobs_blacklisted_from_messages: | ||
660 | 62 | {blacklisted_jobs} | ||
661 | 63 | |||
662 | 64 | #message that is used for "testing in progress" comment | ||
663 | 65 | launchpad_build_in_progress_message: "Jenkins: testing in progress" | ||
664 | 66 | |||
665 | 67 | #login of the launchpad user you will be using for this plugin | ||
666 | 68 | #ideally this user is part of your project group | ||
667 | 69 | launchpad_login: {bot_username} | ||
668 | 70 | |||
669 | 71 | #Review type that is used for voting on merge proposals. | ||
670 | 72 | #Usually you don't need to change this | ||
671 | 73 | launchpad_review_type: continuous-integration | ||
672 | 74 | |||
673 | 75 | # directory containing lockfiles for Launchpad merge proposals | ||
674 | 76 | launchpadlocks_dir: /tmp/jenkins-launchpad-plugin/locks | ||
675 | 77 | |||
676 | 78 | #lock file that is being used to limit the number of parallel launchpad | ||
677 | 79 | #connections | ||
678 | 80 | lock_name: launchpad-trigger-lock | ||
679 | 81 | |||
680 | 82 | #you don't need to change this | ||
681 | 83 | lp_app: launchpad-trigger | ||
682 | 84 | |||
683 | 85 | #which launchpad are you using (production/staging) | ||
684 | 86 | #you don't need to change this | ||
685 | 87 | lp_env: production | ||
686 | 88 | |||
687 | 89 | #URL of your public jenkins in case you are publishing your jobs to some | ||
688 | 90 | #other jenkins | ||
689 | 91 | public_jenkins_url: | ||
690 | 92 | |||
691 | 93 | #in case you are running jenkins in a private infrastructure you probably don't | ||
692 | 94 | #want to expose your private IPs in public merge proposals | ||
693 | 95 | #the following defines (IP, replacement) pairs. Your URLs in merge proposals | ||
694 | 96 | #are then replaced by the replacement (and you can e.g. edit your /etc/hosts | ||
695 | 97 | #so the links still work for you). The form to specify a replacement is: | ||
696 | 98 | #urls_to_hide: | ||
697 | 99 | # - ['http://1.2.3.4:8080','http://jenkins:8080'] | ||
698 | 100 | # | ||
699 | 101 | #To specify no replacement: | ||
700 | 102 | #urls_to_hide: [] | ||
701 | 103 | urls_to_hide: [] | ||
702 | 104 | |||
703 | 105 | # verbosity of the commands | ||
704 | 106 | # one of: debug, info, warning, error, critical | ||
705 | 107 | log_level: debug | ||
706 | 108 | EOF | ||
707 | diff --git a/jobs/infrastructure/credentials-2-launchpad-plugin.yaml b/jobs/infrastructure/credentials-2-launchpad-plugin.yaml | |||
708 | 0 | new file mode 100644 | 109 | new file mode 100644 |
709 | index 0000000..0d8ca61 | |||
710 | --- /dev/null | |||
711 | +++ b/jobs/infrastructure/credentials-2-launchpad-plugin.yaml | |||
712 | @@ -0,0 +1,24 @@ | |||
713 | 1 | - job-template: | ||
714 | 2 | name: '{name}-credentials-2-launchpad-plugin' | ||
715 | 3 | project-type: matrix | ||
716 | 4 | description: This job configures Launchpad integration. | ||
717 | 5 | properties: | ||
718 | 6 | - build-discarder: | ||
719 | 7 | num-to-keep: 1 | ||
720 | 8 | - rebuild | ||
721 | 9 | parameters: | ||
722 | 10 | - matrix-combinations: | ||
723 | 11 | name: nodes | ||
724 | 12 | - password: | ||
725 | 13 | name: jenkins_api_token | ||
726 | 14 | description: Jenkins API key of the "{bot_username}" account | ||
727 | 15 | axes: | ||
728 | 16 | - axis: | ||
729 | 17 | type: slave | ||
730 | 18 | name: node | ||
731 | 19 | values: '{obj:build_slaves}' | ||
732 | 20 | wrappers: | ||
733 | 21 | - timestamps | ||
734 | 22 | builders: | ||
735 | 23 | - shell: | ||
736 | 24 | !include-raw: credentials-2-launchpad-plugin.sh | ||
737 | diff --git a/jobs/infrastructure/deploy-jenkins-jobs.sh b/jobs/infrastructure/deploy-jenkins-jobs.sh | |||
738 | 0 | new file mode 100644 | 25 | new file mode 100644 |
739 | index 0000000..7754fa9 | |||
740 | --- /dev/null | |||
741 | +++ b/jobs/infrastructure/deploy-jenkins-jobs.sh | |||
742 | @@ -0,0 +1,58 @@ | |||
743 | 1 | #!/bin/sh | ||
744 | 2 | # | ||
745 | 3 | # Copyright (C) 2017 Canonical Ltd | ||
746 | 4 | # | ||
747 | 5 | # This program is free software: you can redistribute it and/or modify | ||
748 | 6 | # it under the terms of the GNU General Public License version 3 as | ||
749 | 7 | # published by the Free Software Foundation. | ||
750 | 8 | # | ||
751 | 9 | # This program is distributed in the hope that it will be useful, | ||
752 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
753 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPO. See the | ||
754 | 12 | # GNU General Public License for more details. | ||
755 | 13 | # | ||
756 | 14 | # You should have received a copy of the GNU General Public License | ||
757 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
758 | 16 | |||
759 | 17 | set -ex | ||
760 | 18 | |||
761 | 19 | . "$WORKSPACE/.build_env" | ||
762 | 20 | |||
763 | 21 | JENKINS_JOBS_FOLDER="jenkins-jobs" | ||
764 | 22 | JENKINS_CONFIG_FOLDER="config" | ||
765 | 23 | |||
766 | 24 | # Fetch Jenkaas configuration from config file | ||
767 | 25 | CONFIG_DIR="$JENKINS_HOME/.jlp" | ||
768 | 26 | CONFIG_PATH="$CONFIG_DIR/jlp.config" | ||
769 | 27 | |||
770 | 28 | cat $CONFIG_PATH | ||
771 | 29 | |||
772 | 30 | JENKINS_API_TOKEN=$(cat $CONFIG_PATH | grep 'jenkins_password' | sed 's/[a-z_]*:[ ]//g') | ||
773 | 31 | JENKINS_DEPLOYMENT_SCOPE="jobs/infrastructure:jobs/snap" | ||
774 | 32 | |||
775 | 33 | rm -rf $WORKSPACE/$JENKINS_JOBS_FOLDER | ||
776 | 34 | |||
777 | 35 | # Clone the jenkins-jobs and jenkins-config repositories | ||
778 | 36 | git clone -b $JENKINS_JOBS_BRANCH $JENKINS_JOBS_REPO $JENKINS_JOBS_FOLDER | ||
779 | 37 | cd $JENKINS_JOBS_FOLDER | ||
780 | 38 | git clone -b $JENKINS_CONFIG_BRANCH $JENKINS_CONFIG_REPO $JENKINS_CONFIG_FOLDER | ||
781 | 39 | |||
782 | 40 | # Verify the configuration is present | ||
783 | 41 | PROJECT_CONF="$JENKINS_CONFIG_FOLDER/$JENKINS_INSTANCE.conf" | ||
784 | 42 | PROJECT_YAML="$JENKINS_CONFIG_FOLDER/$JENKINS_INSTANCE.yaml" | ||
785 | 43 | if [ ! -f "$PROJECT_CONF" ] || [ ! -f "$PROJECT_YAML" ]; then | ||
786 | 44 | echo "Cannot find .conf and .yaml pair for $JENKINS_INSTANCE" | ||
787 | 45 | exit 1 | ||
788 | 46 | fi | ||
789 | 47 | |||
790 | 48 | # Update the config file with the API key for the Bot. | ||
791 | 49 | sed -i "/<API TOKEN>/c\password=$JENKINS_API_TOKEN" "$PROJECT_CONF" | ||
792 | 50 | |||
793 | 51 | # Test requested configuration | ||
794 | 52 | /usr/local/bin/jenkins-jobs --conf $PROJECT_CONF test $PROJECT_YAML":"$JENKINS_DEPLOYMENT_SCOPE &>/dev/null | ||
795 | 53 | if [ "$?" -ne 0 ]; then | ||
796 | 54 | echo "Configuration failed verification" | ||
797 | 55 | exit 1 | ||
798 | 56 | fi | ||
799 | 57 | # Deploy requested configuration | ||
800 | 58 | /usr/local/bin/jenkins-jobs --conf $PROJECT_CONF update $PROJECT_YAML":"$JENKINS_DEPLOYMENT_SCOPE | ||
801 | diff --git a/jobs/infrastructure/deploy-jenkins-jobs.yaml b/jobs/infrastructure/deploy-jenkins-jobs.yaml | |||
802 | 0 | new file mode 100644 | 59 | new file mode 100644 |
803 | index 0000000..24b24f0 | |||
804 | --- /dev/null | |||
805 | +++ b/jobs/infrastructure/deploy-jenkins-jobs.yaml | |||
806 | @@ -0,0 +1,24 @@ | |||
807 | 1 | - job-template: | ||
808 | 2 | name: '{name}-deploy-jenkins-jobs' | ||
809 | 3 | project-type: matrix | ||
810 | 4 | description: | | ||
811 | 5 | This job will deploy jenkins jobs. | ||
812 | 6 | properties: | ||
813 | 7 | - build-discarder: | ||
814 | 8 | num-to-keep: 1 | ||
815 | 9 | - rebuild | ||
816 | 10 | parameters: | ||
817 | 11 | - matrix-combinations: | ||
818 | 12 | name: nodes | ||
819 | 13 | axes: | ||
820 | 14 | - axis: | ||
821 | 15 | type: slave | ||
822 | 16 | name: node | ||
823 | 17 | values: '{obj:build_slaves}' | ||
824 | 18 | wrappers: | ||
825 | 19 | - timestamps | ||
826 | 20 | builders: | ||
827 | 21 | - shell: | ||
828 | 22 | !include-raw: common-job-prepare.sh | ||
829 | 23 | - shell: | ||
830 | 24 | !include-raw-escape: deploy-jenkins-jobs.sh | ||
831 | diff --git a/jobs/infrastructure/infrastructure-jobs.yaml b/jobs/infrastructure/infrastructure-jobs.yaml | |||
832 | 0 | new file mode 100644 | 25 | new file mode 100644 |
833 | index 0000000..d440330 | |||
834 | --- /dev/null | |||
835 | +++ b/jobs/infrastructure/infrastructure-jobs.yaml | |||
836 | @@ -0,0 +1,8 @@ | |||
837 | 1 | - job-group: | ||
838 | 2 | name: infrastructure-jobs | ||
839 | 3 | jobs: | ||
840 | 4 | - '{name}-prepare-0-install' | ||
841 | 5 | - '{name}-credentials-0-ssh' | ||
842 | 6 | - '{name}-credentials-1-launchpad' | ||
843 | 7 | - '{name}-credentials-2-launchpad-plugin' | ||
844 | 8 | - '{name}-deploy-jenkins-jobs' | ||
845 | diff --git a/jobs/infrastructure/prepare-0-install.sh b/jobs/infrastructure/prepare-0-install.sh | |||
846 | 0 | new file mode 100644 | 9 | new file mode 100644 |
847 | index 0000000..9fae262 | |||
848 | --- /dev/null | |||
849 | +++ b/jobs/infrastructure/prepare-0-install.sh | |||
850 | @@ -0,0 +1,47 @@ | |||
851 | 1 | #!/bin/sh -ex | ||
852 | 2 | # | ||
853 | 3 | # Copyright (C) 2016 Canonical Ltd | ||
854 | 4 | # | ||
855 | 5 | # This program is free software: you can redistribute it and/or modify | ||
856 | 6 | # it under the terms of the GNU General Public License version 3 as | ||
857 | 7 | # published by the Free Software Foundation. | ||
858 | 8 | # | ||
859 | 9 | # This program is distributed in the hope that it will be useful, | ||
860 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
861 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
862 | 12 | # GNU General Public License for more details. | ||
863 | 13 | # | ||
864 | 14 | # You should have received a copy of the GNU General Public License | ||
865 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
866 | 16 | |||
867 | 17 | sudo apt-get install --yes software-properties-common | ||
868 | 18 | |||
869 | 19 | # build tools as used in Launchpad | ||
870 | 20 | sudo add-apt-repository --yes ppa:launchpad/buildd-staging | ||
871 | 21 | sudo add-apt-repository --yes ppa:jenkaas-hackers/tools | ||
872 | 22 | sudo add-apt-repository --yes ppa:snappy-hwe-team/ci-tools | ||
873 | 23 | |||
874 | 24 | sudo apt-get update | ||
875 | 25 | |||
876 | 26 | sudo apt-get install --yes \ | ||
877 | 27 | git \ | ||
878 | 28 | python \ | ||
879 | 29 | python-launchpadlib \ | ||
880 | 30 | python-bzrlib \ | ||
881 | 31 | python-lockfile \ | ||
882 | 32 | python-yaml \ | ||
883 | 33 | python-jenkins \ | ||
884 | 34 | python-git \ | ||
885 | 35 | python-pip \ | ||
886 | 36 | tarmac \ | ||
887 | 37 | jenkins-launchpad-plugin \ | ||
888 | 38 | openssh-client \ | ||
889 | 39 | debootstrap \ | ||
890 | 40 | qemu \ | ||
891 | 41 | binfmt-support \ | ||
892 | 42 | qemu-user-static \ | ||
893 | 43 | {install_packages} | ||
894 | 44 | |||
895 | 45 | # Install jenkins-jobs-builder | ||
896 | 46 | sudo pip install --upgrade pip | ||
897 | 47 | sudo pip install jenkins-job-builder | ||
898 | diff --git a/jobs/infrastructure/prepare-0-install.yaml b/jobs/infrastructure/prepare-0-install.yaml | |||
899 | 0 | new file mode 100644 | 48 | new file mode 100644 |
900 | index 0000000..32e9612 | |||
901 | --- /dev/null | |||
902 | +++ b/jobs/infrastructure/prepare-0-install.yaml | |||
903 | @@ -0,0 +1,27 @@ | |||
904 | 1 | - job-template: | ||
905 | 2 | name: '{name}-prepare-0-install' | ||
906 | 3 | install_packages: '' | ||
907 | 4 | project-type: matrix | ||
908 | 5 | description: | | ||
909 | 6 | This job adds all the needed repositories and installs dependencies needed | ||
910 | 7 | on build slaves. | ||
911 | 8 | properties: | ||
912 | 9 | - build-discarder: | ||
913 | 10 | num-to-keep: 10 | ||
914 | 11 | - rebuild | ||
915 | 12 | parameters: | ||
916 | 13 | - matrix-combinations: | ||
917 | 14 | name: configurations | ||
918 | 15 | description: Which slaves to install packages on | ||
919 | 16 | - string: | ||
920 | 17 | name: CLEANUP_WORKSPACE | ||
921 | 18 | default: "0" | ||
922 | 19 | axes: | ||
923 | 20 | - axis: | ||
924 | 21 | type: slave | ||
925 | 22 | name: node | ||
926 | 23 | values: '{obj:build_slaves}' | ||
927 | 24 | builders: | ||
928 | 25 | - shell: | ||
929 | 26 | !include-raw: | ||
930 | 27 | - prepare-0-install.sh | ||
931 | diff --git a/jobs/snap/common-job-prepare.sh b/jobs/snap/common-job-prepare.sh | |||
932 | 0 | new file mode 100644 | 28 | new file mode 100644 |
933 | index 0000000..4be0408 | |||
934 | --- /dev/null | |||
935 | +++ b/jobs/snap/common-job-prepare.sh | |||
936 | @@ -0,0 +1,24 @@ | |||
937 | 1 | JENKINS_JOBS_GIT_REPO="{jobs-git-repo}" | ||
938 | 2 | JENKINS_JOBS_GIT_REPO_BRANCH="{jobs-git-repo-branch}" | ||
939 | 3 | |||
940 | 4 | if [ -n "${{CLEANUP_WORKSPACE}}" ] && [ "${{CLEANUP_WORKSPACE}}" -eq 1 ]; then | ||
941 | 5 | rm -rf ${{WORKSPACE}}/* | ||
942 | 6 | fi | ||
943 | 7 | if [ -e jenkins-jobs ] ; then | ||
944 | 8 | (cd jenkins-jobs ; git clean -fdx . ; git fetch origin ; git reset --hard origin/${{JENKINS_JOBS_GIT_REPO_BRANCH}}) | ||
945 | 9 | else | ||
946 | 10 | git clone -b ${{JENKINS_JOBS_GIT_REPO_BRANCH}} ${{JENKINS_JOBS_GIT_REPO}} | ||
947 | 11 | fi | ||
948 | 12 | |||
949 | 13 | cat << EOF > $WORKSPACE/.build_env | ||
950 | 14 | BOT_USERNAME={bot_username} | ||
951 | 15 | LAUNCHPAD_PROJECT={launchpad_project} | ||
952 | 16 | LAUNCHPAD_TEAM={launchpad_team} | ||
953 | 17 | SNAP_BUILD_JOB={name}-snap-build | ||
954 | 18 | BUILD_SCRIPTS=$WORKSPACE/jenkins-jobs | ||
955 | 19 | BUILD_ON_LAUNCHPAD={build_on_launchpad} | ||
956 | 20 | AUTO_MERGE={auto_merge} | ||
957 | 21 | TRIGGER_CI={trigger_ci} | ||
958 | 22 | UPDATE_MPS={update_mps} | ||
959 | 23 | RUN_TESTS={run_tests} | ||
960 | 24 | EOF | ||
961 | diff --git a/jobs/snap/snap-automerger.sh b/jobs/snap/snap-automerger.sh | |||
962 | 0 | new file mode 100644 | 25 | new file mode 100644 |
963 | index 0000000..d955e52 | |||
964 | --- /dev/null | |||
965 | +++ b/jobs/snap/snap-automerger.sh | |||
966 | @@ -0,0 +1,27 @@ | |||
967 | 1 | #!/bin/bash | ||
968 | 2 | # | ||
969 | 3 | # Copyright (C) 2017 Canonical Ltd | ||
970 | 4 | # | ||
971 | 5 | # This program is free software: you can redistribute it and/or modify | ||
972 | 6 | # it under the terms of the GNU General Public License version 3 as | ||
973 | 7 | # published by the Free Software Foundation. | ||
974 | 8 | # | ||
975 | 9 | # This program is distributed in the hope that it will be useful, | ||
976 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
977 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
978 | 12 | # GNU General Public License for more details. | ||
979 | 13 | # | ||
980 | 14 | # You should have received a copy of the GNU General Public License | ||
981 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
982 | 16 | |||
983 | 17 | set -ex | ||
984 | 18 | |||
985 | 19 | . "$WORKSPACE/.build_env" | ||
986 | 20 | |||
987 | 21 | if [ "$AUTO_MERGE" = False ]; then | ||
988 | 22 | echo "WARNING: auto merge is disabled" | ||
989 | 23 | exit 0 | ||
990 | 24 | fi | ||
991 | 25 | |||
992 | 26 | exec $BUILD_SCRIPTS/tools/automerge-mps.py \ | ||
993 | 27 | -p $LAUNCHPAD_PROJECT | ||
994 | 0 | \ No newline at end of file | 28 | \ No newline at end of file |
995 | diff --git a/jobs/snap/snap-automerger.yaml b/jobs/snap/snap-automerger.yaml | |||
996 | 1 | new file mode 100644 | 29 | new file mode 100644 |
997 | index 0000000..f55f252 | |||
998 | --- /dev/null | |||
999 | +++ b/jobs/snap/snap-automerger.yaml | |||
1000 | @@ -0,0 +1,22 @@ | |||
1001 | 1 | - job-template: | ||
1002 | 2 | name: '{name}-snap-automerger' | ||
1003 | 3 | project-type: freestyle | ||
1004 | 4 | defaults: global | ||
1005 | 5 | description: "Monitor Launchpad for new merge proposals" | ||
1006 | 6 | display-name: "{name}-snap-automerger" | ||
1007 | 7 | concurrent: true | ||
1008 | 8 | node: snap && misc | ||
1009 | 9 | triggers: | ||
1010 | 10 | - timed: # every five minutes | ||
1011 | 11 | H/5 * * * * | ||
1012 | 12 | properties: | ||
1013 | 13 | - build-discarder: | ||
1014 | 14 | num-to-keep: 10 | ||
1015 | 15 | - rebuild | ||
1016 | 16 | builders: | ||
1017 | 17 | - shell: | ||
1018 | 18 | !include-raw: | ||
1019 | 19 | - common-job-prepare.sh | ||
1020 | 20 | - shell: | ||
1021 | 21 | !include-raw-escape: | ||
1022 | 22 | - snap-automerger.sh | ||
1023 | diff --git a/jobs/snap/snap-build-update-chroot.sh b/jobs/snap/snap-build-update-chroot.sh | |||
1024 | 0 | new file mode 100644 | 23 | new file mode 100644 |
1025 | index 0000000..912f687 | |||
1026 | --- /dev/null | |||
1027 | +++ b/jobs/snap/snap-build-update-chroot.sh | |||
1028 | @@ -0,0 +1,28 @@ | |||
1029 | 1 | #!/bin/sh | ||
1030 | 2 | # | ||
1031 | 3 | # Copyright (C) 2016 Canonical Ltd | ||
1032 | 4 | # | ||
1033 | 5 | # This program is free software: you can redistribute it and/or modify | ||
1034 | 6 | # it under the terms of the GNU General Public License version 3 as | ||
1035 | 7 | # published by the Free Software Foundation. | ||
1036 | 8 | # | ||
1037 | 9 | # This program is distributed in the hope that it will be useful, | ||
1038 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1039 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1040 | 12 | # GNU General Public License for more details. | ||
1041 | 13 | # | ||
1042 | 14 | # You should have received a copy of the GNU General Public License | ||
1043 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
1044 | 16 | |||
1045 | 17 | set -ex | ||
1046 | 18 | |||
1047 | 19 | . "$WORKSPACE/.build_env" | ||
1048 | 20 | |||
1049 | 21 | if [ "$BUILD_ON_LAUNCHPAD" != False ]; then | ||
1050 | 22 | exit 0 | ||
1051 | 23 | fi | ||
1052 | 24 | |||
1053 | 25 | sudo $BUILD_SCRIPTS/tools/snapbuild.sh \ | ||
1054 | 26 | --update-chroot \ | ||
1055 | 27 | --arch=$ARCHITECTURE \ | ||
1056 | 28 | --series=$SERIES | ||
1057 | 0 | \ No newline at end of file | 29 | \ No newline at end of file |
1058 | diff --git a/jobs/snap/snap-build-update-chroot.yaml b/jobs/snap/snap-build-update-chroot.yaml | |||
1059 | 1 | new file mode 100644 | 30 | new file mode 100644 |
1060 | index 0000000..e50b3c1 | |||
1061 | --- /dev/null | |||
1062 | +++ b/jobs/snap/snap-build-update-chroot.yaml | |||
1063 | @@ -0,0 +1,29 @@ | |||
1064 | 1 | - job-template: | ||
1065 | 2 | name: '{name}-snap-build-update-chroot' | ||
1066 | 3 | project-type: matrix | ||
1067 | 4 | defaults: global | ||
1068 | 5 | description: "Build a snap on launchpad" | ||
1069 | 6 | display-name: "{name}-snap-build-update-chroot" | ||
1070 | 7 | concurrent: false | ||
1071 | 8 | node: snap && build | ||
1072 | 9 | axes: | ||
1073 | 10 | - axis: | ||
1074 | 11 | type: slave | ||
1075 | 12 | name: node | ||
1076 | 13 | values: '{obj:build_slaves}' | ||
1077 | 14 | parameters: | ||
1078 | 15 | - string: | ||
1079 | 16 | name: ARCHITECTURE | ||
1080 | 17 | default: amd64 | ||
1081 | 18 | description: Architecture to build the snap for | ||
1082 | 19 | - string: | ||
1083 | 20 | name: SERIES | ||
1084 | 21 | default: xenial | ||
1085 | 22 | description: "Ubuntu archive series to build for" | ||
1086 | 23 | builders: | ||
1087 | 24 | - shell: | ||
1088 | 25 | !include-raw: | ||
1089 | 26 | - common-job-prepare.sh | ||
1090 | 27 | - shell: | ||
1091 | 28 | !include-raw-escape: | ||
1092 | 29 | - snap-build-update-chroot.sh | ||
1093 | diff --git a/jobs/snap/snap-build-worker.sh b/jobs/snap/snap-build-worker.sh | |||
1094 | 0 | new file mode 100644 | 30 | new file mode 100644 |
1095 | index 0000000..60775d7 | |||
1096 | --- /dev/null | |||
1097 | +++ b/jobs/snap/snap-build-worker.sh | |||
1098 | @@ -0,0 +1,147 @@ | |||
1099 | 1 | #!/bin/sh | ||
1100 | 2 | # | ||
1101 | 3 | # Copyright (C) 2016 Canonical Ltd | ||
1102 | 4 | # | ||
1103 | 5 | # This program is free software: you can redistribute it and/or modify | ||
1104 | 6 | # it under the terms of the GNU General Public License version 3 as | ||
1105 | 7 | # published by the Free Software Foundation. | ||
1106 | 8 | # | ||
1107 | 9 | # This program is distributed in the hope that it will be useful, | ||
1108 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1109 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1110 | 12 | # GNU General Public License for more details. | ||
1111 | 13 | # | ||
1112 | 14 | # You should have received a copy of the GNU General Public License | ||
1113 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
1114 | 16 | |||
1115 | 17 | set -ex | ||
1116 | 18 | |||
1117 | 19 | . "$WORKSPACE/.build_env" | ||
1118 | 20 | |||
1119 | 21 | rm -rf $WORKSPACE/src $WORKSPACE/results $WORKSPACE/build-props | ||
1120 | 22 | |||
1121 | 23 | git clone --no-checkout $TARGET_GIT_REPO $WORKSPACE/src | ||
1122 | 24 | cd $WORKSPACE/src | ||
1123 | 25 | for remote in `git branch -r | grep -v origin/master`; do git checkout --track $remote ; done | ||
1124 | 26 | |||
1125 | 27 | git checkout $TARGET_GIT_REPO_BRANCH | ||
1126 | 28 | |||
1127 | 29 | git config user.name "System Enablement CI Bot" | ||
1128 | 30 | git config user.email "ce-system-enablement@lists.canonical.com" | ||
1129 | 31 | |||
1130 | 32 | if [ -n "$SOURCE_GIT_REPO" ]; then | ||
1131 | 33 | git remote add other $SOURCE_GIT_REPO | ||
1132 | 34 | git fetch other | ||
1133 | 35 | git merge \ | ||
1134 | 36 | --no-ff \ | ||
1135 | 37 | -m "Merge remote tracking branch other/$SOURCE_GIT_REPO_BRANCH" \ | ||
1136 | 38 | $REVISION | ||
1137 | 39 | fi | ||
1138 | 40 | |||
1139 | 41 | # Try to find the correct branch we need to build from. In the case that | ||
1140 | 42 | # $TARGET_GIT_REPO_BRANCH points us to an upstream component branch we | ||
1141 | 43 | # will take master as the next suitable candidate. | ||
1142 | 44 | CI_BRANCH= | ||
1143 | 45 | SNAPCRAFT_YAML_PATH= | ||
1144 | 46 | for branch in $TARGET_GIT_REPO_BRANCH master ; do | ||
1145 | 47 | git checkout $branch | ||
1146 | 48 | if [ -e snapcraft.yaml ]; then | ||
1147 | 49 | SNAPCRAFT_YAML_PATH=snapcraft.yaml | ||
1148 | 50 | elif [ -e snap/snapcraft.yaml ]; then | ||
1149 | 51 | SNAPCRAFT_YAML_PATH=snap/snapcraft.yaml | ||
1150 | 52 | fi | ||
1151 | 53 | |||
1152 | 54 | if [ -n "$SNAPCRAFT_YAML_PATH" ]; then | ||
1153 | 55 | CI_BRANCH=$branch | ||
1154 | 56 | break | ||
1155 | 57 | fi | ||
1156 | 58 | done | ||
1157 | 59 | |||
1158 | 60 | if [ -z "$CI_BRANCH" ]; then | ||
1159 | 61 | echo "WARNING: Can't build snap as no snapcraft.yaml exists!" | ||
1160 | 62 | exit 0 | ||
1161 | 63 | fi | ||
1162 | 64 | |||
1163 | 65 | REPO_NAME=$(awk -v a="$TARGET_GIT_REPO" 'BEGIN{print substr(a, index(a, "+git/") + 5)}') | ||
1164 | 66 | # We rely on the snapcraft.yaml to have the snap name in the first five lines | ||
1165 | 67 | # which is the case for all our snaps. This is a bit lazy but the best way to | ||
1166 | 68 | # ensure we don't fetch any other name: fields which might be present in the file. | ||
1167 | 69 | SNAP_NAME=$(cat $SNAPCRAFT_YAML_PATH | grep -v ^\# | head -n 5 | grep "^name:" | awk '{print $2}') | ||
1168 | 70 | SNAP_REV=$(git rev-parse --short HEAD) | ||
1169 | 71 | CI_REPO=$REPO_NAME-$BUILD_ID-$SNAP_REV | ||
1170 | 72 | |||
1171 | 73 | sed -i "s/~$LAUNCHPAD_TEAM\/$LAUNCHPAD_PROJECT\/+git\/$REPO_NAME/~$LAUNCHPAD_TEAM\/$LAUNCHPAD_PROJECT\/+git\/$CI_REPO/g" \ | ||
1172 | 74 | $SNAPCRAFT_YAML_PATH | ||
1173 | 75 | |||
1174 | 76 | # The project as two different options of how snaps can be build: | ||
1175 | 77 | # | ||
1176 | 78 | # 1. Locally in a chroot but only for the host architecture | ||
1177 | 79 | # 2. On launchpad for any architecture | ||
1178 | 80 | # | ||
1179 | 81 | # Which of both options will be used is configured in the | ||
1180 | 82 | # $WORKSPACE/.build_env file. | ||
1181 | 83 | |||
1182 | 84 | if [ "$BUILD_ON_LAUNCHPAD" = False ]; then | ||
1183 | 85 | SNAPBUILD_EXTRA_ARGS= | ||
1184 | 86 | SNAP_TYPE=$($BUILD_SCRIPTS/tools/shyaml get-value type < $SNAPCRAFT_YAML_PATH || echo app) | ||
1185 | 87 | case "$SNAP_TYPE" in | ||
1186 | 88 | kernel) | ||
1187 | 89 | if [ "$ARCHITECTURE" != amd64 ]; then | ||
1188 | 90 | # If we're building a kernel snap we have to cross-build it | ||
1189 | 91 | # instead of building it in a native environment. | ||
1190 | 92 | SNAPBUILD_EXTRA_ARGS="--cross-build" | ||
1191 | 93 | fi | ||
1192 | 94 | ;; | ||
1193 | 95 | esac | ||
1194 | 96 | |||
1195 | 97 | sudo $BUILD_SCRIPTS/tools/snapbuild.sh \ | ||
1196 | 98 | --source-dir=$WORKSPACE/src \ | ||
1197 | 99 | --results-dir=$WORKSPACE/results \ | ||
1198 | 100 | --arch=$ARCHITECTURE \ | ||
1199 | 101 | --series=$SERIES \ | ||
1200 | 102 | --proxy=squid.internal:3128 \ | ||
1201 | 103 | $SNAPBUILD_EXTRA_ARGS | ||
1202 | 104 | else | ||
1203 | 105 | git remote add jenkins-ci git+ssh://$BOT_USERNAME@git.launchpad.net/~$LAUNCHPAD_TEAM/$LAUNCHPAD_PROJECT/+git/$CI_REPO | ||
1204 | 106 | git push jenkins-ci --all | ||
1205 | 107 | git push jenkins-ci --tags | ||
1206 | 108 | |||
1207 | 109 | # Save repo name as soon as it gets created so it can be deleted by the cleanup | ||
1208 | 110 | # job even if this job fails. | ||
1209 | 111 | echo "CI_REPO=$CI_REPO" >> $WORKSPACE/build-props | ||
1210 | 112 | echo "CI_BRANCH=$CI_BRANCH" >> $WORKSPACE/build-props | ||
1211 | 113 | |||
1212 | 114 | EXTRA_ARGS= | ||
1213 | 115 | if [ -n "$ARCHITECTURE" ]; then | ||
1214 | 116 | EXTRA_ARGS="$EXTRA_ARGS --architectures=$ARCHITECTURE" | ||
1215 | 117 | fi | ||
1216 | 118 | |||
1217 | 119 | $BUILD_SCRIPTS/tools/trigger-lp-build.py \ | ||
1218 | 120 | -s $SNAP_NAME -n \ | ||
1219 | 121 | --git-repo=https://git.launchpad.net/~$LAUNCHPAD_TEAM/$LAUNCHPAD_PROJECT/+git/$CI_REPO \ | ||
1220 | 122 | --git-repo-branch=$CI_BRANCH \ | ||
1221 | 123 | --results-dir=$WORKSPACE/results \ | ||
1222 | 124 | $EXTRA_ARGS | ||
1223 | 125 | fi | ||
1224 | 126 | |||
1225 | 127 | if [ -z "$REMOTE_WORKER" ]; then | ||
1226 | 128 | echo "INFO: No remote worker defined, not copying artifacts to it" | ||
1227 | 129 | exit 0 | ||
1228 | 130 | fi | ||
1229 | 131 | |||
1230 | 132 | SSH_PATH="${JENKINS_HOME}/.ssh/" | ||
1231 | 133 | SSH_KEY_PATH="${SSH_PATH}/git.launchpad.net/$BOT_USERNAME" | ||
1232 | 134 | SSH="ssh -i $SSH_KEY_PATH/id_rsa $REMOTE_USER@$REMOTE_WORKER" | ||
1233 | 135 | SCP="scp -i $SSH_KEY_PATH/id_rsa" | ||
1234 | 136 | |||
1235 | 137 | RESULTS_ID=$(md5sum $(find $WORKSPACE/results/*.snap | tail -n1) | cut -d' ' -f 1) | ||
1236 | 138 | REMOTE_RESULTS_BASE_DIR=/home/$REMOTE_USER/results | ||
1237 | 139 | |||
1238 | 140 | $SSH mkdir -p $REMOTE_RESULTS_BASE_DIR/$RESULTS_ID | ||
1239 | 141 | $SCP $WORKSPACE/results/*.snap $REMOTE_USER@$REMOTE_WORKER:$REMOTE_RESULTS_BASE_DIR/$RESULTS_ID/ | ||
1240 | 142 | |||
1241 | 143 | # Save the id of our results so it can be used by a subsequent build | ||
1242 | 144 | # in a properties file which is then being read from jenkins and its | ||
1243 | 145 | # content passed as parameters to triggered builds. | ||
1244 | 146 | echo "RESULTS_ID=$RESULTS_ID" >> $WORKSPACE/build-props | ||
1245 | 147 | cat $WORKSPACE/build-props | ||
1246 | diff --git a/jobs/snap/snap-build-worker.yaml b/jobs/snap/snap-build-worker.yaml | |||
1247 | 0 | new file mode 100644 | 148 | new file mode 100644 |
1248 | index 0000000..6b45a89 | |||
1249 | --- /dev/null | |||
1250 | +++ b/jobs/snap/snap-build-worker.yaml | |||
1251 | @@ -0,0 +1,72 @@ | |||
1252 | 1 | - job-template: | ||
1253 | 2 | name: '{name}-snap-build-worker' | ||
1254 | 3 | project-type: freestyle | ||
1255 | 4 | defaults: global | ||
1256 | 5 | description: "Build a snap on launchpad" | ||
1257 | 6 | display-name: "{name}-snap-build-worker" | ||
1258 | 7 | concurrent: true | ||
1259 | 8 | node: snap && build | ||
1260 | 9 | parameters: | ||
1261 | 10 | - string: | ||
1262 | 11 | name: ARCHITECTURE | ||
1263 | 12 | default: amd64 | ||
1264 | 13 | description: Architecture to build the snap for | ||
1265 | 14 | - string: | ||
1266 | 15 | name: TARGET_GIT_REPO | ||
1267 | 16 | default: | ||
1268 | 17 | description: "Target git repository" | ||
1269 | 18 | - string: | ||
1270 | 19 | name: TARGET_GIT_REPO_BRANCH | ||
1271 | 20 | default: master | ||
1272 | 21 | description: "Branch of the target git repository to build from" | ||
1273 | 22 | - string: | ||
1274 | 23 | name: SERIES | ||
1275 | 24 | default: xenial | ||
1276 | 25 | description: "Ubuntu archive series to build for" | ||
1277 | 26 | - string: | ||
1278 | 27 | name: FORCE | ||
1279 | 28 | default: "0" | ||
1280 | 29 | description: "Set to 1 to force the build" | ||
1281 | 30 | - string: | ||
1282 | 31 | name: SOURCE_GIT_REPO | ||
1283 | 32 | default: | ||
1284 | 33 | description: "Source git repository" | ||
1285 | 34 | - string: | ||
1286 | 35 | name: SOURCE_GIT_REPO_BRANCH | ||
1287 | 36 | default: | ||
1288 | 37 | description: "Branch of the source git repository to use" | ||
1289 | 38 | - string: | ||
1290 | 39 | name: MERGE_PROPOSAL | ||
1291 | 40 | default: | ||
1292 | 41 | description: "Link to the merge proposal this build relates to" | ||
1293 | 42 | - string: | ||
1294 | 43 | name: REVISION | ||
1295 | 44 | default: | ||
1296 | 45 | description: "Cleanup the whole workspace" | ||
1297 | 46 | - string: | ||
1298 | 47 | name: CLEANUP_WORKSPACE | ||
1299 | 48 | default: "0" | ||
1300 | 49 | description: "Cleanup the whole workspace" | ||
1301 | 50 | - string: | ||
1302 | 51 | name: REMOTE_WORKER | ||
1303 | 52 | default: "{obj:remote_worker}" | ||
1304 | 53 | description: "The remote server to execute the spread jobs on. There's no need to change from the default value unless you know what you're doing." | ||
1305 | 54 | - string: | ||
1306 | 55 | name: REMOTE_USER | ||
1307 | 56 | default: "{obj:remote_user}" | ||
1308 | 57 | description: "The remote server username used to ssh to $REMOTE_WORKER." | ||
1309 | 58 | - string: | ||
1310 | 59 | name: CORE_CHANNEL | ||
1311 | 60 | default: stable | ||
1312 | 61 | description: "Channel of the core snap to use for testing the build snap" | ||
1313 | 62 | - string: | ||
1314 | 63 | name: RESULTS_ID | ||
1315 | 64 | default: '' | ||
1316 | 65 | description: "Alphanumeric identifier used to pass build artifacts through different jobs" | ||
1317 | 66 | builders: | ||
1318 | 67 | - shell: | ||
1319 | 68 | !include-raw: | ||
1320 | 69 | - common-job-prepare.sh | ||
1321 | 70 | - shell: | ||
1322 | 71 | !include-raw-escape: | ||
1323 | 72 | - snap-build-worker.sh | ||
1324 | diff --git a/jobs/snap/snap-build.yaml b/jobs/snap/snap-build.yaml | |||
1325 | 0 | new file mode 100644 | 73 | new file mode 100644 |
1326 | index 0000000..dec3b19 | |||
1327 | --- /dev/null | |||
1328 | +++ b/jobs/snap/snap-build.yaml | |||
1329 | @@ -0,0 +1,91 @@ | |||
1330 | 1 | - job-template: | ||
1331 | 2 | name: '{name}-snap-build' | ||
1332 | 3 | project-type: matrix | ||
1333 | 4 | defaults: global | ||
1334 | 5 | description: "Build a snap with subsequent test execution" | ||
1335 | 6 | display-name: "{name}-snap-build" | ||
1336 | 7 | concurrent: true | ||
1337 | 8 | sequential: false | ||
1338 | 9 | node: monitor | ||
1339 | 10 | axes: | ||
1340 | 11 | - axis: | ||
1341 | 12 | type: user-defined | ||
1342 | 13 | name: ARCHITECTURE | ||
1343 | 14 | values: '{obj:build_architectures}' | ||
1344 | 15 | parameters: | ||
1345 | 16 | - string: | ||
1346 | 17 | name: TARGET_GIT_REPO | ||
1347 | 18 | default: | ||
1348 | 19 | description: "Target git repository" | ||
1349 | 20 | - string: | ||
1350 | 21 | name: TARGET_GIT_REPO_BRANCH | ||
1351 | 22 | default: master | ||
1352 | 23 | description: "Branch of the target git repository to build from" | ||
1353 | 24 | - string: | ||
1354 | 25 | name: SERIES | ||
1355 | 26 | default: xenial | ||
1356 | 27 | description: "Ubuntu archive series to build for" | ||
1357 | 28 | - string: | ||
1358 | 29 | name: FORCE | ||
1359 | 30 | default: "0" | ||
1360 | 31 | description: "Set to 1 to force the build" | ||
1361 | 32 | - string: | ||
1362 | 33 | name: SOURCE_GIT_REPO | ||
1363 | 34 | default: | ||
1364 | 35 | description: "Source git repository" | ||
1365 | 36 | - string: | ||
1366 | 37 | name: SOURCE_GIT_REPO_BRANCH | ||
1367 | 38 | default: | ||
1368 | 39 | description: "Branch of the source git repository to use" | ||
1369 | 40 | - string: | ||
1370 | 41 | name: MERGE_PROPOSAL | ||
1371 | 42 | default: | ||
1372 | 43 | description: "Link to the merge proposal this build relates to" | ||
1373 | 44 | - string: | ||
1374 | 45 | name: REVISION | ||
1375 | 46 | default: | ||
1376 | 47 | description: "Cleanup the whole workspace" | ||
1377 | 48 | - string: | ||
1378 | 49 | name: CLEANUP_WORKSPACE | ||
1379 | 50 | default: "0" | ||
1380 | 51 | description: "Cleanup the whole workspace" | ||
1381 | 52 | builders: | ||
1382 | 53 | - trigger-builds: | ||
1383 | 54 | - project: '{name}-snap-build-worker' | ||
1384 | 55 | current-parameters: true | ||
1385 | 56 | predefined-parameters: | | ||
1386 | 57 | RESULTS_ID=$BUILD_TAG | ||
1387 | 58 | ARCHITECTURE=$ARCHITECTURE | ||
1388 | 59 | block: true | ||
1389 | 60 | - project: '{name}-snap-test' | ||
1390 | 61 | current-parameters: true | ||
1391 | 62 | predefined-parameters: | | ||
1392 | 63 | RESULTS_ID=$BUILD_TAG | ||
1393 | 64 | block: true | ||
1394 | 65 | - project: '{name}-snap-cleanup' | ||
1395 | 66 | current-parameters: true | ||
1396 | 67 | predefined-parameters: | | ||
1397 | 68 | RESULTS_ID=$BUILD_TAG | ||
1398 | 69 | publishers: | ||
1399 | 70 | - archive: | ||
1400 | 71 | artifacts: '**/*.snap' | ||
1401 | 72 | latest-only: false | ||
1402 | 73 | allow-empty: true | ||
1403 | 74 | fingerprint: false | ||
1404 | 75 | - trigger-parameterized-builds: | ||
1405 | 76 | - project: '{name}-snap-update-mp' | ||
1406 | 77 | condition: "SUCCESS" | ||
1407 | 78 | predefined-parameters: | | ||
1408 | 79 | CI_RESULT=PASSED | ||
1409 | 80 | CI_BUILD=${{BUILD_URL}} | ||
1410 | 81 | CI_BRANCH="${{SOURCE_GIT_REPO_BRANCH}}@${{SOURCE_GIT_REPO}}" | ||
1411 | 82 | CI_MERGE_PROPOSAL=${{MERGE_PROPOSAL}} | ||
1412 | 83 | CI_REVISION=${{REVISION}} | ||
1413 | 84 | - project: '{name}-snap-update-mp' | ||
1414 | 85 | condition: "UNSTABLE_OR_WORSE" | ||
1415 | 86 | predefined-parameters: | | ||
1416 | 87 | CI_RESULT=FAILED | ||
1417 | 88 | CI_BUILD=${{BUILD_URL}} | ||
1418 | 89 | CI_BRANCH="${{SOURCE_GIT_REPO_BRANCH}}@${{SOURCE_GIT_REPO}}" | ||
1419 | 90 | CI_MERGE_PROPOSAL=${{MERGE_PROPOSAL}} | ||
1420 | 91 | CI_REVISION=${{REVISION}} | ||
1421 | diff --git a/jobs/snap/snap-cleanup.sh b/jobs/snap/snap-cleanup.sh | |||
1422 | 0 | new file mode 100644 | 92 | new file mode 100644 |
1423 | index 0000000..bf0e03c | |||
1424 | --- /dev/null | |||
1425 | +++ b/jobs/snap/snap-cleanup.sh | |||
1426 | @@ -0,0 +1,38 @@ | |||
1427 | 1 | #!/bin/sh | ||
1428 | 2 | # | ||
1429 | 3 | # Copyright (C) 2017 Canonical Ltd | ||
1430 | 4 | # | ||
1431 | 5 | # This program is free software: you can redistribute it and/or modify | ||
1432 | 6 | # it under the terms of the GNU General Public License version 3 as | ||
1433 | 7 | # published by the Free Software Foundation. | ||
1434 | 8 | # | ||
1435 | 9 | # This program is distributed in the hope that it will be useful, | ||
1436 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1437 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1438 | 12 | # GNU General Public License for more details. | ||
1439 | 13 | # | ||
1440 | 14 | # You should have received a copy of the GNU General Public License | ||
1441 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
1442 | 16 | |||
1443 | 17 | set -x | ||
1444 | 18 | |||
1445 | 19 | . "$WORKSPACE/.build_env" | ||
1446 | 20 | |||
1447 | 21 | # Delete auxiliary repo used in the build | ||
1448 | 22 | $BUILD_SCRIPTS/tools/delete-ci-repo.py \ | ||
1449 | 23 | --git-repo=https://git.launchpad.net/~$LAUNCHPAD_TEAM/$LAUNCHPAD_PROJECT/+git/$CI_REPO | ||
1450 | 24 | |||
1451 | 25 | if [ -z "$REMOTE_WORKER" ]; then | ||
1452 | 26 | echo "INFO: No remote system defined" | ||
1453 | 27 | exit 0 | ||
1454 | 28 | fi | ||
1455 | 29 | |||
1456 | 30 | SSH_PATH="${JENKINS_HOME}/.ssh/" | ||
1457 | 31 | SSH_KEY_PATH="${SSH_PATH}/git.launchpad.net/$BOT_USERNAME" | ||
1458 | 32 | SSH="ssh -i $SSH_KEY_PATH/id_rsa $REMOTE_USER@$REMOTE_WORKER" | ||
1459 | 33 | REMOTE_RESULTS_BASE_DIR=/home/$REMOTE_USER/results | ||
1460 | 34 | |||
1461 | 35 | $SSH rm -rf $REMOTE_RESULTS_BASE_DIR/$RESULTS_ID | ||
1462 | 36 | |||
1463 | 37 | # Now remove any container that might have been left behind... | ||
1464 | 38 | $SSH sudo docker rm \$\(sudo docker ps -q --filter=status=exited --filter=ancestor=snap-spread-tests\) || true | ||
1465 | diff --git a/jobs/snap/snap-cleanup.yaml b/jobs/snap/snap-cleanup.yaml | |||
1466 | 0 | new file mode 100644 | 39 | new file mode 100644 |
1467 | index 0000000..8aa4a03 | |||
1468 | --- /dev/null | |||
1469 | +++ b/jobs/snap/snap-cleanup.yaml | |||
1470 | @@ -0,0 +1,32 @@ | |||
1471 | 1 | - job-template: | ||
1472 | 2 | name: '{name}-snap-cleanup' | ||
1473 | 3 | project-type: freestyle | ||
1474 | 4 | defaults: global | ||
1475 | 5 | description: "Cleanup artifacts left over from a snap build" | ||
1476 | 6 | display-name: "{name}-snap-cleanup" | ||
1477 | 7 | concurrent: true | ||
1478 | 8 | node: snap && build | ||
1479 | 9 | parameters: | ||
1480 | 10 | - string: | ||
1481 | 11 | name: CI_REPO | ||
1482 | 12 | default: "" | ||
1483 | 13 | description: "Auxiliary repo for the build, that we will remove" | ||
1484 | 14 | - string: | ||
1485 | 15 | name: "RESULTS_ID" | ||
1486 | 16 | default: "" | ||
1487 | 17 | description: "Alphanumeric Id of the results being staged on the remote worker" | ||
1488 | 18 | - string: | ||
1489 | 19 | name: REMOTE_WORKER | ||
1490 | 20 | default: "{obj:remote_worker}" | ||
1491 | 21 | description: "The remote server to execute the spread jobs on. There's no need to change from the default value unless you know what you're doing." | ||
1492 | 22 | - string: | ||
1493 | 23 | name: REMOTE_USER | ||
1494 | 24 | default: "{obj:remote_user}" | ||
1495 | 25 | description: "The remote server username used to ssh to $REMOTE_WORKER." | ||
1496 | 26 | builders: | ||
1497 | 27 | - shell: | ||
1498 | 28 | !include-raw: | ||
1499 | 29 | - common-job-prepare.sh | ||
1500 | 30 | - shell: | ||
1501 | 31 | !include-raw-escape: | ||
1502 | 32 | - snap-cleanup.sh | ||
1503 | diff --git a/jobs/snap/snap-nightly.yaml b/jobs/snap/snap-nightly.yaml | |||
1504 | 0 | new file mode 100644 | 33 | new file mode 100644 |
1505 | index 0000000..ae5a71f | |||
1506 | --- /dev/null | |||
1507 | +++ b/jobs/snap/snap-nightly.yaml | |||
1508 | @@ -0,0 +1,41 @@ | |||
1509 | 1 | - job-template: | ||
1510 | 2 | name: '{name}-snap-nightly' | ||
1511 | 3 | project-type: matrix | ||
1512 | 4 | concurrent: false | ||
1513 | 5 | node: monitor | ||
1514 | 6 | sequential: true | ||
1515 | 7 | properties: | ||
1516 | 8 | - build-discarder: | ||
1517 | 9 | days-to-keep: 30 | ||
1518 | 10 | - rebuild: | ||
1519 | 11 | rebuild-disabled: true | ||
1520 | 12 | axes: | ||
1521 | 13 | - axis: | ||
1522 | 14 | type: user-defined | ||
1523 | 15 | name: CORE_CHANNEL | ||
1524 | 16 | values: '{obj:nightly_core_channels}' | ||
1525 | 17 | - axis: | ||
1526 | 18 | type: user-defined | ||
1527 | 19 | name: SNAP | ||
1528 | 20 | values: '{obj:nightly_snaps}' | ||
1529 | 21 | - axis: | ||
1530 | 22 | type: user-defined | ||
1531 | 23 | name: ARCHITECTURE | ||
1532 | 24 | values: '{obj:nightly_architectures}' | ||
1533 | 25 | wrappers: | ||
1534 | 26 | - timeout: | ||
1535 | 27 | timeout: 30 | ||
1536 | 28 | abort: true | ||
1537 | 29 | - timestamps | ||
1538 | 30 | builders: | ||
1539 | 31 | - trigger-builds: | ||
1540 | 32 | - project: '{name}-snap-build-worker' | ||
1541 | 33 | predefined-parameters: | | ||
1542 | 34 | TARGET_GIT_REPO={base_snap_repo_url}/$SNAP | ||
1543 | 35 | TARGET_GIT_REPO_BRANCH=master | ||
1544 | 36 | SOURCE_GIT_REPO= | ||
1545 | 37 | SOURCE_GIT_REPO_BRANCH= | ||
1546 | 38 | REVISION= | ||
1547 | 39 | ARCHITECTURES=$ARCHITECTURE | ||
1548 | 40 | CORE_CHANNEL=$CORE_CHANNEL | ||
1549 | 41 | block: true | ||
1550 | diff --git a/jobs/snap/snap-project-jobs.yaml b/jobs/snap/snap-project-jobs.yaml | |||
1551 | 0 | new file mode 100644 | 42 | new file mode 100644 |
1552 | index 0000000..b693af3 | |||
1553 | --- /dev/null | |||
1554 | +++ b/jobs/snap/snap-project-jobs.yaml | |||
1555 | @@ -0,0 +1,13 @@ | |||
1556 | 1 | - job-group: | ||
1557 | 2 | name: snap-project-jobs | ||
1558 | 3 | jobs: | ||
1559 | 4 | - '{name}-snap-nightly' | ||
1560 | 5 | - '{name}-snap-build-worker' | ||
1561 | 6 | - '{name}-snap-build-update-chroot' | ||
1562 | 7 | - '{name}-snap-build' | ||
1563 | 8 | - '{name}-snap-cleanup' | ||
1564 | 9 | - '{name}-snap-release' | ||
1565 | 10 | - '{name}-snap-test' | ||
1566 | 11 | - '{name}-snap-trigger-ci' | ||
1567 | 12 | - '{name}-snap-update-mp' | ||
1568 | 13 | - '{name}-snap-automerger' | ||
1569 | diff --git a/jobs/snap/snap-release.sh b/jobs/snap/snap-release.sh | |||
1570 | 0 | new file mode 100644 | 14 | new file mode 100644 |
1571 | index 0000000..06fc6c4 | |||
1572 | --- /dev/null | |||
1573 | +++ b/jobs/snap/snap-release.sh | |||
1574 | @@ -0,0 +1,170 @@ | |||
1575 | 1 | #!/bin/bash | ||
1576 | 2 | # | ||
1577 | 3 | # Copyright (C) 2017 Canonical Ltd | ||
1578 | 4 | # | ||
1579 | 5 | # This program is free software: you can redistribute it and/or modify | ||
1580 | 6 | # it under the terms of the GNU General Public License version 3 as | ||
1581 | 7 | # published by the Free Software Foundation. | ||
1582 | 8 | # | ||
1583 | 9 | # This program is distributed in the hope that it will be useful, | ||
1584 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1585 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1586 | 12 | # GNU General Public License for more details. | ||
1587 | 13 | # | ||
1588 | 14 | # You should have received a copy of the GNU General Public License | ||
1589 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
1590 | 16 | |||
1591 | 17 | set -ex | ||
1592 | 18 | |||
1593 | 19 | . "$WORKSPACE/.build_env" | ||
1594 | 20 | |||
1595 | 21 | if [ -z "$VERSION" ]; then | ||
1596 | 22 | echo "ERROR: No version specified" | ||
1597 | 23 | exit 1 | ||
1598 | 24 | fi | ||
1599 | 25 | |||
1600 | 26 | echo "Snap to be released: $SNAP_NAME" | ||
1601 | 27 | echo "Version to be released: $VERSION" | ||
1602 | 28 | echo "New development version: $NEXT_VERSION" | ||
1603 | 29 | |||
1604 | 30 | REPOSITORY_URL="git+ssh://$BOT_USERNAME@git.launchpad.net/~$LAUNCHPAD_TEAM/$LAUNCHPAD_PROJECT/+git/$SNAP_NAME" | ||
1605 | 31 | |||
1606 | 32 | if [ -e "$SNAP_NAME" ]; then | ||
1607 | 33 | rm -rf "$SNAP_NAME" | ||
1608 | 34 | fi | ||
1609 | 35 | |||
1610 | 36 | set_git_identity() { | ||
1611 | 37 | git config user.name "System Enablement CI Bot" | ||
1612 | 38 | git config user.email "ce-system-enablement@lists.canonical.com" | ||
1613 | 39 | } | ||
1614 | 40 | |||
1615 | 41 | # Arguments are: | ||
1616 | 42 | # $1 Snap name | ||
1617 | 43 | # $2 Version to release | ||
1618 | 44 | update_changelog() { | ||
1619 | 45 | local latest_version commits i text range | ||
1620 | 46 | local snap=$1 | ||
1621 | 47 | local ver=$2 | ||
1622 | 48 | local changelog_file=ChangeLog | ||
1623 | 49 | local changes author full_text | ||
1624 | 50 | |||
1625 | 51 | # latest tag is latest version | ||
1626 | 52 | latest_version="$(git describe --abbrev=0)" || true | ||
1627 | 53 | if [ -n "$latest_version" ]; then | ||
1628 | 54 | range=$latest_version..HEAD | ||
1629 | 55 | else | ||
1630 | 56 | range=HEAD | ||
1631 | 57 | fi | ||
1632 | 58 | |||
1633 | 59 | commits=$(git rev-list --merges --reverse "$range") | ||
1634 | 60 | declare -A changes | ||
1635 | 61 | |||
1636 | 62 | for i in $commits; do | ||
1637 | 63 | local body merge_proposal description text | ||
1638 | 64 | |||
1639 | 65 | body=$(git log --format=%B -n1 "$i") | ||
1640 | 66 | merge_proposal=$(echo "$body" | grep "^Merge-Proposal:") || true | ||
1641 | 67 | author=$(echo "$body" | grep "^Author:") || true | ||
1642 | 68 | author="${author#Author: *}" | ||
1643 | 69 | if [ -z "$author" ]; then | ||
1644 | 70 | if [ -z "$merge_proposal" ]; then | ||
1645 | 71 | author="unknown" | ||
1646 | 72 | else | ||
1647 | 73 | author=${merge_proposal#*~} | ||
1648 | 74 | author=${author%%/*} | ||
1649 | 75 | fi | ||
1650 | 76 | fi | ||
1651 | 77 | # 'sed' removes leading blank lines first, then adds indentation | ||
1652 | 78 | description=$(echo "$body" | grep -v "^Author:\|^Merge" | \ | ||
1653 | 79 | sed '/./,$!d' | sed '2,$s/^/ /') || true | ||
1654 | 80 | if [ -z "$description" ]; then | ||
1655 | 81 | description="See more information in merge proposal" | ||
1656 | 82 | fi | ||
1657 | 83 | text=${changes[$author]} | ||
1658 | 84 | printf -v text "%s\n * %s\n %s" \ | ||
1659 | 85 | "$text" "$description" "$merge_proposal" | ||
1660 | 86 | changes[$author]=$text | ||
1661 | 87 | done | ||
1662 | 88 | |||
1663 | 89 | printf -v full_text "%s\n" "$(date --rfc-3339=date --utc) $snap $ver" | ||
1664 | 90 | for author in "${!changes[@]}"; do | ||
1665 | 91 | printf -v full_text "%s\n [ %s ]%s\n" \ | ||
1666 | 92 | "$full_text" "$author" "${changes[$author]}" | ||
1667 | 93 | done | ||
1668 | 94 | |||
1669 | 95 | if [ ! -e "$changelog_file" ]; then | ||
1670 | 96 | touch "$changelog_file" | ||
1671 | 97 | fi | ||
1672 | 98 | echo "$full_text" | cat - "$changelog_file" > "$changelog_file".tmp | ||
1673 | 99 | mv "$changelog_file".tmp "$changelog_file" | ||
1674 | 100 | |||
1675 | 101 | git add "$changelog_file" | ||
1676 | 102 | git commit -m "Update $changelog_file for $ver" | ||
1677 | 103 | } | ||
1678 | 104 | |||
1679 | 105 | # Arguments are: | ||
1680 | 106 | # $1 Version to be set in the snapcraft.yaml file | ||
1681 | 107 | # $2 Path to the snapcraft.yaml file | ||
1682 | 108 | bump_version_and_tag() { | ||
1683 | 109 | sed -i -e "s/^version:\ .*/version: $1/g" "$2" | ||
1684 | 110 | git add "$2" | ||
1685 | 111 | git commit -m "Bump version to $1" | ||
1686 | 112 | git tag -a -m "$1" "$1" HEAD | ||
1687 | 113 | } | ||
1688 | 114 | |||
1689 | 115 | RELEASE_BASE_BRANCH=master | ||
1690 | 116 | if [ "$RELEASE_FROM_STABLE" -eq 1 ]; then | ||
1691 | 117 | RELEASE_BASE_BRANCH=stable | ||
1692 | 118 | fi | ||
1693 | 119 | |||
1694 | 120 | git clone -b "$RELEASE_BASE_BRANCH" "$REPOSITORY_URL" "$SNAP_NAME" | ||
1695 | 121 | cd "$SNAP_NAME" | ||
1696 | 122 | |||
1697 | 123 | SNAPCRAFT_YAML_PATH= | ||
1698 | 124 | if [ -e snapcraft.yaml ]; then | ||
1699 | 125 | SNAPCRAFT_YAML_PATH=snapcraft.yaml | ||
1700 | 126 | elif [ -e snap/snapcraft.yaml ]; then | ||
1701 | 127 | SNAPCRAFT_YAML_PATH=snap/snapcraft.yaml | ||
1702 | 128 | fi | ||
1703 | 129 | |||
1704 | 130 | if [ -z "$SNAPCRAFT_YAML_PATH" ]; then | ||
1705 | 131 | echo "ERROR: No snapcraft.yaml or snap/snapcraft.yaml file!" | ||
1706 | 132 | exit 1 | ||
1707 | 133 | fi | ||
1708 | 134 | |||
1709 | 135 | set_git_identity | ||
1710 | 136 | update_changelog "$SNAP_NAME" "$VERSION" | ||
1711 | 137 | bump_version_and_tag "$VERSION" "$SNAPCRAFT_YAML_PATH" | ||
1712 | 138 | |||
1713 | 139 | if [ "$RELEASE_FROM_STABLE" -eq 1 ]; then | ||
1714 | 140 | git push origin "$RELEASE_BASE_BRANCH" | ||
1715 | 141 | git push origin "$VERSION" | ||
1716 | 142 | |||
1717 | 143 | "$BUILD_SCRIPTS"/tools/trigger-lp-build.py -s "$SNAP_NAME" -p | ||
1718 | 144 | else | ||
1719 | 145 | if ! git branch -r | grep origin/stable ; then | ||
1720 | 146 | git checkout -b stable origin/master | ||
1721 | 147 | else | ||
1722 | 148 | git checkout -b stable origin/stable | ||
1723 | 149 | fi | ||
1724 | 150 | |||
1725 | 151 | # We're using `-X theirs` here as master always takes priority over | ||
1726 | 152 | # what is in the stable. If something was only submitted into stable | ||
1727 | 153 | # the commiter needs to take care that the same change is submitted | ||
1728 | 154 | # to master too or it is overriden the next time a release happens | ||
1729 | 155 | # from master. | ||
1730 | 156 | git merge --no-ff -X theirs "$RELEASE_BASE_BRANCH" | ||
1731 | 157 | |||
1732 | 158 | git push origin stable | ||
1733 | 159 | git push origin "$RELEASE_BASE_BRANCH" | ||
1734 | 160 | git push origin "$VERSION" | ||
1735 | 161 | |||
1736 | 162 | # Build before we change master branch | ||
1737 | 163 | "$BUILD_SCRIPTS"/tools/trigger-lp-build.py -s "$SNAP_NAME" -p | ||
1738 | 164 | |||
1739 | 165 | git checkout "$RELEASE_BASE_BRANCH" | ||
1740 | 166 | sed -i -e "s/^version:\ .*/version: ${NEXT_VERSION}-dev/g" "$SNAPCRAFT_YAML_PATH" | ||
1741 | 167 | git add "$SNAPCRAFT_YAML_PATH" | ||
1742 | 168 | git commit -m "Open development for ${NEXT_VERSION}-dev" | ||
1743 | 169 | git push origin "$RELEASE_BASE_BRANCH" | ||
1744 | 170 | fi | ||
1745 | diff --git a/jobs/snap/snap-release.yaml b/jobs/snap/snap-release.yaml | |||
1746 | 0 | new file mode 100644 | 171 | new file mode 100644 |
1747 | index 0000000..75e3b3a | |||
1748 | --- /dev/null | |||
1749 | +++ b/jobs/snap/snap-release.yaml | |||
1750 | @@ -0,0 +1,58 @@ | |||
1751 | 1 | - job-template: | ||
1752 | 2 | name: '{name}-snap-release' | ||
1753 | 3 | project-type: freestyle | ||
1754 | 4 | defaults: global | ||
1755 | 5 | description: "A job implementing the release process used for snaps" | ||
1756 | 6 | display-name: "{name}-snap-release" | ||
1757 | 7 | concurrent: true | ||
1758 | 8 | node: snap && release | ||
1759 | 9 | parameters: | ||
1760 | 10 | - string: | ||
1761 | 11 | name: SNAP_NAME | ||
1762 | 12 | default: "" | ||
1763 | 13 | description: | | ||
1764 | 14 | Name of the snap which should be released | ||
1765 | 15 | |||
1766 | 16 | Normally the repositories we have on https://code.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/ | ||
1767 | 17 | match with the snap name. In some cases this is not true, in those you have to set the repository | ||
1768 | 18 | name here. For example for 'canonical-se-engineering-tests' it is 'engineering-tests' as this is | ||
1769 | 19 | the repository name. | ||
1770 | 20 | - string: | ||
1771 | 21 | name: VERSION | ||
1772 | 22 | default: "" | ||
1773 | 23 | description: "New version of the snap" | ||
1774 | 24 | - string: | ||
1775 | 25 | name: NEXT_VERSION | ||
1776 | 26 | default: "" | ||
1777 | 27 | description: | | ||
1778 | 28 | Version which will follow the version specified in the VERSION field. | ||
1779 | 29 | For example if you specify VERSION = "1.1" next version is most likely | ||
1780 | 30 | "1.2". The NEXT_VERSION parameter is used to write it into the component | ||
1781 | 31 | snapcraft.yaml as "$NEXT_VERSION-dev" to clearly indicate that snaps | ||
1782 | 32 | build from master are development versions. | ||
1783 | 33 | |||
1784 | 34 | Please note that NEXT_VERSION is not set in stone and can be overriden | ||
1785 | 35 | at any time by changes merged into master. | ||
1786 | 36 | - string: | ||
1787 | 37 | name: CLEANUP_WORKSPACE | ||
1788 | 38 | default: "0" | ||
1789 | 39 | description: "Cleanup the whole workspace" | ||
1790 | 40 | - string: | ||
1791 | 41 | name: SERIES | ||
1792 | 42 | default: xenial | ||
1793 | 43 | description: "Ubuntu archive series to build for" | ||
1794 | 44 | - string: | ||
1795 | 45 | name: RELEASE_FROM_STABLE | ||
1796 | 46 | default: 0 | ||
1797 | 47 | description: | | ||
1798 | 48 | Set to '1' to force a release from stable branch without merging with | ||
1799 | 49 | master. This can be used when single changes are picked into stable | ||
1800 | 50 | manually and need to be released without pulling anything else from | ||
1801 | 51 | master. PLEASE ENSURE THAT THOSE CHANGE GO INTO MASTER TOO!! | ||
1802 | 52 | builders: | ||
1803 | 53 | - shell: | ||
1804 | 54 | !include-raw: | ||
1805 | 55 | - common-job-prepare.sh | ||
1806 | 56 | - shell: | ||
1807 | 57 | !include-raw-escape: | ||
1808 | 58 | - snap-release.sh | ||
1809 | diff --git a/jobs/snap/snap-test.sh b/jobs/snap/snap-test.sh | |||
1810 | 0 | new file mode 100644 | 59 | new file mode 100644 |
1811 | index 0000000..bea5e3a | |||
1812 | --- /dev/null | |||
1813 | +++ b/jobs/snap/snap-test.sh | |||
1814 | @@ -0,0 +1,121 @@ | |||
1815 | 1 | #!/bin/sh | ||
1816 | 2 | # | ||
1817 | 3 | # Copyright (C) 2016 Canonical Ltd | ||
1818 | 4 | # | ||
1819 | 5 | # This program is free software: you can redistribute it and/or modify | ||
1820 | 6 | # it under the terms of the GNU General Public License version 3 as | ||
1821 | 7 | # published by the Free Software Foundation. | ||
1822 | 8 | # | ||
1823 | 9 | # This program is distributed in the hope that it will be useful, | ||
1824 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
1825 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
1826 | 12 | # GNU General Public License for more details. | ||
1827 | 13 | # | ||
1828 | 14 | # You should have received a copy of the GNU General Public License | ||
1829 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
1830 | 16 | |||
1831 | 17 | set -ex | ||
1832 | 18 | |||
1833 | 19 | . "$WORKSPACE/.build_env" | ||
1834 | 20 | |||
1835 | 21 | if [ "$RUN_TESTS" = False ]; then | ||
1836 | 22 | echo "WARNING: test execution is disabled" | ||
1837 | 23 | exit 0 | ||
1838 | 24 | fi | ||
1839 | 25 | |||
1840 | 26 | SSH_PATH="${JENKINS_HOME}/.ssh/" | ||
1841 | 27 | SSH_KEY_PATH="${SSH_PATH}/git.launchpad.net/system-enablement-ci-bot" | ||
1842 | 28 | |||
1843 | 29 | SSH="ssh -i $SSH_KEY_PATH/id_rsa $REMOTE_USER@$REMOTE_WORKER" | ||
1844 | 30 | SCP="scp -i $SSH_KEY_PATH/id_rsa" | ||
1845 | 31 | |||
1846 | 32 | REMOTE_WORKSPACE=/home/$REMOTE_USER/$BUILD_TAG | ||
1847 | 33 | REMOTE_RESULTS_BASE_DIR=/home/$REMOTE_USER/results | ||
1848 | 34 | |||
1849 | 35 | tmp_srcdir=`mktemp -d` | ||
1850 | 36 | git clone --depth 1 -b $SOURCE_GIT_REPO_BRANCH $SOURCE_GIT_REPO $tmp_srcdir/src | ||
1851 | 37 | cd $tmp_srcdir/src | ||
1852 | 38 | # This will fail as we have set set -e above when the revision isn't part of | ||
1853 | 39 | # of the repository we've cloned. | ||
1854 | 40 | git branch --contains $SOURCE_GIT_REPO_REVISION | grep "$SOURCE_GIT_REPO_BRANCH" | ||
1855 | 41 | git checkout -b ci-test $SOURCE_GIT_REPO_REVISION | ||
1856 | 42 | |||
1857 | 43 | # Components have the ability to disable CI tests if they can't provide any. | ||
1858 | 44 | # This only accepted in a few cases and should be generally avoided. | ||
1859 | 45 | if [ -e $tmp_srcdir/src/.ci_tests_disabled ]; then | ||
1860 | 46 | echo "WARNING: Component has no CI tests so not running anything here" | ||
1861 | 47 | exit 0 | ||
1862 | 48 | fi | ||
1863 | 49 | # We require either a run-tests.sh interface script for the spread tests | ||
1864 | 50 | # or the basic spread.yaml spread test definition file, otherwise we fail | ||
1865 | 51 | # the Jenkins job. Spread tests are required for all MRs. | ||
1866 | 52 | if [ ! -e "$tmp_srcdir/src/run-tests.sh" ] && [ ! -e "$tmp_srcdir/src/spread.yaml" ]; then | ||
1867 | 53 | echo "ERROR: missing spread test: you must provide a spread test" | ||
1868 | 54 | exit 1 | ||
1869 | 55 | fi | ||
1870 | 56 | rm -rf $tmp_srcdir | ||
1871 | 57 | |||
1872 | 58 | if [ -n "$RESULTS_ID" ]; then | ||
1873 | 59 | $SSH mkdir -p $REMOTE_WORKSPACE/results | ||
1874 | 60 | $SSH cp -v $REMOTE_RESULTS_BASE_DIR/$RESULTS_ID/*.snap $REMOTE_WORKSPACE/results | ||
1875 | 61 | fi | ||
1876 | 62 | |||
1877 | 63 | $SSH sudo apt-get --yes --force-yes install docker.io | ||
1878 | 64 | |||
1879 | 65 | cat << EOF > $WORKSPACE/run-tests.sh | ||
1880 | 66 | #!/bin/sh | ||
1881 | 67 | set -ex | ||
1882 | 68 | |||
1883 | 69 | export TERM=linux | ||
1884 | 70 | export DEBIAN_FRONTEND=noninteractive | ||
1885 | 71 | export PATH=/build/bin:$PATH | ||
1886 | 72 | |||
1887 | 73 | # At this time it's necessary to build spread manually because | ||
1888 | 74 | # the snapped version does not include the qemu/kvm backend. | ||
1889 | 75 | # Once the snapped version includes this backend, then we can | ||
1890 | 76 | # change the manual building of spread with making sure the snap | ||
1891 | 77 | # package is installed. | ||
1892 | 78 | export GOPATH=`mktemp -d` | ||
1893 | 79 | go get -d -v github.com/snapcore/spread/... | ||
1894 | 80 | go build github.com/snapcore/spread/cmd/spread | ||
1895 | 81 | mkdir /build/bin | ||
1896 | 82 | cp spread /build/bin | ||
1897 | 83 | |||
1898 | 84 | git clone --depth 1 -b $SOURCE_GIT_REPO_BRANCH $SOURCE_GIT_REPO /build/src | ||
1899 | 85 | cd /build/src | ||
1900 | 86 | git checkout -b ci-tests $SOURCE_GIT_REPO_REVISION | ||
1901 | 87 | |||
1902 | 88 | # Copy any stage results from previous generic-build-snap-worker builds | ||
1903 | 89 | cp -v /build/results/*.snap /build/src | ||
1904 | 90 | |||
1905 | 91 | if [ -e "run-tests.sh" ] ; then | ||
1906 | 92 | if [ ! -z "$CHANNEL" ] ; then | ||
1907 | 93 | ./run-tests.sh --channel=$CHANNEL --test-from-channel --debug --force-new-image | ||
1908 | 94 | else | ||
1909 | 95 | ./run-tests.sh --debug --force-new-image | ||
1910 | 96 | fi | ||
1911 | 97 | else | ||
1912 | 98 | if [ ! -z "$CHANNEL" ] ; then | ||
1913 | 99 | SNAP_CHANNEL=$CHANNEL spread -debug | ||
1914 | 100 | else | ||
1915 | 101 | spread -debug | ||
1916 | 102 | fi | ||
1917 | 103 | fi | ||
1918 | 104 | EOF | ||
1919 | 105 | |||
1920 | 106 | $SSH mkdir -p $REMOTE_WORKSPACE | ||
1921 | 107 | $SCP $WORKSPACE/run-tests.sh $REMOTE_USER@$REMOTE_WORKER:$REMOTE_WORKSPACE | ||
1922 | 108 | $SSH chmod u+x $REMOTE_WORKSPACE/run-tests.sh | ||
1923 | 109 | |||
1924 | 110 | $SSH mkdir -p $REMOTE_WORKSPACE/docker | ||
1925 | 111 | $SCP $WORKSPACE/build-scripts/docker/spread-tests/Dockerfile \ | ||
1926 | 112 | $REMOTE_USER@$REMOTE_WORKER:$REMOTE_WORKSPACE/docker | ||
1927 | 113 | $SSH time sudo docker build -t snap-spread-tests $REMOTE_WORKSPACE/docker | ||
1928 | 114 | |||
1929 | 115 | $SSH time sudo docker run \ | ||
1930 | 116 | -v /dev:/dev \ | ||
1931 | 117 | -v $REMOTE_WORKSPACE:/build \ | ||
1932 | 118 | --privileged \ | ||
1933 | 119 | snap-spread-tests /build/run-tests.sh | ||
1934 | 120 | |||
1935 | 121 | $SSH sudo rm -rf $REMOTE_WORKSPACE | ||
1936 | diff --git a/jobs/snap/snap-test.yaml b/jobs/snap/snap-test.yaml | |||
1937 | 0 | new file mode 100644 | 122 | new file mode 100644 |
1938 | index 0000000..9c15699 | |||
1939 | --- /dev/null | |||
1940 | +++ b/jobs/snap/snap-test.yaml | |||
1941 | @@ -0,0 +1,64 @@ | |||
1942 | 1 | - job-template: | ||
1943 | 2 | name: '{name}-snap-test' | ||
1944 | 3 | project-type: freestyle | ||
1945 | 4 | defaults: global | ||
1946 | 5 | description: "Run tests for a single snap on a remote agent which also allows spread execution inside KVM/QEMU" | ||
1947 | 6 | display-name: "{name}-snap-test" | ||
1948 | 7 | concurrent: true | ||
1949 | 8 | node: snap && test | ||
1950 | 9 | parameters: | ||
1951 | 10 | - string: | ||
1952 | 11 | name: SOURCE_GIT_REPO | ||
1953 | 12 | default: "" | ||
1954 | 13 | description: "Source git repository" | ||
1955 | 14 | - string: | ||
1956 | 15 | name: SOURCE_GIT_REPO_BRANCH | ||
1957 | 16 | default: "" | ||
1958 | 17 | description: "Branch of the source git repository to use" | ||
1959 | 18 | - string: | ||
1960 | 19 | name: CHANNEL | ||
1961 | 20 | default: "" | ||
1962 | 21 | description: "Run tests against an image build with a core snap from the specified channel" | ||
1963 | 22 | - string: | ||
1964 | 23 | name: REMOTE_WORKER | ||
1965 | 24 | default: "{obj:remote_worker}" | ||
1966 | 25 | description: "The remote server to execute the spread jobs on. There's no need to change from the default value unless you know what you're doing." | ||
1967 | 26 | - string: | ||
1968 | 27 | name: REMOTE_USER | ||
1969 | 28 | default: "{obj:remote_user}" | ||
1970 | 29 | description: "The remote server username used to ssh to $REMOTE_WORKER." | ||
1971 | 30 | - string: | ||
1972 | 31 | name: CLEANUP_WORKSPACE | ||
1973 | 32 | default: "0" | ||
1974 | 33 | description: "Cleanup the whole workspace" | ||
1975 | 34 | - string: | ||
1976 | 35 | name: SERIES | ||
1977 | 36 | default: xenial | ||
1978 | 37 | description: "Ubuntu archive series to build for" | ||
1979 | 38 | - string: | ||
1980 | 39 | name: REBUILD_ROOTFS | ||
1981 | 40 | default: 0 | ||
1982 | 41 | description: "Rebuild the chroot rootfs or not. Default is to not rebuild and use a previous job's rootfs." | ||
1983 | 42 | - string: | ||
1984 | 43 | name: RESULTS_ID | ||
1985 | 44 | default: "" | ||
1986 | 45 | description: "Alphanumeric Id of the results being staged on the remote worker" | ||
1987 | 46 | - string: | ||
1988 | 47 | name: CORE_CHANNEL | ||
1989 | 48 | default: "stable" | ||
1990 | 49 | description: "Channel used for the core snap inside the test environment. Defaults to 'stable'." | ||
1991 | 50 | - string: | ||
1992 | 51 | name: CI_BRANCH | ||
1993 | 52 | default: "" | ||
1994 | 53 | description: "Branch on which the tests should be executed" | ||
1995 | 54 | - string: | ||
1996 | 55 | name: CI_REPO | ||
1997 | 56 | default: "" | ||
1998 | 57 | description: "Git repository to use for testing (MUST contain $CI_BRANCH)" | ||
1999 | 58 | builders: | ||
2000 | 59 | - shell: | ||
2001 | 60 | !include-raw: | ||
2002 | 61 | - common-job-prepare.sh | ||
2003 | 62 | - shell: | ||
2004 | 63 | !include-raw-escape: | ||
2005 | 64 | - snap-test.sh | ||
2006 | diff --git a/jobs/snap/snap-trigger-ci.sh b/jobs/snap/snap-trigger-ci.sh | |||
2007 | 0 | new file mode 100644 | 65 | new file mode 100644 |
2008 | index 0000000..b86724d | |||
2009 | --- /dev/null | |||
2010 | +++ b/jobs/snap/snap-trigger-ci.sh | |||
2011 | @@ -0,0 +1,29 @@ | |||
2012 | 1 | #!/bin/bash | ||
2013 | 2 | # | ||
2014 | 3 | # Copyright (C) 2016 Canonical Ltd | ||
2015 | 4 | # | ||
2016 | 5 | # This program is free software: you can redistribute it and/or modify | ||
2017 | 6 | # it under the terms of the GNU General Public License version 3 as | ||
2018 | 7 | # published by the Free Software Foundation. | ||
2019 | 8 | # | ||
2020 | 9 | # This program is distributed in the hope that it will be useful, | ||
2021 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2022 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2023 | 12 | # GNU General Public License for more details. | ||
2024 | 13 | # | ||
2025 | 14 | # You should have received a copy of the GNU General Public License | ||
2026 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
2027 | 16 | |||
2028 | 17 | set -ex | ||
2029 | 18 | |||
2030 | 19 | . "$WORKSPACE/.build_env" | ||
2031 | 20 | |||
2032 | 21 | if [ "$TRIGGER_CI" = False ]; then | ||
2033 | 22 | echo "WARNING: CI is disabled" | ||
2034 | 23 | exit 0 | ||
2035 | 24 | fi | ||
2036 | 25 | |||
2037 | 26 | exec "$BUILD_SCRIPTS"/tools/trigger-ci.py \ | ||
2038 | 27 | -p "$LAUNCHPAD_PROJECT" \ | ||
2039 | 28 | -j "$SNAP_BUILD_JOB" \ | ||
2040 | 29 | -t "$LAUNCHPAD_TEAM" | ||
2041 | diff --git a/jobs/snap/snap-trigger-ci.yaml b/jobs/snap/snap-trigger-ci.yaml | |||
2042 | 0 | new file mode 100644 | 30 | new file mode 100644 |
2043 | index 0000000..e412325 | |||
2044 | --- /dev/null | |||
2045 | +++ b/jobs/snap/snap-trigger-ci.yaml | |||
2046 | @@ -0,0 +1,22 @@ | |||
2047 | 1 | - job-template: | ||
2048 | 2 | name: '{name}-snap-trigger-ci' | ||
2049 | 3 | project-type: freestyle | ||
2050 | 4 | defaults: global | ||
2051 | 5 | description: "Monitor Launchpad for new merge proposals" | ||
2052 | 6 | display-name: "{name}-snap-trigger-ci" | ||
2053 | 7 | concurrent: true | ||
2054 | 8 | node: snap && misc | ||
2055 | 9 | triggers: | ||
2056 | 10 | - timed: # every five minutes | ||
2057 | 11 | H/5 * * * * | ||
2058 | 12 | properties: | ||
2059 | 13 | - build-discarder: | ||
2060 | 14 | num-to-keep: 10 | ||
2061 | 15 | - rebuild | ||
2062 | 16 | builders: | ||
2063 | 17 | - shell: | ||
2064 | 18 | !include-raw: | ||
2065 | 19 | - common-job-prepare.sh | ||
2066 | 20 | - shell: | ||
2067 | 21 | !include-raw-escape: | ||
2068 | 22 | - snap-trigger-ci.sh | ||
2069 | diff --git a/jobs/snap/snap-update-mp.sh b/jobs/snap/snap-update-mp.sh | |||
2070 | 0 | new file mode 100644 | 23 | new file mode 100644 |
2071 | index 0000000..fd05d6e | |||
2072 | --- /dev/null | |||
2073 | +++ b/jobs/snap/snap-update-mp.sh | |||
2074 | @@ -0,0 +1,30 @@ | |||
2075 | 1 | #!/bin/sh | ||
2076 | 2 | # | ||
2077 | 3 | # Copyright (C) 2016 Canonical Ltd | ||
2078 | 4 | # | ||
2079 | 5 | # This program is free software: you can redistribute it and/or modify | ||
2080 | 6 | # it under the terms of the GNU General Public License version 3 as | ||
2081 | 7 | # published by the Free Software Foundation. | ||
2082 | 8 | # | ||
2083 | 9 | # This program is distributed in the hope that it will be useful, | ||
2084 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2085 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2086 | 12 | # GNU General Public License for more details. | ||
2087 | 13 | # | ||
2088 | 14 | # You should have received a copy of the GNU General Public License | ||
2089 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
2090 | 16 | |||
2091 | 17 | set -ex | ||
2092 | 18 | |||
2093 | 19 | . "$WORKSPACE/.build_env" | ||
2094 | 20 | |||
2095 | 21 | if [ "$UPDATE_MPS" = False ]; then | ||
2096 | 22 | echo "WARNING: MP updates are disabled" | ||
2097 | 23 | exit 0 | ||
2098 | 24 | fi | ||
2099 | 25 | |||
2100 | 26 | exec $BUILD_SCRIPTS/tools/vote-on-merge-proposal.py \ | ||
2101 | 27 | -s $CI_RESULT \ | ||
2102 | 28 | -u $CI_BUILD \ | ||
2103 | 29 | -r $CI_REVISION \ | ||
2104 | 30 | -p $CI_MERGE_PROPOSAL | ||
2105 | diff --git a/jobs/snap/snap-update-mp.yaml b/jobs/snap/snap-update-mp.yaml | |||
2106 | 0 | new file mode 100644 | 31 | new file mode 100644 |
2107 | index 0000000..967b891 | |||
2108 | --- /dev/null | |||
2109 | +++ b/jobs/snap/snap-update-mp.yaml | |||
2110 | @@ -0,0 +1,31 @@ | |||
2111 | 1 | - job-template: | ||
2112 | 2 | name: '{name}-snap-update-mp' | ||
2113 | 3 | project-type: freestyle | ||
2114 | 4 | defaults: global | ||
2115 | 5 | description: "Update given merge-proposal with the result of the build" | ||
2116 | 6 | display-name: "{name}-snap-update-mp" | ||
2117 | 7 | concurrent: true | ||
2118 | 8 | node: snap && misc | ||
2119 | 9 | parameters: | ||
2120 | 10 | - string: | ||
2121 | 11 | name: CI_RESULT | ||
2122 | 12 | description: Result of the CI build | ||
2123 | 13 | - string: | ||
2124 | 14 | name: CI_BUILD | ||
2125 | 15 | description: Jenkins URL of the build | ||
2126 | 16 | - string: | ||
2127 | 17 | name: CI_BRANCH | ||
2128 | 18 | description: Launchpad branch that was processed | ||
2129 | 19 | - string: | ||
2130 | 20 | name: CI_MERGE_PROPOSAL | ||
2131 | 21 | description: Launchpad merge proposal that was processed | ||
2132 | 22 | - string: | ||
2133 | 23 | name: CI_REVISION | ||
2134 | 24 | description: Revision of the processed branch | ||
2135 | 25 | builders: | ||
2136 | 26 | - shell: | ||
2137 | 27 | !include-raw: | ||
2138 | 28 | - common-job-prepare.sh | ||
2139 | 29 | - shell: | ||
2140 | 30 | !include-raw-escape: | ||
2141 | 31 | - snap-update-mp.sh | ||
2142 | diff --git a/local.conf b/local.conf | |||
2143 | 0 | new file mode 100644 | 32 | new file mode 100644 |
2144 | index 0000000..0170c70 | |||
2145 | --- /dev/null | |||
2146 | +++ b/local.conf | |||
2147 | @@ -0,0 +1,11 @@ | |||
2148 | 1 | [job_builder] | ||
2149 | 2 | ignore_cache=True | ||
2150 | 3 | keep_descriptions=False | ||
2151 | 4 | recursive=False | ||
2152 | 5 | allow_duplicates=False | ||
2153 | 6 | |||
2154 | 7 | [jenkins] | ||
2155 | 8 | user=system-enablement-ci-bot | ||
2156 | 9 | password=jenkins | ||
2157 | 10 | url=http://localhost:8080 | ||
2158 | 11 | query_plugins_info=False | ||
2159 | diff --git a/local.yaml b/local.yaml | |||
2160 | 0 | new file mode 100644 | 12 | new file mode 100644 |
2161 | index 0000000..4de3a0c | |||
2162 | --- /dev/null | |||
2163 | +++ b/local.yaml | |||
2164 | @@ -0,0 +1,60 @@ | |||
2165 | 1 | - project: | ||
2166 | 2 | name: infrastructure | ||
2167 | 3 | |||
2168 | 4 | jobs-git-repo: https://git.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/jenkins-jobs | ||
2169 | 5 | jobs-git-repo-branch: master | ||
2170 | 6 | config-git-repo: https://git.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/jenkins-config | ||
2171 | 7 | config-git-repo-branch: master | ||
2172 | 8 | |||
2173 | 9 | jenkins-instance: localhost | ||
2174 | 10 | |||
2175 | 11 | bot_username: system-enablement-ci-bot | ||
2176 | 12 | credentials_path: /var/lib/jenkins/.launchpad.credentials | ||
2177 | 13 | allowed_users: "canonical-system-enablement" | ||
2178 | 14 | backend_url: http://localhost:8080/ | ||
2179 | 15 | blacklisted_jobs: "" | ||
2180 | 16 | install_packages: "" | ||
2181 | 17 | build_slaves: | ||
2182 | 18 | - master | ||
2183 | 19 | jobs: | ||
2184 | 20 | - infrastructure-jobs | ||
2185 | 21 | |||
2186 | 22 | - project: | ||
2187 | 23 | name: system-enablement | ||
2188 | 24 | |||
2189 | 25 | jobs-git-repo: https://git.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/jenkins-jobs | ||
2190 | 26 | jobs-git-repo-branch: master | ||
2191 | 27 | |||
2192 | 28 | bot_username: system-enablement-ci-bot | ||
2193 | 29 | |||
2194 | 30 | allowed_users: "canonical-system-enablement" | ||
2195 | 31 | launchpad_project: "snappy-hwe-snaps" | ||
2196 | 32 | launchpad_team: "snappy-hwe-team" | ||
2197 | 33 | |||
2198 | 34 | update_mps: false | ||
2199 | 35 | run_tests: false | ||
2200 | 36 | build_on_launchpad: false | ||
2201 | 37 | trigger_ci: false | ||
2202 | 38 | auto_merge: false | ||
2203 | 39 | |||
2204 | 40 | build_architectures: [amd64] | ||
2205 | 41 | |||
2206 | 42 | all_slaves: | ||
2207 | 43 | - master | ||
2208 | 44 | build_slaves: | ||
2209 | 45 | - master | ||
2210 | 46 | |||
2211 | 47 | blacklisted_jobs: "" | ||
2212 | 48 | |||
2213 | 49 | remote_worker: "" | ||
2214 | 50 | remote_user: "" | ||
2215 | 51 | |||
2216 | 52 | nightly_architectures: [] | ||
2217 | 53 | nightly_core_channels: [] | ||
2218 | 54 | nightly_snaps: [] | ||
2219 | 55 | |||
2220 | 56 | base_snap_repo_url: 'https://git.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git' | ||
2221 | 57 | |||
2222 | 58 | jobs: | ||
2223 | 59 | - snap-project-jobs | ||
2224 | 60 | - image-project-jobs | ||
2225 | diff --git a/run-tests.sh b/run-tests.sh | |||
2226 | index 3e02fc5..83cf437 100755 | |||
2227 | --- a/run-tests.sh | |||
2228 | +++ b/run-tests.sh | |||
2229 | @@ -1,4 +1,5 @@ | |||
2230 | 1 | #!/bin/sh | 1 | #!/bin/sh |
2231 | 2 | <<<<<<< run-tests.sh | ||
2232 | 2 | # | 3 | # |
2233 | 3 | # Copyright (C) 2016 Canonical Ltd | 4 | # Copyright (C) 2016 Canonical Ltd |
2234 | 4 | # | 5 | # |
2235 | @@ -74,3 +75,6 @@ fi | |||
2236 | 74 | 75 | ||
2237 | 75 | echo "INFO: Executing tests runner" | 76 | echo "INFO: Executing tests runner" |
2238 | 76 | cd $TESTS_EXTRAS_PATH && ./tests-runner.sh "$@" "$EXTRA_ARGS" | 77 | cd $TESTS_EXTRAS_PATH && ./tests-runner.sh "$@" "$EXTRA_ARGS" |
2239 | 78 | ======= | ||
2240 | 79 | echo "Nothing yet!" | ||
2241 | 80 | >>>>>>> run-tests.sh | ||
2242 | diff --git a/tools/automerge-mps.py b/tools/automerge-mps.py | |||
2243 | 77 | new file mode 100755 | 81 | new file mode 100755 |
2244 | index 0000000..8699dd8 | |||
2245 | --- /dev/null | |||
2246 | +++ b/tools/automerge-mps.py | |||
2247 | @@ -0,0 +1,152 @@ | |||
2248 | 1 | #!/usr/bin/env python | ||
2249 | 2 | # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- | ||
2250 | 3 | # | ||
2251 | 4 | # Copyright (C) 2016 Canonical Ltd | ||
2252 | 5 | # | ||
2253 | 6 | # This program is free software: you can redistribute it and/or modify | ||
2254 | 7 | # it under the terms of the GNU General Public License version 3 as | ||
2255 | 8 | # published by the Free Software Foundation. | ||
2256 | 9 | # | ||
2257 | 10 | # This program is distributed in the hope that it will be useful, | ||
2258 | 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2259 | 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2260 | 13 | # GNU General Public License for more details. | ||
2261 | 14 | # | ||
2262 | 15 | # You should have received a copy of the GNU General Public License | ||
2263 | 16 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
2264 | 17 | |||
2265 | 18 | from launchpadlib.launchpad import Launchpad | ||
2266 | 19 | import os | ||
2267 | 20 | import sys | ||
2268 | 21 | import yaml | ||
2269 | 22 | import git | ||
2270 | 23 | import shutil | ||
2271 | 24 | import se_utils | ||
2272 | 25 | |||
2273 | 26 | from argparse import ArgumentParser | ||
2274 | 27 | |||
2275 | 28 | parser = ArgumentParser(description="Trigger snap builds for pending merge proposals") | ||
2276 | 29 | parser.add_argument('-p', '--project', required=True, | ||
2277 | 30 | help="Launchpad project to check for merge-proposals") | ||
2278 | 31 | |||
2279 | 32 | args = vars(parser.parse_args()) | ||
2280 | 33 | |||
2281 | 34 | class LaunchpadVote(): | ||
2282 | 35 | APPROVE = 'Approve' | ||
2283 | 36 | DISAPPROVE = 'Disapprove' | ||
2284 | 37 | NEEDS_FIXING = 'Needs Fixing' | ||
2285 | 38 | |||
2286 | 39 | def load_config(): | ||
2287 | 40 | files = [os.path.expanduser('~/.jlp/jlp.config'), 'jlp.config'] | ||
2288 | 41 | for config_file in files: | ||
2289 | 42 | try: | ||
2290 | 43 | config = yaml.safe_load(open(config_file, 'r')) | ||
2291 | 44 | return config | ||
2292 | 45 | except IOError: | ||
2293 | 46 | pass | ||
2294 | 47 | print("ERROR: No config file found") | ||
2295 | 48 | sys.exit(1) | ||
2296 | 49 | |||
2297 | 50 | def get_config_option(name): | ||
2298 | 51 | config = load_config() | ||
2299 | 52 | return config[name] | ||
2300 | 53 | |||
2301 | 54 | def clean_branch_name(branch_name): | ||
2302 | 55 | if branch_name.startswith("refs/heads/"): | ||
2303 | 56 | return branch_name[11:] | ||
2304 | 57 | return branch_name | ||
2305 | 58 | |||
2306 | 59 | def correct_ssh_url(url): | ||
2307 | 60 | if not url.startswith("git+ssh://"): | ||
2308 | 61 | return url | ||
2309 | 62 | new_url = "git+ssh://system-enablement-ci-bot@%s" % url[10:] | ||
2310 | 63 | return new_url | ||
2311 | 64 | |||
2312 | 65 | |||
2313 | 66 | def try_merge(proposal, target_repo, target_branch, source_repo, source_branch): | ||
2314 | 67 | source_git_url=source_repo.git_https_url | ||
2315 | 68 | # If we're operating with a private git repository we have to use the | ||
2316 | 69 | # SSH url. Otherwise we try to avoid that to not cause any damage on | ||
2317 | 70 | # the source repository. | ||
2318 | 71 | if source_git_url == None: | ||
2319 | 72 | source_git_url = source_repo.git_ssh_url | ||
2320 | 73 | |||
2321 | 74 | print("Trying to merge %s:%s into %s:%s" % (source_git_url, source_branch, target_repo.git_ssh_url, target_branch)) | ||
2322 | 75 | |||
2323 | 76 | repo_path = os.path.join(os.environ["WORKSPACE"], "repo") | ||
2324 | 77 | if os.path.exists(repo_path): | ||
2325 | 78 | shutil.rmtree(repo_path) | ||
2326 | 79 | |||
2327 | 80 | repo = git.Repo.clone_from(correct_ssh_url(target_repo.git_ssh_url), repo_path, branch=target_branch) | ||
2328 | 81 | source_remote = repo.create_remote("source", source_git_url) | ||
2329 | 82 | source_remote.fetch() | ||
2330 | 83 | |||
2331 | 84 | repo.git.config("user.name", "System Enablement CI Bot") | ||
2332 | 85 | # FIXME: What is the real email address of the bot? | ||
2333 | 86 | repo.git.config("user.email", "ce-system-enablement@lists.canonical.com") | ||
2334 | 87 | |||
2335 | 88 | registrant_name = proposal.registrant.display_name | ||
2336 | 89 | try: | ||
2337 | 90 | registrant_mail = proposal.registrant.preferred_email_address.email | ||
2338 | 91 | except Exception as e: | ||
2339 | 92 | print("WARNING: cannot get e-mail for %s (%s)" % (registrant_name, e)) | ||
2340 | 93 | registrant_mail="(unknown e-mail)" | ||
2341 | 94 | |||
2342 | 95 | repo.git.merge("--no-ff", | ||
2343 | 96 | "-m", "Merge remote tracking branch %s" % (source_branch), | ||
2344 | 97 | "-m", "Merge-Proposal: %s" % proposal.web_link, | ||
2345 | 98 | "-m", "Author: %s <%s>" % (registrant_name, registrant_mail), | ||
2346 | 99 | "-m", "%s" % proposal.description, | ||
2347 | 100 | "source/%s" % source_branch) | ||
2348 | 101 | |||
2349 | 102 | repo.git.push("origin", target_branch) | ||
2350 | 103 | |||
2351 | 104 | def get_last_mp_vote(mp): | ||
2352 | 105 | for vote in mp.votes: | ||
2353 | 106 | if not vote.comment: | ||
2354 | 107 | continue | ||
2355 | 108 | if vote.review_type == "continuous-integration" and vote.comment: | ||
2356 | 109 | return vote.comment.vote | ||
2357 | 110 | return None | ||
2358 | 111 | |||
2359 | 112 | def mp_is_disapproved(mp): | ||
2360 | 113 | for vote in mp.votes: | ||
2361 | 114 | if vote.comment and vote.comment.vote == LaunchpadVote.DISAPPROVE: | ||
2362 | 115 | return True | ||
2363 | 116 | return False | ||
2364 | 117 | |||
2365 | 118 | lp_app = get_config_option("lp_app") | ||
2366 | 119 | lp_env = get_config_option("lp_env") | ||
2367 | 120 | credential_store_path = get_config_option('credential_store_path') | ||
2368 | 121 | launchpad = se_utils.get_launchpad(None, credential_store_path, lp_app, lp_env) | ||
2369 | 122 | |||
2370 | 123 | project = launchpad.projects[args['project']] | ||
2371 | 124 | proposals = project.getMergeProposals(status=['Approved']) | ||
2372 | 125 | |||
2373 | 126 | failed_merges = 0 | ||
2374 | 127 | |||
2375 | 128 | print("Found %d candidate merge proposals" % len(proposals)) | ||
2376 | 129 | |||
2377 | 130 | for proposal in proposals: | ||
2378 | 131 | if get_last_mp_vote(proposal) != LaunchpadVote.APPROVE: | ||
2379 | 132 | print("Not merging %s as not approved by CI" % proposal.web_link) | ||
2380 | 133 | continue | ||
2381 | 134 | |||
2382 | 135 | if mp_is_disapproved(proposal): | ||
2383 | 136 | print("Not merging %s as at least one reviewer has disapproved the change" % proposal.web_link) | ||
2384 | 137 | continue | ||
2385 | 138 | |||
2386 | 139 | print("Found proposal which is ready for merging: %s" % proposal.web_link) | ||
2387 | 140 | |||
2388 | 141 | try: | ||
2389 | 142 | try_merge(proposal, | ||
2390 | 143 | launchpad.load(proposal.target_git_repository_link), | ||
2391 | 144 | clean_branch_name(proposal.target_git_path), | ||
2392 | 145 | launchpad.load(proposal.source_git_repository_link), | ||
2393 | 146 | clean_branch_name(proposal.source_git_path)) | ||
2394 | 147 | except Exception as e: | ||
2395 | 148 | print("ERROR: Failed to merge %s (%s)" % (proposal.web_link, e)) | ||
2396 | 149 | failed_merges += 1 | ||
2397 | 150 | |||
2398 | 151 | if failed_merges > 0: | ||
2399 | 152 | sys.exit(1) | ||
2400 | diff --git a/tools/build-rootfs-create b/tools/build-rootfs-create | |||
2401 | 0 | new file mode 100755 | 153 | new file mode 100755 |
2402 | index 0000000..9a9f9ce | |||
2403 | --- /dev/null | |||
2404 | +++ b/tools/build-rootfs-create | |||
2405 | @@ -0,0 +1,26 @@ | |||
2406 | 1 | #!/bin/bash | ||
2407 | 2 | # | ||
2408 | 3 | # Copyright (C) 2016 Canonical Ltd | ||
2409 | 4 | # | ||
2410 | 5 | # This program is free software: you can redistribute it and/or modify | ||
2411 | 6 | # it under the terms of the GNU General Public License version 3 as | ||
2412 | 7 | # published by the Free Software Foundation. | ||
2413 | 8 | # | ||
2414 | 9 | # This program is distributed in the hope that it will be useful, | ||
2415 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2416 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2417 | 12 | # GNU General Public License for more details. | ||
2418 | 13 | # | ||
2419 | 14 | # You should have received a copy of the GNU General Public License | ||
2420 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
2421 | 16 | |||
2422 | 17 | set -x | ||
2423 | 18 | set -e | ||
2424 | 19 | |||
2425 | 20 | SERIES=$1 | ||
2426 | 21 | TARBALL=$2 | ||
2427 | 22 | |||
2428 | 23 | mkdir -p rootfs | ||
2429 | 24 | debootstrap --components=main,universe $SERIES rootfs | ||
2430 | 25 | tar cf $TARBALL rootfs | ||
2431 | 26 | rm -rf rootfs | ||
2432 | diff --git a/tools/common.sh b/tools/common.sh | |||
2433 | 0 | new file mode 100755 | 27 | new file mode 100755 |
2434 | index 0000000..c30d2d7 | |||
2435 | --- /dev/null | |||
2436 | +++ b/tools/common.sh | |||
2437 | @@ -0,0 +1,93 @@ | |||
2438 | 1 | #!/bin/sh -ex | ||
2439 | 2 | # | ||
2440 | 3 | # Copyright (C) 2017 Canonical Ltd | ||
2441 | 4 | # | ||
2442 | 5 | # This program is free software: you can redistribute it and/or modify | ||
2443 | 6 | # it under the terms of the GNU General Public License version 3 as | ||
2444 | 7 | # published by the Free Software Foundation. | ||
2445 | 8 | # | ||
2446 | 9 | # This program is distributed in the hope that it will be useful, | ||
2447 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2448 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2449 | 12 | # GNU General Public License for more details. | ||
2450 | 13 | # | ||
2451 | 14 | # You should have received a copy of the GNU General Public License | ||
2452 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
2453 | 16 | |||
2454 | 17 | # Set common variables used by the jenkins jobs | ||
2455 | 18 | set_jenkins_env () | ||
2456 | 19 | { | ||
2457 | 20 | SSH_PATH="${JENKINS_HOME}/.ssh/" | ||
2458 | 21 | SSH_KEY_PATH="${SSH_PATH}/bazaar.launchpad.net/system-enablement-ci-bot" | ||
2459 | 22 | |||
2460 | 23 | SSH="ssh -i $SSH_KEY_PATH/id_rsa $REMOTE_USER@$REMOTE_WORKER" | ||
2461 | 24 | SCP="scp -i $SSH_KEY_PATH/id_rsa" | ||
2462 | 25 | |||
2463 | 26 | REPO=https://git.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/$CI_REPO | ||
2464 | 27 | BRANCH=$CI_BRANCH | ||
2465 | 28 | |||
2466 | 29 | # If no CI repo/branch is set fallback to the source repo/branch set | ||
2467 | 30 | # which will be the case for those repositories which don't contain | ||
2468 | 31 | # a snap. | ||
2469 | 32 | if [ -z "$CI_REPO" ]; then | ||
2470 | 33 | REPO=$SOURCE_GIT_REPO | ||
2471 | 34 | BRANCH=$SOURCE_GIT_REPO_BRANCH | ||
2472 | 35 | fi | ||
2473 | 36 | |||
2474 | 37 | REMOTE_WORKSPACE=/home/$REMOTE_USER/$BUILD_TAG | ||
2475 | 38 | REMOTE_RESULTS_BASE_DIR=/home/$REMOTE_USER/results | ||
2476 | 39 | } | ||
2477 | 40 | |||
2478 | 41 | # Sets variables | ||
2479 | 42 | # TEST_TYPE={script, spread} | ||
2480 | 43 | # HW_TESTS_RESULT={0, !=0} -> {has hw tests, does not have hw tests} | ||
2481 | 44 | # FIXME Maybe depending on context the call to clone could be avoided | ||
2482 | 45 | set_test_type () | ||
2483 | 46 | { | ||
2484 | 47 | tmp_srcdir=$(mktemp -d) | ||
2485 | 48 | |||
2486 | 49 | # We use FAIL to make sure we do not exit until we free tmp_srcdir | ||
2487 | 50 | FAIL=no | ||
2488 | 51 | git clone --depth 1 -b "$BRANCH" "$REPO" "$tmp_srcdir"/src || FAIL=yes | ||
2489 | 52 | cd "$tmp_srcdir"/src || FAIL=yes | ||
2490 | 53 | |||
2491 | 54 | TEST_TYPE=none | ||
2492 | 55 | if [ -e "$tmp_srcdir/src/spread.yaml" ]; then | ||
2493 | 56 | TEST_TYPE=spread | ||
2494 | 57 | fi | ||
2495 | 58 | # run-tests.sh gets priority over spread.yaml | ||
2496 | 59 | if [ -e "$tmp_srcdir/src/run-tests.sh" ]; then | ||
2497 | 60 | TEST_TYPE=script | ||
2498 | 61 | fi | ||
2499 | 62 | |||
2500 | 63 | # TODO: Use https://github.com/0k/shyaml in the future for this | ||
2501 | 64 | if grep -q "type: adhoc" spread.yaml; then | ||
2502 | 65 | HW_TESTS_RESULT=0 | ||
2503 | 66 | else | ||
2504 | 67 | HW_TESTS_RESULT=1 | ||
2505 | 68 | fi | ||
2506 | 69 | |||
2507 | 70 | # Components have the ability to disable CI tests if they can't provide any. | ||
2508 | 71 | # This is only accepted in a few cases and should be generally avoided. | ||
2509 | 72 | CI_TESTS_DISABLED=no | ||
2510 | 73 | if [ -e "$tmp_srcdir"/src/.ci_tests_disabled ]; then | ||
2511 | 74 | CI_TESTS_DISABLED=yes | ||
2512 | 75 | fi | ||
2513 | 76 | |||
2514 | 77 | rm -rf "$tmp_srcdir" | ||
2515 | 78 | |||
2516 | 79 | if [ "$FAIL" = yes ]; then | ||
2517 | 80 | echo "ERROR: critical in set_test_type()" | ||
2518 | 81 | exit 1 | ||
2519 | 82 | fi | ||
2520 | 83 | |||
2521 | 84 | if [ "$CI_TESTS_DISABLED" = yes ]; then | ||
2522 | 85 | echo "WARNING: Component has no CI tests so not running anything here" | ||
2523 | 86 | exit 0 | ||
2524 | 87 | fi | ||
2525 | 88 | |||
2526 | 89 | if [ "$TEST_TYPE" = none ]; then | ||
2527 | 90 | echo "ERROR: missing spread or script tests: you must provide one of them" | ||
2528 | 91 | exit 1 | ||
2529 | 92 | fi | ||
2530 | 93 | } | ||
2531 | diff --git a/tools/delete-ci-repo.py b/tools/delete-ci-repo.py | |||
2532 | 0 | new file mode 100755 | 94 | new file mode 100755 |
2533 | index 0000000..35c762d | |||
2534 | --- /dev/null | |||
2535 | +++ b/tools/delete-ci-repo.py | |||
2536 | @@ -0,0 +1,45 @@ | |||
2537 | 1 | #!/usr/bin/env python | ||
2538 | 2 | # | ||
2539 | 3 | # Copyright (C) 2017 Canonical Ltd | ||
2540 | 4 | # | ||
2541 | 5 | # This program is free software: you can redistribute it and/or modify | ||
2542 | 6 | # it under the terms of the GNU General Public License version 3 as | ||
2543 | 7 | # published by the Free Software Foundation. | ||
2544 | 8 | # | ||
2545 | 9 | # This program is distributed in the hope that it will be useful, | ||
2546 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2547 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2548 | 12 | # GNU General Public License for more details. | ||
2549 | 13 | # | ||
2550 | 14 | # You should have received a copy of the GNU General Public License | ||
2551 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
2552 | 16 | |||
2553 | 17 | from launchpadlib.launchpad import Launchpad | ||
2554 | 18 | |||
2555 | 19 | from argparse import ArgumentParser | ||
2556 | 20 | |||
2557 | 21 | import se_utils | ||
2558 | 22 | |||
2559 | 23 | print("Running delete-ci-repo") | ||
2560 | 24 | |||
2561 | 25 | parser = ArgumentParser(description="Delete a git repository stored in launchpad") | ||
2562 | 26 | parser.add_argument('--git-repo', help="Git repository to be deleted") | ||
2563 | 27 | |||
2564 | 28 | args = vars(parser.parse_args()) | ||
2565 | 29 | |||
2566 | 30 | git_repo = args['git_repo'] | ||
2567 | 31 | ind = git_repo.find('~') | ||
2568 | 32 | if ind == -1: | ||
2569 | 33 | print("Bad git repo {}".format(git_repo)) | ||
2570 | 34 | exit(1) | ||
2571 | 35 | |||
2572 | 36 | lp_repo = git_repo[ind:] | ||
2573 | 37 | |||
2574 | 38 | lp_app = se_utils.get_config_option("lp_app") | ||
2575 | 39 | lp_env = se_utils.get_config_option("lp_env") | ||
2576 | 40 | credential_store_path = se_utils.get_config_option('credential_store_path') | ||
2577 | 41 | launchpad = se_utils.get_launchpad(None, credential_store_path, lp_app, lp_env) | ||
2578 | 42 | |||
2579 | 43 | repo = launchpad.git_repositories.getByPath(path=lp_repo) | ||
2580 | 44 | print("Removing {}".format(lp_repo)) | ||
2581 | 45 | repo.lp_delete() | ||
2582 | diff --git a/tools/hardware-test.sh b/tools/hardware-test.sh | |||
2583 | 0 | new file mode 100755 | 46 | new file mode 100755 |
2584 | index 0000000..41eee60 | |||
2585 | --- /dev/null | |||
2586 | +++ b/tools/hardware-test.sh | |||
2587 | @@ -0,0 +1,112 @@ | |||
2588 | 1 | #!/bin/sh -ex | ||
2589 | 2 | # | ||
2590 | 3 | # Copyright (C) 2017 Canonical Ltd | ||
2591 | 4 | # | ||
2592 | 5 | # This program is free software: you can redistribute it and/or modify | ||
2593 | 6 | # it under the terms of the GNU General Public License version 3 as | ||
2594 | 7 | # published by the Free Software Foundation. | ||
2595 | 8 | # | ||
2596 | 9 | # This program is distributed in the hope that it will be useful, | ||
2597 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2598 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2599 | 12 | # GNU General Public License for more details. | ||
2600 | 13 | # | ||
2601 | 14 | # You should have received a copy of the GNU General Public License | ||
2602 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
2603 | 16 | |||
2604 | 17 | # Runs tests on real HW. Requires set_jenkins_env and set_test_type to have | ||
2605 | 18 | # already run. | ||
2606 | 19 | run_hardware_tests () | ||
2607 | 20 | { | ||
2608 | 21 | TEST_RESULTS=test_results | ||
2609 | 22 | |||
2610 | 23 | # Just dragonboard for the moment | ||
2611 | 24 | DRAGONBOARD_TEST=testflinger-dragonboard.yaml | ||
2612 | 25 | |||
2613 | 26 | # We use jq to process testflinger output | ||
2614 | 27 | if ! which jq; then | ||
2615 | 28 | sudo apt install --yes --allow-downgrades --allow-remove-essential \ | ||
2616 | 29 | --allow-change-held-packages jq | ||
2617 | 30 | fi | ||
2618 | 31 | |||
2619 | 32 | # Initially the device has a password-less ubuntu user. But spread needs a | ||
2620 | 33 | # user with a password, so we use the DEVICE_USER/DEVICE_PASSWORD pair to | ||
2621 | 34 | # create it. Note: {device_ip} gets substituted by testflinger. | ||
2622 | 35 | DEVICE_USER=test | ||
2623 | 36 | DEVICE_PASSWORD=test | ||
2624 | 37 | DEVICE_SSH="ssh -q -o UserKnownHostsFile=/dev/null | ||
2625 | 38 | -o StrictHostKeyChecking=no -p 22 ubuntu@{device_ip}" | ||
2626 | 39 | |||
2627 | 40 | if [ "$TEST_TYPE" = script ]; then | ||
2628 | 41 | TEST_COMMAND="./run-tests.sh --spread-system=hw-ubuntu-core-16 | ||
2629 | 42 | --external-address={device_ip}:22 --external-user=$DEVICE_USER | ||
2630 | 43 | --external-password=$DEVICE_PASSWORD --debug" | ||
2631 | 44 | else | ||
2632 | 45 | TEST_COMMAND="export SPREAD_EXTERNAL_ADDRESS={device_ip}:22 && | ||
2633 | 46 | export SPREAD_EXTERNAL_USER=$DEVICE_USER && | ||
2634 | 47 | export SPREAD_EXTERNAL_PASSWORD=$DEVICE_PASSWORD && | ||
2635 | 48 | ./spread -vv external:hw-ubuntu-core-16" | ||
2636 | 49 | fi | ||
2637 | 50 | |||
2638 | 51 | cd "$WORKSPACE" | ||
2639 | 52 | |||
2640 | 53 | # Run testflinger from our bare metal server so we can install it | ||
2641 | 54 | |||
2642 | 55 | $SSH mkdir -p "$REMOTE_WORKSPACE" | ||
2643 | 56 | |||
2644 | 57 | # If the snap has been built, copy over | ||
2645 | 58 | if [ -n "$RESULTS_ID" ]; then | ||
2646 | 59 | set +x | ||
2647 | 60 | # We need to flatten the key here to avoid issues with yaml parsing | ||
2648 | 61 | SSH_KEY_DATA=$(tr '\n:' '?!' < "$SSH_KEY_PATH"/id_rsa) | ||
2649 | 62 | set -x | ||
2650 | 63 | fi | ||
2651 | 64 | |||
2652 | 65 | cat << EOF > "$WORKSPACE"/"$DRAGONBOARD_TEST" | ||
2653 | 66 | job_queue: dragonboard | ||
2654 | 67 | provision_data: | ||
2655 | 68 | channel: stable | ||
2656 | 69 | test_data: | ||
2657 | 70 | test_cmds: | ||
2658 | 71 | - git clone --depth 1 -b $BRANCH $REPO src | ||
2659 | 72 | - cd src && curl -s -O https://niemeyer.s3.amazonaws.com/spread-amd64.tar.gz && tar xzvf spread-amd64.tar.gz | ||
2660 | 73 | - $DEVICE_SSH "sudo adduser --extrausers --quiet --disabled-password --gecos '' $DEVICE_USER" | ||
2661 | 74 | - $DEVICE_SSH "echo $DEVICE_USER:$DEVICE_PASSWORD | sudo chpasswd" | ||
2662 | 75 | - $DEVICE_SSH "echo '$DEVICE_USER ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/create-user-test" | ||
2663 | 76 | - if [ -n $RESULTS_ID ]; then set +x; echo "$SSH_KEY_DATA" | tr '?!' '\n:' > ssh_key; set -x; chmod 600 ssh_key; scp -i ssh_key $REMOTE_USER@$REMOTE_WORKER:$REMOTE_RESULTS_BASE_DIR/$RESULTS_ID/*.snap src/; fi | ||
2664 | 77 | - cd src && export PATH=$PATH:\$(pwd) && $TEST_COMMAND | ||
2665 | 78 | EOF | ||
2666 | 79 | |||
2667 | 80 | $SCP "$WORKSPACE"/"$DRAGONBOARD_TEST" "$REMOTE_USER"@"$REMOTE_WORKER":"$REMOTE_WORKSPACE"/ | ||
2668 | 81 | |||
2669 | 82 | $SSH << EOF | ||
2670 | 83 | #!/bin/sh | ||
2671 | 84 | set -ex | ||
2672 | 85 | |||
2673 | 86 | cd $REMOTE_WORKSPACE | ||
2674 | 87 | if ! which virtualenv; then | ||
2675 | 88 | sudo apt install --yes --allow-downgrades --allow-remove-essential --allow-change-held-packagesvirtualenv | ||
2676 | 89 | fi | ||
2677 | 90 | |||
2678 | 91 | git clone https://git.launchpad.net/testflinger-cli | ||
2679 | 92 | cd testflinger-cli | ||
2680 | 93 | virtualenv -p python3 env | ||
2681 | 94 | . env/bin/activate | ||
2682 | 95 | ./setup.py install | ||
2683 | 96 | |||
2684 | 97 | JOB_ID=\$(testflinger-cli submit -q $REMOTE_WORKSPACE/$DRAGONBOARD_TEST) | ||
2685 | 98 | echo "JOB_ID: \${JOB_ID}" | ||
2686 | 99 | |||
2687 | 100 | testflinger-cli poll \${JOB_ID} | ||
2688 | 101 | testflinger-cli results \${JOB_ID} > $REMOTE_WORKSPACE/$TEST_RESULTS | ||
2689 | 102 | EOF | ||
2690 | 103 | |||
2691 | 104 | $SCP "$REMOTE_USER"@"$REMOTE_WORKER":"$REMOTE_WORKSPACE"/"$TEST_RESULTS" "$WORKSPACE"/ | ||
2692 | 105 | |||
2693 | 106 | $SSH sudo rm -rf "$REMOTE_WORKSPACE" | ||
2694 | 107 | |||
2695 | 108 | TEST_STATUS=$(jq -r .test_status "$WORKSPACE"/"$TEST_RESULTS") | ||
2696 | 109 | echo "Test exit status: $TEST_STATUS" | ||
2697 | 110 | |||
2698 | 111 | return "$TEST_STATUS" | ||
2699 | 112 | } | ||
2700 | diff --git a/tools/se_utils/__init__.py b/tools/se_utils/__init__.py | |||
2701 | 0 | new file mode 100644 | 113 | new file mode 100644 |
2702 | index 0000000..280450c | |||
2703 | --- /dev/null | |||
2704 | +++ b/tools/se_utils/__init__.py | |||
2705 | @@ -0,0 +1,122 @@ | |||
2706 | 1 | #!/usr/bin/env python | ||
2707 | 2 | # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- | ||
2708 | 3 | # | ||
2709 | 4 | # Copyright (C) 2016 Canonical Ltd | ||
2710 | 5 | # | ||
2711 | 6 | # This program is free software: you can redistribute it and/or modify | ||
2712 | 7 | # it under the terms of the GNU General Public License version 3 as | ||
2713 | 8 | # published by the Free Software Foundation. | ||
2714 | 9 | # | ||
2715 | 10 | # This program is distributed in the hope that it will be useful, | ||
2716 | 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
2717 | 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
2718 | 13 | # GNU General Public License for more details. | ||
2719 | 14 | # | ||
2720 | 15 | # You should have received a copy of the GNU General Public License | ||
2721 | 16 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
2722 | 17 | |||
2723 | 18 | import atexit | ||
2724 | 19 | import sys | ||
2725 | 20 | import time | ||
2726 | 21 | import logging | ||
2727 | 22 | import os | ||
2728 | 23 | import yaml | ||
2729 | 24 | from shutil import rmtree | ||
2730 | 25 | from launchpadlib.credentials import RequestTokenAuthorizationEngine | ||
2731 | 26 | from lazr.restfulclient.errors import HTTPError | ||
2732 | 27 | from launchpadlib.launchpad import Launchpad | ||
2733 | 28 | from launchpadlib.credentials import UnencryptedFileCredentialStore | ||
2734 | 29 | |||
2735 | 30 | ACCESS_TOKEN_POLL_TIME = 10 | ||
2736 | 31 | WAITING_FOR_USER = """Open this link: | ||
2737 | 32 | {} | ||
2738 | 33 | to authorize this program to access Launchpad on your behalf. | ||
2739 | 34 | Waiting to hear from Launchpad about your decision. . . .""" | ||
2740 | 35 | |||
2741 | 36 | |||
2742 | 37 | class AuthorizeRequestTokenWithConsole(RequestTokenAuthorizationEngine): | ||
2743 | 38 | """Authorize a token in a server environment (with no browser). | ||
2744 | 39 | |||
2745 | 40 | Print a link for the user to copy-and-paste into his/her browser | ||
2746 | 41 | for authentication. | ||
2747 | 42 | """ | ||
2748 | 43 | |||
2749 | 44 | def __init__(self, *args, **kwargs): | ||
2750 | 45 | # as implemented in AuthorizeRequestTokenWithBrowser | ||
2751 | 46 | kwargs['consumer_name'] = None | ||
2752 | 47 | kwargs.pop('allow_access_levels', None) | ||
2753 | 48 | super(AuthorizeRequestTokenWithConsole, self).__init__(*args, **kwargs) | ||
2754 | 49 | |||
2755 | 50 | def make_end_user_authorize_token(self, credentials, request_token): | ||
2756 | 51 | """Ask the end-user to authorize the token in their browser. | ||
2757 | 52 | |||
2758 | 53 | """ | ||
2759 | 54 | authorization_url = self.authorization_url(request_token) | ||
2760 | 55 | print WAITING_FOR_USER.format(authorization_url) | ||
2761 | 56 | # if we don't flush we may not see the message | ||
2762 | 57 | sys.stdout.flush() | ||
2763 | 58 | while credentials.access_token is None: | ||
2764 | 59 | time.sleep(ACCESS_TOKEN_POLL_TIME) | ||
2765 | 60 | try: | ||
2766 | 61 | credentials.exchange_request_token_for_access_token( | ||
2767 | 62 | self.web_root) | ||
2768 | 63 | break | ||
2769 | 64 | except HTTPError, e: | ||
2770 | 65 | if e.response.status == 403: | ||
2771 | 66 | # The user decided not to authorize this | ||
2772 | 67 | # application. | ||
2773 | 68 | raise e | ||
2774 | 69 | elif e.response.status == 401: | ||
2775 | 70 | # The user has not made a decision yet. | ||
2776 | 71 | pass | ||
2777 | 72 | else: | ||
2778 | 73 | # There was an error accessing the server. | ||
2779 | 74 | raise e | ||
2780 | 75 | |||
2781 | 76 | # launchpadlib is not thread/process safe so we are creating launchpadlib | ||
2782 | 77 | # cache in /tmp per process which gets cleaned up at the end | ||
2783 | 78 | # see also lp:459418 and lp:1025153 | ||
2784 | 79 | launchpad_cachedir = os.path.join('/tmp', str(os.getpid()), '.launchpadlib') | ||
2785 | 80 | |||
2786 | 81 | # `launchpad_cachedir` is leaked upon unexpected exits | ||
2787 | 82 | # adding this cleanup to stop directories filling up `/tmp/` | ||
2788 | 83 | atexit.register(rmtree, os.path.join('/tmp', | ||
2789 | 84 | str(os.getpid())), | ||
2790 | 85 | ignore_errors=True) | ||
2791 | 86 | |||
2792 | 87 | |||
2793 | 88 | def get_launchpad(launchpadlib_dir=None, credential_store_path=None, lp_app=None, lp_env=None): | ||
2794 | 89 | """ return a launchpad API class. In case launchpadlib_dir is | ||
2795 | 90 | specified used that directory to store launchpadlib cache instead of | ||
2796 | 91 | the default """ | ||
2797 | 92 | store = UnencryptedFileCredentialStore(credential_store_path) | ||
2798 | 93 | authorization_engine = AuthorizeRequestTokenWithConsole(lp_env, lp_app) | ||
2799 | 94 | lib_dir=launchpad_cachedir | ||
2800 | 95 | if launchpadlib_dir != None: | ||
2801 | 96 | lib_dir = launchpadlib_dir | ||
2802 | 97 | return Launchpad.login_with(lp_app, lp_env, | ||
2803 | 98 | credential_store=store, | ||
2804 | 99 | authorization_engine=authorization_engine, | ||
2805 | 100 | launchpadlib_dir=lib_dir, | ||
2806 | 101 | version='devel') | ||
2807 | 102 | |||
2808 | 103 | # Load configuration for the current agent we're running on. All agents were | ||
2809 | 104 | # provisioned when they were setup with a proper configuration. See | ||
2810 | 105 | # https://wiki.canonical.com/InformationInfrastructure/Jenkaas/UserDocs for | ||
2811 | 106 | # more details. | ||
2812 | 107 | def load_config(): | ||
2813 | 108 | files = [os.path.expanduser('~/.jlp/jlp.config'), 'jlp.config'] | ||
2814 | 109 | for config_file in files: | ||
2815 | 110 | try: | ||
2816 | 111 | config = yaml.safe_load(open(config_file, 'r')) | ||
2817 | 112 | return config | ||
2818 | 113 | except IOError: | ||
2819 | 114 | pass | ||
2820 | 115 | print("ERROR: No config file found") | ||
2821 | 116 | sys.exit(1) | ||
2822 | 117 | |||
2823 | 118 | # Return a configuration option from the agent configuration specified by the | ||
2824 | 119 | # name argument. | ||
2825 | 120 | def get_config_option(name): | ||
2826 | 121 | config = load_config() | ||
2827 | 122 | return config[name] | ||
2828 | diff --git a/tools/se_utils/__init__.pyc b/tools/se_utils/__init__.pyc | |||
2829 | 0 | new file mode 100644 | 123 | new file mode 100644 |
2830 | index 0000000..f1be9ca | |||
2831 | 1 | Binary files /dev/null and b/tools/se_utils/__init__.pyc differ | 124 | Binary files /dev/null and b/tools/se_utils/__init__.pyc differ |
2832 | diff --git a/tools/shyaml b/tools/shyaml | |||
2833 | 2 | new file mode 100755 | 125 | new file mode 100755 |
2834 | index 0000000..e4618ec | |||
2835 | --- /dev/null | |||
2836 | +++ b/tools/shyaml | |||
2837 | @@ -0,0 +1,454 @@ | |||
2838 | 1 | #!/usr/bin/env python | ||
2839 | 2 | |||
2840 | 3 | # Taken from upstream git repository https://github.com/0k/shyaml | ||
2841 | 4 | # at revision d77e30599a0971c51896ef97d21883550e7e9979 | ||
2842 | 5 | |||
2843 | 6 | ## Note: to launch test, you can use: | ||
2844 | 7 | ## python -m doctest -d shyaml.py | ||
2845 | 8 | ## or | ||
2846 | 9 | ## nosetests | ||
2847 | 10 | |||
2848 | 11 | from __future__ import print_function | ||
2849 | 12 | |||
2850 | 13 | import sys | ||
2851 | 14 | import yaml | ||
2852 | 15 | import os.path | ||
2853 | 16 | import re | ||
2854 | 17 | |||
2855 | 18 | PY3 = sys.version_info[0] >= 3 | ||
2856 | 19 | |||
2857 | 20 | EXNAME = os.path.basename(sys.argv[0]) | ||
2858 | 21 | |||
2859 | 22 | USAGE = """\ | ||
2860 | 23 | Usage: | ||
2861 | 24 | |||
2862 | 25 | %(exname)s (-h|--help) | ||
2863 | 26 | %(exname)s [-y|--yaml] ACTION KEY [DEFAULT] | ||
2864 | 27 | """ % {"exname": EXNAME} | ||
2865 | 28 | |||
2866 | 29 | HELP = """ | ||
2867 | 30 | Parses and output chosen subpart or values from YAML input. | ||
2868 | 31 | It reads YAML in stdin and will output on stdout it's return value. | ||
2869 | 32 | |||
2870 | 33 | %(usage)s | ||
2871 | 34 | |||
2872 | 35 | Options: | ||
2873 | 36 | |||
2874 | 37 | -y, --yaml | ||
2875 | 38 | Output only YAML safe value, more precisely, even | ||
2876 | 39 | literal values will be YAML quoted. This behavior | ||
2877 | 40 | is required if you want to output YAML subparts and | ||
2878 | 41 | further process it. If you know you have are dealing | ||
2879 | 42 | with safe literal value, then you don't need this. | ||
2880 | 43 | (Default: no safe YAML output) | ||
2881 | 44 | |||
2882 | 45 | ACTION Depending on the type of data you've targetted | ||
2883 | 46 | thanks to the KEY, ACTION can be: | ||
2884 | 47 | |||
2885 | 48 | These ACTIONs applies to any YAML type: | ||
2886 | 49 | |||
2887 | 50 | get-type ## returns a short string | ||
2888 | 51 | get-value ## returns YAML | ||
2889 | 52 | |||
2890 | 53 | This ACTION applies to 'sequence' and 'struct' YAML type: | ||
2891 | 54 | |||
2892 | 55 | get-values{,-0} ## return list of YAML | ||
2893 | 56 | |||
2894 | 57 | These ACTION applies to 'struct' YAML type: | ||
2895 | 58 | |||
2896 | 59 | keys{,-0} ## return list of YAML | ||
2897 | 60 | values{,-0} ## return list of YAML | ||
2898 | 61 | key-values,{,-0} ## return list of YAML | ||
2899 | 62 | |||
2900 | 63 | Note that any value returned is returned on stdout, and | ||
2901 | 64 | when returning ``list of YAML``, it'll be separated by | ||
2902 | 65 | ``\\n`` or ``NUL`` char depending of you've used the | ||
2903 | 66 | ``-0`` suffixed ACTION. | ||
2904 | 67 | |||
2905 | 68 | KEY Identifier to browse and target subvalues into YAML | ||
2906 | 69 | structure. Use ``.`` to parse a subvalue. If you need | ||
2907 | 70 | to use a literal ``.`` or ``\``, use ``\`` to quote it. | ||
2908 | 71 | |||
2909 | 72 | Use struct keyword to browse ``struct`` YAML data and use | ||
2910 | 73 | integers to browse ``sequence`` YAML data. | ||
2911 | 74 | |||
2912 | 75 | DEFAULT if not provided and given KEY do not match any value in | ||
2913 | 76 | the provided YAML, then DEFAULT will be returned. If no | ||
2914 | 77 | default is provided and the KEY do not match any value | ||
2915 | 78 | in the provided YAML, %(exname)s will fail with an error | ||
2916 | 79 | message. | ||
2917 | 80 | |||
2918 | 81 | Examples: | ||
2919 | 82 | |||
2920 | 83 | ## get last grocery | ||
2921 | 84 | cat recipe.yaml | %(exname)s get-value groceries.-1 | ||
2922 | 85 | |||
2923 | 86 | ## get all words of my french dictionary | ||
2924 | 87 | cat dictionaries.yaml | %(exname)s keys-0 french.dictionary | ||
2925 | 88 | |||
2926 | 89 | ## get YAML config part of 'myhost' | ||
2927 | 90 | cat hosts_config.yaml | %(exname)s get-value cfgs.myhost | ||
2928 | 91 | |||
2929 | 92 | """ % {"exname": EXNAME, "usage": USAGE} | ||
2930 | 93 | |||
2931 | 94 | ## | ||
2932 | 95 | ## Keep previous order in YAML | ||
2933 | 96 | ## | ||
2934 | 97 | |||
2935 | 98 | try: | ||
2936 | 99 | # included in standard lib from Python 2.7 | ||
2937 | 100 | from collections import OrderedDict | ||
2938 | 101 | except ImportError: | ||
2939 | 102 | # try importing the backported drop-in replacement | ||
2940 | 103 | # it's available on PyPI | ||
2941 | 104 | from ordereddict import OrderedDict | ||
2942 | 105 | |||
2943 | 106 | |||
2944 | 107 | ## Ensure that there are no collision with legacy OrderedDict | ||
2945 | 108 | ## that could be used for omap for instance. | ||
2946 | 109 | class MyOrderedDict(OrderedDict): | ||
2947 | 110 | pass | ||
2948 | 111 | |||
2949 | 112 | yaml.add_representer( | ||
2950 | 113 | MyOrderedDict, | ||
2951 | 114 | lambda cls, data: cls.represent_dict(data.items())) | ||
2952 | 115 | |||
2953 | 116 | |||
2954 | 117 | def construct_omap(cls, node): | ||
2955 | 118 | ## Force unfolding reference and merges | ||
2956 | 119 | ## otherwise it would fail on 'merge' | ||
2957 | 120 | cls.flatten_mapping(node) | ||
2958 | 121 | return MyOrderedDict(cls.construct_pairs(node)) | ||
2959 | 122 | |||
2960 | 123 | |||
2961 | 124 | yaml.add_constructor( | ||
2962 | 125 | yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, | ||
2963 | 126 | construct_omap) | ||
2964 | 127 | |||
2965 | 128 | |||
2966 | 129 | ## | ||
2967 | 130 | ## Key specifier | ||
2968 | 131 | ## | ||
2969 | 132 | |||
2970 | 133 | def tokenize(s): | ||
2971 | 134 | r"""Returns an iterable through all subparts of string splitted by '.' | ||
2972 | 135 | |||
2973 | 136 | So: | ||
2974 | 137 | |||
2975 | 138 | >>> list(tokenize('foo.bar.wiz')) | ||
2976 | 139 | ['foo', 'bar', 'wiz'] | ||
2977 | 140 | |||
2978 | 141 | Contrary to traditional ``.split()`` method, this function has to | ||
2979 | 142 | deal with any type of data in the string. So it actually | ||
2980 | 143 | interprets the string. Characters with meaning are '.' and '\'. | ||
2981 | 144 | Both of these can be included in a token by quoting them with '\'. | ||
2982 | 145 | |||
2983 | 146 | So dot of slashes can be contained in token: | ||
2984 | 147 | |||
2985 | 148 | >>> print('\n'.join(tokenize(r'foo.dot<\.>.slash<\\>'))) | ||
2986 | 149 | foo | ||
2987 | 150 | dot<.> | ||
2988 | 151 | slash<\> | ||
2989 | 152 | |||
2990 | 153 | Notice that empty keys are also supported: | ||
2991 | 154 | |||
2992 | 155 | >>> list(tokenize(r'foo..bar')) | ||
2993 | 156 | ['foo', '', 'bar'] | ||
2994 | 157 | |||
2995 | 158 | Given an empty string: | ||
2996 | 159 | |||
2997 | 160 | >>> list(tokenize(r'')) | ||
2998 | 161 | [''] | ||
2999 | 162 | |||
3000 | 163 | And a None value: | ||
3001 | 164 | |||
3002 | 165 | >>> list(tokenize(None)) | ||
3003 | 166 | [] | ||
3004 | 167 | |||
3005 | 168 | """ | ||
3006 | 169 | if s is None: | ||
3007 | 170 | raise StopIteration | ||
3008 | 171 | tokens = (re.sub(r'\\(\\|\.)', r'\1', m.group(0)) | ||
3009 | 172 | for m in re.finditer(r'((\\.|[^.\\])*)', s)) | ||
3010 | 173 | ## an empty string superfluous token is added after all non-empty token | ||
3011 | 174 | for token in tokens: | ||
3012 | 175 | if len(token) != 0: | ||
3013 | 176 | next(tokens) | ||
3014 | 177 | yield token | ||
3015 | 178 | |||
3016 | 179 | |||
3017 | 180 | def mget(dct, key): | ||
3018 | 181 | r"""Allow to get values deep in recursive dict with doted keys | ||
3019 | 182 | |||
3020 | 183 | Accessing leaf values is quite straightforward: | ||
3021 | 184 | |||
3022 | 185 | >>> dct = {'a': {'x': 1, 'b': {'c': 2}}} | ||
3023 | 186 | >>> mget(dct, 'a.x') | ||
3024 | 187 | 1 | ||
3025 | 188 | >>> mget(dct, 'a.b.c') | ||
3026 | 189 | 2 | ||
3027 | 190 | |||
3028 | 191 | But you can also get subdict if your key is not targeting a | ||
3029 | 192 | leaf value: | ||
3030 | 193 | |||
3031 | 194 | >>> mget(dct, 'a.b') | ||
3032 | 195 | {'c': 2} | ||
3033 | 196 | |||
3034 | 197 | As a special feature, list access is also supported by providing a | ||
3035 | 198 | (possibily signed) integer, it'll be interpreted as usual python | ||
3036 | 199 | sequence access using bracket notation: | ||
3037 | 200 | |||
3038 | 201 | >>> mget({'a': {'x': [1, 5], 'b': {'c': 2}}}, 'a.x.-1') | ||
3039 | 202 | 5 | ||
3040 | 203 | >>> mget({'a': {'x': 1, 'b': [{'c': 2}]}}, 'a.b.0.c') | ||
3041 | 204 | 2 | ||
3042 | 205 | |||
3043 | 206 | Keys that contains '.' can be accessed by escaping them: | ||
3044 | 207 | |||
3045 | 208 | >>> dct = {'a': {'x': 1}, 'a.x': 3, 'a.y': 4} | ||
3046 | 209 | >>> mget(dct, 'a.x') | ||
3047 | 210 | 1 | ||
3048 | 211 | >>> mget(dct, r'a\.x') | ||
3049 | 212 | 3 | ||
3050 | 213 | >>> mget(dct, r'a.y') ## doctest: +IGNORE_EXCEPTION_DETAIL | ||
3051 | 214 | Traceback (most recent call last): | ||
3052 | 215 | ... | ||
3053 | 216 | MissingKeyError: missing key 'y' in dict. | ||
3054 | 217 | >>> mget(dct, r'a\.y') | ||
3055 | 218 | 4 | ||
3056 | 219 | |||
3057 | 220 | As a consequence, if your key contains a '\', you should also escape it: | ||
3058 | 221 | |||
3059 | 222 | >>> dct = {r'a\x': 3, r'a\.x': 4, 'a.x': 5, 'a\\': {'x': 6}} | ||
3060 | 223 | >>> mget(dct, r'a\\x') | ||
3061 | 224 | 3 | ||
3062 | 225 | >>> mget(dct, r'a\\\.x') | ||
3063 | 226 | 4 | ||
3064 | 227 | >>> mget(dct, r'a\\.x') | ||
3065 | 228 | 6 | ||
3066 | 229 | >>> mget({'a\\': {'b': 1}}, r'a\\.b') | ||
3067 | 230 | 1 | ||
3068 | 231 | >>> mget({r'a.b\.c': 1}, r'a\.b\\\.c') | ||
3069 | 232 | 1 | ||
3070 | 233 | |||
3071 | 234 | And even empty strings key are supported: | ||
3072 | 235 | |||
3073 | 236 | >>> dct = {r'a': {'': {'y': 3}, 'y': 4}, 'b': {'': {'': 1}}, '': 2} | ||
3074 | 237 | >>> mget(dct, r'a..y') | ||
3075 | 238 | 3 | ||
3076 | 239 | >>> mget(dct, r'a.y') | ||
3077 | 240 | 4 | ||
3078 | 241 | >>> mget(dct, r'') | ||
3079 | 242 | 2 | ||
3080 | 243 | >>> mget(dct, r'b..') | ||
3081 | 244 | 1 | ||
3082 | 245 | |||
3083 | 246 | It will complain if you are trying to get into a leaf: | ||
3084 | 247 | |||
3085 | 248 | >>> mget({'a': 1}, 'a.y') ## doctest: +IGNORE_EXCEPTION_DETAIL | ||
3086 | 249 | Traceback (most recent call last): | ||
3087 | 250 | ... | ||
3088 | 251 | NonDictLikeTypeError: can't query subvalue 'y' of a leaf... | ||
3089 | 252 | |||
3090 | 253 | if the key is None, the whole dct should be sent back: | ||
3091 | 254 | |||
3092 | 255 | >>> mget({'a': 1}, None) | ||
3093 | 256 | {'a': 1} | ||
3094 | 257 | |||
3095 | 258 | """ | ||
3096 | 259 | return aget(dct, tokenize(key)) | ||
3097 | 260 | |||
3098 | 261 | |||
3099 | 262 | class MissingKeyError(KeyError): | ||
3100 | 263 | """Raised when querying a dict-like structure on non-existing keys""" | ||
3101 | 264 | |||
3102 | 265 | def __str__(self): | ||
3103 | 266 | return self.message | ||
3104 | 267 | |||
3105 | 268 | |||
3106 | 269 | class NonDictLikeTypeError(TypeError): | ||
3107 | 270 | """Raised when attempting to traverse non-dict like structure""" | ||
3108 | 271 | |||
3109 | 272 | |||
3110 | 273 | class IndexNotIntegerError(ValueError): | ||
3111 | 274 | """Raised when attempting to traverse sequence without using an integer""" | ||
3112 | 275 | |||
3113 | 276 | |||
3114 | 277 | class IndexOutOfRange(IndexError): | ||
3115 | 278 | """Raised when attempting to traverse sequence without using an integer""" | ||
3116 | 279 | |||
3117 | 280 | |||
3118 | 281 | def aget(dct, key): | ||
3119 | 282 | r"""Allow to get values deep in a dict with iterable keys | ||
3120 | 283 | |||
3121 | 284 | Accessing leaf values is quite straightforward: | ||
3122 | 285 | |||
3123 | 286 | >>> dct = {'a': {'x': 1, 'b': {'c': 2}}} | ||
3124 | 287 | >>> aget(dct, ('a', 'x')) | ||
3125 | 288 | 1 | ||
3126 | 289 | >>> aget(dct, ('a', 'b', 'c')) | ||
3127 | 290 | 2 | ||
3128 | 291 | |||
3129 | 292 | If key is empty, it returns unchanged the ``dct`` value. | ||
3130 | 293 | |||
3131 | 294 | >>> aget({'x': 1}, ()) | ||
3132 | 295 | {'x': 1} | ||
3133 | 296 | |||
3134 | 297 | """ | ||
3135 | 298 | key = iter(key) | ||
3136 | 299 | try: | ||
3137 | 300 | head = next(key) | ||
3138 | 301 | except StopIteration: | ||
3139 | 302 | return dct | ||
3140 | 303 | |||
3141 | 304 | if isinstance(dct, list): | ||
3142 | 305 | try: | ||
3143 | 306 | idx = int(head) | ||
3144 | 307 | except ValueError: | ||
3145 | 308 | raise IndexNotIntegerError( | ||
3146 | 309 | "non-integer index %r provided on a list." | ||
3147 | 310 | % head) | ||
3148 | 311 | try: | ||
3149 | 312 | value = dct[idx] | ||
3150 | 313 | except IndexError: | ||
3151 | 314 | raise IndexOutOfRange( | ||
3152 | 315 | "index %d is out of range (%d elements in list)." | ||
3153 | 316 | % (idx, len(dct))) | ||
3154 | 317 | else: | ||
3155 | 318 | try: | ||
3156 | 319 | value = dct[head] | ||
3157 | 320 | except KeyError: | ||
3158 | 321 | ## Replace with a more informative KeyError | ||
3159 | 322 | raise MissingKeyError( | ||
3160 | 323 | "missing key %r in dict." | ||
3161 | 324 | % (head, )) | ||
3162 | 325 | except: | ||
3163 | 326 | raise NonDictLikeTypeError( | ||
3164 | 327 | "can't query subvalue %r of a leaf%s." | ||
3165 | 328 | % (head, | ||
3166 | 329 | (" (leaf value is %r)" % dct) | ||
3167 | 330 | if len(repr(dct)) < 15 else "")) | ||
3168 | 331 | return aget(value, key) | ||
3169 | 332 | |||
3170 | 333 | |||
3171 | 334 | def stderr(msg): | ||
3172 | 335 | """Convenience function to write short message to stderr.""" | ||
3173 | 336 | sys.stderr.write(msg) | ||
3174 | 337 | |||
3175 | 338 | |||
3176 | 339 | def stdout(value): | ||
3177 | 340 | """Convenience function to write short message to stdout.""" | ||
3178 | 341 | sys.stdout.write(value) | ||
3179 | 342 | |||
3180 | 343 | |||
3181 | 344 | def die(msg, errlvl=1, prefix="Error: "): | ||
3182 | 345 | """Convenience function to write short message to stderr and quit.""" | ||
3183 | 346 | stderr("%s%s\n" % (prefix, msg)) | ||
3184 | 347 | sys.exit(errlvl) | ||
3185 | 348 | |||
3186 | 349 | SIMPLE_TYPES = (str if PY3 else basestring, int, float, type(None)) | ||
3187 | 350 | COMPLEX_TYPES = (list, dict) | ||
3188 | 351 | |||
3189 | 352 | |||
3190 | 353 | def magic_dump(value): | ||
3191 | 354 | """Returns a representation of values directly usable by bash. | ||
3192 | 355 | |||
3193 | 356 | Literal types are printed as-is (avoiding quotes around string for | ||
3194 | 357 | instance). But complex type are written in a YAML useable format. | ||
3195 | 358 | |||
3196 | 359 | """ | ||
3197 | 360 | return value if isinstance(value, SIMPLE_TYPES) \ | ||
3198 | 361 | else yaml.dump(value, default_flow_style=False) | ||
3199 | 362 | |||
3200 | 363 | def yaml_dump(value): | ||
3201 | 364 | """Returns a representation of values directly usable by bash. | ||
3202 | 365 | |||
3203 | 366 | Literal types are quoted and safe to use as YAML. | ||
3204 | 367 | |||
3205 | 368 | """ | ||
3206 | 369 | return yaml.dump(value, default_flow_style=False) | ||
3207 | 370 | |||
3208 | 371 | |||
3209 | 372 | def type_name(value): | ||
3210 | 373 | """Returns pseudo-YAML type name of given value.""" | ||
3211 | 374 | return "struct" if isinstance(value, dict) else \ | ||
3212 | 375 | "sequence" if isinstance(value, (tuple, list)) else \ | ||
3213 | 376 | type(value).__name__ | ||
3214 | 377 | |||
3215 | 378 | |||
3216 | 379 | def main(args): ## pylint: disable=too-many-branches | ||
3217 | 380 | """Entrypoint of the whole application""" | ||
3218 | 381 | |||
3219 | 382 | if len(args) == 0: | ||
3220 | 383 | stderr("Error: Bad number of arguments.\n") | ||
3221 | 384 | die(USAGE, errlvl=1, prefix="") | ||
3222 | 385 | |||
3223 | 386 | if len(args) == 1 and args[0] in ("-h", "--help"): | ||
3224 | 387 | stdout(HELP) | ||
3225 | 388 | exit(0) | ||
3226 | 389 | |||
3227 | 390 | dump = magic_dump | ||
3228 | 391 | for arg in ["-y", "--yaml"]: | ||
3229 | 392 | if arg in args: | ||
3230 | 393 | args.remove(arg) | ||
3231 | 394 | dump = yaml_dump | ||
3232 | 395 | |||
3233 | 396 | action = args[0] | ||
3234 | 397 | key_value = None if len(args) == 1 else args[1] | ||
3235 | 398 | default = args[2] if len(args) > 2 else None | ||
3236 | 399 | contents = yaml.load(sys.stdin) | ||
3237 | 400 | |||
3238 | 401 | try: | ||
3239 | 402 | try: | ||
3240 | 403 | value = mget(contents, key_value) | ||
3241 | 404 | except (IndexOutOfRange, MissingKeyError): | ||
3242 | 405 | if default is None: | ||
3243 | 406 | raise | ||
3244 | 407 | value = default | ||
3245 | 408 | except (IndexOutOfRange, MissingKeyError, | ||
3246 | 409 | NonDictLikeTypeError, IndexNotIntegerError) as exc: | ||
3247 | 410 | msg = str(exc.message) | ||
3248 | 411 | die("invalid path %r, %s" | ||
3249 | 412 | % (key_value, | ||
3250 | 413 | msg.replace('list', 'sequence').replace('dict', 'struct'))) | ||
3251 | 414 | |||
3252 | 415 | tvalue = type_name(value) | ||
3253 | 416 | termination = "\0" if action.endswith("-0") else "\n" | ||
3254 | 417 | |||
3255 | 418 | if action == "get-value": | ||
3256 | 419 | print(dump(value), end='') | ||
3257 | 420 | elif action in ("get-values", "get-values-0"): | ||
3258 | 421 | if isinstance(value, dict): | ||
3259 | 422 | for k, v in value.iteritems(): | ||
3260 | 423 | stdout("%s%s%s%s" % (dump(k), termination, | ||
3261 | 424 | dump(v), termination)) | ||
3262 | 425 | elif isinstance(value, list): | ||
3263 | 426 | for l in value: | ||
3264 | 427 | stdout("%s%s" % (dump(l), termination)) | ||
3265 | 428 | else: | ||
3266 | 429 | die("%s does not support %r type. " | ||
3267 | 430 | "Please provide or select a sequence or struct." | ||
3268 | 431 | % (action, tvalue)) | ||
3269 | 432 | elif action == "get-type": | ||
3270 | 433 | print(tvalue) | ||
3271 | 434 | elif action in ("keys", "keys-0", | ||
3272 | 435 | "values", "values-0", | ||
3273 | 436 | "key-values", "key-values-0"): | ||
3274 | 437 | if isinstance(value, dict): | ||
3275 | 438 | method = value.keys if action.startswith("keys") else \ | ||
3276 | 439 | value.items if action.startswith("key-values") else \ | ||
3277 | 440 | value.values | ||
3278 | 441 | output = (lambda x: termination.join(str(dump(e)) for e in x)) \ | ||
3279 | 442 | if action.startswith("key-values") else \ | ||
3280 | 443 | dump | ||
3281 | 444 | for k in method(): | ||
3282 | 445 | stdout("%s%s" % (output(k), termination)) | ||
3283 | 446 | else: | ||
3284 | 447 | die("%s does not support %r type. " | ||
3285 | 448 | "Please provide or select a struct." % (action, tvalue)) | ||
3286 | 449 | else: | ||
3287 | 450 | die("Invalid argument.\n%s" % USAGE) | ||
3288 | 451 | |||
3289 | 452 | |||
3290 | 453 | if __name__ == "__main__": | ||
3291 | 454 | sys.exit(main(sys.argv[1:])) | ||
3292 | diff --git a/tools/snapbuild.sh b/tools/snapbuild.sh | |||
3293 | 0 | new file mode 100755 | 455 | new file mode 100755 |
3294 | index 0000000..53b4518 | |||
3295 | --- /dev/null | |||
3296 | +++ b/tools/snapbuild.sh | |||
3297 | @@ -0,0 +1,192 @@ | |||
3298 | 1 | #!/bin/sh | ||
3299 | 2 | # | ||
3300 | 3 | # Copyright (C) 2017 Canonical Ltd | ||
3301 | 4 | # | ||
3302 | 5 | # This program is free software: you can redistribute it and/or modify | ||
3303 | 6 | # it under the terms of the GNU General Public License version 3 as | ||
3304 | 7 | # published by the Free Software Foundation. | ||
3305 | 8 | # | ||
3306 | 9 | # This program is distributed in the hope that it will be useful, | ||
3307 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
3308 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
3309 | 12 | # GNU General Public License for more details. | ||
3310 | 13 | # | ||
3311 | 14 | # You should have received a copy of the GNU General Public License | ||
3312 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
3313 | 16 | |||
3314 | 17 | set -ex | ||
3315 | 18 | |||
3316 | 19 | if [ "$(id -u)" -ne 0 ]; then | ||
3317 | 20 | echo "ERROR: You have to run this script as root!" | ||
3318 | 21 | exit 1 | ||
3319 | 22 | fi | ||
3320 | 23 | |||
3321 | 24 | SERIES=xenial | ||
3322 | 25 | SOURCE_DIR= | ||
3323 | 26 | RESULTS_DIR= | ||
3324 | 27 | # Whenever you change the chroot in a way which needs a regeneration | ||
3325 | 28 | # on the build server bump the version here. This will tell the | ||
3326 | 29 | # job which updates the chroots to generate a new one. | ||
3327 | 30 | CHROOT_VERSION=1 | ||
3328 | 31 | BUILD_ARCH=amd64 | ||
3329 | 32 | TARGET_ARCH=amd64 | ||
3330 | 33 | UPDATE_CHROOT=false | ||
3331 | 34 | PROXY= | ||
3332 | 35 | CROSS_BUILD=false | ||
3333 | 36 | SNAPCRAFT_EXTRA_ARGS= | ||
3334 | 37 | |||
3335 | 38 | while [ -n "$1" ]; do | ||
3336 | 39 | case "$1" in | ||
3337 | 40 | --series=*) | ||
3338 | 41 | SERIES=${1#*=} | ||
3339 | 42 | shift | ||
3340 | 43 | ;; | ||
3341 | 44 | --source-dir=*) | ||
3342 | 45 | SOURCE_DIR=${1#*=} | ||
3343 | 46 | shift | ||
3344 | 47 | ;; | ||
3345 | 48 | --results-dir=*) | ||
3346 | 49 | RESULTS_DIR=${1#*=} | ||
3347 | 50 | shift | ||
3348 | 51 | ;; | ||
3349 | 52 | --arch=*) | ||
3350 | 53 | TARGET_ARCH=${1#*=} | ||
3351 | 54 | BUILD_ARCH=$TARGET_ARCH | ||
3352 | 55 | shift | ||
3353 | 56 | ;; | ||
3354 | 57 | --update-chroot) | ||
3355 | 58 | UPDATE_CHROOT=true | ||
3356 | 59 | shift | ||
3357 | 60 | ;; | ||
3358 | 61 | --proxy=*) | ||
3359 | 62 | PROXY=${1#*=} | ||
3360 | 63 | shift | ||
3361 | 64 | ;; | ||
3362 | 65 | --cross-build) | ||
3363 | 66 | CROSS_BUILD=true | ||
3364 | 67 | shift | ||
3365 | 68 | ;; | ||
3366 | 69 | *) | ||
3367 | 70 | echo "ERROR: Unknown options $1" | ||
3368 | 71 | exit 1 | ||
3369 | 72 | esac | ||
3370 | 73 | done | ||
3371 | 74 | |||
3372 | 75 | if [ -z "$SERIES" ]; then | ||
3373 | 76 | echo "ERROR: No series specified" | ||
3374 | 77 | exit 1 | ||
3375 | 78 | fi | ||
3376 | 79 | |||
3377 | 80 | # If we're cross-building we have to switch the architecture we're using | ||
3378 | 81 | # for our build environment to match our host arch. | ||
3379 | 82 | if [ "$CROSS_BUILD" = true ]; then | ||
3380 | 83 | BUILD_ARCH=$(dpkg --print-architecture) | ||
3381 | 84 | fi | ||
3382 | 85 | |||
3383 | 86 | CHROOT_STORE_PATH=/build/chroots | ||
3384 | 87 | CHROOT_TARBALL=$SERIES-$BUILD_ARCH-$CHROOT_VERSION-rootfs.tar | ||
3385 | 88 | |||
3386 | 89 | if [ "$UPDATE_CHROOT" = true ]; then | ||
3387 | 90 | if [ ! -e $CHROOT_STORE_PATH/$CHROOT_TARBALL ] ; then | ||
3388 | 91 | mkdir -p /build/chroots | ||
3389 | 92 | WORKDIR=$(mktemp -d) | ||
3390 | 93 | mkdir -p $WORKDIR/rootfs | ||
3391 | 94 | |||
3392 | 95 | DEBOOTSTRAP=debootstrap | ||
3393 | 96 | DEB_REPO_URL= | ||
3394 | 97 | case "$BUILD_ARCH" in | ||
3395 | 98 | amd64) | ||
3396 | 99 | DEB_REPO_URL="http://archive.ubuntu.com/ubuntu/" | ||
3397 | 100 | ;; | ||
3398 | 101 | armhf) | ||
3399 | 102 | DEBOOTSTRAP=qemu-debootstrap | ||
3400 | 103 | DEB_REPO_URL="http://ports.ubuntu.com/ubuntu-ports" | ||
3401 | 104 | ;; | ||
3402 | 105 | *) | ||
3403 | 106 | echo "ERROR: Unsupported architecture $BUILD_ARCH" | ||
3404 | 107 | exit 1 | ||
3405 | 108 | ;; | ||
3406 | 109 | esac | ||
3407 | 110 | |||
3408 | 111 | cleanup() { | ||
3409 | 112 | rm -rf $WORKDIR | ||
3410 | 113 | } | ||
3411 | 114 | |||
3412 | 115 | trap cleanup INT EXIT | ||
3413 | 116 | |||
3414 | 117 | $DEBOOTSTRAP --components=main,universe --arch $BUILD_ARCH $SERIES $WORKDIR/rootfs | ||
3415 | 118 | cat << EOF > $WORKDIR/rootfs/etc/apt/sources.list.d/updates.list | ||
3416 | 119 | deb $DEB_REPO_URL xenial universe | ||
3417 | 120 | deb $DEB_REPO_URL xenial-updates main universe | ||
3418 | 121 | EOF | ||
3419 | 122 | cat << EOF > $WORKDIR/rootfs/setup.sh | ||
3420 | 123 | #!/bin/sh | ||
3421 | 124 | set -ex | ||
3422 | 125 | apt update | ||
3423 | 126 | apt upgrade -y | ||
3424 | 127 | apt install -y snapcraft | ||
3425 | 128 | EOF | ||
3426 | 129 | chmod +x $WORKDIR/rootfs/setup.sh | ||
3427 | 130 | sudo chroot $WORKDIR/rootfs /setup.sh | ||
3428 | 131 | rm $WORKDIR/rootfs/setup.sh | ||
3429 | 132 | |||
3430 | 133 | (cd $WORKDIR/rootfs; tar cf $CHROOT_STORE_PATH/$CHROOT_TARBALL .) | ||
3431 | 134 | rm -rf $WORKDIR | ||
3432 | 135 | fi | ||
3433 | 136 | |||
3434 | 137 | exit 0 | ||
3435 | 138 | fi | ||
3436 | 139 | |||
3437 | 140 | if [ -z "$SOURCE_DIR" ]; then | ||
3438 | 141 | echo "ERROR: No source dir specified" | ||
3439 | 142 | exit 1 | ||
3440 | 143 | fi | ||
3441 | 144 | |||
3442 | 145 | if [ -z "$RESULTS_DIR" ]; then | ||
3443 | 146 | echo "ERROR: No results dir specified" | ||
3444 | 147 | exit 1 | ||
3445 | 148 | fi | ||
3446 | 149 | |||
3447 | 150 | BUILDDIR=$(mktemp -d) | ||
3448 | 151 | |||
3449 | 152 | cleanup() { | ||
3450 | 153 | rm -rf $BUILDDIR | ||
3451 | 154 | } | ||
3452 | 155 | |||
3453 | 156 | trap cleanup INT EXIT | ||
3454 | 157 | |||
3455 | 158 | if [ ! -e $CHROOT_STORE_PATH/$CHROOT_TARBALL ] ; then | ||
3456 | 159 | echo "ERROR: Up to date chroot tarball doesn't exist. Please run the snap-build-update-chroot job!" | ||
3457 | 160 | exit 1 | ||
3458 | 161 | fi | ||
3459 | 162 | |||
3460 | 163 | tar xf $CHROOT_STORE_PATH/$CHROOT_TARBALL -C $BUILDDIR | ||
3461 | 164 | |||
3462 | 165 | cp -ra $SOURCE_DIR $BUILDDIR/src | ||
3463 | 166 | |||
3464 | 167 | if [ "$CROSS_BUILD" = true ]; then | ||
3465 | 168 | SNAPCRAFT_EXTRA_ARGS="$SNAPCRAFT_EXTRA_ARGS --target-arch=$TARGET_ARCH" | ||
3466 | 169 | fi | ||
3467 | 170 | |||
3468 | 171 | cat << EOF > $BUILDDIR/do-build.sh | ||
3469 | 172 | #!/bin/sh | ||
3470 | 173 | set -ex | ||
3471 | 174 | apt update | ||
3472 | 175 | apt upgrade -y | ||
3473 | 176 | cd /src | ||
3474 | 177 | |||
3475 | 178 | # snapcraft is pretty unhappy when LC_ALL and LANG aren't set | ||
3476 | 179 | export LC_ALL=C.UTF-8 | ||
3477 | 180 | export LANG=C.UTF-8 | ||
3478 | 181 | |||
3479 | 182 | # To access certain things like the snap store we need a proxy in place | ||
3480 | 183 | # in some environments | ||
3481 | 184 | export http_proxy=$PROXY | ||
3482 | 185 | export https_proxy=$PROXY | ||
3483 | 186 | |||
3484 | 187 | snapcraft clean | ||
3485 | 188 | snapcraft $SNAPCRAFT_EXTRA_ARGS | ||
3486 | 189 | EOF | ||
3487 | 190 | chmod +x $BUILDDIR/do-build.sh | ||
3488 | 191 | |||
3489 | 192 | sudo chroot $BUILDDIR /do-build.sh | ||
3490 | diff --git a/tools/test-snap.sh b/tools/test-snap.sh | |||
3491 | 0 | new file mode 100755 | 193 | new file mode 100755 |
3492 | index 0000000..4851645 | |||
3493 | --- /dev/null | |||
3494 | +++ b/tools/test-snap.sh | |||
3495 | @@ -0,0 +1,100 @@ | |||
3496 | 1 | #!/bin/sh -ex | ||
3497 | 2 | # | ||
3498 | 3 | # Copyright (C) 2017 Canonical Ltd | ||
3499 | 4 | # | ||
3500 | 5 | # This program is free software: you can redistribute it and/or modify | ||
3501 | 6 | # it under the terms of the GNU General Public License version 3 as | ||
3502 | 7 | # published by the Free Software Foundation. | ||
3503 | 8 | # | ||
3504 | 9 | # This program is distributed in the hope that it will be useful, | ||
3505 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
3506 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
3507 | 12 | # GNU General Public License for more details. | ||
3508 | 13 | # | ||
3509 | 14 | # You should have received a copy of the GNU General Public License | ||
3510 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
3511 | 16 | |||
3512 | 17 | # Import used functions | ||
3513 | 18 | . "$WORKSPACE"/build-scripts/scripts/hardware-test.sh | ||
3514 | 19 | |||
3515 | 20 | # Runs snap tests | ||
3516 | 21 | run_snap_tests () | ||
3517 | 22 | { | ||
3518 | 23 | if [ -n "$RESULTS_ID" ]; then | ||
3519 | 24 | $SSH mkdir -p "$REMOTE_WORKSPACE"/results | ||
3520 | 25 | $SSH cp -v "$REMOTE_RESULTS_BASE_DIR"/"$RESULTS_ID"/*.snap "$REMOTE_WORKSPACE"/results | ||
3521 | 26 | fi | ||
3522 | 27 | |||
3523 | 28 | $SSH sudo apt-get --yes --force-yes install docker.io | ||
3524 | 29 | |||
3525 | 30 | cat << EOF > "$WORKSPACE"/run-tests.sh | ||
3526 | 31 | #!/bin/sh | ||
3527 | 32 | set -ex | ||
3528 | 33 | |||
3529 | 34 | export TERM=linux | ||
3530 | 35 | export DEBIAN_FRONTEND=noninteractive | ||
3531 | 36 | export PATH=/build/bin:$PATH | ||
3532 | 37 | |||
3533 | 38 | # At this time it's necessary to build spread manually because | ||
3534 | 39 | # the snapped version does not include the qemu/kvm backend. | ||
3535 | 40 | # Once the snapped version includes this backend, then we can | ||
3536 | 41 | # change the manual building of spread with making sure the snap | ||
3537 | 42 | # package is installed. | ||
3538 | 43 | export GOPATH=$(mktemp -d) | ||
3539 | 44 | go get -d -v github.com/snapcore/spread/... | ||
3540 | 45 | go build github.com/snapcore/spread/cmd/spread | ||
3541 | 46 | mkdir /build/bin | ||
3542 | 47 | cp spread /build/bin | ||
3543 | 48 | |||
3544 | 49 | git clone --depth 1 -b $BRANCH $REPO /build/src | ||
3545 | 50 | cd /build/src | ||
3546 | 51 | |||
3547 | 52 | # Copy any stage results from previous generic-build-snap-worker builds | ||
3548 | 53 | if [ "\$(find /build/results -path ./misc -prune -o -name '*.snap' -print | wc -l)" -gt 0 ]; then | ||
3549 | 54 | cp -v /build/results/*.snap /build/src | ||
3550 | 55 | fi | ||
3551 | 56 | |||
3552 | 57 | if [ -e "run-tests.sh" ] ; then | ||
3553 | 58 | EXTRA_ARGS= | ||
3554 | 59 | if [ -n "$CHANNEL" ] ; then | ||
3555 | 60 | # If CHANNEL is specified we use that channel for image construction and | ||
3556 | 61 | # also load the snap from that channel for testing. | ||
3557 | 62 | EXTRA_ARGS="--test-from-channel=$CHANNEL" | ||
3558 | 63 | fi | ||
3559 | 64 | if [ -n "$CORE_CHANNEL" ]; then | ||
3560 | 65 | EXTRA_ARGS="--channel=$CORE_CHANNEL" | ||
3561 | 66 | fi | ||
3562 | 67 | |||
3563 | 68 | ./run-tests.sh --force-new-image \$EXTRA_ARGS | ||
3564 | 69 | else | ||
3565 | 70 | if [ ! -z "$CHANNEL" ] ; then | ||
3566 | 71 | SNAP_CHANNEL=$CHANNEL spread -v | ||
3567 | 72 | else | ||
3568 | 73 | spread -v | ||
3569 | 74 | fi | ||
3570 | 75 | fi | ||
3571 | 76 | EOF | ||
3572 | 77 | |||
3573 | 78 | $SSH mkdir -p "$REMOTE_WORKSPACE" | ||
3574 | 79 | $SCP "$WORKSPACE"/run-tests.sh "$REMOTE_USER"@"$REMOTE_WORKER":"$REMOTE_WORKSPACE" | ||
3575 | 80 | $SSH chmod u+x "$REMOTE_WORKSPACE"/run-tests.sh | ||
3576 | 81 | |||
3577 | 82 | $SSH mkdir -p "$REMOTE_WORKSPACE"/docker | ||
3578 | 83 | $SCP "$WORKSPACE"/build-scripts/docker/spread-tests/Dockerfile \ | ||
3579 | 84 | "$REMOTE_USER"@"$REMOTE_WORKER":"$REMOTE_WORKSPACE"/docker | ||
3580 | 85 | $SSH time sudo docker build -t snap-spread-tests "$REMOTE_WORKSPACE"/docker | ||
3581 | 86 | |||
3582 | 87 | $SSH time sudo docker run \ | ||
3583 | 88 | --rm \ | ||
3584 | 89 | -v /dev:/dev \ | ||
3585 | 90 | -v "$REMOTE_WORKSPACE":/build \ | ||
3586 | 91 | --privileged \ | ||
3587 | 92 | snap-spread-tests /build/run-tests.sh | ||
3588 | 93 | |||
3589 | 94 | $SSH sudo rm -rf "$REMOTE_WORKSPACE" | ||
3590 | 95 | |||
3591 | 96 | # Now run tests on real hardware if defined in the backend | ||
3592 | 97 | if [ "$HW_TESTS_RESULT" -eq 0 ] ; then | ||
3593 | 98 | run_hardware_tests | ||
3594 | 99 | fi | ||
3595 | 100 | } | ||
3596 | diff --git a/tools/trigger-ci.py b/tools/trigger-ci.py | |||
3597 | 0 | new file mode 100755 | 101 | new file mode 100755 |
3598 | index 0000000..d50c021 | |||
3599 | --- /dev/null | |||
3600 | +++ b/tools/trigger-ci.py | |||
3601 | @@ -0,0 +1,270 @@ | |||
3602 | 1 | #!/usr/bin/env python | ||
3603 | 2 | # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- | ||
3604 | 3 | # | ||
3605 | 4 | # Copyright (C) 2016 Canonical Ltd | ||
3606 | 5 | # | ||
3607 | 6 | # This program is free software: you can redistribute it and/or modify | ||
3608 | 7 | # it under the terms of the GNU General Public License version 3 as | ||
3609 | 8 | # published by the Free Software Foundation. | ||
3610 | 9 | # | ||
3611 | 10 | # This program is distributed in the hope that it will be useful, | ||
3612 | 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
3613 | 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
3614 | 13 | # GNU General Public License for more details. | ||
3615 | 14 | # | ||
3616 | 15 | # You should have received a copy of the GNU General Public License | ||
3617 | 16 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
3618 | 17 | |||
3619 | 18 | from launchpadlib.launchpad import Launchpad | ||
3620 | 19 | import jenkins | ||
3621 | 20 | import os | ||
3622 | 21 | import sys | ||
3623 | 22 | import yaml | ||
3624 | 23 | import re | ||
3625 | 24 | from jlp import launchpadutils | ||
3626 | 25 | from jlp import jenkinsutils | ||
3627 | 26 | |||
3628 | 27 | from argparse import ArgumentParser | ||
3629 | 28 | |||
3630 | 29 | import se_utils | ||
3631 | 30 | |||
3632 | 31 | parser = ArgumentParser(description="Trigger snap builds for pending merge proposals") | ||
3633 | 32 | parser.add_argument('-p', '--project', required=True, | ||
3634 | 33 | help="Launchpad project to check for new merge-proposals") | ||
3635 | 34 | parser.add_argument('-j', '--job', required=True, | ||
3636 | 35 | help="Jenkins job to trigger") | ||
3637 | 36 | parser.add_argument('-t', '--team', required=True, | ||
3638 | 37 | help="Launchpad team for the project") | ||
3639 | 38 | |||
3640 | 39 | args = vars(parser.parse_args()) | ||
3641 | 40 | |||
3642 | 41 | lp_app = se_utils.get_config_option("lp_app") | ||
3643 | 42 | lp_env = se_utils.get_config_option("lp_env") | ||
3644 | 43 | credential_store_path = se_utils.get_config_option('credential_store_path') | ||
3645 | 44 | launchpad = se_utils.get_launchpad(None, credential_store_path, lp_app, lp_env) | ||
3646 | 45 | |||
3647 | 46 | project = launchpad.projects[args['project']] | ||
3648 | 47 | team = args['team'] | ||
3649 | 48 | |||
3650 | 49 | proposals = project.getMergeProposals() | ||
3651 | 50 | |||
3652 | 51 | def load_config(): | ||
3653 | 52 | files = [os.path.expanduser('~/.jlp/jlp.config'), 'jlp.config'] | ||
3654 | 53 | for config_file in files: | ||
3655 | 54 | try: | ||
3656 | 55 | config = yaml.safe_load(open(config_file, 'r')) | ||
3657 | 56 | return config | ||
3658 | 57 | except IOError: | ||
3659 | 58 | pass | ||
3660 | 59 | print("ERROR: No config file found") | ||
3661 | 60 | sys.exit(1) | ||
3662 | 61 | |||
3663 | 62 | def get_config_option(name): | ||
3664 | 63 | config = load_config() | ||
3665 | 64 | return config[name] | ||
3666 | 65 | |||
3667 | 66 | j = jenkins.Jenkins(get_config_option('jenkins_url'), | ||
3668 | 67 | get_config_option('jenkins_user'), | ||
3669 | 68 | get_config_option('jenkins_password')) | ||
3670 | 69 | |||
3671 | 70 | jenkins_job = args['job'] | ||
3672 | 71 | jenkins_build_token = get_config_option('jenkins_build_token') | ||
3673 | 72 | |||
3674 | 73 | job_info = j.get_job_info(jenkins_job) | ||
3675 | 74 | if not job_info['buildable']: | ||
3676 | 75 | print("ERROR: Job is not buildable (propably disabled)") | ||
3677 | 76 | sys.exit(1) | ||
3678 | 77 | |||
3679 | 78 | |||
3680 | 79 | def get_latest_revision(mp): | ||
3681 | 80 | """Return the latest revision of the given merge proposal. | ||
3682 | 81 | |||
3683 | 82 | :param mp: handle to merge proposal | ||
3684 | 83 | """ | ||
3685 | 84 | if '+git' in mp.web_link: | ||
3686 | 85 | for ref in mp.source_git_repository.refs_collection: | ||
3687 | 86 | if ref.path == mp.source_git_path: | ||
3688 | 87 | return ref.commit_sha1 | ||
3689 | 88 | return str(0) | ||
3690 | 89 | else: | ||
3691 | 90 | return mp.source_branch.revision_count | ||
3692 | 91 | |||
3693 | 92 | def clean_branch_name(branch_name): | ||
3694 | 93 | if branch_name.startswith("refs/heads/"): | ||
3695 | 94 | return branch_name[11:] | ||
3696 | 95 | return branch_name | ||
3697 | 96 | |||
3698 | 97 | def series_from_branch_name(branch_name): | ||
3699 | 98 | if branch_name.startswith("vivid/"): | ||
3700 | 99 | return "vivid" | ||
3701 | 100 | return "xenial" | ||
3702 | 101 | |||
3703 | 102 | def get_latest_revision(mp): | ||
3704 | 103 | """Return the latest revision of the given merge proposal. | ||
3705 | 104 | |||
3706 | 105 | :param mp: handle to merge proposal | ||
3707 | 106 | """ | ||
3708 | 107 | if '+git' in mp.web_link: | ||
3709 | 108 | for ref in mp.source_git_repository.refs_collection: | ||
3710 | 109 | if ref.path == mp.source_git_path: | ||
3711 | 110 | return ref.commit_sha1 | ||
3712 | 111 | return str(0) | ||
3713 | 112 | else: | ||
3714 | 113 | return mp.source_branch.revision_count | ||
3715 | 114 | |||
3716 | 115 | def get_review_revision_regex(mp): | ||
3717 | 116 | if '+git' in mp.web_link: | ||
3718 | 117 | return '^(PASSED|FAILED): Continuous integration, rev:([0-9a-f]+)' | ||
3719 | 118 | else: | ||
3720 | 119 | return '^(PASSED|FAILED): Continuous integration, rev:(\d+)' | ||
3721 | 120 | |||
3722 | 121 | def get_latest_review(launchpad_user, mp): | ||
3723 | 122 | """Return the latest revision reviewed by the given launchpad_user. | ||
3724 | 123 | |||
3725 | 124 | This function expects a review comment in the following format: | ||
3726 | 125 | '^(PASSED|FAILED): Continuous integration, rev:(\d+)' | ||
3727 | 126 | |||
3728 | 127 | :param launchpad_user: handle to launchpad user | ||
3729 | 128 | :param mp: handle to merge proposal | ||
3730 | 129 | """ | ||
3731 | 130 | revision = 0 | ||
3732 | 131 | launchpad_review_type = get_config_option('launchpad_review_type') | ||
3733 | 132 | for comment in mp.all_comments: | ||
3734 | 133 | if comment.author.name == launchpad_user.name: | ||
3735 | 134 | if comment.vote_tag == launchpad_review_type: | ||
3736 | 135 | m = re.search( | ||
3737 | 136 | get_review_revision_regex(mp), | ||
3738 | 137 | comment.message_body) | ||
3739 | 138 | if m: | ||
3740 | 139 | revision = m.group(2) | ||
3741 | 140 | return revision | ||
3742 | 141 | |||
3743 | 142 | def latest_candidate_validated(launchpad_user, mp): | ||
3744 | 143 | """Return if the latest candidate revision of the merge proposal is | ||
3745 | 144 | validated. | ||
3746 | 145 | |||
3747 | 146 | :param launchpad_user: handle to launchpad user used to validate the | ||
3748 | 147 | merge proposals | ||
3749 | 148 | :param mp: handle to merge proposal | ||
3750 | 149 | """ | ||
3751 | 150 | |||
3752 | 151 | latest_review = get_latest_review(launchpad_user, mp) | ||
3753 | 152 | print 'Latest review is revision: ' + str(latest_review) | ||
3754 | 153 | latest_revision = get_latest_revision(mp) | ||
3755 | 154 | print 'Latest revision is: ' + str(latest_revision) | ||
3756 | 155 | if latest_review == latest_revision: | ||
3757 | 156 | print 'Skipping this MP. Current revision: ' + str(latest_revision) | ||
3758 | 157 | return True | ||
3759 | 158 | return False | ||
3760 | 159 | |||
3761 | 160 | # NOTE: We can't use the testing_in_progress method from jlp as it checks for | ||
3762 | 161 | # the lower case parameter 'merge_proposal' of the build job and ours is using | ||
3763 | 162 | # the upper case variant 'MERGE_PROPOSAL'. | ||
3764 | 163 | def testing_in_progress(mp, jenkins_job): | ||
3765 | 164 | try: | ||
3766 | 165 | if jenkinsutils.is_job_or_downstream_building( | ||
3767 | 166 | jenkins_job, job_params={'MERGE_PROPOSAL': mp.web_link}): | ||
3768 | 167 | print('Skipping this MP. It is currently being tested by Jenkins.') | ||
3769 | 168 | return True | ||
3770 | 169 | except: | ||
3771 | 170 | print('Failed to check if MP is already building') | ||
3772 | 171 | return False | ||
3773 | 172 | |||
3774 | 173 | # Copied over from lp:jenkins-launchpad-plugin | ||
3775 | 174 | def users_in_team(users, team): | ||
3776 | 175 | """Determine whether any of these users are in the supplied team. | ||
3777 | 176 | |||
3778 | 177 | :param users: The users which may be members of the supplied team. | ||
3779 | 178 | :param team: The team which users may be part of. | ||
3780 | 179 | :return: True if any of the users are members of the team, otherwise | ||
3781 | 180 | False. | ||
3782 | 181 | """ | ||
3783 | 182 | for member in team.participants: | ||
3784 | 183 | if member in users: | ||
3785 | 184 | return True | ||
3786 | 185 | else: | ||
3787 | 186 | return False | ||
3788 | 187 | |||
3789 | 188 | # Copied over from lp:jenkins-launchpad-plugin and slightly modified | ||
3790 | 189 | def users_allowed_to_trigger_jobs(lp_users, allowed_people): | ||
3791 | 190 | """Returns if an of the given users is allowed to run jobs on jenkins. | ||
3792 | 191 | |||
3793 | 192 | This is to avoid random people to run jobs on our internal infrastructure. | ||
3794 | 193 | A user is allowed if they are either directly in the ALLOWED_USERS list or | ||
3795 | 194 | are member of a team in that list. | ||
3796 | 195 | |||
3797 | 196 | :param lp_users: launchpad user handles | ||
3798 | 197 | """ | ||
3799 | 198 | if len(lp_users) == 0: | ||
3800 | 199 | return False | ||
3801 | 200 | for lp_user in lp_users: | ||
3802 | 201 | if lp_user.name in allowed_people: | ||
3803 | 202 | return True | ||
3804 | 203 | lp = lp_users[0]._root | ||
3805 | 204 | for allowed in allowed_people: | ||
3806 | 205 | try: | ||
3807 | 206 | allowed_person = lp.people[allowed] | ||
3808 | 207 | except KeyError: | ||
3809 | 208 | logger.warn('User {} from the allowed_users list is not in ' | ||
3810 | 209 | 'launchpad!'.format(allowed)) | ||
3811 | 210 | continue | ||
3812 | 211 | if not allowed_person.is_team: | ||
3813 | 212 | continue | ||
3814 | 213 | if users_in_team(lp_users, allowed_person): | ||
3815 | 214 | return True | ||
3816 | 215 | logger.debug('Users "' + ', '.join(u.name for u in lp_users) + | ||
3817 | 216 | '" not allowed to trigger jobs') | ||
3818 | 217 | return False | ||
3819 | 218 | |||
3820 | 219 | project_blacklist = [] | ||
3821 | 220 | |||
3822 | 221 | for proposal in proposals: | ||
3823 | 222 | launchpad_user = launchpad.people(get_config_option('launchpad_login')) | ||
3824 | 223 | |||
3825 | 224 | print("Checking proposal %s .." % proposal.web_link) | ||
3826 | 225 | |||
3827 | 226 | # Ignore certain projects which don't build here as they are | ||
3828 | 227 | # fetching source from somewhere else. | ||
3829 | 228 | ignore = False | ||
3830 | 229 | for project in project_blacklist: | ||
3831 | 230 | if project in proposal.web_link: | ||
3832 | 231 | ignore = True | ||
3833 | 232 | break | ||
3834 | 233 | |||
3835 | 234 | if ignore: | ||
3836 | 235 | print "Ignoring %s" % proposal.web_link | ||
3837 | 236 | continue | ||
3838 | 237 | |||
3839 | 238 | if not users_allowed_to_trigger_jobs([proposal.registrant], [team]): | ||
3840 | 239 | continue | ||
3841 | 240 | |||
3842 | 241 | if latest_candidate_validated(launchpad_user, proposal): | ||
3843 | 242 | continue | ||
3844 | 243 | |||
3845 | 244 | if testing_in_progress(proposal, jenkins_job): | ||
3846 | 245 | continue | ||
3847 | 246 | |||
3848 | 247 | target_repo = launchpad.load(proposal.target_git_repository_link) | ||
3849 | 248 | source_repo = launchpad.load(proposal.source_git_repository_link) | ||
3850 | 249 | |||
3851 | 250 | target_git_url = target_repo.git_https_url | ||
3852 | 251 | if target_git_url == None: | ||
3853 | 252 | target_git_url = target_repo.git_ssh_url | ||
3854 | 253 | |||
3855 | 254 | source_git_url = source_repo.git_https_url | ||
3856 | 255 | if source_git_url == None: | ||
3857 | 256 | source_git_url = source_repo.git_ssh_url | ||
3858 | 257 | |||
3859 | 258 | target_branch = clean_branch_name(proposal.target_git_path) | ||
3860 | 259 | jenkins_params = { | ||
3861 | 260 | 'TARGET_GIT_REPO': target_git_url, | ||
3862 | 261 | 'TARGET_GIT_REPO_BRANCH': target_branch, | ||
3863 | 262 | 'SOURCE_GIT_REPO': source_git_url, | ||
3864 | 263 | 'SOURCE_GIT_REPO_BRANCH': clean_branch_name(proposal.source_git_path), | ||
3865 | 264 | 'MERGE_PROPOSAL': proposal.web_link, | ||
3866 | 265 | 'REVISION': get_latest_revision(proposal), | ||
3867 | 266 | 'SERIES': series_from_branch_name(target_branch), | ||
3868 | 267 | } | ||
3869 | 268 | |||
3870 | 269 | print("Triggering build job for %s" % proposal.web_link) | ||
3871 | 270 | j.build_job(jenkins_job, jenkins_params, jenkins_build_token) | ||
3872 | diff --git a/tools/trigger-lp-build.py b/tools/trigger-lp-build.py | |||
3873 | 0 | new file mode 100755 | 271 | new file mode 100755 |
3874 | index 0000000..5434281 | |||
3875 | --- /dev/null | |||
3876 | +++ b/tools/trigger-lp-build.py | |||
3877 | @@ -0,0 +1,230 @@ | |||
3878 | 1 | #! /usr/bin/python | ||
3879 | 2 | |||
3880 | 3 | import os | ||
3881 | 4 | import sys | ||
3882 | 5 | import time | ||
3883 | 6 | import random | ||
3884 | 7 | import smtplib | ||
3885 | 8 | import string | ||
3886 | 9 | import urllib2 | ||
3887 | 10 | import zlib | ||
3888 | 11 | |||
3889 | 12 | from datetime import datetime | ||
3890 | 13 | from os.path import basename | ||
3891 | 14 | from launchpadlib.launchpad import Launchpad | ||
3892 | 15 | |||
3893 | 16 | from argparse import ArgumentParser | ||
3894 | 17 | |||
3895 | 18 | import se_utils | ||
3896 | 19 | |||
3897 | 20 | parser = ArgumentParser(description="Build a specific snap on launchpad") | ||
3898 | 21 | parser.add_argument('-s', '--snap', required=True, | ||
3899 | 22 | help="Name of the snap to build") | ||
3900 | 23 | parser.add_argument('-p', '--publish', action='store_true', | ||
3901 | 24 | help="Trigger a publish build instead of a daily (default)") | ||
3902 | 25 | parser.add_argument('-n', '--new', action='store_true', help="Create a new ephemeral snap build on launchpad") | ||
3903 | 26 | parser.add_argument('--git-repo', help="Git repository to be used for new ephemeral snap build") | ||
3904 | 27 | parser.add_argument('--git-repo-branch', help="Git repository branch to be used for new ephemeral snap build") | ||
3905 | 28 | parser.add_argument('-a', '--architectures', help="Specify architectures to build for. Separate multiple architectures by ','") | ||
3906 | 29 | parser.add_argument('-r', '--results-dir', help="Specify where results should be saved") | ||
3907 | 30 | |||
3908 | 31 | args = vars(parser.parse_args()) | ||
3909 | 32 | |||
3910 | 33 | ephemeral_build=False | ||
3911 | 34 | results_dir=os.path.join(os.getcwd(), "results") | ||
3912 | 35 | |||
3913 | 36 | if 'results_dir' in args: | ||
3914 | 37 | results_dir=args['results_dir'] | ||
3915 | 38 | |||
3916 | 39 | if args['new']: | ||
3917 | 40 | ephemeral_build=True | ||
3918 | 41 | if args['git_repo'] == None or args['git_repo_branch'] == None: | ||
3919 | 42 | print("ERROR: No git repository or a branch supplied") | ||
3920 | 43 | sys.exit(1) | ||
3921 | 44 | |||
3922 | 45 | series = 'xenial' | ||
3923 | 46 | |||
3924 | 47 | lp_app = se_utils.get_config_option("lp_app") | ||
3925 | 48 | lp_env = se_utils.get_config_option("lp_env") | ||
3926 | 49 | credential_store_path = se_utils.get_config_option('credential_store_path') | ||
3927 | 50 | launchpad = se_utils.get_launchpad(None, credential_store_path, lp_app, lp_env) | ||
3928 | 51 | |||
3929 | 52 | team = launchpad.people['snappy-hwe-team'] | ||
3930 | 53 | ubuntu = launchpad.distributions['ubuntu'] | ||
3931 | 54 | release = ubuntu.getSeries(name_or_version=series) | ||
3932 | 55 | primary_archive = ubuntu.getArchive(name='primary') | ||
3933 | 56 | |||
3934 | 57 | snap=None | ||
3935 | 58 | if ephemeral_build: | ||
3936 | 59 | snap_arches=[] | ||
3937 | 60 | if 'architectures' in args and args['architectures'] != None: | ||
3938 | 61 | snap_arches = args["architectures"].split(",") | ||
3939 | 62 | |||
3940 | 63 | if len(snap_arches) == 0: | ||
3941 | 64 | print("WARNING: No architectures to build specified. Will only build for amd64.") | ||
3942 | 65 | snap_arches=["amd64"] | ||
3943 | 66 | |||
3944 | 67 | processors=[] | ||
3945 | 68 | for arch in snap_arches: | ||
3946 | 69 | try: | ||
3947 | 70 | p = launchpad.processors.getByName(name=arch) | ||
3948 | 71 | processors.append(p.self_link) | ||
3949 | 72 | except: | ||
3950 | 73 | print("ERROR: Failed to find processor for '{}' architecture".format(arch)) | ||
3951 | 74 | sys.exit(1) | ||
3952 | 75 | |||
3953 | 76 | build_name = 'ci-%s-%s' % (args["snap"], ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(16))) | ||
3954 | 77 | snap = launchpad.snaps.new(name=build_name, | ||
3955 | 78 | processors=processors, | ||
3956 | 79 | auto_build=False, distro_series=release, | ||
3957 | 80 | git_repository_url=args['git_repo'], | ||
3958 | 81 | git_path='%s' % args["git_repo_branch"], | ||
3959 | 82 | owner=team) | ||
3960 | 83 | else: | ||
3961 | 84 | build_name = "%s-daily" % args["snap"] | ||
3962 | 85 | if args["publish"] == True: | ||
3963 | 86 | build_name = "%s-publish" % args["snap"] | ||
3964 | 87 | snap = launchpad.snaps.getByName(name=build_name, owner=team) | ||
3965 | 88 | |||
3966 | 89 | if snap == None: | ||
3967 | 90 | print("ERROR: Failed to create snap build on launchpad") | ||
3968 | 91 | |||
3969 | 92 | # Not every snap is build agains all arches. | ||
3970 | 93 | arches = [processor.name for processor in snap.processors] | ||
3971 | 94 | if not ephemeral_build and args['architectures'] != None: | ||
3972 | 95 | wanted_arches = args["architectures"].split(",") | ||
3973 | 96 | possible_arches = [] | ||
3974 | 97 | for arch in wanted_arches: | ||
3975 | 98 | if not arch in arches: | ||
3976 | 99 | print("WARNING: Can't build snap for architecture {} as it is not enabled in the build job".format(args["snap"])) | ||
3977 | 100 | continue | ||
3978 | 101 | possible_arches.append(arch) | ||
3979 | 102 | arches = possible_arches | ||
3980 | 103 | |||
3981 | 104 | if len(arches) == 0: | ||
3982 | 105 | print("ERROR: No architectures available to build for") | ||
3983 | 106 | sys.exit(1) | ||
3984 | 107 | |||
3985 | 108 | # Add a big fat warning that we don't really care about fixing things when | ||
3986 | 109 | # the job will be canceled after the following lines are printed out. | ||
3987 | 110 | print("!!!!!!! POINT OF NO RETURN !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") | ||
3988 | 111 | print("DO NOT CANCEL THIS JOB AFTER THIS OR BAD THINGS WILL HAPPEN") | ||
3989 | 112 | print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") | ||
3990 | 113 | |||
3991 | 114 | stamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') | ||
3992 | 115 | print("Trying to trigger builds at: {}".format(stamp)) | ||
3993 | 116 | |||
3994 | 117 | # sometimes we see error such as "u'Unknown architecture lpia for ubuntu xenial'" | ||
3995 | 118 | # and in order to workaround let's validate the arches agains set of valid | ||
3996 | 119 | # architectures that the snap can choose from | ||
3997 | 120 | |||
3998 | 121 | |||
3999 | 122 | # We will now trigger a build for each whitelisted architecture, collect the | ||
4000 | 123 | # build job url and the wait for all builds to finish and collect their results | ||
4001 | 124 | # to vote for a successful or failed build. | ||
4002 | 125 | triggered_builds = [] | ||
4003 | 126 | triggered_build_urls = {} | ||
4004 | 127 | valid_arches = ['armhf', 'i386', 'amd64', 'arm64', 's390x', 'powerpc', 'ppc64el'] | ||
4005 | 128 | for build_arch in arches: | ||
4006 | 129 | # sometimes we see error such as "u'Unknown architecture lpia for | ||
4007 | 130 | # ubuntu xenial'" and in order to workaround let's validate the arches | ||
4008 | 131 | # agains set of valid architectures that the snap can choose from | ||
4009 | 132 | if build_arch not in valid_arches: | ||
4010 | 133 | print("WARNING: Can't build snap for architecture {} as it is not enabled in the build job".format(args["snap"])) | ||
4011 | 134 | continue | ||
4012 | 135 | |||
4013 | 136 | arch = release.getDistroArchSeries(archtag=build_arch) | ||
4014 | 137 | request = snap.requestBuild(archive=primary_archive, distro_arch_series=arch, pocket='Proposed') | ||
4015 | 138 | build_id = str(request).rsplit('/', 1)[-1] | ||
4016 | 139 | triggered_builds.append(build_id) | ||
4017 | 140 | triggered_build_urls[build_id] = request.self_link | ||
4018 | 141 | print("Arch: {} is building under: {}".format(build_arch, request.self_link)) | ||
4019 | 142 | |||
4020 | 143 | failures = [] | ||
4021 | 144 | successful = [] | ||
4022 | 145 | while len(triggered_builds): | ||
4023 | 146 | for build in triggered_builds: | ||
4024 | 147 | try: | ||
4025 | 148 | response = snap.getBuildSummariesForSnapBuildIds(snap_build_ids=[build]) | ||
4026 | 149 | except: | ||
4027 | 150 | print("Could not get response for {} (was there an LP timeout?)".format(build)) | ||
4028 | 151 | continue | ||
4029 | 152 | status = response[build]['status'] | ||
4030 | 153 | if status == "FULLYBUILT": | ||
4031 | 154 | successful.append(build) | ||
4032 | 155 | triggered_builds.remove(build) | ||
4033 | 156 | continue | ||
4034 | 157 | elif status == "FAILEDTOBUILD": | ||
4035 | 158 | failures.append(build) | ||
4036 | 159 | triggered_builds.remove(build) | ||
4037 | 160 | continue | ||
4038 | 161 | elif status == "CANCELLED": | ||
4039 | 162 | print("INFO: {} snap build was canceled for id: {}".format(args["snap"], build)) | ||
4040 | 163 | triggered_builds.remove(build) | ||
4041 | 164 | continue | ||
4042 | 165 | if len(triggered_builds) > 0: | ||
4043 | 166 | time.sleep(60) | ||
4044 | 167 | |||
4045 | 168 | if len(failures): | ||
4046 | 169 | for failure in failures: | ||
4047 | 170 | try: | ||
4048 | 171 | response = snap.getBuildSummariesForSnapBuildIds(snap_build_ids=[failure]) | ||
4049 | 172 | except: | ||
4050 | 173 | print ("Could not get failure data for {} (was there an LP timeout?)".format(build)) | ||
4051 | 174 | continue | ||
4052 | 175 | |||
4053 | 176 | if not failure in response: | ||
4054 | 177 | print("Launchpad didn't returned us the snap build summary we ask it for!?") | ||
4055 | 178 | continue | ||
4056 | 179 | |||
4057 | 180 | build_summary = response[failure] | ||
4058 | 181 | arch = 'unknown' | ||
4059 | 182 | buildlog = None | ||
4060 | 183 | if 'build_log_url' in build_summary: | ||
4061 | 184 | buildlog = build_summary['build_log_url'] | ||
4062 | 185 | |||
4063 | 186 | if buildlog != None and len(buildlog) > 0: | ||
4064 | 187 | parts = arch = str(buildlog).split('_') | ||
4065 | 188 | if len(parts) >= 4: | ||
4066 | 189 | arch = parts[4] | ||
4067 | 190 | elif buildlog == None: | ||
4068 | 191 | buildlog = 'not available' | ||
4069 | 192 | |||
4070 | 193 | print("INFO: {} snap {} build at {} failed for id: {} log: {}".format(args["snap"], arch, stamp, failure, buildlog)) | ||
4071 | 194 | |||
4072 | 195 | # For ephermal builds we need to print out the log file as it will be gone after | ||
4073 | 196 | # the launchpad build is removed. | ||
4074 | 197 | if ephemeral_build and buildlog != None: | ||
4075 | 198 | response = urllib2.urlopen(buildlog) | ||
4076 | 199 | log_data = zlib.decompress(response.read(), 16+zlib.MAX_WBITS) | ||
4077 | 200 | print(log_data) | ||
4078 | 201 | |||
4079 | 202 | # Fetch build results for successful builds and store those in the output | ||
4080 | 203 | # directory so that the caller can reuse them. | ||
4081 | 204 | if len(successful): | ||
4082 | 205 | for success in successful: | ||
4083 | 206 | try: | ||
4084 | 207 | snap_build = launchpad.load(triggered_build_urls[success]) | ||
4085 | 208 | urls = snap_build.getFileUrls() | ||
4086 | 209 | if len(urls): | ||
4087 | 210 | for u in urls: | ||
4088 | 211 | print("Downloading snap from %s ..." % u) | ||
4089 | 212 | response = urllib2.urlopen(u) | ||
4090 | 213 | if not os.path.exists(results_dir): | ||
4091 | 214 | os.makedirs(results_dir) | ||
4092 | 215 | path = os.path.join(results_dir, os.path.basename(u)) | ||
4093 | 216 | with open(path, "w") as out_file: | ||
4094 | 217 | out_file.write(response.read()) | ||
4095 | 218 | except: | ||
4096 | 219 | print ("Could not retrieve snap build data for {} (was there an LP timeout?)".format(build)) | ||
4097 | 220 | continue | ||
4098 | 221 | |||
4099 | 222 | |||
4100 | 223 | if ephemeral_build: | ||
4101 | 224 | snap.lp_delete() | ||
4102 | 225 | |||
4103 | 226 | if len(failures): | ||
4104 | 227 | # Let the build fail as at least a single snap has failed to build | ||
4105 | 228 | sys.exit(1) | ||
4106 | 229 | |||
4107 | 230 | print("Done!") | ||
4108 | diff --git a/tools/vote-on-merge-proposal.py b/tools/vote-on-merge-proposal.py | |||
4109 | 0 | new file mode 100755 | 231 | new file mode 100755 |
4110 | index 0000000..bec6da5 | |||
4111 | --- /dev/null | |||
4112 | +++ b/tools/vote-on-merge-proposal.py | |||
4113 | @@ -0,0 +1,271 @@ | |||
4114 | 1 | #!/usr/bin/env python | ||
4115 | 2 | # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- | ||
4116 | 3 | # | ||
4117 | 4 | # Copyright (C) 2016 Canonical Ltd | ||
4118 | 5 | # | ||
4119 | 6 | # This program is free software: you can redistribute it and/or modify | ||
4120 | 7 | # it under the terms of the GNU General Public License version 3 as | ||
4121 | 8 | # published by the Free Software Foundation. | ||
4122 | 9 | # | ||
4123 | 10 | # This program is distributed in the hope that it will be useful, | ||
4124 | 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
4125 | 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
4126 | 13 | # GNU General Public License for more details. | ||
4127 | 14 | # | ||
4128 | 15 | # You should have received a copy of the GNU General Public License | ||
4129 | 16 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
4130 | 17 | |||
4131 | 18 | import atexit | ||
4132 | 19 | import sys | ||
4133 | 20 | import time | ||
4134 | 21 | import logging | ||
4135 | 22 | import os | ||
4136 | 23 | import yaml | ||
4137 | 24 | import re | ||
4138 | 25 | from shutil import rmtree | ||
4139 | 26 | from argparse import ArgumentParser | ||
4140 | 27 | from launchpadlib.credentials import RequestTokenAuthorizationEngine | ||
4141 | 28 | from lazr.restfulclient.errors import HTTPError | ||
4142 | 29 | from launchpadlib.launchpad import Launchpad | ||
4143 | 30 | from launchpadlib.credentials import UnencryptedFileCredentialStore | ||
4144 | 31 | from jlp import get_config_option | ||
4145 | 32 | from jlp import launchpadutils, jenkinsutils, logger | ||
4146 | 33 | |||
4147 | 34 | logger = logging.getLogger('jenkins-launchpad-plugin') | ||
4148 | 35 | stdout_handler = logging.StreamHandler(stream=sys.stdout) | ||
4149 | 36 | formatter = logging.Formatter('%(levelname)s: %(message)s') | ||
4150 | 37 | stdout_handler.setFormatter(formatter) | ||
4151 | 38 | logger.addHandler(stdout_handler) | ||
4152 | 39 | |||
4153 | 40 | parser = ArgumentParser(description="Vote on a Launchpad merge proposal.") | ||
4154 | 41 | parser.add_argument('-s', '--status') | ||
4155 | 42 | parser.add_argument('-u', '--build-url', required=True, | ||
4156 | 43 | help="URL of the Jenkins job") | ||
4157 | 44 | parser.add_argument('-p', '--merge-proposal', required=True, | ||
4158 | 45 | help="URL of the merge proposal to update") | ||
4159 | 46 | parser.add_argument('-r', '--revision', required=True, | ||
4160 | 47 | help="merge proposal candidate revision") | ||
4161 | 48 | |||
4162 | 49 | args = vars(parser.parse_args()) | ||
4163 | 50 | |||
4164 | 51 | ACCESS_TOKEN_POLL_TIME = 10 | ||
4165 | 52 | WAITING_FOR_USER = """Open this link: | ||
4166 | 53 | {} | ||
4167 | 54 | to authorize this program to access Launchpad on your behalf. | ||
4168 | 55 | Waiting to hear from Launchpad about your decision. . . .""" | ||
4169 | 56 | |||
4170 | 57 | |||
4171 | 58 | class AuthorizeRequestTokenWithConsole(RequestTokenAuthorizationEngine): | ||
4172 | 59 | """Authorize a token in a server environment (with no browser). | ||
4173 | 60 | |||
4174 | 61 | Print a link for the user to copy-and-paste into his/her browser | ||
4175 | 62 | for authentication. | ||
4176 | 63 | """ | ||
4177 | 64 | |||
4178 | 65 | def __init__(self, *args, **kwargs): | ||
4179 | 66 | # as implemented in AuthorizeRequestTokenWithBrowser | ||
4180 | 67 | kwargs['consumer_name'] = None | ||
4181 | 68 | kwargs.pop('allow_access_levels', None) | ||
4182 | 69 | super(AuthorizeRequestTokenWithConsole, self).__init__(*args, **kwargs) | ||
4183 | 70 | |||
4184 | 71 | def make_end_user_authorize_token(self, credentials, request_token): | ||
4185 | 72 | """Ask the end-user to authorize the token in their browser. | ||
4186 | 73 | |||
4187 | 74 | """ | ||
4188 | 75 | authorization_url = self.authorization_url(request_token) | ||
4189 | 76 | print WAITING_FOR_USER.format(authorization_url) | ||
4190 | 77 | # if we don't flush we may not see the message | ||
4191 | 78 | sys.stdout.flush() | ||
4192 | 79 | while credentials.access_token is None: | ||
4193 | 80 | time.sleep(ACCESS_TOKEN_POLL_TIME) | ||
4194 | 81 | try: | ||
4195 | 82 | credentials.exchange_request_token_for_access_token( | ||
4196 | 83 | self.web_root) | ||
4197 | 84 | break | ||
4198 | 85 | except HTTPError, e: | ||
4199 | 86 | if e.response.status == 403: | ||
4200 | 87 | # The user decided not to authorize this | ||
4201 | 88 | # application. | ||
4202 | 89 | raise e | ||
4203 | 90 | elif e.response.status == 401: | ||
4204 | 91 | # The user has not made a decision yet. | ||
4205 | 92 | pass | ||
4206 | 93 | else: | ||
4207 | 94 | # There was an error accessing the server. | ||
4208 | 95 | raise e | ||
4209 | 96 | |||
4210 | 97 | |||
4211 | 98 | def get_launchpad(launchpadlib_dir=None): | ||
4212 | 99 | """ return a launchpad API class. In case launchpadlib_dir is | ||
4213 | 100 | specified used that directory to store launchpadlib cache instead of | ||
4214 | 101 | the default """ | ||
4215 | 102 | store = UnencryptedFileCredentialStore( | ||
4216 | 103 | get_config_option('credential_store_path')) | ||
4217 | 104 | lp_app = get_config_option('lp_app') | ||
4218 | 105 | lp_env = get_config_option('lp_env') | ||
4219 | 106 | authorization_engine = AuthorizeRequestTokenWithConsole(lp_env, lp_app) | ||
4220 | 107 | return Launchpad.login_with(lp_app, lp_env, | ||
4221 | 108 | credential_store=store, | ||
4222 | 109 | authorization_engine=authorization_engine, | ||
4223 | 110 | launchpadlib_dir=launchpadlib_dir, | ||
4224 | 111 | version='devel') | ||
4225 | 112 | |||
4226 | 113 | def get_branch_handle_from_url(lp_handle, url): | ||
4227 | 114 | """ Return a branch/repo handle for the given url. | ||
4228 | 115 | Returns a launchpad branch or git repository handle for the given url. | ||
4229 | 116 | :param lp_handle: launchpad API handle/instance | ||
4230 | 117 | :param url: url of the branch or git repository | ||
4231 | 118 | """ | ||
4232 | 119 | if '+git' in url: | ||
4233 | 120 | name = url.replace('https://code.launchpad.net/', '') | ||
4234 | 121 | logger.debug('fetching repo: ' + name) | ||
4235 | 122 | try: | ||
4236 | 123 | return lp_handle.git_repositories.getByPath(path=name) | ||
4237 | 124 | except AttributeError: | ||
4238 | 125 | logger.debug('git_repositories.getByPath was not found. You may need to set lp_version=devel in the config') | ||
4239 | 126 | return None | ||
4240 | 127 | else: | ||
4241 | 128 | name = url.replace('https://code.launchpad.net/', 'lp:') | ||
4242 | 129 | name = name.replace('https://code.staging.launchpad.net/', 'lp://staging/') | ||
4243 | 130 | logger.debug('fetching branch: ' + name) | ||
4244 | 131 | return lp_handle.branches.getByUrl(url=name) | ||
4245 | 132 | |||
4246 | 133 | def get_branch_from_mp(merge_proposal): | ||
4247 | 134 | """Return a link to branch given a link to a merge proposal. | ||
4248 | 135 | |||
4249 | 136 | If merge_proposal is: | ||
4250 | 137 | https://copde.launchpad.net/~user/project/name/+merge/12345 | ||
4251 | 138 | then the result will be: | ||
4252 | 139 | https://copde.launchpad.net/~user/project/name/ | ||
4253 | 140 | |||
4254 | 141 | :param merge_proposal: url of a launchpad merge proposal | ||
4255 | 142 | """ | ||
4256 | 143 | m = re.search('(.*)\+merge/[0-9]+$', merge_proposal) | ||
4257 | 144 | if m: | ||
4258 | 145 | return m.group(1) | ||
4259 | 146 | return None | ||
4260 | 147 | |||
4261 | 148 | def get_mp_handle_from_url(lp_handle, merge_proposal_link): | ||
4262 | 149 | """ Get launchpad handle for merge proposal given a merge proposal URL. | ||
4263 | 150 | |||
4264 | 151 | Returns None in case the merge proposal can't be found. | ||
4265 | 152 | :param merge_proposal_link: URL of the merge proposal | ||
4266 | 153 | """ | ||
4267 | 154 | branch_link = get_branch_from_mp(merge_proposal_link) | ||
4268 | 155 | if not branch_link: | ||
4269 | 156 | logger.error('Unable to get branch link from merge proposal link.') | ||
4270 | 157 | return None | ||
4271 | 158 | |||
4272 | 159 | branch = get_branch_handle_from_url(lp_handle, branch_link) | ||
4273 | 160 | if not branch: | ||
4274 | 161 | logger.debug('Branch {} does not exist'.format(branch_link)) | ||
4275 | 162 | return None | ||
4276 | 163 | |||
4277 | 164 | logger.debug('mp_link: {}.'.format(merge_proposal_link)) | ||
4278 | 165 | |||
4279 | 166 | for mp in branch.landing_targets: | ||
4280 | 167 | logger.debug('mp.web_link: {}'.format(mp.web_link)) | ||
4281 | 168 | if mp.web_link == merge_proposal_link: | ||
4282 | 169 | return mp | ||
4283 | 170 | |||
4284 | 171 | return None | ||
4285 | 172 | |||
4286 | 173 | class LaunchpadVote(): | ||
4287 | 174 | APPROVE = 'Approve' | ||
4288 | 175 | DISAPPROVE = 'Disapprove' | ||
4289 | 176 | NEEDS_FIXING = 'Needs Fixing' | ||
4290 | 177 | |||
4291 | 178 | def get_vote_subject(mp): | ||
4292 | 179 | """Given a mp handle return a subject for the vote message | ||
4293 | 180 | |||
4294 | 181 | Unfortunately there is no method in the API that gives you the "standard" | ||
4295 | 182 | subject that launchapd is using and some email clients (gmail) are | ||
4296 | 183 | grouping conversations into threads based on subject. | ||
4297 | 184 | |||
4298 | 185 | This returns what seems to be the launchpad way of doing subjects. | ||
4299 | 186 | :param mp: launchpad merge proposal handle | ||
4300 | 187 | """ | ||
4301 | 188 | |||
4302 | 189 | if '+git' in mp.web_link: | ||
4303 | 190 | source = mp.source_git_repository.display_name.replace('lp:', '') + \ | ||
4304 | 191 | ':' + \ | ||
4305 | 192 | mp.source_git_path.replace('refs/heads/', '') | ||
4306 | 193 | target = mp.target_git_repository.display_name.replace('lp:', '') + \ | ||
4307 | 194 | ':' + \ | ||
4308 | 195 | mp.target_git_path.replace('refs/heads/', '') | ||
4309 | 196 | return 'Re: [Merge] {} into {}'.format(source, target) | ||
4310 | 197 | else: | ||
4311 | 198 | return 'Re: [Merge] {} into {}'.format( | ||
4312 | 199 | mp.source_branch.display_name, | ||
4313 | 200 | mp.target_branch.display_name) | ||
4314 | 201 | |||
4315 | 202 | |||
4316 | 203 | def approve_mp(mp, revision, build_url): | ||
4317 | 204 | """Approve a given merge proposal a revision. | ||
4318 | 205 | |||
4319 | 206 | :params mp: launchapd handle to the respective merge proposal | ||
4320 | 207 | :params revision: revision that should be approved | ||
4321 | 208 | :params build_url: jenkins build url with the details. This job is used to | ||
4322 | 209 | generate the message with all the links to test runs as | ||
4323 | 210 | well as artifacts (coverity, deb files, etc) | ||
4324 | 211 | """ | ||
4325 | 212 | state = 'PASSED: Continuous integration, rev:' + str(revision) | ||
4326 | 213 | logger.debug(state) | ||
4327 | 214 | content = jenkinsutils.format_message_for_mp_update(build_url, | ||
4328 | 215 | state + "\n") | ||
4329 | 216 | mp.createComment(review_type=get_config_option('launchpad_review_type'), | ||
4330 | 217 | vote=LaunchpadVote.APPROVE, subject=get_vote_subject(mp), | ||
4331 | 218 | content=content) | ||
4332 | 219 | |||
4333 | 220 | |||
4334 | 221 | def disapprove_mp(mp, revision, build_url, reason=None): | ||
4335 | 222 | """Disapprove a given merge proposal a revision (vote Needs Fixing). | ||
4336 | 223 | |||
4337 | 224 | :params mp: launchapd handle to the respective merge proposal | ||
4338 | 225 | :params revision: revision that should be fixed | ||
4339 | 226 | :params build_url: jenkins build url with the details. This job is used to | ||
4340 | 227 | generate the message with all the links to test runs as | ||
4341 | 228 | well as artifacts (coverity, deb files, etc) | ||
4342 | 229 | :params reason: optional string that is attached to the comment | ||
4343 | 230 | """ | ||
4344 | 231 | state = "FAILED: Continuous integration, rev:{revision}".format( | ||
4345 | 232 | revision=revision) | ||
4346 | 233 | if reason: | ||
4347 | 234 | state = "{state}\n{reason}".format(state=state, reason=reason) | ||
4348 | 235 | |||
4349 | 236 | logger.debug(state) | ||
4350 | 237 | content = jenkinsutils.format_message_for_mp_update( | ||
4351 | 238 | build_url, state + "\n") | ||
4352 | 239 | mp.createComment(review_type=get_config_option('launchpad_review_type'), | ||
4353 | 240 | vote=LaunchpadVote.NEEDS_FIXING, | ||
4354 | 241 | subject=get_vote_subject(mp), | ||
4355 | 242 | content=content) | ||
4356 | 243 | |||
4357 | 244 | # launchpadlib is not thread/process safe so we are creating launchpadlib | ||
4358 | 245 | # cache in /tmp per process which gets cleaned up at the end | ||
4359 | 246 | # see also lp:459418 and lp:1025153 | ||
4360 | 247 | launchpad_cachedir = os.path.join('/tmp', str(os.getpid()), '.launchpadlib') | ||
4361 | 248 | |||
4362 | 249 | # `launchpad_cachedir` is leaked upon unexpected exits | ||
4363 | 250 | # adding this cleanup to stop directories filling up `/tmp/` | ||
4364 | 251 | atexit.register(rmtree, os.path.join('/tmp', | ||
4365 | 252 | str(os.getpid())), | ||
4366 | 253 | ignore_errors=True) | ||
4367 | 254 | |||
4368 | 255 | lp_handle = get_launchpad(launchpadlib_dir=launchpad_cachedir) | ||
4369 | 256 | mp = get_mp_handle_from_url(lp_handle, args["merge_proposal"]) | ||
4370 | 257 | if not mp: | ||
4371 | 258 | parser.error('merge proposal related to this branch was not found') | ||
4372 | 259 | |||
4373 | 260 | # this is the status from tests | ||
4374 | 261 | overal_status = args['status'] | ||
4375 | 262 | # by default reason is empty as it is usually just a failed build | ||
4376 | 263 | reason = '' | ||
4377 | 264 | |||
4378 | 265 | if overal_status == 'PASSED': | ||
4379 | 266 | approve_mp(mp, args['revision'], args['build_url']) | ||
4380 | 267 | else: # status == False corresponds to NOT 'PASSED' | ||
4381 | 268 | disapprove_mp(mp, | ||
4382 | 269 | args['revision'], | ||
4383 | 270 | args['build_url'], | ||
4384 | 271 | reason) |