[prebuilt] Updates to cipd.py error handling

Improve error handling and messages in cipd.py.

Change-Id: I7db679f54fc3f11417a94127822194cc21008d5d
Reviewed-on: https://fuchsia-review.googlesource.com/c/infra/prebuilt/+/405395
Commit-Queue: Rob Mohr <mohrr@google.com>
Reviewed-by: Gary Boone <gboone@google.com>
diff --git a/cipd.py b/cipd.py
index fe0eece..f3401e2 100755
--- a/cipd.py
+++ b/cipd.py
@@ -4,24 +4,35 @@
 # found in the LICENSE file.
 """Installs and then runs cipd.
 
-This script installs cipd in ./tools/ and then executes it, passing through
-all arguments.
+This script installs cipd in ./tools/ (if necessary) and then executes it,
+passing through all arguments.
+
+Must be tested with Python 2 and Python 3.
 """
 
 from __future__ import print_function
 
 import hashlib
-import httplib
 import os
 import platform
+import ssl
 import subprocess
 import sys
-import urlparse
 
+try:
+    import httplib
+except ImportError:
+    import http.client as httplib  # type: ignore
+
+try:
+    import urlparse  # Python 2.
+except ImportError:
+    import urllib.parse as urlparse  # type: ignore
 
 SCRIPT_DIR = os.path.dirname(__file__)
 VERSION_FILE = os.path.join(SCRIPT_DIR, ".cipd_version")
 DIGESTS_FILE = VERSION_FILE + ".digests"
+
 # Put CIPD client in tools so that users can easily get it in their PATH.
 CLIENT = os.path.join(SCRIPT_DIR, "tools", "cipd")
 CIPD_HOST = "chrome-infra-packages.appspot.com"
@@ -43,8 +54,8 @@
     """Normalize arch into format expected in CIPD paths."""
 
     machine = platform.machine()
-    if machine.startswith("arm"):
-        return machine
+    if machine.startswith(("arm", "aarch")):
+        return machine.replace("aarch", "arm")
     if machine.endswith("64"):
         return "amd64"
     if machine.endswith("86"):
@@ -53,16 +64,24 @@
 
 
 def user_agent():
+    """Generate a user-agent based on the project name and current hash."""
+
     try:
         rev = subprocess.check_output(
             ["git", "-C", SCRIPT_DIR, "rev-parse", "HEAD"]
         ).strip()
     except subprocess.CalledProcessError:
         rev = "???"
+
+    if isinstance(rev, bytes):
+        rev = rev.decode()
+
     return "fuchsia-infra/tools/{}".format(rev)
 
 
 def actual_hash(path):
+    """Hash the file at path and return it."""
+
     hasher = hashlib.sha256()
     with open(path, "rb") as ins:
         hasher.update(ins.read())
@@ -72,46 +91,81 @@
 def expected_hash():
     """Pulls expected hash from digests file."""
 
+    expected_plat = "{}-{}".format(platform_normalized(), arch_normalized())
+
     with open(DIGESTS_FILE, "r") as ins:
         for line in ins:
             line = line.strip()
             if line.startswith("#") or not line:
                 continue
             plat, hashtype, hashval = line.split()
-            if hashtype == "sha256" and plat == "{}-{}".format(
-                platform_normalized(), arch_normalized()
-            ):
+            if hashtype == "sha256" and plat == expected_plat:
                 return hashval
-        raise Exception(
-            "platform {} not in {}".format(platform_normalized(), DIGESTS_FILE)
-        )
+    raise Exception("platform {} not in {}".format(expected_plat, DIGESTS_FILE))
 
 
 def client_bytes():
     """Pull down the CIPD client and return it as a bytes object.
 
-  Often CIPD_HOST returns a 302 FOUND with a pointer to storage.googleapis.com,
-  so this needs to handle redirects, but it shouldn't require the initial
-  response to be a redirect either.
-  """
+    Often CIPD_HOST returns a 302 FOUND with a pointer to
+    storage.googleapis.com, so this needs to handle redirects, but it
+    shouldn't require the initial response to be a redirect either.
+    """
 
     with open(VERSION_FILE, "r") as ins:
         version = ins.read().strip()
 
-    conn = httplib.HTTPSConnection(CIPD_HOST)
+    try:
+        conn = httplib.HTTPSConnection(CIPD_HOST)
+    except AttributeError:
+        print("=" * 70)
+        print(
+            """
+It looks like this version of Python does not support SSL. This is common
+when using Homebrew. If using Homebrew please run the following commands.
+If not using Homebrew check how your version of Python was built.
+
+brew install openssl  # Probably already installed, but good to confirm.
+brew uninstall python && brew install python
+""".strip()
+        )
+        print("=" * 70)
+        raise
+
     path = "/client?platform={platform}-{arch}&version={version}".format(
         platform=platform_normalized(), arch=arch_normalized(), version=version
     )
 
     for _ in range(10):
-        conn.request("GET", path)
-        res = conn.getresponse()
-        # Have to read the response before making a new request, so make sure
-        # we always read it.
-        content = res.read()
+        try:
+            conn.request("GET", path)
+            res = conn.getresponse()
+            # Have to read the response before making a new request, so make
+            # sure we always read it.
+            content = res.read()
+        except ssl.SSLError:
+            print(
+                "\n"
+                "SSL error in Python when downloading CIPD client.\n"
+                "If using system Python try\n"
+                "\n"
+                "    sudo pip install certifi\n"
+                "\n"
+                "If using Homebrew Python try\n"
+                "\n"
+                "    brew install openssl\n"
+                "    brew uninstall python\n"
+                "    brew install python\n"
+                "\n"
+                "Otherwise, check that your machine's Python can use SSL, "
+                "testing with the httplib module on Python 2 or http.client on "
+                "Python 3.",
+                file=sys.stderr,
+            )
+            raise
 
         # Found client bytes.
-        if res.status == httplib.OK:
+        if res.status == httplib.OK:  # pylint: disable=no-else-return
             return content
 
         # Redirecting to another location.
@@ -129,19 +183,21 @@
     raise Exception("failed to download client")
 
 
-def bootstrap():
+def bootstrap(client, silent=False):
     """Bootstrap cipd client installation."""
 
-    client_dir = os.path.dirname(CLIENT)
+    client_dir = os.path.dirname(client)
     if not os.path.isdir(client_dir):
         os.makedirs(client_dir)
 
-    print(
-        "Bootstrapping cipd client for {}-{}".format(
-            platform_normalized(), arch_normalized()
+    if not silent:
+        print(
+            "Bootstrapping cipd client for {}-{}".format(
+                platform_normalized(), arch_normalized()
+            )
         )
-    )
-    tmp_path = os.path.join(SCRIPT_DIR, "tools", ".cipd")
+
+    tmp_path = client + ".tmp"
     with open(tmp_path, "wb") as tmp:
         tmp.write(client_bytes())
 
@@ -150,19 +206,19 @@
 
     if expected != actual:
         raise Exception(
-            "digest of downloaded CIPD client is incorrect, check that "
-            "digests file is current"
+            "digest of downloaded CIPD client is incorrect, "
+            "check that digests file is current"
         )
 
-    os.chmod(tmp_path, 0755)
-    os.rename(tmp_path, CLIENT)
+    os.chmod(tmp_path, 0o755)
+    os.rename(tmp_path, client)
 
 
-def selfupdate():
+def selfupdate(client):
     """Update cipd client."""
 
     cmd = [
-        CLIENT,
+        client,
         "selfupdate",
         "-version-file",
         VERSION_FILE,
@@ -172,24 +228,24 @@
     subprocess.check_call(cmd)
 
 
-def init():
+def init(client=CLIENT, silent=False):
     """Install/update cipd client."""
 
     os.environ["CIPD_HTTP_USER_AGENT_PREFIX"] = user_agent()
 
     try:
-        if not os.path.isfile(CLIENT):
-            bootstrap()
+        if not os.path.isfile(client):
+            bootstrap(client, silent)
 
         try:
-            selfupdate()
+            selfupdate(client)
         except subprocess.CalledProcessError:
             print(
                 "CIPD selfupdate failed. Bootstrapping then retrying...",
                 file=sys.stderr,
             )
-            bootstrap()
-            selfupdate()
+            bootstrap(client)
+            selfupdate(client)
 
     except Exception:
         print(
@@ -197,13 +253,15 @@
             "`CIPD_HTTP_USER_AGENT_PREFIX={user_agent}/manual {client} "
             "selfupdate -version-file '{version_file}'` "
             "to diagnose if this is persistent.".format(
-                user_agent=user_agent(), client=CLIENT, version_file=VERSION_FILE,
+                user_agent=user_agent(), client=client, version_file=VERSION_FILE,
             ),
             file=sys.stderr,
         )
         raise
 
+    return client
+
 
 if __name__ == "__main__":
-    init()
-    subprocess.check_call([CLIENT] + sys.argv[1:])
+    client_exe = init()
+    subprocess.check_call([client_exe] + sys.argv[1:])