88
99import atexit
1010from concurrent .futures import ProcessPoolExecutor , ThreadPoolExecutor
11- import gc
1211import ipaddress
1312import logging
1413import operator
1514from os import PathLike
15+ import os
1616from pathlib import Path
1717import shutil
1818import tempfile
@@ -126,6 +126,7 @@ class StatisticalModel:
126126 "EXPORT_TIME" : "first" ,
127127 "SEQ_NUMBER" : "first" ,
128128 "MSG_LENGTH" : "first" ,
129+ "FLOW_COUNT" : ("START_TIME" , "count" ),
129130 }
130131
131132 def __init__ (
@@ -167,68 +168,32 @@ def __init__(
167168
168169 self ._log_dir = log_dir
169170
170- try :
171- logging .getLogger ().debug ("reading file with flows=%s" , flows )
172- # ports could be empty in flows with protocol like ICMP
173- self ._flows_path = flows
174- flows = pd .read_csv (flows , engine = "pyarrow" , dtype = self .CSV_COLUMN_TYPES )
175- flows ["SRC_PORT" ] = flows ["SRC_PORT" ].fillna (0 )
176- flows ["DST_PORT" ] = flows ["DST_PORT" ].fillna (0 )
177- self ._flows : pd .DataFrame = flows .astype (self .CSV_COLUMN_TYPES )
178-
179- if isinstance (reference , str ):
180- self ._ref = None
181- self ._ref_path = reference
182- else :
183- self ._ref = reference
184- except Exception as err :
185- raise SMException ("Unable to read file with flows." ) from err
186-
187- self ._zero_icmp_ports (self ._flows )
188-
189- if stats .start_time > 0 :
190- # filter out flows that start before the start time with 500 ms tolerance
191- self ._flows = self ._flows [
192- self ._flows ["START_TIME" ] >= stats .start_time - 500
193- ]
194-
195- # if stats.end_time > 0:
196- # # filter out flows that start before the end time
197- # self._flows = self._flows[self._flows["START_TIME"] <= stats.end_time]
198-
199- self ._filter_multicast ()
200-
201- if merge :
202- self ._merge_flows (biflows_ts_correction )
203-
204- # write dataframes back to file and read later when needed
205- if not (hasattr (self , "_flows_path" ) and self ._flows_path ):
206- self ._flows_path = tempfile .NamedTemporaryFile (
207- delete = False , suffix = ".csv"
208- ).name
209- self ._flows .to_csv (self ._flows_path , index = False )
210- del self ._flows
211- gc .collect ()
212-
213- _TEMP_FILES .append (self ._flows_path )
214-
215- if not (hasattr (self , "_ref_path" ) and self ._ref_path ):
216- self ._ref_path = tempfile .NamedTemporaryFile (
217- delete = False , suffix = ".csv"
171+ if isinstance (reference , str ):
172+ self ._ref_path = reference
173+ else :
174+ self ._ref_path = self ._ref_path = tempfile .NamedTemporaryFile (
175+ delete = False , prefix = "tmp_ref" , suffix = ".csv"
218176 ).name
219- if self ._ref :
220- self ._ref .to_csv (self ._ref_path , index = False )
221- del self ._ref
222- gc .collect ()
223-
224- _TEMP_FILES .append (self ._ref_path )
177+ reference .to_csv (
178+ self ._ref_path ,
179+ index = False ,
180+ )
181+ reference = pd .DataFrame (columns = self .CSV_COLUMN_TYPES .keys ())
225182
226183 self ._generator_stats : GeneratorStats = stats
227184 self ._flows_ip_addresses_converted = False
228185 self ._ref_ip_addresses_converted = isinstance (reference , pd .DataFrame )
229186 self ._stat_counter = use_statistical_counter
230187 self ._inactive_timeout = inactive_timeout
231188
189+ try :
190+ self ._flows_path = self ._init_flows (flows )
191+ except Exception as err :
192+ raise SMException ("Unable to read file with flows." ) from err
193+
194+ if merge :
195+ self ._merge_flows (biflows_ts_correction )
196+
232197 if use_statistical_counter :
233198 # statistic objects
234199 self ._executor = (
@@ -256,6 +221,52 @@ def __init__(
256221 self ._future_ref = None
257222 self ._future_sim = None
258223
224+ def _init_flows (self , path : os .PathLike ):
225+ """initial read of flows.csv in chunks
226+ replaces faulty values and filters out some flows
227+
228+ Args:
229+ path (os.PathLike): _description_
230+
231+ Returns:
232+ _type_: _description_
233+ """
234+ out_file = tempfile .NamedTemporaryFile (
235+ delete = False , prefix = "tmp_flows" , suffix = ".csv"
236+ ).name
237+ first_write = True
238+ logging .getLogger ().debug ("reading file with flows=%s" , path )
239+ # ports could be empty in flows with protocol like ICMP
240+ for chunk in pd .read_csv (path , dtype = self .CSV_COLUMN_TYPES , chunksize = 100_000 ):
241+ chunk ["SRC_PORT" ] = chunk ["SRC_PORT" ].fillna (0 )
242+ chunk ["DST_PORT" ] = chunk ["DST_PORT" ].fillna (0 )
243+ chunk = chunk .astype (self .CSV_COLUMN_TYPES )
244+
245+ self ._zero_icmp_ports (chunk )
246+
247+ if self ._generator_stats .start_time > 0 :
248+ # filter out flows that start before the start time with 500 ms tolerance
249+ chunk = chunk [
250+ chunk ["START_TIME" ] >= self ._generator_stats .start_time - 500
251+ ]
252+
253+ # if stats.end_time > 0:
254+ # # filter out flows that start before the end time
255+ # chunk = chunk[chunk["START_TIME"] <= stats.end_time]
256+
257+ self ._filter_multicast (chunk )
258+
259+ chunk .to_csv (
260+ out_file ,
261+ index = False ,
262+ mode = "w" if first_write else "a" ,
263+ header = first_write ,
264+ )
265+ first_write = False
266+
267+ os .remove (path )
268+ return out_file
269+
259270 @staticmethod
260271 def _run_sim (
261272 host_stats : GeneratorStats ,
@@ -417,12 +428,12 @@ def validate(
417428
418429 return report
419430
420- def _filter_multicast (self ):
431+ def _filter_multicast (self , flows : pd . DataFrame ):
421432 # ipv4
422- self . _flows = self . _flows [ self . _flows ["DST_IP" ] != "255.255.255.255" ]
433+ flows = flows [ flows ["DST_IP" ] != "255.255.255.255" ]
423434
424435 # ipv6
425- self . _flows = self . _flows [ ~ self . _flows ["DST_IP" ].str .startswith ("ff02::" )]
436+ flows = flows [ flows ["DST_IP" ].str .startswith ("ff02::" )]
426437
427438 def _merge_flows (self , biflows_ts_correction : bool ) -> None :
428439 """
@@ -437,18 +448,25 @@ def _merge_flows(self, biflows_ts_correction: bool) -> None:
437448 Timestamps in reverse direction flows is corrected.
438449 """
439450
440- assert len (self ._ref .index ) == self ._ref .groupby (self .FLOW_KEY ).ngroups , (
451+ ref_df = self ._load_ref_df
452+ assert len (ref_df .index ) == ref_df .groupby (self .FLOW_KEY ).ngroups , (
441453 "Cannot merge flows, duplicated key."
442454 )
455+ del ref_df
456+
457+ # wait till self._flows_path is not used anymore
458+ while self ._future_sim .running :
459+ time .sleep (1 )
443460
444- flows = self ._flows .groupby (self .FLOW_KEY ).aggregate (self .AGGREGATE_FLOWS )
445- flows ["FLOW_COUNT" ] = self ._flows .groupby (self .FLOW_KEY ).size ()
446- self ._flows = flows .reset_index ()
461+ flows_df = self ._load_flows_df ()
462+ flows = flows_df .groupby (self .FLOW_KEY ).aggregate (self .AGGREGATE_FLOWS )
463+ flows ["FLOW_COUNT" ] = flows_df .groupby (self .FLOW_KEY ).size ()
464+ flows_df = flows .reset_index ()
447465
448466 if biflows_ts_correction :
449467 # correct timestamps in reverse direction of flows originating from biflows
450468 # using direction invariant flow key
451- flows = self . _flows
469+ flows = flows_df
452470
453471 swap_cond = flows ["SRC_IP" ] > flows ["DST_IP" ]
454472 flows ["INV_SRC_IP" ] = np .where (swap_cond , flows ["DST_IP" ], flows ["SRC_IP" ])
@@ -464,9 +482,9 @@ def _merge_flows(self, biflows_ts_correction: bool) -> None:
464482 flows ["START_TIME" ] = grouped ["START_TIME" ].transform ("min" )
465483 flows ["END_TIME" ] = grouped ["END_TIME" ].transform ("max" )
466484
467- self . _flows = flows .loc [
468- :, list ( self . CSV_COLUMN_TYPES . keys ()) + [ "FLOW_COUNT" ]
469- ]
485+ flows_df = flows .loc [:, list ( self . CSV_COLUMN_TYPES . keys ()) + [ "FLOW_COUNT" ]]
486+
487+ flows_df . to_csv ( self . _ref_path , index = False )
470488
471489 def _convert_ip_addresses (self ) -> None :
472490 """Convert str ip addresses to objects (ipaddress library) in DataFrames."""
0 commit comments