Merge lp:~canonical-platform-qa/snappy-ecosystem-tests/build-snap into lp:snappy-ecosystem-tests

Proposed by Omer Akram
Status: Merged
Approved by: Omer Akram
Approved revision: 38
Merged at revision: 24
Proposed branch: lp:~canonical-platform-qa/snappy-ecosystem-tests/build-snap
Merge into: lp:snappy-ecosystem-tests
Diff against target: 235 lines (+174/-10)
3 files modified
snappy_ecosystem_tests/helpers/snapcraft/build_snap.py (+140/-0)
snappy_ecosystem_tests/helpers/snapd/snapd.py (+1/-4)
snappy_ecosystem_tests/utils/ssh.py (+33/-6)
To merge this branch: bzr merge lp:~canonical-platform-qa/snappy-ecosystem-tests/build-snap
Reviewer Review Type Date Requested Status
Heber Parrucci (community) Approve
platform-qa-bot continuous-integration Approve
Santiago Baldassin (community) Approve
Review via email: mp+318543@code.launchpad.net

Commit message

Build a snap and pull it

Description of the change

We have many test cases that upload a new snap to the store, or upload a new revision of an already existing snap, we could ship snaps in our test suite but I am not sure how will that play out if we have to register and upload a new snap for a random new name. So in such cases we just quickly build a new snap in an isolated environment.

To post a comment you must log in.
31. By Omer Akram

tweak existing

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
32. By Omer Akram

Fix absolute path

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
33. By Omer Akram

fix

34. By Omer Akram

some cleanup; also require less parameters to build

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
35. By Omer Akram

extend paramiko client so that we don't have to provide credentials for every call

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
36. By Omer Akram

push new revision for kicks

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Santiago Baldassin (sbaldassin) wrote :

Looks good. Minor comments/question inline

review: Needs Information
37. By Omer Akram

Make changes per questions

Revision history for this message
Omer Akram (om26er) wrote :

replied.

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Santiago Baldassin (sbaldassin) wrote :

Code looks good to me

review: Approve
Revision history for this message
Heber Parrucci (heber013) wrote :

Just minor comments inline.

review: Needs Fixing
38. By Omer Akram

Add detailed docstrings

Revision history for this message
Omer Akram (om26er) wrote :

Replied inline and also updated docstrings.

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Heber Parrucci (heber013) wrote :

Code LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'snappy_ecosystem_tests/helpers/snapcraft/build_snap.py'
2--- snappy_ecosystem_tests/helpers/snapcraft/build_snap.py 1970-01-01 00:00:00 +0000
3+++ snappy_ecosystem_tests/helpers/snapcraft/build_snap.py 2017-03-02 16:25:26 +0000
4@@ -0,0 +1,140 @@
5+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
6+
7+#
8+# Snappy Ecosystem Tests
9+# Copyright (C) 2017 Canonical
10+#
11+# This program is free software: you can redistribute it and/or modify
12+# it under the terms of the GNU General Public License as published by
13+# the Free Software Foundation, either version 3 of the License, or
14+# (at your option) any later version.
15+#
16+# This program is distributed in the hope that it will be useful,
17+# but WITHOUT ANY WARRANTY; without even the implied warranty of
18+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+# GNU General Public License for more details.
20+#
21+# You should have received a copy of the GNU General Public License
22+# along with this program. If not, see <http://www.gnu.org/licenses/>.
23+#
24+
25+"""Module to build a snap in a remote machine."""
26+
27+import os
28+from pathlib import PurePath
29+
30+from snappy_ecosystem_tests.utils.ssh import SSHManager
31+
32+COMMANDS_SETUP = [
33+ 'apt update',
34+ 'apt dist-upgrade -y',
35+ 'apt install snapcraft -y'
36+]
37+SNAPCRAFT_YAML = """\
38+name: {snap_name}
39+version: {version}
40+summary: {summary}
41+description: |
42+ {description}
43+
44+grade: {grade}
45+confinement: {confinement}
46+
47+parts:
48+ {snap_name}:
49+ plugin: nil
50+
51+apps:
52+ {snap_name}:
53+ command: echo ok
54+"""
55+
56+
57+class SnapBuilder:
58+ """Class to build a snap on a remote machine and download its content."""
59+ def __init__(self, hostname, username, port=22):
60+ self.client = SSHManager.get_instance(hostname, username, port)
61+
62+ def is_host_setup(self):
63+ """Return bool representing whether the remote host is setup by
64+ checking if snapcraft package is installed."""
65+ try:
66+ self.client.exec_command('which snapcraft')
67+ return True
68+ except ValueError:
69+ return False
70+
71+ def setup_host(self):
72+ """Setup the host to build a snap, currently only installs
73+ snapcraft."""
74+ for command in COMMANDS_SETUP:
75+ self.client.exec_command(command)
76+
77+ def build(self, name, version, summary, description, grade='stable',
78+ confinement='strict'):
79+ """Build a new snap in the remote host and return its remote path.
80+
81+ :param name: name of the snap to build.
82+ :param version: version number for the snap to build.
83+ :param summary: short summary of the snap to build.
84+ :param description: long description of the snap.
85+ :param grade: stability grade of the snap e.g. stable or devel.
86+ :param confinement: confinement policy for the snap e.g.
87+ classic, devmode or strict.
88+ :raises ValueError: If an error is occurred during snap build.
89+ :return: Remote path of the newly build snap package.
90+ """
91+ if not self.is_host_setup():
92+ self.setup_host()
93+ tempdir = self.client.exec_command('mktemp -d')
94+ self.client.exec_command('mkdir snap', cwd=tempdir)
95+ self.client.exec_command('echo "{}" > snap/snapcraft.yaml'.format(
96+ SNAPCRAFT_YAML.format(
97+ snap_name=name,
98+ version=version,
99+ summary=summary,
100+ description=description,
101+ grade=grade,
102+ confinement=confinement
103+ )
104+ ), cwd=tempdir)
105+ self.client.exec_command('snapcraft', cwd=tempdir)
106+ return os.path.join(
107+ tempdir,
108+ '{name}_{version}_{arch}.snap'.format(
109+ name=name,
110+ version=version,
111+ arch=self.client.exec_command('dpkg --print-architecture')
112+ )
113+ )
114+
115+ def build_and_pull(self, name, version, summary, description,
116+ grade='stable', confinement='strict',
117+ output_location='.'):
118+ """Build a snap based on the provided arguments and download it.
119+
120+ :param name: name of the snap to build.
121+ :param version: version number for the snap to build.
122+ :param summary: short summary of the snap to build.
123+ :param description: long description of the snap.
124+ :param grade: stability grade of the snap e.g. stable or devel.
125+ :param confinement: confinement policy for the snap e.g.
126+ classic, devmode or strict.
127+ :param output_location: Local location to download the newly built
128+ snap to. Defaults to pwd.
129+ :raises ValueError: If an error is occurred during snap build.
130+ :return: Local path of the newly built and downloaded snap package.
131+ """
132+ remote_snap_path = self.build(
133+ name=name,
134+ version=version,
135+ summary=summary,
136+ description=description,
137+ grade=grade,
138+ confinement=confinement
139+ )
140+ output_path_absolute = os.path.abspath(
141+ os.path.join(output_location, PurePath(remote_snap_path).name)
142+ )
143+ self.client.pull(remote_snap_path, output_path_absolute)
144+ return output_path_absolute
145
146=== modified file 'snappy_ecosystem_tests/helpers/snapd/snapd.py'
147--- snappy_ecosystem_tests/helpers/snapd/snapd.py 2017-02-21 15:17:12 +0000
148+++ snappy_ecosystem_tests/helpers/snapd/snapd.py 2017-03-02 16:25:26 +0000
149@@ -56,10 +56,7 @@
150 should run.
151 :returns: stdout of the command
152 """
153- if cwd:
154- return ssh.run_command(
155- 'cd {}; {} {}'.format(cwd, PATH_SNAP, parameters))
156- return ssh.run_command('{} {}'.format(PATH_SNAP, parameters))
157+ return ssh.run_command('{} {}'.format(PATH_SNAP, parameters), cwd=cwd)
158
159
160 def login(email, password):
161
162=== modified file 'snappy_ecosystem_tests/utils/ssh.py'
163--- snappy_ecosystem_tests/utils/ssh.py 2017-02-22 18:56:04 +0000
164+++ snappy_ecosystem_tests/utils/ssh.py 2017-03-02 16:25:26 +0000
165@@ -20,10 +20,39 @@
166
167 """Module to connect to ssh client for running tests."""
168
169+import logging
170 import paramiko
171
172 from snappy_ecosystem_tests.utils.user import get_remote_host_credentials
173
174+LOGGER = logging.getLogger(__name__)
175+
176+
177+class SSHClient(paramiko.SSHClient):
178+ """Extended SSHClient."""
179+
180+ def exec_command(self, command, bufsize=-1, timeout=None, get_pty=False,
181+ cwd=''):
182+ """Execute the given command over ssh."""
183+ if cwd:
184+ command = 'cd {}; {}'.format(cwd, command)
185+ _, stdout, stderr = super().exec_command(command, bufsize, timeout,
186+ get_pty)
187+ if stdout.channel.recv_exit_status() != 0:
188+ raise ValueError(stderr.read().decode().strip())
189+ response = stdout.read().decode().strip()
190+ LOGGER.info(response)
191+ return response
192+
193+ def pull(self, remote_path, local_path):
194+ """Pull file/directory from a remote location.
195+
196+ :param remote_path: Path on the remote location.
197+ :param local_path: Path to download to.
198+ """
199+ sftp = self.open_sftp()
200+ sftp.get(remote_path, local_path)
201+
202
203 class SSHManager:
204 """Manager class to keep a pool of ssh connections."""
205@@ -46,7 +75,7 @@
206 client.connect(
207 hostname, username=username, port=port, **kwargs)
208 else:
209- client = paramiko.SSHClient()
210+ client = SSHClient()
211 client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
212 client.connect(hostname, username=username, port=port, **kwargs)
213 SSHManager.__connection_pool.append(client)
214@@ -73,10 +102,11 @@
215 return connections[0] if connections else None
216
217
218-def run_command(command, hostname=None, username=None, port=None):
219+def run_command(command, cwd=None, hostname=None, username=None, port=None):
220 """Run the given command on remote machine over ssh.
221
222 :param command: a string of the command to run.
223+ :param cwd: Current working directory for the command.
224 :param hostname: The host to run command on.
225 :param username: Name of the user on the remote host to login to.
226 :param port: SSH port number.
227@@ -86,7 +116,4 @@
228 _hostname, _username, _port = get_remote_host_credentials()
229 ssh = SSHManager.get_instance(
230 hostname or _hostname, username or _username, port or _port)
231- _, stdout, stderr = ssh.exec_command(command)
232- if stdout.channel.recv_exit_status() != 0:
233- raise ValueError(stderr.read().decode())
234- return stdout.read().decode()
235+ return ssh.exec_command(command, cwd=cwd)

Subscribers

People subscribed via source and target branches