Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 1 | # Copyright 2014-2015 ARM Limited |
| 2 | # |
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | # you may not use this file except in compliance with the License. |
| 5 | # You may obtain a copy of the License at |
| 6 | # |
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | # |
| 9 | # Unless required by applicable law or agreed to in writing, software |
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | # See the License for the specific language governing permissions and |
| 13 | # limitations under the License. |
| 14 | # |
| 15 | |
| 16 | |
| 17 | import os |
| 18 | import stat |
| 19 | import logging |
| 20 | import subprocess |
| 21 | import re |
| 22 | import threading |
| 23 | import tempfile |
| 24 | import shutil |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 25 | import socket |
Sergei Trofimov | ebe3a8a | 2016-02-25 10:28:45 +0000 | [diff] [blame] | 26 | import time |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 27 | |
Patrick Bellasi | c1d7cfa | 2015-10-12 15:25:12 +0100 | [diff] [blame] | 28 | import pexpect |
| 29 | from distutils.version import StrictVersion as V |
| 30 | if V(pexpect.__version__) < V('4.0.0'): |
| 31 | import pxssh |
| 32 | else: |
| 33 | from pexpect import pxssh |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 34 | from pexpect import EOF, TIMEOUT, spawn |
| 35 | |
| 36 | from devlib.exception import HostError, TargetError, TimeoutError |
| 37 | from devlib.utils.misc import which, strip_bash_colors, escape_single_quotes, check_output |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 38 | from devlib.utils.types import boolean |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 39 | |
| 40 | |
| 41 | ssh = None |
| 42 | scp = None |
| 43 | sshpass = None |
| 44 | |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 45 | |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 46 | logger = logging.getLogger('ssh') |
| 47 | gem5_logger = logging.getLogger('gem5-connection') |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 48 | |
Gareth Stockwell | 76b059c | 2016-07-29 15:47:50 +0100 | [diff] [blame] | 49 | def ssh_get_shell(host, username, password=None, keyfile=None, port=None, timeout=10, telnet=False, original_prompt=None): |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 50 | _check_env() |
Sergei Trofimov | ebe3a8a | 2016-02-25 10:28:45 +0000 | [diff] [blame] | 51 | start_time = time.time() |
| 52 | while True: |
| 53 | if telnet: |
| 54 | if keyfile: |
| 55 | raise ValueError('keyfile may not be used with a telnet connection.') |
Sergei Trofimov | b3cea0c | 2016-12-08 11:48:41 +0000 | [diff] [blame] | 56 | conn = TelnetPxssh(original_prompt=original_prompt) |
Sergei Trofimov | ebe3a8a | 2016-02-25 10:28:45 +0000 | [diff] [blame] | 57 | else: # ssh |
| 58 | conn = pxssh.pxssh() |
| 59 | |
| 60 | try: |
| 61 | if keyfile: |
| 62 | conn.login(host, username, ssh_key=keyfile, port=port, login_timeout=timeout) |
| 63 | else: |
| 64 | conn.login(host, username, password, port=port, login_timeout=timeout) |
| 65 | break |
| 66 | except EOF: |
| 67 | timeout -= time.time() - start_time |
| 68 | if timeout <= 0: |
| 69 | message = 'Could not connect to {}; is the host name correct?' |
| 70 | raise TargetError(message.format(host)) |
| 71 | time.sleep(5) |
| 72 | |
Patrick Bellasi | 5d288ef | 2015-11-10 11:26:31 +0000 | [diff] [blame] | 73 | conn.setwinsize(500,200) |
| 74 | conn.sendline('') |
| 75 | conn.prompt() |
Patrick Bellasi | 10c80a9 | 2015-11-10 11:27:25 +0000 | [diff] [blame] | 76 | conn.setecho(False) |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 77 | return conn |
| 78 | |
| 79 | |
Sergei Trofimov | b3cea0c | 2016-12-08 11:48:41 +0000 | [diff] [blame] | 80 | class TelnetPxssh(pxssh.pxssh): |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 81 | # pylint: disable=arguments-differ |
| 82 | |
Gareth Stockwell | 76b059c | 2016-07-29 15:47:50 +0100 | [diff] [blame] | 83 | def __init__(self, original_prompt): |
Sergei Trofimov | b3cea0c | 2016-12-08 11:48:41 +0000 | [diff] [blame] | 84 | super(TelnetPxssh, self).__init__() |
Gareth Stockwell | 76b059c | 2016-07-29 15:47:50 +0100 | [diff] [blame] | 85 | self.original_prompt = original_prompt or r'[#$]' |
| 86 | |
| 87 | def login(self, server, username, password='', login_timeout=10, |
Sebastian Goscik | 73f2e28 | 2016-07-29 15:14:41 +0100 | [diff] [blame] | 88 | auto_prompt_reset=True, sync_multiplier=1, port=23): |
Gareth Stockwell | f244936 | 2016-07-29 15:49:41 +0100 | [diff] [blame] | 89 | args = ['telnet'] |
| 90 | if username is not None: |
| 91 | args += ['-l', username] |
| 92 | args += [server, str(port)] |
| 93 | cmd = ' '.join(args) |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 94 | |
| 95 | spawn._spawn(self, cmd) # pylint: disable=protected-access |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 96 | |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 97 | try: |
Gareth Stockwell | f244936 | 2016-07-29 15:49:41 +0100 | [diff] [blame] | 98 | i = self.expect('(?i)(?:password)', timeout=login_timeout) |
| 99 | if i == 0: |
| 100 | self.sendline(password) |
| 101 | i = self.expect([self.original_prompt, 'Login incorrect'], timeout=login_timeout) |
Gareth Stockwell | f244936 | 2016-07-29 15:49:41 +0100 | [diff] [blame] | 102 | if i: |
| 103 | raise pxssh.ExceptionPxssh('could not log in: password was incorrect') |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 104 | except TIMEOUT: |
| 105 | if not password: |
| 106 | # No password promt before TIMEOUT & no password provided |
| 107 | # so assume everything is okay |
| 108 | pass |
| 109 | else: |
| 110 | raise pxssh.ExceptionPxssh('could not log in: did not see a password prompt') |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 111 | |
| 112 | if not self.sync_original_prompt(sync_multiplier): |
| 113 | self.close() |
| 114 | raise pxssh.ExceptionPxssh('could not synchronize with original prompt') |
| 115 | |
| 116 | if auto_prompt_reset: |
| 117 | if not self.set_unique_prompt(): |
| 118 | self.close() |
| 119 | message = 'could not set shell prompt (recieved: {}, expected: {}).' |
| 120 | raise pxssh.ExceptionPxssh(message.format(self.before, self.PROMPT)) |
| 121 | return True |
| 122 | |
| 123 | |
| 124 | def check_keyfile(keyfile): |
| 125 | """ |
| 126 | keyfile must have the right access premissions in order to be useable. If the specified |
| 127 | file doesn't, create a temporary copy and set the right permissions for that. |
| 128 | |
| 129 | Returns either the ``keyfile`` (if the permissions on it are correct) or the path to a |
| 130 | temporary copy with the right permissions. |
| 131 | """ |
| 132 | desired_mask = stat.S_IWUSR | stat.S_IRUSR |
| 133 | actual_mask = os.stat(keyfile).st_mode & 0xFF |
| 134 | if actual_mask != desired_mask: |
| 135 | tmp_file = os.path.join(tempfile.gettempdir(), os.path.basename(keyfile)) |
| 136 | shutil.copy(keyfile, tmp_file) |
| 137 | os.chmod(tmp_file, desired_mask) |
| 138 | return tmp_file |
| 139 | else: # permissions on keyfile are OK |
| 140 | return keyfile |
| 141 | |
| 142 | |
| 143 | class SshConnection(object): |
| 144 | |
| 145 | default_password_prompt = '[sudo] password' |
| 146 | max_cancel_attempts = 5 |
Sergei Trofimov | 89256fd | 2016-05-17 14:00:01 +0100 | [diff] [blame] | 147 | default_timeout=10 |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 148 | |
| 149 | @property |
| 150 | def name(self): |
| 151 | return self.host |
| 152 | |
| 153 | def __init__(self, |
| 154 | host, |
| 155 | username, |
| 156 | password=None, |
| 157 | keyfile=None, |
| 158 | port=None, |
Sergei Trofimov | 89256fd | 2016-05-17 14:00:01 +0100 | [diff] [blame] | 159 | timeout=None, |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 160 | telnet=False, |
| 161 | password_prompt=None, |
Gareth Stockwell | 76b059c | 2016-07-29 15:47:50 +0100 | [diff] [blame] | 162 | original_prompt=None, |
Anthony Barbier | c837a29 | 2017-06-28 15:08:35 +0100 | [diff] [blame^] | 163 | platform=None, |
| 164 | sudo_cmd="sudo -- sh -c '{}'" |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 165 | ): |
| 166 | self.host = host |
| 167 | self.username = username |
| 168 | self.password = password |
| 169 | self.keyfile = check_keyfile(keyfile) if keyfile else keyfile |
| 170 | self.port = port |
| 171 | self.lock = threading.Lock() |
| 172 | self.password_prompt = password_prompt if password_prompt is not None else self.default_password_prompt |
Anthony Barbier | c837a29 | 2017-06-28 15:08:35 +0100 | [diff] [blame^] | 173 | self.sudo_cmd = sudo_cmd |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 174 | logger.debug('Logging in {}@{}'.format(username, host)) |
Sergei Trofimov | 89256fd | 2016-05-17 14:00:01 +0100 | [diff] [blame] | 175 | timeout = timeout if timeout is not None else self.default_timeout |
Sergei Trofimov | b3cea0c | 2016-12-08 11:48:41 +0000 | [diff] [blame] | 176 | self.conn = ssh_get_shell(host, username, password, self.keyfile, port, timeout, False, None) |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 177 | |
| 178 | def push(self, source, dest, timeout=30): |
| 179 | dest = '{}@{}:{}'.format(self.username, self.host, dest) |
| 180 | return self._scp(source, dest, timeout) |
| 181 | |
| 182 | def pull(self, source, dest, timeout=30): |
| 183 | source = '{}@{}:{}'.format(self.username, self.host, source) |
| 184 | return self._scp(source, dest, timeout) |
| 185 | |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 186 | def execute(self, command, timeout=None, check_exit_code=True, |
| 187 | as_root=False, strip_colors=True): #pylint: disable=unused-argument |
Brendan Jackman | 0a20cec | 2017-05-03 10:53:43 +0100 | [diff] [blame] | 188 | if command == '': |
| 189 | # Empty command is valid but the __devlib_ec stuff below will |
| 190 | # produce a syntax error with bash. Treat as a special case. |
| 191 | return '' |
Sergei Trofimov | ebe3a8a | 2016-02-25 10:28:45 +0000 | [diff] [blame] | 192 | try: |
| 193 | with self.lock: |
Brendan Jackman | 0a20cec | 2017-05-03 10:53:43 +0100 | [diff] [blame] | 194 | _command = '({}); __devlib_ec=$?; echo; echo $__devlib_ec'.format(command) |
| 195 | raw_output = self._execute_and_wait_for_prompt( |
| 196 | _command, timeout, as_root, strip_colors) |
| 197 | output, exit_code_text, _ = raw_output.rsplit('\r\n', 2) |
Sergei Trofimov | ebe3a8a | 2016-02-25 10:28:45 +0000 | [diff] [blame] | 198 | if check_exit_code: |
Sergei Trofimov | ebe3a8a | 2016-02-25 10:28:45 +0000 | [diff] [blame] | 199 | try: |
Brendan Jackman | 0a20cec | 2017-05-03 10:53:43 +0100 | [diff] [blame] | 200 | exit_code = int(exit_code_text) |
Sergei Trofimov | ebe3a8a | 2016-02-25 10:28:45 +0000 | [diff] [blame] | 201 | if exit_code: |
| 202 | message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}' |
| 203 | raise TargetError(message.format(exit_code, command, output)) |
| 204 | except (ValueError, IndexError): |
Brendan Jackman | 0a20cec | 2017-05-03 10:53:43 +0100 | [diff] [blame] | 205 | logger.warning( |
| 206 | 'Could not get exit code for "{}",\ngot: "{}"'\ |
| 207 | .format(command, exit_code_text)) |
Sergei Trofimov | ebe3a8a | 2016-02-25 10:28:45 +0000 | [diff] [blame] | 208 | return output |
| 209 | except EOF: |
| 210 | raise TargetError('Connection lost.') |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 211 | |
Michele Di Giorgio | 3bf3017 | 2016-03-01 11:12:10 +0000 | [diff] [blame] | 212 | def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False): |
Sergei Trofimov | ebe3a8a | 2016-02-25 10:28:45 +0000 | [diff] [blame] | 213 | try: |
| 214 | port_string = '-p {}'.format(self.port) if self.port else '' |
| 215 | keyfile_string = '-i {}'.format(self.keyfile) if self.keyfile else '' |
Michele Di Giorgio | 3bf3017 | 2016-03-01 11:12:10 +0000 | [diff] [blame] | 216 | if as_root: |
Anthony Barbier | c837a29 | 2017-06-28 15:08:35 +0100 | [diff] [blame^] | 217 | command = self.sudo_cmd.format(command) |
Sergei Trofimov | ebe3a8a | 2016-02-25 10:28:45 +0000 | [diff] [blame] | 218 | command = '{} {} {} {}@{} {}'.format(ssh, keyfile_string, port_string, self.username, self.host, command) |
| 219 | logger.debug(command) |
| 220 | if self.password: |
| 221 | command = _give_password(self.password, command) |
| 222 | return subprocess.Popen(command, stdout=stdout, stderr=stderr, shell=True) |
| 223 | except EOF: |
| 224 | raise TargetError('Connection lost.') |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 225 | |
| 226 | def close(self): |
| 227 | logger.debug('Logging out {}@{}'.format(self.username, self.host)) |
| 228 | self.conn.logout() |
| 229 | |
| 230 | def cancel_running_command(self): |
| 231 | # simulate impatiently hitting ^C until command prompt appears |
| 232 | logger.debug('Sending ^C') |
| 233 | for _ in xrange(self.max_cancel_attempts): |
| 234 | self.conn.sendline(chr(3)) |
| 235 | if self.conn.prompt(0.1): |
| 236 | return True |
| 237 | return False |
| 238 | |
| 239 | def _execute_and_wait_for_prompt(self, command, timeout=None, as_root=False, strip_colors=True, log=True): |
| 240 | self.conn.prompt(0.1) # clear an existing prompt if there is one. |
Sergei Trofimov | bd27de1 | 2017-04-20 17:00:17 +0100 | [diff] [blame] | 241 | if self.username == 'root': |
| 242 | # As we're already root, there is no need to use sudo. |
| 243 | as_root = False |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 244 | if as_root: |
Anthony Barbier | c837a29 | 2017-06-28 15:08:35 +0100 | [diff] [blame^] | 245 | command = self.sudo_cmd.format(escape_single_quotes(command)) |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 246 | if log: |
| 247 | logger.debug(command) |
| 248 | self.conn.sendline(command) |
| 249 | if self.password: |
| 250 | index = self.conn.expect_exact([self.password_prompt, TIMEOUT], timeout=0.5) |
| 251 | if index == 0: |
| 252 | self.conn.sendline(self.password) |
| 253 | else: # not as_root |
| 254 | if log: |
| 255 | logger.debug(command) |
| 256 | self.conn.sendline(command) |
| 257 | timed_out = self._wait_for_prompt(timeout) |
| 258 | # the regex removes line breaks potential introduced when writing |
| 259 | # command to shell. |
| 260 | output = process_backspaces(self.conn.before) |
| 261 | output = re.sub(r'\r([^\n])', r'\1', output) |
| 262 | if '\r\n' in output: # strip the echoed command |
| 263 | output = output.split('\r\n', 1)[1] |
| 264 | if timed_out: |
| 265 | self.cancel_running_command() |
| 266 | raise TimeoutError(command, output) |
| 267 | if strip_colors: |
| 268 | output = strip_bash_colors(output) |
| 269 | return output |
| 270 | |
| 271 | def _wait_for_prompt(self, timeout=None): |
| 272 | if timeout: |
| 273 | return not self.conn.prompt(timeout) |
| 274 | else: # cannot timeout; wait forever |
| 275 | while not self.conn.prompt(1): |
| 276 | pass |
| 277 | return False |
| 278 | |
| 279 | def _scp(self, source, dest, timeout=30): |
| 280 | # NOTE: the version of scp in Ubuntu 12.04 occasionally (and bizarrely) |
| 281 | # fails to connect to a device if port is explicitly specified using -P |
| 282 | # option, even if it is the default port, 22. To minimize this problem, |
| 283 | # only specify -P for scp if the port is *not* the default. |
| 284 | port_string = '-P {}'.format(self.port) if (self.port and self.port != 22) else '' |
| 285 | keyfile_string = '-i {}'.format(self.keyfile) if self.keyfile else '' |
| 286 | command = '{} -r {} {} {} {}'.format(scp, keyfile_string, port_string, source, dest) |
| 287 | pass_string = '' |
| 288 | logger.debug(command) |
| 289 | if self.password: |
| 290 | command = _give_password(self.password, command) |
| 291 | try: |
| 292 | check_output(command, timeout=timeout, shell=True) |
| 293 | except subprocess.CalledProcessError as e: |
| 294 | raise subprocess.CalledProcessError(e.returncode, e.cmd.replace(pass_string, ''), e.output) |
| 295 | except TimeoutError as e: |
| 296 | raise TimeoutError(e.command.replace(pass_string, ''), e.output) |
| 297 | |
| 298 | |
Sergei Trofimov | b3cea0c | 2016-12-08 11:48:41 +0000 | [diff] [blame] | 299 | class TelnetConnection(SshConnection): |
| 300 | |
| 301 | def __init__(self, |
| 302 | host, |
| 303 | username, |
| 304 | password=None, |
| 305 | port=None, |
| 306 | timeout=None, |
| 307 | password_prompt=None, |
| 308 | original_prompt=None, |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 309 | platform=None): |
Sergei Trofimov | b3cea0c | 2016-12-08 11:48:41 +0000 | [diff] [blame] | 310 | self.host = host |
| 311 | self.username = username |
| 312 | self.password = password |
| 313 | self.port = port |
| 314 | self.keyfile = None |
| 315 | self.lock = threading.Lock() |
| 316 | self.password_prompt = password_prompt if password_prompt is not None else self.default_password_prompt |
| 317 | logger.debug('Logging in {}@{}'.format(username, host)) |
| 318 | timeout = timeout if timeout is not None else self.default_timeout |
| 319 | self.conn = ssh_get_shell(host, username, password, None, port, timeout, True, original_prompt) |
| 320 | |
| 321 | |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 322 | class Gem5Connection(TelnetConnection): |
| 323 | |
| 324 | def __init__(self, |
| 325 | platform, |
| 326 | host=None, |
| 327 | username=None, |
| 328 | password=None, |
| 329 | port=None, |
| 330 | timeout=None, |
| 331 | password_prompt=None, |
| 332 | original_prompt=None, |
| 333 | ): |
| 334 | if host is not None: |
| 335 | host_system = socket.gethostname() |
| 336 | if host_system != host: |
| 337 | raise TargetError("Gem5Connection can only connect to gem5 " |
| 338 | "simulations on your current host, which " |
| 339 | "differs from the one given {}!" |
| 340 | .format(host_system, host)) |
| 341 | if username is not None and username != 'root': |
| 342 | raise ValueError('User should be root in gem5!') |
| 343 | if password is not None and password != '': |
| 344 | raise ValueError('No password needed in gem5!') |
| 345 | self.username = 'root' |
| 346 | self.is_rooted = True |
| 347 | self.password = None |
| 348 | self.port = None |
| 349 | # Long timeouts to account for gem5 being slow |
| 350 | # Can be overriden if the given timeout is longer |
| 351 | self.default_timeout = 3600 |
| 352 | if timeout is not None: |
| 353 | if timeout > self.default_timeout: |
| 354 | logger.info('Overwriting the default timeout of gem5 ({})' |
| 355 | ' to {}'.format(self.default_timeout, timeout)) |
| 356 | self.default_timeout = timeout |
| 357 | else: |
| 358 | logger.info('Ignoring the given timeout --> gem5 needs longer timeouts') |
| 359 | self.ready_timeout = self.default_timeout * 3 |
| 360 | # Counterpart in gem5_interact_dir |
| 361 | self.gem5_input_dir = '/mnt/host/' |
| 362 | # Location of m5 binary in the gem5 simulated system |
| 363 | self.m5_path = None |
| 364 | # Actual telnet connection to gem5 simulation |
| 365 | self.conn = None |
| 366 | # Flag to indicate the gem5 device is ready to interact with the |
| 367 | # outer world |
| 368 | self.ready = False |
| 369 | # Lock file to prevent multiple connections to same gem5 simulation |
| 370 | # (gem5 does not allow this) |
| 371 | self.lock_directory = '/tmp/' |
| 372 | self.lock_file_name = None # Will be set once connected to gem5 |
| 373 | |
| 374 | # These parameters will be set by either the method to connect to the |
| 375 | # gem5 platform or directly to the gem5 simulation |
| 376 | # Intermediate directory to push things to gem5 using VirtIO |
| 377 | self.gem5_interact_dir = None |
| 378 | # Directory to store output from gem5 on the host |
| 379 | self.gem5_out_dir = None |
| 380 | # Actual gem5 simulation |
| 381 | self.gem5simulation = None |
| 382 | |
| 383 | # Connect to gem5 |
| 384 | if platform: |
| 385 | self._connect_gem5_platform(platform) |
| 386 | |
| 387 | # Wait for boot |
| 388 | self._wait_for_boot() |
| 389 | |
| 390 | # Mount the virtIO to transfer files in/out gem5 system |
| 391 | self._mount_virtio() |
| 392 | |
| 393 | def set_hostinteractdir(self, indir): |
| 394 | logger.info('Setting hostinteractdir from {} to {}' |
| 395 | .format(self.gem5_input_dir, indir)) |
| 396 | self.gem5_input_dir = indir |
| 397 | |
| 398 | def push(self, source, dest, timeout=None): |
| 399 | """ |
| 400 | Push a file to the gem5 device using VirtIO |
| 401 | |
| 402 | The file to push to the device is copied to the temporary directory on |
| 403 | the host, before being copied within the simulation to the destination. |
| 404 | Checks, in the form of 'ls' with error code checking, are performed to |
| 405 | ensure that the file is copied to the destination. |
| 406 | """ |
| 407 | # First check if the connection is set up to interact with gem5 |
| 408 | self._check_ready() |
| 409 | |
| 410 | filename = os.path.basename(source) |
| 411 | logger.debug("Pushing {} to device.".format(source)) |
| 412 | logger.debug("gem5interactdir: {}".format(self.gem5_interact_dir)) |
| 413 | logger.debug("dest: {}".format(dest)) |
| 414 | logger.debug("filename: {}".format(filename)) |
| 415 | |
| 416 | # We need to copy the file to copy to the temporary directory |
| 417 | self._move_to_temp_dir(source) |
| 418 | |
| 419 | # Dest in gem5 world is a file rather than directory |
| 420 | if os.path.basename(dest) != filename: |
| 421 | dest = os.path.join(dest, filename) |
| 422 | # Back to the gem5 world |
| 423 | self._gem5_shell("ls -al {}{}".format(self.gem5_input_dir, filename)) |
| 424 | self._gem5_shell("cat '{}''{}' > '{}'".format(self.gem5_input_dir, |
| 425 | filename, |
| 426 | dest)) |
| 427 | self._gem5_shell("sync") |
| 428 | self._gem5_shell("ls -al {}".format(dest)) |
| 429 | self._gem5_shell("ls -al {}".format(self.gem5_input_dir)) |
| 430 | logger.debug("Push complete.") |
| 431 | |
| 432 | def pull(self, source, dest, timeout=0): #pylint: disable=unused-argument |
| 433 | """ |
| 434 | Pull a file from the gem5 device using m5 writefile |
| 435 | |
| 436 | The file is copied to the local directory within the guest as the m5 |
| 437 | writefile command assumes that the file is local. The file is then |
| 438 | written out to the host system using writefile, prior to being moved to |
| 439 | the destination on the host. |
| 440 | """ |
| 441 | # First check if the connection is set up to interact with gem5 |
| 442 | self._check_ready() |
| 443 | |
Ionela Voinescu | cac70cb | 2017-05-23 16:45:29 +0100 | [diff] [blame] | 444 | result = self._gem5_shell("ls {}".format(source)) |
| 445 | files = result.split() |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 446 | |
Ionela Voinescu | cac70cb | 2017-05-23 16:45:29 +0100 | [diff] [blame] | 447 | for filename in files: |
| 448 | dest_file = os.path.basename(filename) |
| 449 | logger.debug("pull_file {} {}".format(filename, dest_file)) |
| 450 | # writefile needs the file to be copied to be in the current |
| 451 | # working directory so if needed, copy to the working directory |
| 452 | # We don't check the exit code here because it is non-zero if the |
| 453 | # source and destination are the same. The ls below will cause an |
| 454 | # error if the file was not where we expected it to be. |
| 455 | if os.path.isabs(source): |
| 456 | if os.path.dirname(source) != self.execute('pwd', |
| 457 | check_exit_code=False): |
| 458 | self._gem5_shell("cat '{}' > '{}'".format(filename, |
| 459 | dest_file)) |
| 460 | self._gem5_shell("sync") |
| 461 | self._gem5_shell("ls -la {}".format(dest_file)) |
| 462 | logger.debug('Finished the copy in the simulator') |
| 463 | self._gem5_util("writefile {}".format(dest_file)) |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 464 | |
Ionela Voinescu | cac70cb | 2017-05-23 16:45:29 +0100 | [diff] [blame] | 465 | if 'cpu' not in filename: |
| 466 | while not os.path.exists(os.path.join(self.gem5_out_dir, |
| 467 | dest_file)): |
| 468 | time.sleep(1) |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 469 | |
Ionela Voinescu | cac70cb | 2017-05-23 16:45:29 +0100 | [diff] [blame] | 470 | # Perform the local move |
| 471 | if os.path.exists(os.path.join(dest, dest_file)): |
| 472 | logger.warning( |
| 473 | 'Destination file {} already exists!'\ |
| 474 | .format(dest_file)) |
| 475 | else: |
| 476 | shutil.move(os.path.join(self.gem5_out_dir, dest_file), dest) |
| 477 | logger.debug("Pull complete.") |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 478 | |
| 479 | def execute(self, command, timeout=1000, check_exit_code=True, |
| 480 | as_root=False, strip_colors=True): |
| 481 | """ |
| 482 | Execute a command on the gem5 platform |
| 483 | """ |
| 484 | # First check if the connection is set up to interact with gem5 |
| 485 | self._check_ready() |
| 486 | |
Sascha Bischoff | f5906cb | 2017-05-11 10:28:05 +0100 | [diff] [blame] | 487 | output = self._gem5_shell(command, |
| 488 | check_exit_code=check_exit_code, |
| 489 | as_root=as_root) |
Anouk Van Laer | 29a7940 | 2017-01-31 12:48:58 +0000 | [diff] [blame] | 490 | if strip_colors: |
| 491 | output = strip_bash_colors(output) |
| 492 | return output |
| 493 | |
| 494 | def background(self, command, stdout=subprocess.PIPE, |
| 495 | stderr=subprocess.PIPE, as_root=False): |
| 496 | # First check if the connection is set up to interact with gem5 |
| 497 | self._check_ready() |
| 498 | |
| 499 | # Create the logfile for stderr/stdout redirection |
| 500 | command_name = command.split(' ')[0].split('/')[-1] |
| 501 | redirection_file = 'BACKGROUND_{}.log'.format(command_name) |
| 502 | trial = 0 |
| 503 | while os.path.isfile(redirection_file): |
| 504 | # Log file already exists so add to name |
| 505 | redirection_file = 'BACKGROUND_{}{}.log'.format(command_name, trial) |
| 506 | trial += 1 |
| 507 | |
| 508 | # Create the command to pass on to gem5 shell |
| 509 | complete_command = '{} >> {} 2>&1 &'.format(command, redirection_file) |
| 510 | output = self._gem5_shell(complete_command, as_root=as_root) |
| 511 | output = strip_bash_colors(output) |
| 512 | gem5_logger.info('STDERR/STDOUT of background command will be ' |
| 513 | 'redirected to {}. Use target.pull() to ' |
| 514 | 'get this file'.format(redirection_file)) |
| 515 | return output |
| 516 | |
| 517 | def close(self): |
| 518 | """ |
| 519 | Close and disconnect from the gem5 simulation. Additionally, we remove |
| 520 | the temporary directory used to pass files into the simulation. |
| 521 | """ |
| 522 | gem5_logger.info("Gracefully terminating the gem5 simulation.") |
| 523 | try: |
| 524 | self._gem5_util("exit") |
| 525 | self.gem5simulation.wait() |
| 526 | except EOF: |
| 527 | pass |
| 528 | gem5_logger.info("Removing the temporary directory") |
| 529 | try: |
| 530 | shutil.rmtree(self.gem5_interact_dir) |
| 531 | except OSError: |
| 532 | gem5_logger.warn("Failed to remove the temporary directory!") |
| 533 | |
| 534 | # Delete the lock file |
| 535 | os.remove(self.lock_file_name) |
| 536 | |
| 537 | # Functions only to be called by the Gem5 connection itself |
| 538 | def _connect_gem5_platform(self, platform): |
| 539 | port = platform.gem5_port |
| 540 | gem5_simulation = platform.gem5 |
| 541 | gem5_interact_dir = platform.gem5_interact_dir |
| 542 | gem5_out_dir = platform.gem5_out_dir |
| 543 | |
| 544 | self.connect_gem5(port, gem5_simulation, gem5_interact_dir, gem5_out_dir) |
| 545 | |
| 546 | # This function connects to the gem5 simulation |
| 547 | def connect_gem5(self, port, gem5_simulation, gem5_interact_dir, |
| 548 | gem5_out_dir): |
| 549 | """ |
| 550 | Connect to the telnet port of the gem5 simulation. |
| 551 | |
| 552 | We connect, and wait for the prompt to be found. We do not use a timeout |
| 553 | for this, and wait for the prompt in a while loop as the gem5 simulation |
| 554 | can take many hours to reach a prompt when booting the system. We also |
| 555 | inject some newlines periodically to try and force gem5 to show a |
| 556 | prompt. Once the prompt has been found, we replace it with a unique |
| 557 | prompt to ensure that we are able to match it properly. We also disable |
| 558 | the echo as this simplifies parsing the output when executing commands |
| 559 | on the device. |
| 560 | """ |
| 561 | host = socket.gethostname() |
| 562 | gem5_logger.info("Connecting to the gem5 simulation on port {}".format(port)) |
| 563 | |
| 564 | # Check if there is no on-going connection yet |
| 565 | lock_file_name = '{}{}_{}.LOCK'.format(self.lock_directory, host, port) |
| 566 | if os.path.isfile(lock_file_name): |
| 567 | # There is already a connection to this gem5 simulation |
| 568 | raise TargetError('There is already a connection to the gem5 ' |
| 569 | 'simulation using port {} on {}!' |
| 570 | .format(port, host)) |
| 571 | |
| 572 | # Connect to the gem5 telnet port. Use a short timeout here. |
| 573 | attempts = 0 |
| 574 | while attempts < 10: |
| 575 | attempts += 1 |
| 576 | try: |
| 577 | self.conn = TelnetPxssh(original_prompt=None) |
| 578 | self.conn.login(host, self.username, port=port, |
| 579 | login_timeout=10, auto_prompt_reset=False) |
| 580 | break |
| 581 | except pxssh.ExceptionPxssh: |
| 582 | pass |
| 583 | else: |
| 584 | gem5_simulation.kill() |
| 585 | raise TargetError("Failed to connect to the gem5 telnet session.") |
| 586 | |
| 587 | gem5_logger.info("Connected! Waiting for prompt...") |
| 588 | |
| 589 | # Create the lock file |
| 590 | self.lock_file_name = lock_file_name |
| 591 | open(self.lock_file_name, 'w').close() # Similar to touch |
| 592 | gem5_logger.info("Created lock file {} to prevent reconnecting to " |
| 593 | "same simulation".format(self.lock_file_name)) |
| 594 | |
| 595 | # We need to find the prompt. It might be different if we are resuming |
| 596 | # from a checkpoint. Therefore, we test multiple options here. |
| 597 | prompt_found = False |
| 598 | while not prompt_found: |
| 599 | try: |
| 600 | self._login_to_device() |
| 601 | except TIMEOUT: |
| 602 | pass |
| 603 | try: |
| 604 | # Try and force a prompt to be shown |
| 605 | self.conn.send('\n') |
| 606 | self.conn.expect([r'# ', self.conn.UNIQUE_PROMPT, r'\[PEXPECT\][\\\$\#]+ '], timeout=60) |
| 607 | prompt_found = True |
| 608 | except TIMEOUT: |
| 609 | pass |
| 610 | |
| 611 | gem5_logger.info("Successfully logged in") |
| 612 | gem5_logger.info("Setting unique prompt...") |
| 613 | |
| 614 | self.conn.set_unique_prompt() |
| 615 | self.conn.prompt() |
| 616 | gem5_logger.info("Prompt found and replaced with a unique string") |
| 617 | |
| 618 | # We check that the prompt is what we think it should be. If not, we |
| 619 | # need to update the regex we use to match. |
| 620 | self._find_prompt() |
| 621 | |
| 622 | self.conn.setecho(False) |
| 623 | self._sync_gem5_shell() |
| 624 | |
| 625 | # Fully connected to gem5 simulation |
| 626 | self.gem5_interact_dir = gem5_interact_dir |
| 627 | self.gem5_out_dir = gem5_out_dir |
| 628 | self.gem5simulation = gem5_simulation |
| 629 | |
| 630 | # Ready for interaction now |
| 631 | self.ready = True |
| 632 | |
| 633 | def _login_to_device(self): |
| 634 | """ |
| 635 | Login to device, will be overwritten if there is an actual login |
| 636 | """ |
| 637 | pass |
| 638 | |
| 639 | def _find_prompt(self): |
| 640 | prompt = r'\[PEXPECT\][\\\$\#]+ ' |
| 641 | synced = False |
| 642 | while not synced: |
| 643 | self.conn.send('\n') |
| 644 | i = self.conn.expect([prompt, self.conn.UNIQUE_PROMPT, r'[\$\#] '], timeout=self.default_timeout) |
| 645 | if i == 0: |
| 646 | synced = True |
| 647 | elif i == 1: |
| 648 | prompt = self.conn.UNIQUE_PROMPT |
| 649 | synced = True |
| 650 | else: |
| 651 | prompt = re.sub(r'\$', r'\\\$', self.conn.before.strip() + self.conn.after.strip()) |
| 652 | prompt = re.sub(r'\#', r'\\\#', prompt) |
| 653 | prompt = re.sub(r'\[', r'\[', prompt) |
| 654 | prompt = re.sub(r'\]', r'\]', prompt) |
| 655 | |
| 656 | self.conn.PROMPT = prompt |
| 657 | |
| 658 | def _sync_gem5_shell(self): |
| 659 | """ |
| 660 | Synchronise with the gem5 shell. |
| 661 | |
| 662 | Write some unique text to the gem5 device to allow us to synchronise |
| 663 | with the shell output. We actually get two prompts so we need to match |
| 664 | both of these. |
| 665 | """ |
| 666 | gem5_logger.debug("Sending Sync") |
| 667 | self.conn.send("echo \*\*sync\*\*\n") |
| 668 | self.conn.expect(r"\*\*sync\*\*", timeout=self.default_timeout) |
| 669 | self.conn.expect([self.conn.UNIQUE_PROMPT, self.conn.PROMPT], timeout=self.default_timeout) |
| 670 | self.conn.expect([self.conn.UNIQUE_PROMPT, self.conn.PROMPT], timeout=self.default_timeout) |
| 671 | |
| 672 | def _gem5_util(self, command): |
| 673 | """ Execute a gem5 utility command using the m5 binary on the device """ |
| 674 | if self.m5_path is None: |
| 675 | raise TargetError('Path to m5 binary on simulated system is not set!') |
| 676 | self._gem5_shell('{} {}'.format(self.m5_path, command)) |
| 677 | |
| 678 | def _gem5_shell(self, command, as_root=False, timeout=None, check_exit_code=True, sync=True): # pylint: disable=R0912 |
| 679 | """ |
| 680 | Execute a command in the gem5 shell |
| 681 | |
| 682 | This wraps the telnet connection to gem5 and processes the raw output. |
| 683 | |
| 684 | This method waits for the shell to return, and then will try and |
| 685 | separate the output from the command from the command itself. If this |
| 686 | fails, warn, but continue with the potentially wrong output. |
| 687 | |
| 688 | The exit code is also checked by default, and non-zero exit codes will |
| 689 | raise a TargetError. |
| 690 | """ |
| 691 | if sync: |
| 692 | self._sync_gem5_shell() |
| 693 | |
| 694 | gem5_logger.debug("gem5_shell command: {}".format(command)) |
| 695 | |
| 696 | # Send the actual command |
| 697 | self.conn.send("{}\n".format(command)) |
| 698 | |
| 699 | # Wait for the response. We just sit here and wait for the prompt to |
| 700 | # appear, as gem5 might take a long time to provide the output. This |
| 701 | # avoids timeout issues. |
| 702 | command_index = -1 |
| 703 | while command_index == -1: |
| 704 | if self.conn.prompt(): |
| 705 | output = re.sub(r' \r([^\n])', r'\1', self.conn.before) |
| 706 | output = re.sub(r'[\b]', r'', output) |
| 707 | # Deal with line wrapping |
| 708 | output = re.sub(r'[\r].+?<', r'', output) |
| 709 | command_index = output.find(command) |
| 710 | |
| 711 | # If we have -1, then we cannot match the command, but the |
| 712 | # prompt has returned. Hence, we have a bit of an issue. We |
| 713 | # warn, and return the whole output. |
| 714 | if command_index == -1: |
| 715 | gem5_logger.warn("gem5_shell: Unable to match command in " |
| 716 | "command output. Expect parsing errors!") |
| 717 | command_index = 0 |
| 718 | |
| 719 | output = output[command_index + len(command):].strip() |
| 720 | |
| 721 | # It is possible that gem5 will echo the command. Therefore, we need to |
| 722 | # remove that too! |
| 723 | command_index = output.find(command) |
| 724 | if command_index != -1: |
| 725 | output = output[command_index + len(command):].strip() |
| 726 | |
| 727 | gem5_logger.debug("gem5_shell output: {}".format(output)) |
| 728 | |
| 729 | # We get a second prompt. Hence, we need to eat one to make sure that we |
| 730 | # stay in sync. If we do not do this, we risk getting out of sync for |
| 731 | # slower simulations. |
| 732 | self.conn.expect([self.conn.UNIQUE_PROMPT, self.conn.PROMPT], timeout=self.default_timeout) |
| 733 | |
| 734 | if check_exit_code: |
| 735 | exit_code_text = self._gem5_shell('echo $?', as_root=as_root, |
| 736 | timeout=timeout, check_exit_code=False, |
| 737 | sync=False) |
| 738 | try: |
| 739 | exit_code = int(exit_code_text.split()[0]) |
| 740 | if exit_code: |
| 741 | message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}' |
| 742 | raise TargetError(message.format(exit_code, command, output)) |
| 743 | except (ValueError, IndexError): |
| 744 | gem5_logger.warning('Could not get exit code for "{}",\ngot: "{}"'.format(command, exit_code_text)) |
| 745 | |
| 746 | return output |
| 747 | |
| 748 | def _mount_virtio(self): |
| 749 | """ |
| 750 | Mount the VirtIO device in the simulated system. |
| 751 | """ |
| 752 | gem5_logger.info("Mounting VirtIO device in simulated system") |
| 753 | |
| 754 | self._gem5_shell('su -c "mkdir -p {}" root'.format(self.gem5_input_dir)) |
| 755 | mount_command = "mount -t 9p -o trans=virtio,version=9p2000.L,aname={} gem5 {}".format(self.gem5_interact_dir, self.gem5_input_dir) |
| 756 | self._gem5_shell(mount_command) |
| 757 | |
| 758 | def _move_to_temp_dir(self, source): |
| 759 | """ |
| 760 | Move a file to the temporary directory on the host for copying to the |
| 761 | gem5 device |
| 762 | """ |
| 763 | command = "cp {} {}".format(source, self.gem5_interact_dir) |
| 764 | gem5_logger.debug("Local copy command: {}".format(command)) |
| 765 | subprocess.call(command.split()) |
| 766 | subprocess.call("sync".split()) |
| 767 | |
| 768 | def _check_ready(self): |
| 769 | """ |
| 770 | Check if the gem5 platform is ready |
| 771 | """ |
| 772 | if not self.ready: |
| 773 | raise TargetError('Gem5 is not ready to interact yet') |
| 774 | |
| 775 | def _wait_for_boot(self): |
| 776 | pass |
| 777 | |
| 778 | def _probe_file(self, filepath): |
| 779 | """ |
| 780 | Internal method to check if the target has a certain file |
| 781 | """ |
| 782 | command = 'if [ -e \'{}\' ]; then echo 1; else echo 0; fi' |
| 783 | output = self.execute(command.format(filepath), as_root=self.is_rooted) |
| 784 | return boolean(output.strip()) |
| 785 | |
| 786 | |
| 787 | class LinuxGem5Connection(Gem5Connection): |
| 788 | |
| 789 | def _login_to_device(self): |
| 790 | gem5_logger.info("Trying to log in to gem5 device") |
| 791 | login_prompt = ['login:', 'AEL login:', 'username:', 'aarch64-gem5 login:'] |
| 792 | login_password_prompt = ['password:'] |
| 793 | # Wait for the login prompt |
| 794 | prompt = login_prompt + [self.conn.UNIQUE_PROMPT] |
| 795 | i = self.conn.expect(prompt, timeout=10) |
| 796 | # Check if we are already at a prompt, or if we need to log in. |
| 797 | if i < len(prompt) - 1: |
| 798 | self.conn.sendline("{}".format(self.username)) |
| 799 | password_prompt = login_password_prompt + [r'# ', self.conn.UNIQUE_PROMPT] |
| 800 | j = self.conn.expect(password_prompt, timeout=self.default_timeout) |
| 801 | if j < len(password_prompt) - 2: |
| 802 | self.conn.sendline("{}".format(self.password)) |
| 803 | self.conn.expect([r'# ', self.conn.UNIQUE_PROMPT], timeout=self.default_timeout) |
| 804 | |
| 805 | |
| 806 | |
| 807 | class AndroidGem5Connection(Gem5Connection): |
| 808 | |
| 809 | def _wait_for_boot(self): |
| 810 | """ |
| 811 | Wait for the system to boot |
| 812 | |
| 813 | We monitor the sys.boot_completed and service.bootanim.exit system |
| 814 | properties to determine when the system has finished booting. In the |
| 815 | event that we cannot coerce the result of service.bootanim.exit to an |
| 816 | integer, we assume that the boot animation was disabled and do not wait |
| 817 | for it to finish. |
| 818 | |
| 819 | """ |
| 820 | gem5_logger.info("Waiting for Android to boot...") |
| 821 | while True: |
| 822 | booted = False |
| 823 | anim_finished = True # Assume boot animation was disabled on except |
| 824 | try: |
| 825 | booted = (int('0' + self._gem5_shell('getprop sys.boot_completed', check_exit_code=False).strip()) == 1) |
| 826 | anim_finished = (int(self._gem5_shell('getprop service.bootanim.exit', check_exit_code=False).strip()) == 1) |
| 827 | except ValueError: |
| 828 | pass |
| 829 | if booted and anim_finished: |
| 830 | break |
| 831 | time.sleep(60) |
| 832 | |
| 833 | gem5_logger.info("Android booted") |
| 834 | |
Sergei Trofimov | 4e6afe9 | 2015-10-09 09:30:04 +0100 | [diff] [blame] | 835 | def _give_password(password, command): |
| 836 | if not sshpass: |
| 837 | raise HostError('Must have sshpass installed on the host in order to use password-based auth.') |
| 838 | pass_string = "sshpass -p '{}' ".format(password) |
| 839 | return pass_string + command |
| 840 | |
| 841 | |
| 842 | def _check_env(): |
| 843 | global ssh, scp, sshpass # pylint: disable=global-statement |
| 844 | if not ssh: |
| 845 | ssh = which('ssh') |
| 846 | scp = which('scp') |
| 847 | sshpass = which('sshpass') |
| 848 | if not (ssh and scp): |
| 849 | raise HostError('OpenSSH must be installed on the host.') |
| 850 | |
| 851 | |
| 852 | def process_backspaces(text): |
| 853 | chars = [] |
| 854 | for c in text: |
| 855 | if c == chr(8) and chars: # backspace |
| 856 | chars.pop() |
| 857 | else: |
| 858 | chars.append(c) |
| 859 | return ''.join(chars) |