def get(self): """Process an Action task with the correct Action class.""" audit_hours = config_model.Config.get('shelf_audit_interval') earliest_time = ( datetime.datetime.utcnow() - datetime.timedelta(hours=audit_hours)) # pylint: disable=g-explicit-bool-comparison, g-equals-none global_setting_query = shelf_model.Shelf.query( shelf_model.Shelf.audit_notification_enabled == True, # pylint: disable=singleton-comparison shelf_model.Shelf.audit_requested == False, # pylint: disable=singleton-comparison shelf_model.Shelf.last_audit_time < earliest_time, shelf_model.Shelf.audit_interval_override == None) # pylint: disable=singleton-comparison override_query = shelf_model.Shelf.query( shelf_model.Shelf.audit_interval_override != None, shelf_model.Shelf.audit_notification_enabled == True, # pylint: disable=singleton-comparison shelf_model.Shelf.audit_requested == False) # pylint: disable=singleton-comparison # pylint: enable=g-explicit-bool-comparison, g-equals-none override_shelves = [] for shelf in override_query.fetch(): override_earliest_time = ( datetime.datetime.utcnow() - datetime.timedelta( hours=shelf.audit_interval_override)) if ( shelf.last_audit_time and shelf.last_audit_time < override_earliest_time): override_shelves.append(shelf) for shelf in override_shelves + global_setting_query.fetch(): events.raise_event(event_name='shelf_needs_audit', shelf=shelf)
def loan_assign(self, user_email): """Assigns a device to a user. Args: user_email: str, email address of the user to whom the device should be assigned. Returns: The key of the datastore record. Raises: AssignmentError: if the device is not enrolled. """ if not self.enrolled: raise AssignmentError('Cannot assign an unenrolled device.') if self.assigned_user and self.assigned_user != user_email: self._loan_return(user_email) self.assigned_user = user_email self.assignment_date = datetime.datetime.utcnow() self.mark_pending_return_date = None self.shelf = None self.due_date = self.calculate_return_dates().default self.move_to_default_ou(user_email=user_email) self.put() self.stream_to_bq(user_email, 'Beginning new loan.') events.raise_event('device_loan_assign', device=self) return self.key
def _remind_for_devices(self): """Find devices marked as being in a remindable state and raise event.""" for device in device_model.Device.query( device_model.Device.next_reminder.time <= datetime.datetime.utcnow()).fetch(): logging.info(_DEVICE_REMINDING_NOW_MSG, device.identifier, device.next_reminder.level) events.raise_event(event_name=event_models.ReminderEvent.make_name( device.next_reminder.level), device=device)
def disable(self, user_email): """Marks a shelf as disabled. Args: user_email: str, email of the user disabling the shelf. """ self.enabled = False logging.info(_DISABLE_MSG, self.name) self.put() events.raise_event('shelf_disable', shelf=self) self.stream_to_bq(user_email, _DISABLE_MSG % self.name)
def get(self): """Process an Action task with the correct Action class.""" custom_events = event_models.CustomEvent.get_all_enabled() for custom_event in custom_events: for entity in custom_event.get_matching_entities(): device = (entity if custom_event.model.lower() == 'device' else None) shelf = (entity if custom_event.model.lower() == 'shelf' else None) events.raise_event(event_name=custom_event.name, device=device, shelf=shelf)
def audit(self, user_email): """Marks a shelf audited. Args: user_email: str, email of the user auditing the shelf. """ self.last_audit_time = datetime.datetime.utcnow() self.last_audit_by = user_email self.audit_requested = False logging.info(_AUDIT_MSG, self.name) self.put() events.raise_event('shelf_audited', shelf=self) self.stream_to_bq(user_email, _AUDIT_MSG % self.name)
def _remind_for_devices(self): """Find devices marked as being in a remindable state and raise event.""" for device in device_model.Device.query( device_model.Device.next_reminder.time <= datetime.datetime.utcnow()).fetch(): logging.info(_DEVICE_REMINDING_NOW_MSG, device.identifier, device.next_reminder.level) try: events.raise_event( event_name=event_models.ReminderEvent.make_name( device.next_reminder.level), device=device) except events.EventActionsError as err: # We log the error so that a single device does not disrupt all other # devices that need reminders set. logging.error(_EVENT_ACTION_ERROR_MSG, err)
def _loan_return(self, user_email): """Returns a device in a loan. Args: user_email: str, user_email of the user initiating the return. Returns: The key of the datastore record. """ event_action = 'device_loan_return' try: self = events.raise_event(event_action, device=self) except events.EventActionsError as err: # For any action that is implemented for device_loan_return that is # required for the rest of the logic, an error should be raised. If all # actions are not required, eg sending a notification email only, the # error should only be logged. logging.error(_EVENT_ACTION_ERROR_MSG, event_action, err) if self.lost: self.lost = False if self.locked: self.unlock(user_email) self.assigned_user = None self.assignment_date = None self.due_date = None self.mark_pending_return_date = None self.move_to_default_ou(user_email=user_email) self.last_reminder = None self.next_reminder = None self.put() self.stream_to_bq( user_email, 'Marking device %s as returned.' % self.identifier) return self.key
def get(self): """Process an Action task with the correct Action class.""" if config_model.Config.get('shelf_audit'): audit_hours = config_model.Config.get('shelf_audit_interval') earliest_time = (datetime.datetime.utcnow() - datetime.timedelta(hours=audit_hours)) # pylint: disable=g-explicit-bool-comparison, g-equals-none global_setting_query = shelf_model.Shelf.query( shelf_model.Shelf.audit_notification_enabled == True, # pylint: disable=singleton-comparison shelf_model.Shelf.audit_requested == False, # pylint: disable=singleton-comparison shelf_model.Shelf.last_audit_time != None, shelf_model.Shelf.last_audit_time < earliest_time, shelf_model.Shelf.audit_interval_override == None) # pylint: disable=singleton-comparison override_query = shelf_model.Shelf.query( shelf_model.Shelf.audit_interval_override != None, shelf_model.Shelf.audit_notification_enabled == True, # pylint: disable=singleton-comparison shelf_model.Shelf.audit_requested == False) # pylint: disable=singleton-comparison # pylint: enable=g-explicit-bool-comparison, g-equals-none override_shelves = [] for shelf in override_query.fetch(): override_earliest_time = ( datetime.datetime.utcnow() - datetime.timedelta(hours=shelf.audit_interval_override)) if (shelf.last_audit_time and shelf.last_audit_time < override_earliest_time): override_shelves.append(shelf) for shelf in override_shelves + global_setting_query.fetch(): try: events.raise_event(event_name='shelf_needs_audit', shelf=shelf) except events.EventActionsError as err: # We catch the event error and only log that error so that a single # shelf will not disrupt requesting audit on all other shelves that # may need to be audited. logging.error( 'Failed to request audit for shelf %r because the following ' 'error occurred: %s', shelf.identifier, err) else: logging.warning('Shelf audit reminders are currently disabled.')
def test_raise_event(self, mock_geteventactions, mock_taskqueueadd, mock_loginfo): """Tests raising an Action if the Event is configured for Actions.""" self.testbed.raise_event_patcher.stop( ) # Disable patcher; use real method. # No Actions configured for the Event. mock_geteventactions.return_value = [] events.raise_event('sample_event') mock_loginfo.assert_called_with(events._NO_ACTIONS_MSG, 'sample_event') mock_geteventactions.return_value = ['action1', 'action2'] test_device = device_model.Device(chrome_device_id='4815162342', serial_number='123456') test_shelf = shelf_model.Shelf(capacity=42, location='Helpdesk 123') expected_payload1 = pickle.dumps({ 'action_name': 'action1', 'device': test_device, 'shelf': test_shelf }) expected_payload2 = pickle.dumps({ 'action_name': 'action2', 'device': test_device, 'shelf': test_shelf }) events.raise_event('sample_event', device=test_device, shelf=test_shelf) self.testbed.raise_event_patcher.start( ) # Because cleanup will stop(). expected_calls = [ mock.call(queue_name='process-action', payload=expected_payload1, target='default'), mock.call(queue_name='process-action', payload=expected_payload2, target='default') ] mock_taskqueueadd.assert_has_calls(expected_calls)
def get(self): """Process an Action task with the correct Action class.""" custom_events = event_models.CustomEvent.get_all_enabled() for custom_event in custom_events: for entity in custom_event.get_matching_entities(): device = (entity if custom_event.model.lower() == 'device' else None) shelf = (entity if custom_event.model.lower() == 'shelf' else None) try: events.raise_event(event_name=custom_event.name, device=device, shelf=shelf) except events.EventActionsError as err: # We log the error instead of raising an error so that we do not # disrupt the handler for executing other devices/shelves when one of # them fails. logging.error( 'The following error occurred while trying to perform the event ' '%r: %s', custom_event.name, err)
def device_audit_check(self): """Checks a device to make sure it passes all prechecks for audit. Raises: DeviceNotEnrolledError: when a device is not enrolled in the application. UnableToMoveToShelfError: when a deivce can not be checked into a shelf. DeviceAuditError:when a device encounters an error during auditing """ if not self.enrolled: raise DeviceNotEnrolledError(DEVICE_NOT_ENROLLED_MSG % self.identifier) if self.damaged: raise UnableToMoveToShelfError(_DEVICE_DAMAGED_MSG % self.identifier) try: events.raise_event('device_audit', device=self) except events.EventActionsError as err: # For any action that is implemented for device_audit that is # required for the rest of the logic an error should be raised. # If all actions are not required, eg sending a notification email only, # the error should only be logged. raise DeviceAuditEventError(err)
def unenroll(self, user_email): """Unenrolls a device, removing it from the Grab n Go program. This moves the device to the root Chrome OU, however it does not change its losr or locked attributes, nor does it unlock it if it's locked (i.e., disabled in the Directory API). Args: user_email: str, email address of the user making the request. Returns: The unenrolled device. Raises: FailedToUnenrollError: raised when moving the device's OU fails. """ unenroll_ou = config_model.Config.get('unenroll_ou') directory_client = directory.DirectoryApiClient(user_email) try: directory_client.move_chrome_device_org_unit( device_id=self.chrome_device_id, org_unit_path=unenroll_ou) except directory.DirectoryRPCError as err: raise FailedToUnenrollError( _FAILED_TO_MOVE_DEVICE_MSG % (self.identifier, unenroll_ou, str(err))) self.enrolled = False self.due_date = None self.shelf = None self.assigned_user = None self.assignment_date = None self.current_ou = unenroll_ou self.ou_changed_date = datetime.datetime.utcnow() self.mark_pending_return_date = None self.last_reminder = None self.next_reminder = None self.put() self.stream_to_bq(user_email, 'Unenrolling device.') events.raise_event('device_unenroll', device=self) return self
def unenroll(self, user_email): """Unenrolls a device, removing it from the Grab n Go program. This moves the device to the root Chrome OU, however it does not change its losr or locked attributes, nor does it unlock it if it's locked (i.e., disabled in the Directory API). Args: user_email: str, email address of the user making the request. Returns: The unenrolled device. Raises: FailedToUnenrollError: raised when moving the device's OU fails. """ if self.assigned_user: self._loan_return(user_email) unenroll_ou = config_model.Config.get('unenroll_ou') directory_client = directory.DirectoryApiClient(user_email) try: directory_client.move_chrome_device_org_unit( device_id=self.chrome_device_id, org_unit_path=unenroll_ou) except directory.DirectoryRPCError as err: raise FailedToUnenrollError( _FAILED_TO_MOVE_DEVICE_MSG % (self.identifier, unenroll_ou, str(err))) self.enrolled = False self.due_date = None self.shelf = None self.assigned_user = None self.assignment_date = None self.current_ou = unenroll_ou self.ou_changed_date = datetime.datetime.utcnow() self.mark_pending_return_date = None self.last_reminder = None self.next_reminder = None event_action = 'device_unenroll' try: self = events.raise_event(event_action, device=self) except events.EventActionsError as err: # For any action that is implemented for device_unenroll that is required # for the rest of the logic an error should be raised. If all actions are # not required, eg sending a notification email only, the error should be # logged. logging.error(_EVENT_ACTION_ERROR_MSG, event_action, err) self.put() self.stream_to_bq(user_email, 'Unenrolling device %s.' % self.identifier) return self
def _loan_return(self, user_email): """Returns a device in a loan. Args: user_email: str, user_email of the user initiating the return. Returns: The key of the datastore record. """ events.raise_event('device_loan_return', device=self) if self.lost: self.lost = False if self.locked: self.unlock(user_email) self.assigned_user = None self.assignment_date = None self.due_date = None self.mark_pending_return_date = None self.move_to_default_ou(user_email=user_email) self.last_reminder = None self.next_reminder = None self.put() self.stream_to_bq(user_email, 'Marking device as returned.') return self.key
def disable(self, user_email): """Marks a shelf as disabled. Args: user_email: str, email of the user disabling the shelf. """ self.enabled = False logging.info(_DISABLE_MSG, self.identifier) event_action = 'shelf_disable' try: self = events.raise_event(event_action, shelf=self) except events.EventActionsError as err: # For any action that is implemented for shelf_disable that is required # for the rest of the logic an error should be raised. If all # actions are not required, eg sending a notification email only, # the error should only be logged. logging.error(_EVENT_ACTION_ERROR_MSG, event_action, err) self.put() self.stream_to_bq(user_email, _DISABLE_MSG % self.identifier)
def loan_assign(self, user_email): """Assigns a device to a user. Args: user_email: str, email address of the user to whom the device should be assigned. Returns: The key of the datastore record. Raises: AssignmentError: if the device is not enrolled. """ if not self.enrolled: raise AssignmentError( 'Cannot assign an unenrolled device %s.' % self.identifier) if self.assigned_user and self.assigned_user != user_email: self._loan_return(user_email) self.assigned_user = user_email self.assignment_date = datetime.datetime.utcnow() self.mark_pending_return_date = None self.shelf = None self.due_date = calculate_return_dates(self.assignment_date).default self.move_to_default_ou(user_email=user_email) event_action = 'device_loan_assign' try: self = events.raise_event(event_action, device=self) except events.EventActionsError as err: # For any action that is implemented for device_loan_assign that is # required for the rest of the logic an error should be raised. # If all actions are not required, eg sending a notification email only, # the error should only be logged. logging.error(_EVENT_ACTION_ERROR_MSG, event_action, err) self.put() self.stream_to_bq( user_email, 'Beginning new loan for user %s with device %s.' % (self.assigned_user, self.identifier)) return self.key
def audit(self, user_email, num_of_devices): """Marks a shelf audited. Args: user_email: str, email of the user auditing the shelf. num_of_devices: int, the number of devices on shelf. """ self.last_audit_time = datetime.datetime.utcnow() self.last_audit_by = user_email self.audit_requested = False logging.info(_AUDIT_MSG, self.identifier, num_of_devices) event_action = 'shelf_audited' try: self = events.raise_event(event_action, shelf=self) except events.EventActionsError as err: # For any action that is implemented for shelf_audited that is required # for the rest of the logic an error should be raised. If all # actions are not required, eg sending a notification email only, # the error should only be logged. logging.error(_EVENT_ACTION_ERROR_MSG, event_action, err) self.put() self.stream_to_bq( user_email, _AUDIT_MSG % (self.identifier, num_of_devices))
def test_raise_event(self, mock_loadactions, mock_getactionsforevent, mock_taskqueueadd, mock_logwarn, mock_logerror): """Tests raising an Action if the Event is configured for Actions.""" self.testbed.raise_event_patcher.stop( ) # Disable patcher; use real method. # No Actions configured for the Event. mock_getactionsforevent.return_value = [] events.raise_event('sample_event') mock_logwarn.assert_called_with(events._NO_ACTIONS_MSG, 'sample_event') # Everything is running smoothly. def side_effect1(device=None): """Side effect for sync action's run method that returns the model.""" return device mock_sync_action = mock.Mock() mock_sync_action.run.side_effect = side_effect1 mock_loadactions.return_value = { 'sync': { 'sync_action': mock_sync_action }, 'async': { 'async_action1': 'fake_async_action1', 'async_action2': 'fake_async_action2', 'async_action3': 'fake_async_action3', } } mock_getactionsforevent.return_value = [ 'sync_action', 'async_action3', 'async_action1', 'async_action2' ] test_device = device_model.Device(chrome_device_id='4815162342', serial_number='123456') expected_async_payload = pickle.dumps({ 'async_actions': ['async_action1', 'async_action2', 'async_action3'], 'device': test_device }) events.raise_event('sample_event', device=test_device) expected_calls = [ mock.call(queue_name='process-action', payload=expected_async_payload, target='default'), ] mock_taskqueueadd.assert_has_calls(expected_calls) mock_sync_action.run.assert_called_once_with(device=test_device) # A sync action raises a catchable exception. mock_sync_action.reset_mock() mock_logerror.reset_mock() mock_getactionsforevent.reset_mock() mock_loadactions.reset_mock() def side_effect2(device=None): """Side effect for sync action's run method that returns the model.""" del device # Unused. raise base_action.BadDeviceError('Found a bad attribute.') mock_sync_action.run.side_effect = side_effect2 mock_loadactions.return_value = { 'sync': { 'sync_action': mock_sync_action }, 'async': {} } mock_getactionsforevent.return_value = ['sync_action'] with self.assertRaises(events.EventActionsError): events.raise_event('sample_event', device=test_device) self.assertLen(mock_logerror.mock_calls, 1) self.testbed.raise_event_patcher.start( ) # Because cleanup will stop().
def enroll(cls, user_email, location, capacity, friendly_name=None, latitude=None, longitude=None, altitude=None, responsible_for_audit=None, audit_notification_enabled=True, audit_interval_override=None): """Creates a new shelf or reactivates an existing one. Args: user_email: str, email of the user enrolling the shelf. location: str, location description of a shelf. capacity: int, maximum shelf capacity. friendly_name: str, optional, friendly name for a shelf. latitude: float, optional, latitude. Required if long provided. longitude: float, optional, longitude. Required if lat provided. altitude: int, optional, altitude of the shelf. responsible_for_audit: str, optional, string email (if email enabled) or other modifier (eg ticket queue) for the party responsible for auditing this shelf. audit_notification_enabled: bool, optional, enable or disable shelf audit notifications. audit_interval_override: An integer for the number of hours to allow a shelf to remain unaudited, overriding the global shelf_audit_interval setting. Returns: The newly created or reactivated shelf. Raises: EnrollmentError: If enrollment fails. """ if bool(latitude) ^ bool(longitude): raise EnrollmentError(_LAT_LONG_MSG) shelf = cls.get(location=location, friendly_name=friendly_name) if shelf: shelf.enabled = True shelf.capacity = capacity shelf.friendly_name = friendly_name shelf.altitude = altitude shelf.responsible_for_audit = responsible_for_audit if latitude is not None and longitude is not None: shelf.lat_long = ndb.GeoPt(latitude, longitude) shelf.audit_interval_override = audit_interval_override logging.info(_REACTIVATE_MSG, shelf.identifier) else: shelf = cls(location=location, capacity=capacity, friendly_name=friendly_name, altitude=altitude, audit_notification_enabled=audit_notification_enabled, responsible_for_audit=responsible_for_audit, audit_interval_override=audit_interval_override) if latitude is not None and longitude is not None: shelf.lat_long = ndb.GeoPt(latitude, longitude) logging.info(_CREATE_NEW_SHELF_MSG, shelf.identifier) shelf = events.raise_event('shelf_enroll', shelf=shelf) shelf.put() shelf.stream_to_bq(user_email, _ENROLL_MSG % shelf.identifier) return shelf
def enroll(cls, user_email, serial_number=None, asset_tag=None): """Enrolls a new device. Args: user_email: str, email address of the user making the request. serial_number: str, serial number of the device. asset_tag: str, optional, asset tag of the device. Returns: The enrolled device object. Raises: DeviceCreationError: raised when moving the device's OU fails or when the directory API responds with incomplete information or if the device is not found in the directory API. """ if serial_number: serial_number = serial_number.upper() if asset_tag: asset_tag = asset_tag.upper() device_identifier_mode = config_model.Config.get('device_identifier_mode') if not asset_tag and device_identifier_mode in ( config_model.DeviceIdentifierMode.BOTH_REQUIRED, config_model.DeviceIdentifierMode.ASSET_TAG): raise datastore_errors.BadValueError(_ASSET_TAGS_REQUIRED_MSG) elif not serial_number and device_identifier_mode in ( config_model.DeviceIdentifierMode.BOTH_REQUIRED, config_model.DeviceIdentifierMode.SERIAL_NUMBER): raise datastore_errors.BadValueError(_SERIAL_NUMBERS_REQUIRED_MSG) directory_client = directory.DirectoryApiClient(user_email) device = cls.get(serial_number=serial_number, asset_tag=asset_tag) now = datetime.datetime.utcnow() existing_device = bool(device) if existing_device: device = _update_existing_device(device, user_email, asset_tag) else: device = cls(serial_number=serial_number, asset_tag=asset_tag) identifier = serial_number or asset_tag logging.info('Enrolling device %s', identifier) try: device = events.raise_event('device_enroll', device=device) except events.EventActionsError as err: # For any action that is implemented for device_enroll that is required # for the rest of the logic an error should be raised. If all actions are # not required, eg sending a notification email only, the error should # only be logged. raise DeviceCreationError(err) if device.serial_number: serial_number = device.serial_number else: raise DeviceCreationError( 'No serial number for device %s.' % identifier) if not existing_device: # If this implementation of the app can translate asset tags to serial # numbers, recheck for an existing device now that we may have the serial. if device_identifier_mode == ( config_model.DeviceIdentifierMode.ASSET_TAG): device_by_serial = cls.get(serial_number=serial_number) if device_by_serial: device = _update_existing_device( device_by_serial, user_email, asset_tag) existing_device = True try: # Get a Chrome OS Device object as per # https://developers.google.com/admin-sdk/directory/v1/reference/chromeosdevices directory_device_object = directory_client.get_chrome_device_by_serial( serial_number) except directory.DeviceDoesNotExistError as err: raise DeviceCreationError(str(err)) try: device.chrome_device_id = directory_device_object[directory.DEVICE_ID] device.current_ou = directory_device_object[directory.ORG_UNIT_PATH] device.device_model = directory_device_object[directory.MODEL] except KeyError: raise DeviceCreationError(_DIRECTORY_INFO_INCOMPLETE_MSG) try: directory_client.move_chrome_device_org_unit( device_id=directory_device_object[directory.DEVICE_ID], org_unit_path=constants.ORG_UNIT_DICT['DEFAULT']) except directory.DirectoryRPCError as err: raise DeviceCreationError( _FAILED_TO_MOVE_DEVICE_MSG % ( serial_number, constants.ORG_UNIT_DICT['DEFAULT'], str(err))) device.current_ou = constants.ORG_UNIT_DICT['DEFAULT'] device.ou_changed_date = now device.last_known_healthy = now device.put() device.stream_to_bq(user_email, 'Enrolling device %s.' % identifier) return device
def enroll(cls, serial_number, user_email, asset_tag=None): """Enrolls a new device. Args: serial_number: str, serial number of the device. user_email: str, email address of the user making the request. asset_tag: str, optional, asset tag of the device. Returns: The enrolled device object. Raises: DeviceCreationError: raised when moving the device's OU fails or when the directory API responds with incomplete information or if the device is not found in the directory API. """ directory_client = directory.DirectoryApiClient(user_email) device = cls.get(serial_number=serial_number) now = datetime.datetime.utcnow() was_lost_or_locked = False if device: logging.info('Previous device found, re-enrolling.') if device.locked: was_lost_or_locked = True device.unlock(user_email) if device.lost: was_lost_or_locked = True device.lost = False try: device.move_to_default_ou(user_email=user_email) except UnableToMoveToDefaultOUError as err: raise DeviceCreationError(str(err)) device.enrolled = True device.asset_tag = asset_tag device.last_known_healthy = now device.mark_pending_return_date = None else: try: dir_device = directory_client.get_chrome_device_by_serial(serial_number) except directory.DeviceDoesNotExistError as err: raise DeviceCreationError(str(err)) if dir_device[ directory.ORG_UNIT_PATH] != constants.ORG_UNIT_DICT['DEFAULT']: try: directory_client.move_chrome_device_org_unit( device_id=dir_device[directory.DEVICE_ID], org_unit_path=constants.ORG_UNIT_DICT['DEFAULT']) except directory.DirectoryRPCError as err: raise DeviceCreationError( _FAILED_TO_MOVE_DEVICE_MSG % ( serial_number, constants.ORG_UNIT_DICT['DEFAULT'], str(err))) try: device = cls( serial_number=serial_number, asset_tag=asset_tag, device_model=dir_device.get(directory.MODEL), last_known_healthy=now, current_ou=constants.ORG_UNIT_DICT['DEFAULT'], ou_changed_date=now, chrome_device_id=dir_device[directory.DEVICE_ID]) except KeyError: raise DeviceCreationError(_DIRECTORY_INFO_INCOMPLETE_MSG) logging.info('Enrolling device %s', serial_number) device.put() device.stream_to_bq(user_email, 'Enrolling device.') if was_lost_or_locked: events.raise_event('device_enroll_lost_or_locked', device=device) else: events.raise_event('device_enroll', device=device) return device