github.com/fozzysec/SiaPrime@v0.0.0-20190612043147-66c8e8d11fe3/cmd/spc/hostcmd.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"math/big"
     6  	"os"
     7  	"sort"
     8  	"strings"
     9  	"text/tabwriter"
    10  
    11  	"SiaPrime/crypto"
    12  	"SiaPrime/modules"
    13  	"SiaPrime/node/api/client"
    14  	"SiaPrime/types"
    15  
    16  	"github.com/spf13/cobra"
    17  )
    18  
    19  var (
    20  	hostAnnounceCmd = &cobra.Command{
    21  		Use:   "announce",
    22  		Short: "Announce yourself as a host",
    23  		Long: `Announce yourself as a host on the network.
    24  Announcing will also configure the host to start accepting contracts.
    25  You can revert this by running:
    26  	spc host config acceptingcontracts false
    27  You may also supply a specific address to be announced, e.g.:
    28  	spc host announce my-host-domain.com:9001
    29  Doing so will override the standard connectivity checks.`,
    30  		Run: hostannouncecmd,
    31  	}
    32  
    33  	hostCmd = &cobra.Command{
    34  		Use:   "host",
    35  		Short: "Perform host actions",
    36  		Long:  "View or modify host settings.",
    37  		Run:   wrap(hostcmd),
    38  	}
    39  
    40  	hostConfigCmd = &cobra.Command{
    41  		Use:   "config [setting] [value]",
    42  		Short: "Modify host settings",
    43  		Long: `Modify host settings.
    44  
    45  Available settings:
    46       acceptingcontracts:   boolean
    47       maxduration:          blocks
    48       maxdownloadbatchsize: bytes
    49       maxrevisebatchsize:   bytes
    50       netaddress:           string
    51       windowsize:           blocks
    52  
    53       collateral:       currency
    54       collateralbudget: currency
    55       maxcollateral:    currency
    56  
    57       mincontractprice:          currency
    58       mindownloadbandwidthprice: currency / TB
    59       minstorageprice:           currency / TB / Month
    60       minuploadbandwidthprice:   currency / TB
    61  
    62  Currency units can be specified, e.g. 10SCP; run 'spc help wallet' for details.
    63  
    64  Durations (maxduration and windowsize) must be specified in either blocks (b),
    65  hours (h), days (d), or weeks (w). A block is approximately 10 minutes, so one
    66  hour is six blocks, a day is 144 blocks, and a week is 1008 blocks.
    67  
    68  For a description of each parameter, see doc/API.md.
    69  
    70  To configure the host to accept new contracts, set acceptingcontracts to true:
    71  	spc host config acceptingcontracts true
    72  `,
    73  		Run: wrap(hostconfigcmd),
    74  	}
    75  
    76  	hostContractCmd = &cobra.Command{
    77  		Use:   "contracts",
    78  		Short: "Show host contracts",
    79  		Long: `Show host contracts sorted by expiration height.
    80  
    81  Available output types:
    82       value:  show financial information
    83       status: show status information
    84  `,
    85  		Run: wrap(hostcontractcmd),
    86  	}
    87  
    88  	hostFolderAddCmd = &cobra.Command{
    89  		Use:   "add [path] [size]",
    90  		Short: "Add a storage folder to the host",
    91  		Long:  "Add a storage folder to the host, specifying how much data it should store",
    92  		Run:   wrap(hostfolderaddcmd),
    93  	}
    94  
    95  	hostFolderCmd = &cobra.Command{
    96  		Use:   "folder",
    97  		Short: "Add, remove, or resize a storage folder",
    98  		Long:  "Add, remove, or resize a storage folder.",
    99  	}
   100  
   101  	hostFolderRemoveCmd = &cobra.Command{
   102  		Use:   "remove [path]",
   103  		Short: "Remove a storage folder from the host",
   104  		Long: `Remove a storage folder from the host. Note that this does not delete any
   105  data; it will instead be distributed across the remaining storage folders.`,
   106  
   107  		Run: wrap(hostfolderremovecmd),
   108  	}
   109  
   110  	hostFolderResizeCmd = &cobra.Command{
   111  		Use:   "resize [path] [size]",
   112  		Short: "Resize a storage folder",
   113  		Long: `Change how much data a storage folder should store. If the new size is less
   114  than what the folder is currently storing, data will be distributed across the
   115  other storage folders.`,
   116  		Run: wrap(hostfolderresizecmd),
   117  	}
   118  
   119  	hostSectorCmd = &cobra.Command{
   120  		Use:   "sector",
   121  		Short: "Add or delete a sector (add not supported)",
   122  		Long: `Add or delete a sector. Adding is not currently supported. Note that
   123  deleting a sector may impact host revenue.`,
   124  	}
   125  
   126  	hostSectorDeleteCmd = &cobra.Command{
   127  		Use:   "delete [root]",
   128  		Short: "Delete a sector",
   129  		Long: `Delete a sector, identified by its Merkle root. Note that deleting a
   130  sector may impact host revenue.`,
   131  		Run: wrap(hostsectordeletecmd),
   132  	}
   133  )
   134  
   135  // hostcmd is the handler for the command `siac host`.
   136  // Prints info about the host and its storage folders.
   137  func hostcmd() {
   138  	hg, err := httpClient.HostGet()
   139  	if err != nil {
   140  		die("Could not fetch host settings:", err)
   141  	}
   142  	sg, err := httpClient.HostStorageGet()
   143  	if err != nil {
   144  		die("Could not fetch storage info:", err)
   145  	}
   146  
   147  	es := hg.ExternalSettings
   148  	fm := hg.FinancialMetrics
   149  	is := hg.InternalSettings
   150  	nm := hg.NetworkMetrics
   151  
   152  	// calculate total storage available and remaining
   153  	var totalstorage, storageremaining uint64
   154  	for _, folder := range sg.Folders {
   155  		totalstorage += folder.Capacity
   156  		storageremaining += folder.CapacityRemaining
   157  	}
   158  
   159  	// convert price from bytes/block to TB/Month
   160  	price := currencyUnits(is.MinStoragePrice.Mul(modules.BlockBytesPerMonthTerabyte))
   161  	// calculate total revenue
   162  	totalRevenue := fm.ContractCompensation.
   163  		Add(fm.StorageRevenue).
   164  		Add(fm.DownloadBandwidthRevenue).
   165  		Add(fm.UploadBandwidthRevenue)
   166  	totalPotentialRevenue := fm.PotentialContractCompensation.
   167  		Add(fm.PotentialStorageRevenue).
   168  		Add(fm.PotentialDownloadBandwidthRevenue).
   169  		Add(fm.PotentialUploadBandwidthRevenue)
   170  	// determine the display method for the net address.
   171  	netaddr := es.NetAddress
   172  	if is.NetAddress == "" {
   173  		netaddr += " (automatically determined)"
   174  	} else {
   175  		netaddr += " (manually specified)"
   176  	}
   177  
   178  	var connectabilityString string
   179  	if hg.WorkingStatus == "working" {
   180  		connectabilityString = "Host appears to be working."
   181  	} else if hg.WorkingStatus == "not working" && hg.ConnectabilityStatus == "connectable" {
   182  		connectabilityString = "Nobody is connecting to host. Try re-announcing."
   183  	} else if hg.WorkingStatus == "checking" || hg.ConnectabilityStatus == "checking" {
   184  		connectabilityString = "Host is checking status (takes a few minues)."
   185  	} else {
   186  		connectabilityString = "Host is not connectable (re-checks every few minutes)."
   187  	}
   188  
   189  	if hostVerbose {
   190  		// describe net address
   191  		fmt.Printf(`General Info:
   192  	Connectability Status: %v
   193  
   194  Host Internal Settings:
   195  	acceptingcontracts:   %v
   196  	maxduration:          %v Weeks
   197  	maxdownloadbatchsize: %v
   198  	maxrevisebatchsize:   %v
   199  	netaddress:           %v
   200  	windowsize:           %v Hours
   201  
   202  	collateral:       %v / TB / Month
   203  	collateralbudget: %v
   204  	maxcollateral:    %v Per Contract
   205  
   206  	mincontractprice:          %v
   207  	mindownloadbandwidthprice: %v / TB
   208  	minstorageprice:           %v / TB / Month
   209  	minuploadbandwidthprice:   %v / TB
   210  
   211  Host Financials:
   212  	Contract Count:               %v
   213  	Transaction Fee Compensation: %v
   214  	Potential Fee Compensation:   %v
   215  	Transaction Fee Expenses:     %v
   216  
   217  	Storage Revenue:           %v
   218  	Potential Storage Revenue: %v
   219  
   220  	Locked Collateral: %v
   221  	Risked Collateral: %v
   222  	Lost Collateral:   %v
   223  
   224  	Download Revenue:           %v
   225  	Potential Download Revenue: %v
   226  	Upload Revenue:             %v
   227  	Potential Upload Revenue:   %v
   228  
   229  RPC Stats:
   230  	Error Calls:        %v
   231  	Unrecognized Calls: %v
   232  	Download Calls:     %v
   233  	Renew Calls:        %v
   234  	Revise Calls:       %v
   235  	Settings Calls:     %v
   236  	FormContract Calls: %v
   237  `,
   238  			connectabilityString,
   239  
   240  			yesNo(is.AcceptingContracts), periodUnits(is.MaxDuration),
   241  			filesizeUnits(int64(is.MaxDownloadBatchSize)),
   242  			filesizeUnits(int64(is.MaxReviseBatchSize)), netaddr,
   243  			is.WindowSize/6,
   244  
   245  			currencyUnits(is.Collateral.Mul(modules.BlockBytesPerMonthTerabyte)),
   246  			currencyUnits(is.CollateralBudget),
   247  			currencyUnits(is.MaxCollateral),
   248  
   249  			currencyUnits(is.MinContractPrice),
   250  			currencyUnits(is.MinDownloadBandwidthPrice.Mul(modules.BytesPerTerabyte)),
   251  			currencyUnits(is.MinStoragePrice.Mul(modules.BlockBytesPerMonthTerabyte)),
   252  			currencyUnits(is.MinUploadBandwidthPrice.Mul(modules.BytesPerTerabyte)),
   253  
   254  			fm.ContractCount, currencyUnits(fm.ContractCompensation),
   255  			currencyUnits(fm.PotentialContractCompensation),
   256  			currencyUnits(fm.TransactionFeeExpenses),
   257  
   258  			currencyUnits(fm.StorageRevenue),
   259  			currencyUnits(fm.PotentialStorageRevenue),
   260  
   261  			currencyUnits(fm.LockedStorageCollateral),
   262  			currencyUnits(fm.RiskedStorageCollateral),
   263  			currencyUnits(fm.LostStorageCollateral),
   264  
   265  			currencyUnits(fm.DownloadBandwidthRevenue),
   266  			currencyUnits(fm.PotentialDownloadBandwidthRevenue),
   267  			currencyUnits(fm.UploadBandwidthRevenue),
   268  			currencyUnits(fm.PotentialUploadBandwidthRevenue),
   269  
   270  			nm.ErrorCalls, nm.UnrecognizedCalls, nm.DownloadCalls,
   271  			nm.RenewCalls, nm.ReviseCalls, nm.SettingsCalls,
   272  			nm.FormContractCalls)
   273  	} else {
   274  		fmt.Printf(`Host info:
   275  	Connectability Status: %v
   276  
   277  	Storage:      %v (%v used)
   278  	Price:        %v / TB / Month
   279  	Max Duration: %v Weeks
   280  
   281  	Accepting Contracts:  %v
   282  	Anticipated Revenue:  %v
   283  	Locked Collateral:    %v
   284  	Revenue:              %v
   285  `,
   286  			connectabilityString,
   287  
   288  			filesizeUnits(int64(totalstorage)),
   289  			filesizeUnits(int64(totalstorage-storageremaining)), price,
   290  			periodUnits(is.MaxDuration),
   291  
   292  			yesNo(is.AcceptingContracts), currencyUnits(totalPotentialRevenue),
   293  			currencyUnits(fm.LockedStorageCollateral),
   294  			currencyUnits(totalRevenue))
   295  	}
   296  
   297  	// if wallet is locked print warning
   298  	walletstatus, walleterr := httpClient.WalletGet()
   299  	if walleterr != nil {
   300  		fmt.Print("\nWarning:\n	Could not get wallet status. A working wallet is needed in order to operate your host. Error: ")
   301  		fmt.Println(walleterr)
   302  	} else if !walletstatus.Unlocked {
   303  		fmt.Println("\nWarning:\n	Your wallet is locked. You must unlock your wallet for the host to function properly.")
   304  	}
   305  
   306  	fmt.Println("\nStorage Folders:")
   307  
   308  	// display storage folder info
   309  	sort.Slice(sg.Folders, func(i, j int) bool {
   310  		return sg.Folders[i].Path < sg.Folders[j].Path
   311  	})
   312  	if len(sg.Folders) == 0 {
   313  		fmt.Println("No storage folders configured")
   314  		return
   315  	}
   316  	w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0)
   317  	fmt.Fprintf(w, "\tUsed\tCapacity\t%% Used\tPath\n")
   318  	for _, folder := range sg.Folders {
   319  		curSize := int64(folder.Capacity - folder.CapacityRemaining)
   320  		pctUsed := 100 * (float64(curSize) / float64(folder.Capacity))
   321  		fmt.Fprintf(w, "\t%s\t%s\t%.2f\t%s\n", filesizeUnits(curSize), filesizeUnits(int64(folder.Capacity)), pctUsed, folder.Path)
   322  	}
   323  	w.Flush()
   324  }
   325  
   326  // hostconfigcmd is the handler for the command `siac host config [setting] [value]`.
   327  // Modifies host settings.
   328  func hostconfigcmd(param, value string) {
   329  	var err error
   330  	switch param {
   331  	// currency (convert to hastings)
   332  	case "collateralbudget", "maxcollateral", "mincontractprice":
   333  		value, err = parseCurrency(value)
   334  		if err != nil {
   335  			die("Could not parse "+param+":", err)
   336  		}
   337  
   338  	// currency/TB (convert to hastings/byte)
   339  	case "mindownloadbandwidthprice", "minuploadbandwidthprice":
   340  		hastings, err := parseCurrency(value)
   341  		if err != nil {
   342  			die("Could not parse "+param+":", err)
   343  		}
   344  		i, _ := new(big.Int).SetString(hastings, 10)
   345  		c := types.NewCurrency(i).Div(modules.BytesPerTerabyte)
   346  		value = c.String()
   347  
   348  	// currency/TB/month (convert to hastings/byte/block)
   349  	case "collateral", "minstorageprice":
   350  		hastings, err := parseCurrency(value)
   351  		if err != nil {
   352  			die("Could not parse "+param+":", err)
   353  		}
   354  		i, _ := new(big.Int).SetString(hastings, 10)
   355  		c := types.NewCurrency(i).Div(modules.BlockBytesPerMonthTerabyte)
   356  		value = c.String()
   357  
   358  	// bool (allow "yes" and "no")
   359  	case "acceptingcontracts":
   360  		switch strings.ToLower(value) {
   361  		case "yes":
   362  			value = "true"
   363  		case "no":
   364  			value = "false"
   365  		}
   366  
   367  	// duration (convert to blocks)
   368  	case "maxduration", "windowsize":
   369  		value, err = parsePeriod(value)
   370  		if err != nil {
   371  			die("Could not parse "+param+":", err)
   372  		}
   373  
   374  	// other valid settings
   375  	case "maxdownloadbatchsize", "maxrevisebatchsize", "netaddress":
   376  
   377  	// invalid settings
   378  	default:
   379  		die("\"" + param + "\" is not a host setting")
   380  	}
   381  	err = httpClient.HostModifySettingPost(client.HostParam(param), value)
   382  	if err != nil {
   383  		die("Failed to update host settings:", err)
   384  	}
   385  	fmt.Println("Host settings updated.")
   386  
   387  	// get the estimated conversion rate.
   388  	eg, err := httpClient.HostEstimateScoreGet(param, value)
   389  	if err != nil {
   390  		if err.Error() == "cannot call /host/estimatescore without the renter module" {
   391  			// score estimate requires the renter module
   392  			return
   393  		}
   394  		die("could not get host score estimate:", err)
   395  	}
   396  	fmt.Printf("Estimated conversion rate: %v%%\n", eg.ConversionRate)
   397  }
   398  
   399  // hostcontractcmd is the handler for the command `siac host contracts [type]`.
   400  func hostcontractcmd() {
   401  	cg, err := httpClient.HostContractInfoGet()
   402  	if err != nil {
   403  		die("Could not fetch host contract info:", err)
   404  	}
   405  	sort.Slice(cg.Contracts, func(i, j int) bool { return cg.Contracts[i].ExpirationHeight < cg.Contracts[j].ExpirationHeight })
   406  	w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0)
   407  	switch hostContractOutputType {
   408  	case "value":
   409  		fmt.Fprintf(w, "Obligation Id\tObligation Status\tContract Cost\tLocked Collateral\tRisked Collateral\tPotential Revenue\tExpiration Height\tTransaction Fees\n")
   410  		for _, so := range cg.Contracts {
   411  			potentialRevenue := so.PotentialDownloadRevenue.Add(so.PotentialUploadRevenue).Add(so.PotentialStorageRevenue)
   412  			fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%d\t%s\n", so.ObligationId, strings.TrimPrefix(so.ObligationStatus, "obligation"), currencyUnits(so.ContractCost), currencyUnits(so.LockedCollateral),
   413  				currencyUnits(so.RiskedCollateral), currencyUnits(potentialRevenue), so.ExpirationHeight, currencyUnits(so.TransactionFeesAdded))
   414  		}
   415  	case "status":
   416  		fmt.Fprintf(w, "Obligation ID\tObligation Status\tExpiration Height\tOrigin Confirmed\tRevision Constructed\tRevision Confirmed\tProof Constructed\tProof Confirmed\n")
   417  		for _, so := range cg.Contracts {
   418  			fmt.Fprintf(w, "%s\t%s\t%d\t%t\t%t\t%t\t%t\t%t\n", so.ObligationId, strings.TrimPrefix(so.ObligationStatus, "obligation"), so.ExpirationHeight, so.OriginConfirmed,
   419  				so.RevisionConstructed, so.RevisionConfirmed, so.ProofConstructed, so.ProofConfirmed)
   420  		}
   421  	default:
   422  		die("\"" + hostContractOutputType + "\" is not a format")
   423  	}
   424  	w.Flush()
   425  }
   426  
   427  // hostannouncecmd is the handler for the command `siac host announce`.
   428  // Announces yourself as a host to the network. Optionally takes an address to
   429  // announce as.
   430  func hostannouncecmd(cmd *cobra.Command, args []string) {
   431  	var err error
   432  	switch len(args) {
   433  	case 0:
   434  		err = httpClient.HostAnnouncePost()
   435  	case 1:
   436  		err = httpClient.HostAnnounceAddrPost(modules.NetAddress(args[0]))
   437  	default:
   438  		cmd.UsageFunc()(cmd)
   439  		os.Exit(exitCodeUsage)
   440  	}
   441  	if err != nil {
   442  		die("Could not announce host:", err)
   443  	}
   444  	fmt.Println("Host announcement submitted to network.")
   445  
   446  	// start accepting contracts
   447  	err = httpClient.HostModifySettingPost(client.HostParamAcceptingContracts, true)
   448  	if err != nil {
   449  		die("Could not configure host to accept contracts:", err)
   450  	}
   451  	fmt.Println(`The host has also been configured to accept contracts.
   452  To revert this, run:
   453  	spc host config acceptingcontracts false`)
   454  }
   455  
   456  // hostfolderaddcmd adds a folder to the host.
   457  func hostfolderaddcmd(path, size string) {
   458  	size, err := parseFilesize(size)
   459  	if err != nil {
   460  		die("Could not parse size:", err)
   461  	}
   462  	// round size down to nearest multiple of 256MiB
   463  	var sizeUint64 uint64
   464  	fmt.Sscan(size, &sizeUint64)
   465  	sizeUint64 /= 64 * modules.SectorSize
   466  	sizeUint64 *= 64 * modules.SectorSize
   467  
   468  	err = httpClient.HostStorageFoldersAddPost(abs(path), sizeUint64)
   469  	if err != nil {
   470  		die("Could not add folder:", err)
   471  	}
   472  	fmt.Println("Added folder", path)
   473  }
   474  
   475  // hostfolderremovecmd removes a folder from the host.
   476  func hostfolderremovecmd(path string) {
   477  	err := httpClient.HostStorageFoldersRemovePost(abs(path))
   478  	if err != nil {
   479  		die("Could not remove folder:", err)
   480  	}
   481  	fmt.Println("Removed folder", path)
   482  }
   483  
   484  // hostfolderresizecmd resizes a folder in the host.
   485  func hostfolderresizecmd(path, newsize string) {
   486  	newsize, err := parseFilesize(newsize)
   487  	if err != nil {
   488  		die("Could not parse size:", err)
   489  	}
   490  	// round size down to nearest multiple of 256MiB
   491  	var sizeUint64 uint64
   492  	fmt.Sscan(newsize, &sizeUint64)
   493  	sizeUint64 /= 64 * modules.SectorSize
   494  	sizeUint64 *= 64 * modules.SectorSize
   495  
   496  	err = httpClient.HostStorageFoldersResizePost(abs(path), sizeUint64)
   497  	if err != nil {
   498  		die("Could not resize folder:", err)
   499  	}
   500  	fmt.Printf("Resized folder %v to %v\n", path, newsize)
   501  }
   502  
   503  // hostsectordeletecmd deletes a sector from the host.
   504  func hostsectordeletecmd(root string) {
   505  	var hash crypto.Hash
   506  	err := hash.LoadString(root)
   507  	if err != nil {
   508  		die("Could not parse root:", err)
   509  	}
   510  	err = httpClient.HostStorageSectorsDeletePost(hash)
   511  	if err != nil {
   512  		die("Could not delete sector:", err)
   513  	}
   514  	fmt.Println("Deleted sector", root)
   515  }