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 }