Merge ~kzapalowicz/snappy-hwe-snaps/+git/jenkins-jobs:fix/infrastructure-is-missing-file into ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-ap:master

Proposed by Konrad Zapałowicz
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
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.

Description of the change

jobs/infrastructure: add missing file

To post a comment you must log in.

Unmerged commits

4fb056a... by =?utf-8?q?Konrad_Zapa=C5=82owicz?= <email address hidden>

jobs/infrastructure: add missing file

a7a4e17... by System Enablement CI Bot <email address hidden>

Merge remote tracking branch debug-perm

Merge-Proposal: https://code.launchpad.net/~alfonsosanchezbeato/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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/.ci_tests_disabled b/.ci_tests_disabled
0new file mode 1006440new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/.ci_tests_disabled
diff --git a/README.md b/README.md
index 1a49a98..c4af3d3 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
1<<<<<<< README.md
1# wifi-ap2# wifi-ap
23
3This snap provided WiFi AP functionality out of the box.4This snap provided WiFi AP functionality out of the box.
@@ -81,3 +82,58 @@ snap.
81If you want to see more verbose debugging output of spread run82If you want to see more verbose debugging output of spread run
8283
83 $ ./run-tests --debug84 $ ./run-tests --debug
85=======
86# Jenkins Jobs
87
88This is a repository collecting a set of jenkins job definitions
89used to build a contineous integrate pipeline for snap based
90components.
91
92## Required Jenkins Plugins
93
94 * build-name-setter
95 * conditional-buildstep
96 * description-setter
97 * dynamic-axis
98 * matrix-combinations-parameter
99 * matrix-project
100 * nodelabelparameter
101 * parameterized-trigger
102 * rebuild
103 * timestamper
104 * ws-cleanup
105
106## Test locally
107
108Build the docker container which comes with this repository:
109
110```
111$ cd docker
112$ sudo docker build -t se-jenkins .
113```
114
115Spawn up a docker container with jenkins
116
117```
118$ sudo docker run -p 8080:8080 -p 50000:50000 --name jenkins se-jenkins
119```
120
121You can now login into jenkins on http://localhost:8080 with user 'system-enablement-ci-bot'
122and password 'jenkins'.
123
124Install jenkins-job-builder on your host via
125
126```
127$ sudo apt install python3-pip
128$ pip3 install jenkins-job-builder
129```
130
131Now you should have everything setup and ready for a first deployment:
132
133```
134$ jenkins-jobs --conf local.conf update local.yaml:jobs/infrastructure:jobs/snap:jobs/image
135```
136
137This will create all configured jobs on the jenkins instance or update
138them if already available.
139>>>>>>> README.md
diff --git a/docker/Dockerfile b/docker/Dockerfile
84new file mode 100644140new file mode 100644
index 0000000..a898d1e
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,59 @@
1FROM ubuntu:latest
2
3ENV DEBIAN_FRONTEND noninteractive
4ENV INITRD No
5ENV LANG en_US.UTF-8
6
7RUN apt-get update && \
8 apt-get upgrade -y && \
9 apt-get install -y --no-install-recommends \
10 vim.tiny wget curl sudo net-tools pwgen \
11 git-core logrotate software-properties-common locales openssh-client && \
12 locale-gen en_US en_US.UTF-8 && \
13 apt-get clean && \
14 rm -rf /var/lib/apt/lists/*
15
16RUN apt-get update && \
17 apt-get install --no-install-recommends -y openjdk-8-jre-headless && \
18 apt-get clean && \
19 rm -rf /var/lib/apt/lists/*
20
21RUN wget -qO - http://pkg.jenkins-ci.org/debian/jenkins-ci.org.key | sudo apt-key add - && \
22 echo 'deb http://pkg.jenkins-ci.org/debian binary/' \
23 | tee /etc/apt/sources.list.d/jenkins.list && \
24 apt-get update && \
25 apt-get install --no-install-recommends -y jenkins && \
26 apt-get clean && \
27 rm -rf /var/lib/apt/lists/* && \
28 update-rc.d -f jenkins disable
29
30RUN usermod -d /var/jenkins jenkins
31
32# Passthrough all sudo requests without requiring a password for our jenkins user
33RUN echo "jenkins ALL = NOPASSWD: ALL" > /etc/sudoers.d/jenkins
34
35RUN apt-get update && apt-get install --yes unzip zip wget
36
37ENV JENKINS_UC https://updates.jenkins-ci.org
38ENV JENKINS_HOME /var/jenkins
39ENV COPY_REFERENCE_FILE_LOG $JENKINS_HOME/copy_reference_file.log
40ENV JENKINS_USER system-enablement-ci-bot
41ENV JENKINS_PASS jenkins
42
43COPY plugins.sh /usr/local/bin/plugins.sh
44RUN chmod +x /usr/local/bin/plugins.sh
45COPY plugins.txt /tmp/plugins.txt
46RUN /usr/local/bin/plugins.sh /tmp/plugins.txt
47
48COPY jenkins.sh /usr/local/bin/jenkins.sh
49RUN chmod +x /usr/local/bin/jenkins.sh
50
51COPY initial-setup.groovy /usr/share/jenkins/ref/init.groovy.d/
52
53ENV JAVA_OPTS -Djenkins.install.runSetupWizard=false
54
55EXPOSE 8080 50000
56
57USER jenkins
58
59CMD ["/usr/local/bin/jenkins.sh"]
diff --git a/docker/initial-setup.groovy b/docker/initial-setup.groovy
0new file mode 10064460new file mode 100644
index 0000000..5a9598d
--- /dev/null
+++ b/docker/initial-setup.groovy
@@ -0,0 +1,34 @@
1import jenkins.model.*
2import jenkins.security.*
3import hudson.security.*
4
5def env = System.getenv()
6
7def jenkins = Jenkins.getInstance()
8
9jenkins.setLabelString("snap build test release misc monitor")
10
11jenkins.setSecurityRealm(new HudsonPrivateSecurityRealm(false))
12jenkins.setAuthorizationStrategy(new GlobalMatrixAuthorizationStrategy())
13
14def user = jenkins.getSecurityRealm().createAccount(env.JENKINS_USER, env.JENKINS_PASS)
15ApiTokenProperty t = user.getProperty(ApiTokenProperty.class)
16def token = t.getApiTokenInsecure()
17
18println ""
19println "########################################################################"
20println "API token for user " + env.JENKINS_USER + " is " + token
21println "########################################################################"
22println ""
23
24user.save()
25
26jenkins.getAuthorizationStrategy().add(Jenkins.ADMINISTER, env.JENKINS_USER)
27jenkins.save()
28
29for (slave in jenkins.model.Jenkins.instance.slaves) {
30 println "Slave: " + slave.getNodeName() + "\n";
31 println "Label: " + slave.getLabelString() + "\n\n";
32 slave.setLabelString("snap build test monitor release misc")
33}
34jenkins.save()
diff --git a/docker/jenkins.sh b/docker/jenkins.sh
0new file mode 10064435new file mode 100644
index 0000000..5d095be
--- /dev/null
+++ b/docker/jenkins.sh
@@ -0,0 +1,24 @@
1#! /bin/bash
2
3set -e
4
5# Copy files from /usr/share/jenkins/ref into $JENKINS_HOME
6# So the initial JENKINS-HOME is set with expected content.
7# Don't override, as this is just a reference setup, and use from UI
8# can then change this, upgrade plugins, etc.
9copy_reference_file() {
10 f=${1%/}
11 rel=${f:23}
12 dir=$(dirname ${f})
13 if [[ ! -e $JENKINS_HOME/${rel} ]]
14 then
15 mkdir -p $JENKINS_HOME/${dir:23}
16 cp -r /usr/share/jenkins/ref/${rel} $JENKINS_HOME/${rel};
17 # pin plugins on initial copy
18 [[ ${rel} == plugins/*.jpi ]] && touch $JENKINS_HOME/${rel}.pinned
19 fi;
20}
21export -f copy_reference_file
22find /usr/share/jenkins/ref/ -type f -exec bash -c "copy_reference_file '{}'" \;
23
24exec /usr/bin/java $JAVA_OPTS -jar /usr/share/jenkins/jenkins.war
diff --git a/docker/plugins.sh b/docker/plugins.sh
0new file mode 10075525new file mode 100755
index 0000000..ec8e8e5
--- /dev/null
+++ b/docker/plugins.sh
@@ -0,0 +1,28 @@
1#! /bin/bash
2
3# Parse a support-core plugin -style txt file as specification for jenkins plugins to be installed
4# in the reference directory, so user can define a derived Docker image with just :
5#
6# FROM jenkins
7# COPY plugins.txt /plugins.txt
8# RUN /usr/local/bin/plugins.sh /plugins.txt
9#
10
11set -e
12
13REF=/usr/share/jenkins/ref/plugins
14mkdir -p $REF
15
16while read spec || [ -n "$spec" ]; do
17 plugin=(${spec//:/ });
18 [[ ${plugin[0]} =~ ^# ]] && continue
19 [[ ${plugin[0]} =~ ^\s*$ ]] && continue
20 [[ -z ${plugin[1]} ]] && plugin[1]="latest"
21 echo "Downloading ${plugin[0]}:${plugin[1]}"
22
23 if [ -z "$JENKINS_UC_DOWNLOAD" ]; then
24 JENKINS_UC_DOWNLOAD=$JENKINS_UC/download
25 fi
26 curl -sSL -f ${JENKINS_UC_DOWNLOAD}/plugins/${plugin[0]}/${plugin[1]}/${plugin[0]}.hpi -o $REF/${plugin[0]}.jpi
27 unzip -qqt $REF/${plugin[0]}.jpi
28done < $1
diff --git a/docker/plugins.txt b/docker/plugins.txt
0new file mode 10064429new file mode 100644
index 0000000..5020d33
--- /dev/null
+++ b/docker/plugins.txt
@@ -0,0 +1,29 @@
1 build-name-setter
2 conditional-buildstep
3 description-setter
4 dynamic-axis
5 matrix-combinations-parameter
6 matrix-project
7 nodelabelparameter
8 parameterized-trigger
9 rebuild
10 timestamper
11 ws-cleanup
12 maven-plugin
13 token-macro
14 junit
15 script-security
16 jquery
17 workflow-basic-steps
18 structs
19 javadoc
20 workflow-api
21 workflow-step-api
22 mailer
23 matrix-auth
24 scm-api
25 display-url-api
26 scm-api
27 icon-shim
28 resource-disposer
29 run-condition
diff --git a/jobs/image/common-job-prepare.sh b/jobs/image/common-job-prepare.sh
0new file mode 10064430new file mode 100644
index 0000000..5feaa8d
--- /dev/null
+++ b/jobs/image/common-job-prepare.sh
@@ -0,0 +1,7 @@
1cat << EOF > $WORKSPACE/.build_env
2BOT_USERNAME={bot_username}
3LAUNCHPAD_PROJECT={launchpad_project}
4LAUNCHPAD_TEAM={launchpad_team}
5BUILD_SCRIPTS=$WORKSPACE/jenkins-jobs
6BUILD_ID=$BUILD_ID
7EOF
diff --git a/jobs/image/image-build-worker.sh b/jobs/image/image-build-worker.sh
0new file mode 1006448new file mode 100644
index 0000000..161c5e4
--- /dev/null
+++ b/jobs/image/image-build-worker.sh
@@ -0,0 +1,27 @@
1#!/bin/bash
2#
3# Copyright (C) 2017 Canonical Ltd
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17set -ex
18
19. "$WORKSPACE/.build_env"
20
21rm -rf "$WORKSPACE"/image-builds
22
23git clone git+ssh://git.launchpad.net/~$LAUNCHPAD_TEAM/$LAUNCHPAD_PROJECT/+git/image-builds
24cd image-builds
25
26# Project specific image build process
27./build.sh
diff --git a/jobs/image/image-build-worker.yaml b/jobs/image/image-build-worker.yaml
0new file mode 10064428new file mode 100644
index 0000000..f34faf3
--- /dev/null
+++ b/jobs/image/image-build-worker.yaml
@@ -0,0 +1,15 @@
1- job-template:
2 name: '{name}-image-build-worker'
3 project-type: freestyle
4 defaults: global
5 description: "Build an image."
6 display-name: "{name}-image-build-worker"
7 concurrent: true
8 node: snap && build
9 builders:
10 - shell:
11 !include-raw:
12 - common-job-prepare.sh
13 - shell:
14 !include-raw-escape:
15 - image-build-worker.sh
diff --git a/jobs/image/image-project-jobs.yaml b/jobs/image/image-project-jobs.yaml
0new file mode 10064416new file mode 100644
index 0000000..0b517c7
--- /dev/null
+++ b/jobs/image/image-project-jobs.yaml
@@ -0,0 +1,4 @@
1- job-group:
2 name: image-project-jobs
3 jobs:
4 - '{name}-image-build-worker'
diff --git a/jobs/infrastructure/common-job-prepare.sh b/jobs/infrastructure/common-job-prepare.sh
0new file mode 1006445new file mode 100644
index 0000000..3dbc9fd
--- /dev/null
+++ b/jobs/infrastructure/common-job-prepare.sh
@@ -0,0 +1,14 @@
1JENKINS_JOBS_GIT_REPO="{jobs-git-repo}"
2JENKINS_JOBS_GIT_REPO_BRANCH="{jobs-git-repo-branch}"
3
4if [ -n "${{CLEANUP_WORKSPACE}}" ] && [ "${{CLEANUP_WORKSPACE}}" -eq 1 ]; then
5 rm -rf ${{WORKSPACE}}/*
6fi
7
8cat << EOF > $WORKSPACE/.build_env
9JENKINS_JOBS_REPO="{jobs-git-repo}"
10JENKINS_JOBS_BRANCH="{jobs-git-repo-branch}"
11JENKINS_CONFIG_REPO="{config-git-repo}"
12JENKINS_CONFIG_BRANCH="{config-git-repo-branch}"
13JENKINS_INSTANCE="{jenkins-instance}"
14EOF
diff --git a/jobs/infrastructure/credentials-0-ssh.sh b/jobs/infrastructure/credentials-0-ssh.sh
0new file mode 10064415new file mode 100644
index 0000000..d72fd49
--- /dev/null
+++ b/jobs/infrastructure/credentials-0-ssh.sh
@@ -0,0 +1,58 @@
1#!/bin/sh -ex
2#
3# Copyright (C) 2016 Canonical Ltd
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17# put it in $JENKINS_HOME so it doesn't get purged with the workspace
18SSH_PATH="${{JENKINS_HOME}}/.ssh/"
19
20# this is so that this key's only used with git
21SSH_KEY_PATH="${{SSH_PATH}}/git.launchpad.net/{bot_username}"
22
23if [ ! -d "${{SSH_KEY_PATH}}" ]; then
24 mkdir -p "${{SSH_KEY_PATH}}"
25fi
26
27# don't care about host keys, but use the one id key for launchpad
28# TODO: use http://pad.lv/p/canonical-sshebang instead
29cat > "${{SSH_PATH}}/config" << EOF
30Host *
31 StrictHostKeyChecking no
32
33Host git.launchpad.net
34 User {bot_username}
35 IdentityFile ~/.ssh/%h/%r/id_rsa
36EOF
37
38# only use keys if both were uploaded, otherwise fail
39if [ -f "keys/id_rsa" -o -f "keys/id_rsa.pub" ]; then
40 [ -f "keys/id_rsa" -a -f "keys/id_rsa.pub" ] || \
41 (echo "ERROR: You need to upload both keys, or none of them"; exit 1)
42 mv "keys/id_rsa" "keys/id_rsa.pub" "${{SSH_KEY_PATH}}/"
43 chmod 600 ${{SSH_KEY_PATH}}/*
44fi
45
46# generate the keypair if none was uploaded or pre-existing
47if [ ! -f "${{SSH_KEY_PATH}}/id_rsa" ]; then
48 ssh-keygen -f "${{SSH_KEY_PATH}}/id_rsa"
49fi
50
51# display the public key
52echo "The public key for this jenkins user is:"
53echo "----------"
54cat "${{SSH_KEY_PATH}}/id_rsa.pub"
55echo "----------"
56
57# Probe ssh public ssh key for relevant hosts and add it to our known_hosts file
58ssh-keyscan -t rsa,dsa git.launchpad.net >> ${{SSH_PATH}}/known_hosts
diff --git a/jobs/infrastructure/credentials-0-ssh.yaml b/jobs/infrastructure/credentials-0-ssh.yaml
0new file mode 10064459new file mode 100644
index 0000000..add42d4
--- /dev/null
+++ b/jobs/infrastructure/credentials-0-ssh.yaml
@@ -0,0 +1,29 @@
1- job-template:
2 name: '{name}-credentials-0-ssh'
3 project-type: matrix
4 description: |
5 This job will generate or store the supplied RSA keypair
6 and display the public key for use with Launchpad.
7 properties:
8 - build-discarder:
9 num-to-keep: 1
10 - rebuild
11 parameters:
12 - matrix-combinations:
13 name: nodes
14 - file:
15 name: 'keys/id_rsa'
16 description: Private RSA key
17 - file:
18 name: 'keys/id_rsa.pub'
19 description: Public RSA key
20 axes:
21 - axis:
22 type: slave
23 name: node
24 values: '{obj:build_slaves}'
25 wrappers:
26 - timestamps
27 builders:
28 - shell:
29 !include-raw: credentials-0-ssh.sh
diff --git a/jobs/infrastructure/credentials-1-launchpad.py b/jobs/infrastructure/credentials-1-launchpad.py
0new file mode 10064430new file mode 100644
index 0000000..59dd706
--- /dev/null
+++ b/jobs/infrastructure/credentials-1-launchpad.py
@@ -0,0 +1,86 @@
1#!/usr/bin/env python
2#
3# Copyright (C) 2016 Canonical Ltd
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17import sys
18import time
19
20from launchpadlib.credentials import RequestTokenAuthorizationEngine
21from launchpadlib.credentials import UnencryptedFileCredentialStore
22from launchpadlib.launchpad import Launchpad
23from lazr.restfulclient.errors import HTTPError
24
25
26ACCESS_TOKEN_POLL_TIME = 1
27WAITING_FOR_USER = """Open this link:
28{{}}
29to authorize this program to access Launchpad on your behalf.
30Waiting to hear from Launchpad about your decision. . . ."""
31
32
33class AuthorizeRequestTokenWithConsole(RequestTokenAuthorizationEngine):
34 """Authorize a token in a server environment (with no browser).
35
36 Print a link for the user to copy-and-paste into his/her browser
37 for authentication.
38 """
39
40 def __init__(self, *args, **kwargs):
41 # as implemented in AuthorizeRequestTokenWithBrowser
42 kwargs['consumer_name'] = None
43 kwargs.pop('allow_access_levels', None)
44 super(AuthorizeRequestTokenWithConsole, self).__init__(*args, **kwargs)
45
46 def make_end_user_authorize_token(self, credentials, request_token):
47 """Ask the end-user to authorize the token in their browser.
48
49 """
50 authorization_url = self.authorization_url(request_token)
51 print(WAITING_FOR_USER.format(authorization_url))
52 # if we don't flush we may not see the message
53 sys.stdout.flush()
54 while credentials.access_token is None:
55 time.sleep(ACCESS_TOKEN_POLL_TIME)
56 try:
57 credentials.exchange_request_token_for_access_token(
58 self.web_root)
59 break
60 except HTTPError as e:
61 if e.response.status == 403:
62 # The user decided not to authorize this
63 # application.
64 raise e
65 elif e.response.status == 401:
66 # The user has not made a decision yet.
67 pass
68 else:
69 # There was an error accessing the server.
70 raise e
71
72
73def get_launchpad(cred_path, launchpadlib_dir=None):
74 """ return a launchpad API class. In case launchpadlib_dir is
75 specified used that directory to store launchpadlib cache instead of
76 the default """
77 store = UnencryptedFileCredentialStore(cred_path)
78 authorization_engine = AuthorizeRequestTokenWithConsole(
79 'production', 'ci-jenkins-slave')
80 return Launchpad.login_with('ci-jenkins-slave', 'production',
81 credential_store=store,
82 authorization_engine=authorization_engine,
83 launchpadlib_dir=launchpadlib_dir)
84
85if __name__ == '__main__':
86 get_launchpad("{credentials_path}")
diff --git a/jobs/infrastructure/credentials-1-launchpad.yaml b/jobs/infrastructure/credentials-1-launchpad.yaml
0new file mode 10064487new file mode 100644
index 0000000..d1c4968
--- /dev/null
+++ b/jobs/infrastructure/credentials-1-launchpad.yaml
@@ -0,0 +1,21 @@
1- job-template:
2 name: '{name}-credentials-1-launchpad'
3 project-type: matrix
4 description: This job creates or validates OAuth tokens to allow launchpadlib to work.
5 properties:
6 - build-discarder:
7 num-to-keep: 1
8 - rebuild
9 parameters:
10 - matrix-combinations:
11 name: nodes
12 axes:
13 - axis:
14 type: slave
15 name: node
16 values: '{obj:build_slaves}'
17 wrappers:
18 - timestamps
19 builders:
20 - shell:
21 !include-raw: credentials-1-launchpad.py
diff --git a/jobs/infrastructure/credentials-2-launchpad-plugin.sh b/jobs/infrastructure/credentials-2-launchpad-plugin.sh
0new file mode 10064422new file mode 100644
index 0000000..d47164f
--- /dev/null
+++ b/jobs/infrastructure/credentials-2-launchpad-plugin.sh
@@ -0,0 +1,108 @@
1#!/bin/bash -ex
2#
3# Copyright (C) 2016 Canonical Ltd
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17 # Apply the configuration file.
18CONFIG_DIR="${{JENKINS_HOME}}/.jlp"
19CONFIG_PATH="${{CONFIG_DIR}}/jlp.config"
20
21if [ ! -d "${{CONFIG_DIR}}" ]; then
22 mkdir -p "${{CONFIG_DIR}}"
23fi
24
25cat > "${{CONFIG_PATH}}" << EOF
26#You must explicitely allow users to trigger the jobs on your jenkins
27#Otherwise anybody can run arbitrary code on your jenkins servers.
28allowed_users: [{allowed_users}]
29
30#path to your credentials file. The first time you run one of these scripts,
31#launchpad will ask you to authenticate (via a provided URL). Once you do so
32#(in launchpad) you won't need to do this again.
33#If your jenkins "lives" in /var/lib/jenkins you probably don't need to change
34#this
35credential_store_path: {credentials_path}
36
37# When doing a dput into ppa (in autoland.py) a new changelog entry is
38# generated. DEBEMAIL and DEBFULLNAME are used to generate the entry correctly.
39# Please note that the gpg keys of the user specified here must be available
40# on the host where autoland.py is running
41DEBEMAIL:
42DEBFULLNAME:
43
44#user and password for accessing jenkins. This is needed as we need to find
45#out if a job is being published to public jenkins or not. The user needs to be
46#able to see the job configuration
47jenkins_user: "{bot_username}"
48jenkins_password: "${{jenkins_api_token}}"
49
50#Actual URL of your jenkins (e.g. the jenkins backend URL)
51jenkins_url: "{backend_url}"
52
53#Proxy URL of your jenkins (e.g. the URL accessed by users)
54jenkins_proxy_url: "${{JENKINS_URL}}"
55
56#Token to pass when triggering a jenkins build (leave blank for none)
57jenkins_build_token: "BUILD_ME"
58
59# console output from the following jobs will not be printed to the
60# affected merge proposal (in the "Executed test runs:" section)
61jobs_blacklisted_from_messages:
62{blacklisted_jobs}
63
64#message that is used for "testing in progress" comment
65launchpad_build_in_progress_message: "Jenkins: testing in progress"
66
67#login of the launchpad user you will be using for this plugin
68#ideally this user is part of your project group
69launchpad_login: {bot_username}
70
71#Review type that is used for voting on merge proposals.
72#Usually you don't need to change this
73launchpad_review_type: continuous-integration
74
75# directory containing lockfiles for Launchpad merge proposals
76launchpadlocks_dir: /tmp/jenkins-launchpad-plugin/locks
77
78#lock file that is being used to limit the number of parallel launchpad
79#connections
80lock_name: launchpad-trigger-lock
81
82#you don't need to change this
83lp_app: launchpad-trigger
84
85#which launchpad are you using (production/staging)
86#you don't need to change this
87lp_env: production
88
89#URL of your public jenkins in case you are publishing your jobs to some
90#other jenkins
91public_jenkins_url:
92
93#in case you are running jenkins in a private infrastructure you probably don't
94#want to expose your private IPs in public merge proposals
95#the following defines (IP, replacement) pairs. Your URLs in merge proposals
96#are then replaced by the replacement (and you can e.g. edit your /etc/hosts
97#so the links still work for you). The form to specify a replacement is:
98#urls_to_hide:
99# - ['http://1.2.3.4:8080','http://jenkins:8080']
100#
101#To specify no replacement:
102#urls_to_hide: []
103urls_to_hide: []
104
105# verbosity of the commands
106# one of: debug, info, warning, error, critical
107log_level: debug
108EOF
diff --git a/jobs/infrastructure/credentials-2-launchpad-plugin.yaml b/jobs/infrastructure/credentials-2-launchpad-plugin.yaml
0new file mode 100644109new file mode 100644
index 0000000..0d8ca61
--- /dev/null
+++ b/jobs/infrastructure/credentials-2-launchpad-plugin.yaml
@@ -0,0 +1,24 @@
1- job-template:
2 name: '{name}-credentials-2-launchpad-plugin'
3 project-type: matrix
4 description: This job configures Launchpad integration.
5 properties:
6 - build-discarder:
7 num-to-keep: 1
8 - rebuild
9 parameters:
10 - matrix-combinations:
11 name: nodes
12 - password:
13 name: jenkins_api_token
14 description: Jenkins API key of the "{bot_username}" account
15 axes:
16 - axis:
17 type: slave
18 name: node
19 values: '{obj:build_slaves}'
20 wrappers:
21 - timestamps
22 builders:
23 - shell:
24 !include-raw: credentials-2-launchpad-plugin.sh
diff --git a/jobs/infrastructure/deploy-jenkins-jobs.sh b/jobs/infrastructure/deploy-jenkins-jobs.sh
0new file mode 10064425new file mode 100644
index 0000000..7754fa9
--- /dev/null
+++ b/jobs/infrastructure/deploy-jenkins-jobs.sh
@@ -0,0 +1,58 @@
1#!/bin/sh
2#
3# Copyright (C) 2017 Canonical Ltd
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPO. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17set -ex
18
19. "$WORKSPACE/.build_env"
20
21JENKINS_JOBS_FOLDER="jenkins-jobs"
22JENKINS_CONFIG_FOLDER="config"
23
24# Fetch Jenkaas configuration from config file
25CONFIG_DIR="$JENKINS_HOME/.jlp"
26CONFIG_PATH="$CONFIG_DIR/jlp.config"
27
28cat $CONFIG_PATH
29
30JENKINS_API_TOKEN=$(cat $CONFIG_PATH | grep 'jenkins_password' | sed 's/[a-z_]*:[ ]//g')
31JENKINS_DEPLOYMENT_SCOPE="jobs/infrastructure:jobs/snap"
32
33rm -rf $WORKSPACE/$JENKINS_JOBS_FOLDER
34
35# Clone the jenkins-jobs and jenkins-config repositories
36git clone -b $JENKINS_JOBS_BRANCH $JENKINS_JOBS_REPO $JENKINS_JOBS_FOLDER
37cd $JENKINS_JOBS_FOLDER
38git clone -b $JENKINS_CONFIG_BRANCH $JENKINS_CONFIG_REPO $JENKINS_CONFIG_FOLDER
39
40# Verify the configuration is present
41PROJECT_CONF="$JENKINS_CONFIG_FOLDER/$JENKINS_INSTANCE.conf"
42PROJECT_YAML="$JENKINS_CONFIG_FOLDER/$JENKINS_INSTANCE.yaml"
43if [ ! -f "$PROJECT_CONF" ] || [ ! -f "$PROJECT_YAML" ]; then
44 echo "Cannot find .conf and .yaml pair for $JENKINS_INSTANCE"
45 exit 1
46fi
47
48# Update the config file with the API key for the Bot.
49sed -i "/<API TOKEN>/c\password=$JENKINS_API_TOKEN" "$PROJECT_CONF"
50
51# Test requested configuration
52/usr/local/bin/jenkins-jobs --conf $PROJECT_CONF test $PROJECT_YAML":"$JENKINS_DEPLOYMENT_SCOPE &>/dev/null
53if [ "$?" -ne 0 ]; then
54 echo "Configuration failed verification"
55 exit 1
56fi
57# Deploy requested configuration
58/usr/local/bin/jenkins-jobs --conf $PROJECT_CONF update $PROJECT_YAML":"$JENKINS_DEPLOYMENT_SCOPE
diff --git a/jobs/infrastructure/deploy-jenkins-jobs.yaml b/jobs/infrastructure/deploy-jenkins-jobs.yaml
0new file mode 10064459new file mode 100644
index 0000000..24b24f0
--- /dev/null
+++ b/jobs/infrastructure/deploy-jenkins-jobs.yaml
@@ -0,0 +1,24 @@
1- job-template:
2 name: '{name}-deploy-jenkins-jobs'
3 project-type: matrix
4 description: |
5 This job will deploy jenkins jobs.
6 properties:
7 - build-discarder:
8 num-to-keep: 1
9 - rebuild
10 parameters:
11 - matrix-combinations:
12 name: nodes
13 axes:
14 - axis:
15 type: slave
16 name: node
17 values: '{obj:build_slaves}'
18 wrappers:
19 - timestamps
20 builders:
21 - shell:
22 !include-raw: common-job-prepare.sh
23 - shell:
24 !include-raw-escape: deploy-jenkins-jobs.sh
diff --git a/jobs/infrastructure/infrastructure-jobs.yaml b/jobs/infrastructure/infrastructure-jobs.yaml
0new file mode 10064425new file mode 100644
index 0000000..d440330
--- /dev/null
+++ b/jobs/infrastructure/infrastructure-jobs.yaml
@@ -0,0 +1,8 @@
1- job-group:
2 name: infrastructure-jobs
3 jobs:
4 - '{name}-prepare-0-install'
5 - '{name}-credentials-0-ssh'
6 - '{name}-credentials-1-launchpad'
7 - '{name}-credentials-2-launchpad-plugin'
8 - '{name}-deploy-jenkins-jobs'
diff --git a/jobs/infrastructure/prepare-0-install.sh b/jobs/infrastructure/prepare-0-install.sh
0new file mode 1006449new file mode 100644
index 0000000..9fae262
--- /dev/null
+++ b/jobs/infrastructure/prepare-0-install.sh
@@ -0,0 +1,47 @@
1#!/bin/sh -ex
2#
3# Copyright (C) 2016 Canonical Ltd
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17sudo apt-get install --yes software-properties-common
18
19# build tools as used in Launchpad
20sudo add-apt-repository --yes ppa:launchpad/buildd-staging
21sudo add-apt-repository --yes ppa:jenkaas-hackers/tools
22sudo add-apt-repository --yes ppa:snappy-hwe-team/ci-tools
23
24sudo apt-get update
25
26sudo apt-get install --yes \
27 git \
28 python \
29 python-launchpadlib \
30 python-bzrlib \
31 python-lockfile \
32 python-yaml \
33 python-jenkins \
34 python-git \
35 python-pip \
36 tarmac \
37 jenkins-launchpad-plugin \
38 openssh-client \
39 debootstrap \
40 qemu \
41 binfmt-support \
42 qemu-user-static \
43 {install_packages}
44
45# Install jenkins-jobs-builder
46sudo pip install --upgrade pip
47sudo pip install jenkins-job-builder
diff --git a/jobs/infrastructure/prepare-0-install.yaml b/jobs/infrastructure/prepare-0-install.yaml
0new file mode 10064448new file mode 100644
index 0000000..32e9612
--- /dev/null
+++ b/jobs/infrastructure/prepare-0-install.yaml
@@ -0,0 +1,27 @@
1- job-template:
2 name: '{name}-prepare-0-install'
3 install_packages: ''
4 project-type: matrix
5 description: |
6 This job adds all the needed repositories and installs dependencies needed
7 on build slaves.
8 properties:
9 - build-discarder:
10 num-to-keep: 10
11 - rebuild
12 parameters:
13 - matrix-combinations:
14 name: configurations
15 description: Which slaves to install packages on
16 - string:
17 name: CLEANUP_WORKSPACE
18 default: "0"
19 axes:
20 - axis:
21 type: slave
22 name: node
23 values: '{obj:build_slaves}'
24 builders:
25 - shell:
26 !include-raw:
27 - prepare-0-install.sh
diff --git a/jobs/snap/common-job-prepare.sh b/jobs/snap/common-job-prepare.sh
0new file mode 10064428new file mode 100644
index 0000000..4be0408
--- /dev/null
+++ b/jobs/snap/common-job-prepare.sh
@@ -0,0 +1,24 @@
1JENKINS_JOBS_GIT_REPO="{jobs-git-repo}"
2JENKINS_JOBS_GIT_REPO_BRANCH="{jobs-git-repo-branch}"
3
4if [ -n "${{CLEANUP_WORKSPACE}}" ] && [ "${{CLEANUP_WORKSPACE}}" -eq 1 ]; then
5 rm -rf ${{WORKSPACE}}/*
6fi
7if [ -e jenkins-jobs ] ; then
8 (cd jenkins-jobs ; git clean -fdx . ; git fetch origin ; git reset --hard origin/${{JENKINS_JOBS_GIT_REPO_BRANCH}})
9else
10 git clone -b ${{JENKINS_JOBS_GIT_REPO_BRANCH}} ${{JENKINS_JOBS_GIT_REPO}}
11fi
12
13cat << EOF > $WORKSPACE/.build_env
14BOT_USERNAME={bot_username}
15LAUNCHPAD_PROJECT={launchpad_project}
16LAUNCHPAD_TEAM={launchpad_team}
17SNAP_BUILD_JOB={name}-snap-build
18BUILD_SCRIPTS=$WORKSPACE/jenkins-jobs
19BUILD_ON_LAUNCHPAD={build_on_launchpad}
20AUTO_MERGE={auto_merge}
21TRIGGER_CI={trigger_ci}
22UPDATE_MPS={update_mps}
23RUN_TESTS={run_tests}
24EOF
diff --git a/jobs/snap/snap-automerger.sh b/jobs/snap/snap-automerger.sh
0new file mode 10064425new file mode 100644
index 0000000..d955e52
--- /dev/null
+++ b/jobs/snap/snap-automerger.sh
@@ -0,0 +1,27 @@
1#!/bin/bash
2#
3# Copyright (C) 2017 Canonical Ltd
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17set -ex
18
19. "$WORKSPACE/.build_env"
20
21if [ "$AUTO_MERGE" = False ]; then
22 echo "WARNING: auto merge is disabled"
23 exit 0
24fi
25
26exec $BUILD_SCRIPTS/tools/automerge-mps.py \
27 -p $LAUNCHPAD_PROJECT
0\ No newline at end of file28\ No newline at end of file
diff --git a/jobs/snap/snap-automerger.yaml b/jobs/snap/snap-automerger.yaml
1new file mode 10064429new file mode 100644
index 0000000..f55f252
--- /dev/null
+++ b/jobs/snap/snap-automerger.yaml
@@ -0,0 +1,22 @@
1- job-template:
2 name: '{name}-snap-automerger'
3 project-type: freestyle
4 defaults: global
5 description: "Monitor Launchpad for new merge proposals"
6 display-name: "{name}-snap-automerger"
7 concurrent: true
8 node: snap && misc
9 triggers:
10 - timed: # every five minutes
11 H/5 * * * *
12 properties:
13 - build-discarder:
14 num-to-keep: 10
15 - rebuild
16 builders:
17 - shell:
18 !include-raw:
19 - common-job-prepare.sh
20 - shell:
21 !include-raw-escape:
22 - snap-automerger.sh
diff --git a/jobs/snap/snap-build-update-chroot.sh b/jobs/snap/snap-build-update-chroot.sh
0new file mode 10064423new file mode 100644
index 0000000..912f687
--- /dev/null
+++ b/jobs/snap/snap-build-update-chroot.sh
@@ -0,0 +1,28 @@
1#!/bin/sh
2#
3# Copyright (C) 2016 Canonical Ltd
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17set -ex
18
19. "$WORKSPACE/.build_env"
20
21if [ "$BUILD_ON_LAUNCHPAD" != False ]; then
22 exit 0
23fi
24
25sudo $BUILD_SCRIPTS/tools/snapbuild.sh \
26 --update-chroot \
27 --arch=$ARCHITECTURE \
28 --series=$SERIES
0\ No newline at end of file29\ No newline at end of file
diff --git a/jobs/snap/snap-build-update-chroot.yaml b/jobs/snap/snap-build-update-chroot.yaml
1new file mode 10064430new file mode 100644
index 0000000..e50b3c1
--- /dev/null
+++ b/jobs/snap/snap-build-update-chroot.yaml
@@ -0,0 +1,29 @@
1- job-template:
2 name: '{name}-snap-build-update-chroot'
3 project-type: matrix
4 defaults: global
5 description: "Build a snap on launchpad"
6 display-name: "{name}-snap-build-update-chroot"
7 concurrent: false
8 node: snap && build
9 axes:
10 - axis:
11 type: slave
12 name: node
13 values: '{obj:build_slaves}'
14 parameters:
15 - string:
16 name: ARCHITECTURE
17 default: amd64
18 description: Architecture to build the snap for
19 - string:
20 name: SERIES
21 default: xenial
22 description: "Ubuntu archive series to build for"
23 builders:
24 - shell:
25 !include-raw:
26 - common-job-prepare.sh
27 - shell:
28 !include-raw-escape:
29 - snap-build-update-chroot.sh
diff --git a/jobs/snap/snap-build-worker.sh b/jobs/snap/snap-build-worker.sh
0new file mode 10064430new file mode 100644
index 0000000..60775d7
--- /dev/null
+++ b/jobs/snap/snap-build-worker.sh
@@ -0,0 +1,147 @@
1#!/bin/sh
2#
3# Copyright (C) 2016 Canonical Ltd
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17set -ex
18
19. "$WORKSPACE/.build_env"
20
21rm -rf $WORKSPACE/src $WORKSPACE/results $WORKSPACE/build-props
22
23git clone --no-checkout $TARGET_GIT_REPO $WORKSPACE/src
24cd $WORKSPACE/src
25for remote in `git branch -r | grep -v origin/master`; do git checkout --track $remote ; done
26
27git checkout $TARGET_GIT_REPO_BRANCH
28
29git config user.name "System Enablement CI Bot"
30git config user.email "ce-system-enablement@lists.canonical.com"
31
32if [ -n "$SOURCE_GIT_REPO" ]; then
33 git remote add other $SOURCE_GIT_REPO
34 git fetch other
35 git merge \
36 --no-ff \
37 -m "Merge remote tracking branch other/$SOURCE_GIT_REPO_BRANCH" \
38 $REVISION
39fi
40
41# Try to find the correct branch we need to build from. In the case that
42# $TARGET_GIT_REPO_BRANCH points us to an upstream component branch we
43# will take master as the next suitable candidate.
44CI_BRANCH=
45SNAPCRAFT_YAML_PATH=
46for branch in $TARGET_GIT_REPO_BRANCH master ; do
47 git checkout $branch
48 if [ -e snapcraft.yaml ]; then
49 SNAPCRAFT_YAML_PATH=snapcraft.yaml
50 elif [ -e snap/snapcraft.yaml ]; then
51 SNAPCRAFT_YAML_PATH=snap/snapcraft.yaml
52 fi
53
54 if [ -n "$SNAPCRAFT_YAML_PATH" ]; then
55 CI_BRANCH=$branch
56 break
57 fi
58done
59
60if [ -z "$CI_BRANCH" ]; then
61 echo "WARNING: Can't build snap as no snapcraft.yaml exists!"
62 exit 0
63fi
64
65REPO_NAME=$(awk -v a="$TARGET_GIT_REPO" 'BEGIN{print substr(a, index(a, "+git/") + 5)}')
66# We rely on the snapcraft.yaml to have the snap name in the first five lines
67# which is the case for all our snaps. This is a bit lazy but the best way to
68# ensure we don't fetch any other name: fields which might be present in the file.
69SNAP_NAME=$(cat $SNAPCRAFT_YAML_PATH | grep -v ^\# | head -n 5 | grep "^name:" | awk '{print $2}')
70SNAP_REV=$(git rev-parse --short HEAD)
71CI_REPO=$REPO_NAME-$BUILD_ID-$SNAP_REV
72
73sed -i "s/~$LAUNCHPAD_TEAM\/$LAUNCHPAD_PROJECT\/+git\/$REPO_NAME/~$LAUNCHPAD_TEAM\/$LAUNCHPAD_PROJECT\/+git\/$CI_REPO/g" \
74 $SNAPCRAFT_YAML_PATH
75
76# The project as two different options of how snaps can be build:
77#
78# 1. Locally in a chroot but only for the host architecture
79# 2. On launchpad for any architecture
80#
81# Which of both options will be used is configured in the
82# $WORKSPACE/.build_env file.
83
84if [ "$BUILD_ON_LAUNCHPAD" = False ]; then
85 SNAPBUILD_EXTRA_ARGS=
86 SNAP_TYPE=$($BUILD_SCRIPTS/tools/shyaml get-value type < $SNAPCRAFT_YAML_PATH || echo app)
87 case "$SNAP_TYPE" in
88 kernel)
89 if [ "$ARCHITECTURE" != amd64 ]; then
90 # If we're building a kernel snap we have to cross-build it
91 # instead of building it in a native environment.
92 SNAPBUILD_EXTRA_ARGS="--cross-build"
93 fi
94 ;;
95 esac
96
97 sudo $BUILD_SCRIPTS/tools/snapbuild.sh \
98 --source-dir=$WORKSPACE/src \
99 --results-dir=$WORKSPACE/results \
100 --arch=$ARCHITECTURE \
101 --series=$SERIES \
102 --proxy=squid.internal:3128 \
103 $SNAPBUILD_EXTRA_ARGS
104else
105 git remote add jenkins-ci git+ssh://$BOT_USERNAME@git.launchpad.net/~$LAUNCHPAD_TEAM/$LAUNCHPAD_PROJECT/+git/$CI_REPO
106 git push jenkins-ci --all
107 git push jenkins-ci --tags
108
109 # Save repo name as soon as it gets created so it can be deleted by the cleanup
110 # job even if this job fails.
111 echo "CI_REPO=$CI_REPO" >> $WORKSPACE/build-props
112 echo "CI_BRANCH=$CI_BRANCH" >> $WORKSPACE/build-props
113
114 EXTRA_ARGS=
115 if [ -n "$ARCHITECTURE" ]; then
116 EXTRA_ARGS="$EXTRA_ARGS --architectures=$ARCHITECTURE"
117 fi
118
119 $BUILD_SCRIPTS/tools/trigger-lp-build.py \
120 -s $SNAP_NAME -n \
121 --git-repo=https://git.launchpad.net/~$LAUNCHPAD_TEAM/$LAUNCHPAD_PROJECT/+git/$CI_REPO \
122 --git-repo-branch=$CI_BRANCH \
123 --results-dir=$WORKSPACE/results \
124 $EXTRA_ARGS
125fi
126
127if [ -z "$REMOTE_WORKER" ]; then
128 echo "INFO: No remote worker defined, not copying artifacts to it"
129 exit 0
130fi
131
132SSH_PATH="${JENKINS_HOME}/.ssh/"
133SSH_KEY_PATH="${SSH_PATH}/git.launchpad.net/$BOT_USERNAME"
134SSH="ssh -i $SSH_KEY_PATH/id_rsa $REMOTE_USER@$REMOTE_WORKER"
135SCP="scp -i $SSH_KEY_PATH/id_rsa"
136
137RESULTS_ID=$(md5sum $(find $WORKSPACE/results/*.snap | tail -n1) | cut -d' ' -f 1)
138REMOTE_RESULTS_BASE_DIR=/home/$REMOTE_USER/results
139
140$SSH mkdir -p $REMOTE_RESULTS_BASE_DIR/$RESULTS_ID
141$SCP $WORKSPACE/results/*.snap $REMOTE_USER@$REMOTE_WORKER:$REMOTE_RESULTS_BASE_DIR/$RESULTS_ID/
142
143# Save the id of our results so it can be used by a subsequent build
144# in a properties file which is then being read from jenkins and its
145# content passed as parameters to triggered builds.
146echo "RESULTS_ID=$RESULTS_ID" >> $WORKSPACE/build-props
147cat $WORKSPACE/build-props
diff --git a/jobs/snap/snap-build-worker.yaml b/jobs/snap/snap-build-worker.yaml
0new file mode 100644148new file mode 100644
index 0000000..6b45a89
--- /dev/null
+++ b/jobs/snap/snap-build-worker.yaml
@@ -0,0 +1,72 @@
1- job-template:
2 name: '{name}-snap-build-worker'
3 project-type: freestyle
4 defaults: global
5 description: "Build a snap on launchpad"
6 display-name: "{name}-snap-build-worker"
7 concurrent: true
8 node: snap && build
9 parameters:
10 - string:
11 name: ARCHITECTURE
12 default: amd64
13 description: Architecture to build the snap for
14 - string:
15 name: TARGET_GIT_REPO
16 default:
17 description: "Target git repository"
18 - string:
19 name: TARGET_GIT_REPO_BRANCH
20 default: master
21 description: "Branch of the target git repository to build from"
22 - string:
23 name: SERIES
24 default: xenial
25 description: "Ubuntu archive series to build for"
26 - string:
27 name: FORCE
28 default: "0"
29 description: "Set to 1 to force the build"
30 - string:
31 name: SOURCE_GIT_REPO
32 default:
33 description: "Source git repository"
34 - string:
35 name: SOURCE_GIT_REPO_BRANCH
36 default:
37 description: "Branch of the source git repository to use"
38 - string:
39 name: MERGE_PROPOSAL
40 default:
41 description: "Link to the merge proposal this build relates to"
42 - string:
43 name: REVISION
44 default:
45 description: "Cleanup the whole workspace"
46 - string:
47 name: CLEANUP_WORKSPACE
48 default: "0"
49 description: "Cleanup the whole workspace"
50 - string:
51 name: REMOTE_WORKER
52 default: "{obj:remote_worker}"
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."
54 - string:
55 name: REMOTE_USER
56 default: "{obj:remote_user}"
57 description: "The remote server username used to ssh to $REMOTE_WORKER."
58 - string:
59 name: CORE_CHANNEL
60 default: stable
61 description: "Channel of the core snap to use for testing the build snap"
62 - string:
63 name: RESULTS_ID
64 default: ''
65 description: "Alphanumeric identifier used to pass build artifacts through different jobs"
66 builders:
67 - shell:
68 !include-raw:
69 - common-job-prepare.sh
70 - shell:
71 !include-raw-escape:
72 - snap-build-worker.sh
diff --git a/jobs/snap/snap-build.yaml b/jobs/snap/snap-build.yaml
0new file mode 10064473new file mode 100644
index 0000000..dec3b19
--- /dev/null
+++ b/jobs/snap/snap-build.yaml
@@ -0,0 +1,91 @@
1- job-template:
2 name: '{name}-snap-build'
3 project-type: matrix
4 defaults: global
5 description: "Build a snap with subsequent test execution"
6 display-name: "{name}-snap-build"
7 concurrent: true
8 sequential: false
9 node: monitor
10 axes:
11 - axis:
12 type: user-defined
13 name: ARCHITECTURE
14 values: '{obj:build_architectures}'
15 parameters:
16 - string:
17 name: TARGET_GIT_REPO
18 default:
19 description: "Target git repository"
20 - string:
21 name: TARGET_GIT_REPO_BRANCH
22 default: master
23 description: "Branch of the target git repository to build from"
24 - string:
25 name: SERIES
26 default: xenial
27 description: "Ubuntu archive series to build for"
28 - string:
29 name: FORCE
30 default: "0"
31 description: "Set to 1 to force the build"
32 - string:
33 name: SOURCE_GIT_REPO
34 default:
35 description: "Source git repository"
36 - string:
37 name: SOURCE_GIT_REPO_BRANCH
38 default:
39 description: "Branch of the source git repository to use"
40 - string:
41 name: MERGE_PROPOSAL
42 default:
43 description: "Link to the merge proposal this build relates to"
44 - string:
45 name: REVISION
46 default:
47 description: "Cleanup the whole workspace"
48 - string:
49 name: CLEANUP_WORKSPACE
50 default: "0"
51 description: "Cleanup the whole workspace"
52 builders:
53 - trigger-builds:
54 - project: '{name}-snap-build-worker'
55 current-parameters: true
56 predefined-parameters: |
57 RESULTS_ID=$BUILD_TAG
58 ARCHITECTURE=$ARCHITECTURE
59 block: true
60 - project: '{name}-snap-test'
61 current-parameters: true
62 predefined-parameters: |
63 RESULTS_ID=$BUILD_TAG
64 block: true
65 - project: '{name}-snap-cleanup'
66 current-parameters: true
67 predefined-parameters: |
68 RESULTS_ID=$BUILD_TAG
69 publishers:
70 - archive:
71 artifacts: '**/*.snap'
72 latest-only: false
73 allow-empty: true
74 fingerprint: false
75 - trigger-parameterized-builds:
76 - project: '{name}-snap-update-mp'
77 condition: "SUCCESS"
78 predefined-parameters: |
79 CI_RESULT=PASSED
80 CI_BUILD=${{BUILD_URL}}
81 CI_BRANCH="${{SOURCE_GIT_REPO_BRANCH}}@${{SOURCE_GIT_REPO}}"
82 CI_MERGE_PROPOSAL=${{MERGE_PROPOSAL}}
83 CI_REVISION=${{REVISION}}
84 - project: '{name}-snap-update-mp'
85 condition: "UNSTABLE_OR_WORSE"
86 predefined-parameters: |
87 CI_RESULT=FAILED
88 CI_BUILD=${{BUILD_URL}}
89 CI_BRANCH="${{SOURCE_GIT_REPO_BRANCH}}@${{SOURCE_GIT_REPO}}"
90 CI_MERGE_PROPOSAL=${{MERGE_PROPOSAL}}
91 CI_REVISION=${{REVISION}}
diff --git a/jobs/snap/snap-cleanup.sh b/jobs/snap/snap-cleanup.sh
0new file mode 10064492new file mode 100644
index 0000000..bf0e03c
--- /dev/null
+++ b/jobs/snap/snap-cleanup.sh
@@ -0,0 +1,38 @@
1#!/bin/sh
2#
3# Copyright (C) 2017 Canonical Ltd
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17set -x
18
19. "$WORKSPACE/.build_env"
20
21# Delete auxiliary repo used in the build
22$BUILD_SCRIPTS/tools/delete-ci-repo.py \
23 --git-repo=https://git.launchpad.net/~$LAUNCHPAD_TEAM/$LAUNCHPAD_PROJECT/+git/$CI_REPO
24
25if [ -z "$REMOTE_WORKER" ]; then
26 echo "INFO: No remote system defined"
27 exit 0
28fi
29
30SSH_PATH="${JENKINS_HOME}/.ssh/"
31SSH_KEY_PATH="${SSH_PATH}/git.launchpad.net/$BOT_USERNAME"
32SSH="ssh -i $SSH_KEY_PATH/id_rsa $REMOTE_USER@$REMOTE_WORKER"
33REMOTE_RESULTS_BASE_DIR=/home/$REMOTE_USER/results
34
35$SSH rm -rf $REMOTE_RESULTS_BASE_DIR/$RESULTS_ID
36
37# Now remove any container that might have been left behind...
38$SSH sudo docker rm \$\(sudo docker ps -q --filter=status=exited --filter=ancestor=snap-spread-tests\) || true
diff --git a/jobs/snap/snap-cleanup.yaml b/jobs/snap/snap-cleanup.yaml
0new file mode 10064439new file mode 100644
index 0000000..8aa4a03
--- /dev/null
+++ b/jobs/snap/snap-cleanup.yaml
@@ -0,0 +1,32 @@
1- job-template:
2 name: '{name}-snap-cleanup'
3 project-type: freestyle
4 defaults: global
5 description: "Cleanup artifacts left over from a snap build"
6 display-name: "{name}-snap-cleanup"
7 concurrent: true
8 node: snap && build
9 parameters:
10 - string:
11 name: CI_REPO
12 default: ""
13 description: "Auxiliary repo for the build, that we will remove"
14 - string:
15 name: "RESULTS_ID"
16 default: ""
17 description: "Alphanumeric Id of the results being staged on the remote worker"
18 - string:
19 name: REMOTE_WORKER
20 default: "{obj:remote_worker}"
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."
22 - string:
23 name: REMOTE_USER
24 default: "{obj:remote_user}"
25 description: "The remote server username used to ssh to $REMOTE_WORKER."
26 builders:
27 - shell:
28 !include-raw:
29 - common-job-prepare.sh
30 - shell:
31 !include-raw-escape:
32 - snap-cleanup.sh
diff --git a/jobs/snap/snap-nightly.yaml b/jobs/snap/snap-nightly.yaml
0new file mode 10064433new file mode 100644
index 0000000..ae5a71f
--- /dev/null
+++ b/jobs/snap/snap-nightly.yaml
@@ -0,0 +1,41 @@
1- job-template:
2 name: '{name}-snap-nightly'
3 project-type: matrix
4 concurrent: false
5 node: monitor
6 sequential: true
7 properties:
8 - build-discarder:
9 days-to-keep: 30
10 - rebuild:
11 rebuild-disabled: true
12 axes:
13 - axis:
14 type: user-defined
15 name: CORE_CHANNEL
16 values: '{obj:nightly_core_channels}'
17 - axis:
18 type: user-defined
19 name: SNAP
20 values: '{obj:nightly_snaps}'
21 - axis:
22 type: user-defined
23 name: ARCHITECTURE
24 values: '{obj:nightly_architectures}'
25 wrappers:
26 - timeout:
27 timeout: 30
28 abort: true
29 - timestamps
30 builders:
31 - trigger-builds:
32 - project: '{name}-snap-build-worker'
33 predefined-parameters: |
34 TARGET_GIT_REPO={base_snap_repo_url}/$SNAP
35 TARGET_GIT_REPO_BRANCH=master
36 SOURCE_GIT_REPO=
37 SOURCE_GIT_REPO_BRANCH=
38 REVISION=
39 ARCHITECTURES=$ARCHITECTURE
40 CORE_CHANNEL=$CORE_CHANNEL
41 block: true
diff --git a/jobs/snap/snap-project-jobs.yaml b/jobs/snap/snap-project-jobs.yaml
0new file mode 10064442new file mode 100644
index 0000000..b693af3
--- /dev/null
+++ b/jobs/snap/snap-project-jobs.yaml
@@ -0,0 +1,13 @@
1- job-group:
2 name: snap-project-jobs
3 jobs:
4 - '{name}-snap-nightly'
5 - '{name}-snap-build-worker'
6 - '{name}-snap-build-update-chroot'
7 - '{name}-snap-build'
8 - '{name}-snap-cleanup'
9 - '{name}-snap-release'
10 - '{name}-snap-test'
11 - '{name}-snap-trigger-ci'
12 - '{name}-snap-update-mp'
13 - '{name}-snap-automerger'
diff --git a/jobs/snap/snap-release.sh b/jobs/snap/snap-release.sh
0new file mode 10064414new file mode 100644
index 0000000..06fc6c4
--- /dev/null
+++ b/jobs/snap/snap-release.sh
@@ -0,0 +1,170 @@
1#!/bin/bash
2#
3# Copyright (C) 2017 Canonical Ltd
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17set -ex
18
19. "$WORKSPACE/.build_env"
20
21if [ -z "$VERSION" ]; then
22 echo "ERROR: No version specified"
23 exit 1
24fi
25
26echo "Snap to be released: $SNAP_NAME"
27echo "Version to be released: $VERSION"
28echo "New development version: $NEXT_VERSION"
29
30REPOSITORY_URL="git+ssh://$BOT_USERNAME@git.launchpad.net/~$LAUNCHPAD_TEAM/$LAUNCHPAD_PROJECT/+git/$SNAP_NAME"
31
32if [ -e "$SNAP_NAME" ]; then
33 rm -rf "$SNAP_NAME"
34fi
35
36set_git_identity() {
37 git config user.name "System Enablement CI Bot"
38 git config user.email "ce-system-enablement@lists.canonical.com"
39}
40
41# Arguments are:
42# $1 Snap name
43# $2 Version to release
44update_changelog() {
45 local latest_version commits i text range
46 local snap=$1
47 local ver=$2
48 local changelog_file=ChangeLog
49 local changes author full_text
50
51 # latest tag is latest version
52 latest_version="$(git describe --abbrev=0)" || true
53 if [ -n "$latest_version" ]; then
54 range=$latest_version..HEAD
55 else
56 range=HEAD
57 fi
58
59 commits=$(git rev-list --merges --reverse "$range")
60 declare -A changes
61
62 for i in $commits; do
63 local body merge_proposal description text
64
65 body=$(git log --format=%B -n1 "$i")
66 merge_proposal=$(echo "$body" | grep "^Merge-Proposal:") || true
67 author=$(echo "$body" | grep "^Author:") || true
68 author="${author#Author: *}"
69 if [ -z "$author" ]; then
70 if [ -z "$merge_proposal" ]; then
71 author="unknown"
72 else
73 author=${merge_proposal#*~}
74 author=${author%%/*}
75 fi
76 fi
77 # 'sed' removes leading blank lines first, then adds indentation
78 description=$(echo "$body" | grep -v "^Author:\|^Merge" | \
79 sed '/./,$!d' | sed '2,$s/^/ /') || true
80 if [ -z "$description" ]; then
81 description="See more information in merge proposal"
82 fi
83 text=${changes[$author]}
84 printf -v text "%s\n * %s\n %s" \
85 "$text" "$description" "$merge_proposal"
86 changes[$author]=$text
87 done
88
89 printf -v full_text "%s\n" "$(date --rfc-3339=date --utc) $snap $ver"
90 for author in "${!changes[@]}"; do
91 printf -v full_text "%s\n [ %s ]%s\n" \
92 "$full_text" "$author" "${changes[$author]}"
93 done
94
95 if [ ! -e "$changelog_file" ]; then
96 touch "$changelog_file"
97 fi
98 echo "$full_text" | cat - "$changelog_file" > "$changelog_file".tmp
99 mv "$changelog_file".tmp "$changelog_file"
100
101 git add "$changelog_file"
102 git commit -m "Update $changelog_file for $ver"
103}
104
105# Arguments are:
106# $1 Version to be set in the snapcraft.yaml file
107# $2 Path to the snapcraft.yaml file
108bump_version_and_tag() {
109 sed -i -e "s/^version:\ .*/version: $1/g" "$2"
110 git add "$2"
111 git commit -m "Bump version to $1"
112 git tag -a -m "$1" "$1" HEAD
113}
114
115RELEASE_BASE_BRANCH=master
116if [ "$RELEASE_FROM_STABLE" -eq 1 ]; then
117 RELEASE_BASE_BRANCH=stable
118fi
119
120git clone -b "$RELEASE_BASE_BRANCH" "$REPOSITORY_URL" "$SNAP_NAME"
121cd "$SNAP_NAME"
122
123SNAPCRAFT_YAML_PATH=
124if [ -e snapcraft.yaml ]; then
125 SNAPCRAFT_YAML_PATH=snapcraft.yaml
126elif [ -e snap/snapcraft.yaml ]; then
127 SNAPCRAFT_YAML_PATH=snap/snapcraft.yaml
128fi
129
130if [ -z "$SNAPCRAFT_YAML_PATH" ]; then
131 echo "ERROR: No snapcraft.yaml or snap/snapcraft.yaml file!"
132 exit 1
133fi
134
135set_git_identity
136update_changelog "$SNAP_NAME" "$VERSION"
137bump_version_and_tag "$VERSION" "$SNAPCRAFT_YAML_PATH"
138
139if [ "$RELEASE_FROM_STABLE" -eq 1 ]; then
140 git push origin "$RELEASE_BASE_BRANCH"
141 git push origin "$VERSION"
142
143 "$BUILD_SCRIPTS"/tools/trigger-lp-build.py -s "$SNAP_NAME" -p
144else
145 if ! git branch -r | grep origin/stable ; then
146 git checkout -b stable origin/master
147 else
148 git checkout -b stable origin/stable
149 fi
150
151 # We're using `-X theirs` here as master always takes priority over
152 # what is in the stable. If something was only submitted into stable
153 # the commiter needs to take care that the same change is submitted
154 # to master too or it is overriden the next time a release happens
155 # from master.
156 git merge --no-ff -X theirs "$RELEASE_BASE_BRANCH"
157
158 git push origin stable
159 git push origin "$RELEASE_BASE_BRANCH"
160 git push origin "$VERSION"
161
162 # Build before we change master branch
163 "$BUILD_SCRIPTS"/tools/trigger-lp-build.py -s "$SNAP_NAME" -p
164
165 git checkout "$RELEASE_BASE_BRANCH"
166 sed -i -e "s/^version:\ .*/version: ${NEXT_VERSION}-dev/g" "$SNAPCRAFT_YAML_PATH"
167 git add "$SNAPCRAFT_YAML_PATH"
168 git commit -m "Open development for ${NEXT_VERSION}-dev"
169 git push origin "$RELEASE_BASE_BRANCH"
170fi
diff --git a/jobs/snap/snap-release.yaml b/jobs/snap/snap-release.yaml
0new file mode 100644171new file mode 100644
index 0000000..75e3b3a
--- /dev/null
+++ b/jobs/snap/snap-release.yaml
@@ -0,0 +1,58 @@
1- job-template:
2 name: '{name}-snap-release'
3 project-type: freestyle
4 defaults: global
5 description: "A job implementing the release process used for snaps"
6 display-name: "{name}-snap-release"
7 concurrent: true
8 node: snap && release
9 parameters:
10 - string:
11 name: SNAP_NAME
12 default: ""
13 description: |
14 Name of the snap which should be released
15
16 Normally the repositories we have on https://code.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/
17 match with the snap name. In some cases this is not true, in those you have to set the repository
18 name here. For example for 'canonical-se-engineering-tests' it is 'engineering-tests' as this is
19 the repository name.
20 - string:
21 name: VERSION
22 default: ""
23 description: "New version of the snap"
24 - string:
25 name: NEXT_VERSION
26 default: ""
27 description: |
28 Version which will follow the version specified in the VERSION field.
29 For example if you specify VERSION = "1.1" next version is most likely
30 "1.2". The NEXT_VERSION parameter is used to write it into the component
31 snapcraft.yaml as "$NEXT_VERSION-dev" to clearly indicate that snaps
32 build from master are development versions.
33
34 Please note that NEXT_VERSION is not set in stone and can be overriden
35 at any time by changes merged into master.
36 - string:
37 name: CLEANUP_WORKSPACE
38 default: "0"
39 description: "Cleanup the whole workspace"
40 - string:
41 name: SERIES
42 default: xenial
43 description: "Ubuntu archive series to build for"
44 - string:
45 name: RELEASE_FROM_STABLE
46 default: 0
47 description: |
48 Set to '1' to force a release from stable branch without merging with
49 master. This can be used when single changes are picked into stable
50 manually and need to be released without pulling anything else from
51 master. PLEASE ENSURE THAT THOSE CHANGE GO INTO MASTER TOO!!
52 builders:
53 - shell:
54 !include-raw:
55 - common-job-prepare.sh
56 - shell:
57 !include-raw-escape:
58 - snap-release.sh
diff --git a/jobs/snap/snap-test.sh b/jobs/snap/snap-test.sh
0new file mode 10064459new file mode 100644
index 0000000..bea5e3a
--- /dev/null
+++ b/jobs/snap/snap-test.sh
@@ -0,0 +1,121 @@
1#!/bin/sh
2#
3# Copyright (C) 2016 Canonical Ltd
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17set -ex
18
19. "$WORKSPACE/.build_env"
20
21if [ "$RUN_TESTS" = False ]; then
22 echo "WARNING: test execution is disabled"
23 exit 0
24fi
25
26SSH_PATH="${JENKINS_HOME}/.ssh/"
27SSH_KEY_PATH="${SSH_PATH}/git.launchpad.net/system-enablement-ci-bot"
28
29SSH="ssh -i $SSH_KEY_PATH/id_rsa $REMOTE_USER@$REMOTE_WORKER"
30SCP="scp -i $SSH_KEY_PATH/id_rsa"
31
32REMOTE_WORKSPACE=/home/$REMOTE_USER/$BUILD_TAG
33REMOTE_RESULTS_BASE_DIR=/home/$REMOTE_USER/results
34
35tmp_srcdir=`mktemp -d`
36git clone --depth 1 -b $SOURCE_GIT_REPO_BRANCH $SOURCE_GIT_REPO $tmp_srcdir/src
37cd $tmp_srcdir/src
38# This will fail as we have set set -e above when the revision isn't part of
39# of the repository we've cloned.
40git branch --contains $SOURCE_GIT_REPO_REVISION | grep "$SOURCE_GIT_REPO_BRANCH"
41git checkout -b ci-test $SOURCE_GIT_REPO_REVISION
42
43# Components have the ability to disable CI tests if they can't provide any.
44# This only accepted in a few cases and should be generally avoided.
45if [ -e $tmp_srcdir/src/.ci_tests_disabled ]; then
46 echo "WARNING: Component has no CI tests so not running anything here"
47 exit 0
48fi
49# We require either a run-tests.sh interface script for the spread tests
50# or the basic spread.yaml spread test definition file, otherwise we fail
51# the Jenkins job. Spread tests are required for all MRs.
52if [ ! -e "$tmp_srcdir/src/run-tests.sh" ] && [ ! -e "$tmp_srcdir/src/spread.yaml" ]; then
53 echo "ERROR: missing spread test: you must provide a spread test"
54 exit 1
55fi
56rm -rf $tmp_srcdir
57
58if [ -n "$RESULTS_ID" ]; then
59 $SSH mkdir -p $REMOTE_WORKSPACE/results
60 $SSH cp -v $REMOTE_RESULTS_BASE_DIR/$RESULTS_ID/*.snap $REMOTE_WORKSPACE/results
61fi
62
63$SSH sudo apt-get --yes --force-yes install docker.io
64
65cat << EOF > $WORKSPACE/run-tests.sh
66#!/bin/sh
67set -ex
68
69export TERM=linux
70export DEBIAN_FRONTEND=noninteractive
71export PATH=/build/bin:$PATH
72
73# At this time it's necessary to build spread manually because
74# the snapped version does not include the qemu/kvm backend.
75# Once the snapped version includes this backend, then we can
76# change the manual building of spread with making sure the snap
77# package is installed.
78export GOPATH=`mktemp -d`
79go get -d -v github.com/snapcore/spread/...
80go build github.com/snapcore/spread/cmd/spread
81mkdir /build/bin
82cp spread /build/bin
83
84git clone --depth 1 -b $SOURCE_GIT_REPO_BRANCH $SOURCE_GIT_REPO /build/src
85cd /build/src
86git checkout -b ci-tests $SOURCE_GIT_REPO_REVISION
87
88# Copy any stage results from previous generic-build-snap-worker builds
89cp -v /build/results/*.snap /build/src
90
91if [ -e "run-tests.sh" ] ; then
92 if [ ! -z "$CHANNEL" ] ; then
93 ./run-tests.sh --channel=$CHANNEL --test-from-channel --debug --force-new-image
94 else
95 ./run-tests.sh --debug --force-new-image
96 fi
97else
98 if [ ! -z "$CHANNEL" ] ; then
99 SNAP_CHANNEL=$CHANNEL spread -debug
100 else
101 spread -debug
102 fi
103fi
104EOF
105
106$SSH mkdir -p $REMOTE_WORKSPACE
107$SCP $WORKSPACE/run-tests.sh $REMOTE_USER@$REMOTE_WORKER:$REMOTE_WORKSPACE
108$SSH chmod u+x $REMOTE_WORKSPACE/run-tests.sh
109
110$SSH mkdir -p $REMOTE_WORKSPACE/docker
111$SCP $WORKSPACE/build-scripts/docker/spread-tests/Dockerfile \
112 $REMOTE_USER@$REMOTE_WORKER:$REMOTE_WORKSPACE/docker
113$SSH time sudo docker build -t snap-spread-tests $REMOTE_WORKSPACE/docker
114
115$SSH time sudo docker run \
116 -v /dev:/dev \
117 -v $REMOTE_WORKSPACE:/build \
118 --privileged \
119 snap-spread-tests /build/run-tests.sh
120
121$SSH sudo rm -rf $REMOTE_WORKSPACE
diff --git a/jobs/snap/snap-test.yaml b/jobs/snap/snap-test.yaml
0new file mode 100644122new file mode 100644
index 0000000..9c15699
--- /dev/null
+++ b/jobs/snap/snap-test.yaml
@@ -0,0 +1,64 @@
1- job-template:
2 name: '{name}-snap-test'
3 project-type: freestyle
4 defaults: global
5 description: "Run tests for a single snap on a remote agent which also allows spread execution inside KVM/QEMU"
6 display-name: "{name}-snap-test"
7 concurrent: true
8 node: snap && test
9 parameters:
10 - string:
11 name: SOURCE_GIT_REPO
12 default: ""
13 description: "Source git repository"
14 - string:
15 name: SOURCE_GIT_REPO_BRANCH
16 default: ""
17 description: "Branch of the source git repository to use"
18 - string:
19 name: CHANNEL
20 default: ""
21 description: "Run tests against an image build with a core snap from the specified channel"
22 - string:
23 name: REMOTE_WORKER
24 default: "{obj:remote_worker}"
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."
26 - string:
27 name: REMOTE_USER
28 default: "{obj:remote_user}"
29 description: "The remote server username used to ssh to $REMOTE_WORKER."
30 - string:
31 name: CLEANUP_WORKSPACE
32 default: "0"
33 description: "Cleanup the whole workspace"
34 - string:
35 name: SERIES
36 default: xenial
37 description: "Ubuntu archive series to build for"
38 - string:
39 name: REBUILD_ROOTFS
40 default: 0
41 description: "Rebuild the chroot rootfs or not. Default is to not rebuild and use a previous job's rootfs."
42 - string:
43 name: RESULTS_ID
44 default: ""
45 description: "Alphanumeric Id of the results being staged on the remote worker"
46 - string:
47 name: CORE_CHANNEL
48 default: "stable"
49 description: "Channel used for the core snap inside the test environment. Defaults to 'stable'."
50 - string:
51 name: CI_BRANCH
52 default: ""
53 description: "Branch on which the tests should be executed"
54 - string:
55 name: CI_REPO
56 default: ""
57 description: "Git repository to use for testing (MUST contain $CI_BRANCH)"
58 builders:
59 - shell:
60 !include-raw:
61 - common-job-prepare.sh
62 - shell:
63 !include-raw-escape:
64 - snap-test.sh
diff --git a/jobs/snap/snap-trigger-ci.sh b/jobs/snap/snap-trigger-ci.sh
0new file mode 10064465new file mode 100644
index 0000000..b86724d
--- /dev/null
+++ b/jobs/snap/snap-trigger-ci.sh
@@ -0,0 +1,29 @@
1#!/bin/bash
2#
3# Copyright (C) 2016 Canonical Ltd
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17set -ex
18
19. "$WORKSPACE/.build_env"
20
21if [ "$TRIGGER_CI" = False ]; then
22 echo "WARNING: CI is disabled"
23 exit 0
24fi
25
26exec "$BUILD_SCRIPTS"/tools/trigger-ci.py \
27 -p "$LAUNCHPAD_PROJECT" \
28 -j "$SNAP_BUILD_JOB" \
29 -t "$LAUNCHPAD_TEAM"
diff --git a/jobs/snap/snap-trigger-ci.yaml b/jobs/snap/snap-trigger-ci.yaml
0new file mode 10064430new file mode 100644
index 0000000..e412325
--- /dev/null
+++ b/jobs/snap/snap-trigger-ci.yaml
@@ -0,0 +1,22 @@
1- job-template:
2 name: '{name}-snap-trigger-ci'
3 project-type: freestyle
4 defaults: global
5 description: "Monitor Launchpad for new merge proposals"
6 display-name: "{name}-snap-trigger-ci"
7 concurrent: true
8 node: snap && misc
9 triggers:
10 - timed: # every five minutes
11 H/5 * * * *
12 properties:
13 - build-discarder:
14 num-to-keep: 10
15 - rebuild
16 builders:
17 - shell:
18 !include-raw:
19 - common-job-prepare.sh
20 - shell:
21 !include-raw-escape:
22 - snap-trigger-ci.sh
diff --git a/jobs/snap/snap-update-mp.sh b/jobs/snap/snap-update-mp.sh
0new file mode 10064423new file mode 100644
index 0000000..fd05d6e
--- /dev/null
+++ b/jobs/snap/snap-update-mp.sh
@@ -0,0 +1,30 @@
1#!/bin/sh
2#
3# Copyright (C) 2016 Canonical Ltd
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17set -ex
18
19. "$WORKSPACE/.build_env"
20
21if [ "$UPDATE_MPS" = False ]; then
22 echo "WARNING: MP updates are disabled"
23 exit 0
24fi
25
26exec $BUILD_SCRIPTS/tools/vote-on-merge-proposal.py \
27 -s $CI_RESULT \
28 -u $CI_BUILD \
29 -r $CI_REVISION \
30 -p $CI_MERGE_PROPOSAL
diff --git a/jobs/snap/snap-update-mp.yaml b/jobs/snap/snap-update-mp.yaml
0new file mode 10064431new file mode 100644
index 0000000..967b891
--- /dev/null
+++ b/jobs/snap/snap-update-mp.yaml
@@ -0,0 +1,31 @@
1- job-template:
2 name: '{name}-snap-update-mp'
3 project-type: freestyle
4 defaults: global
5 description: "Update given merge-proposal with the result of the build"
6 display-name: "{name}-snap-update-mp"
7 concurrent: true
8 node: snap && misc
9 parameters:
10 - string:
11 name: CI_RESULT
12 description: Result of the CI build
13 - string:
14 name: CI_BUILD
15 description: Jenkins URL of the build
16 - string:
17 name: CI_BRANCH
18 description: Launchpad branch that was processed
19 - string:
20 name: CI_MERGE_PROPOSAL
21 description: Launchpad merge proposal that was processed
22 - string:
23 name: CI_REVISION
24 description: Revision of the processed branch
25 builders:
26 - shell:
27 !include-raw:
28 - common-job-prepare.sh
29 - shell:
30 !include-raw-escape:
31 - snap-update-mp.sh
diff --git a/local.conf b/local.conf
0new file mode 10064432new file mode 100644
index 0000000..0170c70
--- /dev/null
+++ b/local.conf
@@ -0,0 +1,11 @@
1[job_builder]
2ignore_cache=True
3keep_descriptions=False
4recursive=False
5allow_duplicates=False
6
7[jenkins]
8user=system-enablement-ci-bot
9password=jenkins
10url=http://localhost:8080
11query_plugins_info=False
diff --git a/local.yaml b/local.yaml
0new file mode 10064412new file mode 100644
index 0000000..4de3a0c
--- /dev/null
+++ b/local.yaml
@@ -0,0 +1,60 @@
1- project:
2 name: infrastructure
3
4 jobs-git-repo: https://git.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/jenkins-jobs
5 jobs-git-repo-branch: master
6 config-git-repo: https://git.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/jenkins-config
7 config-git-repo-branch: master
8
9 jenkins-instance: localhost
10
11 bot_username: system-enablement-ci-bot
12 credentials_path: /var/lib/jenkins/.launchpad.credentials
13 allowed_users: "canonical-system-enablement"
14 backend_url: http://localhost:8080/
15 blacklisted_jobs: ""
16 install_packages: ""
17 build_slaves:
18 - master
19 jobs:
20 - infrastructure-jobs
21
22- project:
23 name: system-enablement
24
25 jobs-git-repo: https://git.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/jenkins-jobs
26 jobs-git-repo-branch: master
27
28 bot_username: system-enablement-ci-bot
29
30 allowed_users: "canonical-system-enablement"
31 launchpad_project: "snappy-hwe-snaps"
32 launchpad_team: "snappy-hwe-team"
33
34 update_mps: false
35 run_tests: false
36 build_on_launchpad: false
37 trigger_ci: false
38 auto_merge: false
39
40 build_architectures: [amd64]
41
42 all_slaves:
43 - master
44 build_slaves:
45 - master
46
47 blacklisted_jobs: ""
48
49 remote_worker: ""
50 remote_user: ""
51
52 nightly_architectures: []
53 nightly_core_channels: []
54 nightly_snaps: []
55
56 base_snap_repo_url: 'https://git.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git'
57
58 jobs:
59 - snap-project-jobs
60 - image-project-jobs
diff --git a/run-tests.sh b/run-tests.sh
index 3e02fc5..83cf437 100755
--- a/run-tests.sh
+++ b/run-tests.sh
@@ -1,4 +1,5 @@
1#!/bin/sh1#!/bin/sh
2<<<<<<< run-tests.sh
2#3#
3# Copyright (C) 2016 Canonical Ltd4# Copyright (C) 2016 Canonical Ltd
4#5#
@@ -74,3 +75,6 @@ fi
7475
75echo "INFO: Executing tests runner"76echo "INFO: Executing tests runner"
76cd $TESTS_EXTRAS_PATH && ./tests-runner.sh "$@" "$EXTRA_ARGS"77cd $TESTS_EXTRAS_PATH && ./tests-runner.sh "$@" "$EXTRA_ARGS"
78=======
79echo "Nothing yet!"
80>>>>>>> run-tests.sh
diff --git a/tools/automerge-mps.py b/tools/automerge-mps.py
77new file mode 10075581new file mode 100755
index 0000000..8699dd8
--- /dev/null
+++ b/tools/automerge-mps.py
@@ -0,0 +1,152 @@
1#!/usr/bin/env python
2# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
3#
4# Copyright (C) 2016 Canonical Ltd
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License version 3 as
8# published by the Free Software Foundation.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18from launchpadlib.launchpad import Launchpad
19import os
20import sys
21import yaml
22import git
23import shutil
24import se_utils
25
26from argparse import ArgumentParser
27
28parser = ArgumentParser(description="Trigger snap builds for pending merge proposals")
29parser.add_argument('-p', '--project', required=True,
30 help="Launchpad project to check for merge-proposals")
31
32args = vars(parser.parse_args())
33
34class LaunchpadVote():
35 APPROVE = 'Approve'
36 DISAPPROVE = 'Disapprove'
37 NEEDS_FIXING = 'Needs Fixing'
38
39def load_config():
40 files = [os.path.expanduser('~/.jlp/jlp.config'), 'jlp.config']
41 for config_file in files:
42 try:
43 config = yaml.safe_load(open(config_file, 'r'))
44 return config
45 except IOError:
46 pass
47 print("ERROR: No config file found")
48 sys.exit(1)
49
50def get_config_option(name):
51 config = load_config()
52 return config[name]
53
54def clean_branch_name(branch_name):
55 if branch_name.startswith("refs/heads/"):
56 return branch_name[11:]
57 return branch_name
58
59def correct_ssh_url(url):
60 if not url.startswith("git+ssh://"):
61 return url
62 new_url = "git+ssh://system-enablement-ci-bot@%s" % url[10:]
63 return new_url
64
65
66def try_merge(proposal, target_repo, target_branch, source_repo, source_branch):
67 source_git_url=source_repo.git_https_url
68 # If we're operating with a private git repository we have to use the
69 # SSH url. Otherwise we try to avoid that to not cause any damage on
70 # the source repository.
71 if source_git_url == None:
72 source_git_url = source_repo.git_ssh_url
73
74 print("Trying to merge %s:%s into %s:%s" % (source_git_url, source_branch, target_repo.git_ssh_url, target_branch))
75
76 repo_path = os.path.join(os.environ["WORKSPACE"], "repo")
77 if os.path.exists(repo_path):
78 shutil.rmtree(repo_path)
79
80 repo = git.Repo.clone_from(correct_ssh_url(target_repo.git_ssh_url), repo_path, branch=target_branch)
81 source_remote = repo.create_remote("source", source_git_url)
82 source_remote.fetch()
83
84 repo.git.config("user.name", "System Enablement CI Bot")
85 # FIXME: What is the real email address of the bot?
86 repo.git.config("user.email", "ce-system-enablement@lists.canonical.com")
87
88 registrant_name = proposal.registrant.display_name
89 try:
90 registrant_mail = proposal.registrant.preferred_email_address.email
91 except Exception as e:
92 print("WARNING: cannot get e-mail for %s (%s)" % (registrant_name, e))
93 registrant_mail="(unknown e-mail)"
94
95 repo.git.merge("--no-ff",
96 "-m", "Merge remote tracking branch %s" % (source_branch),
97 "-m", "Merge-Proposal: %s" % proposal.web_link,
98 "-m", "Author: %s <%s>" % (registrant_name, registrant_mail),
99 "-m", "%s" % proposal.description,
100 "source/%s" % source_branch)
101
102 repo.git.push("origin", target_branch)
103
104def get_last_mp_vote(mp):
105 for vote in mp.votes:
106 if not vote.comment:
107 continue
108 if vote.review_type == "continuous-integration" and vote.comment:
109 return vote.comment.vote
110 return None
111
112def mp_is_disapproved(mp):
113 for vote in mp.votes:
114 if vote.comment and vote.comment.vote == LaunchpadVote.DISAPPROVE:
115 return True
116 return False
117
118lp_app = get_config_option("lp_app")
119lp_env = get_config_option("lp_env")
120credential_store_path = get_config_option('credential_store_path')
121launchpad = se_utils.get_launchpad(None, credential_store_path, lp_app, lp_env)
122
123project = launchpad.projects[args['project']]
124proposals = project.getMergeProposals(status=['Approved'])
125
126failed_merges = 0
127
128print("Found %d candidate merge proposals" % len(proposals))
129
130for proposal in proposals:
131 if get_last_mp_vote(proposal) != LaunchpadVote.APPROVE:
132 print("Not merging %s as not approved by CI" % proposal.web_link)
133 continue
134
135 if mp_is_disapproved(proposal):
136 print("Not merging %s as at least one reviewer has disapproved the change" % proposal.web_link)
137 continue
138
139 print("Found proposal which is ready for merging: %s" % proposal.web_link)
140
141 try:
142 try_merge(proposal,
143 launchpad.load(proposal.target_git_repository_link),
144 clean_branch_name(proposal.target_git_path),
145 launchpad.load(proposal.source_git_repository_link),
146 clean_branch_name(proposal.source_git_path))
147 except Exception as e:
148 print("ERROR: Failed to merge %s (%s)" % (proposal.web_link, e))
149 failed_merges += 1
150
151if failed_merges > 0:
152 sys.exit(1)
diff --git a/tools/build-rootfs-create b/tools/build-rootfs-create
0new file mode 100755153new file mode 100755
index 0000000..9a9f9ce
--- /dev/null
+++ b/tools/build-rootfs-create
@@ -0,0 +1,26 @@
1#!/bin/bash
2#
3# Copyright (C) 2016 Canonical Ltd
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17set -x
18set -e
19
20SERIES=$1
21TARBALL=$2
22
23mkdir -p rootfs
24debootstrap --components=main,universe $SERIES rootfs
25tar cf $TARBALL rootfs
26rm -rf rootfs
diff --git a/tools/common.sh b/tools/common.sh
0new file mode 10075527new file mode 100755
index 0000000..c30d2d7
--- /dev/null
+++ b/tools/common.sh
@@ -0,0 +1,93 @@
1#!/bin/sh -ex
2#
3# Copyright (C) 2017 Canonical Ltd
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17# Set common variables used by the jenkins jobs
18set_jenkins_env ()
19{
20 SSH_PATH="${JENKINS_HOME}/.ssh/"
21 SSH_KEY_PATH="${SSH_PATH}/bazaar.launchpad.net/system-enablement-ci-bot"
22
23 SSH="ssh -i $SSH_KEY_PATH/id_rsa $REMOTE_USER@$REMOTE_WORKER"
24 SCP="scp -i $SSH_KEY_PATH/id_rsa"
25
26 REPO=https://git.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/$CI_REPO
27 BRANCH=$CI_BRANCH
28
29 # If no CI repo/branch is set fallback to the source repo/branch set
30 # which will be the case for those repositories which don't contain
31 # a snap.
32 if [ -z "$CI_REPO" ]; then
33 REPO=$SOURCE_GIT_REPO
34 BRANCH=$SOURCE_GIT_REPO_BRANCH
35 fi
36
37 REMOTE_WORKSPACE=/home/$REMOTE_USER/$BUILD_TAG
38 REMOTE_RESULTS_BASE_DIR=/home/$REMOTE_USER/results
39}
40
41# Sets variables
42# TEST_TYPE={script, spread}
43# HW_TESTS_RESULT={0, !=0} -> {has hw tests, does not have hw tests}
44# FIXME Maybe depending on context the call to clone could be avoided
45set_test_type ()
46{
47 tmp_srcdir=$(mktemp -d)
48
49 # We use FAIL to make sure we do not exit until we free tmp_srcdir
50 FAIL=no
51 git clone --depth 1 -b "$BRANCH" "$REPO" "$tmp_srcdir"/src || FAIL=yes
52 cd "$tmp_srcdir"/src || FAIL=yes
53
54 TEST_TYPE=none
55 if [ -e "$tmp_srcdir/src/spread.yaml" ]; then
56 TEST_TYPE=spread
57 fi
58 # run-tests.sh gets priority over spread.yaml
59 if [ -e "$tmp_srcdir/src/run-tests.sh" ]; then
60 TEST_TYPE=script
61 fi
62
63 # TODO: Use https://github.com/0k/shyaml in the future for this
64 if grep -q "type: adhoc" spread.yaml; then
65 HW_TESTS_RESULT=0
66 else
67 HW_TESTS_RESULT=1
68 fi
69
70 # Components have the ability to disable CI tests if they can't provide any.
71 # This is only accepted in a few cases and should be generally avoided.
72 CI_TESTS_DISABLED=no
73 if [ -e "$tmp_srcdir"/src/.ci_tests_disabled ]; then
74 CI_TESTS_DISABLED=yes
75 fi
76
77 rm -rf "$tmp_srcdir"
78
79 if [ "$FAIL" = yes ]; then
80 echo "ERROR: critical in set_test_type()"
81 exit 1
82 fi
83
84 if [ "$CI_TESTS_DISABLED" = yes ]; then
85 echo "WARNING: Component has no CI tests so not running anything here"
86 exit 0
87 fi
88
89 if [ "$TEST_TYPE" = none ]; then
90 echo "ERROR: missing spread or script tests: you must provide one of them"
91 exit 1
92 fi
93}
diff --git a/tools/delete-ci-repo.py b/tools/delete-ci-repo.py
0new file mode 10075594new file mode 100755
index 0000000..35c762d
--- /dev/null
+++ b/tools/delete-ci-repo.py
@@ -0,0 +1,45 @@
1#!/usr/bin/env python
2#
3# Copyright (C) 2017 Canonical Ltd
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17from launchpadlib.launchpad import Launchpad
18
19from argparse import ArgumentParser
20
21import se_utils
22
23print("Running delete-ci-repo")
24
25parser = ArgumentParser(description="Delete a git repository stored in launchpad")
26parser.add_argument('--git-repo', help="Git repository to be deleted")
27
28args = vars(parser.parse_args())
29
30git_repo = args['git_repo']
31ind = git_repo.find('~')
32if ind == -1:
33 print("Bad git repo {}".format(git_repo))
34 exit(1)
35
36lp_repo = git_repo[ind:]
37
38lp_app = se_utils.get_config_option("lp_app")
39lp_env = se_utils.get_config_option("lp_env")
40credential_store_path = se_utils.get_config_option('credential_store_path')
41launchpad = se_utils.get_launchpad(None, credential_store_path, lp_app, lp_env)
42
43repo = launchpad.git_repositories.getByPath(path=lp_repo)
44print("Removing {}".format(lp_repo))
45repo.lp_delete()
diff --git a/tools/hardware-test.sh b/tools/hardware-test.sh
0new file mode 10075546new file mode 100755
index 0000000..41eee60
--- /dev/null
+++ b/tools/hardware-test.sh
@@ -0,0 +1,112 @@
1#!/bin/sh -ex
2#
3# Copyright (C) 2017 Canonical Ltd
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17# Runs tests on real HW. Requires set_jenkins_env and set_test_type to have
18# already run.
19run_hardware_tests ()
20{
21 TEST_RESULTS=test_results
22
23 # Just dragonboard for the moment
24 DRAGONBOARD_TEST=testflinger-dragonboard.yaml
25
26 # We use jq to process testflinger output
27 if ! which jq; then
28 sudo apt install --yes --allow-downgrades --allow-remove-essential \
29 --allow-change-held-packages jq
30 fi
31
32 # Initially the device has a password-less ubuntu user. But spread needs a
33 # user with a password, so we use the DEVICE_USER/DEVICE_PASSWORD pair to
34 # create it. Note: {device_ip} gets substituted by testflinger.
35 DEVICE_USER=test
36 DEVICE_PASSWORD=test
37 DEVICE_SSH="ssh -q -o UserKnownHostsFile=/dev/null
38 -o StrictHostKeyChecking=no -p 22 ubuntu@{device_ip}"
39
40 if [ "$TEST_TYPE" = script ]; then
41 TEST_COMMAND="./run-tests.sh --spread-system=hw-ubuntu-core-16
42 --external-address={device_ip}:22 --external-user=$DEVICE_USER
43 --external-password=$DEVICE_PASSWORD --debug"
44 else
45 TEST_COMMAND="export SPREAD_EXTERNAL_ADDRESS={device_ip}:22 &&
46 export SPREAD_EXTERNAL_USER=$DEVICE_USER &&
47 export SPREAD_EXTERNAL_PASSWORD=$DEVICE_PASSWORD &&
48 ./spread -vv external:hw-ubuntu-core-16"
49 fi
50
51 cd "$WORKSPACE"
52
53 # Run testflinger from our bare metal server so we can install it
54
55 $SSH mkdir -p "$REMOTE_WORKSPACE"
56
57 # If the snap has been built, copy over
58 if [ -n "$RESULTS_ID" ]; then
59 set +x
60 # We need to flatten the key here to avoid issues with yaml parsing
61 SSH_KEY_DATA=$(tr '\n:' '?!' < "$SSH_KEY_PATH"/id_rsa)
62 set -x
63 fi
64
65 cat << EOF > "$WORKSPACE"/"$DRAGONBOARD_TEST"
66job_queue: dragonboard
67provision_data:
68 channel: stable
69test_data:
70 test_cmds:
71 - git clone --depth 1 -b $BRANCH $REPO src
72 - cd src && curl -s -O https://niemeyer.s3.amazonaws.com/spread-amd64.tar.gz && tar xzvf spread-amd64.tar.gz
73 - $DEVICE_SSH "sudo adduser --extrausers --quiet --disabled-password --gecos '' $DEVICE_USER"
74 - $DEVICE_SSH "echo $DEVICE_USER:$DEVICE_PASSWORD | sudo chpasswd"
75 - $DEVICE_SSH "echo '$DEVICE_USER ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/create-user-test"
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
77 - cd src && export PATH=$PATH:\$(pwd) && $TEST_COMMAND
78EOF
79
80 $SCP "$WORKSPACE"/"$DRAGONBOARD_TEST" "$REMOTE_USER"@"$REMOTE_WORKER":"$REMOTE_WORKSPACE"/
81
82 $SSH << EOF
83#!/bin/sh
84set -ex
85
86cd $REMOTE_WORKSPACE
87if ! which virtualenv; then
88 sudo apt install --yes --allow-downgrades --allow-remove-essential --allow-change-held-packagesvirtualenv
89fi
90
91git clone https://git.launchpad.net/testflinger-cli
92cd testflinger-cli
93virtualenv -p python3 env
94. env/bin/activate
95./setup.py install
96
97JOB_ID=\$(testflinger-cli submit -q $REMOTE_WORKSPACE/$DRAGONBOARD_TEST)
98echo "JOB_ID: \${JOB_ID}"
99
100testflinger-cli poll \${JOB_ID}
101testflinger-cli results \${JOB_ID} > $REMOTE_WORKSPACE/$TEST_RESULTS
102EOF
103
104 $SCP "$REMOTE_USER"@"$REMOTE_WORKER":"$REMOTE_WORKSPACE"/"$TEST_RESULTS" "$WORKSPACE"/
105
106 $SSH sudo rm -rf "$REMOTE_WORKSPACE"
107
108 TEST_STATUS=$(jq -r .test_status "$WORKSPACE"/"$TEST_RESULTS")
109 echo "Test exit status: $TEST_STATUS"
110
111 return "$TEST_STATUS"
112}
diff --git a/tools/se_utils/__init__.py b/tools/se_utils/__init__.py
0new file mode 100644113new file mode 100644
index 0000000..280450c
--- /dev/null
+++ b/tools/se_utils/__init__.py
@@ -0,0 +1,122 @@
1#!/usr/bin/env python
2# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
3#
4# Copyright (C) 2016 Canonical Ltd
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License version 3 as
8# published by the Free Software Foundation.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18import atexit
19import sys
20import time
21import logging
22import os
23import yaml
24from shutil import rmtree
25from launchpadlib.credentials import RequestTokenAuthorizationEngine
26from lazr.restfulclient.errors import HTTPError
27from launchpadlib.launchpad import Launchpad
28from launchpadlib.credentials import UnencryptedFileCredentialStore
29
30ACCESS_TOKEN_POLL_TIME = 10
31WAITING_FOR_USER = """Open this link:
32{}
33to authorize this program to access Launchpad on your behalf.
34Waiting to hear from Launchpad about your decision. . . ."""
35
36
37class AuthorizeRequestTokenWithConsole(RequestTokenAuthorizationEngine):
38 """Authorize a token in a server environment (with no browser).
39
40 Print a link for the user to copy-and-paste into his/her browser
41 for authentication.
42 """
43
44 def __init__(self, *args, **kwargs):
45 # as implemented in AuthorizeRequestTokenWithBrowser
46 kwargs['consumer_name'] = None
47 kwargs.pop('allow_access_levels', None)
48 super(AuthorizeRequestTokenWithConsole, self).__init__(*args, **kwargs)
49
50 def make_end_user_authorize_token(self, credentials, request_token):
51 """Ask the end-user to authorize the token in their browser.
52
53 """
54 authorization_url = self.authorization_url(request_token)
55 print WAITING_FOR_USER.format(authorization_url)
56 # if we don't flush we may not see the message
57 sys.stdout.flush()
58 while credentials.access_token is None:
59 time.sleep(ACCESS_TOKEN_POLL_TIME)
60 try:
61 credentials.exchange_request_token_for_access_token(
62 self.web_root)
63 break
64 except HTTPError, e:
65 if e.response.status == 403:
66 # The user decided not to authorize this
67 # application.
68 raise e
69 elif e.response.status == 401:
70 # The user has not made a decision yet.
71 pass
72 else:
73 # There was an error accessing the server.
74 raise e
75
76# launchpadlib is not thread/process safe so we are creating launchpadlib
77# cache in /tmp per process which gets cleaned up at the end
78# see also lp:459418 and lp:1025153
79launchpad_cachedir = os.path.join('/tmp', str(os.getpid()), '.launchpadlib')
80
81# `launchpad_cachedir` is leaked upon unexpected exits
82# adding this cleanup to stop directories filling up `/tmp/`
83atexit.register(rmtree, os.path.join('/tmp',
84 str(os.getpid())),
85 ignore_errors=True)
86
87
88def get_launchpad(launchpadlib_dir=None, credential_store_path=None, lp_app=None, lp_env=None):
89 """ return a launchpad API class. In case launchpadlib_dir is
90 specified used that directory to store launchpadlib cache instead of
91 the default """
92 store = UnencryptedFileCredentialStore(credential_store_path)
93 authorization_engine = AuthorizeRequestTokenWithConsole(lp_env, lp_app)
94 lib_dir=launchpad_cachedir
95 if launchpadlib_dir != None:
96 lib_dir = launchpadlib_dir
97 return Launchpad.login_with(lp_app, lp_env,
98 credential_store=store,
99 authorization_engine=authorization_engine,
100 launchpadlib_dir=lib_dir,
101 version='devel')
102
103# Load configuration for the current agent we're running on. All agents were
104# provisioned when they were setup with a proper configuration. See
105# https://wiki.canonical.com/InformationInfrastructure/Jenkaas/UserDocs for
106# more details.
107def load_config():
108 files = [os.path.expanduser('~/.jlp/jlp.config'), 'jlp.config']
109 for config_file in files:
110 try:
111 config = yaml.safe_load(open(config_file, 'r'))
112 return config
113 except IOError:
114 pass
115 print("ERROR: No config file found")
116 sys.exit(1)
117
118# Return a configuration option from the agent configuration specified by the
119# name argument.
120def get_config_option(name):
121 config = load_config()
122 return config[name]
diff --git a/tools/se_utils/__init__.pyc b/tools/se_utils/__init__.pyc
0new file mode 100644123new file mode 100644
index 0000000..f1be9ca
1Binary files /dev/null and b/tools/se_utils/__init__.pyc differ124Binary files /dev/null and b/tools/se_utils/__init__.pyc differ
diff --git a/tools/shyaml b/tools/shyaml
2new file mode 100755125new file mode 100755
index 0000000..e4618ec
--- /dev/null
+++ b/tools/shyaml
@@ -0,0 +1,454 @@
1#!/usr/bin/env python
2
3# Taken from upstream git repository https://github.com/0k/shyaml
4# at revision d77e30599a0971c51896ef97d21883550e7e9979
5
6## Note: to launch test, you can use:
7## python -m doctest -d shyaml.py
8## or
9## nosetests
10
11from __future__ import print_function
12
13import sys
14import yaml
15import os.path
16import re
17
18PY3 = sys.version_info[0] >= 3
19
20EXNAME = os.path.basename(sys.argv[0])
21
22USAGE = """\
23Usage:
24
25 %(exname)s (-h|--help)
26 %(exname)s [-y|--yaml] ACTION KEY [DEFAULT]
27""" % {"exname": EXNAME}
28
29HELP = """
30Parses and output chosen subpart or values from YAML input.
31It reads YAML in stdin and will output on stdout it's return value.
32
33%(usage)s
34
35Options:
36
37 -y, --yaml
38 Output only YAML safe value, more precisely, even
39 literal values will be YAML quoted. This behavior
40 is required if you want to output YAML subparts and
41 further process it. If you know you have are dealing
42 with safe literal value, then you don't need this.
43 (Default: no safe YAML output)
44
45 ACTION Depending on the type of data you've targetted
46 thanks to the KEY, ACTION can be:
47
48 These ACTIONs applies to any YAML type:
49
50 get-type ## returns a short string
51 get-value ## returns YAML
52
53 This ACTION applies to 'sequence' and 'struct' YAML type:
54
55 get-values{,-0} ## return list of YAML
56
57 These ACTION applies to 'struct' YAML type:
58
59 keys{,-0} ## return list of YAML
60 values{,-0} ## return list of YAML
61 key-values,{,-0} ## return list of YAML
62
63 Note that any value returned is returned on stdout, and
64 when returning ``list of YAML``, it'll be separated by
65 ``\\n`` or ``NUL`` char depending of you've used the
66 ``-0`` suffixed ACTION.
67
68 KEY Identifier to browse and target subvalues into YAML
69 structure. Use ``.`` to parse a subvalue. If you need
70 to use a literal ``.`` or ``\``, use ``\`` to quote it.
71
72 Use struct keyword to browse ``struct`` YAML data and use
73 integers to browse ``sequence`` YAML data.
74
75 DEFAULT if not provided and given KEY do not match any value in
76 the provided YAML, then DEFAULT will be returned. If no
77 default is provided and the KEY do not match any value
78 in the provided YAML, %(exname)s will fail with an error
79 message.
80
81Examples:
82
83 ## get last grocery
84 cat recipe.yaml | %(exname)s get-value groceries.-1
85
86 ## get all words of my french dictionary
87 cat dictionaries.yaml | %(exname)s keys-0 french.dictionary
88
89 ## get YAML config part of 'myhost'
90 cat hosts_config.yaml | %(exname)s get-value cfgs.myhost
91
92""" % {"exname": EXNAME, "usage": USAGE}
93
94##
95## Keep previous order in YAML
96##
97
98try:
99 # included in standard lib from Python 2.7
100 from collections import OrderedDict
101except ImportError:
102 # try importing the backported drop-in replacement
103 # it's available on PyPI
104 from ordereddict import OrderedDict
105
106
107## Ensure that there are no collision with legacy OrderedDict
108## that could be used for omap for instance.
109class MyOrderedDict(OrderedDict):
110 pass
111
112yaml.add_representer(
113 MyOrderedDict,
114 lambda cls, data: cls.represent_dict(data.items()))
115
116
117def construct_omap(cls, node):
118 ## Force unfolding reference and merges
119 ## otherwise it would fail on 'merge'
120 cls.flatten_mapping(node)
121 return MyOrderedDict(cls.construct_pairs(node))
122
123
124yaml.add_constructor(
125 yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
126 construct_omap)
127
128
129##
130## Key specifier
131##
132
133def tokenize(s):
134 r"""Returns an iterable through all subparts of string splitted by '.'
135
136 So:
137
138 >>> list(tokenize('foo.bar.wiz'))
139 ['foo', 'bar', 'wiz']
140
141 Contrary to traditional ``.split()`` method, this function has to
142 deal with any type of data in the string. So it actually
143 interprets the string. Characters with meaning are '.' and '\'.
144 Both of these can be included in a token by quoting them with '\'.
145
146 So dot of slashes can be contained in token:
147
148 >>> print('\n'.join(tokenize(r'foo.dot<\.>.slash<\\>')))
149 foo
150 dot<.>
151 slash<\>
152
153 Notice that empty keys are also supported:
154
155 >>> list(tokenize(r'foo..bar'))
156 ['foo', '', 'bar']
157
158 Given an empty string:
159
160 >>> list(tokenize(r''))
161 ['']
162
163 And a None value:
164
165 >>> list(tokenize(None))
166 []
167
168 """
169 if s is None:
170 raise StopIteration
171 tokens = (re.sub(r'\\(\\|\.)', r'\1', m.group(0))
172 for m in re.finditer(r'((\\.|[^.\\])*)', s))
173 ## an empty string superfluous token is added after all non-empty token
174 for token in tokens:
175 if len(token) != 0:
176 next(tokens)
177 yield token
178
179
180def mget(dct, key):
181 r"""Allow to get values deep in recursive dict with doted keys
182
183 Accessing leaf values is quite straightforward:
184
185 >>> dct = {'a': {'x': 1, 'b': {'c': 2}}}
186 >>> mget(dct, 'a.x')
187 1
188 >>> mget(dct, 'a.b.c')
189 2
190
191 But you can also get subdict if your key is not targeting a
192 leaf value:
193
194 >>> mget(dct, 'a.b')
195 {'c': 2}
196
197 As a special feature, list access is also supported by providing a
198 (possibily signed) integer, it'll be interpreted as usual python
199 sequence access using bracket notation:
200
201 >>> mget({'a': {'x': [1, 5], 'b': {'c': 2}}}, 'a.x.-1')
202 5
203 >>> mget({'a': {'x': 1, 'b': [{'c': 2}]}}, 'a.b.0.c')
204 2
205
206 Keys that contains '.' can be accessed by escaping them:
207
208 >>> dct = {'a': {'x': 1}, 'a.x': 3, 'a.y': 4}
209 >>> mget(dct, 'a.x')
210 1
211 >>> mget(dct, r'a\.x')
212 3
213 >>> mget(dct, r'a.y') ## doctest: +IGNORE_EXCEPTION_DETAIL
214 Traceback (most recent call last):
215 ...
216 MissingKeyError: missing key 'y' in dict.
217 >>> mget(dct, r'a\.y')
218 4
219
220 As a consequence, if your key contains a '\', you should also escape it:
221
222 >>> dct = {r'a\x': 3, r'a\.x': 4, 'a.x': 5, 'a\\': {'x': 6}}
223 >>> mget(dct, r'a\\x')
224 3
225 >>> mget(dct, r'a\\\.x')
226 4
227 >>> mget(dct, r'a\\.x')
228 6
229 >>> mget({'a\\': {'b': 1}}, r'a\\.b')
230 1
231 >>> mget({r'a.b\.c': 1}, r'a\.b\\\.c')
232 1
233
234 And even empty strings key are supported:
235
236 >>> dct = {r'a': {'': {'y': 3}, 'y': 4}, 'b': {'': {'': 1}}, '': 2}
237 >>> mget(dct, r'a..y')
238 3
239 >>> mget(dct, r'a.y')
240 4
241 >>> mget(dct, r'')
242 2
243 >>> mget(dct, r'b..')
244 1
245
246 It will complain if you are trying to get into a leaf:
247
248 >>> mget({'a': 1}, 'a.y') ## doctest: +IGNORE_EXCEPTION_DETAIL
249 Traceback (most recent call last):
250 ...
251 NonDictLikeTypeError: can't query subvalue 'y' of a leaf...
252
253 if the key is None, the whole dct should be sent back:
254
255 >>> mget({'a': 1}, None)
256 {'a': 1}
257
258 """
259 return aget(dct, tokenize(key))
260
261
262class MissingKeyError(KeyError):
263 """Raised when querying a dict-like structure on non-existing keys"""
264
265 def __str__(self):
266 return self.message
267
268
269class NonDictLikeTypeError(TypeError):
270 """Raised when attempting to traverse non-dict like structure"""
271
272
273class IndexNotIntegerError(ValueError):
274 """Raised when attempting to traverse sequence without using an integer"""
275
276
277class IndexOutOfRange(IndexError):
278 """Raised when attempting to traverse sequence without using an integer"""
279
280
281def aget(dct, key):
282 r"""Allow to get values deep in a dict with iterable keys
283
284 Accessing leaf values is quite straightforward:
285
286 >>> dct = {'a': {'x': 1, 'b': {'c': 2}}}
287 >>> aget(dct, ('a', 'x'))
288 1
289 >>> aget(dct, ('a', 'b', 'c'))
290 2
291
292 If key is empty, it returns unchanged the ``dct`` value.
293
294 >>> aget({'x': 1}, ())
295 {'x': 1}
296
297 """
298 key = iter(key)
299 try:
300 head = next(key)
301 except StopIteration:
302 return dct
303
304 if isinstance(dct, list):
305 try:
306 idx = int(head)
307 except ValueError:
308 raise IndexNotIntegerError(
309 "non-integer index %r provided on a list."
310 % head)
311 try:
312 value = dct[idx]
313 except IndexError:
314 raise IndexOutOfRange(
315 "index %d is out of range (%d elements in list)."
316 % (idx, len(dct)))
317 else:
318 try:
319 value = dct[head]
320 except KeyError:
321 ## Replace with a more informative KeyError
322 raise MissingKeyError(
323 "missing key %r in dict."
324 % (head, ))
325 except:
326 raise NonDictLikeTypeError(
327 "can't query subvalue %r of a leaf%s."
328 % (head,
329 (" (leaf value is %r)" % dct)
330 if len(repr(dct)) < 15 else ""))
331 return aget(value, key)
332
333
334def stderr(msg):
335 """Convenience function to write short message to stderr."""
336 sys.stderr.write(msg)
337
338
339def stdout(value):
340 """Convenience function to write short message to stdout."""
341 sys.stdout.write(value)
342
343
344def die(msg, errlvl=1, prefix="Error: "):
345 """Convenience function to write short message to stderr and quit."""
346 stderr("%s%s\n" % (prefix, msg))
347 sys.exit(errlvl)
348
349SIMPLE_TYPES = (str if PY3 else basestring, int, float, type(None))
350COMPLEX_TYPES = (list, dict)
351
352
353def magic_dump(value):
354 """Returns a representation of values directly usable by bash.
355
356 Literal types are printed as-is (avoiding quotes around string for
357 instance). But complex type are written in a YAML useable format.
358
359 """
360 return value if isinstance(value, SIMPLE_TYPES) \
361 else yaml.dump(value, default_flow_style=False)
362
363def yaml_dump(value):
364 """Returns a representation of values directly usable by bash.
365
366 Literal types are quoted and safe to use as YAML.
367
368 """
369 return yaml.dump(value, default_flow_style=False)
370
371
372def type_name(value):
373 """Returns pseudo-YAML type name of given value."""
374 return "struct" if isinstance(value, dict) else \
375 "sequence" if isinstance(value, (tuple, list)) else \
376 type(value).__name__
377
378
379def main(args): ## pylint: disable=too-many-branches
380 """Entrypoint of the whole application"""
381
382 if len(args) == 0:
383 stderr("Error: Bad number of arguments.\n")
384 die(USAGE, errlvl=1, prefix="")
385
386 if len(args) == 1 and args[0] in ("-h", "--help"):
387 stdout(HELP)
388 exit(0)
389
390 dump = magic_dump
391 for arg in ["-y", "--yaml"]:
392 if arg in args:
393 args.remove(arg)
394 dump = yaml_dump
395
396 action = args[0]
397 key_value = None if len(args) == 1 else args[1]
398 default = args[2] if len(args) > 2 else None
399 contents = yaml.load(sys.stdin)
400
401 try:
402 try:
403 value = mget(contents, key_value)
404 except (IndexOutOfRange, MissingKeyError):
405 if default is None:
406 raise
407 value = default
408 except (IndexOutOfRange, MissingKeyError,
409 NonDictLikeTypeError, IndexNotIntegerError) as exc:
410 msg = str(exc.message)
411 die("invalid path %r, %s"
412 % (key_value,
413 msg.replace('list', 'sequence').replace('dict', 'struct')))
414
415 tvalue = type_name(value)
416 termination = "\0" if action.endswith("-0") else "\n"
417
418 if action == "get-value":
419 print(dump(value), end='')
420 elif action in ("get-values", "get-values-0"):
421 if isinstance(value, dict):
422 for k, v in value.iteritems():
423 stdout("%s%s%s%s" % (dump(k), termination,
424 dump(v), termination))
425 elif isinstance(value, list):
426 for l in value:
427 stdout("%s%s" % (dump(l), termination))
428 else:
429 die("%s does not support %r type. "
430 "Please provide or select a sequence or struct."
431 % (action, tvalue))
432 elif action == "get-type":
433 print(tvalue)
434 elif action in ("keys", "keys-0",
435 "values", "values-0",
436 "key-values", "key-values-0"):
437 if isinstance(value, dict):
438 method = value.keys if action.startswith("keys") else \
439 value.items if action.startswith("key-values") else \
440 value.values
441 output = (lambda x: termination.join(str(dump(e)) for e in x)) \
442 if action.startswith("key-values") else \
443 dump
444 for k in method():
445 stdout("%s%s" % (output(k), termination))
446 else:
447 die("%s does not support %r type. "
448 "Please provide or select a struct." % (action, tvalue))
449 else:
450 die("Invalid argument.\n%s" % USAGE)
451
452
453if __name__ == "__main__":
454 sys.exit(main(sys.argv[1:]))
diff --git a/tools/snapbuild.sh b/tools/snapbuild.sh
0new file mode 100755455new file mode 100755
index 0000000..53b4518
--- /dev/null
+++ b/tools/snapbuild.sh
@@ -0,0 +1,192 @@
1#!/bin/sh
2#
3# Copyright (C) 2017 Canonical Ltd
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17set -ex
18
19if [ "$(id -u)" -ne 0 ]; then
20 echo "ERROR: You have to run this script as root!"
21 exit 1
22fi
23
24SERIES=xenial
25SOURCE_DIR=
26RESULTS_DIR=
27# Whenever you change the chroot in a way which needs a regeneration
28# on the build server bump the version here. This will tell the
29# job which updates the chroots to generate a new one.
30CHROOT_VERSION=1
31BUILD_ARCH=amd64
32TARGET_ARCH=amd64
33UPDATE_CHROOT=false
34PROXY=
35CROSS_BUILD=false
36SNAPCRAFT_EXTRA_ARGS=
37
38while [ -n "$1" ]; do
39 case "$1" in
40 --series=*)
41 SERIES=${1#*=}
42 shift
43 ;;
44 --source-dir=*)
45 SOURCE_DIR=${1#*=}
46 shift
47 ;;
48 --results-dir=*)
49 RESULTS_DIR=${1#*=}
50 shift
51 ;;
52 --arch=*)
53 TARGET_ARCH=${1#*=}
54 BUILD_ARCH=$TARGET_ARCH
55 shift
56 ;;
57 --update-chroot)
58 UPDATE_CHROOT=true
59 shift
60 ;;
61 --proxy=*)
62 PROXY=${1#*=}
63 shift
64 ;;
65 --cross-build)
66 CROSS_BUILD=true
67 shift
68 ;;
69 *)
70 echo "ERROR: Unknown options $1"
71 exit 1
72 esac
73done
74
75if [ -z "$SERIES" ]; then
76 echo "ERROR: No series specified"
77 exit 1
78fi
79
80# If we're cross-building we have to switch the architecture we're using
81# for our build environment to match our host arch.
82if [ "$CROSS_BUILD" = true ]; then
83 BUILD_ARCH=$(dpkg --print-architecture)
84fi
85
86CHROOT_STORE_PATH=/build/chroots
87CHROOT_TARBALL=$SERIES-$BUILD_ARCH-$CHROOT_VERSION-rootfs.tar
88
89if [ "$UPDATE_CHROOT" = true ]; then
90 if [ ! -e $CHROOT_STORE_PATH/$CHROOT_TARBALL ] ; then
91 mkdir -p /build/chroots
92 WORKDIR=$(mktemp -d)
93 mkdir -p $WORKDIR/rootfs
94
95 DEBOOTSTRAP=debootstrap
96 DEB_REPO_URL=
97 case "$BUILD_ARCH" in
98 amd64)
99 DEB_REPO_URL="http://archive.ubuntu.com/ubuntu/"
100 ;;
101 armhf)
102 DEBOOTSTRAP=qemu-debootstrap
103 DEB_REPO_URL="http://ports.ubuntu.com/ubuntu-ports"
104 ;;
105 *)
106 echo "ERROR: Unsupported architecture $BUILD_ARCH"
107 exit 1
108 ;;
109 esac
110
111 cleanup() {
112 rm -rf $WORKDIR
113 }
114
115 trap cleanup INT EXIT
116
117 $DEBOOTSTRAP --components=main,universe --arch $BUILD_ARCH $SERIES $WORKDIR/rootfs
118 cat << EOF > $WORKDIR/rootfs/etc/apt/sources.list.d/updates.list
119deb $DEB_REPO_URL xenial universe
120deb $DEB_REPO_URL xenial-updates main universe
121EOF
122 cat << EOF > $WORKDIR/rootfs/setup.sh
123#!/bin/sh
124set -ex
125apt update
126apt upgrade -y
127apt install -y snapcraft
128EOF
129 chmod +x $WORKDIR/rootfs/setup.sh
130 sudo chroot $WORKDIR/rootfs /setup.sh
131 rm $WORKDIR/rootfs/setup.sh
132
133 (cd $WORKDIR/rootfs; tar cf $CHROOT_STORE_PATH/$CHROOT_TARBALL .)
134 rm -rf $WORKDIR
135 fi
136
137 exit 0
138fi
139
140if [ -z "$SOURCE_DIR" ]; then
141 echo "ERROR: No source dir specified"
142 exit 1
143fi
144
145if [ -z "$RESULTS_DIR" ]; then
146 echo "ERROR: No results dir specified"
147 exit 1
148fi
149
150BUILDDIR=$(mktemp -d)
151
152cleanup() {
153 rm -rf $BUILDDIR
154}
155
156trap cleanup INT EXIT
157
158if [ ! -e $CHROOT_STORE_PATH/$CHROOT_TARBALL ] ; then
159 echo "ERROR: Up to date chroot tarball doesn't exist. Please run the snap-build-update-chroot job!"
160 exit 1
161fi
162
163tar xf $CHROOT_STORE_PATH/$CHROOT_TARBALL -C $BUILDDIR
164
165cp -ra $SOURCE_DIR $BUILDDIR/src
166
167if [ "$CROSS_BUILD" = true ]; then
168 SNAPCRAFT_EXTRA_ARGS="$SNAPCRAFT_EXTRA_ARGS --target-arch=$TARGET_ARCH"
169fi
170
171cat << EOF > $BUILDDIR/do-build.sh
172#!/bin/sh
173set -ex
174apt update
175apt upgrade -y
176cd /src
177
178# snapcraft is pretty unhappy when LC_ALL and LANG aren't set
179export LC_ALL=C.UTF-8
180export LANG=C.UTF-8
181
182# To access certain things like the snap store we need a proxy in place
183# in some environments
184export http_proxy=$PROXY
185export https_proxy=$PROXY
186
187snapcraft clean
188snapcraft $SNAPCRAFT_EXTRA_ARGS
189EOF
190chmod +x $BUILDDIR/do-build.sh
191
192sudo chroot $BUILDDIR /do-build.sh
diff --git a/tools/test-snap.sh b/tools/test-snap.sh
0new file mode 100755193new file mode 100755
index 0000000..4851645
--- /dev/null
+++ b/tools/test-snap.sh
@@ -0,0 +1,100 @@
1#!/bin/sh -ex
2#
3# Copyright (C) 2017 Canonical Ltd
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17# Import used functions
18. "$WORKSPACE"/build-scripts/scripts/hardware-test.sh
19
20# Runs snap tests
21run_snap_tests ()
22{
23 if [ -n "$RESULTS_ID" ]; then
24 $SSH mkdir -p "$REMOTE_WORKSPACE"/results
25 $SSH cp -v "$REMOTE_RESULTS_BASE_DIR"/"$RESULTS_ID"/*.snap "$REMOTE_WORKSPACE"/results
26 fi
27
28 $SSH sudo apt-get --yes --force-yes install docker.io
29
30 cat << EOF > "$WORKSPACE"/run-tests.sh
31#!/bin/sh
32set -ex
33
34export TERM=linux
35export DEBIAN_FRONTEND=noninteractive
36export PATH=/build/bin:$PATH
37
38# At this time it's necessary to build spread manually because
39# the snapped version does not include the qemu/kvm backend.
40# Once the snapped version includes this backend, then we can
41# change the manual building of spread with making sure the snap
42# package is installed.
43export GOPATH=$(mktemp -d)
44go get -d -v github.com/snapcore/spread/...
45go build github.com/snapcore/spread/cmd/spread
46mkdir /build/bin
47cp spread /build/bin
48
49git clone --depth 1 -b $BRANCH $REPO /build/src
50cd /build/src
51
52# Copy any stage results from previous generic-build-snap-worker builds
53if [ "\$(find /build/results -path ./misc -prune -o -name '*.snap' -print | wc -l)" -gt 0 ]; then
54 cp -v /build/results/*.snap /build/src
55fi
56
57if [ -e "run-tests.sh" ] ; then
58 EXTRA_ARGS=
59 if [ -n "$CHANNEL" ] ; then
60 # If CHANNEL is specified we use that channel for image construction and
61 # also load the snap from that channel for testing.
62 EXTRA_ARGS="--test-from-channel=$CHANNEL"
63 fi
64 if [ -n "$CORE_CHANNEL" ]; then
65 EXTRA_ARGS="--channel=$CORE_CHANNEL"
66 fi
67
68 ./run-tests.sh --force-new-image \$EXTRA_ARGS
69else
70 if [ ! -z "$CHANNEL" ] ; then
71 SNAP_CHANNEL=$CHANNEL spread -v
72 else
73 spread -v
74 fi
75fi
76EOF
77
78 $SSH mkdir -p "$REMOTE_WORKSPACE"
79 $SCP "$WORKSPACE"/run-tests.sh "$REMOTE_USER"@"$REMOTE_WORKER":"$REMOTE_WORKSPACE"
80 $SSH chmod u+x "$REMOTE_WORKSPACE"/run-tests.sh
81
82 $SSH mkdir -p "$REMOTE_WORKSPACE"/docker
83 $SCP "$WORKSPACE"/build-scripts/docker/spread-tests/Dockerfile \
84 "$REMOTE_USER"@"$REMOTE_WORKER":"$REMOTE_WORKSPACE"/docker
85 $SSH time sudo docker build -t snap-spread-tests "$REMOTE_WORKSPACE"/docker
86
87 $SSH time sudo docker run \
88 --rm \
89 -v /dev:/dev \
90 -v "$REMOTE_WORKSPACE":/build \
91 --privileged \
92 snap-spread-tests /build/run-tests.sh
93
94 $SSH sudo rm -rf "$REMOTE_WORKSPACE"
95
96 # Now run tests on real hardware if defined in the backend
97 if [ "$HW_TESTS_RESULT" -eq 0 ] ; then
98 run_hardware_tests
99 fi
100}
diff --git a/tools/trigger-ci.py b/tools/trigger-ci.py
0new file mode 100755101new file mode 100755
index 0000000..d50c021
--- /dev/null
+++ b/tools/trigger-ci.py
@@ -0,0 +1,270 @@
1#!/usr/bin/env python
2# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
3#
4# Copyright (C) 2016 Canonical Ltd
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License version 3 as
8# published by the Free Software Foundation.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18from launchpadlib.launchpad import Launchpad
19import jenkins
20import os
21import sys
22import yaml
23import re
24from jlp import launchpadutils
25from jlp import jenkinsutils
26
27from argparse import ArgumentParser
28
29import se_utils
30
31parser = ArgumentParser(description="Trigger snap builds for pending merge proposals")
32parser.add_argument('-p', '--project', required=True,
33 help="Launchpad project to check for new merge-proposals")
34parser.add_argument('-j', '--job', required=True,
35 help="Jenkins job to trigger")
36parser.add_argument('-t', '--team', required=True,
37 help="Launchpad team for the project")
38
39args = vars(parser.parse_args())
40
41lp_app = se_utils.get_config_option("lp_app")
42lp_env = se_utils.get_config_option("lp_env")
43credential_store_path = se_utils.get_config_option('credential_store_path')
44launchpad = se_utils.get_launchpad(None, credential_store_path, lp_app, lp_env)
45
46project = launchpad.projects[args['project']]
47team = args['team']
48
49proposals = project.getMergeProposals()
50
51def load_config():
52 files = [os.path.expanduser('~/.jlp/jlp.config'), 'jlp.config']
53 for config_file in files:
54 try:
55 config = yaml.safe_load(open(config_file, 'r'))
56 return config
57 except IOError:
58 pass
59 print("ERROR: No config file found")
60 sys.exit(1)
61
62def get_config_option(name):
63 config = load_config()
64 return config[name]
65
66j = jenkins.Jenkins(get_config_option('jenkins_url'),
67 get_config_option('jenkins_user'),
68 get_config_option('jenkins_password'))
69
70jenkins_job = args['job']
71jenkins_build_token = get_config_option('jenkins_build_token')
72
73job_info = j.get_job_info(jenkins_job)
74if not job_info['buildable']:
75 print("ERROR: Job is not buildable (propably disabled)")
76 sys.exit(1)
77
78
79def get_latest_revision(mp):
80 """Return the latest revision of the given merge proposal.
81
82 :param mp: handle to merge proposal
83 """
84 if '+git' in mp.web_link:
85 for ref in mp.source_git_repository.refs_collection:
86 if ref.path == mp.source_git_path:
87 return ref.commit_sha1
88 return str(0)
89 else:
90 return mp.source_branch.revision_count
91
92def clean_branch_name(branch_name):
93 if branch_name.startswith("refs/heads/"):
94 return branch_name[11:]
95 return branch_name
96
97def series_from_branch_name(branch_name):
98 if branch_name.startswith("vivid/"):
99 return "vivid"
100 return "xenial"
101
102def get_latest_revision(mp):
103 """Return the latest revision of the given merge proposal.
104
105 :param mp: handle to merge proposal
106 """
107 if '+git' in mp.web_link:
108 for ref in mp.source_git_repository.refs_collection:
109 if ref.path == mp.source_git_path:
110 return ref.commit_sha1
111 return str(0)
112 else:
113 return mp.source_branch.revision_count
114
115def get_review_revision_regex(mp):
116 if '+git' in mp.web_link:
117 return '^(PASSED|FAILED): Continuous integration, rev:([0-9a-f]+)'
118 else:
119 return '^(PASSED|FAILED): Continuous integration, rev:(\d+)'
120
121def get_latest_review(launchpad_user, mp):
122 """Return the latest revision reviewed by the given launchpad_user.
123
124 This function expects a review comment in the following format:
125 '^(PASSED|FAILED): Continuous integration, rev:(\d+)'
126
127 :param launchpad_user: handle to launchpad user
128 :param mp: handle to merge proposal
129 """
130 revision = 0
131 launchpad_review_type = get_config_option('launchpad_review_type')
132 for comment in mp.all_comments:
133 if comment.author.name == launchpad_user.name:
134 if comment.vote_tag == launchpad_review_type:
135 m = re.search(
136 get_review_revision_regex(mp),
137 comment.message_body)
138 if m:
139 revision = m.group(2)
140 return revision
141
142def latest_candidate_validated(launchpad_user, mp):
143 """Return if the latest candidate revision of the merge proposal is
144 validated.
145
146 :param launchpad_user: handle to launchpad user used to validate the
147 merge proposals
148 :param mp: handle to merge proposal
149 """
150
151 latest_review = get_latest_review(launchpad_user, mp)
152 print 'Latest review is revision: ' + str(latest_review)
153 latest_revision = get_latest_revision(mp)
154 print 'Latest revision is: ' + str(latest_revision)
155 if latest_review == latest_revision:
156 print 'Skipping this MP. Current revision: ' + str(latest_revision)
157 return True
158 return False
159
160# NOTE: We can't use the testing_in_progress method from jlp as it checks for
161# the lower case parameter 'merge_proposal' of the build job and ours is using
162# the upper case variant 'MERGE_PROPOSAL'.
163def testing_in_progress(mp, jenkins_job):
164 try:
165 if jenkinsutils.is_job_or_downstream_building(
166 jenkins_job, job_params={'MERGE_PROPOSAL': mp.web_link}):
167 print('Skipping this MP. It is currently being tested by Jenkins.')
168 return True
169 except:
170 print('Failed to check if MP is already building')
171 return False
172
173# Copied over from lp:jenkins-launchpad-plugin
174def users_in_team(users, team):
175 """Determine whether any of these users are in the supplied team.
176
177 :param users: The users which may be members of the supplied team.
178 :param team: The team which users may be part of.
179 :return: True if any of the users are members of the team, otherwise
180 False.
181 """
182 for member in team.participants:
183 if member in users:
184 return True
185 else:
186 return False
187
188# Copied over from lp:jenkins-launchpad-plugin and slightly modified
189def users_allowed_to_trigger_jobs(lp_users, allowed_people):
190 """Returns if an of the given users is allowed to run jobs on jenkins.
191
192 This is to avoid random people to run jobs on our internal infrastructure.
193 A user is allowed if they are either directly in the ALLOWED_USERS list or
194 are member of a team in that list.
195
196 :param lp_users: launchpad user handles
197 """
198 if len(lp_users) == 0:
199 return False
200 for lp_user in lp_users:
201 if lp_user.name in allowed_people:
202 return True
203 lp = lp_users[0]._root
204 for allowed in allowed_people:
205 try:
206 allowed_person = lp.people[allowed]
207 except KeyError:
208 logger.warn('User {} from the allowed_users list is not in '
209 'launchpad!'.format(allowed))
210 continue
211 if not allowed_person.is_team:
212 continue
213 if users_in_team(lp_users, allowed_person):
214 return True
215 logger.debug('Users "' + ', '.join(u.name for u in lp_users) +
216 '" not allowed to trigger jobs')
217 return False
218
219project_blacklist = []
220
221for proposal in proposals:
222 launchpad_user = launchpad.people(get_config_option('launchpad_login'))
223
224 print("Checking proposal %s .." % proposal.web_link)
225
226 # Ignore certain projects which don't build here as they are
227 # fetching source from somewhere else.
228 ignore = False
229 for project in project_blacklist:
230 if project in proposal.web_link:
231 ignore = True
232 break
233
234 if ignore:
235 print "Ignoring %s" % proposal.web_link
236 continue
237
238 if not users_allowed_to_trigger_jobs([proposal.registrant], [team]):
239 continue
240
241 if latest_candidate_validated(launchpad_user, proposal):
242 continue
243
244 if testing_in_progress(proposal, jenkins_job):
245 continue
246
247 target_repo = launchpad.load(proposal.target_git_repository_link)
248 source_repo = launchpad.load(proposal.source_git_repository_link)
249
250 target_git_url = target_repo.git_https_url
251 if target_git_url == None:
252 target_git_url = target_repo.git_ssh_url
253
254 source_git_url = source_repo.git_https_url
255 if source_git_url == None:
256 source_git_url = source_repo.git_ssh_url
257
258 target_branch = clean_branch_name(proposal.target_git_path)
259 jenkins_params = {
260 'TARGET_GIT_REPO': target_git_url,
261 'TARGET_GIT_REPO_BRANCH': target_branch,
262 'SOURCE_GIT_REPO': source_git_url,
263 'SOURCE_GIT_REPO_BRANCH': clean_branch_name(proposal.source_git_path),
264 'MERGE_PROPOSAL': proposal.web_link,
265 'REVISION': get_latest_revision(proposal),
266 'SERIES': series_from_branch_name(target_branch),
267 }
268
269 print("Triggering build job for %s" % proposal.web_link)
270 j.build_job(jenkins_job, jenkins_params, jenkins_build_token)
diff --git a/tools/trigger-lp-build.py b/tools/trigger-lp-build.py
0new file mode 100755271new file mode 100755
index 0000000..5434281
--- /dev/null
+++ b/tools/trigger-lp-build.py
@@ -0,0 +1,230 @@
1#! /usr/bin/python
2
3import os
4import sys
5import time
6import random
7import smtplib
8import string
9import urllib2
10import zlib
11
12from datetime import datetime
13from os.path import basename
14from launchpadlib.launchpad import Launchpad
15
16from argparse import ArgumentParser
17
18import se_utils
19
20parser = ArgumentParser(description="Build a specific snap on launchpad")
21parser.add_argument('-s', '--snap', required=True,
22 help="Name of the snap to build")
23parser.add_argument('-p', '--publish', action='store_true',
24 help="Trigger a publish build instead of a daily (default)")
25parser.add_argument('-n', '--new', action='store_true', help="Create a new ephemeral snap build on launchpad")
26parser.add_argument('--git-repo', help="Git repository to be used for new ephemeral snap build")
27parser.add_argument('--git-repo-branch', help="Git repository branch to be used for new ephemeral snap build")
28parser.add_argument('-a', '--architectures', help="Specify architectures to build for. Separate multiple architectures by ','")
29parser.add_argument('-r', '--results-dir', help="Specify where results should be saved")
30
31args = vars(parser.parse_args())
32
33ephemeral_build=False
34results_dir=os.path.join(os.getcwd(), "results")
35
36if 'results_dir' in args:
37 results_dir=args['results_dir']
38
39if args['new']:
40 ephemeral_build=True
41 if args['git_repo'] == None or args['git_repo_branch'] == None:
42 print("ERROR: No git repository or a branch supplied")
43 sys.exit(1)
44
45series = 'xenial'
46
47lp_app = se_utils.get_config_option("lp_app")
48lp_env = se_utils.get_config_option("lp_env")
49credential_store_path = se_utils.get_config_option('credential_store_path')
50launchpad = se_utils.get_launchpad(None, credential_store_path, lp_app, lp_env)
51
52team = launchpad.people['snappy-hwe-team']
53ubuntu = launchpad.distributions['ubuntu']
54release = ubuntu.getSeries(name_or_version=series)
55primary_archive = ubuntu.getArchive(name='primary')
56
57snap=None
58if ephemeral_build:
59 snap_arches=[]
60 if 'architectures' in args and args['architectures'] != None:
61 snap_arches = args["architectures"].split(",")
62
63 if len(snap_arches) == 0:
64 print("WARNING: No architectures to build specified. Will only build for amd64.")
65 snap_arches=["amd64"]
66
67 processors=[]
68 for arch in snap_arches:
69 try:
70 p = launchpad.processors.getByName(name=arch)
71 processors.append(p.self_link)
72 except:
73 print("ERROR: Failed to find processor for '{}' architecture".format(arch))
74 sys.exit(1)
75
76 build_name = 'ci-%s-%s' % (args["snap"], ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(16)))
77 snap = launchpad.snaps.new(name=build_name,
78 processors=processors,
79 auto_build=False, distro_series=release,
80 git_repository_url=args['git_repo'],
81 git_path='%s' % args["git_repo_branch"],
82 owner=team)
83else:
84 build_name = "%s-daily" % args["snap"]
85 if args["publish"] == True:
86 build_name = "%s-publish" % args["snap"]
87 snap = launchpad.snaps.getByName(name=build_name, owner=team)
88
89if snap == None:
90 print("ERROR: Failed to create snap build on launchpad")
91
92# Not every snap is build agains all arches.
93arches = [processor.name for processor in snap.processors]
94if not ephemeral_build and args['architectures'] != None:
95 wanted_arches = args["architectures"].split(",")
96 possible_arches = []
97 for arch in wanted_arches:
98 if not arch in arches:
99 print("WARNING: Can't build snap for architecture {} as it is not enabled in the build job".format(args["snap"]))
100 continue
101 possible_arches.append(arch)
102 arches = possible_arches
103
104if len(arches) == 0:
105 print("ERROR: No architectures available to build for")
106 sys.exit(1)
107
108# Add a big fat warning that we don't really care about fixing things when
109# the job will be canceled after the following lines are printed out.
110print("!!!!!!! POINT OF NO RETURN !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
111print("DO NOT CANCEL THIS JOB AFTER THIS OR BAD THINGS WILL HAPPEN")
112print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
113
114stamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
115print("Trying to trigger builds at: {}".format(stamp))
116
117# sometimes we see error such as "u'Unknown architecture lpia for ubuntu xenial'"
118# and in order to workaround let's validate the arches agains set of valid
119# architectures that the snap can choose from
120
121
122# We will now trigger a build for each whitelisted architecture, collect the
123# build job url and the wait for all builds to finish and collect their results
124# to vote for a successful or failed build.
125triggered_builds = []
126triggered_build_urls = {}
127valid_arches = ['armhf', 'i386', 'amd64', 'arm64', 's390x', 'powerpc', 'ppc64el']
128for build_arch in arches:
129 # sometimes we see error such as "u'Unknown architecture lpia for
130 # ubuntu xenial'" and in order to workaround let's validate the arches
131 # agains set of valid architectures that the snap can choose from
132 if build_arch not in valid_arches:
133 print("WARNING: Can't build snap for architecture {} as it is not enabled in the build job".format(args["snap"]))
134 continue
135
136 arch = release.getDistroArchSeries(archtag=build_arch)
137 request = snap.requestBuild(archive=primary_archive, distro_arch_series=arch, pocket='Proposed')
138 build_id = str(request).rsplit('/', 1)[-1]
139 triggered_builds.append(build_id)
140 triggered_build_urls[build_id] = request.self_link
141 print("Arch: {} is building under: {}".format(build_arch, request.self_link))
142
143failures = []
144successful = []
145while len(triggered_builds):
146 for build in triggered_builds:
147 try:
148 response = snap.getBuildSummariesForSnapBuildIds(snap_build_ids=[build])
149 except:
150 print("Could not get response for {} (was there an LP timeout?)".format(build))
151 continue
152 status = response[build]['status']
153 if status == "FULLYBUILT":
154 successful.append(build)
155 triggered_builds.remove(build)
156 continue
157 elif status == "FAILEDTOBUILD":
158 failures.append(build)
159 triggered_builds.remove(build)
160 continue
161 elif status == "CANCELLED":
162 print("INFO: {} snap build was canceled for id: {}".format(args["snap"], build))
163 triggered_builds.remove(build)
164 continue
165 if len(triggered_builds) > 0:
166 time.sleep(60)
167
168if len(failures):
169 for failure in failures:
170 try:
171 response = snap.getBuildSummariesForSnapBuildIds(snap_build_ids=[failure])
172 except:
173 print ("Could not get failure data for {} (was there an LP timeout?)".format(build))
174 continue
175
176 if not failure in response:
177 print("Launchpad didn't returned us the snap build summary we ask it for!?")
178 continue
179
180 build_summary = response[failure]
181 arch = 'unknown'
182 buildlog = None
183 if 'build_log_url' in build_summary:
184 buildlog = build_summary['build_log_url']
185
186 if buildlog != None and len(buildlog) > 0:
187 parts = arch = str(buildlog).split('_')
188 if len(parts) >= 4:
189 arch = parts[4]
190 elif buildlog == None:
191 buildlog = 'not available'
192
193 print("INFO: {} snap {} build at {} failed for id: {} log: {}".format(args["snap"], arch, stamp, failure, buildlog))
194
195 # For ephermal builds we need to print out the log file as it will be gone after
196 # the launchpad build is removed.
197 if ephemeral_build and buildlog != None:
198 response = urllib2.urlopen(buildlog)
199 log_data = zlib.decompress(response.read(), 16+zlib.MAX_WBITS)
200 print(log_data)
201
202# Fetch build results for successful builds and store those in the output
203# directory so that the caller can reuse them.
204if len(successful):
205 for success in successful:
206 try:
207 snap_build = launchpad.load(triggered_build_urls[success])
208 urls = snap_build.getFileUrls()
209 if len(urls):
210 for u in urls:
211 print("Downloading snap from %s ..." % u)
212 response = urllib2.urlopen(u)
213 if not os.path.exists(results_dir):
214 os.makedirs(results_dir)
215 path = os.path.join(results_dir, os.path.basename(u))
216 with open(path, "w") as out_file:
217 out_file.write(response.read())
218 except:
219 print ("Could not retrieve snap build data for {} (was there an LP timeout?)".format(build))
220 continue
221
222
223if ephemeral_build:
224 snap.lp_delete()
225
226if len(failures):
227 # Let the build fail as at least a single snap has failed to build
228 sys.exit(1)
229
230print("Done!")
diff --git a/tools/vote-on-merge-proposal.py b/tools/vote-on-merge-proposal.py
0new file mode 100755231new file mode 100755
index 0000000..bec6da5
--- /dev/null
+++ b/tools/vote-on-merge-proposal.py
@@ -0,0 +1,271 @@
1#!/usr/bin/env python
2# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
3#
4# Copyright (C) 2016 Canonical Ltd
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License version 3 as
8# published by the Free Software Foundation.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18import atexit
19import sys
20import time
21import logging
22import os
23import yaml
24import re
25from shutil import rmtree
26from argparse import ArgumentParser
27from launchpadlib.credentials import RequestTokenAuthorizationEngine
28from lazr.restfulclient.errors import HTTPError
29from launchpadlib.launchpad import Launchpad
30from launchpadlib.credentials import UnencryptedFileCredentialStore
31from jlp import get_config_option
32from jlp import launchpadutils, jenkinsutils, logger
33
34logger = logging.getLogger('jenkins-launchpad-plugin')
35stdout_handler = logging.StreamHandler(stream=sys.stdout)
36formatter = logging.Formatter('%(levelname)s: %(message)s')
37stdout_handler.setFormatter(formatter)
38logger.addHandler(stdout_handler)
39
40parser = ArgumentParser(description="Vote on a Launchpad merge proposal.")
41parser.add_argument('-s', '--status')
42parser.add_argument('-u', '--build-url', required=True,
43 help="URL of the Jenkins job")
44parser.add_argument('-p', '--merge-proposal', required=True,
45 help="URL of the merge proposal to update")
46parser.add_argument('-r', '--revision', required=True,
47 help="merge proposal candidate revision")
48
49args = vars(parser.parse_args())
50
51ACCESS_TOKEN_POLL_TIME = 10
52WAITING_FOR_USER = """Open this link:
53{}
54to authorize this program to access Launchpad on your behalf.
55Waiting to hear from Launchpad about your decision. . . ."""
56
57
58class AuthorizeRequestTokenWithConsole(RequestTokenAuthorizationEngine):
59 """Authorize a token in a server environment (with no browser).
60
61 Print a link for the user to copy-and-paste into his/her browser
62 for authentication.
63 """
64
65 def __init__(self, *args, **kwargs):
66 # as implemented in AuthorizeRequestTokenWithBrowser
67 kwargs['consumer_name'] = None
68 kwargs.pop('allow_access_levels', None)
69 super(AuthorizeRequestTokenWithConsole, self).__init__(*args, **kwargs)
70
71 def make_end_user_authorize_token(self, credentials, request_token):
72 """Ask the end-user to authorize the token in their browser.
73
74 """
75 authorization_url = self.authorization_url(request_token)
76 print WAITING_FOR_USER.format(authorization_url)
77 # if we don't flush we may not see the message
78 sys.stdout.flush()
79 while credentials.access_token is None:
80 time.sleep(ACCESS_TOKEN_POLL_TIME)
81 try:
82 credentials.exchange_request_token_for_access_token(
83 self.web_root)
84 break
85 except HTTPError, e:
86 if e.response.status == 403:
87 # The user decided not to authorize this
88 # application.
89 raise e
90 elif e.response.status == 401:
91 # The user has not made a decision yet.
92 pass
93 else:
94 # There was an error accessing the server.
95 raise e
96
97
98def get_launchpad(launchpadlib_dir=None):
99 """ return a launchpad API class. In case launchpadlib_dir is
100 specified used that directory to store launchpadlib cache instead of
101 the default """
102 store = UnencryptedFileCredentialStore(
103 get_config_option('credential_store_path'))
104 lp_app = get_config_option('lp_app')
105 lp_env = get_config_option('lp_env')
106 authorization_engine = AuthorizeRequestTokenWithConsole(lp_env, lp_app)
107 return Launchpad.login_with(lp_app, lp_env,
108 credential_store=store,
109 authorization_engine=authorization_engine,
110 launchpadlib_dir=launchpadlib_dir,
111 version='devel')
112
113def get_branch_handle_from_url(lp_handle, url):
114 """ Return a branch/repo handle for the given url.
115 Returns a launchpad branch or git repository handle for the given url.
116 :param lp_handle: launchpad API handle/instance
117 :param url: url of the branch or git repository
118 """
119 if '+git' in url:
120 name = url.replace('https://code.launchpad.net/', '')
121 logger.debug('fetching repo: ' + name)
122 try:
123 return lp_handle.git_repositories.getByPath(path=name)
124 except AttributeError:
125 logger.debug('git_repositories.getByPath was not found. You may need to set lp_version=devel in the config')
126 return None
127 else:
128 name = url.replace('https://code.launchpad.net/', 'lp:')
129 name = name.replace('https://code.staging.launchpad.net/', 'lp://staging/')
130 logger.debug('fetching branch: ' + name)
131 return lp_handle.branches.getByUrl(url=name)
132
133def get_branch_from_mp(merge_proposal):
134 """Return a link to branch given a link to a merge proposal.
135
136 If merge_proposal is:
137 https://copde.launchpad.net/~user/project/name/+merge/12345
138 then the result will be:
139 https://copde.launchpad.net/~user/project/name/
140
141 :param merge_proposal: url of a launchpad merge proposal
142 """
143 m = re.search('(.*)\+merge/[0-9]+$', merge_proposal)
144 if m:
145 return m.group(1)
146 return None
147
148def get_mp_handle_from_url(lp_handle, merge_proposal_link):
149 """ Get launchpad handle for merge proposal given a merge proposal URL.
150
151 Returns None in case the merge proposal can't be found.
152 :param merge_proposal_link: URL of the merge proposal
153 """
154 branch_link = get_branch_from_mp(merge_proposal_link)
155 if not branch_link:
156 logger.error('Unable to get branch link from merge proposal link.')
157 return None
158
159 branch = get_branch_handle_from_url(lp_handle, branch_link)
160 if not branch:
161 logger.debug('Branch {} does not exist'.format(branch_link))
162 return None
163
164 logger.debug('mp_link: {}.'.format(merge_proposal_link))
165
166 for mp in branch.landing_targets:
167 logger.debug('mp.web_link: {}'.format(mp.web_link))
168 if mp.web_link == merge_proposal_link:
169 return mp
170
171 return None
172
173class LaunchpadVote():
174 APPROVE = 'Approve'
175 DISAPPROVE = 'Disapprove'
176 NEEDS_FIXING = 'Needs Fixing'
177
178def get_vote_subject(mp):
179 """Given a mp handle return a subject for the vote message
180
181 Unfortunately there is no method in the API that gives you the "standard"
182 subject that launchapd is using and some email clients (gmail) are
183 grouping conversations into threads based on subject.
184
185 This returns what seems to be the launchpad way of doing subjects.
186 :param mp: launchpad merge proposal handle
187 """
188
189 if '+git' in mp.web_link:
190 source = mp.source_git_repository.display_name.replace('lp:', '') + \
191 ':' + \
192 mp.source_git_path.replace('refs/heads/', '')
193 target = mp.target_git_repository.display_name.replace('lp:', '') + \
194 ':' + \
195 mp.target_git_path.replace('refs/heads/', '')
196 return 'Re: [Merge] {} into {}'.format(source, target)
197 else:
198 return 'Re: [Merge] {} into {}'.format(
199 mp.source_branch.display_name,
200 mp.target_branch.display_name)
201
202
203def approve_mp(mp, revision, build_url):
204 """Approve a given merge proposal a revision.
205
206 :params mp: launchapd handle to the respective merge proposal
207 :params revision: revision that should be approved
208 :params build_url: jenkins build url with the details. This job is used to
209 generate the message with all the links to test runs as
210 well as artifacts (coverity, deb files, etc)
211 """
212 state = 'PASSED: Continuous integration, rev:' + str(revision)
213 logger.debug(state)
214 content = jenkinsutils.format_message_for_mp_update(build_url,
215 state + "\n")
216 mp.createComment(review_type=get_config_option('launchpad_review_type'),
217 vote=LaunchpadVote.APPROVE, subject=get_vote_subject(mp),
218 content=content)
219
220
221def disapprove_mp(mp, revision, build_url, reason=None):
222 """Disapprove a given merge proposal a revision (vote Needs Fixing).
223
224 :params mp: launchapd handle to the respective merge proposal
225 :params revision: revision that should be fixed
226 :params build_url: jenkins build url with the details. This job is used to
227 generate the message with all the links to test runs as
228 well as artifacts (coverity, deb files, etc)
229 :params reason: optional string that is attached to the comment
230 """
231 state = "FAILED: Continuous integration, rev:{revision}".format(
232 revision=revision)
233 if reason:
234 state = "{state}\n{reason}".format(state=state, reason=reason)
235
236 logger.debug(state)
237 content = jenkinsutils.format_message_for_mp_update(
238 build_url, state + "\n")
239 mp.createComment(review_type=get_config_option('launchpad_review_type'),
240 vote=LaunchpadVote.NEEDS_FIXING,
241 subject=get_vote_subject(mp),
242 content=content)
243
244# launchpadlib is not thread/process safe so we are creating launchpadlib
245# cache in /tmp per process which gets cleaned up at the end
246# see also lp:459418 and lp:1025153
247launchpad_cachedir = os.path.join('/tmp', str(os.getpid()), '.launchpadlib')
248
249# `launchpad_cachedir` is leaked upon unexpected exits
250# adding this cleanup to stop directories filling up `/tmp/`
251atexit.register(rmtree, os.path.join('/tmp',
252 str(os.getpid())),
253 ignore_errors=True)
254
255lp_handle = get_launchpad(launchpadlib_dir=launchpad_cachedir)
256mp = get_mp_handle_from_url(lp_handle, args["merge_proposal"])
257if not mp:
258 parser.error('merge proposal related to this branch was not found')
259
260# this is the status from tests
261overal_status = args['status']
262# by default reason is empty as it is usually just a failed build
263reason = ''
264
265if overal_status == 'PASSED':
266 approve_mp(mp, args['revision'], args['build_url'])
267else: # status == False corresponds to NOT 'PASSED'
268 disapprove_mp(mp,
269 args['revision'],
270 args['build_url'],
271 reason)

Subscribers

People subscribed via source and target branches