Files
GoogleDriveManagement/src/gam/gapi/cloudidentity/devices.py
Ross Scroggs 3200de56cc Several fixes/updates (#1426)
* agreedToTerms is now read-only

* Fix sync devices

* assetTag if specified is part of sync device key

* Handle missing assetTags

* Leave agreedtoterms as an undocumented option

* More assetTag processing, the field is not returned from the API if it's empty

* Fix DriveFileAttribute formatting

* memberKey has been replaced by preferredMemberKey

* Correct license name

* If notdemail.txt is present, write_csv_file will not send an email
2021-10-05 08:37:09 -04:00

481 lines
19 KiB
Python

import csv
import sys
import googleapiclient
import gam
from gam.var import *
from gam import controlflow
from gam import display
from gam import fileutils
from gam import gapi
from gam import utils
from gam.gapi import errors as gapi_errors
from gam.gapi import cloudidentity as gapi_cloudidentity
from gam.gapi.directory import customer as gapi_directory_customer
def _get_device_customerid():
customer = GC_Values[GC_CUSTOMER_ID]
if customer.startswith('C'):
customer = customer[1:]
return f'customers/{customer}'
def create():
ci = gapi_cloudidentity.build_dwd()
customer = _get_device_customerid()
device_types = gapi.get_enum_values_minus_unspecified(
ci._rootDesc['schemas']['GoogleAppsCloudidentityDevicesV1Device']['properties']['deviceType']['enum'])
body = {'deviceType': '', 'serialNumber': ''}
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'serialnumber':
body['serialNumber'] = sys.argv[i+1]
i += 2
elif myarg == 'devicetype':
body['deviceType'] = sys.argv[i+1].upper()
if body['deviceType'] not in device_types:
controlflow.expected_argument_exit('device_type',
', '.join(device_types),
sys.argv[i+1])
i += 2
elif myarg in {'assettag', 'assetid'}:
body['assetTag'] = sys.argv[i+1]
i += 2
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam create device')
if not body['serialNumber'] or not body['deviceType']:
controlflow.system_error_exit(
3, 'serial_number and device_type are required arguments for "gam create device".')
result = gapi.call(ci.devices(), 'create', customer=customer, body=body)
print(f'Created device {result["response"]["name"]}')
def _get_device_name():
name = sys.argv[3]
if name == 'id':
name = sys.argv[4]
if not name.startswith('devices/'):
name = f'devices/{name}'
return name
def info():
ci = gapi_cloudidentity.build_dwd()
customer = _get_device_customerid()
name = _get_device_name()
device = gapi.call(ci.devices(), 'get', name=name, customer=customer)
device_users = gapi.get_all_pages(ci.devices().deviceUsers(), 'list',
'deviceUsers', parent=name, customer=customer)
for device_user in device_users:
parent = device_user['name']
device_user['client_states'] = gapi.get_all_pages(
ci.devices().deviceUsers().clientStates(),
'list', 'clientStates', parent=parent, customer=customer)
display.print_json(device)
print('Device Users:')
display.print_json(device_users)
def _generic_action(action, device_user=False):
ci = gapi_cloudidentity.build_dwd()
customer = _get_device_customerid()
name = _get_device_name()
# bah, inconsistencies in API
if action == 'delete':
kwargs = {'customer': customer}
else:
kwargs = {'body': {'customer': customer}}
if device_user:
endpoint = ci.devices().deviceUsers()
else:
endpoint = ci.devices()
op = gapi.call(endpoint, action, name=name, **kwargs)
print(op)
def delete():
_generic_action('delete')
def cancel_wipe():
_generic_action('cancelWipe')
def wipe():
_generic_action('wipe')
def approve_user():
_generic_action('approve', True)
def block_user():
_generic_action('block', True)
def cancel_wipe_user():
_generic_action('cancelWipe', True)
def delete_user():
_generic_action('delete', True)
def wipe_user():
_generic_action('wipe', True)
def _get_deviceuser_name():
i = 3
name = sys.argv[i]
if name == 'id':
i += 1
name = sys.argv[i]
if not name.startswith('devices/'):
name = f'devices/{name}'
return (i+1, name)
def info_state():
ci = gapi_cloudidentity.build_dwd()
gapi_directory_customer.setTrueCustomerId()
customer = _get_device_customerid()
customer_id = customer[10:]
client_id = f'{customer_id}-gam'
i, deviceuser = _get_deviceuser_name()
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'clientid':
client_id = f'{customer_id}-{sys.argv[i+1]}'
i += 2
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam info deviceuserstate')
name = f'{deviceuser}/clientStates/{client_id}'
result = gapi.call(ci.devices().deviceUsers().clientStates(), 'get',
name=name, customer=customer)
display.print_json(result)
def update_state():
ci = gapi_cloudidentity.build_dwd()
gapi_directory_customer.setTrueCustomerId()
customer = _get_device_customerid()
customer_id = customer[10:]
client_id = f'{customer_id}-gam'
body = {}
i, deviceuser = _get_deviceuser_name()
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'clientid':
client_id = f'{customer_id}-{sys.argv[i+1]}'
i += 2
elif myarg in ['assettag', 'assettags']:
body['assetTags'] = gam.shlexSplitList(sys.argv[i+1])
if body['assetTags'] == ['clear']:
# TODO: this doesn't work to clear
# existing values. Figure out why.
body['assetTags'] = [None]
i += 2
elif myarg in ['compliantstate', 'compliancestate']:
comp_states = gapi.get_enum_values_minus_unspecified(
ci._rootDesc['schemas']['GoogleAppsCloudidentityDevicesV1ClientState']['properties']['complianceState']['enum'])
body['complianceState'] = sys.argv[i+1].upper()
if body['complianceState'] not in comp_states:
controlflow.expected_argument_exit('compliant_state',
', '.join(comp_states),
sys.argv[i+1])
i += 2
elif myarg == 'customid':
body['customId'] = sys.argv[i+1]
i += 2
elif myarg == 'healthscore':
health_scores = gapi.get_enum_values_minus_unspecified(
ci._rootDesc['schemas']['GoogleAppsCloudidentityDevicesV1ClientState']['properties']['healthScore']['enum'])
body['healthScore'] = sys.argv[i+1].upper()
if body['healthScore'] == 'CLEAR':
body['healthScore'] = None
if body['healthScore'] and body['healthScore'] not in health_scores:
controlflow.expected_argument_exit('health_score',
', '.join(health_scores),
sys.argv[i+1])
i += 2
elif myarg == 'customvalue':
allowed_types = ['bool', 'number', 'string']
value_type = sys.argv[i+1].lower()
if value_type not in allowed_types:
controlflow.expected_argument_exit('custom_value',
', '.join(allowed_types),
sys.argv[i+1])
key = sys.argv[i+2]
value = sys.argv[i+3]
if value_type == 'bool':
value = gam.getBoolean(value, key)
elif value_type == 'number':
value = int(value)
body.setdefault('keyValuePairs', {})
body['keyValuePairs'][key] = {f'{value_type}Value': value}
i += 4
elif myarg in ['managedstate']:
managed_states = gapi.get_enum_values_minus_unspecified(
ci._rootDesc['schemas']['GoogleAppsCloudidentityDevicesV1ClientState']['properties']['managed']['enum'])
body['managed'] = sys.argv[i+1].upper()
if body['managed'] == 'CLEAR':
body['managed'] = None
if body['managed'] and body['managed'] not in managed_states:
controlflow.expected_argument_exit('managed_state',
', '.join(managed_states),
sys.argv[i+1])
i += 2
elif myarg in ['scorereason']:
body['scoreReason'] = sys.argv[i+1]
if body['scoreReason'] == 'clear':
body['scoreReason'] = None
i += 2
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam update deviceuserstate')
name = f'{deviceuser}/clientStates/{client_id}'
updateMask = ','.join(body.keys())
result = gapi.call(ci.devices().deviceUsers().clientStates(), 'patch',
name=name, customer=customer, updateMask=updateMask, body=body)
display.print_json(result)
def print_():
ci = gapi_cloudidentity.build_dwd()
customer = _get_device_customerid()
parent = 'devices/-'
device_filter = None
get_device_users = True
view = None
orderByList = []
titles = []
csvRows = []
todrive = False
sortHeaders = False
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg in ['filter', 'query']:
device_filter = sys.argv[i+1]
i += 2
elif myarg == 'company':
view = 'COMPANY_INVENTORY'
i += 1
elif myarg == 'personal':
view = 'USER_ASSIGNED_DEVICES'
i += 1
elif myarg == 'nocompanydevices':
view = 'USER_ASSIGNED_DEVICES'
i += 1
elif myarg == 'nopersonaldevices':
view = 'COMPANY_INVENTORY'
i += 1
elif myarg == 'nodeviceusers':
get_device_users = False
i += 1
elif myarg == 'todrive':
todrive = True
i += 1
elif myarg == 'orderby':
fieldName = sys.argv[i + 1].lower()
i += 2
if fieldName in DEVICE_ORDERBY_CHOICES_MAP:
fieldName = DEVICE_ORDERBY_CHOICES_MAP[fieldName]
orderBy = ''
if i < len(sys.argv):
orderBy = sys.argv[i].lower()
if orderBy in SORTORDER_CHOICES_MAP:
orderBy = SORTORDER_CHOICES_MAP[orderBy]
i += 1
if orderBy != 'DESCENDING':
orderByList.append(fieldName)
else:
orderByList.append(f'{fieldName} desc')
else:
controlflow.expected_argument_exit(
'orderby', ', '.join(sorted(DEVICE_ORDERBY_CHOICES_MAP)),
fieldName)
elif myarg == 'sortheaders':
sortHeaders = True
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam print devices')
view_name_map = {
None: 'Devices',
'COMPANY_INVENTORY': 'Company Devices',
'USER_ASSIGNED_DEVICES': 'Personal Devices',
}
if orderByList:
orderBy = ','.join(orderByList)
else:
orderBy = None
devices = []
page_message = gapi.got_total_items_msg(view_name_map[view], '...\n')
devices += gapi.get_all_pages(ci.devices(), 'list', 'devices',
customer=customer, page_message=page_message,
pageSize=100, filter=device_filter, view=view, orderBy=orderBy)
if get_device_users:
page_message = gapi.got_total_items_msg('Device Users', '...\n')
device_users = gapi.get_all_pages(ci.devices().deviceUsers(), 'list',
'deviceUsers', customer=customer, parent=parent,
page_message=page_message, pageSize=20, filter=device_filter)
for device_user in device_users:
for device in devices:
if device_user.get('name').startswith(device.get('name')):
if 'users' not in device:
device['users'] = []
device['users'].append(device_user)
break
for device in devices:
device = utils.flatten_json(device)
for a_key in device:
if a_key not in titles:
titles.append(a_key)
csvRows.append(device)
if sortHeaders:
display.sort_csv_titles(['name',], titles)
display.write_csv_file(csvRows, titles, 'Devices', todrive)
def sync():
ci = gapi_cloudidentity.build_dwd()
device_types = gapi.get_enum_values_minus_unspecified(
ci._rootDesc['schemas']['GoogleAppsCloudidentityDevicesV1Device']['properties']['deviceType']['enum'])
customer = _get_device_customerid()
device_filter = None
csv_file = None
serialnumber_column = 'serialNumber'
devicetype_column = 'deviceType'
static_devicetype = None
assettag_column = None
unassigned_missing_action = 'delete'
assigned_missing_action = 'donothing'
missing_actions = ['delete', 'wipe', 'donothing']
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg in ['filter', 'query']:
device_filter = sys.argv[i+1]
i += 2
elif myarg == 'csvfile':
csv_file = sys.argv[i+1]
i += 2
elif myarg == 'serialnumbercolumn':
serialnumber_column = sys.argv[i+1]
i += 2
elif myarg == 'devicetypecolumn':
devicetype_column = sys.argv[i+1]
i += 2
elif myarg == 'staticdevicetype':
static_devicetype = sys.argv[i+1].upper()
if static_devicetype not in device_types:
controlflow.expected_argument_exit('device_type',
', '.join(device_types),
sys.argv[i+1])
i += 2
elif myarg in {'assettagcolumn', 'assetidcolumn'}:
assettag_column = sys.argv[i+1]
i += 2
elif myarg == 'unassignedmissingaction':
unassigned_missing_action = sys.argv[i+1].lower().replace('_', '')
if unassigned_missing_action not in missing_actions:
controlflow.expected_argument_exit('unassigned_missing_action',
', '.join(missing_actions),
sys.argv[i+1])
i += 2
elif myarg == 'assignedmissingaction':
assigned_missing_action = sys.argv[i+1].lower().replace('_', '')
if assigned_missing_action not in missing_actions:
controlflow.expected_argument_exit('assigned_missing_action',
', '.join(missing_actions),
sys.argv[i+1])
i += 2
else:
controlflow.invalid_argument_exit(sys.argv[i], 'gam sync devices')
if not csv_file:
controlflow.system_error_exit(
3, 'csvfile is a required argument for "gam sync devices".')
f = fileutils.open_file(csv_file)
input_file = csv.DictReader(f, restval='')
if serialnumber_column not in input_file.fieldnames:
controlflow.csv_field_error_exit(serialnumber_column, input_file.fieldnames)
if not static_devicetype and devicetype_column not in input_file.fieldnames:
controlflow.csv_field_error_exit(devicetype_column, input_file.fieldnames)
if assettag_column and assettag_column not in input_file.fieldnames:
controlflow.csv_field_error_exit(assettag_column, input_file.fieldnames)
local_devices = {}
for row in input_file:
# upper() is very important to comparison since Google
# always return uppercase serials
local_device = {'serialNumber': row[serialnumber_column].strip().upper()}
if static_devicetype:
local_device['deviceType'] = static_devicetype
else:
local_device['deviceType'] = row[devicetype_column].strip()
sndt = f"{local_device['serialNumber']}-{local_device['deviceType']}"
if assettag_column:
local_device['assetTag'] = row[assettag_column].strip()
sndt += f"-{local_device['assetTag']}"
local_devices[sndt] = local_device
fileutils.close_file(f)
page_message = gapi.got_total_items_msg('Company Devices', '...\n')
device_fields = ['serialNumber', 'deviceType', 'lastSyncTime', 'name']
if assettag_column:
device_fields.append('assetTag')
fields = f'nextPageToken,devices({",".join(device_fields)})'
remote_devices = {}
remote_device_map = {}
result = gapi.get_all_pages(ci.devices(), 'list', 'devices',
customer=customer, page_message=page_message,
pageSize=100, filter=device_filter, view='COMPANY_INVENTORY', fields=fields)
for remote_device in result:
sn = remote_device['serialNumber']
last_sync = remote_device.pop('lastSyncTime', NEVER_TIME_NOMS)
name = remote_device.pop('name')
sndt = f"{remote_device['serialNumber']}-{remote_device['deviceType']}"
if assettag_column:
if 'assetTag' not in remote_device:
remote_device['assetTag'] = ''
sndt += f"-{remote_device['assetTag']}"
remote_devices[sndt] = remote_device
remote_device_map[sndt] = {'name': name}
if last_sync == NEVER_TIME_NOMS:
remote_device_map[sndt]['unassigned'] = True
devices_to_add = []
for sndt, device in iter(local_devices.items()):
if sndt not in remote_devices:
devices_to_add.append(device)
missing_devices = []
for sndt, device in iter(remote_devices.items()):
if sndt not in local_devices:
missing_devices.append(device)
print(f'Need to add {len(devices_to_add)} and remove {len(missing_devices)} devices...')
for add_device in devices_to_add:
print(f'Creating {add_device["serialNumber"]}')
try:
result = gapi.call(ci.devices(), 'create', customer=customer,
throw_reasons=[gapi_errors.ErrorReason.FOUR_O_NINE], body=add_device)
print(f' created {result["response"]["deviceType"]} device {result["response"]["name"]} with serial {result["response"]["serialNumber"]}')
except googleapiclient.errors.HttpError:
print(f' {add_device["serialNumber"]} already exists')
for missing_device in missing_devices:
sn = missing_device['serialNumber']
sndt = f"{sn}-{missing_device['deviceType']}"
if assettag_column:
sndt += f"-{missing_device['assetTag']}"
name = remote_device_map[sndt]['name']
unassigned = remote_device_map[sndt].get('unassigned')
action = unassigned_missing_action if unassigned else assigned_missing_action
if action == 'donothing':
pass
else:
if action == 'delete':
kwargs = {'customer': customer}
else:
kwargs = {'body': {'customer': customer}}
gapi.call(ci.devices(), action,
name=name, **kwargs)
print(f'{action}d {sn}')