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