Merge lp:~heber013/auto-upgrade-testing/adding-keep-overlay-option into lp:auto-upgrade-testing

Proposed by Heber Parrucci
Status: Merged
Approved by: Jean-Baptiste Lallement
Approved revision: 96
Merged at revision: 89
Proposed branch: lp:~heber013/auto-upgrade-testing/adding-keep-overlay-option
Merge into: lp:auto-upgrade-testing
Diff against target: 860 lines (+714/-8)
8 files modified
.flake8 (+5/-0)
.project-flake8.sh (+19/-1)
debian/control (+6/-0)
upgrade_testing/command_line.py (+14/-4)
upgrade_testing/provisioning/_provisionconfig.py (+3/-0)
upgrade_testing/provisioning/backends/_qemu.py (+201/-3)
upgrade_testing/provisioning/backends/_ssh.py (+260/-0)
upgrade_testing/provisioning/executors.py (+206/-0)
To merge this branch: bzr merge lp:~heber013/auto-upgrade-testing/adding-keep-overlay-option
Reviewer Review Type Date Requested Status
Jean-Baptiste Lallement Approve
platform-qa-bot continuous-integration Approve
Review via email: mp+327450@code.launchpad.net

Commit message

Adding keep-overlay option to persist the resulting image file in order to be able to run system tests after the upgrade is done.

Description of the change

Adding keep-overlay option to persist the resulting image file in order to be able to run system tests after the upgrade is done.

To post a comment you must log in.
Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
90. By Heber Parrucci

Fixing flake8 issues

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
91. By Heber Parrucci

fixing virtualenv issue

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
92. By Heber Parrucci

running flake8 without virtualenv

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
93. By Heber Parrucci

removing deactivate from script

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
94. By Heber Parrucci

fixing import error when running selftests

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
95. By Heber Parrucci

Adding missing dependency

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Needs Fixing (continuous-integration)
96. By Heber Parrucci

Adding missing dependecies

Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Jean-Baptiste Lallement (jibel) wrote :

Thanks. Approved!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file '.flake8'
--- .flake8 1970-01-01 00:00:00 +0000
+++ .flake8 2017-07-14 19:31:31 +0000
@@ -0,0 +1,5 @@
1[flake8]
2ignore = E999, W503
3exclude = venv_tests/
4max-complexity = 7
5max-line-length = 80
06
=== modified file '.project-flake8.sh'
--- .project-flake8.sh 2016-03-03 04:29:09 +0000
+++ .project-flake8.sh 2017-07-14 19:31:31 +0000
@@ -1,2 +1,20 @@
1# Helpful doc: http://pep8.readthedocs.org/en/latest/intro.html#error-codes1# Helpful doc: http://pep8.readthedocs.org/en/latest/intro.html#error-codes
2python3 -m flake8.run --ignore=W503 .2#!/bin/bash
3RED='\033[0;31m'
4GREEN='\033[0;32m'
5
6OUTPUT=flake8_output.txt
7if [ -f $OUTPUT ]; then
8 rm $OUTPUT
9fi
10
11python3 -m flake8 --output-file=$OUTPUT .
12
13result=$?
14if [ $result != 0 ]; then
15 echo -e -n "${RED}Flake8 errors. Check flake8 output for more information\n"
16 cat $OUTPUT
17 exit 1;
18else
19 echo -e -n "${GREEN}Congratulations!!! Static check PASSED\n"
20fi
321
=== modified file 'debian/control'
--- debian/control 2017-02-03 20:16:09 +0000
+++ debian/control 2017-07-14 19:31:31 +0000
@@ -7,6 +7,9 @@
7 python3-all-dev (>= 3.4),7 python3-all-dev (>= 3.4),
8 python3-flake8,8 python3-flake8,
9 python3-lxc,9 python3-lxc,
10 python3-paramiko,
11 python3-pexpect,
12 python3-retrying,
10 python3-setuptools,13 python3-setuptools,
11 python3-yaml,14 python3-yaml,
12Standards-Version: 3.9.315Standards-Version: 3.9.3
@@ -22,7 +25,10 @@
22 lxc-templates,25 lxc-templates,
23 python3-yaml,26 python3-yaml,
24 python3-lxc,27 python3-lxc,
28 python3-paramiko,
29 python3-pexpect,
25 python3-pkg-resources,30 python3-pkg-resources,
31 python3-retrying,
26 android-tools-adb,32 android-tools-adb,
27 phablet-tools,33 phablet-tools,
28Description: Test release upgrades in a virtual environment34Description: Test release upgrades in a virtual environment
2935
=== modified file 'upgrade_testing/command_line.py'
--- upgrade_testing/command_line.py 2017-05-23 09:54:22 +0000
+++ upgrade_testing/command_line.py 2017-07-14 19:31:31 +0000
@@ -69,6 +69,11 @@
69 parser.add_argument(69 parser.add_argument(
70 '--adt-args', '-a', default='',70 '--adt-args', '-a', default='',
71 help='Arguments to pass through to the autopkgtest runner.')71 help='Arguments to pass through to the autopkgtest runner.')
72 parser.add_argument(
73 '--keep-overlay', '-k',
74 default=False,
75 action='store_true',
76 help='Whether to keep the resulting overlay image')
72 return parser.parse_args()77 return parser.parse_args()
7378
7479
@@ -137,7 +142,8 @@
137 print('\n'.join(output))142 print('\n'.join(output))
138143
139144
140def execute_adt_run(testsuite, testrun_files, output_dir, adt_args=''):145def execute_adt_run(testsuite, testrun_files, output_dir, adt_args='',
146 keep_overlay=False):
141 """Prepare the autopkgtest to execute.147 """Prepare the autopkgtest to execute.
142148
143 Copy all the files into the expected place etc.149 Copy all the files into the expected place etc.
@@ -153,13 +159,14 @@
153 output_dir,159 output_dir,
154 testsuite.backend_args,160 testsuite.backend_args,
155 adt_args,161 adt_args,
162 keep_overlay
156 )163 )
157 subprocess.check_call(adt_run_command)164 subprocess.check_call(adt_run_command)
158165
159166
160def get_adt_run_command(167def get_adt_run_command(
161 provisioning, testrun_files, results_dir, backend_args=[],168 provisioning, testrun_files, results_dir, backend_args=[],
162 adt_args=''):169 adt_args='', keep_overlay=False):
163 """Construct the adt command to run.170 """Construct the adt command to run.
164171
165 :param provisioning: upgrade_testing.provisioning.ProvisionSpecification172 :param provisioning: upgrade_testing.provisioning.ProvisionSpecification
@@ -210,7 +217,8 @@
210 )217 )
211218
212 backend_args = provisioning.get_adt_run_args(219 backend_args = provisioning.get_adt_run_args(
213 tmp_dir=testrun_files.testrun_tmp_dir220 tmp_dir=testrun_files.testrun_tmp_dir,
221 keep_overlay=keep_overlay
214 ) + backend_args222 ) + backend_args
215223
216 return adt_cmd + ['---'] + backend_args224 return adt_cmd + ['---'] + backend_args
@@ -263,7 +271,9 @@
263 output_dir = get_output_dir(args)271 output_dir = get_output_dir(args)
264272
265 execute_adt_run(testsuite, created_files, output_dir,273 execute_adt_run(testsuite, created_files, output_dir,
266 args.adt_args)274 args.adt_args, args.keep_overlay)
275
276 testsuite.provisioning.close()
267277
268 display_results(output_dir)278 display_results(output_dir)
269279
270280
=== modified file 'upgrade_testing/provisioning/_provisionconfig.py'
--- upgrade_testing/provisioning/_provisionconfig.py 2016-03-18 06:15:14 +0000
+++ upgrade_testing/provisioning/_provisionconfig.py 2017-07-14 19:31:31 +0000
@@ -58,6 +58,9 @@
58 """Provision the stored backend."""58 """Provision the stored backend."""
59 return self.backend.create(adt_base_path)59 return self.backend.create(adt_base_path)
6060
61 def close(self):
62 return self.backend.close() if hasattr(self.backend, 'close') else None
63
61 def get_adt_run_args(self, **kwargs):64 def get_adt_run_args(self, **kwargs):
62 """Return list with the adt args for this provisioning backend."""65 """Return list with the adt args for this provisioning backend."""
63 raise NotImplementedError()66 raise NotImplementedError()
6467
=== modified file 'upgrade_testing/provisioning/backends/_qemu.py'
--- upgrade_testing/provisioning/backends/_qemu.py 2016-05-24 18:32:17 +0000
+++ upgrade_testing/provisioning/backends/_qemu.py 2017-07-14 19:31:31 +0000
@@ -18,16 +18,60 @@
1818
19import logging19import logging
20import os20import os
21import shlex
22import shutil
23import signal
24import subprocess
25import tempfile
26import threading
27
28from paramiko.ssh_exception import SSHException
2129
22from upgrade_testing.provisioning._util import run_command_with_logged_output30from upgrade_testing.provisioning._util import run_command_with_logged_output
23from upgrade_testing.provisioning.backends._base import ProviderBackend31from upgrade_testing.provisioning.backends._ssh import SshBackend
2432
25CACHE_DIR = '/var/cache/auto-upgrade-testing'33CACHE_DIR = '/var/cache/auto-upgrade-testing'
34OVERLAY_DIR = os.path.join(CACHE_DIR, 'overlay')
35QEMU_LAUNCH_OPTS = (
36 '{qemu} -m {ram} -smp {cpu} -pidfile {workdir}/qemu.pid -localtime '
37 '-cpu core2duo -enable-kvm '
38)
39QEMU_SYSTEM_AMD64 = 'qemu-system-x86_64'
40QEMU_SYSTEM_I386 = 'qemu-system-i386'
41ARCH_AMD64 = 'amd64'
42ARCH_I386 = 'i386'
43QEMU_DISPLAY_OPTS = (
44 '-display sdl '
45)
46QEMU_DISPLAY_VGA_OPTS = (
47 '-vga qxl '
48)
49QEMU_SOUND_OPTS = (
50 '-soundhw all '
51)
52QEMU_DISPLAY_HEADLESS = (
53 '-display none '
54)
55QEMU_NET_OPTS = (
56 '-net nic,model=virtio -net user'
57)
58QEMU_PORT_OPTS = (
59 ',hostfwd=tcp::{port}-:22 '
60)
61QEMU_DISK_IMAGE_OPTS = (
62 '-drive file={disk_img},if=virtio '
63)
64QEMU_DISK_IMAGE_OVERLAY_OPTS = (
65 '-drive file={overlay_img},cache=unsafe,if=virtio,index=0 '
66)
67DEFAULT_RAM = '2G'
68DEFAULT_CPU = '2'
69HEADLESS = True
2670
27logger = logging.getLogger(__name__)71logger = logging.getLogger(__name__)
2872
2973
30class QemuBackend(ProviderBackend):74class QemuBackend(SshBackend):
3175
32 # We can change the Backends to require just what they need. In this case76 # We can change the Backends to require just what they need. In this case
33 # it would be distribution, release name (, arch)77 # it would be distribution, release name (, arch)
@@ -38,10 +82,14 @@
38 details.82 details.
3983
40 """84 """
85 super().__init__(release, arch, image_name, build_args)
41 self.release = release86 self.release = release
42 self.arch = arch87 self.arch = arch
43 self.image_name = image_name88 self.image_name = image_name
44 self.build_args = build_args89 self.build_args = build_args
90 self.working_dir = None
91 self.qemu_runner = None
92 self.find_free_port()
4593
46 def available(self):94 def available(self):
47 """Return true if a qemu exists that matches the provided args.95 """Return true if a qemu exists that matches the provided args.
@@ -73,9 +121,60 @@
73 os.rename(initial_image_path, final_image_path)121 os.rename(initial_image_path, final_image_path)
74 logger.info('Image created.')122 logger.info('Image created.')
75123
76 def get_adt_run_args(self, **kwargs):124 def close(self):
125 if self.qemu_runner:
126 try:
127 self.shutdown()
128 except PermissionError:
129 print('Shutdown sudo command failed. '
130 'Check password: "{}".'.format(self.password))
131 self.stop_qemu()
132 except SSHException:
133 self.stop_qemu()
134 finally:
135 self.qemu_runner.join(timeout=5)
136 shutil.rmtree(self.working_dir)
137 self.working_dir = None
138 self.qemu_runner = None
139 super().close()
140
141 def reboot(self):
142 self.close()
143 self.connect()
144
145 def stop_qemu(self):
146 pid_file = os.path.join(self.working_dir, 'qemu.pid')
147 with open(pid_file) as f:
148 pid = int(f.read().strip())
149 os.kill(pid, signal.SIGTERM)
150
151 def get_adt_run_args(self, keep_overlay=False, **kwargs):
152 if keep_overlay:
153 self.qemu_runner = self.launch_qemu(
154 self.image_name,
155 kwargs.get('ram', DEFAULT_RAM),
156 kwargs.get('cpu', DEFAULT_CPU),
157 kwargs.get('headless', HEADLESS),
158 port=self.port,
159 overlay=os.path.join(OVERLAY_DIR,
160 self.image_name))
161 super().connect()
162 return super().get_adt_run_args()
77 return ['qemu', os.path.join(CACHE_DIR, self.image_name)]163 return ['qemu', os.path.join(CACHE_DIR, self.image_name)]
78164
165 def create_overlay_image(self, overlay_img):
166 """Create an overlay image for specified base image."""
167 overlay_dir = os.path.dirname(overlay_img)
168 if os.path.isfile(overlay_img):
169 os.remove(overlay_img)
170 elif not os.path.isdir(overlay_dir):
171 os.makedirs(overlay_dir)
172 subprocess.check_call(
173 ['qemu-img', 'create', '-f', 'qcow2', '-b',
174 os.path.join(CACHE_DIR, self.image_name),
175 overlay_img])
176 subprocess.check_call(['sudo', 'chmod', '777', overlay_img])
177
79 @property178 @property
80 def name(self):179 def name(self):
81 return 'qemu'180 return 'qemu'
@@ -85,3 +184,102 @@
85 classname=self.__class__.__name__,184 classname=self.__class__.__name__,
86 release=self.release185 release=self.release
87 )186 )
187
188 @staticmethod
189 def get_architecture():
190 """Return architecture string for system."""
191 return subprocess.check_output(
192 ['dpkg', '--print-architecture']).decode().strip()
193
194 def get_qemu_path(self):
195 """Return path of qemu-system executable for system."""
196 if self.get_architecture() == ARCH_AMD64:
197 target = QEMU_SYSTEM_AMD64
198 else:
199 target = QEMU_SYSTEM_I386
200 return subprocess.check_output(['which', target]).decode().strip()
201
202 def get_disk_args(self, overlay):
203 """Return qemu-system disk args. If overlay is specified then an overlay
204 image at that path will be created and specified in returned arguments.
205 If no overlay is none then the base image will be returned in
206 the arguments.
207 :param overlay: Path of overlay image to use, otherwise None
208 if not needed.
209 :return: Disk image arguments as string.
210 """
211 if overlay:
212 self.create_overlay_image(overlay)
213 return QEMU_DISK_IMAGE_OVERLAY_OPTS.format(overlay_img=overlay)
214 else:
215 return QEMU_DISK_IMAGE_OPTS.format(disk_img=self.image_name)
216
217 @staticmethod
218 def get_display_args(headless):
219 """Return qemu-system display arguments based on headless parameter.
220 :param headless: Whether qemu-system should run in headless mode or not.
221 :return: Display parameters for required display state.
222 """
223 if headless:
224 return QEMU_DISPLAY_HEADLESS + QEMU_DISPLAY_VGA_OPTS
225 else:
226 return QEMU_DISPLAY_OPTS + QEMU_DISPLAY_VGA_OPTS + QEMU_SOUND_OPTS
227
228 def launch_qemu(self, img, ram, cpu, headless, port, overlay):
229 """Boot the qemu from a different thread to stop this thread from being
230 blocked whilst the qemu is running.
231 """
232 self.working_dir = tempfile.mkdtemp()
233 runner = threading.Thread(
234 target=self._launch_qemu,
235 args=(self.working_dir, img, ram, cpu, headless, port, overlay))
236 runner.start()
237 return runner
238
239 def _launch_qemu(self, working_dir, disk_image_path, ram, cpu, headless,
240 port=None, overlay=None):
241 """Launch qemu-system to install the iso file into the disk image.
242 :param working_dir: Working directory to use.
243 :param disk_image_path: Path of the disk image file used for
244 installation.
245 :param ram: Amount of ram allocated to qemu.
246 :param cpu: Number of cpus allocated to qemu.
247 :param headless: Whether to run installer in headless mode or not.
248 :param port: Host port number to enable port forwarding to qemu port 22.
249
250 """
251 cmd = self.get_qemu_launch_command(
252 working_dir, disk_image_path, ram, cpu, headless,
253 port, overlay)
254 print(' '.join(cmd))
255 subprocess.check_call(cmd)
256
257 def get_qemu_launch_command(self, work_dir, disk_img, ram, cpu,
258 headless, port=None, overlay=None):
259 """Return command to launch qemu process using optional install parameters.
260 :param work_dir: Working directory to use.
261 :param disk_img: Path of the disk image file used for installation.
262 :param ram: Amount of ram allocated to qemu.
263 :param cpu: Number of cpus allocated to qemu.
264 :param headless: Whether to run installer in headless mode or not.
265 :return: Qemu launch command string.
266 :param port: Host port number to enable port forwarding to qemu port 22.
267 :param overlay: path to the overlay image to be created
268 """
269 # Create command base with resource parameters
270 cmd = QEMU_LAUNCH_OPTS.format(
271 qemu=self.get_qemu_path(), ram=ram, cpu=cpu, disk_img=disk_img,
272 workdir=work_dir)
273 # Get disk args including overlay image if specified
274 cmd += self.get_disk_args(overlay)
275 # Add display parameters
276 cmd += self.get_display_args(headless)
277 # Add network. This must preceed the port forwarding option.
278 cmd += QEMU_NET_OPTS
279 # Add port forwarding if specified
280 if port:
281 cmd += QEMU_PORT_OPTS.format(port=port)
282 else:
283 # Add space to separate options
284 cmd += ' '
285 return shlex.split(cmd)
88286
=== added file 'upgrade_testing/provisioning/backends/_ssh.py'
--- upgrade_testing/provisioning/backends/_ssh.py 1970-01-01 00:00:00 +0000
+++ upgrade_testing/provisioning/backends/_ssh.py 2017-07-14 19:31:31 +0000
@@ -0,0 +1,260 @@
1#
2# Ubuntu Upgrade Testing
3# Copyright (C) 2015 Canonical
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 as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
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#
18
19import logging
20import os
21import socket
22import subprocess
23
24import time
25
26import errno
27import pexpect
28from retrying import retry
29
30from upgrade_testing.provisioning.backends._base import ProviderBackend
31from upgrade_testing.provisioning.executors import SSHExecutor
32
33CACHE_DIR = '/var/cache/auto-upgrade-testing'
34
35logger = logging.getLogger(__name__)
36TIMEOUT_CMD = 60
37TIMEOUT_CONNECT = 120
38TIMEOUT_WAIT_FOR_DEVICE = 120
39
40
41class SshBackend(ProviderBackend):
42
43 # We can change the Backends to require just what they need. In this case
44 # it would be distribution, release name (, arch)
45 def __init__(self, release, arch, image_name, build_args=[],
46 username=None, password=None, device_ip=None):
47 """Provide backend capabilities as requested in the provision spec.
48
49 :param provision_spec: ProvisionSpecification object containing backend
50 details.
51
52 """
53 self.release = release
54 self.arch = arch
55 self.image_name = image_name
56 self.build_args = build_args
57 self.executor = SSHExecutor()
58 self.username = username or 'ubuntu'
59 self.password = password or 'ubuntu'
60 self.connected = False
61 self.key_file = None
62 self.device_ip = device_ip or 'localhost'
63 self.port = -1
64
65 def available(self):
66 """Return true if a qemu exists that matches the provided args.
67
68 """
69 image_name = self.image_name
70 logger.info('Checking for {}'.format(image_name))
71 return image_name in os.listdir(CACHE_DIR)
72
73 def create(self, adt_base_path):
74 raise NotImplementedError('Cannot create ssh backend directly')
75
76 def get_adt_run_args(self, **kwargs):
77 return ['ssh', '--port', str(self.port),
78 '--login', self.username,
79 '--password', self.password,
80 '--identity', self.key_file,
81 '--hostname',
82 self.device_ip,
83 '--reboot']
84
85 @property
86 def name(self):
87 return 'ssh'
88
89 def __repr__(self):
90 return '{classname}(release={release})'.format(
91 classname=self.__class__.__name__,
92 release=self.release
93 )
94
95 port_from = 22220
96 port_to = 23000
97
98 def connect(self, timeout=TIMEOUT_CONNECT):
99 if not self.connected:
100 self.enable_ssh()
101 self.executor.connect(
102 self.username, self.password, self.port, self.device_ip,
103 timeout)
104 self.connected = True
105
106 def close(self):
107 if self.connected:
108 self.executor.close()
109 self.connected = False
110
111 def reboot(self):
112 self.executor.reboot()
113
114 def shutdown(self):
115 self.executor.shutdown()
116
117 def put(self, src, dst):
118 self.executor.put(src, dst)
119
120 def run(self, command, timeout=TIMEOUT_CMD, log_stdout=True):
121 return self.executor.run(command, timeout, log_stdout)
122
123 def run_sudo(self, command, timeout=TIMEOUT_CMD, log_stdout=True):
124 return self.executor.run_sudo(command, timeout, log_stdout)
125
126 def find_free_port(self):
127 for port in range(self.port_from, self.port_to):
128 try:
129 s = socket.create_connection(('127.0.0.1', port))
130 except socket.error as e:
131 if e.errno == errno.ECONNREFUSED:
132 # This means port is not currently used, so use this one
133 self.port = port
134 return
135 else:
136 pass
137 else:
138 # port is already taken
139 s.close()
140 raise RuntimeError('Could not find free port for SSH connection.')
141
142 def enable_ssh(self):
143 """Enable ssh using public key."""
144 self._wait_for_device()
145 self._get_ssh_id_path()
146 if not self._try_public_key_login():
147 self._update_device_host_key()
148 self._copy_ssh_id_to_device()
149 self._verify_ssh_connect()
150
151 def _wait_for_device(self, timeout=TIMEOUT_CONNECT):
152 end = time.time() + timeout
153 while time.time() < end:
154 try:
155 s = socket.create_connection(
156 (self.device_ip, str(self.port)), timeout)
157 except ConnectionRefusedError:
158 time.sleep(1)
159 else:
160 s.close()
161 return
162 raise TimeoutError(
163 'Could not connect to {} '
164 'port {}.'.format(self.device_ip, self.port))
165
166 def _update_device_host_key(self):
167 hosts_path = os.path.expanduser('~/.ssh/known_hosts')
168 subprocess.call([
169 'ssh-keygen', '-f', hosts_path, '-R',
170 '[{}]:{}'.format(self.device_ip, self.port)])
171
172 @retry(stop_max_attempt_number=20, wait_fixed=2000,
173 retry_on_exception=lambda exception: isinstance(
174 exception, RuntimeError))
175 def _copy_ssh_id_to_device(self):
176 pub_path = '{}.pub'.format(self.key_file)
177 home_ssh = '/home/{u}/.ssh'.format(u=self.username)
178 authorized_keys = os.path.join(home_ssh, 'authorized_keys')
179 self._run(['mkdir', '-p', home_ssh])
180 self._put(pub_path, authorized_keys)
181 self._run(
182 ['chown', '{u}:{u}'.format(u=self.username), '-R', home_ssh])
183 self._run(['chmod', '700', home_ssh])
184 self._run(['chmod', '600', authorized_keys])
185
186 def _get_ssh_id_path(self):
187 match = False
188 for id in ['~/.ssh/id_rsa', '~/.ssh/id_autopkgtest']:
189 path = os.path.expanduser(id)
190 if os.path.exists(path):
191 match = True
192 break
193 if not match:
194 subprocess.check_call([
195 'ssh-keygen', '-q', '-t', 'rsa', '-f', path, '-N', ''])
196 self.key_file = path
197
198 def _try_public_key_login(self):
199 """Try and log in using public key. If this succeeds then there is no
200 need to do any further ssh setup.
201 :return: True if login was successful, False otherwise
202 """
203 cmd = ' '.join(['ssh', '-p', str(self.port),
204 '-o', 'UserKnownHostsFile=/dev/null',
205 '-o', 'StrictHostKeyChecking=no',
206 '-i', self.key_file, '-l', self.username,
207 self.device_ip])
208 child = pexpect.spawn(cmd)
209 try:
210 index = child.expect(
211 ['\$', 'password', 'denied'], timeout=TIMEOUT_CONNECT)
212 except (pexpect.exceptions.TIMEOUT, pexpect.exceptions.EOF):
213 index = -1
214 finally:
215 child.close()
216 return index == 0
217
218 def _verify_ssh_connect(self):
219 """Verify that an ssh connection can be established without using
220 password.
221 """
222 for count in range(20):
223 if self._try_public_key_login():
224 return
225 else:
226 time.sleep(1)
227 raise RuntimeError('Could not create ssh connection')
228
229 def _run(self, commands, timeout=TIMEOUT_CMD):
230 """Run a command setting up an ssh connection using password."""
231 ssh_cmd = [
232 'ssh', '-o', 'StrictHostKeyChecking=no', '-p', str(self.port),
233 '{}@{}'.format(self.username, self.device_ip)]
234 self._run_with_password(ssh_cmd + commands, self.password, timeout)
235
236 def _put(self, src, dst, timeout=TIMEOUT_CMD):
237 """Put file onto device setting up an ssh connection using password."""
238 scp_cmd = [
239 'scp', '-o', 'StrictHostKeyChecking=no', '-P', str(self.port),
240 src, '{}@{}:{}'.format(self.username, self.device_ip, dst)]
241 self._run_with_password(scp_cmd, self.password, timeout)
242
243 def _run_with_password(self, commands, password, timeout=TIMEOUT_CMD):
244 """Run command expecting a password prompt to be displayed."""
245 command = ' '.join(commands)
246 child = pexpect.spawn(command)
247 try:
248 child.expect('password', timeout=timeout)
249 except pexpect.exceptions.EOF:
250 # No password prompt is displayed, so just continue
251 pass
252 else:
253 child.sendline(password)
254 if child.expect([pexpect.EOF, 'denied'], timeout=timeout):
255 raise PermissionError(
256 'Check password is correct: "{}"'.format(password))
257 finally:
258 child.close()
259 if child.exitstatus:
260 raise RuntimeError('Error running {}'.format(command))
0261
=== added file 'upgrade_testing/provisioning/executors.py'
--- upgrade_testing/provisioning/executors.py 1970-01-01 00:00:00 +0000
+++ upgrade_testing/provisioning/executors.py 2017-07-14 19:31:31 +0000
@@ -0,0 +1,206 @@
1#
2# Ubuntu Upgrade Testing
3# Copyright (C) 2017 Canonical
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 as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
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#
18
19
20import os
21import sys
22import time
23from abc import ABCMeta
24from abc import abstractmethod
25
26import paramiko
27
28TIMEOUT_CMD = 60
29TIMEOUT_CONNECT = 120
30TIMEOUT_WAIT_FOR_DEVICE = 120
31
32
33class Result:
34 """Result of command with status and output properties."""
35
36 def __init__(self):
37 self.status = None
38 self.output = ''
39
40
41class SSHClient:
42 """This class manages the paramiko ssh client"""
43
44 def __init__(self):
45 """ The ssh can be initialized either through a password or with a
46 private key file and the passphrase
47 :param hostname: The hostname to connect
48 :param user: The remote user in the host
49 :param keyfile: The private key used to connect to the remote host
50 :param password: The password used to connect to the remote host
51 :param timeout: An optional timeout (in seconds) for the TCP connect
52 """
53 self.client = paramiko.SSHClient()
54 self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
55
56 def connect(self, hostname, user, password, port, timeout=60):
57 """Connect to remote host."""
58 timeout = float(timeout)
59 self.client.connect(
60 hostname, username=user, password=password, port=port,
61 timeout=timeout, banner_timeout=timeout)
62
63 def close(self):
64 """Close the connection"""
65 self.client.close()
66
67 def run(self, cmd, timeout=TIMEOUT_CMD, log_stdout=True):
68 """Run a command in the remote host.
69 :param cmd: Command to run.
70 :param timeout: Period to wait before raising TimeoutError.
71 :param log_stdout: Whether to log output to stdout.
72 :return: Result object containing command output and status code.
73 """
74 channel = self.client.get_transport().open_session()
75 channel.set_combine_stderr(True)
76 end = time.time() + timeout
77 result = Result()
78 channel.exec_command(cmd)
79 while (not channel.exit_status_ready() or (
80 channel.exit_status_ready() and channel.recv_ready())):
81 if channel.recv_ready():
82 self._process_output(
83 result, log_stdout, channel.recv(1024).decode())
84 elif time.time() > end:
85 print('Timeout waiting {} seconds for command to '
86 'complete: {}'.format(timeout, cmd))
87 raise TimeoutError
88 time.sleep(0.2)
89 # Add a new line after command has completed to ensure output
90 # is separated.
91 self._process_output(result, log_stdout, '\n')
92 result.status = channel.recv_exit_status()
93 return result
94
95 def _process_output(self, result, log_stdout, content):
96 """Save output to result and print to stdout if required."""
97 result.output += content
98 if log_stdout:
99 sys.stdout.write(content)
100 sys.stdout.flush()
101
102 def put(self, local_path, remote_path):
103 """Copy a file through sftp from the local_path to the remote_path"""
104 if not os.path.isfile(local_path):
105 raise RuntimeError('File to copy does not exist')
106
107 with self.client.open_sftp() as sftp:
108 sftp.put(local_path, remote_path)
109
110 def get(self, remote_path, local_path):
111 """Copy a file through sftp from the remote_path to the local_path"""
112 with self.client.open_sftp() as sftp:
113 sftp.get(remote_path, local_path)
114
115 if not os.path.isfile(local_path):
116 raise RuntimeError('File couldn\'t be copied')
117
118
119class Executor:
120 __metaclass__ = ABCMeta
121 """Base class for all target executors."""
122
123 @abstractmethod
124 def connect(self, username, password, port, host=None, timeout=None):
125 pass
126
127 @abstractmethod
128 def close(self):
129 pass
130
131 @abstractmethod
132 def run(self, cmd, timeout=None, log_stdout=True):
133 pass
134
135 @abstractmethod
136 def run_sudo(self, cmd, timeout=None, log_stdout=True):
137 pass
138
139 def reboot(self):
140 result = self.run_sudo('shutdown -r now')
141 if result.status > 0:
142 raise PermissionError('Reboot failed, check password.')
143
144 def shutdown(self):
145 result = self.run_sudo('shutdown now')
146 if result.status > 0:
147 raise PermissionError('Shutdown failed, check password.')
148
149 @abstractmethod
150 def wait_for_device(self, timeout=None):
151 pass
152
153 @abstractmethod
154 def put(self, localpath, remotepath):
155 pass
156
157 @abstractmethod
158 def get(self, remotepath, localpath):
159 pass
160
161 def _get_sudo_command(self, cmd):
162 command = 'sudo {}'.format(cmd)
163 if self.password:
164 command = 'echo {} | sudo -S {}'.format(self.password, cmd)
165 return command
166
167
168class SSHExecutor(Executor):
169
170 def __init__(self):
171 self.ssh_client = SSHClient()
172
173 def connect(self, username, password, port, host='localhost',
174 timeout=TIMEOUT_CONNECT):
175 self.password = password
176 count = max(1, timeout)
177 for attempt in range(count):
178 try:
179 self.ssh_client.connect(
180 host, username, password, port, timeout)
181 except TypeError:
182 # This can happen when target not yet running so just try again
183 time.sleep(1)
184 except paramiko.ssh_exception.AuthenticationException:
185 raise
186 else:
187 return
188 raise RuntimeError('Could not connect to target.')
189
190 def close(self):
191 self.ssh_client.close()
192
193 def _run(self, cmd, timeout=TIMEOUT_CMD, log_stdout=True):
194 return self.ssh_client.run(cmd, timeout, log_stdout)
195
196 def run(self, cmd, timeout=TIMEOUT_CMD, log_stdout=True):
197 return self._run(cmd, timeout, log_stdout)
198
199 def run_sudo(self, cmd, timeout=TIMEOUT_CMD, log_stdout=True):
200 return self._run(self._get_sudo_command(cmd), timeout, log_stdout)
201
202 def put(self, localpath, remotepath):
203 self.ssh_client.put(localpath, remotepath)
204
205 def get(self, remotepath, localpath):
206 self.ssh_client.get(remotepath, localpath)

Subscribers

People subscribed via source and target branches