blob: 8df737ecca134786d6004fc0d5a8380aca528b70 [file] [log] [blame]
# Copyright 2015 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Module to download and run the CIPD client.
CIPD is the Chrome Infra Package Deployer, a simple method of resolving a
package/version into a GStorage link and installing them.
"""
import hashlib
import json
import logging
import os
from pathlib import Path
import pprint
import tempfile
from typing import Dict, Iterable, Optional, Union
import urllib.parse
from chromite.third_party import httplib2
from chromite.lib import cache
from chromite.lib import cros_build_lib
from chromite.lib import osutils
from chromite.lib import path_util
from chromite.utils import memoize
# pylint: disable=line-too-long
# CIPD client to download.
#
# This is version "git_revision:78137bc2d58ebea680b6d513a3d7f6a8d25aa643".
#
# To switch to another version:
# 1. Find it in CIPD Web UI, e.g.
# https://chrome-infra-packages.appspot.com/p/infra/tools/cipd/linux-amd64/+/latest
# 2. Look up SHA256 there.
# pylint: enable=line-too-long
CIPD_CLIENT_PACKAGE = "infra/tools/cipd/linux-amd64"
CIPD_CLIENT_SHA256 = (
"b431c29c9fa132d4f7d6e86f053af02d490b51d742b4f01ea55618a5365d357d"
)
CHROME_INFRA_PACKAGES_API_BASE = (
"https://chrome-infra-packages.appspot.com/prpc/cipd.Repository/"
)
STAGING_SERVICE_URL = "https://chrome-infra-packages-dev.appspot.com"
class Error(Exception):
"""Raised on fatal errors."""
def _ChromeInfraRequest(method, request):
"""Makes a request to the Chrome Infra Packages API with httplib2.
Args:
method: Name of RPC method to call.
request: RPC request body.
Returns:
Deserialized RPC response body.
"""
resp, body = httplib2.Http().request(
uri=CHROME_INFRA_PACKAGES_API_BASE + method,
method="POST",
headers={
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": "chromite",
},
body=json.dumps(request),
)
if resp.status != 200:
raise Error(
"Got HTTP %d from CIPD %r: %s" % (resp.status, method, body)
)
try:
return json.loads(body.lstrip(b")]}'\n"))
except ValueError:
raise Error("Bad response from CIPD server:\n%s" % (body,))
def _DownloadCIPD(instance_sha256):
"""Finds the CIPD download link and requests the binary.
Args:
instance_sha256: The version of CIPD client to download.
Returns:
The CIPD binary as a string.
"""
# Grab the signed URL to fetch the client binary from.
resp = _ChromeInfraRequest(
"DescribeClient",
{
"package": CIPD_CLIENT_PACKAGE,
"instance": {
"hashAlgo": "SHA256",
"hexDigest": instance_sha256,
},
},
)
if "clientBinary" not in resp:
logging.error(
"Error requesting the link to download CIPD from. Got:\n%s",
pprint.pformat(resp),
)
raise Error("Failed to bootstrap CIPD client")
# Download the actual binary.
http = httplib2.Http(cache=None)
response, binary = http.request(uri=resp["clientBinary"]["signedUrl"])
if response.status != 200:
raise Error("Got a %d response from Google Storage." % response.status)
# Check SHA256 matches what server expects.
digest = hashlib.sha256(binary).hexdigest()
for alias in resp["clientRefAliases"]:
if alias["hashAlgo"] == "SHA256":
if digest != alias["hexDigest"]:
raise Error(
"Unexpected CIPD client SHA256: got %s, want %s"
% (digest, alias["hexDigest"])
)
break
else:
raise Error("CIPD server didn't provide expected SHA256")
return binary
class CipdCache(cache.RemoteCache):
"""Supports caching of the CIPD download."""
def _Fetch(self, url, local_path): # pylint: disable=arguments-differ
instance_sha256 = urllib.parse.urlparse(url).netloc
binary = _DownloadCIPD(instance_sha256)
logging.info(
"Fetched CIPD package %s:%s", CIPD_CLIENT_PACKAGE, instance_sha256
)
osutils.WriteFile(local_path, binary, mode="wb")
os.chmod(local_path, 0o755)
def GetCIPDFromCache():
"""Checks the cache, downloading CIPD if it is missing.
Returns:
Path to the CIPD binary.
"""
cache_dir = os.path.join(path_util.GetCacheDir(), "cipd")
bin_cache = CipdCache(cache_dir)
key = (CIPD_CLIENT_SHA256,)
ref = bin_cache.Lookup(key)
ref.SetDefault("cipd://" + CIPD_CLIENT_SHA256)
return ref.path
def GetInstanceID(cipd_path, package, version, service_account_json=None):
"""Get the latest instance ID for ref latest.
Args:
cipd_path: Path to a cipd executable. GetCIPDFromCache can give this.
package: A string package name.
version: A string version of package.
service_account_json: The path of the service account credentials.
Returns:
A string instance ID.
"""
service_account_flag = []
if service_account_json:
service_account_flag = ["-service-account-json", service_account_json]
result = cros_build_lib.run(
[cipd_path, "resolve", package, "-version", version]
+ service_account_flag,
capture_output=True,
encoding="utf-8",
)
# An example output of resolve is like:
# Packages:\n package:instance_id
return result.stdout.splitlines()[-1].split(":")[-1]
@memoize.Memoize
def InstallPackage(
cipd_path,
package,
version,
destination: Optional[Union[os.PathLike, str]] = None,
service_account_json=None,
print_cmd: bool = True,
):
"""Installs a package at a given destination using cipd.
Args:
cipd_path: Path to a cipd executable. GetCIPDFromCache can give this.
package: A package name.
version: The CIPD version of the package to install (can be instance ID
or a ref).
destination: The folder to install the package under.
service_account_json: The path of the service account credentials.
print_cmd: Whether to print the command before running it.
Returns:
The path of the package.
"""
if not destination:
# GetCacheDir does a non-trivial amount of work,
# too much for a constant. If needed elsewhere, a
# memoized function would be a good alternative.
destination = (
Path(path_util.GetCacheDir()).absolute() / "cipd" / "packages"
)
destination = Path(destination) / package
service_account_flag = []
if service_account_json:
service_account_flag = ["-service-account-json", service_account_json]
with tempfile.NamedTemporaryFile() as f:
f.write(("%s %s" % (package, version)).encode("utf-8"))
f.flush()
cros_build_lib.run(
[cipd_path, "ensure", "-root", destination, "-list", f.name]
+ service_account_flag,
capture_output=True,
print_cmd=print_cmd,
)
return destination
def CreatePackage(
cipd_path: Union[os.PathLike, str],
package: str,
in_dir: Union[os.PathLike, str],
tags: Dict[str, str],
refs: Iterable[str],
cred_path: Optional[Union[os.PathLike, str]] = None,
service_url: Optional[str] = None,
) -> None:
"""Create (build and register) a package using cipd.
Args:
cipd_path: Path to a cipd executable. GetCIPDFromCache can give this.
package: A package name.
in_dir: The directory to create the package from.
tags: A mapping of tags to apply to the package.
refs: An Iterable of refs to apply to the package.
cred_path: The path of the service account credentials.
service_url: If provided, overrides the default CIPD backend URL. E.g.,
`STAGING_SERVICE_URL` will use staging.
"""
args = [
cipd_path,
"create",
"-name",
package,
"-in",
in_dir,
]
for key, value in tags.items():
args.extend(["-tag", "%s:%s" % (key, value)])
for ref in refs:
args.extend(["-ref", ref])
if cred_path:
args.extend(["-service-account-json", cred_path])
if service_url:
args.extend(["-service-url", service_url])
cros_build_lib.run(args, capture_output=True)