Upstream version 11.40.277.0
[platform/framework/web/crosswalk.git] / src / third_party / chromite / cros / commands / cros_deploy.py
1 # Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 """cros deploy: Deploy the packages onto the target device."""
6
7 from __future__ import print_function
8
9 import os
10 import logging
11 import urlparse
12
13 from chromite import cros
14 from chromite.lib import cros_build_lib
15 from chromite.lib import portage_util
16 from chromite.lib import remote_access
17
18
19 @cros.CommandDecorator('deploy')
20 class DeployCommand(cros.CrosCommand):
21   """Deploy the requested packages to the target device.
22
23   This command assumes the requested packages are already built in the
24   chroot. This command needs to run inside the chroot for inspecting
25   the installed packages.
26
27   Note: If the rootfs on your device is read-only, this command
28   remounts it as read-write. If the rootfs verification is enabled on
29   your device, this command disables it.
30   """
31
32   EPILOG = """
33 To deploy packages:
34   cros deploy device power_manager cherrypy
35   cros deploy device /path/to/package
36
37 To uninstall packages:
38   cros deploy --unmerge cherrypy
39
40 For more information of cros build usage:
41   cros build -h
42 """
43
44   DEVICE_BASE_DIR = '/usr/local/tmp/cros-deploy'
45   # This is defined in src/platform/dev/builder.py
46   STRIPPED_PACKAGES_DIR = 'stripped-packages'
47
48   # Override base class property to enable stats upload.
49   upload_stats = True
50
51   def __init__(self, options):
52     """Initializes DeployCommand."""
53     cros.CrosCommand.__init__(self, options)
54     self.emerge = True
55     self.strip = True
56     self.clean_binpkg = True
57     self.ssh_hostname = None
58     self.ssh_port = None
59     self.ssh_username = None
60     self.ssh_private_key = None
61     # The installation root of packages.
62     self.root = None
63     self.ping = True
64
65   @classmethod
66   def AddParser(cls, parser):
67     """Add a parser."""
68     super(cls, DeployCommand).AddParser(parser)
69     parser.add_argument(
70         'device', help='IP[:port] address of the target device.')
71     parser.add_argument(
72         'packages', help='Packages to install. You can specify '
73         '[category/]package[:slot] or the path to the binary package.',
74         nargs='+')
75     parser.add_argument(
76         '--board', default=None, help='The board to use. By default it is '
77         'automatically detected. You can override the detected board with '
78         'this option.')
79     parser.add_argument(
80         '--no-strip', dest='strip', action='store_false', default=True,
81         help='Do not run strip_package to filter out preset paths in the '
82         'package. Stripping removes debug symbol files and reduces the size '
83         'of the package significantly. Defaults to always strip.')
84     parser.add_argument(
85         '--unmerge',  dest='emerge', action='store_false', default=True,
86         help='Unmerge requested packages.')
87     parser.add_argument(
88         '--root', default='/',
89         help="Package installation root, e.g. '/' or '/usr/local'"
90         "(default: '/').")
91     parser.add_argument(
92         '--no-clean-binpkg', dest='clean_binpkg', action='store_false',
93         default=True, help='Do not clean outdated binary packages. '
94         ' Defaults to always clean.')
95     parser.add_argument(
96         '--emerge-args', default=None,
97         help='Extra arguments to pass to emerge.')
98     parser.add_argument(
99         '--private-key', type='path', default=None,
100         help='SSH identify file (private key).')
101     parser.add_argument(
102         '--no-ping', dest='ping', action='store_false', default=True,
103         help='Do not ping the device before attempting to connect to it.')
104
105   def GetLatestPackage(self, board, pkg):
106     """Returns the path to the latest |pkg| for |board|."""
107     sysroot = cros_build_lib.GetSysroot(board=board)
108     matches = portage_util.FindPackageNameMatches(pkg, board=board)
109     if not matches:
110       raise ValueError('Package %s is not installed!' % pkg)
111
112     idx = 0
113     if len(matches) > 1:
114       # Ask user to pick among multiple matches.
115       idx = cros_build_lib.GetChoice(
116           'Multiple matches found for %s: ' % pkg,
117           [os.path.join(x.category, x.pv) for x in matches])
118
119     cpv = matches[idx]
120     packages_dir = None
121     if self.strip:
122       try:
123         cros_build_lib.RunCommand(
124             ['strip_package', '--board', board,
125              os.path.join(cpv.category, '%s' % (cpv.pv))])
126         packages_dir = self.STRIPPED_PACKAGES_DIR
127       except cros_build_lib.RunCommandError:
128         logging.error('Cannot strip package %s', pkg)
129         raise
130
131     return portage_util.GetBinaryPackagePath(
132         cpv.category, cpv.package, cpv.version, sysroot=sysroot,
133         packages_dir=packages_dir)
134
135   def _Emerge(self, device, board, pkg, root, extra_args=None):
136     """Copies |pkg| to |device| and emerges it.
137
138     Args:
139       device: A ChromiumOSDevice object.
140       board: The board to use for retrieving |pkg|.
141       pkg: A package name.
142       root: The installation root of |pkg|.
143       extra_args: Extra arguments to pass to emerge.
144     """
145     if os.path.isfile(pkg):
146       latest_pkg = pkg
147     else:
148       latest_pkg = self.GetLatestPackage(board, pkg)
149
150     if not latest_pkg:
151       cros_build_lib.Die('Missing package %s.' % pkg)
152
153     pkgroot = os.path.join(device.work_dir, 'packages')
154     pkg_name = os.path.basename(latest_pkg)
155     pkg_dirname = os.path.basename(os.path.dirname(latest_pkg))
156     pkg_dir = os.path.join(pkgroot, pkg_dirname)
157     device.RunCommand(['mkdir', '-p', pkg_dir], remote_sudo=True)
158
159     logging.info('Copying %s to device...', latest_pkg)
160     device.CopyToDevice(latest_pkg, pkg_dir, remote_sudo=True)
161
162     portage_tmpdir = os.path.join(device.work_dir, 'portage-tmp')
163     device.RunCommand(['mkdir', '-p', portage_tmpdir], remote_sudo=True)
164     logging.info('Use portage temp dir %s', portage_tmpdir)
165
166     logging.info('Installing %s...', latest_pkg)
167     pkg_path = os.path.join(pkg_dir, pkg_name)
168
169     # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
170     # chromeos-base packages will be skipped due to the configuration
171     # in /etc/protage/make.profile/package.provided. However, there is
172     # a known bug that /usr/local/etc/portage is not setup properly
173     # (crbug.com/312041). This does not affect `cros deploy` because
174     # we do not use the preset PKGDIR.
175     extra_env = {
176         'FEATURES': '-sandbox',
177         'PKGDIR': pkgroot,
178         'PORTAGE_CONFIGROOT': '/usr/local',
179         'PORTAGE_TMPDIR': portage_tmpdir,
180         'PORTDIR': device.work_dir,
181         'CONFIG_PROTECT': '-*',
182     }
183     cmd = ['emerge', '--usepkg', pkg_path]
184     cmd.append('--root=%s' % root)
185     if extra_args:
186       cmd.append(extra_args)
187
188     try:
189       # Always showing the emerge output for clarity.
190       device.RunCommand(cmd, extra_env=extra_env, remote_sudo=True,
191                         capture_output=False, debug_level=logging.INFO)
192     except Exception:
193       logging.error('Failed to emerge package %s', pkg)
194       raise
195     else:
196       logging.info('%s has been installed.', pkg)
197     finally:
198       # Free up the space for other packages.
199       device.RunCommand(['rm', '-rf', portage_tmpdir, pkg_dir],
200                         error_code_ok=True, remote_sudo=True)
201
202   def _Unmerge(self, device, pkg, root):
203     """Unmerges |pkg| on |device|.
204
205     Args:
206       device: A RemoteDevice object.
207       pkg: A package name.
208       root: The installation root of |pkg|.
209     """
210     logging.info('Unmerging %s...', pkg)
211     cmd = ['qmerge', '--yes']
212     # Check if qmerge is available on the device. If not, use emerge.
213     if device.RunCommand(
214         ['qmerge', '--version'], error_code_ok=True).returncode != 0:
215       cmd = ['emerge']
216
217     cmd.extend(['--unmerge', pkg, '--root=%s' % root])
218     try:
219       # Always showing the qmerge/emerge output for clarity.
220       device.RunCommand(cmd, capture_output=False, remote_sudo=True,
221                         debug_level=logging.INFO)
222     except Exception:
223       logging.error('Failed to unmerge package %s', pkg)
224       raise
225     else:
226       logging.info('%s has been uninstalled.', pkg)
227
228   def _IsPathWritable(self, device, path):
229     """Returns True if |path| on |device| is writable."""
230     tmp_file = os.path.join(path, 'tmp.cros_flash')
231     result = device.RunCommand(['touch', tmp_file], remote_sudo=True,
232                                error_code_ok=True, capture_output=True)
233
234     if result.returncode != 0:
235       return False
236
237     device.RunCommand(['rm', tmp_file], error_code_ok=True, remote_sudo=True)
238
239     return True
240
241   def _ReadOptions(self):
242     """Processes options and set variables."""
243     self.emerge = self.options.emerge
244     self.strip = self.options.strip
245     self.clean_binpkg = self.options.clean_binpkg
246     self.root = self.options.root
247     self.ping = self.options.ping
248     device = self.options.device
249     # pylint: disable=E1101
250     if urlparse.urlparse(device).scheme == '':
251       # For backward compatibility, prepend ssh:// ourselves.
252       device = 'ssh://%s' % device
253
254     parsed = urlparse.urlparse(device)
255
256     if parsed.scheme == 'ssh':
257       self.ssh_hostname = parsed.hostname
258       self.ssh_username = parsed.username
259       self.ssh_port = parsed.port
260       self.ssh_private_key = self.options.private_key
261     else:
262       cros_build_lib.Die('Does not support device %s' % self.options.device)
263
264   def Run(self):
265     """Run cros deploy."""
266     cros_build_lib.AssertInsideChroot()
267     self._ReadOptions()
268     try:
269       with remote_access.ChromiumOSDeviceHandler(
270           self.ssh_hostname, port=self.ssh_port, username=self.ssh_username,
271           private_key=self.ssh_private_key, base_dir=self.DEVICE_BASE_DIR,
272           ping=self.ping) as device:
273         board = cros_build_lib.GetBoard(device_board=device.board,
274                                         override_board=self.options.board)
275         logging.info('Board is %s', board)
276
277         if self.clean_binpkg:
278           logging.info('Cleaning outdated binary packages for %s', board)
279           portage_util.CleanOutdatedBinaryPackages(board)
280
281         if not self._IsPathWritable(device, self.root):
282           # Only remounts rootfs if the given root is not writable.
283           if not device.MountRootfsReadWrite():
284             cros_build_lib.Die('Cannot remount rootfs as read-write. Exiting.')
285
286         for pkg in self.options.packages:
287           if self.emerge:
288             self._Emerge(device, board, pkg, self.root,
289                          extra_args=self.options.emerge_args)
290           else:
291             self._Unmerge(device, pkg, self.root)
292
293     except (Exception, KeyboardInterrupt) as e:
294       logging.error(e)
295       logging.error('Cros Deploy terminated before completing!')
296       if self.options.debug:
297         raise
298     else:
299       logging.info('Cros Deploy completed successfully.')