// Prints out the output of tasks func printTaskList(taskList []photon.Task, c *cli.Context) error { if c.GlobalIsSet("non-interactive") { for _, task := range taskList { fmt.Printf("%s\t%s\t%s\t%d\t%d\n", task.ID, task.State, task.Operation, task.StartedTime, task.EndTime-task.StartedTime) } } else if utils.NeedsFormatting(c) { utils.FormatObjects(taskList, os.Stdout, c) } else { w := new(tabwriter.Writer) w.Init(os.Stdout, 4, 4, 2, ' ', 0) fmt.Fprintf(w, "\nTask\tStart Time\tDuration\n") for _, task := range taskList { var duration int64 startTime := timestampToString(task.StartedTime) if task.EndTime-task.StartedTime > 0 { duration = (task.EndTime - task.StartedTime) / 1000 } else { duration = 0 } fmt.Fprintf(w, "%s\t%s\t%.2d:%.2d:%.2d\n", task.ID, startTime, duration/3600, (duration/60)%60, duration%60) err := w.Flush() if err != nil { return err } fmt.Printf("%s, %s\n", task.Operation, task.State) } if len(taskList) > 0 { fmt.Printf("\nYou can run 'photon task show <id>' for more information\n") } fmt.Printf("Total: %d\n", len(taskList)) } return nil }
func printClusterList(clusterList []photon.Cluster, w io.Writer, c *cli.Context, summaryView bool) error { stateCount := make(map[string]int) for _, cluster := range clusterList { stateCount[cluster.State]++ } if c.GlobalIsSet("non-interactive") { if !summaryView { for _, cluster := range clusterList { fmt.Printf("%s\t%s\t%s\t%s\t%d\n", cluster.ID, cluster.Name, cluster.Type, cluster.State, cluster.WorkerCount) } } } else if c.GlobalString("output") != "" { utils.FormatObjects(clusterList, w, c) } else { if !summaryView { w := new(tabwriter.Writer) w.Init(os.Stdout, 4, 4, 2, ' ', 0) fmt.Fprintf(w, "ID\tName\tType\tState\tWorker Count\n") for _, cluster := range clusterList { fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\n", cluster.ID, cluster.Name, cluster.Type, cluster.State, cluster.WorkerCount) } err := w.Flush() if err != nil { return err } } fmt.Printf("\nTotal: %d\n", len(clusterList)) for key, value := range stateCount { fmt.Printf("%s: %d\n", key, value) } } return nil }
func listVMNetworks(c *cli.Context, w io.Writer) error { err := checkArgCount(c, 1) if err != nil { return err } id := c.Args().First() client.Esxclient, err = client.GetClient(c) if err != nil { return err } networks, err := getVMNetworks(id, c) if err != nil { return err } if !utils.NeedsFormatting(c) { err = printVMNetworks(networks, c.GlobalIsSet("non-interactive")) if err != nil { return err } } else { utils.FormatObjects(networks, w, c) } return nil }
// Sends a cluster trigger_maintenance request to the API client based on the cli.Context. // Returns an error if one occurred. func triggerMaintenance(c *cli.Context) error { err := checkArgCount(c, 1) if err != nil { return nil } clusterId := c.Args().First() if len(clusterId) == 0 { return fmt.Errorf("Please provide a valid cluster ID") } client.Esxclient, err = client.GetClient(c) if err != nil { return err } if !c.GlobalIsSet("non-interactive") { fmt.Printf("Maintenance triggered for cluster %s\n", clusterId) } task, err := client.Esxclient.Clusters.TriggerMaintenance(clusterId) if err != nil { return err } _, err = waitOnTaskOperation(task.ID, c) if err != nil { return err } return nil }
func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { err := checkFolder(c.GlobalString("path")) if err != nil { logger().Fatalf("Cound not check/create path: %s", err.Error()) } conf := NewConfiguration(c) if len(c.GlobalString("email")) == 0 { logger().Fatal("You have to pass an account (email address) to the program using --email or -m") } //TODO: move to account struct? Currently MUST pass email. acc := NewAccount(c.GlobalString("email"), conf) client, err := acme.NewClient(c.GlobalString("server"), acc, conf.RsaBits()) if err != nil { logger().Fatalf("Could not create client: %s", err.Error()) } if len(c.GlobalStringSlice("exclude")) > 0 { client.ExcludeChallenges(conf.ExcludedSolvers()) } if c.GlobalIsSet("http") { client.SetHTTPAddress(c.GlobalString("http")) } if c.GlobalIsSet("tls") { client.SetTLSAddress(c.GlobalString("tls")) } return conf, acc, client }
// Retrieves availability zone against specified id. func showAvailabilityZone(c *cli.Context, w io.Writer) error { err := checkArgCount(c, 1) if err != nil { return err } id := c.Args().First() client.Esxclient, err = client.GetClient(c) if err != nil { return err } zone, err := client.Esxclient.AvailabilityZones.Get(id) if err != nil { return err } if c.GlobalIsSet("non-interactive") { fmt.Printf("%s\t%s\t%s\t%s\n", zone.ID, zone.Name, zone.Kind, zone.State) } else if utils.NeedsFormatting(c) { utils.FormatObject(zone, w, c) } else { fmt.Printf("AvailabilityZone ID: %s\n", zone.ID) fmt.Printf(" Name: %s\n", zone.Name) fmt.Printf(" Kind: %s\n", zone.Kind) fmt.Printf(" State: %s\n", zone.State) } return nil }
// Track the progress of the task, returns an error if one occurred func monitorTask(c *cli.Context) error { err := checkArgCount(c, 1) if err != nil { return err } id := c.Args()[0] client.Esxclient, err = client.GetClient(c) if err != nil { return err } if c.GlobalIsSet("non-interactive") { task, err := client.Esxclient.Tasks.Wait(id) if err != nil { return err } fmt.Printf("%s\t%s\t%s\t%s\n", task.ID, task.State, task.Entity.ID, task.Entity.Kind) } else { task, err := pollTask(id) if err != nil { return err } w := new(tabwriter.Writer) w.Init(os.Stdout, 4, 4, 2, ' ', 0) fmt.Fprintf(w, "Task:\t%s\n", task.ID) fmt.Fprintf(w, "Entity:\t%s %s\n", task.Entity.Kind, task.Entity.ID) fmt.Fprintf(w, "State:\t%s\n", task.State) err = w.Flush() if err != nil { return err } } return nil }
// displays the migration status func showMigrationStatusDeprecated(c *cli.Context) error { err := checkArgCount(c, 0) if err != nil { return err } client.Esxclient, err = client.GetClient(c) if err != nil { return err } deployments, err := client.Esxclient.Deployments.GetAll() if err != nil { return err } for _, deployment := range deployments.Items { if deployment.Migration != nil { migration := deployment.Migration if c.GlobalIsSet("non-interactive") { fmt.Printf("%d\t%d\t%d\t%d\t%d\n", migration.CompletedDataMigrationCycles, migration.DataMigrationCycleProgress, migration.DataMigrationCycleSize, migration.VibsUploaded, migration.VibsUploading+migration.VibsUploaded) } else { fmt.Printf(" Migration status:\n") fmt.Printf(" Completed data migration cycles: %d\n", migration.CompletedDataMigrationCycles) fmt.Printf(" Current data migration cycles progress: %d / %d\n", migration.DataMigrationCycleProgress, migration.DataMigrationCycleSize) fmt.Printf(" VIB upload progress: %d / %d\n", migration.VibsUploaded, migration.VibsUploading+migration.VibsUploaded) } } return nil } return nil }
// Retrieves a list of tasks, returns an error if one occurred func listTasks(c *cli.Context) error { err := checkArgNum(c.Args(), 0, "task list <options>") if err != nil { return err } entityId := c.String("entityId") entityKind := c.String("entityKind") state := c.String("state") client.Esxclient, err = client.GetClient(c.GlobalIsSet("non-interactive")) if err != nil { return err } options := &photon.TaskGetOptions{ State: state, EntityID: entityId, EntityKind: entityKind, } taskList, err := client.Esxclient.Tasks.GetAll(options) if err != nil { return err } err = printTaskList(taskList.Items, c) if err != nil { return err } return nil }
// Handles show-login-token, which shows the current login token, if any func showLoginTokenWriter(c *cli.Context, w io.Writer, config *configuration.Configuration) error { err := checkArgNum(c.Args(), 0, "auth show-login-token") if err != nil { return err } if config == nil { config, err = configuration.LoadConfig() if err != nil { return err } } if config.Token == "" { err = fmt.Errorf("No login token available") return err } if !c.GlobalIsSet("non-interactive") { raw := c.Bool("raw") if raw { dumpTokenDetailsRaw(w, "Login Access Token", config.Token) } else { dumpTokenDetails(w, "Login Access Token", config.Token) } } else { fmt.Fprintf(w, "%s\n", config.Token) } return nil }
// Read config from config file, change target and then write back to file // Also check if the target is reachable securely func setEndpoint(c *cli.Context) error { err := checkArgNum(c.Args(), 1, "target set <url>") if err != nil { return err } endpoint := c.Args()[0] noCertCheck := c.Bool("nocertcheck") config, err := cf.LoadConfig() if err != nil { return err } config.CloudTarget = endpoint config.IgnoreCertificate = noCertCheck err = cf.SaveConfig(config) if err != nil { return err } err = configureServerCerts(endpoint, noCertCheck, c.GlobalIsSet("non-interactive")) if err != nil { return err } fmt.Printf("API target set to '%s'\n", endpoint) err = clearConfigTenant("") if err != nil { return err } return err }
func configfile(c *cli.Context, have map[string]authCred, need map[string]string) error { dir, err := util.RackDir() if err != nil { // return fmt.Errorf("Error retrieving rack directory: %s\n", err) return nil } f := path.Join(dir, "config") cfg, err := ini.Load(f) if err != nil { // return fmt.Errorf("Error loading config file: %s\n", err) return nil } cfg.BlockMode = false var profile string if c.GlobalIsSet("profile") { profile = c.GlobalString("profile") } else if c.IsSet("profile") { profile = c.String("profile") } section, err := cfg.GetSection(profile) if err != nil && profile != "" { return fmt.Errorf("Invalid config file profile: %s\n", profile) } for opt := range need { if val := section.Key(opt).String(); val != "" { have[opt] = authCred{value: val, from: fmt.Sprintf("config file (profile: %s)", section.Name())} delete(need, opt) } } return nil }
func sftpSetup(c *cli.Context) (connector.Connection, error) { var ( password *string privateKeyFilepath *string ) host := c.GlobalString("sftp-host") port := c.GlobalInt("sftp-port") remotePath := c.GlobalString("sftp-remote-path") username := c.GlobalString("sftp-username") passwordInput := os.Getenv("ARQ_SFTP_PASSWORD") if passwordInput != "" { password = &passwordInput } else { password = nil } if c.GlobalIsSet("sftp-private-key-filepath") { privateKey := c.GlobalString("sftp-private-key-filepath") privateKeyFilepath = &privateKey } else { privateKeyFilepath = nil } cacheDirectory := c.GlobalString("cache-directory") connection, err := connector.NewSFTPConnection(host, port, remotePath, username, password, privateKeyFilepath, cacheDirectory) if err != nil { log.Errorf("Error while establishing SFTP connection: %s", err) return connector.SFTPConnection{}, err } return *connection, nil }
func getConfig(c *cli.Context) (*manager.Config, string, error) { var reader io.Reader configFile := "" if !c.GlobalIsSet("config") { logrus.Debugf("no configuration was specified, starting with default.") } else if c.GlobalString("config") == "-" { logrus.Debugf("reading configuration from stdin") reader = bufio.NewReader(os.Stdin) } else { f, err := os.Open(c.GlobalString("config")) if err != nil { return nil, "", errored.Errorf("failed to open config file. Error: %v", err) } defer func() { f.Close() }() logrus.Debugf("reading configuration from file: %q", c.GlobalString("config")) reader = bufio.NewReader(f) configFile = c.GlobalString("config") } config := manager.DefaultConfig() if reader != nil { if _, err := config.MergeFromReader(reader); err != nil { return nil, "", errored.Errorf("failed to merge configuration. Error: %v", err) } } return config, configFile, nil }
// Starts the recurring copy state of source system into destination func deploymentMigrationPrepareDeprecated(c *cli.Context) error { err := checkArgNum(c.Args(), 1, "system migration prepare <old_management_endpoint>") if err != nil { return err } sourceAddress := c.Args().First() client.Esxclient, err = client.GetClient(c.GlobalIsSet("non-interactive")) if err != nil { return err } deployments, err := client.Esxclient.Deployments.GetAll() if err != nil { return err } initializeMigrationSpec := photon.InitializeMigrationOperation{} initializeMigrationSpec.SourceLoadBalancerAddress = sourceAddress // Initialize deployment migration for _, deployment := range deployments.Items { initializeMigrate, err := client.Esxclient.Deployments.InitializeDeploymentMigration(&initializeMigrationSpec, deployment.ID) if err != nil { return err } _, err = pollTask(initializeMigrate.ID) if err != nil { return err } fmt.Printf("Deployment '%s' migration started [source management endpoint: '%s'].\n", deployment.ID, sourceAddress) return nil } return nil }
func initLogs(ctx *cli.Context) { logger := log.StandardLogger() if ctx.GlobalBool("verbose") { logger.Level = log.DebugLevel } var ( isTerm = log.IsTerminal() json = ctx.GlobalBool("json") useColors = isTerm && !json ) if ctx.GlobalIsSet("colors") { useColors = ctx.GlobalBool("colors") } color.NoColor = !useColors if json { logger.Formatter = &log.JSONFormatter{} } else { formatter := &textformatter.TextFormatter{} formatter.DisableColors = !useColors logger.Formatter = formatter } }
// Finishes the copy state of source system into destination and makes this system the active one func deploymentMigrationFinalizeDeprecated(c *cli.Context) error { fmt.Printf("'%d'", len(c.Args())) err := checkArgNum(c.Args(), 1, "system migration finalize <old_management_endpoint>") if err != nil { return err } sourceAddress := c.Args().First() client.Esxclient, err = client.GetClient(c.GlobalIsSet("non-interactive")) if err != nil { return err } deployments, err := client.Esxclient.Deployments.GetAll() if err != nil { return err } finalizeMigrationSpec := photon.FinalizeMigrationOperation{} finalizeMigrationSpec.SourceLoadBalancerAddress = sourceAddress // Finalize deployment migration for _, deployment := range deployments.Items { finalizeMigrate, err := client.Esxclient.Deployments.FinalizeDeploymentMigration(&finalizeMigrationSpec, deployment.ID) if err != nil { return err } _, err = pollTask(finalizeMigrate.ID) if err != nil { return err } return nil } return nil }
func deleteNetwork(c *cli.Context) error { err := checkArgNum(c.Args(), 1, "network delete <id>") if err != nil { return err } id := c.Args().First() client.Esxclient, err = client.GetClient(utils.IsNonInteractive(c)) if err != nil { return err } task, err := client.Esxclient.Subnets.Delete(id) if err != nil { return err } if confirmed(c.GlobalIsSet("non-interactive")) { _, err = waitOnTaskOperation(task.ID, c) if err != nil { return err } } else { fmt.Println("OK. Canceled") } return nil }
func ResolveComMethod(c *cli.Context) (string, string, error) { if c.GlobalIsSet("a") && c.GlobalIsSet("t") { return c.GlobalString("t"), c.GlobalString("a"), nil } else { return "", "", errors.New("Transport flag error") } }
func showPhysicalNetwork(c *cli.Context, w io.Writer) error { err := checkArgCount(c, 1) if err != nil { return err } id := c.Args().First() client.Esxclient, err = client.GetClient(c) if err != nil { return err } network, err := client.Esxclient.Subnets.Get(id) if err != nil { return err } if c.GlobalIsSet("non-interactive") { portGroups := getCommaSeparatedStringFromStringArray(network.PortGroups) fmt.Printf("%s\t%s\t%s\t%s\t%s\t%t\n", network.ID, network.Name, network.State, portGroups, network.Description, network.IsDefault) } else if utils.NeedsFormatting(c) { utils.FormatObject(network, w, c) } else { fmt.Printf("Network ID: %s\n", network.ID) fmt.Printf(" Name: %s\n", network.Name) fmt.Printf(" State: %s\n", network.State) fmt.Printf(" Description: %s\n", network.Description) fmt.Printf(" Port Groups: %s\n", network.PortGroups) fmt.Printf(" Is Default: %t\n", network.IsDefault) } return nil }
func printHostList(hostList []photon.Host, w io.Writer, c *cli.Context) error { if c.GlobalIsSet("non-interactive") { for _, host := range hostList { tag := strings.Trim(fmt.Sprint(host.Tags), "[]") scriptTag := strings.Replace(tag, " ", ",", -1) fmt.Printf("%s\t%s\t%s\t%s\n", host.ID, host.State, host.Address, scriptTag) } } else if c.GlobalString("output") != "" { utils.FormatObjects(hostList, w, c) } else { w := new(tabwriter.Writer) w.Init(os.Stdout, 4, 4, 2, ' ', 0) fmt.Fprintf(w, "ID\tState\tIP\tTags\n") for _, host := range hostList { fmt.Fprintf(w, "%s\t%v\t%s\t%s\n", host.ID, host.State, host.Address, strings.Trim(fmt.Sprint(host.Tags), "[]")) } err := w.Flush() if err != nil { return err } fmt.Printf("\nTotal: %d\n", len(hostList)) } return nil }
// displays the migration status func showMigrationStatus(c *cli.Context) error { id, err := getDeploymentId(c) if err != nil { return err } client.Esxclient, err = client.GetClient(c.GlobalIsSet("non-interactive")) if err != nil { return err } deployment, err := client.Esxclient.Deployments.Get(id) if err != nil { return err } if deployment.Migration == nil { fmt.Print("No migration information available") return nil } migration := deployment.Migration if c.GlobalIsSet("non-interactive") { fmt.Printf("%d\t%d\t%d\t%d\t%d\n", migration.CompletedDataMigrationCycles, migration.DataMigrationCycleProgress, migration.DataMigrationCycleSize, migration.VibsUploaded, migration.VibsUploading+migration.VibsUploaded) } else { fmt.Printf(" Migration status:\n") fmt.Printf(" Completed data migration cycles: %d\n", migration.CompletedDataMigrationCycles) fmt.Printf(" Current data migration cycles progress: %d / %d\n", migration.DataMigrationCycleProgress, migration.DataMigrationCycleSize) fmt.Printf(" VIB upload progress: %d / %d\n", migration.VibsUploaded, migration.VibsUploading+migration.VibsUploaded) } return nil }
func revoke(c *cli.Context) { err := checkFolder(c.GlobalString("path")) if err != nil { logger().Fatalf("Cound not check/create path: %v", err) } conf := NewConfiguration(c) if !c.GlobalIsSet("email") { logger().Fatal("You have to pass an account (email address) to the program using --email or -m") } acc := NewAccount(c.GlobalString("email"), conf) client := acme.NewClient(c.GlobalString("server"), acc, conf.RsaBits(), conf.OptPort()) err = checkFolder(conf.CertPath()) if err != nil { logger().Fatalf("Cound not check/create path: %v", err) } for _, domain := range c.GlobalStringSlice("domains") { logger().Printf("Trying to revoke certificate for domain %s", domain) certPath := path.Join(conf.CertPath(), domain+".crt") certBytes, err := ioutil.ReadFile(certPath) err = client.RevokeCertificate(certBytes) if err != nil { logger().Printf("Error while revoking the certificate for domain %s\n\t%v", domain, err) } else { logger().Print("Certificate was revoked.") } } }
func detachDisk(c *cli.Context) error { err := checkArgNum(c.Args(), 1, "vm detach-disk --disk <id> <vm-id>") if err != nil { return err } id := c.Args().First() diskID := c.String("disk") client.Esxclient, err = client.GetClient(c.GlobalIsSet("non-interactive")) if err != nil { return err } operation := &photon.VmDiskOperation{ DiskID: diskID, } task, err := client.Esxclient.VMs.DetachDisk(id, operation) if err != nil { return err } _, err = waitOnTaskOperation(task.ID, c) if err != nil { return err } return nil }
func detachIso(c *cli.Context) error { err := checkArgNum(c.Args(), 1, "vm detach-iso <id>") if err != nil { return err } id := c.Args().First() client.Esxclient, err = client.GetClient(c.GlobalIsSet("non-interactive")) if err != nil { return err } task, err := client.Esxclient.VMs.DetachISO(id) if err != nil { return err } _, err = waitOnTaskOperation(task.ID, c) if err != nil { return err } return nil }
func restartVM(c *cli.Context) error { err := checkArgNum(c.Args(), 1, "vm restart <id>") if err != nil { return err } id := c.Args().First() client.Esxclient, err = client.GetClient(c.GlobalIsSet("non-interactive")) if err != nil { return err } opTask, err := client.Esxclient.VMs.Restart(id) if err != nil { return err } _, err = waitOnTaskOperation(opTask.ID, c) if err != nil { return err } return nil }
// Retrieves tasks for VM func getVMTasks(c *cli.Context) error { err := checkArgNum(c.Args(), 1, "vm tasks <id> [<options>]") if err != nil { return err } id := c.Args().First() state := c.String("state") client.Esxclient, err = client.GetClient(c.GlobalIsSet("non-interactive")) if err != nil { return err } options := &photon.TaskGetOptions{ State: state, } taskList, err := client.Esxclient.VMs.GetTasks(id, options) if err != nil { return err } err = printTaskList(taskList.Items, c) if err != nil { return err } return nil }
func getVMMksTicket(c *cli.Context) error { err := checkArgNum(c.Args(), 1, "vm mks-ticket <id>") if err != nil { return err } id := c.Args().First() client.Esxclient, err = client.GetClient(c.GlobalIsSet("non-interactive")) if err != nil { return err } task, err := client.Esxclient.VMs.GetMKSTicket(id) if err != nil { return err } if c.GlobalIsSet("non-interactive") { task, err := client.Esxclient.Tasks.Wait(task.ID) if err != nil { return err } mksTicket := task.ResourceProperties.(map[string]interface{}) fmt.Printf("%s\t%v\n", task.Entity.ID, mksTicket["ticket"]) } else { task, err = pollTask(task.ID) if err != nil { return err } mksTicket := task.ResourceProperties.(map[string]interface{}) fmt.Printf("VM ID: %s \nMks ticket ID is %v\n", task.Entity.ID, mksTicket["ticket"]) } return nil }
func setVMTag(c *cli.Context) error { err := checkArgNum(c.Args(), 1, "vm set-tag <id> [<options>]") if err != nil { return err } id := c.Args().First() tag := c.String("tag") vmTag := &photon.VmTag{} if len(tag) == 0 { return fmt.Errorf("Please input a tag") } vmTag.Tag = tag client.Esxclient, err = client.GetClient(c.GlobalIsSet("non-interactive")) if err != nil { return err } task, err := client.Esxclient.VMs.SetTag(id, vmTag) if err != nil { return err } _, err = waitOnTaskOperation(task.ID, c) if err != nil { return err } return nil }
// Retrieves a list of projects, returns an error if one occurred func listProjects(c *cli.Context, w io.Writer) error { err := checkArgCount(c, 0) if err != nil { return err } tenantName := c.String("tenant") client.Esxclient, err = client.GetClient(c) if err != nil { return err } tenant, err := verifyTenant(tenantName) if err != nil { return err } projects, err := client.Esxclient.Tenants.GetProjects(tenant.ID, nil) if err != nil { return err } if c.GlobalIsSet("non-interactive") { for _, t := range projects.Items { limits := quotaLineItemListToString(t.ResourceTicket.Limits) usage := quotaLineItemListToString(t.ResourceTicket.Usage) fmt.Printf("%s\t%s\t%s\t%s\n", t.ID, t.Name, limits, usage) } } else if utils.NeedsFormatting(c) { utils.FormatObjects(projects.Items, w, c) } else { w := new(tabwriter.Writer) w.Init(os.Stdout, 4, 4, 2, ' ', 0) fmt.Fprintf(w, "ID\tName\tLimit\tUsage\n") for _, t := range projects.Items { rt := t.ResourceTicket for i := 0; i < len(rt.Limits); i++ { if i == 0 { fmt.Fprintf(w, "%s\t%s\t%s %g %s\t%s %g %s\n", t.ID, t.Name, rt.Limits[i].Key, rt.Limits[i].Value, rt.Limits[i].Unit, rt.Usage[i].Key, rt.Usage[i].Value, rt.Usage[i].Unit) } else { fmt.Fprintf(w, "\t\t%s %g %s\t%s %g %s\n", rt.Limits[i].Key, rt.Limits[i].Value, rt.Limits[i].Unit, rt.Usage[i].Key, rt.Usage[i].Value, rt.Usage[i].Unit) } } for i := len(rt.Limits); i < len(rt.Usage); i++ { fmt.Fprintf(w, "\t\t\t%s %g %s\n", rt.Usage[i].Key, rt.Usage[i].Value, rt.Usage[i].Unit) } } err := w.Flush() if err != nil { return err } fmt.Printf("\nTotal projects: %d\n", len(projects.Items)) } return nil }