X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/6f5431413448f52f3ae5c88553b7f7ee1532b9fd..0348478eba20e59c3305dd5eda702d9192b45058:/lib/costanalyzer/costanalyzer.go diff --git a/lib/costanalyzer/costanalyzer.go b/lib/costanalyzer/costanalyzer.go index bca23b1535..37e655e53a 100644 --- a/lib/costanalyzer/costanalyzer.go +++ b/lib/costanalyzer/costanalyzer.go @@ -35,6 +35,7 @@ type nodeInfo struct { // Modern ProviderType string Price float64 + Preemptible bool } type arrayFlags []string @@ -56,12 +57,12 @@ func parseFlags(prog string, args []string, loader *config.Loader, logger *logru flags.Usage = func() { fmt.Fprintf(flags.Output(), ` Usage: - %s [options ...] + %s [options ...] ... This program analyzes the cost of Arvados container requests. For each uuid supplied, it creates a CSV report that lists all the containers used to fulfill the container request, together with the machine type and cost of - each container. + each container. At least one uuid must be specified. When supplied with the uuid of a container request, it will calculate the cost of that container request and all its children. @@ -90,6 +91,12 @@ Usage: was fulfilled. This program uses the cost data stored at the time of the execution of the container, stored in the 'node.json' file in its log collection. + - if a container was run on a preemptible ("spot") instance, the cost data + reported by this program may be wildly inaccurate, because it does not have + access to the spot pricing in effect for the node then the container ran. The + UUID report file that is generated when the '-output' option is specified has + a column that indicates the preemptible state of the instance that ran the + container. In order to get the data for the uuids supplied, the ARVADOS_API_HOST and ARVADOS_API_TOKEN environment variables must be set. @@ -97,13 +104,15 @@ Usage: This program prints the total dollar amount from the aggregate cost accounting across all provided uuids on stdout. + When the '-output' option is specified, a set of CSV files with cost details + will be written to the provided directory. + Options: `, prog) flags.PrintDefaults() } loglevel := flags.String("log-level", "info", "logging `level` (debug, info, ...)") - flags.StringVar(&resultsDir, "output", "", "output `directory` for the CSV reports (required)") - flags.Var(&uuids, "uuid", "object uuid. May be specified more than once. Also accepts a comma separated list of uuids (required)") + flags.StringVar(&resultsDir, "output", "", "output `directory` for the CSV reports") flags.BoolVar(&cache, "cache", true, "create and use a local disk cache of Arvados objects") err = flags.Parse(args) if err == flag.ErrHelp { @@ -114,17 +123,11 @@ Options: exitCode = 2 return } + uuids = flags.Args() if len(uuids) < 1 { flags.Usage() - err = fmt.Errorf("Error: no uuid(s) provided") - exitCode = 2 - return - } - - if resultsDir == "" { - flags.Usage() - err = fmt.Errorf("Error: output directory must be specified") + err = fmt.Errorf("error: no uuid(s) provided") exitCode = 2 return } @@ -185,7 +188,7 @@ func addContainerLine(logger *logrus.Logger, node nodeInfo, cr arvados.Container size = node.ProviderType } cost = delta.Seconds() / 3600 * price - csv += size + "," + strconv.FormatFloat(price, 'f', 8, 64) + "," + strconv.FormatFloat(cost, 'f', 8, 64) + "\n" + csv += size + "," + fmt.Sprintf("%+v", node.Preemptible) + "," + strconv.FormatFloat(price, 'f', 8, 64) + "," + strconv.FormatFloat(cost, 'f', 8, 64) + "\n" return } @@ -289,7 +292,7 @@ func loadObject(logger *logrus.Logger, ac *arvados.Client, path string, uuid str func getNode(arv *arvadosclient.ArvadosClient, ac *arvados.Client, kc *keepclient.KeepClient, cr arvados.ContainerRequest) (node nodeInfo, err error) { if cr.LogUUID == "" { - err = errors.New("No log collection") + err = errors.New("no log collection") return } @@ -373,7 +376,7 @@ func generateCrCsv(logger *logrus.Logger, uuid string, arv *arvadosclient.Arvado cost = make(map[string]float64) - csv := "CR UUID,CR name,Container UUID,State,Started At,Finished At,Duration in seconds,Compute node type,Hourly node cost,Total cost\n" + csv := "CR UUID,CR name,Container UUID,State,Started At,Finished At,Duration in seconds,Compute node type,Preemptible,Hourly node cost,Total cost\n" var tmpCsv string var tmpTotalCost float64 var totalCost float64 @@ -390,7 +393,10 @@ func generateCrCsv(logger *logrus.Logger, uuid string, arv *arvadosclient.Arvado if !ok { return nil, fmt.Errorf("error: collection %s does not have a 'container_request' property", uuid) } - crUUID = value.(string) + crUUID, ok = value.(string) + if !ok { + return nil, fmt.Errorf("error: collection %s does not have a 'container_request' property of the string type", uuid) + } } // This is a container request, find the container @@ -400,7 +406,7 @@ func generateCrCsv(logger *logrus.Logger, uuid string, arv *arvadosclient.Arvado return nil, fmt.Errorf("error loading cr object %s: %s", uuid, err) } var container arvados.Container - err = loadObject(logger, ac, uuid, cr.ContainerUUID, cache, &container) + err = loadObject(logger, ac, crUUID, cr.ContainerUUID, cache, &container) if err != nil { return nil, fmt.Errorf("error loading container object %s: %s", cr.ContainerUUID, err) } @@ -429,7 +435,7 @@ func generateCrCsv(logger *logrus.Logger, uuid string, arv *arvadosclient.Arvado if err != nil { return nil, fmt.Errorf("error querying container_requests: %s", err.Error()) } - logger.Infof("Collecting child containers for container request %s", uuid) + logger.Infof("Collecting child containers for container request %s", crUUID) for _, cr2 := range childCrs.Items { logger.Info(".") node, err := getNode(arv, ac, kc, cr2) @@ -438,7 +444,7 @@ func generateCrCsv(logger *logrus.Logger, uuid string, arv *arvadosclient.Arvado } logger.Debug("\nChild container: " + cr2.ContainerUUID + "\n") var c2 arvados.Container - err = loadObject(logger, ac, uuid, cr2.ContainerUUID, cache, &c2) + err = loadObject(logger, ac, cr.UUID, cr2.ContainerUUID, cache, &c2) if err != nil { return nil, fmt.Errorf("error loading object %s: %s", cr2.ContainerUUID, err) } @@ -451,13 +457,15 @@ func generateCrCsv(logger *logrus.Logger, uuid string, arv *arvadosclient.Arvado csv += "TOTAL,,,,,,,,," + strconv.FormatFloat(totalCost, 'f', 8, 64) + "\n" - // Write the resulting CSV file - fName := resultsDir + "/" + uuid + ".csv" - err = ioutil.WriteFile(fName, []byte(csv), 0644) - if err != nil { - return nil, fmt.Errorf("error writing file with path %s: %s", fName, err.Error()) + if resultsDir != "" { + // Write the resulting CSV file + fName := resultsDir + "/" + crUUID + ".csv" + err = ioutil.WriteFile(fName, []byte(csv), 0644) + if err != nil { + return nil, fmt.Errorf("error writing file with path %s: %s", fName, err.Error()) + } + logger.Infof("\nUUID report in %s\n\n", fName) } - logger.Infof("\nUUID report in %s\n\n", fName) return } @@ -467,10 +475,12 @@ func costanalyzer(prog string, args []string, loader *config.Loader, logger *log if exitcode != 0 { return } - err = ensureDirectory(logger, resultsDir) - if err != nil { - exitcode = 3 - return + if resultsDir != "" { + err = ensureDirectory(logger, resultsDir) + if err != nil { + exitcode = 3 + return + } } // Arvados Client setup @@ -506,7 +516,7 @@ func costanalyzer(prog string, args []string, loader *config.Loader, logger *log var crCsv map[string]float64 crCsv, err = generateCrCsv(logger, uuid, arv, ac, kc, resultsDir, cache) if err != nil { - err = fmt.Errorf("Error generating CSV for uuid %s: %s", uuid, err.Error()) + err = fmt.Errorf("error generating CSV for uuid %s: %s", uuid, err.Error()) exitcode = 2 return } @@ -518,7 +528,11 @@ func costanalyzer(prog string, args []string, loader *config.Loader, logger *log // It is identified by the user uuid. As such, cost analysis for the // "Home" project is not supported by this program. Skip this uuid, but // keep going. - logger.Errorf("Cost analysis is not supported for the 'Home' project: %s", uuid) + logger.Errorf("cost analysis is not supported for the 'Home' project: %s", uuid) + } else { + logger.Errorf("this argument does not look like a uuid: %s\n", uuid) + exitcode = 3 + return } } @@ -542,15 +556,18 @@ func costanalyzer(prog string, args []string, loader *config.Loader, logger *log csv += "TOTAL," + strconv.FormatFloat(total, 'f', 8, 64) + "\n" - // Write the resulting CSV file - aFile := resultsDir + "/" + time.Now().Format("2006-01-02-15-04-05") + "-aggregate-costaccounting.csv" - err = ioutil.WriteFile(aFile, []byte(csv), 0644) - if err != nil { - err = fmt.Errorf("Error writing file with path %s: %s", aFile, err.Error()) - exitcode = 1 - return + if resultsDir != "" { + // Write the resulting CSV file + aFile := resultsDir + "/" + time.Now().Format("2006-01-02-15-04-05") + "-aggregate-costaccounting.csv" + err = ioutil.WriteFile(aFile, []byte(csv), 0644) + if err != nil { + err = fmt.Errorf("error writing file with path %s: %s", aFile, err.Error()) + exitcode = 1 + return + } + logger.Infof("Aggregate cost accounting for all supplied uuids in %s\n", aFile) } - logger.Infof("Aggregate cost accounting for all supplied uuids in %s\n", aFile) + // Output the total dollar amount on stdout fmt.Fprintf(stdout, "%s\n", strconv.FormatFloat(total, 'f', 8, 64))