def write_file(filename, data, mode='w', continue_on_error=False, display_errors=True): """Writes data to a file. Args: filename: String, the path of the file to write to disk. data: Serializable data to write to the file. mode: String, the mode in which to open the file and write to it. continue_on_error: Boolean, If True, suppresses any IO errors and returns to the caller without any externalities. display_errors: Boolean, If True, prints error messages when errors are encountered and continue_on_error is True. Returns: Boolean, True if the write operation succeeded, or False if not. """ try: with _open_file(filename, mode) as f: f.write(data) return True except IOError as e: if continue_on_error: if display_errors: display.print_error(e) return False else: controlflow.system_error_exit(6, e)
def getHoldInfo(): v = buildGAPIObject() hold = sys.argv[3] matterId = None i = 4 while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') if myarg == 'matter': matterId = getMatterItem(v, sys.argv[i+1]) holdId = convertHoldNameToID(v, hold, matterId) i += 2 else: controlflow.invalid_argument_exit(myarg, "gam info hold") if not matterId: controlflow.system_error_exit( 3, 'you must specify a matter for the hold.') results = gapi.call(v.matters().holds(), 'get', matterId=matterId, holdId=holdId) cd = __main__.buildGAPIObject('directory') if 'accounts' in results: account_type = 'group' if results['corpus'] == 'GROUPS' else 'user' for i in range(0, len(results['accounts'])): uid = f'uid:{results["accounts"][i]["accountId"]}' acct_email = __main__.convertUIDtoEmailAddress( uid, cd, [account_type]) results['accounts'][i]['email'] = acct_email if 'orgUnit' in results: results['orgUnit']['orgUnitPath'] = __main__.doGetOrgInfo( results['orgUnit']['orgUnitId'], return_attrib='orgUnitPath') display.print_json(results)
def doUpdateCustomer(): cd = gapi.directory.buildGAPIObject() body = {} i = 3 while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') if myarg in ADDRESS_FIELDS_ARGUMENT_MAP: body.setdefault('postalAddress', {}) arg = ADDRESS_FIELDS_ARGUMENT_MAP[myarg] body['postalAddress'][arg] = sys.argv[i + 1] i += 2 elif myarg in ['adminsecondaryemail', 'alternateemail']: body['alternateEmail'] = sys.argv[i + 1] i += 2 elif myarg in ['phone', 'phonenumber']: body['phoneNumber'] = sys.argv[i + 1] i += 2 elif myarg == 'language': body['language'] = sys.argv[i + 1] i += 2 else: controlflow.invalid_argument_exit(myarg, "gam update customer") if not body: controlflow.system_error_exit( 2, 'no arguments specified for "gam ' 'update customer"') gapi.call(cd.customers(), 'patch', customerKey=GC_Values[GC_CUSTOMER_ID], body=body) print('Updated customer')
def updateMatter(action=None): v = buildGAPIObject() matterId = getMatterItem(v, sys.argv[3]) body = {} action_kwargs = {'body': {}} add_collaborators = [] remove_collaborators = [] cd = None i = 4 while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') if myarg == 'action': action = sys.argv[i+1].lower() if action not in VAULT_MATTER_ACTIONS: controlflow.system_error_exit(3, f'allowed actions are ' \ f'{", ".join(VAULT_MATTER_ACTIONS)}, got {action}') i += 2 elif myarg == 'name': body['name'] = sys.argv[i+1] i += 2 elif myarg == 'description': body['description'] = sys.argv[i+1] i += 2 elif myarg in ['addcollaborator', 'addcollaborators']: if not cd: cd = __main__.buildGAPIObject('directory') add_collaborators.extend(validateCollaborators(sys.argv[i+1], cd)) i += 2 elif myarg in ['removecollaborator', 'removecollaborators']: if not cd: cd = __main__.buildGAPIObject('directory') remove_collaborators.extend( validateCollaborators(sys.argv[i+1], cd)) i += 2 else: controlflow.invalid_argument_exit(sys.argv[i], "gam update matter") if action == 'delete': action_kwargs = {} if body: print(f'Updating matter {sys.argv[3]}...') if 'name' not in body or 'description' not in body: # bah, API requires name/description to be sent # on update even when it's not changing result = gapi.call(v.matters(), 'get', matterId=matterId, view='BASIC') body.setdefault('name', result['name']) body.setdefault('description', result.get('description')) gapi.call(v.matters(), 'update', body=body, matterId=matterId) if action: print(f'Performing {action} on matter {sys.argv[3]}') gapi.call(v.matters(), action, matterId=matterId, **action_kwargs) for collaborator in add_collaborators: print(f' adding collaborator {collaborator["email"]}') body = {'matterPermission': {'role': 'COLLABORATOR', 'accountId': collaborator['id']}} gapi.call(v.matters(), 'addPermissions', matterId=matterId, body=body) for collaborator in remove_collaborators: print(f' removing collaborator {collaborator["email"]}') gapi.call(v.matters(), 'removePermissions', matterId=matterId, body={'accountId': collaborator['id']})
def _adjust_date(errMsg): match_date = re.match('Data for dates later than (.*) is not yet ' 'available. Please check back later', errMsg) if not match_date: match_date = re.match('Start date can not be later than (.*)', errMsg) if not match_date: controlflow.system_error_exit(4, errMsg) return str(match_date.group(1))
def get_date_zero_time_or_full_time(time_string): time_string = time_string.strip() if time_string: if YYYYMMDD_PATTERN.match(time_string): return get_yyyymmdd(time_string) + 'T00:00:00.000Z' return get_time_or_delta_from_now(time_string) controlflow.system_error_exit( 2, f'expected a <{YYYYMMDDTHHMMSS_FORMAT_REQUIRED}>')
def validateCollaborators(collaboratorList, cd): collaborators = [] for collaborator in collaboratorList.split(','): collaborator_id = __main__.convertEmailAddressToUID(collaborator, cd) if not collaborator_id: controlflow.system_error_exit(4, f'failed to get a UID for ' f'{collaborator}. Please make ' f'sure this is a real user.') collaborators.append({'email': collaborator, 'id': collaborator_id}) return collaborators
def md5_matches_file(local_file, expected_md5, exitOnError): f = fileutils.open_file(local_file, 'rb') hash_md5 = md5() for chunk in iter(lambda: f.read(4096), b""): hash_md5.update(chunk) actual_hash = hash_md5.hexdigest() if exitOnError and actual_hash != expected_md5: controlflow.system_error_exit( 6, f'actual hash was {actual_hash}. Exiting on corrupt file.') return actual_hash == expected_md5
def open_file(filename, mode='r', encoding=None, newline=None, strip_utf_bom=False): """Opens a file. Args: filename: String, the name of the file to open, or '-' to use stdin/stdout, to read/write, depending on the mode param, respectively. mode: String, the common file mode to open the file with. Default is read. encoding: String, the name of the encoding used to decode or encode the file. This should only be used in text mode. newline: See param description in https://docs.python.org/3.7/library/functions.html#open strip_utf_bom: Boolean, True if the file being opened should seek past the UTF Byte Order Mark before being returned. See more: https://en.wikipedia.org/wiki/UTF-8#Byte_order_mark Returns: The opened file. """ try: if filename == '-': # Read from stdin, rather than a file if 'r' in mode: return io.StringIO(str(sys.stdin.read())) return sys.stdout # Open a file on disk f = _open_file(filename, mode, newline=newline, encoding=encoding) if strip_utf_bom: utf_bom = u'\ufeff' has_bom = False if 'b' in mode: has_bom = f.read(3).decode('UTF-8') == utf_bom elif f.encoding and not f.encoding.lower().startswith('utf'): # Convert UTF BOM into ISO-8859-1 via Bytes utf8_bom_bytes = utf_bom.encode('UTF-8') iso_8859_1_bom = utf8_bom_bytes.decode('iso-8859-1').encode( 'iso-8859-1') has_bom = f.read(3).encode('iso-8859-1', 'replace') == iso_8859_1_bom else: has_bom = f.read(1) == utf_bom if not has_bom: f.seek(0) return f except IOError as e: controlflow.system_error_exit(6, e)
def convertHoldNameToID(v, nameOrID, matterId): nameOrID = nameOrID.lower() cg = UID_PATTERN.match(nameOrID) if cg: return cg.group(1) fields = 'holds(holdId,name),nextPageToken' holds = gapi.get_all_pages(v.matters().holds( ), 'list', 'holds', matterId=matterId, fields=fields) for hold in holds: if hold['name'].lower() == nameOrID: return hold['holdId'] controlflow.system_error_exit(4, f'could not find hold name {nameOrID} ' f'in matter {matterId}')
def convertExportNameToID(v, nameOrID, matterId): nameOrID = nameOrID.lower() cg = UID_PATTERN.match(nameOrID) if cg: return cg.group(1) fields = 'exports(id,name),nextPageToken' exports = gapi.get_all_pages(v.matters().exports( ), 'list', 'exports', matterId=matterId, fields=fields) for export in exports: if export['name'].lower() == nameOrID: return export['id'] controlflow.system_error_exit(4, f'could not find export name {nameOrID} ' f'in matter {matterId}')
def get_time_or_delta_from_now(time_string): """Get an ISO 8601 time or a positive/negative delta applied to now. Args: time_string (string): The time or delta (e.g. '2017-09-01T12:34:56Z' or '-4h') Returns: string: iso8601 formatted datetime in UTC. """ time_string = time_string.strip().upper() if time_string: if time_string[0] not in ['+', '-']: return time_string return (datetime.datetime.utcnow() + get_delta_time(time_string)).isoformat() + 'Z' controlflow.system_error_exit( 2, f'expected a <{YYYYMMDDTHHMMSS_FORMAT_REQUIRED}>')
def deleteHold(): v = buildGAPIObject() hold = sys.argv[3] matterId = None i = 4 while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') if myarg == 'matter': matterId = getMatterItem(v, sys.argv[i+1]) holdId = convertHoldNameToID(v, hold, matterId) i += 2 else: controlflow.invalid_argument_exit(myarg, "gam delete hold") if not matterId: controlflow.system_error_exit( 3, 'you must specify a matter for the hold.') print(f'Deleting hold {hold} / {holdId}') gapi.call(v.matters().holds(), 'delete', matterId=matterId, holdId=holdId)
def read_file(filename, mode='r', encoding=None, newline=None, continue_on_error=False, display_errors=True): """Reads a file from disk. Args: filename: String, the path of the file to open from disk, or "-" to read from stdin. mode: String, the mode in which to open the file. encoding: String, the name of the encoding used to decode or encode the file. This should only be used in text mode. newline: See param description in https://docs.python.org/3.7/library/functions.html#open continue_on_error: Boolean, If True, suppresses any IO errors and returns to the caller without any externalities. display_errors: Boolean, If True, prints error messages when errors are encountered and continue_on_error is True. Returns: The contents of the file, or stdin if filename == "-". Returns None if an error is encountered and continue_on_errors is True. """ try: if filename == '-': # Read from stdin, rather than a file. return str(sys.stdin.read()) with _open_file(filename, mode, newline=newline, encoding=encoding) as f: return f.read() except IOError as e: if continue_on_error: if display_errors: display.print_warning(e) return None controlflow.system_error_exit(6, e) except (LookupError, UnicodeDecodeError, UnicodeError) as e: controlflow.system_error_exit(2, str(e))
def moveOrDeleteEvent(moveOrDelete): calendarId, cal = buildCalendarDataGAPIObject(sys.argv[2]) if not cal: return sendUpdates = None doit = False kwargs = {} i = 4 while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') if myarg in ['notifyattendees', 'sendnotifications', 'sendupdates']: sendUpdates, i = getSendUpdates(myarg, i, cal) elif myarg in ['id', 'eventid']: eventId = sys.argv[i + 1] i += 2 elif myarg in ['query', 'eventquery']: controlflow.system_error_exit( 2, f'query is no longer supported for {moveOrDelete}event. ' \ f'Use "gam calendar <email> printevents query <query> | ' \ f'gam csv - gam {moveOrDelete}event id ~id" instead.') elif myarg == 'doit': doit = True i += 1 elif moveOrDelete == 'move' and myarg == 'destination': kwargs['destination'] = sys.argv[i + 1] i += 2 else: controlflow.invalid_argument_exit( sys.argv[i], f"gam calendar <email> {moveOrDelete}event") if doit: print(f' going to {moveOrDelete} eventId {eventId}') gapi.call(cal.events(), moveOrDelete, calendarId=calendarId, eventId=eventId, sendUpdates=sendUpdates, **kwargs) else: print( f' would {moveOrDelete} eventId {eventId}. Add doit to command ' \ f'to actually {moveOrDelete} event')
def handle_oauth_token_error(e, soft_errors): """On a token error, exits the application and writes a message to stderr. Args: e: google.auth.exceptions.RefreshError, The error to handle. soft_errors: Boolean, if True, suppresses any applicable errors and instead returns to the caller. """ token_error = str(e).replace('.', '') if token_error in errors.OAUTH2_TOKEN_ERRORS or e.startswith( 'Invalid response'): if soft_errors: return if not GM_Globals[GM_CURRENT_API_USER]: display.print_error( MESSAGE_API_ACCESS_DENIED.format( GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID], ','.join(GM_Globals[GM_CURRENT_API_SCOPES]))) controlflow.system_error_exit(12, MESSAGE_API_ACCESS_CONFIG) else: controlflow.system_error_exit( 19, MESSAGE_SERVICE_NOT_APPLICABLE.format( GM_Globals[GM_CURRENT_API_USER])) controlflow.system_error_exit(18, f'Authentication Token Error - {str(e)}')
def get_yyyymmdd(argstr, minLen=1, returnTimeStamp=False, returnDateTime=False): argstr = argstr.strip() if argstr: if argstr[0] in ['+', '-']: today = datetime.date.today() argstr = (datetime.datetime(today.year, today.month, today.day) + get_delta_date(argstr)).strftime(YYYYMMDD_FORMAT) try: dateTime = datetime.datetime.strptime(argstr, YYYYMMDD_FORMAT) if returnTimeStamp: return time.mktime(dateTime.timetuple()) * 1000 if returnDateTime: return dateTime return argstr except ValueError: controlflow.system_error_exit( 2, f'expected a <{YYYYMMDD_FORMAT_REQUIRED}>; got {argstr}') elif minLen == 0: return '' controlflow.system_error_exit(2, f'expected a <{YYYYMMDD_FORMAT_REQUIRED}>')
def getBuildingByNameOrId(cd, which_building, minLen=1): if not which_building or \ (minLen == 0 and which_building in ['id:', 'uid:']): if minLen == 0: return '' controlflow.system_error_exit(3, 'Building id/name is empty') cg = UID_PATTERN.match(which_building) if cg: return cg.group(1) if GM_Globals[GM_MAP_BUILDING_NAME_TO_ID] is None: _makeBuildingIdNameMap(cd) # Exact name match, return ID if which_building in GM_Globals[GM_MAP_BUILDING_NAME_TO_ID]: return GM_Globals[GM_MAP_BUILDING_NAME_TO_ID][which_building] # No exact name match, check for case insensitive name matches which_building_lower = which_building.lower() ci_matches = [] for buildingName, buildingId in GM_Globals[ GM_MAP_BUILDING_NAME_TO_ID].items(): if buildingName.lower() == which_building_lower: ci_matches.append({ 'buildingName': buildingName, 'buildingId': buildingId }) # One match, return ID if len(ci_matches) == 1: return ci_matches[0]['buildingId'] # No or multiple name matches, try ID # Exact ID match, return ID if which_building in GM_Globals[GM_MAP_BUILDING_ID_TO_NAME]: return which_building # No exact ID match, check for case insensitive id match for buildingId in GM_Globals[GM_MAP_BUILDING_ID_TO_NAME]: # Match, return ID if buildingId.lower() == which_building_lower: return buildingId # Multiple name matches if len(ci_matches) > 1: message = 'Multiple buildings with same name:\n' for building in ci_matches: message += f' Name:{building["buildingName"]} ' \ f'id:{building["buildingId"]}\n' message += '\nPlease specify building name by exact case or by id.' controlflow.system_error_exit(3, message) # No matches else: controlflow.system_error_exit(3, f'No such building {which_building}')
def get_string(i, item, optional=False, minLen=1, maxLen=None): if i < len(sys.argv): argstr = sys.argv[i] if argstr: if (len(argstr) >= minLen) and ((maxLen is None) or (len(argstr) <= maxLen)): return argstr controlflow.system_error_exit( 2, f'expected <{integerLimits(minLen, maxLen, "string length")} for {item}>' ) if optional or (minLen == 0): return '' controlflow.system_error_exit(2, f'expected a Non-empty <{item}>') elif optional: return '' controlflow.system_error_exit(2, f'expected a <{item}>')
def write_csv_file(csvRows, titles, list_type, todrive): def rowDateTimeFilterMatch(dateMode, rowDate, op, filterDate): if not rowDate or not isinstance(rowDate, str): return False try: rowTime = dateutil.parser.parse(rowDate, ignoretz=True) if dateMode: rowDate = datetime.datetime(rowTime.year, rowTime.month, rowTime.day).isoformat() + 'Z' except ValueError: rowDate = NEVER_TIME if op == '<': return rowDate < filterDate if op == '<=': return rowDate <= filterDate if op == '>': return rowDate > filterDate if op == '>=': return rowDate >= filterDate if op == '!=': return rowDate != filterDate return rowDate == filterDate def rowCountFilterMatch(rowCount, op, filterCount): if isinstance(rowCount, str): if not rowCount.isdigit(): return False rowCount = int(rowCount) elif not isinstance(rowCount, int): return False if op == '<': return rowCount < filterCount if op == '<=': return rowCount <= filterCount if op == '>': return rowCount > filterCount if op == '>=': return rowCount >= filterCount if op == '!=': return rowCount != filterCount return rowCount == filterCount def rowBooleanFilterMatch(rowBoolean, filterBoolean): if not isinstance(rowBoolean, bool): return False return rowBoolean == filterBoolean def headerFilterMatch(title): for filterStr in GC_Values[GC_CSV_HEADER_FILTER]: if filterStr.match(title): return True return False if GC_Values[GC_CSV_ROW_FILTER]: for column, filterVal in iter(GC_Values[GC_CSV_ROW_FILTER].items()): if column not in titles: sys.stderr.write( f'WARNING: Row filter column "{column}" is not in output columns\n' ) continue if filterVal[0] == 'regex': csvRows = [ row for row in csvRows if filterVal[1].search(str(row.get(column, ''))) ] elif filterVal[0] == 'notregex': csvRows = [ row for row in csvRows if not filterVal[1].search(str(row.get(column, ''))) ] elif filterVal[0] in ['date', 'time']: csvRows = [ row for row in csvRows if rowDateTimeFilterMatch( filterVal[0] == 'date', row.get(column, ''), filterVal[1], filterVal[2]) ] elif filterVal[0] == 'count': csvRows = [ row for row in csvRows if rowCountFilterMatch( row.get(column, 0), filterVal[1], filterVal[2]) ] else: #boolean csvRows = [ row for row in csvRows if rowBooleanFilterMatch( row.get(column, False), filterVal[1]) ] if GC_Values[GC_CSV_HEADER_FILTER]: titles = [t for t in titles if headerFilterMatch(t)] if not titles: controlflow.system_error_exit( 3, 'No columns selected with GAM_CSV_HEADER_FILTER\n') return csv.register_dialect('nixstdout', lineterminator='\n') if todrive: write_to = io.StringIO() else: write_to = sys.stdout writer = csv.DictWriter(write_to, fieldnames=titles, dialect='nixstdout', extrasaction='ignore', quoting=csv.QUOTE_MINIMAL) try: writer.writerow(dict((item, item) for item in writer.fieldnames)) writer.writerows(csvRows) except IOError as e: controlflow.system_error_exit(6, e) if todrive: admin_email = __main__._getValueFromOAuth('email') _, drive = __main__.buildDrive3GAPIObject(admin_email) if not drive: print( f'''\nGAM is not authorized to create Drive files. Please run: gam user {admin_email} check serviceaccount and follow recommend steps to authorize GAM for Drive access.''') sys.exit(5) result = gapi.call(drive.about(), 'get', fields='maxImportSizes') columns = len(titles) rows = len(csvRows) cell_count = rows * columns data_size = len(write_to.getvalue()) max_sheet_bytes = int( result['maxImportSizes'][MIMETYPE_GA_SPREADSHEET]) if cell_count > MAX_GOOGLE_SHEET_CELLS or data_size > max_sheet_bytes: print( f'{WARNING_PREFIX}{MESSAGE_RESULTS_TOO_LARGE_FOR_GOOGLE_SPREADSHEET}' ) mimeType = 'text/csv' else: mimeType = MIMETYPE_GA_SPREADSHEET body = { 'description': QuotedArgumentList(sys.argv), 'name': f'{GC_Values[GC_DOMAIN]} - {list_type}', 'mimeType': mimeType } result = gapi.call(drive.files(), 'create', fields='webViewLink', body=body, media_body=googleapiclient.http.MediaInMemoryUpload( write_to.getvalue().encode(), mimetype='text/csv')) file_url = result['webViewLink'] if GC_Values[GC_NO_BROWSER]: msg_txt = f'Drive file uploaded to:\n {file_url}' msg_subj = f'{GC_Values[GC_DOMAIN]} - {list_type}' __main__.send_email(msg_subj, msg_txt) print(msg_txt) else: webbrowser.open(file_url)
def get_gapi_error_detail(e, soft_errors=False, silent_errors=False, retry_on_http_error=False): """Extracts error detail from a non-200 GAPI Response. Args: e: googleapiclient.HttpError, The HTTP Error received. soft_errors: Boolean, If true, causes error messages to be surpressed, rather than sending them to stderr. silent_errors: Boolean, If true, suppresses and ignores any errors from being displayed retry_on_http_error: Boolean, If true, will return -1 as the HTTP Response code, indicating that the request can be retried. TODO: Remove this param, as it seems to be outside the scope of this method. Returns: A tuple containing the HTTP Response code, GAPI error reason, and error message. """ try: error = json.loads(e.content.decode(UTF8)) except ValueError: error_content = e.content.decode(UTF8) if isinstance( e.content, bytes) else e.content if (e.resp['status'] == '503') and (error_content == 'Quota exceeded for the current request'): return (e.resp['status'], ErrorReason.QUOTA_EXCEEDED.value, error_content) if (e.resp['status'] == '403') and (error_content.startswith( 'Request rate higher than configured')): return (e.resp['status'], ErrorReason.QUOTA_EXCEEDED.value, error_content) if (e.resp['status'] == '403') and ('Invalid domain.' in error_content): error = _create_http_error_dict(403, ErrorReason.NOT_FOUND.value, 'Domain not found') elif (e.resp['status'] == '400') and ('InvalidSsoSigningKey' in error_content): error = _create_http_error_dict(400, ErrorReason.INVALID.value, 'InvalidSsoSigningKey') elif (e.resp['status'] == '400') and ('UnknownError' in error_content): error = _create_http_error_dict(400, ErrorReason.INVALID.value, 'UnknownError') elif retry_on_http_error: return (-1, None, None) elif soft_errors: if not silent_errors: display.print_error(error_content) return (0, None, None) else: controlflow.system_error_exit(5, error_content) # END: ValueError catch if 'error' in error: http_status = error['error']['code'] try: message = error['error']['errors'][0]['message'] except KeyError: message = error['error']['message'] else: if 'error_description' in error: if error['error_description'] == 'Invalid Value': message = error['error_description'] http_status = 400 error = _create_http_error_dict(400, ErrorReason.INVALID.value, message) else: controlflow.system_error_exit(4, str(error)) else: controlflow.system_error_exit(4, str(error)) # Extract the error reason try: reason = error['error']['errors'][0]['reason'] if reason == 'notFound': if 'userKey' in message: reason = ErrorReason.USER_NOT_FOUND.value elif 'groupKey' in message: reason = ErrorReason.GROUP_NOT_FOUND.value elif 'memberKey' in message: reason = ErrorReason.MEMBER_NOT_FOUND.value elif 'Domain not found' in message: reason = ErrorReason.DOMAIN_NOT_FOUND.value elif 'Resource Not Found' in message: reason = ErrorReason.RESOURCE_NOT_FOUND.value elif reason == 'invalid': if 'userId' in message: reason = ErrorReason.USER_NOT_FOUND.value elif 'memberKey' in message: reason = ErrorReason.INVALID_MEMBER.value elif reason == 'failedPrecondition': if 'Bad Request' in message: reason = ErrorReason.BAD_REQUEST.value elif 'Mail service not enabled' in message: reason = ErrorReason.SERVICE_NOT_AVAILABLE.value elif reason == 'required': if 'memberKey' in message: reason = ErrorReason.MEMBER_NOT_FOUND.value elif reason == 'conditionNotMet': if 'Cyclic memberships not allowed' in message: reason = ErrorReason.CYCLIC_MEMBERSHIPS_NOT_ALLOWED.value except KeyError: reason = '{0}'.format(http_status) return (http_status, reason, message)
def createHold(): v = buildGAPIObject() allowed_corpuses = gapi.get_enum_values_minus_unspecified( v._rootDesc['schemas']['Hold']['properties']['corpus']['enum']) body = {'query': {}} i = 3 query = None start_time = None end_time = None matterId = None accounts = [] while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') if myarg == 'name': body['name'] = sys.argv[i+1] i += 2 elif myarg == 'query': query = sys.argv[i+1] i += 2 elif myarg == 'corpus': body['corpus'] = sys.argv[i+1].upper() if body['corpus'] not in allowed_corpuses: controlflow.expected_argument_exit( "corpus", ", ".join(allowed_corpuses), sys.argv[i+1]) i += 2 elif myarg in ['accounts', 'users', 'groups']: accounts = sys.argv[i+1].split(',') i += 2 elif myarg in ['orgunit', 'ou']: body['orgUnit'] = { 'orgUnitId': __main__.getOrgUnitId(sys.argv[i+1])[1]} i += 2 elif myarg in ['start', 'starttime']: start_time = utils.get_date_zero_time_or_full_time(sys.argv[i+1]) i += 2 elif myarg in ['end', 'endtime']: end_time = utils.get_date_zero_time_or_full_time(sys.argv[i+1]) i += 2 elif myarg == 'matter': matterId = getMatterItem(v, sys.argv[i+1]) i += 2 else: controlflow.invalid_argument_exit(sys.argv[i], "gam create hold") if not matterId: controlflow.system_error_exit( 3, 'you must specify a matter for the new hold.') if not body.get('name'): controlflow.system_error_exit( 3, 'you must specify a name for the new hold.') if not body.get('corpus'): controlflow.system_error_exit(3, f'you must specify a corpus for ' \ f'the new hold. Choose one of {", ".join(allowed_corpuses)}') if body['corpus'] == 'HANGOUTS_CHAT': query_type = 'hangoutsChatQuery' else: query_type = f'{body["corpus"].lower()}Query' body['query'][query_type] = {} if body['corpus'] == 'DRIVE': if query: try: body['query'][query_type] = json.loads(query) except ValueError as e: controlflow.system_error_exit(3, f'{str(e)}, query: {query}') elif body['corpus'] in ['GROUPS', 'MAIL']: if query: body['query'][query_type] = {'terms': query} if start_time: body['query'][query_type]['startTime'] = start_time if end_time: body['query'][query_type]['endTime'] = end_time if accounts: body['accounts'] = [] cd = __main__.buildGAPIObject('directory') account_type = 'group' if body['corpus'] == 'GROUPS' else 'user' for account in accounts: body['accounts'].append( {'accountId': __main__.convertEmailAddressToUID(account, cd, account_type)} ) holdId = gapi.call(v.matters().holds(), 'create', matterId=matterId, body=body, fields='holdId') print(f'Created hold {holdId["holdId"]}')
def call(service, function, silent_errors=False, soft_errors=False, throw_reasons=None, retry_reasons=None, **kwargs): """Executes a single request on a Google service function. Args: service: A Google service object for the desired API. function: String, The name of a service request method to execute. silent_errors: Bool, If True, error messages are suppressed when encountered. soft_errors: Bool, If True, writes non-fatal errors to stderr. throw_reasons: A list of Google HTTP error reason strings indicating the errors generated by this request should be re-thrown. All other HTTP errors are consumed. retry_reasons: A list of Google HTTP error reason strings indicating which error should be retried, using exponential backoff techniques, when the error reason is encountered. **kwargs: Additional params to pass to the request method. Returns: A response object for the corresponding Google API call. """ if throw_reasons is None: throw_reasons = [] if retry_reasons is None: retry_reasons = [] method = getattr(service, function) retries = 10 parameters = dict( list(kwargs.items()) + list(GM_Globals[GM_EXTRA_ARGS_DICT].items())) for n in range(1, retries + 1): try: return method(**parameters).execute() except googleapiclient.errors.HttpError as e: http_status, reason, message = errors.get_gapi_error_detail( e, soft_errors=soft_errors, silent_errors=silent_errors, retry_on_http_error=n < 3) if http_status == -1: # The error detail indicated that we should retry this request # We'll refresh credentials and make another pass service._http.request.credentials.refresh( transport.create_http()) continue if http_status == 0: return None is_known_error_reason = reason in [ r.value for r in errors.ErrorReason ] if is_known_error_reason and errors.ErrorReason( reason) in throw_reasons: if errors.ErrorReason( reason) in errors.ERROR_REASON_TO_EXCEPTION: raise errors.ERROR_REASON_TO_EXCEPTION[errors.ErrorReason( reason)](message) raise e if (n != retries) and ( is_known_error_reason and errors.ErrorReason(reason) in errors.DEFAULT_RETRY_REASONS + retry_reasons): controlflow.wait_on_failure(n, retries, reason) continue if soft_errors: display.print_error( f'{http_status}: {message} - {reason}{["", ": Giving up."][n > 1]}' ) return None controlflow.system_error_exit( int(http_status), f'{http_status}: {message} - {reason}') except google.auth.exceptions.RefreshError as e: handle_oauth_token_error( e, soft_errors or errors.ErrorReason.SERVICE_NOT_AVAILABLE in throw_reasons) if errors.ErrorReason.SERVICE_NOT_AVAILABLE in throw_reasons: raise errors.GapiServiceNotAvailableError(str(e)) display.print_error( f'User {GM_Globals[GM_CURRENT_API_USER]}: {str(e)}') return None except ValueError as e: if hasattr(service._http, 'cache') and service._http.cache is not None: service._http.cache = None continue controlflow.system_error_exit(4, str(e)) except (httplib2.ServerNotFoundError, RuntimeError) as e: if n != retries: service._http.connections = {} controlflow.wait_on_failure(n, retries, str(e)) continue controlflow.system_error_exit(4, str(e)) except TypeError as e: controlflow.system_error_exit(4, str(e))
def test_system_error_exit_raises_systemexit_error(self): with self.assertRaises(SystemExit): controlflow.system_error_exit(1, 'exit message')
def doUpdateCros(): cd = gapi.directory.buildGAPIObject() i, devices = getCrOSDeviceEntity(3, cd) update_body = {} action_body = {} orgUnitPath = None ack_wipe = False while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') if myarg == 'user': update_body['annotatedUser'] = sys.argv[i+1] i += 2 elif myarg == 'location': update_body['annotatedLocation'] = sys.argv[i+1] i += 2 elif myarg == 'notes': update_body['notes'] = sys.argv[i+1].replace('\\n', '\n') i += 2 elif myarg in ['tag', 'asset', 'assetid']: update_body['annotatedAssetId'] = sys.argv[i+1] i += 2 elif myarg in ['ou', 'org']: orgUnitPath = __main__.getOrgUnitItem(sys.argv[i+1]) i += 2 elif myarg == 'action': action = sys.argv[i+1].lower().replace('_', '').replace('-', '') deprovisionReason = None if action in ['deprovisionsamemodelreplace', 'deprovisionsamemodelreplacement']: action = 'deprovision' deprovisionReason = 'same_model_replacement' elif action in ['deprovisiondifferentmodelreplace', 'deprovisiondifferentmodelreplacement']: action = 'deprovision' deprovisionReason = 'different_model_replacement' elif action in ['deprovisionretiringdevice']: action = 'deprovision' deprovisionReason = 'retiring_device' elif action not in ['disable', 'reenable']: controlflow.system_error_exit(2, f'expected action of ' \ f'deprovision_same_model_replace, ' \ f'deprovision_different_model_replace, ' \ f'deprovision_retiring_device, disable or reenable,' f' got {action}') action_body = {'action': action} if deprovisionReason: action_body['deprovisionReason'] = deprovisionReason i += 2 elif myarg == 'acknowledgedevicetouchrequirement': ack_wipe = True i += 1 else: controlflow.invalid_argument_exit(sys.argv[i], "gam update cros") i = 0 count = len(devices) if action_body: if action_body['action'] == 'deprovision' and not ack_wipe: print(f'WARNING: Refusing to deprovision {count} devices because ' 'acknowledge_device_touch_requirement not specified. ' \ 'Deprovisioning a device means the device will have to ' \ 'be physically wiped and re-enrolled to be managed by ' \ 'your domain again. This requires physical access to ' \ 'the device and is very time consuming to perform for ' \ 'each device. Please add ' \ '"acknowledge_device_touch_requirement" to the GAM ' \ 'command if you understand this and wish to proceed ' \ 'with the deprovision. Please also be aware that ' \ 'deprovisioning can have an effect on your device ' \ 'license count. See ' \ 'https://support.google.com/chrome/a/answer/3523633 '\ 'for full details.') sys.exit(3) for deviceId in devices: i += 1 cur_count = __main__.currentCount(i, count) print(f' performing action {action} for {deviceId}{cur_count}') gapi.call(cd.chromeosdevices(), function='action', customerId=GC_Values[GC_CUSTOMER_ID], resourceId=deviceId, body=action_body) else: if update_body: for deviceId in devices: i += 1 current_count = __main__.currentCount(i, count) print(f' updating {deviceId}{current_count}') gapi.call(cd.chromeosdevices(), 'update', customerId=GC_Values[GC_CUSTOMER_ID], deviceId=deviceId, body=update_body) if orgUnitPath: # split moves into max 50 devices per batch for l in range(0, len(devices), 50): move_body = {'deviceIds': devices[l:l+50]} print(f' moving {len(move_body["deviceIds"])} devices to ' \ f'{orgUnitPath}') gapi.call(cd.chromeosdevices(), 'moveDevicesToOu', customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath=orgUnitPath, body=move_body)
def test_system_error_exit_prints_error_before_exiting( self, mock_print_err): with self.assertRaises(SystemExit): controlflow.system_error_exit(100, 'exit message') self.assertIn('exit message', mock_print_err.call_args[0][0])
def test_system_error_exit_raises_systemexit_with_return_code(self): with self.assertRaises(SystemExit) as context_manager: controlflow.system_error_exit(100, 'exit message') self.assertEqual(context_manager.exception.code, 100)
def getMatterItem(v, nameOrID): matterId = convertMatterNameToID(v, nameOrID) if not matterId: controlflow.system_error_exit(4, f'could not find matter {nameOrID}') return matterId
def updateHold(): v = buildGAPIObject() hold = sys.argv[3] matterId = None body = {} query = None add_accounts = [] del_accounts = [] start_time = None end_time = None i = 4 while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') if myarg == 'matter': matterId = getMatterItem(v, sys.argv[i+1]) holdId = convertHoldNameToID(v, hold, matterId) i += 2 elif myarg == 'query': query = sys.argv[i+1] i += 2 elif myarg in ['orgunit', 'ou']: body['orgUnit'] = {'orgUnitId': __main__.getOrgUnitId(sys.argv[i+1])[1]} i += 2 elif myarg in ['start', 'starttime']: start_time = utils.get_date_zero_time_or_full_time(sys.argv[i+1]) i += 2 elif myarg in ['end', 'endtime']: end_time = utils.get_date_zero_time_or_full_time(sys.argv[i+1]) i += 2 elif myarg in ['addusers', 'addaccounts', 'addgroups']: add_accounts = sys.argv[i+1].split(',') i += 2 elif myarg in ['removeusers', 'removeaccounts', 'removegroups']: del_accounts = sys.argv[i+1].split(',') i += 2 else: controlflow.invalid_argument_exit(myarg, "gam update hold") if not matterId: controlflow.system_error_exit( 3, 'you must specify a matter for the hold.') if query or start_time or end_time or body.get('orgUnit'): fields = 'corpus,query,orgUnit' old_body = gapi.call(v.matters().holds( ), 'get', matterId=matterId, holdId=holdId, fields=fields) body['query'] = old_body['query'] body['corpus'] = old_body['corpus'] if 'orgUnit' in old_body and 'orgUnit' not in body: # bah, API requires this to be sent # on update even when it's not changing body['orgUnit'] = old_body['orgUnit'] query_type = f'{body["corpus"].lower()}Query' if body['corpus'] == 'DRIVE': if query: try: body['query'][query_type] = json.loads(query) except ValueError as e: message = f'{str(e)}, query: {query}' controlflow.system_error_exit(3, message) elif body['corpus'] in ['GROUPS', 'MAIL']: if query: body['query'][query_type]['terms'] = query if start_time: body['query'][query_type]['startTime'] = start_time if end_time: body['query'][query_type]['endTime'] = end_time if body: print(f'Updating hold {hold} / {holdId}') gapi.call(v.matters().holds(), 'update', matterId=matterId, holdId=holdId, body=body) if add_accounts or del_accounts: cd = __main__.buildGAPIObject('directory') for account in add_accounts: print(f'adding {account} to hold.') add_body = {'accountId': __main__.convertEmailAddressToUID(account, cd)} gapi.call(v.matters().holds().accounts(), 'create', matterId=matterId, holdId=holdId, body=add_body) for account in del_accounts: print(f'removing {account} from hold.') accountId = __main__.convertEmailAddressToUID(account, cd) gapi.call(v.matters().holds().accounts(), 'delete', matterId=matterId, holdId=holdId, accountId=accountId)
def createExport(): v = buildGAPIObject() allowed_corpuses = gapi.get_enum_values_minus_unspecified( v._rootDesc['schemas']['Query']['properties']['corpus']['enum']) allowed_scopes = gapi.get_enum_values_minus_unspecified( v._rootDesc['schemas']['Query']['properties']['dataScope']['enum']) allowed_formats = gapi.get_enum_values_minus_unspecified( v._rootDesc['schemas']['MailExportOptions']['properties'] ['exportFormat']['enum']) export_format = 'MBOX' showConfidentialModeContent = None # default to not even set matterId = None body = {'query': {'dataScope': 'ALL_DATA'}, 'exportOptions': {}} i = 3 while i < len(sys.argv): myarg = sys.argv[i].lower().replace('_', '') if myarg == 'matter': matterId = getMatterItem(v, sys.argv[i+1]) body['matterId'] = matterId i += 2 elif myarg == 'name': body['name'] = sys.argv[i+1] i += 2 elif myarg == 'corpus': body['query']['corpus'] = sys.argv[i+1].upper() if body['query']['corpus'] not in allowed_corpuses: controlflow.expected_argument_exit( "corpus", ", ".join(allowed_corpuses), sys.argv[i+1]) i += 2 elif myarg in VAULT_SEARCH_METHODS_MAP: if body['query'].get('searchMethod'): message = f'Multiple search methods ' \ f'({", ".join(VAULT_SEARCH_METHODS_LIST)})' \ f'specified, only one is allowed' controlflow.system_error_exit(3, message) searchMethod = VAULT_SEARCH_METHODS_MAP[myarg] body['query']['searchMethod'] = searchMethod if searchMethod == 'ACCOUNT': body['query']['accountInfo'] = { 'emails': sys.argv[i+1].split(',')} i += 2 elif searchMethod == 'ORG_UNIT': body['query']['orgUnitInfo'] = { 'orgUnitId': __main__.getOrgUnitId(sys.argv[i+1])[1]} i += 2 elif searchMethod == 'SHARED_DRIVE': body['query']['sharedDriveInfo'] = { 'sharedDriveIds': sys.argv[i+1].split(',')} i += 2 elif searchMethod == 'ROOM': body['query']['hangoutsChatInfo'] = { 'roomId': sys.argv[i+1].split(',')} i += 2 else: i += 1 elif myarg == 'scope': body['query']['dataScope'] = sys.argv[i+1].upper() if body['query']['dataScope'] not in allowed_scopes: controlflow.expected_argument_exit( "scope", ", ".join(allowed_scopes), sys.argv[i+1]) i += 2 elif myarg in ['terms']: body['query']['terms'] = sys.argv[i+1] i += 2 elif myarg in ['start', 'starttime']: body['query']['startTime'] = utils.get_date_zero_time_or_full_time( sys.argv[i+1]) i += 2 elif myarg in ['end', 'endtime']: body['query']['endTime'] = utils.get_date_zero_time_or_full_time( sys.argv[i+1]) i += 2 elif myarg in ['timezone']: body['query']['timeZone'] = sys.argv[i+1] i += 2 elif myarg in ['excludedrafts']: body['query']['mailOptions'] = { 'excludeDrafts': __main__.getBoolean(sys.argv[i+1], myarg)} i += 2 elif myarg in ['driveversiondate']: body['query'].setdefault('driveOptions', {})['versionDate'] = \ utils.get_date_zero_time_or_full_time(sys.argv[i+1]) i += 2 elif myarg in ['includeshareddrives', 'includeteamdrives']: body['query'].setdefault('driveOptions', {})[ 'includeSharedDrives'] = __main__.getBoolean(sys.argv[i+1], myarg) i += 2 elif myarg in ['includerooms']: body['query']['hangoutsChatOptions'] = { 'includeRooms': __main__.getBoolean(sys.argv[i+1], myarg)} i += 2 elif myarg in ['format']: export_format = sys.argv[i+1].upper() if export_format not in allowed_formats: controlflow.expected_argument_exit( "export format", ", ".join(allowed_formats), export_format) i += 2 elif myarg in ['showconfidentialmodecontent']: showConfidentialModeContent = __main__.getBoolean(sys.argv[i+1], myarg) i += 2 elif myarg in ['region']: allowed_regions = gapi.get_enum_values_minus_unspecified( v._rootDesc['schemas']['ExportOptions']['properties'][ 'region']['enum']) body['exportOptions']['region'] = sys.argv[i+1].upper() if body['exportOptions']['region'] not in allowed_regions: controlflow.expected_argument_exit("region", ", ".join( allowed_regions), body['exportOptions']['region']) i += 2 elif myarg in ['includeaccessinfo']: body['exportOptions'].setdefault('driveOptions', {})[ 'includeAccessInfo'] = __main__.getBoolean(sys.argv[i+1], myarg) i += 2 else: controlflow.invalid_argument_exit(sys.argv[i], "gam create export") if not matterId: controlflow.system_error_exit( 3, 'you must specify a matter for the new export.') if 'corpus' not in body['query']: controlflow.system_error_exit(3, f'you must specify a corpus for the ' \ f'new export. Choose one of {", ".join(allowed_corpuses)}') if 'searchMethod' not in body['query']: controlflow.system_error_exit(3, f'you must specify a search method ' \ 'for the new export. Choose one of ' \ f'{", ".join(VAULT_SEARCH_METHODS_LIST)}') if 'name' not in body: corpus_name = body["query"]["corpus"] corpus_date = datetime.datetime.now() body['name'] = f'GAM {corpus_name} export - {corpus_date}' options_field = None if body['query']['corpus'] == 'MAIL': options_field = 'mailOptions' elif body['query']['corpus'] == 'GROUPS': options_field = 'groupsOptions' elif body['query']['corpus'] == 'HANGOUTS_CHAT': options_field = 'hangoutsChatOptions' if options_field: body['exportOptions'].pop('driveOptions', None) body['exportOptions'][options_field] = {'exportFormat': export_format} if showConfidentialModeContent is not None: body['exportOptions'][options_field][ 'showConfidentialModeContent'] = showConfidentialModeContent results = gapi.call(v.matters().exports(), 'create', matterId=matterId, body=body) print(f'Created export {results["id"]}') display.print_json(results)