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  }