go.dedis.ch/onet/v3@v3.2.11-0.20210930124529-e36530bca7ef/simul/build.go (about) 1 package simul 2 3 import ( 4 "flag" 5 "fmt" 6 "os" 7 "path/filepath" 8 "strconv" 9 "strings" 10 11 "math" 12 "time" 13 14 "go.dedis.ch/onet/v3/log" 15 "go.dedis.ch/onet/v3/simul/monitor" 16 "go.dedis.ch/onet/v3/simul/platform" 17 "golang.org/x/xerrors" 18 ) 19 20 // Configuration-variables 21 var platformDst = "localhost" 22 var nobuild = false 23 var clean = true 24 var build = "" 25 var machines = 3 26 var monitorPort = monitor.DefaultSinkPort 27 var simRange = "" 28 var race = false 29 var runWait = 180 * time.Second 30 var experimentWait = 0 * time.Second 31 32 func init() { 33 flag.StringVar(&platformDst, "platform", platformDst, "platform to deploy to [localhost,mininet,deterlab]") 34 flag.BoolVar(&nobuild, "nobuild", false, "Don't rebuild all helpers") 35 flag.BoolVar(&clean, "clean", false, "Only clean platform") 36 flag.StringVar(&build, "build", "", "List of packages to build") 37 flag.BoolVar(&race, "race", false, "Build with go's race detection enabled (doesn't work on all platforms)") 38 flag.IntVar(&machines, "machines", machines, "Number of machines on Deterlab") 39 flag.IntVar(&monitorPort, "mport", monitorPort, "Port-number for monitor") 40 flag.StringVar(&simRange, "range", simRange, "Range of simulations to run. 0: or 3:4 or :4") 41 flag.DurationVar(&runWait, "runwait", runWait, "How long to wait for each simulation to finish - overwrites .toml-value") 42 flag.DurationVar(&experimentWait, "experimentwait", experimentWait, "How long to wait for the whole experiment to finish") 43 log.RegisterFlags() 44 } 45 46 // Reads in the platform that we want to use and prepares for the tests 47 func startBuild() { 48 flag.Parse() 49 deployP := platform.NewPlatform(platformDst) 50 if deployP == nil { 51 log.Fatal("Platform not recognized.", platformDst) 52 } 53 log.Lvl1("Deploying to", platformDst) 54 55 simulations := flag.Args() 56 if len(simulations) == 0 { 57 log.Fatal("Please give a simulation to run") 58 } 59 60 for _, simulation := range simulations { 61 runconfigs := platform.ReadRunFile(deployP, simulation) 62 63 if len(runconfigs) == 0 { 64 log.Fatal("No tests found in", simulation) 65 } 66 deployP.Configure(&platform.Config{ 67 MonitorPort: monitorPort, 68 Debug: log.DebugVisible(), 69 Suite: runconfigs[0].Get("Suite"), 70 }) 71 72 if clean { 73 err := deployP.Deploy(runconfigs[0]) 74 if err != nil { 75 log.Fatal("Couldn't deploy:", err) 76 } 77 if err := deployP.Cleanup(); err != nil { 78 log.Error("Couldn't cleanup correctly:", err) 79 } 80 } else { 81 logname := strings.Replace(filepath.Base(simulation), ".toml", "", 1) 82 testsDone := make(chan bool) 83 timeout, err := getExperimentWait(runconfigs) 84 if err != nil { 85 log.Fatal("ExperimentWait:", err) 86 } 87 go func() { 88 RunTests(deployP, logname, runconfigs) 89 testsDone <- true 90 }() 91 select { 92 case <-testsDone: 93 log.Lvl3("Done with test", simulation) 94 case <-time.After(timeout): 95 log.Fatal("Test failed to finish in", timeout) 96 } 97 } 98 } 99 } 100 101 // RunTests the given tests and puts the output into the 102 // given file name. It outputs RunStats in a CSV format. 103 func RunTests(deployP platform.Platform, name string, runconfigs []*platform.RunConfig) { 104 105 if nobuild == false { 106 if race { 107 if err := deployP.Build(build, "-race"); err != nil { 108 log.Error("Couln't finish build without errors:", 109 err) 110 } 111 } else { 112 if err := deployP.Build(build); err != nil { 113 log.Error("Couln't finish build without errors:", 114 err) 115 } 116 } 117 } 118 119 mkTestDir() 120 args := os.O_CREATE | os.O_RDWR | os.O_TRUNC 121 // If a range is given, we only append 122 if simRange != "" { 123 args = os.O_CREATE | os.O_RDWR | os.O_APPEND 124 } 125 files := []*os.File{} 126 defer func() { 127 for _, f := range files { 128 if err := f.Close(); err != nil { 129 log.Error("Couln't close", f.Name()) 130 } 131 } 132 }() 133 134 start, stop := getStartStop(len(runconfigs)) 135 for i, rc := range runconfigs { 136 // Implement a simple range-argument that will skip checks not in range 137 if i < start || i > stop { 138 log.Lvl2("Skipping", rc, "because of range") 139 continue 140 } 141 142 // run test t nTimes times 143 // take the average of all successful runs 144 log.Lvl1("Running test with config:", rc) 145 stats, err := RunTest(deployP, rc) 146 if err != nil { 147 log.Error("Error running test:", err) 148 continue 149 } 150 log.Lvl1("Test results:", stats[0]) 151 152 for j, bucketStat := range stats { 153 if j >= len(files) { 154 f, err := os.OpenFile(generateResultFileName(name, j), args, 0660) 155 if err != nil { 156 log.Fatal("error opening test file:", err) 157 } 158 err = f.Sync() 159 if err != nil { 160 log.Fatal("error syncing test file:", err) 161 } 162 163 files = append(files, f) 164 } 165 f := files[j] 166 167 if i == 0 { 168 bucketStat.WriteHeader(f) 169 } 170 if rc.Get("IndividualStats") != "" { 171 err := bucketStat.WriteIndividualStats(f) 172 log.ErrFatal(err) 173 } else { 174 bucketStat.WriteValues(f) 175 } 176 err = f.Sync() 177 if err != nil { 178 log.Fatal("error syncing data to test file:", err) 179 } 180 } 181 } 182 } 183 184 // RunTest a single test - takes a test-file as a string that will be copied 185 // to the deterlab-server 186 func RunTest(deployP platform.Platform, rc *platform.RunConfig) ([]*monitor.Stats, error) { 187 CheckHosts(rc) 188 rc.Delete("simulation") 189 stats := []*monitor.Stats{ 190 // this is the global bucket 191 monitor.NewStats(rc.Map(), "hosts", "bf"), 192 } 193 194 if err := deployP.Cleanup(); err != nil { 195 log.Error(err) 196 return nil, xerrors.Errorf("cleanup: %v", err) 197 } 198 199 if err := deployP.Deploy(rc); err != nil { 200 log.Error(err) 201 return nil, xerrors.Errorf("deploy: %v", err) 202 } 203 204 m := monitor.NewMonitor(stats[0]) 205 m.SinkPort = uint16(monitorPort) 206 defer m.Stop() 207 208 // create the buckets that will split the statistics of the hosts 209 // according to the configuration file 210 buckets, err := rc.GetBuckets() 211 if err != nil { 212 if err != platform.ErrorFieldNotPresent { 213 return nil, xerrors.Errorf("db bucket: %v", err) 214 } 215 216 // Do nothing, there won't be any bucket. 217 } else { 218 for i, rules := range buckets { 219 bs := monitor.NewStats(rc.Map(), "hosts", "bf") 220 stats = append(stats, bs) 221 m.InsertBucket(i, rules, bs) 222 } 223 } 224 225 done := make(chan error) 226 go func() { 227 if err := m.Listen(); err != nil { 228 log.Error("error while closing monitor: " + err.Error()) 229 } 230 }() 231 232 go func() { 233 // Start monitor before so ssh tunnel can connect to the monitor 234 // in case of deterlab. 235 err := deployP.Start() 236 if err != nil { 237 done <- err 238 return 239 } 240 241 if err = deployP.Wait(); err != nil { 242 log.Error("Test failed:", err) 243 if err := deployP.Cleanup(); err != nil { 244 log.Lvl3("Couldn't cleanup platform:", err) 245 } 246 done <- err 247 return 248 } 249 done <- nil 250 }() 251 252 timeout, err := getRunWait(rc) 253 if err != nil { 254 log.Fatal("RunWait:", err) 255 } 256 257 // can timeout the command if it takes too long 258 select { 259 case err := <-done: 260 if err != nil { 261 return nil, xerrors.Errorf("simulation error: %v", err) 262 } 263 return stats, nil 264 case <-time.After(timeout): 265 return nil, xerrors.New("simulation timeout") 266 } 267 } 268 269 // CheckHosts verifies that at least two out of the three parameters: hosts, BF 270 // and depth are set in RunConfig. If one is missing, it tries to fix it. When 271 // more than one is missing, it stops the program. 272 func CheckHosts(rc *platform.RunConfig) { 273 hosts, _ := rc.GetInt("hosts") 274 bf, _ := rc.GetInt("bf") 275 depth, _ := rc.GetInt("depth") 276 if hosts == 0 { 277 if depth == 0 || bf == 0 { 278 log.Fatal("When hosts is not set, depth and BF must be set.") 279 } 280 hosts = calcHosts(bf, depth) 281 rc.Put("hosts", strconv.Itoa(hosts)) 282 } else if bf == 0 { 283 if depth == 0 || hosts == 0 { 284 log.Fatal("When BF is not set, depth and hosts must be set.") 285 } 286 bf = 1 287 for calcHosts(bf, depth) < hosts { 288 bf++ 289 } 290 rc.Put("bf", strconv.Itoa(bf)) 291 } else if depth == 0 { 292 if hosts == 0 || bf == 0 { 293 log.Fatal("When depth is not set, hsots and BF must be set.") 294 } 295 depth = 1 296 for calcHosts(bf, depth) < hosts { 297 depth++ 298 } 299 rc.Put("depth", strconv.Itoa(depth)) 300 } 301 // don't do anything if all three parameters are set 302 } 303 304 // Geometric sum to count the total number of nodes: 305 // Root-node: 1 306 // 1st level: bf (branching-factor)*/ 307 // 2nd level: bf^2 (each child has bf children) 308 // 3rd level: bf^3 309 // So total: sum(level=0..depth)(bf^level) 310 func calcHosts(bf, depth int) int { 311 if bf <= 0 { 312 log.Fatal("illegal branching-factor") 313 } else if depth <= 0 { 314 log.Fatal("illegal depth") 315 } else if bf == 1 { 316 return depth + 1 317 } 318 return int((1 - math.Pow(float64(bf), float64(depth+1))) / 319 float64(1-bf)) 320 } 321 322 type runFile struct { 323 Machines int 324 Args string 325 Runs string 326 } 327 328 func mkTestDir() { 329 err := os.MkdirAll("test_data/", 0777) 330 if err != nil { 331 log.Fatal("failed to make test directory") 332 } 333 } 334 335 func generateResultFileName(name string, index int) string { 336 if index == 0 { 337 // don't add the bucket index if it is the global one 338 return fmt.Sprintf("test_data/%s.csv", name) 339 } 340 341 return fmt.Sprintf("test_data/%s_%d.csv", name, index) 342 } 343 344 // returns a tuple of start and stop configurations to run 345 func getStartStop(rcs int) (int, int) { 346 ssStr := strings.Split(simRange, ":") 347 start, err := strconv.Atoi(ssStr[0]) 348 stop := rcs - 1 349 if err == nil { 350 stop = start 351 if len(ssStr) > 1 { 352 stop, err = strconv.Atoi(ssStr[1]) 353 if err != nil { 354 stop = rcs 355 } 356 } 357 } 358 log.Lvl2("Range is", start, ":", stop) 359 return start, stop 360 } 361 362 // getRunWait returns either the command-line value or the value from the runconfig 363 // file 364 func getRunWait(rc *platform.RunConfig) (time.Duration, error) { 365 rcWait, err := rc.GetDuration("runwait") 366 if err == platform.ErrorFieldNotPresent { 367 return runWait, nil 368 } 369 if err == nil { 370 return rcWait, nil 371 } 372 return 0, err 373 } 374 375 // getExperimentWait returns, in the following order of precedence: 376 // 1. the command-line value 377 // 2. the value from runconfig 378 // 3. #runconfigs * runWait 379 func getExperimentWait(rcs []*platform.RunConfig) (time.Duration, error) { 380 if experimentWait > 0 { 381 return experimentWait, nil 382 } 383 rcExp, err := rcs[0].GetDuration("experimentwait") 384 if err == nil { 385 return rcExp, nil 386 } 387 // Probably a parse error parsing the duration. 388 if err != platform.ErrorFieldNotPresent { 389 return 0, err 390 } 391 392 // Otherwise calculate a useful default. 393 wait := 0 * time.Second 394 for _, rc := range rcs { 395 w, err := getRunWait(rc) 396 if err != nil { 397 return 0, err 398 } 399 wait += w 400 } 401 return wait, nil 402 }