xref: /aosp_15_r20/external/autotest/utils/frozen_chromite/lib/cipd.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# -*- coding: utf-8 -*-
2*9c5db199SXin Li# Copyright 2015 The Chromium OS Authors. All rights reserved.
3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
4*9c5db199SXin Li# found in the LICENSE file.
5*9c5db199SXin Li
6*9c5db199SXin Li"""Module to download and run the CIPD client.
7*9c5db199SXin Li
8*9c5db199SXin LiCIPD is the Chrome Infra Package Deployer, a simple method of resolving a
9*9c5db199SXin Lipackage/version into a GStorage link and installing them.
10*9c5db199SXin Li"""
11*9c5db199SXin Li
12*9c5db199SXin Lifrom __future__ import print_function
13*9c5db199SXin Li
14*9c5db199SXin Liimport hashlib
15*9c5db199SXin Liimport json
16*9c5db199SXin Liimport os
17*9c5db199SXin Liimport pprint
18*9c5db199SXin Liimport tempfile
19*9c5db199SXin Li
20*9c5db199SXin Liimport httplib2
21*9c5db199SXin Lifrom six.moves import urllib
22*9c5db199SXin Li
23*9c5db199SXin Liimport autotest_lib.utils.frozen_chromite.lib.cros_logging as log
24*9c5db199SXin Lifrom autotest_lib.utils.frozen_chromite.lib import cache
25*9c5db199SXin Lifrom autotest_lib.utils.frozen_chromite.lib import osutils
26*9c5db199SXin Lifrom autotest_lib.utils.frozen_chromite.lib import path_util
27*9c5db199SXin Lifrom autotest_lib.utils.frozen_chromite.lib import cros_build_lib
28*9c5db199SXin Lifrom autotest_lib.utils.frozen_chromite.utils import memoize
29*9c5db199SXin Li
30*9c5db199SXin Li# pylint: disable=line-too-long
31*9c5db199SXin Li# CIPD client to download.
32*9c5db199SXin Li#
33*9c5db199SXin Li# This is version "git_revision:db7a486094873e3944b8e27ab5b23a3ae3c401e7".
34*9c5db199SXin Li#
35*9c5db199SXin Li# To switch to another version:
36*9c5db199SXin Li#   1. Find it in CIPD Web UI, e.g.
37*9c5db199SXin Li#      https://chrome-infra-packages.appspot.com/p/infra/tools/cipd/linux-amd64/+/latest
38*9c5db199SXin Li#   2. Look up SHA256 there.
39*9c5db199SXin Li# pylint: enable=line-too-long
40*9c5db199SXin LiCIPD_CLIENT_PACKAGE = 'infra/tools/cipd/linux-amd64'
41*9c5db199SXin LiCIPD_CLIENT_SHA256 = (
42*9c5db199SXin Li    'ea6b7547ddd316f32fd9974f598949c3f8f22f6beb8c260370242d0d84825162')
43*9c5db199SXin Li
44*9c5db199SXin LiCHROME_INFRA_PACKAGES_API_BASE = (
45*9c5db199SXin Li    'https://chrome-infra-packages.appspot.com/prpc/cipd.Repository/')
46*9c5db199SXin Li
47*9c5db199SXin Li
48*9c5db199SXin Liclass Error(Exception):
49*9c5db199SXin Li  """Raised on fatal errors."""
50*9c5db199SXin Li
51*9c5db199SXin Li
52*9c5db199SXin Lidef _ChromeInfraRequest(method, request):
53*9c5db199SXin Li  """Makes a request to the Chrome Infra Packages API with httplib2.
54*9c5db199SXin Li
55*9c5db199SXin Li  Args:
56*9c5db199SXin Li    method: Name of RPC method to call.
57*9c5db199SXin Li    request: RPC request body.
58*9c5db199SXin Li
59*9c5db199SXin Li  Returns:
60*9c5db199SXin Li    Deserialized RPC response body.
61*9c5db199SXin Li  """
62*9c5db199SXin Li  resp, body = httplib2.Http().request(
63*9c5db199SXin Li      uri=CHROME_INFRA_PACKAGES_API_BASE+method,
64*9c5db199SXin Li      method='POST',
65*9c5db199SXin Li      headers={
66*9c5db199SXin Li          'Accept': 'application/json',
67*9c5db199SXin Li          'Content-Type': 'application/json',
68*9c5db199SXin Li          'User-Agent': 'chromite',
69*9c5db199SXin Li      },
70*9c5db199SXin Li      body=json.dumps(request))
71*9c5db199SXin Li  if resp.status != 200:
72*9c5db199SXin Li    raise Error('Got HTTP %d from CIPD %r: %s' % (resp.status, method, body))
73*9c5db199SXin Li  try:
74*9c5db199SXin Li    return json.loads(body.lstrip(b")]}'\n"))
75*9c5db199SXin Li  except ValueError:
76*9c5db199SXin Li    raise Error('Bad response from CIPD server:\n%s' % (body,))
77*9c5db199SXin Li
78*9c5db199SXin Li
79*9c5db199SXin Lidef _DownloadCIPD(instance_sha256):
80*9c5db199SXin Li  """Finds the CIPD download link and requests the binary.
81*9c5db199SXin Li
82*9c5db199SXin Li  Args:
83*9c5db199SXin Li    instance_sha256: The version of CIPD client to download.
84*9c5db199SXin Li
85*9c5db199SXin Li  Returns:
86*9c5db199SXin Li    The CIPD binary as a string.
87*9c5db199SXin Li  """
88*9c5db199SXin Li  # Grab the signed URL to fetch the client binary from.
89*9c5db199SXin Li  resp = _ChromeInfraRequest('DescribeClient', {
90*9c5db199SXin Li      'package': CIPD_CLIENT_PACKAGE,
91*9c5db199SXin Li      'instance': {
92*9c5db199SXin Li          'hashAlgo': 'SHA256',
93*9c5db199SXin Li          'hexDigest': instance_sha256,
94*9c5db199SXin Li      },
95*9c5db199SXin Li  })
96*9c5db199SXin Li  if 'clientBinary' not in resp:
97*9c5db199SXin Li    log.error(
98*9c5db199SXin Li        'Error requesting the link to download CIPD from. Got:\n%s',
99*9c5db199SXin Li        pprint.pformat(resp))
100*9c5db199SXin Li    raise Error('Failed to bootstrap CIPD client')
101*9c5db199SXin Li
102*9c5db199SXin Li  # Download the actual binary.
103*9c5db199SXin Li  http = httplib2.Http(cache=None)
104*9c5db199SXin Li  response, binary = http.request(uri=resp['clientBinary']['signedUrl'])
105*9c5db199SXin Li  if response.status != 200:
106*9c5db199SXin Li    raise Error('Got a %d response from Google Storage.' % response.status)
107*9c5db199SXin Li
108*9c5db199SXin Li  # Check SHA256 matches what server expects.
109*9c5db199SXin Li  digest = hashlib.sha256(binary).hexdigest()
110*9c5db199SXin Li  for alias in resp['clientRefAliases']:
111*9c5db199SXin Li    if alias['hashAlgo'] == 'SHA256':
112*9c5db199SXin Li      if digest != alias['hexDigest']:
113*9c5db199SXin Li        raise Error(
114*9c5db199SXin Li            'Unexpected CIPD client SHA256: got %s, want %s' %
115*9c5db199SXin Li            (digest, alias['hexDigest']))
116*9c5db199SXin Li      break
117*9c5db199SXin Li  else:
118*9c5db199SXin Li    raise Error("CIPD server didn't provide expected SHA256")
119*9c5db199SXin Li
120*9c5db199SXin Li  return binary
121*9c5db199SXin Li
122*9c5db199SXin Li
123*9c5db199SXin Liclass CipdCache(cache.RemoteCache):
124*9c5db199SXin Li  """Supports caching of the CIPD download."""
125*9c5db199SXin Li  def _Fetch(self, url, local_path):
126*9c5db199SXin Li    instance_sha256 = urllib.parse.urlparse(url).netloc
127*9c5db199SXin Li    binary = _DownloadCIPD(instance_sha256)
128*9c5db199SXin Li    log.info('Fetched CIPD package %s:%s', CIPD_CLIENT_PACKAGE, instance_sha256)
129*9c5db199SXin Li    osutils.WriteFile(local_path, binary, mode='wb')
130*9c5db199SXin Li    os.chmod(local_path, 0o755)
131*9c5db199SXin Li
132*9c5db199SXin Li
133*9c5db199SXin Lidef GetCIPDFromCache():
134*9c5db199SXin Li  """Checks the cache, downloading CIPD if it is missing.
135*9c5db199SXin Li
136*9c5db199SXin Li  Returns:
137*9c5db199SXin Li    Path to the CIPD binary.
138*9c5db199SXin Li  """
139*9c5db199SXin Li  cache_dir = os.path.join(path_util.GetCacheDir(), 'cipd')
140*9c5db199SXin Li  bin_cache = CipdCache(cache_dir)
141*9c5db199SXin Li  key = (CIPD_CLIENT_SHA256,)
142*9c5db199SXin Li  ref = bin_cache.Lookup(key)
143*9c5db199SXin Li  ref.SetDefault('cipd://' + CIPD_CLIENT_SHA256)
144*9c5db199SXin Li  return ref.path
145*9c5db199SXin Li
146*9c5db199SXin Li
147*9c5db199SXin Lidef GetInstanceID(cipd_path, package, version, service_account_json=None):
148*9c5db199SXin Li  """Get the latest instance ID for ref latest.
149*9c5db199SXin Li
150*9c5db199SXin Li  Args:
151*9c5db199SXin Li    cipd_path: The path to a cipd executable. GetCIPDFromCache can give this.
152*9c5db199SXin Li    package: A string package name.
153*9c5db199SXin Li    version: A string version of package.
154*9c5db199SXin Li    service_account_json: The path of the service account credentials.
155*9c5db199SXin Li
156*9c5db199SXin Li  Returns:
157*9c5db199SXin Li    A string instance ID.
158*9c5db199SXin Li  """
159*9c5db199SXin Li  service_account_flag = []
160*9c5db199SXin Li  if service_account_json:
161*9c5db199SXin Li    service_account_flag = ['-service-account-json', service_account_json]
162*9c5db199SXin Li
163*9c5db199SXin Li  result = cros_build_lib.run(
164*9c5db199SXin Li      [cipd_path, 'resolve', package, '-version', version] +
165*9c5db199SXin Li      service_account_flag, capture_output=True, encoding='utf-8')
166*9c5db199SXin Li  # An example output of resolve is like:
167*9c5db199SXin Li  #   Packages:\n package:instance_id
168*9c5db199SXin Li  return result.output.splitlines()[-1].split(':')[-1]
169*9c5db199SXin Li
170*9c5db199SXin Li
171*9c5db199SXin Li@memoize.Memoize
172*9c5db199SXin Lidef InstallPackage(cipd_path, package, instance_id, destination,
173*9c5db199SXin Li                   service_account_json=None):
174*9c5db199SXin Li  """Installs a package at a given destination using cipd.
175*9c5db199SXin Li
176*9c5db199SXin Li  Args:
177*9c5db199SXin Li    cipd_path: The path to a cipd executable. GetCIPDFromCache can give this.
178*9c5db199SXin Li    package: A package name.
179*9c5db199SXin Li    instance_id: The version of the package to install.
180*9c5db199SXin Li    destination: The folder to install the package under.
181*9c5db199SXin Li    service_account_json: The path of the service account credentials.
182*9c5db199SXin Li
183*9c5db199SXin Li  Returns:
184*9c5db199SXin Li    The path of the package.
185*9c5db199SXin Li  """
186*9c5db199SXin Li  destination = os.path.join(destination, package)
187*9c5db199SXin Li
188*9c5db199SXin Li  service_account_flag = []
189*9c5db199SXin Li  if service_account_json:
190*9c5db199SXin Li    service_account_flag = ['-service-account-json', service_account_json]
191*9c5db199SXin Li
192*9c5db199SXin Li  with tempfile.NamedTemporaryFile() as f:
193*9c5db199SXin Li    f.write(('%s %s' % (package, instance_id)).encode('utf-8'))
194*9c5db199SXin Li    f.flush()
195*9c5db199SXin Li
196*9c5db199SXin Li    cros_build_lib.run(
197*9c5db199SXin Li        [cipd_path, 'ensure', '-root', destination, '-list', f.name]
198*9c5db199SXin Li        + service_account_flag,
199*9c5db199SXin Li        capture_output=True)
200*9c5db199SXin Li
201*9c5db199SXin Li  return destination
202*9c5db199SXin Li
203*9c5db199SXin Li
204*9c5db199SXin Lidef CreatePackage(cipd_path, package, in_dir, tags, refs,
205*9c5db199SXin Li                  cred_path=None):
206*9c5db199SXin Li  """Create (build and register) a package using cipd.
207*9c5db199SXin Li
208*9c5db199SXin Li  Args:
209*9c5db199SXin Li    cipd_path: The path to a cipd executable. GetCIPDFromCache can give this.
210*9c5db199SXin Li    package: A package name.
211*9c5db199SXin Li    in_dir: The directory to create the package from.
212*9c5db199SXin Li    tags: A mapping of tags to apply to the package.
213*9c5db199SXin Li    refs: An Iterable of refs to apply to the package.
214*9c5db199SXin Li    cred_path: The path of the service account credentials.
215*9c5db199SXin Li  """
216*9c5db199SXin Li  args = [
217*9c5db199SXin Li      cipd_path, 'create',
218*9c5db199SXin Li      '-name', package,
219*9c5db199SXin Li      '-in', in_dir,
220*9c5db199SXin Li  ]
221*9c5db199SXin Li  for key, value in tags.items():
222*9c5db199SXin Li    args.extend(['-tag', '%s:%s' % (key, value)])
223*9c5db199SXin Li  for ref in refs:
224*9c5db199SXin Li    args.extend(['-ref', ref])
225*9c5db199SXin Li  if cred_path:
226*9c5db199SXin Li    args.extend(['-service-account-json', cred_path])
227*9c5db199SXin Li
228*9c5db199SXin Li  cros_build_lib.run(args, capture_output=True)
229*9c5db199SXin Li
230*9c5db199SXin Li
231*9c5db199SXin Lidef BuildPackage(cipd_path, package, in_dir, outfile):
232*9c5db199SXin Li  """Build a package using cipd.
233*9c5db199SXin Li
234*9c5db199SXin Li  Args:
235*9c5db199SXin Li    cipd_path: The path to a cipd executable. GetCIPDFromCache can give this.
236*9c5db199SXin Li    package: A package name.
237*9c5db199SXin Li    in_dir: The directory to create the package from.
238*9c5db199SXin Li    outfile: Output file.  Should have extension .cipd
239*9c5db199SXin Li  """
240*9c5db199SXin Li  args = [
241*9c5db199SXin Li      cipd_path, 'pkg-build',
242*9c5db199SXin Li      '-name', package,
243*9c5db199SXin Li      '-in', in_dir,
244*9c5db199SXin Li      '-out', outfile,
245*9c5db199SXin Li  ]
246*9c5db199SXin Li  cros_build_lib.run(args, capture_output=True)
247*9c5db199SXin Li
248*9c5db199SXin Li
249*9c5db199SXin Lidef RegisterPackage(cipd_path, package_file, tags, refs, cred_path=None):
250*9c5db199SXin Li  """Register and upload a package using cipd.
251*9c5db199SXin Li
252*9c5db199SXin Li  Args:
253*9c5db199SXin Li    cipd_path: The path to a cipd executable. GetCIPDFromCache can give this.
254*9c5db199SXin Li    package_file: The path to a .cipd package file.
255*9c5db199SXin Li    tags: A mapping of tags to apply to the package.
256*9c5db199SXin Li    refs: An Iterable of refs to apply to the package.
257*9c5db199SXin Li    cred_path: The path of the service account credentials.
258*9c5db199SXin Li  """
259*9c5db199SXin Li  args = [cipd_path, 'pkg-register', package_file]
260*9c5db199SXin Li  for key, value in tags.items():
261*9c5db199SXin Li    args.extend(['-tag', '%s:%s' % (key, value)])
262*9c5db199SXin Li  for ref in refs:
263*9c5db199SXin Li    args.extend(['-ref', ref])
264*9c5db199SXin Li  if cred_path:
265*9c5db199SXin Li    args.extend(['-service-account-json', cred_path])
266*9c5db199SXin Li  cros_build_lib.run(args, capture_output=True)
267