diff --git a/docs/Chrome-Policies.md b/docs/Chrome-Policies.md index c1923da0..ffc19afa 100644 --- a/docs/Chrome-Policies.md +++ b/docs/Chrome-Policies.md @@ -199,6 +199,16 @@ gam update chromepolicy convertcrnl chrome.devices.DisabledDeviceReturnInstructi ``` ### Examples +Restrict use of Chromebooks in an OU to a specific list of users. +``` +gam update chromepolicy chrome.devices.SignInRestriction deviceAllowNewUsers RESTRICTED_LIST userAllowlist "user1@domain.com,user2@domain.com" ou "" +``` + +Restrict use of Chromebooks in an OU to users in a specific domain. +``` +gam update chromepolicy chrome.devices.SignInRestriction deviceAllowNewUsers RESTRICTED_LIST userAllowlist "*@domain.com" ou "" +``` + Restrict student users from adding additional printers and set default printing to black and white. ``` gam update chromepolicy chrome.users.UserPrintersAllowed userPrintersAllowed false chrome.users.DefaultPrintColor printingColorDefault MONOCHROME orgunit "/Students" diff --git a/docs/GamUpdates.md b/docs/GamUpdates.md index f69c9226..e161c3ad 100644 --- a/docs/GamUpdates.md +++ b/docs/GamUpdates.md @@ -10,6 +10,13 @@ Add the `-s` option to the end of the above commands to suppress creating the `g See [Downloads-Installs](https://github.com/taers232c/GAMADV-XTD3/wiki/Downloads-Installs) for Windows or other options, including manual installation +### 6.79.04 + +Added options `filename ` and `movetoou ` to `gam check ou ` +that causes GAM to create a batch file of GAM commands that will move any remaining items +in `ou ` to `movetoou `; executing the batch file will then allow +`ou ` to be deleted if desired. + ### 6.79.03 Added column|field `assignedToUnknown` to `gam print|show admins` that will be True when diff --git a/docs/How-to-Upgrade-from-Standard-GAM.md b/docs/How-to-Upgrade-from-Standard-GAM.md index e36a0b52..d1f5198e 100644 --- a/docs/How-to-Upgrade-from-Standard-GAM.md +++ b/docs/How-to-Upgrade-from-Standard-GAM.md @@ -251,7 +251,7 @@ writes the credentials into the file oauth2.txt. admin@server:/Users/admin$ rm -f /Users/admin/GAMConfig/oauth2.txt admin@server:/Users/admin$ gam version WARNING: Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: /Users/admin/GAMConfig/oauth2.txt, Not Found -GAMADV-XTD3 6.79.03 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.79.04 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.12.4 64-bit final MacOS Sonoma 14.5 x86_64 @@ -923,7 +923,7 @@ writes the credentials into the file oauth2.txt. C:\>del C:\GAMConfig\oauth2.txt C:\>gam version WARNING: Config File: C:\GAMConfig\gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: C:\GAMConfig\oauth2.txt, Not Found -GAMADV-XTD3 6.79.03 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.79.04 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.12.4 64-bit final Windows-10-10.0.17134 AMD64 diff --git a/docs/Organizational-Units.md b/docs/Organizational-Units.md index ea2a1444..013ec83c 100644 --- a/docs/Organizational-Units.md +++ b/docs/Organizational-Units.md @@ -295,6 +295,7 @@ Only items directly within the OU are counted, items in sub-OUs are not counted. gam check org|ou [todrive *] [*|(fields )] + [filename ] [movetoou ] [formatjson [quotechar ]] ``` By default, GAM checks each of the five items; you can select specfic fields @@ -309,6 +310,17 @@ When using the `formatjson` option, double quotes are used extensively in the da The `quotechar ` option allows you to choose an alternate quote character, single quote for instance, that makes for readable/processable output. `quotechar` defaults to `gam.cfg/csv_output_quote_char`. When uploading CSV files to Google, double quote `"` should be used. +If `movetoou ` is specified, GAM will create a batch file of GAM commands that will move any remaining items +in `ou ` to `movetoou `. + +By default, the batch file will be named `CleanOuBatch.txt` and will be created in `gam.cfg/drive_dir`. +This can be overridden with `filename `. + +You can inspect the file and execute it if desired; substitute actual filenames as desired. +``` +gam redirect stdout CleanOuLog.txt multiproces redirect stderr stdout batch CleanOuBatch.txt +``` + ## Special case handling for large number of organizational units By default, the `print orgs` and `show orgtree` commands issue a single API call to get the diff --git a/docs/Version-and-Help.md b/docs/Version-and-Help.md index 32e0544b..53c7c7f1 100644 --- a/docs/Version-and-Help.md +++ b/docs/Version-and-Help.md @@ -3,7 +3,7 @@ Print the current version of Gam with details ``` gam version -GAMADV-XTD3 6.79.03 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.79.04 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.12.4 64-bit final MacOS Sonoma 14.5 x86_64 @@ -15,7 +15,7 @@ Time: 2023-06-02T21:10:00-07:00 Print the current version of Gam with details and time offset information ``` gam version timeoffset -GAMADV-XTD3 6.79.03 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.79.04 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.12.4 64-bit final MacOS Sonoma 14.5 x86_64 @@ -27,7 +27,7 @@ Your system time differs from www.googleapis.com by less than 1 second Print the current version of Gam with extended details and SSL information ``` gam version extended -GAMADV-XTD3 6.79.03 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.79.04 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.12.4 64-bit final MacOS Sonoma 14.5 x86_64 @@ -64,7 +64,7 @@ MacOS High Sierra 10.13.6 x86_64 Path: /Users/Admin/bin/gamadv-xtd3 Version Check: Current: 5.35.08 - Latest: 6.79.03 + Latest: 6.79.04 echo $? 1 ``` @@ -72,7 +72,7 @@ echo $? Print the current version number without details ``` gam version simple -6.79.03 +6.79.04 ``` In Linux/MacOS you can do: ``` @@ -82,7 +82,7 @@ echo $VER Print the current version of Gam and address of this Wiki ``` gam help -GAM 6.79.03 - https://github.com/taers232c/GAMADV-XTD3 +GAM 6.79.04 - https://github.com/taers232c/GAMADV-XTD3 Ross Scroggs Python 3.12.4 64-bit final MacOS Sonoma 14.5 x86_64 diff --git a/src/GamCommands.txt b/src/GamCommands.txt index 82547d9f..f7fa70aa 100644 --- a/src/GamCommands.txt +++ b/src/GamCommands.txt @@ -4225,8 +4225,9 @@ gam show orgtree [fromparent ] [batchsuborgs []] users ::= "(,)*" -gam check org|ou [todrive *] +gam check ou|org [todrive *] [*|(fields )] + [filename ] [movetoou ] [formatjson [quotechar ]] # Printers diff --git a/src/GamUpdate.txt b/src/GamUpdate.txt index 3d89e7b5..c3da6d18 100644 --- a/src/GamUpdate.txt +++ b/src/GamUpdate.txt @@ -2,6 +2,13 @@ Merged GAM-Team version +6.79.04 + +Added options `filename ` and `movetoou ` to `gam check ou ` +that causes GAM to create a batch file of GAM commands that will move any remaining items +in `ou ` to `movetoou `; executing the batch file will then allow +`ou ` to be deleted if desired. + 6.79.03 Added column|field `assignedToUnknown` to `gam print|show admins` that will be True when diff --git a/src/gam/__init__.py b/src/gam/__init__.py index 3f52c2e1..bb1a4913 100755 --- a/src/gam/__init__.py +++ b/src/gam/__init__.py @@ -2810,6 +2810,15 @@ def entityModifierNewValueKeyValueActionPerformed(entityValueList, modifier, new def cleanFilename(filename): return sanitize_filename(filename, '_') +def setFilePath(fileName): + if fileName.startswith('./') or fileName.startswith('.\\'): + fileName = os.path.join(os.getcwd(), fileName[2:]) + else: + fileName = os.path.expanduser(fileName) + if not os.path.isabs(fileName): + fileName = os.path.join(GC.Values[GC.DRIVE_DIR], fileName) + return fileName + def uniqueFilename(targetFolder, filetitle, overwrite, extension=None): filename = filetitle y = 0 @@ -3773,12 +3782,7 @@ def SetGlobalVariables(): def _setCSVFile(fileName, mode, encoding, writeHeader, multi): if fileName != '-': - if fileName.startswith('./') or fileName.startswith('.\\'): - fileName = os.path.join(os.getcwd(), fileName[2:]) - else: - fileName = os.path.expanduser(fileName) - if not os.path.isabs(fileName): - fileName = os.path.join(GC.Values[GC.DRIVE_DIR], fileName) + fileName = setFilePath(fileName) GM.Globals[GM.CSVFILE][GM.REDIRECT_NAME] = fileName GM.Globals[GM.CSVFILE][GM.REDIRECT_MODE] = mode GM.Globals[GM.CSVFILE][GM.REDIRECT_ENCODING] = encoding @@ -3799,12 +3803,7 @@ def SetGlobalVariables(): else: GM.Globals[stdtype][GM.REDIRECT_FD] = os.fdopen(os.dup(sys.stderr.fileno()), mode, encoding=GM.Globals[GM.SYS_ENCODING]) else: - if fileName.startswith('./') or fileName.startswith('.\\'): - fileName = os.path.join(os.getcwd(), fileName[2:]) - else: - fileName = os.path.expanduser(fileName) - if not os.path.isabs(fileName): - fileName = os.path.join(GC.Values[GC.DRIVE_DIR], fileName) + fileName = setFilePath(fileName) if multi and mode == DEFAULT_FILE_WRITE_MODE: deleteFile(fileName) mode = DEFAULT_FILE_APPEND_MODE @@ -17773,20 +17772,32 @@ ORG_ITEMS_FIELD_MAP = { 'users': 'users', } -# gam check org|ou [todrive *] +# gam check ou|org [todrive *] # [*|(fields )] +# [filename ] [movetoou ] # [formatjson [quotechar ]] def doCheckOrgUnit(): + def writeCommandInfo(field): + nonlocal commitBatch + if commitBatch: + f.write(f'{Cmd.COMMIT_BATCH_CMD}\n') + else: + commitBatch = True + f.write(f'{Cmd.PRINT_CMD} Move {field} from {orgUnitPath} to {moveToOrgUnitPath}\n') + cd = buildGAPIObject(API.DIRECTORY) csvPF = CSVPrintFile(['orgUnitPath', 'orgUnitId', 'empty']) FJQC = FormatJSONQuoteChar(csvPF) - orgUnitPath = None + f = orgUnitPath = None fieldsList = [] titlesList = [] status, orgUnitPath, orgUnitId = checkOrgUnitPathExists(cd, getOrgUnitItem()) - orgUnitPathLower = orgUnitPath.lower() if not status: entityDoesNotExistExit(Ent.ORGANIZATIONAL_UNIT, orgUnitPath) + orgUnitPathLower = orgUnitPath.lower() + fileName = 'CleanOuBatch.txt' + moveToOrgUnitPath = moveToOrgUnitPathLower = None + commitBatch = False while Cmd.ArgumentsRemaining(): myarg = getArgument() if csvPF and myarg == 'todrive': @@ -17799,12 +17810,26 @@ def doCheckOrgUnit(): fieldsList.append(field) else: invalidChoiceExit(field, list(ORG_ITEMS_FIELD_MAP), True) + elif myarg == 'filename': + fileName = setFilePath(getString(Cmd.OB_FILE_NAME)) + elif myarg == 'movetoou': + movetoouLocation = Cmd.Location() + status, moveToOrgUnitPath, _ = checkOrgUnitPathExists(cd, getOrgUnitItem()) + moveToOrgUnitPathLower = moveToOrgUnitPath.lower() + if not status: + entityDoesNotExistExit(Ent.ORGANIZATIONAL_UNIT, moveToOrgUnitPath) else: FJQC.GetFormatJSONQuoteChar(myarg, True) - if orgUnitPath is None: - missingArgumentExit('orgunit ') if not fieldsList: fieldsList = ORG_ITEMS_FIELD_MAP.keys() + if moveToOrgUnitPath is not None: + Cmd.SetLocation(movetoouLocation) + if orgUnitPathLower == moveToOrgUnitPathLower: + usageErrorExit(Msg.OU_AND_MOVETOOU_CANNOT_BE_IDENTICAL.format(orgUnitPath, moveToOrgUnitPath)) + if 'subous' in fieldsList and moveToOrgUnitPathLower.startswith(orgUnitPathLower): + usageErrorExit(Msg.OU_SUBOUS_CANNOT_BE_MOVED_TO_MOVETOOU.format(orgUnitPath, moveToOrgUnitPath)) + fileName = setFilePath(fileName) + f = openFile(fileName, DEFAULT_FILE_WRITE_MODE) orgUnitItemCounts = {} for field in sorted(fieldsList): title = ORG_ITEMS_FIELD_MAP[field] @@ -17825,6 +17850,9 @@ def doCheckOrgUnit(): fields='nextPageToken,browsers(deviceId)') for browsers in feed: orgUnitItemCounts['browsers'] += len(browsers) + if f is not None and orgUnitItemCounts['browsers'] > 0: + writeCommandInfo('browsers') + f.write(f'gam move browsers ou {moveToOrgUnitPath} browserou {orgUnitPath}\n') except (GAPI.invalidInput, GAPI.forbidden) as e: entityActionFailedWarning([Ent.CHROME_BROWSER, None], str(e)) except GAPI.invalidOrgunit as e: @@ -17854,6 +17882,9 @@ def doCheckOrgUnit(): _finalizeGAPIpagesResult(pageMessage) printGotAccountEntities(totalItems) break + if f is not None and orgUnitItemCounts['devices'] > 0: + writeCommandInfo('devices') + f.write(f'gam update ou {moveToOrgUnitPath} add cros_ou {orgUnitPath}\n') except GAPI.invalidInput as e: message = str(e) # Invalid Input: xyz - Check for invalid pageToken!! @@ -17881,8 +17912,18 @@ def doCheckOrgUnit(): customer=_getCustomersCustomerIdWithC(), filter="type == 'shared_drive'") orgUnitItemCounts['sharedDrives'] = len(sds) + if f is not None and orgUnitItemCounts['sharedDrives'] > 0: + writeCommandInfo('Shared Drives') + for sd in sds: + name = sd['name'].split(';')[1] + f.write(f'gam update shareddrive {name} ou {moveToOrgUnitPath}\n') if 'subous' in fieldsList: - orgUnitItemCounts['subOus'] = len(_getOrgUnits(cd, orgUnitPath, ['orgUnitPath'], 'children', False, False, None, None)) + subOus = _getOrgUnits(cd, orgUnitPath, ['orgUnitPath'], 'children', False, False, None, None) + orgUnitItemCounts['subOus'] = len(subOus) + if f is not None and orgUnitItemCounts['subOus'] > 0: + writeCommandInfo('Sub OrgUnit') + for ou in subOus: + f.write(f'gam update ou {ou["orgUnitPath"]} parent {moveToOrgUnitPath}\n') if 'users' in fieldsList: printGettingAllEntityItemsForWhom(Ent.USER, orgUnitPath, entityType=Ent.ORGANIZATIONAL_UNIT) pageMessage = getPageMessageForWhom() @@ -17897,9 +17938,15 @@ def doCheckOrgUnit(): for user in users: if orgUnitPathLower == user.get('orgUnitPath', '').lower(): orgUnitItemCounts['users'] += 1 + if f is not None and orgUnitItemCounts['users'] > 0: + writeCommandInfo('users') + f.write(f'gam update ou {moveToOrgUnitPath} add ou {orgUnitPath}\n') except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.invalidInput, GAPI.badRequest, GAPI.backendError, GAPI.invalidCustomerId, GAPI.loginRequired, GAPI.resourceNotFound, GAPI.forbidden): checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitPath) + if f is not None: + closeFile(f) + writeStderr(Msg.GAM_BATCH_FILE_WRITTEN.format(fileName)) empty = True for count in orgUnitItemCounts.values(): if count > 0: @@ -17915,7 +17962,7 @@ def doCheckOrgUnit(): csvPF.writeCSVfile(f'OrgUnit {orgUnitPath} Item Counts') if not empty and GM.Globals[GM.SYSEXITRC] == 0: setSysExitRC(ORGUNIT_NOT_EMPTY_RC) - + ALIAS_TARGET_TYPES = ['user', 'group', 'target'] # gam create aliases|nicknames user|group|target | @@ -68384,8 +68431,8 @@ def forwardMessagesThreads(users, entityType): if not gmail: continue service = gmail.users().messages() if entityType == Ent.MESSAGE else gmail.users().threads() - try: - if parameters['messageEntity'] is None: + if parameters['messageEntity'] is None: + try: printGettingAllEntityItemsForWhom(entityType, user, i, count) listResult = callGAPIpages(service, 'list', parameters['listType'], pageMessage=getPageMessageForWhom(), maxItems=parameters['maxItems'], @@ -68393,12 +68440,12 @@ def forwardMessagesThreads(users, entityType): userId='me', q=parameters['query'], fields=parameters['fields'], includeSpamTrash=includeSpamTrash, maxResults=GC.Values[GC.MESSAGE_MAX_RESULTS]) entityIds = [entity['id'] for entity in listResult] - except (GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.invalid, GAPI.invalidArgument) as e: - entityActionFailedWarning([Ent.USER, user], str(e), i, count) - continue - except (GAPI.serviceNotAvailable, GAPI.badRequest): - entityServiceNotApplicableWarning(Ent.USER, user, i, count) - continue + except (GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.invalid, GAPI.invalidArgument) as e: + entityActionFailedWarning([Ent.USER, user], str(e), i, count) + continue + except (GAPI.serviceNotAvailable, GAPI.badRequest): + entityServiceNotApplicableWarning(Ent.USER, user, i, count) + continue jcount = len(entityIds) if jcount == 0: entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.NO_ENTITIES_MATCHED.format(Ent.Plural(entityType)), i, count) diff --git a/src/gam/gamlib/glmsgs.py b/src/gam/gamlib/glmsgs.py index 9e44999a..ffa092d2 100644 --- a/src/gam/gamlib/glmsgs.py +++ b/src/gam/gamlib/glmsgs.py @@ -258,6 +258,7 @@ FORMAT_NOT_AVAILABLE = 'Format ({0}) not available' FORMAT_NOT_DOWNLOADABLE = 'Format not downloadable' FROM = 'From' FULL_PATH_MUST_START_WITH_DRIVE = 'fullpath must start with {0} or {1}' +GAM_BATCH_FILE_WRITTEN = 'GAM batch file {0} written\n' GAM_LATEST_VERSION_NOT_AVAILABLE = 'GAM Latest Version information not available' GAM_OUT_OF_MEMORY = 'GAM has run out of memory. If this is a large Google Workspace instance, you should use a 64-bit version of GAM on Windows or a 64-bit version of Python on other systems.' GENERATING_NEW_PRIVATE_KEY = 'Generating new private key' @@ -420,6 +421,8 @@ ONLY_ONE_DEVICE_SELECTION_ALLOWED = 'Only one device selection allowed, filter = ONLY_ONE_JSON_RANGE_ALLOWED = 'Only one range/json allowed' ONLY_ONE_OWNER_ALLOWED = 'Only one owner allowed' OR = 'or' +OU_AND_MOVETOOU_CANNOT_BE_IDENTICAL = 'ou {0} can not be be identical to movetoou {1}' +OU_SUBOUS_CANNOT_BE_MOVED_TO_MOVETOOU = 'ou {0} sub OUs can not be be moved to movetoou {1}' PERMISSION_DENIED = 'The caller does not have permission' PLEASE_CORRECT_YOUR_SYSTEM_TIME = 'Please correct your system time.' PLEASE_ENTER_A_OR_M = 'Please enter a or m ...\n'