This is definitely possible. I imagine you're going to need to write a little loader program that will read in the bit file and send it to the FPGA via ethernet. In this case, you don't need to do anything special when generating the bit file, just generate it normally as you would if you were going to program the FPGA directly via JTAG. The bit file contains a little bit of header information in a relatively easy to decode binary format. Your loader script should read in the bit file, strip the header, and then send the rest of the bit file on to the FPGA for writing into the flash chip. After verifying that the data has been written correctly, you can have the FPGA reset itself via the ICAP interface to load the new configuration. I have actually implemented something similar, but instead of writing to Flash memory it writes the bitstream directly to a different FPGA with a wide parallel bus (slave SelectMAP). Actually, you can probably get away with loading the entire bit file verbatim without stripping off the header - the data section of the bit file contains synchronization information, and the FPGA will ignore anything before the synchronization data, including the header.
As for where to burn the config data to flash...it depends on precisely what you want to implement. It is possible to store multiple configurations in flash and trigger the FPGA to reset and load a different configuration. The idea is that you can have a 'golden' configuration that loads first, and then an additional configuration that can be upgraded. I think you may need to set it up so that the FPGA always boots into the 'golden' configuration, then the 'golden' configuration will attempt to reset the FPGA and load the other one via writes to the ICAP primitive. If the configuration fails, the FPGA will reload the 'golden' configuration again. You'll have to perform ICAP accesses with a small state machine to check the configuration status when the 'golden' configuration starts up. The 'golden' configuration should at least implement a way to access the Flash memory so you can update the firmware. It is also possible to implement power on board initialization and board testing in the 'golden' bitstream. Another option is to edit the bit file to set up the write to WBSTAR and trigger a reset with IPROG to skip loading the golden configuration and immediately try loading the main configuration. If the second configuration cannot be loaded, then the FPGA will fall back on the golden configuration. Take a look at the configuration user guide for details; this isn't something that I have experimented with before.
See UG470 and XAPP1247 for more information. XAPP1247 also contains what you need to put in the XDC files to adjust the bit files to automatically boot the 'main' image and automatically fall back on the 'golden' image without having to perform ICAP operations in the design itself.
Here is some python code to parse the header on a bit file:
import struct
import sys
bit_name = 'some_bit_file.bit'
print("Reading bit file %s" % bit_name)
try:
bit_file = open(bit_name, 'rb')
except Exception as ex:
print("Error opening \"%s\": %s" %(bit_name, ex.strerror), file=sys.stderr)
exit(1)
# parse bit file header
# Field 1
# Unknown header
l = struct.unpack(">H", bit_file.read(2))[0]
hdr = bit_file.read(l)
# Field 2
# Unknown header
l = struct.unpack(">H", bit_file.read(2))[0]
hdr = bit_file.read(l)
# Field 3
# Design name
l = struct.unpack(">H", bit_file.read(2))[0]
design_name = bit_file.read(l).decode("utf-8")
print("Design name: %s" % design_name)
# Field 4
# Part name
k = bit_file.read(1)
l = struct.unpack(">H", bit_file.read(2))[0]
part_name = bit_file.read(l).decode("utf-8")
print("Part name: %s" % part_name)
# Field 5
# date
k = bit_file.read(1)
l = struct.unpack(">H", bit_file.read(2))[0]
date = bit_file.read(l).decode("utf-8")
print("Date: %s" % date)
# Field 6
# time
k = bit_file.read(1)
l = struct.unpack(">H", bit_file.read(2))[0]
t = bit_file.read(l).decode("utf-8")
print("Time: %s" % t)
# Field 7
# data
k = bit_file.read(1)
l = struct.unpack(">L", bit_file.read(4))[0]
config_data = bit_file.read(l)
print("Bitfile contains %d bytes" % len(config_data))