blob: 7af4063bfff5678f4f3533010c1acd0f960e55be [file] [log] [blame]
Dennis Kempincee37612013-06-14 10:40:41 -07001# Copyright (c) 2013 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
5import cookielib
Sean O'Brienfd204da2017-05-02 15:13:11 -07006import datetime
7import time
Dennis Kempincee37612013-06-14 10:40:41 -07008import getpass
9import imghdr
10import os
11import os.path
Dennis Kempin13d948e2014-04-18 11:23:32 -070012import re
Charlie Mooneyd30322b2014-09-04 15:02:29 -070013import subprocess
Dennis Kempincee37612013-06-14 10:40:41 -070014import sys
15import urllib
16import urllib2
Sean O'Brienfd204da2017-05-02 15:13:11 -070017import StringIO
18import multiprocessing
19import random
20import errno
Dennis Kempincee37612013-06-14 10:40:41 -070021
22# path to current script directory
23script_dir = os.path.dirname(os.path.realpath(__file__))
24cache_dir = os.path.realpath(os.path.join(script_dir, "..", "cache"))
25if not os.path.exists(cache_dir):
26 os.mkdir(cache_dir)
27
28# path to the cookies file used for storing the login cookies.
Sean O'Brienfd204da2017-05-02 15:13:11 -070029rpc_cred_file = os.path.join(cache_dir, "rpc_cred")
Dennis Kempincee37612013-06-14 10:40:41 -070030
31# path to folder where downloaded reports are cached
32log_cache_dir = os.path.join(cache_dir, "reports")
33if not os.path.exists(log_cache_dir):
34 os.mkdir(log_cache_dir)
35
36class FeedbackDownloader():
Sean O'Brienfd204da2017-05-02 15:13:11 -070037 # Stubby command to download a feedback log:
38 # field_id - determines what data to download:
39 # 1: feedback log ID #
40 # 7: screenshot
41 # 8: system log
42 # resource_id - feedback log ID #
43 STUBBY_FILE_CMD = 'stubby --rpc_creds_file=' + rpc_cred_file + \
44 ' call blade:feedback-export-api-prod ' \
45 'ReportService.Get ' \
46 '\'result_mask <field <id:{field_id}>>, ' \
47 'resource_id: "{resource_id}"\''
48 # Stubby command to download a list of feedback log IDs:
49 # product_id - 208 refers to ChromeOS
50 # submission_time_[start|end]_time_ms - feedback submission time range
51 # max_results - number of IDs to download
52 # token - page token
53 STUBBY_LIST_CMD = 'stubby --rpc_creds_file=' + rpc_cred_file + \
54 ' call blade:feedback-export-api-prod ' \
55 'ReportService.List --proto2 ' \
56 '\'result_mask <field <id:1>>, ' \
57 'product_id: "208", ' \
58 'submission_time_start_ms: 0, ' \
59 'submission_time_end_ms: {end_time}, ' \
60 'page_selection {{ max_results: {max_results} ' \
61 'token: "{token}" }}\''
62 # Refer to go/touch-feedback-download for information on getting permission
63 # to download feedback logs.
64 GAIA_CMD = '/google/data/ro/projects/gaiamint/bin/get_mint --type=loas ' \
65 '--text --scopes=40700 --endusercreds > ' + rpc_cred_file
66 SYSTEM_LOG_FIELD = 7
67 SCREENSHOT_FIELD = 8
68 sleep_sec = 4.0
69 auth_lock = multiprocessing.Lock()
Charlie Mooneyd30322b2014-09-04 15:02:29 -070070
Sean O'Brienfd204da2017-05-02 15:13:11 -070071 def __init__(self, force_authenticate=False):
72 if force_authenticate:
73 self._Authenticate()
Dennis Kempincee37612013-06-14 10:40:41 -070074
Charlie Mooneyd30322b2014-09-04 15:02:29 -070075 def _OctetStreamToBinary(self, octetStream):
76 """ The zip files are returned in an octet-stream format that must
77 be decoded back into a binary. This function scans through the stream
78 and unescapes the special characters
79 """
80 binary = ''
81 i = 0
82 while i < len(octetStream):
83 if ord(octetStream[i]) is ord('\\'):
84 if re.match('\d\d\d', octetStream[i + 1:i + 4]):
85 binary += chr(int(octetStream[i + 1:i + 4], 8))
86 i += 4
87 else:
88 binary += octetStream[i:i + 2].decode("string-escape")
89 i += 2
90 else:
91 binary += octetStream[i]
92 i += 1
93 return binary
94
Sean O'Brienfd204da2017-05-02 15:13:11 -070095 def _AuthenticateWithLock(self):
96 is_owner = self.auth_lock.acquire(False)
97 if is_owner:
98 self._Authenticate()
99 self.auth_lock.release()
100 else:
101 self.auth_lock.acquire()
102 self.auth_lock.release()
103
104 def _StubbyCall(self, cmd):
105 if not os.path.exists(rpc_cred_file):
106 try:
107 os.mkdir(cache_dir)
108 except OSError as ex:
109 if ex.errno != errno.EEXIST:
110 raise
111 pass
112 self._AuthenticateWithLock()
113 while True:
114 process = subprocess.Popen(cmd, shell=True,
115 stdout=subprocess.PIPE,
116 stderr=subprocess.PIPE)
117
118 output, errors = process.communicate()
119 errorcode = process.returncode
120
121 if ('AUTH_FAIL' in errors or
122 'CredentialsExpiredException' in output or
123 'FORBIDDEN' in output):
124 self._AuthenticateWithLock()
125 continue
126 elif ('exceeds rate limit' in errors or
127 'WaitUntilNonEmpty' in errors):
128 self._AuthenticateWithLock()
129
130 sleep_time = self.sleep_sec + random.uniform(0.0, self.sleep_sec * 2)
131 time.sleep(sleep_time)
132 continue
133 elif errorcode != 0:
134 print errors
135 print 'default error'
136 print "An error (%d) occurred while downloading" % errorcode
137 sys.exit(errorcode)
138 return output
139
140 def _DownloadAttachedFile(self, id, field):
141 cmd = FeedbackDownloader.STUBBY_FILE_CMD.format(
142 field_id=field, resource_id=id)
143 output = self._StubbyCall(cmd)
144 return output
145
146 def _Authenticate(self):
147 cmd = FeedbackDownloader.GAIA_CMD
Charlie Mooneyd30322b2014-09-04 15:02:29 -0700148 process = subprocess.Popen(cmd, shell=True,
149 stdout=subprocess.PIPE,
150 stderr=subprocess.PIPE)
Sean O'Brienfd204da2017-05-02 15:13:11 -0700151 print "Authenticating..."
Charlie Mooneyd30322b2014-09-04 15:02:29 -0700152 output, errors = process.communicate()
153 errorcode = process.returncode
Charlie Mooneyd30322b2014-09-04 15:02:29 -0700154
Sean O'Brienfd204da2017-05-02 15:13:11 -0700155 if errorcode != 0:
156 print errors
157 print "An error (%d) occurred while authenticating" % errorcode
158 if errorcode == 126:
159 print "You may need to run prodaccess"
160 sys.exit(errorcode)
161 return None
162 print "Done Authenticating"
163
164 def DownloadIDs(self, num, end_time=None, page_token=''):
165 if not end_time:
166 dt = datetime.datetime.utcnow() - datetime.datetime(1970, 1, 1)
167 end_time = (((dt.days * 24 * 60 * 60 + dt.seconds) * 1000) +
168 (dt.microseconds / 10))
169
170 cmd = FeedbackDownloader.STUBBY_LIST_CMD.format(
171 end_time=end_time, max_results=num, token=page_token)
172 output = self._StubbyCall(cmd)
173
174 page_token = filter(lambda x: 'next_page_token' in x, output.split('\n'))[0]
175 page_token = page_token.split(" ")[-1][1:-1]
176 ids = filter(lambda x: 'id' in x, output.split('\n'))
177 ids = [x[7:-1] for x in ids]
178 return page_token, ids
Dennis Kempincee37612013-06-14 10:40:41 -0700179
180 def DownloadSystemLog(self, id):
Charlie Mooneyd30322b2014-09-04 15:02:29 -0700181 report = self._DownloadAttachedFile(id,
Sean O'Brienfd204da2017-05-02 15:13:11 -0700182 FeedbackDownloader.SYSTEM_LOG_FIELD)
183 sleep_time = self.sleep_sec + random.uniform(0.0, self.sleep_sec * 2)
184 time.sleep(sleep_time)
185 data_line = None
186 system_log = None
187 for count, line in enumerate(StringIO.StringIO(report)):
188 if 'name: "system_logs.zip"' in line:
189 data_line = count + 2
190 elif data_line and data_line == count:
191 system_log = re.search('data: "(.*)"\s*', line).group(1)
Dennis Kempinc6c981d2014-04-18 11:49:16 -0700192
Sean O'Brienfd204da2017-05-02 15:13:11 -0700193 if not system_log or (system_log[0:2] != "BZ" and system_log[0:2] != "PK"):
194 print "Report " + id + " does not seem to include include log files..."
Dennis Kempincee37612013-06-14 10:40:41 -0700195 return None
Charlie Mooneyd30322b2014-09-04 15:02:29 -0700196
Sean O'Brienfd204da2017-05-02 15:13:11 -0700197 return self._OctetStreamToBinary(system_log)
Dennis Kempincee37612013-06-14 10:40:41 -0700198
199 def DownloadScreenshot(self, id):
Sean O'Brienfd204da2017-05-02 15:13:11 -0700200 print "Downloading screenshot from %s..." % id
201 report = self._DownloadAttachedFile(id,
202 FeedbackDownloader.SCREENSHOT_FIELD)
203 data_line = None
204 screenshot = None
205 for count, line in enumerate(StringIO.StringIO(report)):
206 if 'screenshot <' in line:
207 data_line = count + 2
208 elif data_line and data_line == count:
209 screenshot = re.search('content: "(.*)"\s*', line).group(1)
210
211 if not screenshot:
212 print "Report does not seem to include include a screenshot..."
213 return None
214
215 return self._OctetStreamToBinary(screenshot)