Factor common SSH logic into a new flavor.

Change-Id: I3e9acc3f8e2690169b3c76f9aec1ac22f6dfc93a
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/205620
Commit-Queue: Ben Wagner aka dogben <benjaminwagner@google.com>
Auto-Submit: Ben Wagner aka dogben <benjaminwagner@google.com>
Reviewed-by: Eric Boren <borenet@google.com>
Reviewed-by: Kevin Lubick <kjlubick@google.com>
diff --git a/infra/bots/recipe_modules/flavor/chromebook.py b/infra/bots/recipe_modules/flavor/chromebook.py
index 2dafd09..53bffbb 100644
--- a/infra/bots/recipe_modules/flavor/chromebook.py
+++ b/infra/bots/recipe_modules/flavor/chromebook.py
@@ -6,18 +6,16 @@
 from recipe_engine import recipe_api
 
 import default
-import json  # TODO(borenet): No! Remove this.
+import ssh
 
 
 """Chromebook flavor, used for running code on Chromebooks."""
 
 
-class ChromebookFlavor(default.DefaultFlavor):
+class ChromebookFlavor(ssh.SSHFlavor):
 
   def __init__(self, m):
     super(ChromebookFlavor, self).__init__(m)
-    self._user_ip = ''
-
     self.chromeos_homedir = '/home/chronos/user/'
     self.device_dirs = default.DeviceDirs(
       bin_dir       = self.chromeos_homedir + 'bin',
@@ -30,70 +28,12 @@
       svg_dir       = self.chromeos_homedir + 'svgs',
       tmp_dir       = self.chromeos_homedir)
 
-  @property
-  def user_ip(self):
-    if not self._user_ip:
-      ssh_info = self.m.run(self.m.python.inline, 'read chromeos ip',
-                            program="""
-      import os
-      SSH_MACHINE_FILE = os.path.expanduser('~/ssh_machine.json')
-      with open(SSH_MACHINE_FILE, 'r') as f:
-        print f.read()
-      """,
-      stdout=self.m.raw_io.output(),
-      infra_step=True).stdout
-
-      self._user_ip = json.loads(ssh_info).get(u'user_ip', 'ERROR')
-    return self._user_ip
-
-  def _ssh(self, title, *cmd, **kwargs):
-    if 'infra_step' not in kwargs:
-      kwargs['infra_step'] = True
-
-    ssh_cmd = ['ssh', '-oConnectTimeout=15', '-oBatchMode=yes',
-               '-t', '-t', self.user_ip] + list(cmd)
-
-    return self._run(title, ssh_cmd, **kwargs)
-
   def install(self):
-    self._ssh('mkdir %s' % self.device_dirs.resource_dir, 'mkdir', '-p',
-              self.device_dirs.resource_dir)
+    super(ChromebookFlavor, self).install()
 
     # Ensure the home dir is marked executable
-    self._ssh('remount %s as exec' % self.chromeos_homedir,
-              'sudo', 'mount', '-i', '-o', 'remount,exec', '/home/chronos')
-
-    self.create_clean_device_dir(self.device_dirs.bin_dir)
-
-  def create_clean_device_dir(self, path):
-    # use -f to silently return if path doesn't exist
-    self._ssh('rm %s' % path, 'rm', '-rf', path)
-    self._ssh('mkdir %s' % path, 'mkdir', '-p', path)
-
-  def read_file_on_device(self, path, **kwargs):
-    rv = self._ssh('read %s' % path,
-                   'cat', path, stdout=self.m.raw_io.output(),
-                   **kwargs)
-    return rv.stdout.rstrip() if rv and rv.stdout else None
-
-  def remove_file_on_device(self, path):
-    # use -f to silently return if path doesn't exist
-    self._ssh('rm %s' % path, 'rm', '-f', path)
-
-  def _prefix_device_path(self, device_path):
-    return '%s:%s' % (self.user_ip, device_path)
-
-  def copy_file_to_device(self, host_path, device_path):
-    device_path = self._prefix_device_path(device_path)
-    # Recipe
-    self.m.python.inline(str('scp %s %s' % (host_path, device_path)),
-    """
-    import subprocess
-    import sys
-    host = sys.argv[1]
-    device   = sys.argv[2]
-    print subprocess.check_output(['scp', host, device])
-    """, args=[host_path, device_path], infra_step=True)
+    self.ssh('remount %s as exec' % self.chromeos_homedir,
+             'sudo', 'mount', '-i', '-o', 'remount,exec', '/home/chronos')
 
   def _copy_dir(self, src, dest):
     # We can't use rsync to communicate with the chromebooks because the
@@ -108,25 +48,7 @@
     """, args=[src, dest], infra_step=True)
 
   def copy_directory_contents_to_device(self, host_path, device_path):
-    self._copy_dir(host_path, self._prefix_device_path(device_path))
+    self._copy_dir(host_path, self.scp_device_path(device_path))
 
   def copy_directory_contents_to_host(self, device_path, host_path):
-    self._copy_dir(self._prefix_device_path(device_path), host_path)
-
-  def step(self, name, cmd, **kwargs):
-    # Push and run either dm or nanobench
-
-    name = cmd[0]
-
-    if name == 'dm':
-      self.create_clean_host_dir(self.host_dirs.dm_dir)
-    if name == 'nanobench':
-      self.create_clean_host_dir(self.host_dirs.perf_data_dir)
-
-    app = self.host_dirs.bin_dir.join(cmd[0])
-
-    cmd[0] = '%s/%s' % (self.device_dirs.bin_dir, cmd[0])
-    self.copy_file_to_device(app, cmd[0])
-
-    self._ssh('chmod %s' % name, 'chmod', '+x', cmd[0])
-    self._ssh(str(name), *cmd)
+    self._copy_dir(self.scp_device_path(device_path), host_path)
diff --git a/infra/bots/recipe_modules/flavor/examples/full.expected/Perf-ChromeOS-Clang-SamsungChromebookPlus-GPU-MaliT860-arm-Release-All.json b/infra/bots/recipe_modules/flavor/examples/full.expected/Perf-ChromeOS-Clang-SamsungChromebookPlus-GPU-MaliT860-arm-Release-All.json
index 2e52e5e..198a1bd 100644
--- a/infra/bots/recipe_modules/flavor/examples/full.expected/Perf-ChromeOS-Clang-SamsungChromebookPlus-GPU-MaliT860-arm-Release-All.json
+++ b/infra/bots/recipe_modules/flavor/examples/full.expected/Perf-ChromeOS-Clang-SamsungChromebookPlus-GPU-MaliT860-arm-Release-All.json
@@ -3,43 +3,28 @@
     "cmd": [
       "python",
       "-u",
-      "\nimport os\nSSH_MACHINE_FILE = os.path.expanduser('~/ssh_machine.json')\nwith open(SSH_MACHINE_FILE, 'r') as f:\n  print f.read()\n"
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[HOME]/ssh_machine.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "read ssh_machine.json"
+  },
+  {
+    "cmd": [
+      "scp",
+      "file.txt",
+      "foo@127.0.0.1:file.txt"
     ],
     "env": {
       "CHROME_HEADLESS": "1",
       "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
     },
     "infra_step": true,
-    "name": "read chromeos ip",
-    "stdout": "/path/to/tmp/",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import os@@@",
-      "@@@STEP_LOG_LINE@python.inline@SSH_MACHINE_FILE = os.path.expanduser('~/ssh_machine.json')@@@",
-      "@@@STEP_LOG_LINE@python.inline@with open(SSH_MACHINE_FILE, 'r') as f:@@@",
-      "@@@STEP_LOG_LINE@python.inline@  print f.read()@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "python",
-      "-u",
-      "\nimport subprocess\nimport sys\nhost = sys.argv[1]\ndevice   = sys.argv[2]\nprint subprocess.check_output(['scp', host, device])\n",
-      "file.txt",
-      "foo@127.0.0.1:file.txt"
-    ],
-    "infra_step": true,
-    "name": "scp file.txt foo@127.0.0.1:file.txt",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import subprocess@@@",
-      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
-      "@@@STEP_LOG_LINE@python.inline@host = sys.argv[1]@@@",
-      "@@@STEP_LOG_LINE@python.inline@device   = sys.argv[2]@@@",
-      "@@@STEP_LOG_LINE@python.inline@print subprocess.check_output(['scp', host, device])@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
+    "name": "scp file.txt foo@127.0.0.1:file.txt"
   },
   {
     "cmd": [
@@ -172,28 +157,6 @@
       "-t",
       "-t",
       "foo@127.0.0.1",
-      "sudo",
-      "mount",
-      "-i",
-      "-o",
-      "remount,exec",
-      "/home/chronos"
-    ],
-    "env": {
-      "CHROME_HEADLESS": "1",
-      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
-    },
-    "infra_step": true,
-    "name": "remount /home/chronos/user/ as exec"
-  },
-  {
-    "cmd": [
-      "ssh",
-      "-oConnectTimeout=15",
-      "-oBatchMode=yes",
-      "-t",
-      "-t",
-      "foo@127.0.0.1",
       "rm",
       "-rf",
       "/home/chronos/user/bin"
@@ -226,6 +189,28 @@
   },
   {
     "cmd": [
+      "ssh",
+      "-oConnectTimeout=15",
+      "-oBatchMode=yes",
+      "-t",
+      "-t",
+      "foo@127.0.0.1",
+      "sudo",
+      "mount",
+      "-i",
+      "-o",
+      "remount,exec",
+      "/home/chronos"
+    ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
+    "infra_step": true,
+    "name": "remount /home/chronos/user/ as exec"
+  },
+  {
+    "cmd": [
       "python",
       "-u",
       "\nimport subprocess\nimport sys\nsrc = sys.argv[1] + '/*'\ndest   = sys.argv[2]\nprint subprocess.check_output('scp -r %s %s' % (src, dest), shell=True)\n",
@@ -370,23 +355,16 @@
   },
   {
     "cmd": [
-      "python",
-      "-u",
-      "\nimport subprocess\nimport sys\nhost = sys.argv[1]\ndevice   = sys.argv[2]\nprint subprocess.check_output(['scp', host, device])\n",
+      "scp",
       "[START_DIR]/tmp/SKP_VERSION",
       "foo@127.0.0.1:/home/chronos/user/SKP_VERSION"
     ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
     "infra_step": true,
-    "name": "scp [START_DIR]/tmp/SKP_VERSION foo@127.0.0.1:/home/chronos/user/SKP_VERSION",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import subprocess@@@",
-      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
-      "@@@STEP_LOG_LINE@python.inline@host = sys.argv[1]@@@",
-      "@@@STEP_LOG_LINE@python.inline@device   = sys.argv[2]@@@",
-      "@@@STEP_LOG_LINE@python.inline@print subprocess.check_output(['scp', host, device])@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
+    "name": "scp [START_DIR]/tmp/SKP_VERSION foo@127.0.0.1:/home/chronos/user/SKP_VERSION"
   },
   {
     "cmd": [
@@ -514,23 +492,16 @@
   },
   {
     "cmd": [
-      "python",
-      "-u",
-      "\nimport subprocess\nimport sys\nhost = sys.argv[1]\ndevice   = sys.argv[2]\nprint subprocess.check_output(['scp', host, device])\n",
+      "scp",
       "[START_DIR]/tmp/SK_IMAGE_VERSION",
       "foo@127.0.0.1:/home/chronos/user/SK_IMAGE_VERSION"
     ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
     "infra_step": true,
-    "name": "scp [START_DIR]/tmp/SK_IMAGE_VERSION foo@127.0.0.1:/home/chronos/user/SK_IMAGE_VERSION",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import subprocess@@@",
-      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
-      "@@@STEP_LOG_LINE@python.inline@host = sys.argv[1]@@@",
-      "@@@STEP_LOG_LINE@python.inline@device   = sys.argv[2]@@@",
-      "@@@STEP_LOG_LINE@python.inline@print subprocess.check_output(['scp', host, device])@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
+    "name": "scp [START_DIR]/tmp/SK_IMAGE_VERSION foo@127.0.0.1:/home/chronos/user/SK_IMAGE_VERSION"
   },
   {
     "cmd": [
@@ -658,71 +629,29 @@
   },
   {
     "cmd": [
-      "python",
-      "-u",
-      "\nimport subprocess\nimport sys\nhost = sys.argv[1]\ndevice   = sys.argv[2]\nprint subprocess.check_output(['scp', host, device])\n",
+      "scp",
       "[START_DIR]/tmp/SVG_VERSION",
       "foo@127.0.0.1:/home/chronos/user/SVG_VERSION"
     ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
     "infra_step": true,
-    "name": "scp [START_DIR]/tmp/SVG_VERSION foo@127.0.0.1:/home/chronos/user/SVG_VERSION",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import subprocess@@@",
-      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
-      "@@@STEP_LOG_LINE@python.inline@host = sys.argv[1]@@@",
-      "@@@STEP_LOG_LINE@python.inline@device   = sys.argv[2]@@@",
-      "@@@STEP_LOG_LINE@python.inline@print subprocess.check_output(['scp', host, device])@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
+    "name": "scp [START_DIR]/tmp/SVG_VERSION foo@127.0.0.1:/home/chronos/user/SVG_VERSION"
   },
   {
     "cmd": [
-      "python",
-      "-u",
-      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
-      "--json-output",
-      "/path/to/tmp/json",
-      "rmtree",
-      "[START_DIR]/[SWARM_OUT_DIR]"
-    ],
-    "infra_step": true,
-    "name": "rmtree [SWARM_OUT_DIR]"
-  },
-  {
-    "cmd": [
-      "python",
-      "-u",
-      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
-      "--json-output",
-      "/path/to/tmp/json",
-      "ensure-directory",
-      "--mode",
-      "0777",
-      "[START_DIR]/[SWARM_OUT_DIR]"
-    ],
-    "infra_step": true,
-    "name": "makedirs [SWARM_OUT_DIR]"
-  },
-  {
-    "cmd": [
-      "python",
-      "-u",
-      "\nimport subprocess\nimport sys\nhost = sys.argv[1]\ndevice   = sys.argv[2]\nprint subprocess.check_output(['scp', host, device])\n",
+      "scp",
       "[START_DIR]/build/nanobench",
       "foo@127.0.0.1:/home/chronos/user/bin/nanobench"
     ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
     "infra_step": true,
-    "name": "scp [START_DIR]/build/nanobench foo@127.0.0.1:/home/chronos/user/bin/nanobench",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import subprocess@@@",
-      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
-      "@@@STEP_LOG_LINE@python.inline@host = sys.argv[1]@@@",
-      "@@@STEP_LOG_LINE@python.inline@device   = sys.argv[2]@@@",
-      "@@@STEP_LOG_LINE@python.inline@print subprocess.check_output(['scp', host, device])@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
+    "name": "scp [START_DIR]/build/nanobench foo@127.0.0.1:/home/chronos/user/bin/nanobench"
   },
   {
     "cmd": [
@@ -741,7 +670,7 @@
       "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
     },
     "infra_step": true,
-    "name": "chmod nanobench"
+    "name": "make nanobench executable"
   },
   {
     "cmd": [
diff --git a/infra/bots/recipe_modules/flavor/examples/full.expected/Test-ChromeOS-Clang-SamsungChromebookPlus-GPU-MaliT860-arm-Release-All.json b/infra/bots/recipe_modules/flavor/examples/full.expected/Test-ChromeOS-Clang-SamsungChromebookPlus-GPU-MaliT860-arm-Release-All.json
index ea98f1f..5b05da3 100644
--- a/infra/bots/recipe_modules/flavor/examples/full.expected/Test-ChromeOS-Clang-SamsungChromebookPlus-GPU-MaliT860-arm-Release-All.json
+++ b/infra/bots/recipe_modules/flavor/examples/full.expected/Test-ChromeOS-Clang-SamsungChromebookPlus-GPU-MaliT860-arm-Release-All.json
@@ -3,43 +3,28 @@
     "cmd": [
       "python",
       "-u",
-      "\nimport os\nSSH_MACHINE_FILE = os.path.expanduser('~/ssh_machine.json')\nwith open(SSH_MACHINE_FILE, 'r') as f:\n  print f.read()\n"
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[HOME]/ssh_machine.json",
+      "/path/to/tmp/"
+    ],
+    "infra_step": true,
+    "name": "read ssh_machine.json"
+  },
+  {
+    "cmd": [
+      "scp",
+      "file.txt",
+      "foo@127.0.0.1:file.txt"
     ],
     "env": {
       "CHROME_HEADLESS": "1",
       "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
     },
     "infra_step": true,
-    "name": "read chromeos ip",
-    "stdout": "/path/to/tmp/",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import os@@@",
-      "@@@STEP_LOG_LINE@python.inline@SSH_MACHINE_FILE = os.path.expanduser('~/ssh_machine.json')@@@",
-      "@@@STEP_LOG_LINE@python.inline@with open(SSH_MACHINE_FILE, 'r') as f:@@@",
-      "@@@STEP_LOG_LINE@python.inline@  print f.read()@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "python",
-      "-u",
-      "\nimport subprocess\nimport sys\nhost = sys.argv[1]\ndevice   = sys.argv[2]\nprint subprocess.check_output(['scp', host, device])\n",
-      "file.txt",
-      "foo@127.0.0.1:file.txt"
-    ],
-    "infra_step": true,
-    "name": "scp file.txt foo@127.0.0.1:file.txt",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import subprocess@@@",
-      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
-      "@@@STEP_LOG_LINE@python.inline@host = sys.argv[1]@@@",
-      "@@@STEP_LOG_LINE@python.inline@device   = sys.argv[2]@@@",
-      "@@@STEP_LOG_LINE@python.inline@print subprocess.check_output(['scp', host, device])@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
+    "name": "scp file.txt foo@127.0.0.1:file.txt"
   },
   {
     "cmd": [
@@ -172,28 +157,6 @@
       "-t",
       "-t",
       "foo@127.0.0.1",
-      "sudo",
-      "mount",
-      "-i",
-      "-o",
-      "remount,exec",
-      "/home/chronos"
-    ],
-    "env": {
-      "CHROME_HEADLESS": "1",
-      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
-    },
-    "infra_step": true,
-    "name": "remount /home/chronos/user/ as exec"
-  },
-  {
-    "cmd": [
-      "ssh",
-      "-oConnectTimeout=15",
-      "-oBatchMode=yes",
-      "-t",
-      "-t",
-      "foo@127.0.0.1",
       "rm",
       "-rf",
       "/home/chronos/user/bin"
@@ -226,6 +189,28 @@
   },
   {
     "cmd": [
+      "ssh",
+      "-oConnectTimeout=15",
+      "-oBatchMode=yes",
+      "-t",
+      "-t",
+      "foo@127.0.0.1",
+      "sudo",
+      "mount",
+      "-i",
+      "-o",
+      "remount,exec",
+      "/home/chronos"
+    ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
+    "infra_step": true,
+    "name": "remount /home/chronos/user/ as exec"
+  },
+  {
+    "cmd": [
       "python",
       "-u",
       "\nimport subprocess\nimport sys\nsrc = sys.argv[1] + '/*'\ndest   = sys.argv[2]\nprint subprocess.check_output('scp -r %s %s' % (src, dest), shell=True)\n",
@@ -370,23 +355,16 @@
   },
   {
     "cmd": [
-      "python",
-      "-u",
-      "\nimport subprocess\nimport sys\nhost = sys.argv[1]\ndevice   = sys.argv[2]\nprint subprocess.check_output(['scp', host, device])\n",
+      "scp",
       "[START_DIR]/tmp/SKP_VERSION",
       "foo@127.0.0.1:/home/chronos/user/SKP_VERSION"
     ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
     "infra_step": true,
-    "name": "scp [START_DIR]/tmp/SKP_VERSION foo@127.0.0.1:/home/chronos/user/SKP_VERSION",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import subprocess@@@",
-      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
-      "@@@STEP_LOG_LINE@python.inline@host = sys.argv[1]@@@",
-      "@@@STEP_LOG_LINE@python.inline@device   = sys.argv[2]@@@",
-      "@@@STEP_LOG_LINE@python.inline@print subprocess.check_output(['scp', host, device])@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
+    "name": "scp [START_DIR]/tmp/SKP_VERSION foo@127.0.0.1:/home/chronos/user/SKP_VERSION"
   },
   {
     "cmd": [
@@ -514,23 +492,16 @@
   },
   {
     "cmd": [
-      "python",
-      "-u",
-      "\nimport subprocess\nimport sys\nhost = sys.argv[1]\ndevice   = sys.argv[2]\nprint subprocess.check_output(['scp', host, device])\n",
+      "scp",
       "[START_DIR]/tmp/SK_IMAGE_VERSION",
       "foo@127.0.0.1:/home/chronos/user/SK_IMAGE_VERSION"
     ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
     "infra_step": true,
-    "name": "scp [START_DIR]/tmp/SK_IMAGE_VERSION foo@127.0.0.1:/home/chronos/user/SK_IMAGE_VERSION",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import subprocess@@@",
-      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
-      "@@@STEP_LOG_LINE@python.inline@host = sys.argv[1]@@@",
-      "@@@STEP_LOG_LINE@python.inline@device   = sys.argv[2]@@@",
-      "@@@STEP_LOG_LINE@python.inline@print subprocess.check_output(['scp', host, device])@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
+    "name": "scp [START_DIR]/tmp/SK_IMAGE_VERSION foo@127.0.0.1:/home/chronos/user/SK_IMAGE_VERSION"
   },
   {
     "cmd": [
@@ -658,71 +629,29 @@
   },
   {
     "cmd": [
-      "python",
-      "-u",
-      "\nimport subprocess\nimport sys\nhost = sys.argv[1]\ndevice   = sys.argv[2]\nprint subprocess.check_output(['scp', host, device])\n",
+      "scp",
       "[START_DIR]/tmp/SVG_VERSION",
       "foo@127.0.0.1:/home/chronos/user/SVG_VERSION"
     ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
     "infra_step": true,
-    "name": "scp [START_DIR]/tmp/SVG_VERSION foo@127.0.0.1:/home/chronos/user/SVG_VERSION",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import subprocess@@@",
-      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
-      "@@@STEP_LOG_LINE@python.inline@host = sys.argv[1]@@@",
-      "@@@STEP_LOG_LINE@python.inline@device   = sys.argv[2]@@@",
-      "@@@STEP_LOG_LINE@python.inline@print subprocess.check_output(['scp', host, device])@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
+    "name": "scp [START_DIR]/tmp/SVG_VERSION foo@127.0.0.1:/home/chronos/user/SVG_VERSION"
   },
   {
     "cmd": [
-      "python",
-      "-u",
-      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
-      "--json-output",
-      "/path/to/tmp/json",
-      "rmtree",
-      "[START_DIR]/[SWARM_OUT_DIR]"
-    ],
-    "infra_step": true,
-    "name": "rmtree [SWARM_OUT_DIR]"
-  },
-  {
-    "cmd": [
-      "python",
-      "-u",
-      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
-      "--json-output",
-      "/path/to/tmp/json",
-      "ensure-directory",
-      "--mode",
-      "0777",
-      "[START_DIR]/[SWARM_OUT_DIR]"
-    ],
-    "infra_step": true,
-    "name": "makedirs [SWARM_OUT_DIR]"
-  },
-  {
-    "cmd": [
-      "python",
-      "-u",
-      "\nimport subprocess\nimport sys\nhost = sys.argv[1]\ndevice   = sys.argv[2]\nprint subprocess.check_output(['scp', host, device])\n",
+      "scp",
       "[START_DIR]/build/dm",
       "foo@127.0.0.1:/home/chronos/user/bin/dm"
     ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
     "infra_step": true,
-    "name": "scp [START_DIR]/build/dm foo@127.0.0.1:/home/chronos/user/bin/dm",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import subprocess@@@",
-      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
-      "@@@STEP_LOG_LINE@python.inline@host = sys.argv[1]@@@",
-      "@@@STEP_LOG_LINE@python.inline@device   = sys.argv[2]@@@",
-      "@@@STEP_LOG_LINE@python.inline@print subprocess.check_output(['scp', host, device])@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
+    "name": "scp [START_DIR]/build/dm foo@127.0.0.1:/home/chronos/user/bin/dm"
   },
   {
     "cmd": [
@@ -741,7 +670,7 @@
       "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
     },
     "infra_step": true,
-    "name": "chmod dm"
+    "name": "make dm executable"
   },
   {
     "cmd": [
diff --git a/infra/bots/recipe_modules/flavor/examples/full.py b/infra/bots/recipe_modules/flavor/examples/full.py
index aae9049..20d9b9d 100644
--- a/infra/bots/recipe_modules/flavor/examples/full.py
+++ b/infra/bots/recipe_modules/flavor/examples/full.py
@@ -105,10 +105,6 @@
       api.test(buildername) +
       api.properties(**defaultProps(buildername))
     )
-    if 'Chromebook' in buildername and not 'Build' in buildername:
-      test += api.step_data(
-          'read chromeos ip',
-          stdout=api.raw_io.output('{"user_ip":"foo@127.0.0.1"}'))
     if 'Chromecast' in buildername:
       test += api.step_data(
           'read chromecast ip',
diff --git a/infra/bots/recipe_modules/flavor/ssh.py b/infra/bots/recipe_modules/flavor/ssh.py
new file mode 100644
index 0000000..54b7d24
--- /dev/null
+++ b/infra/bots/recipe_modules/flavor/ssh.py
@@ -0,0 +1,85 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+
+from recipe_engine import recipe_api
+
+import default
+
+
+"""SSH flavor, used for running code on a remote device via SSH.
+
+Must be subclassed to set self.device_dirs. The default implementation assumes
+a Linux-based device.
+"""
+
+
+class SSHFlavor(default.DefaultFlavor):
+
+  def __init__(self, m):
+    super(SSHFlavor, self).__init__(m)
+    self._user_ip = ''
+
+  @property
+  def user_ip(self):
+    if not self._user_ip:
+      path = self.m.path.expanduser('~/ssh_machine.json')
+      ssh_info = self.m.file.read_json('read ssh_machine.json', path,
+                                       test_data={'user_ip':'foo@127.0.0.1'})
+      self._user_ip = ssh_info.get(u'user_ip')
+    return self._user_ip
+
+  def ssh(self, title, *cmd, **kwargs):
+    if 'infra_step' not in kwargs:
+      kwargs['infra_step'] = True
+
+    ssh_cmd = ['ssh', '-oConnectTimeout=15', '-oBatchMode=yes',
+               '-t', '-t', self.user_ip] + list(cmd)
+
+    return self._run(title, ssh_cmd, **kwargs)
+
+  def ensure_device_dir(self, path):
+    self.ssh('mkdir %s' % path, 'mkdir', '-p', path)
+
+  def install(self):
+    self.ensure_device_dir(self.device_dirs.resource_dir)
+    self.create_clean_device_dir(self.device_dirs.bin_dir)
+
+  def create_clean_device_dir(self, path):
+    # use -f to silently return if path doesn't exist
+    self.ssh('rm %s' % path, 'rm', '-rf', path)
+    self.ensure_device_dir(path)
+
+  def read_file_on_device(self, path, **kwargs):
+    rv = self.ssh('read %s' % path,
+                   'cat', path, stdout=self.m.raw_io.output(),
+                   **kwargs)
+    return rv.stdout.rstrip() if rv and rv.stdout else None
+
+  def remove_file_on_device(self, path):
+    # use -f to silently return if path doesn't exist
+    self.ssh('rm %s' % path, 'rm', '-f', path)
+
+  def scp_device_path(self, device_path):
+    return '%s:%s' % (self.user_ip, device_path)
+
+  def copy_file_to_device(self, host_path, device_path):
+    device_path = self.scp_device_path(device_path)
+    self._run('scp %s %s' % (host_path, device_path),
+              ['scp', host_path, device_path], infra_step=True)
+
+  # TODO(benjaminwagner): implement with rsync
+  #def copy_directory_contents_to_device(self, host_path, device_path):
+
+  # TODO(benjaminwagner): implement with rsync
+  #def copy_directory_contents_to_host(self, device_path, host_path):
+
+  def step(self, name, cmd, **kwargs):
+    # Push and run cmd
+    host_path = self.host_dirs.bin_dir.join(cmd[0])
+    cmd[0] = self.device_path_join(self.device_dirs.bin_dir, cmd[0])
+    self.copy_file_to_device(host_path, cmd[0])
+
+    self.ssh('make %s executable' % name, 'chmod', '+x', cmd[0])
+    self.ssh(str(name), *cmd)
diff --git a/infra/bots/recipes/perf.expected/Perf-ChromeOS-Clang-ASUSChromebookFlipC100-GPU-MaliT764-arm-Release-All.json b/infra/bots/recipes/perf.expected/Perf-ChromeOS-Clang-ASUSChromebookFlipC100-GPU-MaliT764-arm-Release-All.json
index b8e57bb..d8b1c52 100644
--- a/infra/bots/recipes/perf.expected/Perf-ChromeOS-Clang-ASUSChromebookFlipC100-GPU-MaliT764-arm-Release-All.json
+++ b/infra/bots/recipes/perf.expected/Perf-ChromeOS-Clang-ASUSChromebookFlipC100-GPU-MaliT764-arm-Release-All.json
@@ -18,23 +18,15 @@
     "cmd": [
       "python",
       "-u",
-      "\nimport os\nSSH_MACHINE_FILE = os.path.expanduser('~/ssh_machine.json')\nwith open(SSH_MACHINE_FILE, 'r') as f:\n  print f.read()\n"
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[HOME]/ssh_machine.json",
+      "/path/to/tmp/"
     ],
-    "env": {
-      "CHROME_HEADLESS": "1",
-      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
-    },
     "infra_step": true,
-    "name": "read chromeos ip",
-    "stdout": "/path/to/tmp/",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import os@@@",
-      "@@@STEP_LOG_LINE@python.inline@SSH_MACHINE_FILE = os.path.expanduser('~/ssh_machine.json')@@@",
-      "@@@STEP_LOG_LINE@python.inline@with open(SSH_MACHINE_FILE, 'r') as f:@@@",
-      "@@@STEP_LOG_LINE@python.inline@  print f.read()@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
+    "name": "read ssh_machine.json"
   },
   {
     "cmd": [
@@ -63,28 +55,6 @@
       "-t",
       "-t",
       "foo@127.0.0.1",
-      "sudo",
-      "mount",
-      "-i",
-      "-o",
-      "remount,exec",
-      "/home/chronos"
-    ],
-    "env": {
-      "CHROME_HEADLESS": "1",
-      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
-    },
-    "infra_step": true,
-    "name": "remount /home/chronos/user/ as exec"
-  },
-  {
-    "cmd": [
-      "ssh",
-      "-oConnectTimeout=15",
-      "-oBatchMode=yes",
-      "-t",
-      "-t",
-      "foo@127.0.0.1",
       "rm",
       "-rf",
       "/home/chronos/user/bin"
@@ -117,6 +87,28 @@
   },
   {
     "cmd": [
+      "ssh",
+      "-oConnectTimeout=15",
+      "-oBatchMode=yes",
+      "-t",
+      "-t",
+      "foo@127.0.0.1",
+      "sudo",
+      "mount",
+      "-i",
+      "-o",
+      "remount,exec",
+      "/home/chronos"
+    ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
+    "infra_step": true,
+    "name": "remount /home/chronos/user/ as exec"
+  },
+  {
+    "cmd": [
       "python",
       "-u",
       "\nimport subprocess\nimport sys\nsrc = sys.argv[1] + '/*'\ndest   = sys.argv[2]\nprint subprocess.check_output('scp -r %s %s' % (src, dest), shell=True)\n",
@@ -261,23 +253,16 @@
   },
   {
     "cmd": [
-      "python",
-      "-u",
-      "\nimport subprocess\nimport sys\nhost = sys.argv[1]\ndevice   = sys.argv[2]\nprint subprocess.check_output(['scp', host, device])\n",
+      "scp",
       "[START_DIR]/tmp/SKP_VERSION",
       "foo@127.0.0.1:/home/chronos/user/SKP_VERSION"
     ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
     "infra_step": true,
-    "name": "scp [START_DIR]/tmp/SKP_VERSION foo@127.0.0.1:/home/chronos/user/SKP_VERSION",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import subprocess@@@",
-      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
-      "@@@STEP_LOG_LINE@python.inline@host = sys.argv[1]@@@",
-      "@@@STEP_LOG_LINE@python.inline@device   = sys.argv[2]@@@",
-      "@@@STEP_LOG_LINE@python.inline@print subprocess.check_output(['scp', host, device])@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
+    "name": "scp [START_DIR]/tmp/SKP_VERSION foo@127.0.0.1:/home/chronos/user/SKP_VERSION"
   },
   {
     "cmd": [
@@ -405,23 +390,16 @@
   },
   {
     "cmd": [
-      "python",
-      "-u",
-      "\nimport subprocess\nimport sys\nhost = sys.argv[1]\ndevice   = sys.argv[2]\nprint subprocess.check_output(['scp', host, device])\n",
+      "scp",
       "[START_DIR]/tmp/SK_IMAGE_VERSION",
       "foo@127.0.0.1:/home/chronos/user/SK_IMAGE_VERSION"
     ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
     "infra_step": true,
-    "name": "scp [START_DIR]/tmp/SK_IMAGE_VERSION foo@127.0.0.1:/home/chronos/user/SK_IMAGE_VERSION",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import subprocess@@@",
-      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
-      "@@@STEP_LOG_LINE@python.inline@host = sys.argv[1]@@@",
-      "@@@STEP_LOG_LINE@python.inline@device   = sys.argv[2]@@@",
-      "@@@STEP_LOG_LINE@python.inline@print subprocess.check_output(['scp', host, device])@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
+    "name": "scp [START_DIR]/tmp/SK_IMAGE_VERSION foo@127.0.0.1:/home/chronos/user/SK_IMAGE_VERSION"
   },
   {
     "cmd": [
@@ -549,23 +527,16 @@
   },
   {
     "cmd": [
-      "python",
-      "-u",
-      "\nimport subprocess\nimport sys\nhost = sys.argv[1]\ndevice   = sys.argv[2]\nprint subprocess.check_output(['scp', host, device])\n",
+      "scp",
       "[START_DIR]/tmp/SVG_VERSION",
       "foo@127.0.0.1:/home/chronos/user/SVG_VERSION"
     ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
     "infra_step": true,
-    "name": "scp [START_DIR]/tmp/SVG_VERSION foo@127.0.0.1:/home/chronos/user/SVG_VERSION",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import subprocess@@@",
-      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
-      "@@@STEP_LOG_LINE@python.inline@host = sys.argv[1]@@@",
-      "@@@STEP_LOG_LINE@python.inline@device   = sys.argv[2]@@@",
-      "@@@STEP_LOG_LINE@python.inline@print subprocess.check_output(['scp', host, device])@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
+    "name": "scp [START_DIR]/tmp/SVG_VERSION foo@127.0.0.1:/home/chronos/user/SVG_VERSION"
   },
   {
     "cmd": [
@@ -635,45 +606,7 @@
   },
   {
     "cmd": [
-      "python",
-      "-u",
-      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
-      "--json-output",
-      "/path/to/tmp/json",
-      "rmtree",
-      "[START_DIR]/[SWARM_OUT_DIR]"
-    ],
-    "env": {
-      "CHROME_HEADLESS": "1",
-      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
-    },
-    "infra_step": true,
-    "name": "rmtree [SWARM_OUT_DIR]"
-  },
-  {
-    "cmd": [
-      "python",
-      "-u",
-      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
-      "--json-output",
-      "/path/to/tmp/json",
-      "ensure-directory",
-      "--mode",
-      "0777",
-      "[START_DIR]/[SWARM_OUT_DIR]"
-    ],
-    "env": {
-      "CHROME_HEADLESS": "1",
-      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
-    },
-    "infra_step": true,
-    "name": "makedirs [SWARM_OUT_DIR]"
-  },
-  {
-    "cmd": [
-      "python",
-      "-u",
-      "\nimport subprocess\nimport sys\nhost = sys.argv[1]\ndevice   = sys.argv[2]\nprint subprocess.check_output(['scp', host, device])\n",
+      "scp",
       "[START_DIR]/build/nanobench",
       "foo@127.0.0.1:/home/chronos/user/bin/nanobench"
     ],
@@ -682,16 +615,7 @@
       "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
     },
     "infra_step": true,
-    "name": "scp [START_DIR]/build/nanobench foo@127.0.0.1:/home/chronos/user/bin/nanobench",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import subprocess@@@",
-      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
-      "@@@STEP_LOG_LINE@python.inline@host = sys.argv[1]@@@",
-      "@@@STEP_LOG_LINE@python.inline@device   = sys.argv[2]@@@",
-      "@@@STEP_LOG_LINE@python.inline@print subprocess.check_output(['scp', host, device])@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
+    "name": "scp [START_DIR]/build/nanobench foo@127.0.0.1:/home/chronos/user/bin/nanobench"
   },
   {
     "cmd": [
@@ -710,7 +634,7 @@
       "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
     },
     "infra_step": true,
-    "name": "chmod nanobench"
+    "name": "make nanobench executable"
   },
   {
     "cmd": [
diff --git a/infra/bots/recipes/perf.py b/infra/bots/recipes/perf.py
index 040920f..f4b9549 100644
--- a/infra/bots/recipes/perf.py
+++ b/infra/bots/recipes/perf.py
@@ -458,11 +458,6 @@
           'read chromecast ip',
           stdout=api.raw_io.output('192.168.1.2:5555'))
 
-    if 'ChromeOS' in builder:
-      test += api.step_data(
-          'read chromeos ip',
-          stdout=api.raw_io.output('{"user_ip":"foo@127.0.0.1"}'))
-
     yield test
 
   builder = 'Perf-Win10-Clang-NUCD34010WYKH-GPU-IntelHD4400-x86_64-Release-All'
diff --git a/infra/bots/recipes/test.expected/Test-ChromeOS-Clang-AcerChromebookR13Convertible-GPU-PowerVRGX6250-arm-Debug-All.json b/infra/bots/recipes/test.expected/Test-ChromeOS-Clang-AcerChromebookR13Convertible-GPU-PowerVRGX6250-arm-Debug-All.json
index aa05eb6..90200f1 100644
--- a/infra/bots/recipes/test.expected/Test-ChromeOS-Clang-AcerChromebookR13Convertible-GPU-PowerVRGX6250-arm-Debug-All.json
+++ b/infra/bots/recipes/test.expected/Test-ChromeOS-Clang-AcerChromebookR13Convertible-GPU-PowerVRGX6250-arm-Debug-All.json
@@ -18,23 +18,15 @@
     "cmd": [
       "python",
       "-u",
-      "\nimport os\nSSH_MACHINE_FILE = os.path.expanduser('~/ssh_machine.json')\nwith open(SSH_MACHINE_FILE, 'r') as f:\n  print f.read()\n"
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "copy",
+      "[HOME]/ssh_machine.json",
+      "/path/to/tmp/"
     ],
-    "env": {
-      "CHROME_HEADLESS": "1",
-      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
-    },
     "infra_step": true,
-    "name": "read chromeos ip",
-    "stdout": "/path/to/tmp/",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import os@@@",
-      "@@@STEP_LOG_LINE@python.inline@SSH_MACHINE_FILE = os.path.expanduser('~/ssh_machine.json')@@@",
-      "@@@STEP_LOG_LINE@python.inline@with open(SSH_MACHINE_FILE, 'r') as f:@@@",
-      "@@@STEP_LOG_LINE@python.inline@  print f.read()@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
+    "name": "read ssh_machine.json"
   },
   {
     "cmd": [
@@ -63,28 +55,6 @@
       "-t",
       "-t",
       "foo@127.0.0.1",
-      "sudo",
-      "mount",
-      "-i",
-      "-o",
-      "remount,exec",
-      "/home/chronos"
-    ],
-    "env": {
-      "CHROME_HEADLESS": "1",
-      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
-    },
-    "infra_step": true,
-    "name": "remount /home/chronos/user/ as exec"
-  },
-  {
-    "cmd": [
-      "ssh",
-      "-oConnectTimeout=15",
-      "-oBatchMode=yes",
-      "-t",
-      "-t",
-      "foo@127.0.0.1",
       "rm",
       "-rf",
       "/home/chronos/user/bin"
@@ -117,6 +87,28 @@
   },
   {
     "cmd": [
+      "ssh",
+      "-oConnectTimeout=15",
+      "-oBatchMode=yes",
+      "-t",
+      "-t",
+      "foo@127.0.0.1",
+      "sudo",
+      "mount",
+      "-i",
+      "-o",
+      "remount,exec",
+      "/home/chronos"
+    ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
+    "infra_step": true,
+    "name": "remount /home/chronos/user/ as exec"
+  },
+  {
+    "cmd": [
       "python",
       "-u",
       "\nimport subprocess\nimport sys\nsrc = sys.argv[1] + '/*'\ndest   = sys.argv[2]\nprint subprocess.check_output('scp -r %s %s' % (src, dest), shell=True)\n",
@@ -261,23 +253,16 @@
   },
   {
     "cmd": [
-      "python",
-      "-u",
-      "\nimport subprocess\nimport sys\nhost = sys.argv[1]\ndevice   = sys.argv[2]\nprint subprocess.check_output(['scp', host, device])\n",
+      "scp",
       "[START_DIR]/tmp/SKP_VERSION",
       "foo@127.0.0.1:/home/chronos/user/SKP_VERSION"
     ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
     "infra_step": true,
-    "name": "scp [START_DIR]/tmp/SKP_VERSION foo@127.0.0.1:/home/chronos/user/SKP_VERSION",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import subprocess@@@",
-      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
-      "@@@STEP_LOG_LINE@python.inline@host = sys.argv[1]@@@",
-      "@@@STEP_LOG_LINE@python.inline@device   = sys.argv[2]@@@",
-      "@@@STEP_LOG_LINE@python.inline@print subprocess.check_output(['scp', host, device])@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
+    "name": "scp [START_DIR]/tmp/SKP_VERSION foo@127.0.0.1:/home/chronos/user/SKP_VERSION"
   },
   {
     "cmd": [
@@ -405,23 +390,16 @@
   },
   {
     "cmd": [
-      "python",
-      "-u",
-      "\nimport subprocess\nimport sys\nhost = sys.argv[1]\ndevice   = sys.argv[2]\nprint subprocess.check_output(['scp', host, device])\n",
+      "scp",
       "[START_DIR]/tmp/SK_IMAGE_VERSION",
       "foo@127.0.0.1:/home/chronos/user/SK_IMAGE_VERSION"
     ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
     "infra_step": true,
-    "name": "scp [START_DIR]/tmp/SK_IMAGE_VERSION foo@127.0.0.1:/home/chronos/user/SK_IMAGE_VERSION",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import subprocess@@@",
-      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
-      "@@@STEP_LOG_LINE@python.inline@host = sys.argv[1]@@@",
-      "@@@STEP_LOG_LINE@python.inline@device   = sys.argv[2]@@@",
-      "@@@STEP_LOG_LINE@python.inline@print subprocess.check_output(['scp', host, device])@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
+    "name": "scp [START_DIR]/tmp/SK_IMAGE_VERSION foo@127.0.0.1:/home/chronos/user/SK_IMAGE_VERSION"
   },
   {
     "cmd": [
@@ -549,23 +527,16 @@
   },
   {
     "cmd": [
-      "python",
-      "-u",
-      "\nimport subprocess\nimport sys\nhost = sys.argv[1]\ndevice   = sys.argv[2]\nprint subprocess.check_output(['scp', host, device])\n",
+      "scp",
       "[START_DIR]/tmp/SVG_VERSION",
       "foo@127.0.0.1:/home/chronos/user/SVG_VERSION"
     ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
     "infra_step": true,
-    "name": "scp [START_DIR]/tmp/SVG_VERSION foo@127.0.0.1:/home/chronos/user/SVG_VERSION",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import subprocess@@@",
-      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
-      "@@@STEP_LOG_LINE@python.inline@host = sys.argv[1]@@@",
-      "@@@STEP_LOG_LINE@python.inline@device   = sys.argv[2]@@@",
-      "@@@STEP_LOG_LINE@python.inline@print subprocess.check_output(['scp', host, device])@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
+    "name": "scp [START_DIR]/tmp/SVG_VERSION foo@127.0.0.1:/home/chronos/user/SVG_VERSION"
   },
   {
     "cmd": [
@@ -683,23 +654,16 @@
   },
   {
     "cmd": [
-      "python",
-      "-u",
-      "\nimport subprocess\nimport sys\nhost = sys.argv[1]\ndevice   = sys.argv[2]\nprint subprocess.check_output(['scp', host, device])\n",
+      "scp",
       "[START_DIR]/tmp/uninteresting_hashes.txt",
       "foo@127.0.0.1:/home/chronos/user/uninteresting_hashes.txt"
     ],
+    "env": {
+      "CHROME_HEADLESS": "1",
+      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
+    },
     "infra_step": true,
-    "name": "scp [START_DIR]/tmp/uninteresting_hashes.txt foo@127.0.0.1:/home/chronos/user/uninteresting_hashes.txt",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import subprocess@@@",
-      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
-      "@@@STEP_LOG_LINE@python.inline@host = sys.argv[1]@@@",
-      "@@@STEP_LOG_LINE@python.inline@device   = sys.argv[2]@@@",
-      "@@@STEP_LOG_LINE@python.inline@print subprocess.check_output(['scp', host, device])@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
+    "name": "scp [START_DIR]/tmp/uninteresting_hashes.txt foo@127.0.0.1:/home/chronos/user/uninteresting_hashes.txt"
   },
   {
     "cmd": [
@@ -731,45 +695,7 @@
   },
   {
     "cmd": [
-      "python",
-      "-u",
-      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
-      "--json-output",
-      "/path/to/tmp/json",
-      "rmtree",
-      "[START_DIR]/[SWARM_OUT_DIR]"
-    ],
-    "env": {
-      "CHROME_HEADLESS": "1",
-      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
-    },
-    "infra_step": true,
-    "name": "rmtree [SWARM_OUT_DIR]"
-  },
-  {
-    "cmd": [
-      "python",
-      "-u",
-      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
-      "--json-output",
-      "/path/to/tmp/json",
-      "ensure-directory",
-      "--mode",
-      "0777",
-      "[START_DIR]/[SWARM_OUT_DIR]"
-    ],
-    "env": {
-      "CHROME_HEADLESS": "1",
-      "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
-    },
-    "infra_step": true,
-    "name": "makedirs [SWARM_OUT_DIR]"
-  },
-  {
-    "cmd": [
-      "python",
-      "-u",
-      "\nimport subprocess\nimport sys\nhost = sys.argv[1]\ndevice   = sys.argv[2]\nprint subprocess.check_output(['scp', host, device])\n",
+      "scp",
       "[START_DIR]/build/dm",
       "foo@127.0.0.1:/home/chronos/user/bin/dm"
     ],
@@ -778,16 +704,7 @@
       "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
     },
     "infra_step": true,
-    "name": "scp [START_DIR]/build/dm foo@127.0.0.1:/home/chronos/user/bin/dm",
-    "~followup_annotations": [
-      "@@@STEP_LOG_LINE@python.inline@@@@",
-      "@@@STEP_LOG_LINE@python.inline@import subprocess@@@",
-      "@@@STEP_LOG_LINE@python.inline@import sys@@@",
-      "@@@STEP_LOG_LINE@python.inline@host = sys.argv[1]@@@",
-      "@@@STEP_LOG_LINE@python.inline@device   = sys.argv[2]@@@",
-      "@@@STEP_LOG_LINE@python.inline@print subprocess.check_output(['scp', host, device])@@@",
-      "@@@STEP_LOG_END@python.inline@@@"
-    ]
+    "name": "scp [START_DIR]/build/dm foo@127.0.0.1:/home/chronos/user/bin/dm"
   },
   {
     "cmd": [
@@ -806,7 +723,7 @@
       "PATH": "<PATH>:RECIPE_REPO[depot_tools]"
     },
     "infra_step": true,
-    "name": "chmod dm"
+    "name": "make dm executable"
   },
   {
     "cmd": [
diff --git a/infra/bots/recipes/test.py b/infra/bots/recipes/test.py
index 5e1333a..9a8dce6 100644
--- a/infra/bots/recipes/test.py
+++ b/infra/bots/recipes/test.py
@@ -1090,11 +1090,6 @@
           'read chromecast ip',
           stdout=api.raw_io.output('192.168.1.2:5555'))
 
-    if 'ChromeOS' in builder:
-      test += api.step_data(
-          'read chromeos ip',
-          stdout=api.raw_io.output('{"user_ip":"foo@127.0.0.1"}'))
-
     yield test
 
   builder = 'Test-Win8-Clang-Golo-CPU-AVX-x86-Debug-All'