def create_tv_config_map(device_ids, async_kvstore_client, request_context): """ Function to create tv config map for initializing TVBookmark class :param device_ids: device ids to fetch tv configs for :param request_context: Used to authenticate kvstore requests :param async_kvstore_client: kvstore client used to make kvstore requests """ # When we are setting a tv bookmark, we're only given device ids tv_config_map = {} tv_configs = yield get_drone_mode_tvs(request_context, async_kvstore_client, device_ids=device_ids) LOGGER.debug('device_ids in create_tv_config_map=%s', device_ids) LOGGER.debug('tv_configs in create_tv_config_map=%s', tv_configs) for tv_config in tv_configs: device_id = b64encode_to_str(base64.urlsafe_b64decode(str(tv_config.get(constants.KEY)))) LOGGER.debug('tv_config for device_id=%s is: %s', device_id, tv_config) # Don't save captain id and captain url. bookmark should be agnostic to this concept # only upon activation should we care about the captain stuff. This way, when we activate # a config in a grid configuration, it starts off with no captain data, and their for a captain # is immediately elected. if not all(key in tv_config for key in TVConfig.required_kvstore_keys()): raise SpacebridgeApiRequestError('No config data exists for device_id={}' .format(device_id), status_code=http.NOT_FOUND) tv_config[constants.CAPTAIN_ID] = '' tv_config[constants.CAPTAIN_URL] = '' tv_config_object = TVConfig(**tv_config) tv_config_map[device_id] = json.loads(tv_config_object.to_json()) defer.returnValue(tv_config_map)
def process_tv_captain_url_request(request_context, client_single_request, single_server_response, async_client_factory): """ This method processes captain url requests from drone mode TVs. It is called when picking a captain when a grid request is activated, and reelecting a captain when the captain dies. :param request_context: Used to authenticate kvstore requests :param client_single_request: client request object protobuf :param single_server_response: server response object protobuf :param async_client factory: factory class used to generate kvstore and spacebridge clients """ captain_url_request = client_single_request.tvCaptainUrlRequest update_flag = captain_url_request.updateFlag captain_id = client_single_request.tvCaptainUrlRequest.captainId async_kvstore_client = async_client_factory.kvstore_client() # fetch tv config for captain id tvs = yield get_drone_mode_tvs(request_context, async_kvstore_client, device_ids=[captain_id]) if not tvs: raise SpacebridgeApiRequestError('Invalid captain id provided. captain_id={}'.format(captain_id), status_code=http.BAD_REQUEST) tv = tvs[0] LOGGER.debug('tvs in captain url request = %s', tvs) tv_config = TVConfig(**tv) # if we're updating the captain url, or if we're trying to be elected # captain and no one is yet, we do the same thing if not (tv_config.captain_url or tv_config.captain_id) or update_flag: device_ids = tv_config.tv_grid['device_ids'] captain_url = captain_url_request.captainUrl timestamp = get_current_timestamp_str() yield update_grid_members(request_context, async_kvstore_client, device_ids, captain_id, captain_url, timestamp) yield process_subscriptions(request_context, async_client_factory, tv_device_ids=device_ids) else: LOGGER.debug('captain url already set for device_id=%s, doing nothing', tv_config.device_id) single_server_response.tvCaptainUrlResponse.SetInParent()
def fetch_active_tv_configs(user_subscription_map, async_kvstore_client, request_context): """ Fetch active tv configs based on subscription data :param user_subscription_map: Map from user to list of subscriptions :param async_kvstore_client: kvstore client used to make requests :param request_context: request context used for kvstore client @returns: tv_config_json_list """ # fetch tv config data from drone mode tvs collection kvstore_requests = [] LOGGER.debug('user_subscription_map=%s', user_subscription_map) for user, subscription_array in user_subscription_map.items(): device_ids = [ subscription.device_id for subscription in subscription_array ] LOGGER.debug("device_ids in subscription array for user=%s: =%s", user, device_ids) kvstore_request = get_drone_mode_tvs(request_context, async_kvstore_client, device_ids=device_ids, user=user) kvstore_requests.append(kvstore_request) tv_config_json_list = [] exceptions = [] responses = yield defer.DeferredList(kvstore_requests, consumeErrors=True) for response in responses: if isinstance(response[1], Failure): exceptions.append(response[1]) else: (_, tv_config_json) = response LOGGER.debug('response in fetch active config=%s', response) tv_config_json_list.extend(tv_config_json) LOGGER.debug( 'Finished fetching tv configs for subscription updates, ' 'tv_config_json_list=%s', tv_config_json_list) if exceptions: LOGGER.error( 'Encountered exceptions fetching drone mode tv data: e=%s', exceptions) defer.returnValue(tv_config_json_list)
def update_grid_members(request_context, async_kvstore_client, device_ids, captain_id, captain_url, timestamp, user=None): """ Function to update the grid members with the correct captain id, captain url, and device ids data :param request_context: used to make kvstore request :param async_kvstore_client: client used to make kvstore request @device_ids: device ids to update @captain_id: captain id @captain_url: captain url """ if not user: user = request_context.current_user tvs = yield get_drone_mode_tvs(request_context, async_kvstore_client, device_ids=device_ids, user=user) for tv in tvs: # if grid was deleted or if this somehow doesn't exist, don't do anything. if constants.TV_GRID in tv: tv[constants.TV_GRID][constants.DEVICE_IDS] = device_ids tv[constants.CAPTAIN_URL] = captain_url tv[constants.CAPTAIN_ID] = captain_id tv[constants.TIMESTAMP] = timestamp written_ids = yield async_kvstore_client.async_batch_save_request( request_context.system_auth_header, constants.DRONE_MODE_TVS_COLLECTION_NAME, tvs, owner=user) LOGGER.debug("updated tvs:%s", written_ids) defer.returnValue(written_ids)
def fetch_valid_tvs(request_context, async_kvstore_client, user=None, tv_ids=None): """ Function to fetch valid tvs for drone mode :param request_context: request context used to make kvstore requests :param async_kvstore_client: async client used to make kvstore requests :param device_id: optional device id to filter on """ # fetch list of all registered tvs valid_tv_protos = [] valid_tv_json = [] if not user: user = request_context.current_user tv_list = yield get_registered_tvs(request_context.system_auth_header, user, async_kvstore_client, device_ids=tv_ids) # fetch devices from drone_mode_tvs collection # if there is data, return that, otherwise just return blank TV data # construct set containing device ids drone_mode_tvs = yield get_drone_mode_tvs(request_context, async_kvstore_client, user=user) LOGGER.debug('registered_tvs=%s, drone mode tvs=%s', tv_list, drone_mode_tvs) drone_mode_tv_dict = {} for element in drone_mode_tvs: if constants.CONTENT in element: raw_id = base64.urlsafe_b64decode(str(element[constants.KEY])) encoded_id = b64encode_to_str(raw_id) element[constants.DEVICE_ID] = encoded_id drone_mode_tv_dict[encoded_id] = element if tv_list: active_subscriptions = yield fetch_subscriptions( request_context.auth_header, async_kvstore_client, user_list=[user], subscription_type=constants.DRONE_MODE_TV, device_ids=tv_ids) LOGGER.debug('active_subscriptions=%s', active_subscriptions) active_subscription_ids = { subscription.device_id for subscription in active_subscriptions } for device in tv_list: device_id = device[constants.DEVICE_ID] tv_proto = drone_mode_pb2.TVData() tv_proto.device_id = device_id tv_proto.display_name = device['device_name'] tv_proto.is_active = device_id in active_subscription_ids tv_proto.tv_config.SetInParent() if device_id in drone_mode_tv_dict: tv_config = TVConfig(**drone_mode_tv_dict[device_id]) tv_config.set_protobuf(tv_proto.tv_config) valid_tv_protos.append(tv_proto) json_obj = MessageToDict(tv_proto, including_default_value_fields=True, use_integers_for_enums=True, preserving_proto_field_name=True) # since we're storing the user choices and input tokens as blobs, # when we deserialize from proto to dict, we need to json dumps each of the fields json_obj[constants.TV_CONFIG][constants.INPUT_TOKENS] = json.dumps( json_obj.get(constants.TV_CONFIG, {}).get(constants.INPUT_TOKENS, {})) json_obj[constants.TV_CONFIG][constants.USER_CHOICES] = json.dumps( json_obj.get(constants.TV_CONFIG, {}).get(constants.USER_CHOICES, {})) valid_tv_json.append(json_obj) LOGGER.debug( 'finished fetch valid tvs: valid_tv_protos=%s, valid_tv_json=%s', valid_tv_protos, valid_tv_json) defer.returnValue([valid_tv_protos, valid_tv_json])
def process_tv_config_delete_request(request_context, client_single_request, single_server_response, async_client_factory): """This method will process a tvConfigDeleteRequest. :param request_context: Used to authenticate kvstore requests :param client_single_request: client request object protobuf :param single_server_response: server response object protobuf :param async_client factory: factory class used to generate kvstore and spacebridge clients """ async_kvstore_client = async_client_factory.kvstore_client() tv_config_delete_request = client_single_request.tvConfigDeleteRequest device_ids = tv_config_delete_request.deviceId LOGGER.debug('device_ids to delete=%s', device_ids) yield validate_devices(set(device_ids), request_context, async_kvstore_client) # need to fetch configs before deleting to make sure we're not deleting the captain # and orphaning the workers, if so, for each one that is a captain, # we need to fetch the other configs in the grid # delete the to be deleted captain from the device_id list, and update # the existing members of the grid tvs = yield get_drone_mode_tvs(request_context, async_kvstore_client, device_ids=device_ids) # fetch configs from kvstore # check to see if they are currently captain for tv in tvs: raw_id = base64.urlsafe_b64decode(str(tv[constants.KEY])) encoded_id = b64encode_to_str(raw_id) tv[constants.DEVICE_ID] = encoded_id if (has_grid(tv, is_json=True)): tv_config = TVConfig(**tv) grid_ids = tv.get(constants.TV_GRID, {}).get(constants.DEVICE_IDS, []) yield remove_from_grid(tv_config=tv_config, device_ids=grid_ids, request_context=request_context, async_kvstore_client=async_kvstore_client) entries_to_delete = [] for device_id in device_ids: kvstore_key = b64_to_urlsafe_b64(device_id) post_data = {constants.KEY: kvstore_key} entries_to_delete.append(post_data) deleted_ids = yield async_kvstore_client.async_batch_save_request( request_context.system_auth_header, constants.DRONE_MODE_TVS_COLLECTION_NAME, entries_to_delete, owner=request_context.current_user) deleted_device_ids = [urlsafe_b64_to_b64(deleted_id) for deleted_id in deleted_ids] if deleted_device_ids: single_server_response.tvConfigDeleteResponse.deletedIds.extend(deleted_device_ids) if not deleted_ids: raise SpacebridgeApiRequestError('None of the device_ids={} were deleted'.format(device_ids), status_code=http.INTERNAL_SERVER_ERROR) elif len(deleted_ids) != len(device_ids): kvstore_keys = {b64_to_urlsafe_b64(device_id) for device_id in device_ids} LOGGER.error('TV configs with these ids: %s were deleted, ' 'while TV configs with these ids: %s were not.', deleted_ids, list(kvstore_keys-set(deleted_ids))) # update current deleted tv subscriptions and # all ipads registered to current user yield process_subscriptions(request_context, async_client_factory, tv_device_ids=device_ids) LOGGER.info('Successful TV Config Delete Request for device_ids=%s', deleted_device_ids)
def process_tv_config_set_request(request_context, client_single_request, single_server_response, async_client_factory): """ This method will process a tvConfigSetRequest. :param request_context: Used to authenticate kvstore requests :param client_single_request: client request object protobuf :param single_server_response: server response object protobuf :param async_client factory: factory class used to generate kvstore and spacebridge clients """ async_kvstore_client = async_client_factory.kvstore_client() tv_config_set_request = client_single_request.tvConfigSetRequest tv_config_proto = tv_config_set_request.tvConfig tv_config = TVConfig() tv_config.from_protobuf(tv_config_proto) timestamp = get_current_timestamp_str() ids_to_update = [tv_config.device_id] # Check to make sure device is valid before proceeding yield validate_devices(set(ids_to_update), request_context, async_kvstore_client) # Before setting config, we need to check if # we're setting a config on a tv that is currently in # a grid configuration, so we need to fetch it. tvs = yield get_drone_mode_tvs(request_context, async_kvstore_client, device_ids=[tv_config.device_id]) if tvs: tv = tvs[0] existing_config = TVConfig(**tv) # If it's a grid, we remove it from the existing grid list, if has_grid(tv, is_json=True) and not has_grid(tv_config_proto): device_ids = ids_to_update = existing_config.tv_grid[constants.DEVICE_IDS] yield remove_from_grid(tv_config=existing_config, device_ids=device_ids, request_context=request_context, async_kvstore_client=async_kvstore_client, timestamp=timestamp) # set timestamp here tv_config.timestamp = timestamp tv_config_json = json.loads(tv_config.to_json()) # delete device id if constants.DEVICE_ID in tv_config_json: del tv_config_json[constants.DEVICE_ID] LOGGER.debug('tv_config_json=%s', tv_config_json) # convert device id to urlsafe b64 encoded to use as kvstore key urlsafe_b64encoded_device_id = b64_to_urlsafe_b64(tv_config.device_id) tv_config_json[constants.KEY] = urlsafe_b64encoded_device_id response = yield async_kvstore_client.async_kvstore_post_or_update_request( constants.DRONE_MODE_TVS_COLLECTION_NAME, json.dumps(tv_config_json), request_context.system_auth_header, key_id=urlsafe_b64encoded_device_id, owner=request_context.current_user) yield check_and_raise_error(response, request_context, 'Write TV Config', valid_codes=[http.OK, http.CREATED]) single_server_response.tvConfigSetResponse.SetInParent() # update tv subscription for config we just created # (and affected grid device ids, if applicable) as well as # all ipads registered to current user yield process_subscriptions(request_context, async_client_factory, tv_device_ids=ids_to_update) LOGGER.info("Successful TV Config Set Request for device id=%s", tv_config.device_id)
def process_tv_config_bulk_set_request(request_context, client_single_request, single_server_response, async_client_factory): """ Bulk tv config set request. Used to bulk send tv config set requests when we're setting a grid configuration :param request_context: Used to authenticate kvstore requests :param client_single_request: client request object protobuf :param single_server_response: server response object protobuf :param async_client factory: factory class used to generate kvstore and spacebridge clients """ async_kvstore_client = async_client_factory.kvstore_client() tv_config_protos = client_single_request.tvConfigBulkSetRequest.tvConfig is_token_update = client_single_request.tvConfigBulkSetRequest.isTokenUpdate LOGGER.debug('tv_config_protos=%s', tv_config_protos) device_ids = {proto.device_id for proto in tv_config_protos} timestamp = get_current_timestamp_str() warnings = yield validate_devices(device_ids, request_context, async_kvstore_client) # Before setting configs, we need to check if # we're setting configs on tvs that are currently in # grid configurations, so we need to fetch them. # This logic is ignored if we're doing a token update existing_grids_to_update = set() if not is_token_update: tvs = yield get_drone_mode_tvs(request_context, async_kvstore_client, device_ids=device_ids) if tvs: requests = [] for tv in tvs: existing_config = TVConfig(**tv) # If it's a grid, we remove it from the existing grid list if has_grid(tv, is_json=True): grid_device_ids = existing_config.tv_grid[constants.DEVICE_IDS] existing_grids_to_update.update(grid_device_ids) request = remove_from_grid(tv_config=existing_config, device_ids=grid_device_ids, request_context=request_context, async_kvstore_client=async_kvstore_client, timestamp=timestamp) requests.append(request) if requests: exceptions = [] responses = yield defer.DeferredList(requests, consumeErrors=True) for response in responses: if isinstance(response[1], Failure): exceptions.append(response[1]) LOGGER.debug('Finished updating modified tvs') if exceptions: LOGGER.error('Encountered exceptions updating tvs, e=%s', exceptions) # if we couldn't update any of the existing configs, we should not continue the request if len(exceptions) == len(requests): raise SpacebridgeApiRequestError('Unable to update all existing tv configs', status_code=http.INTERNAL_SERVER_ERROR) post_data = [] for tv_config_proto in tv_config_protos: tv_config = TVConfig() tv_config.from_protobuf(tv_config_proto) # set timestamp here tv_config.timestamp = timestamp tv_config_json = json.loads(tv_config.to_json()) # delete device id if constants.DEVICE_ID in tv_config_json: del tv_config_json[constants.DEVICE_ID] LOGGER.debug('tv_config_json=%s', tv_config_json) # convert device id to urlsafe b64 encoded to use as kvstore key urlsafe_b64encoded_device_id = b64_to_urlsafe_b64(tv_config.device_id) tv_config_json[constants.KEY] = urlsafe_b64encoded_device_id post_data.append(tv_config_json) LOGGER.debug('post_data=%s', post_data) updated_keys = yield async_kvstore_client.async_batch_save_request( request_context.system_auth_header, constants.DRONE_MODE_TVS_COLLECTION_NAME, post_data, owner=request_context.current_user) ids_to_update = {urlsafe_b64_to_b64(key) for key in updated_keys} ids_to_update.update(existing_grids_to_update) ids_to_update = list(ids_to_update) LOGGER.debug('ids_to_update=%s', ids_to_update) yield process_subscriptions(request_context, async_client_factory, tv_device_ids=ids_to_update) if warnings: single_server_response.tvConfigBulkSetResponse.warnings.extend(warnings) single_server_response.tvConfigBulkSetResponse.SetInParent()