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