01ec229749
* feat: curio: Migrate lotus-miner sector metadata into Curio DB * curio seal: Transfer seal pipeline entries to long-term sector metadata table * curio: Only open db in sectors migrate cmd * curio: Add an envvar to force migration * curio: Debugging sector migration * curio: Fix typo in table name * curio: Plumb KeepUnsealed into the sealing pipeline * Don't add redundant keep_data to sdr pipeline pieces table * fix transferFinalizedSectorData where check * ui for sector fail --------- Co-authored-by: Andrew Jackson (Ajax) <snadrus@gmail.com>
897 lines
28 KiB
Go
897 lines
28 KiB
Go
// guidedSetup for migration from lotus-miner to Curio
|
|
//
|
|
// IF STRINGS CHANGED {
|
|
// follow instructions at ../internal/translations/translations.go
|
|
// }
|
|
package guidedsetup
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math/bits"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"path"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/docker/go-units"
|
|
"github.com/manifoldco/promptui"
|
|
"github.com/mitchellh/go-homedir"
|
|
"github.com/samber/lo"
|
|
"github.com/urfave/cli/v2"
|
|
"golang.org/x/text/language"
|
|
"golang.org/x/text/message"
|
|
|
|
"github.com/filecoin-project/go-address"
|
|
"github.com/filecoin-project/go-jsonrpc"
|
|
"github.com/filecoin-project/go-state-types/abi"
|
|
|
|
"github.com/filecoin-project/lotus/api"
|
|
"github.com/filecoin-project/lotus/api/v1api"
|
|
"github.com/filecoin-project/lotus/build"
|
|
"github.com/filecoin-project/lotus/chain/types"
|
|
"github.com/filecoin-project/lotus/cli/spcli"
|
|
cliutil "github.com/filecoin-project/lotus/cli/util"
|
|
"github.com/filecoin-project/lotus/cmd/curio/deps"
|
|
_ "github.com/filecoin-project/lotus/cmd/curio/internal/translations"
|
|
"github.com/filecoin-project/lotus/lib/harmony/harmonydb"
|
|
"github.com/filecoin-project/lotus/node/config"
|
|
"github.com/filecoin-project/lotus/node/repo"
|
|
)
|
|
|
|
// URL to upload user-selected fields to help direct developer's focus.
|
|
const DeveloperFocusRequestURL = "https://curiostorage.org/cgi-bin/savedata.php"
|
|
|
|
var GuidedsetupCmd = &cli.Command{
|
|
Name: "guided-setup",
|
|
Usage: "Run the guided setup for migrating from lotus-miner to Curio or Creating a new Curio miner",
|
|
Flags: []cli.Flag{
|
|
&cli.StringFlag{ // for cliutil.GetFullNodeAPI
|
|
Name: "repo",
|
|
EnvVars: []string{"LOTUS_PATH"},
|
|
Hidden: true,
|
|
Value: "~/.lotus",
|
|
},
|
|
},
|
|
Action: func(cctx *cli.Context) (err error) {
|
|
T, say := SetupLanguage()
|
|
setupCtrlC(say)
|
|
|
|
// Run the migration steps
|
|
migrationData := MigrationData{
|
|
T: T,
|
|
say: say,
|
|
selectTemplates: &promptui.SelectTemplates{
|
|
Help: T("Use the arrow keys to navigate: ↓ ↑ → ← "),
|
|
},
|
|
cctx: cctx,
|
|
ctx: cctx.Context,
|
|
}
|
|
|
|
newOrMigrate(&migrationData)
|
|
if migrationData.init {
|
|
say(header, "This interactive tool creates a new miner actor and creates the basic configuration layer for it.")
|
|
say(notice, "This process is partially idempotent. Once a new miner actor has been created and subsequent steps fail, the user need to run 'curio config new-cluster < miner ID >' to finish the configuration.")
|
|
for _, step := range newMinerSteps {
|
|
step(&migrationData)
|
|
}
|
|
} else {
|
|
say(header, "This interactive tool migrates lotus-miner to Curio in 5 minutes.")
|
|
say(notice, "Each step needs your confirmation and can be reversed. Press Ctrl+C to exit at any time.")
|
|
|
|
for _, step := range migrationSteps {
|
|
step(&migrationData)
|
|
}
|
|
}
|
|
|
|
for _, closer := range migrationData.closers {
|
|
closer()
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
func setupCtrlC(say func(style lipgloss.Style, key message.Reference, a ...interface{})) {
|
|
c := make(chan os.Signal, 1)
|
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
|
go func() {
|
|
<-c
|
|
say(notice, "Ctrl+C pressed in Terminal")
|
|
os.Exit(2)
|
|
}()
|
|
}
|
|
|
|
var (
|
|
header = lipgloss.NewStyle().
|
|
Align(lipgloss.Left).
|
|
Foreground(lipgloss.Color("#00FF00")).
|
|
Background(lipgloss.Color("#242424")).
|
|
BorderStyle(lipgloss.NormalBorder()).
|
|
Width(60).Margin(1)
|
|
|
|
notice = lipgloss.NewStyle().
|
|
Align(lipgloss.Left).
|
|
Bold(true).
|
|
Foreground(lipgloss.Color("#CCCCCC")).
|
|
Background(lipgloss.Color("#333300")).MarginBottom(1)
|
|
|
|
green = lipgloss.NewStyle().
|
|
Align(lipgloss.Left).
|
|
Foreground(lipgloss.Color("#00FF00")).
|
|
Background(lipgloss.Color("#000000"))
|
|
|
|
plain = lipgloss.NewStyle().Align(lipgloss.Left)
|
|
|
|
section = lipgloss.NewStyle().
|
|
Align(lipgloss.Left).
|
|
Foreground(lipgloss.Color("#000000")).
|
|
Background(lipgloss.Color("#FFFFFF")).
|
|
Underline(true)
|
|
|
|
code = lipgloss.NewStyle().
|
|
Align(lipgloss.Left).
|
|
Foreground(lipgloss.Color("#00FF00")).
|
|
Background(lipgloss.Color("#f8f9fa"))
|
|
)
|
|
|
|
func SetupLanguage() (func(key message.Reference, a ...interface{}) string, func(style lipgloss.Style, key message.Reference, a ...interface{})) {
|
|
langText := "en"
|
|
problem := false
|
|
if len(os.Getenv("LANG")) > 1 {
|
|
langText = os.Getenv("LANG")[:2]
|
|
} else {
|
|
problem = true
|
|
}
|
|
|
|
lang, err := language.Parse(langText)
|
|
if err != nil {
|
|
lang = language.English
|
|
problem = true
|
|
fmt.Println("Error parsing language")
|
|
}
|
|
|
|
langs := message.DefaultCatalog.Languages()
|
|
have := lo.SliceToMap(langs, func(t language.Tag) (string, bool) { return t.String(), true })
|
|
if _, ok := have[lang.String()]; !ok {
|
|
lang = language.English
|
|
problem = true
|
|
}
|
|
if problem {
|
|
_ = os.Setenv("LANG", "en-US") // for later users of this function
|
|
notice.Copy().AlignHorizontal(lipgloss.Right).
|
|
Render("$LANG=" + langText + " unsupported. Available: " + strings.Join(lo.Keys(have), ", "))
|
|
fmt.Println("Defaulting to English. Please reach out to the Curio team if you would like to have additional language support.")
|
|
}
|
|
return func(key message.Reference, a ...interface{}) string {
|
|
return message.NewPrinter(lang).Sprintf(key, a...)
|
|
}, func(sty lipgloss.Style, key message.Reference, a ...interface{}) {
|
|
msg := message.NewPrinter(lang).Sprintf(key, a...)
|
|
fmt.Println(sty.Render(msg))
|
|
}
|
|
}
|
|
|
|
func newOrMigrate(d *MigrationData) {
|
|
i, _, err := (&promptui.Select{
|
|
Label: d.T("I want to:"),
|
|
Items: []string{
|
|
d.T("Migrate from existing Lotus-Miner"),
|
|
d.T("Create a new miner")},
|
|
Templates: d.selectTemplates,
|
|
}).Run()
|
|
if err != nil {
|
|
d.say(notice, "Aborting remaining steps.", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
if i == 1 {
|
|
d.init = true
|
|
}
|
|
}
|
|
|
|
type migrationStep func(*MigrationData)
|
|
|
|
var migrationSteps = []migrationStep{
|
|
readMinerConfig, // Tells them to be on the miner machine
|
|
yugabyteConnect, // Miner is updated
|
|
configToDB, // work on base configuration migration.
|
|
verifySectors, // Verify the sectors are in the database
|
|
doc,
|
|
oneLastThing,
|
|
complete,
|
|
}
|
|
|
|
type newMinerStep func(data *MigrationData)
|
|
|
|
var newMinerSteps = []newMinerStep{
|
|
stepPresteps,
|
|
stepCreateActor,
|
|
stepNewMinerConfig,
|
|
doc,
|
|
oneLastThing,
|
|
completeInit,
|
|
}
|
|
|
|
type MigrationData struct {
|
|
T func(key message.Reference, a ...interface{}) string
|
|
say func(style lipgloss.Style, key message.Reference, a ...interface{})
|
|
selectTemplates *promptui.SelectTemplates
|
|
MinerConfigPath string
|
|
MinerConfig *config.StorageMiner
|
|
DB *harmonydb.DB
|
|
MinerID address.Address
|
|
full v1api.FullNode
|
|
cctx *cli.Context
|
|
closers []jsonrpc.ClientCloser
|
|
ctx context.Context
|
|
owner address.Address
|
|
worker address.Address
|
|
sender address.Address
|
|
ssize abi.SectorSize
|
|
confidence uint64
|
|
init bool
|
|
}
|
|
|
|
func complete(d *MigrationData) {
|
|
stepCompleted(d, d.T("Lotus-Miner to Curio Migration."))
|
|
d.say(plain, "Try the web interface with %s for further guided improvements.", code.Render("curio run --layers=gui"))
|
|
d.say(plain, "You can now migrate your market node (%s), if applicable.", "Boost")
|
|
}
|
|
|
|
func completeInit(d *MigrationData) {
|
|
stepCompleted(d, d.T("New Miner initialization complete."))
|
|
d.say(plain, "Try the web interface with %s for further guided improvements.", "--layers=gui")
|
|
}
|
|
|
|
func configToDB(d *MigrationData) {
|
|
d.say(section, "Migrating lotus-miner config.toml to Curio in-database configuration.")
|
|
|
|
{
|
|
var closer jsonrpc.ClientCloser
|
|
var err error
|
|
d.full, closer, err = cliutil.GetFullNodeAPIV1(d.cctx)
|
|
d.closers = append(d.closers, closer)
|
|
if err != nil {
|
|
d.say(notice, "Error getting API: %s", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
ainfo, err := cliutil.GetAPIInfo(d.cctx, repo.FullNode)
|
|
if err != nil {
|
|
d.say(notice, "could not get API info for FullNode: %w", err)
|
|
os.Exit(1)
|
|
}
|
|
token, err := d.full.AuthNew(context.Background(), api.AllPermissions)
|
|
if err != nil {
|
|
d.say(notice, "Error getting token: %s", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
chainApiInfo := fmt.Sprintf("%s:%s", string(token), ainfo.Addr)
|
|
|
|
shouldErrPrompt := func() bool {
|
|
i, _, err := (&promptui.Select{
|
|
Label: d.T("Unmigratable sectors found. Do you want to continue?"),
|
|
Items: []string{
|
|
d.T("Yes, continue"),
|
|
d.T("No, abort")},
|
|
Templates: d.selectTemplates,
|
|
}).Run()
|
|
if err != nil {
|
|
d.say(notice, "Aborting migration.", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
return i == 1
|
|
}
|
|
|
|
d.MinerID, err = SaveConfigToLayerMigrateSectors(d.MinerConfigPath, chainApiInfo, shouldErrPrompt)
|
|
if err != nil {
|
|
d.say(notice, "Error saving config to layer: %s. Aborting Migration", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// bucket returns the power's 4 highest bits (rounded down).
|
|
func bucket(power *api.MinerPower) uint64 {
|
|
rawQAP := power.TotalPower.QualityAdjPower.Uint64()
|
|
magnitude := lo.Max([]int{bits.Len64(rawQAP), 5})
|
|
|
|
// shifting erases resolution so we cannot distinguish SPs of similar scales.
|
|
return rawQAP >> (uint64(magnitude) - 4) << (uint64(magnitude - 4))
|
|
}
|
|
|
|
type uploadType int
|
|
|
|
const uploadTypeIndividual uploadType = 0
|
|
const uploadTypeAggregate uploadType = 1
|
|
|
|
// const uploadTypeHint uploadType = 2
|
|
const uploadTypeNothing uploadType = 3
|
|
|
|
func oneLastThing(d *MigrationData) {
|
|
d.say(section, "The Curio team wants to improve the software you use. Tell the team you're using `%s`.", "curio")
|
|
i, _, err := (&promptui.Select{
|
|
Label: d.T("Select what you want to share with the Curio team."),
|
|
Items: []string{
|
|
d.T("Individual Data: Miner ID, Curio version, chain (%s or %s). Signed.", "mainnet", "calibration"),
|
|
d.T("Aggregate-Anonymous: version, chain, and Miner power (bucketed)."),
|
|
d.T("Hint: I am someone running Curio on whichever chain."),
|
|
d.T("Nothing.")},
|
|
Templates: d.selectTemplates,
|
|
}).Run()
|
|
preference := uploadType(i)
|
|
if err != nil {
|
|
d.say(notice, "Aborting remaining steps.", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
if preference != uploadTypeNothing {
|
|
msgMap := map[string]any{
|
|
"domain": "curio-newuser",
|
|
"net": build.BuildTypeString(),
|
|
}
|
|
if preference == uploadTypeIndividual || preference == uploadTypeAggregate {
|
|
// articles of incorporation
|
|
power, err := d.full.StateMinerPower(context.Background(), d.MinerID, types.EmptyTSK)
|
|
if err != nil {
|
|
d.say(notice, "Error getting miner power: %s", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
msgMap["version"] = build.BuildVersion
|
|
msgMap["net"] = build.BuildType
|
|
msgMap["power"] = map[uploadType]uint64{
|
|
uploadTypeIndividual: power.MinerPower.QualityAdjPower.Uint64(),
|
|
uploadTypeAggregate: bucket(power)}[preference]
|
|
|
|
if preference == uploadTypeIndividual { // Sign it
|
|
msgMap["miner_id"] = d.MinerID
|
|
msg, err := json.Marshal(msgMap)
|
|
if err != nil {
|
|
d.say(notice, "Error marshalling message: %s", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
mi, err := d.full.StateMinerInfo(context.Background(), d.MinerID, types.EmptyTSK)
|
|
if err != nil {
|
|
d.say(notice, "Error getting miner info: %s", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
sig, err := d.full.WalletSign(context.Background(), mi.Worker, msg)
|
|
if err != nil {
|
|
d.say(notice, "Error signing message: %s", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
msgMap["signature"] = base64.StdEncoding.EncodeToString(sig.Data)
|
|
}
|
|
}
|
|
msg, err := json.Marshal(msgMap)
|
|
if err != nil {
|
|
d.say(notice, "Error marshalling message: %s", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Post(DeveloperFocusRequestURL, "application/json", bytes.NewReader(msg))
|
|
if err != nil {
|
|
d.say(notice, "Error sending message: %s", err.Error())
|
|
}
|
|
if resp != nil {
|
|
defer func() { _ = resp.Body.Close() }()
|
|
if resp.StatusCode != 200 {
|
|
b, err := io.ReadAll(resp.Body)
|
|
if err == nil {
|
|
d.say(notice, "Error sending message: Status %s, Message: ", resp.Status, string(b))
|
|
}
|
|
} else {
|
|
stepCompleted(d, d.T("Message sent."))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func doc(d *MigrationData) {
|
|
d.say(plain, "Documentation: ")
|
|
d.say(plain, "The '%s' layer stores common configuration. All curio instances can include it in their %s argument.", "base", "--layers")
|
|
d.say(plain, "You can add other layers for per-machine configuration changes.")
|
|
|
|
d.say(plain, "Filecoin %s channels: %s and %s", "Slack", "#fil-curio-help", "#fil-curio-dev")
|
|
|
|
d.say(plain, "Increase reliability using redundancy: start multiple machines with at-least the post layer: 'curio run --layers=post'")
|
|
//d.say(plain, "Point your browser to your web GUI to complete setup with %s and advanced featues.", "Boost")
|
|
d.say(plain, "One database can serve multiple miner IDs: Run a migration for each lotus-miner.")
|
|
}
|
|
|
|
func verifySectors(d *MigrationData) {
|
|
var i []int
|
|
var lastError string
|
|
fmt.Println()
|
|
d.say(section, "Please start (or restart) %s now that database credentials are in %s.", "lotus-miner", "config.toml")
|
|
d.say(notice, "Waiting for %s to write sectors into Yugabyte.", "lotus-miner")
|
|
|
|
mid, err := address.IDFromAddress(d.MinerID)
|
|
if err != nil {
|
|
d.say(notice, "Error interpreting miner ID: %s: ID: %s", err.Error(), d.MinerID.String())
|
|
os.Exit(1)
|
|
}
|
|
|
|
for {
|
|
err := d.DB.Select(context.Background(), &i, `
|
|
SELECT count(*) FROM sector_location WHERE miner_id=$1`, mid)
|
|
if err != nil {
|
|
if err.Error() != lastError {
|
|
d.say(notice, "Error verifying sectors: %s", err.Error())
|
|
lastError = err.Error()
|
|
}
|
|
continue
|
|
}
|
|
if i[0] > 0 {
|
|
break
|
|
}
|
|
fmt.Print(".")
|
|
time.Sleep(5 * time.Second)
|
|
}
|
|
d.say(plain, "The sectors are in the database. The database is ready for %s.", "Curio")
|
|
d.say(notice, "Now shut down lotus-miner and lotus-worker and use run %s instead.", code.Render("curio run"))
|
|
|
|
_, err = (&promptui.Prompt{Label: d.T("Press return to continue")}).Run()
|
|
if err != nil {
|
|
d.say(notice, "Aborting migration.")
|
|
os.Exit(1)
|
|
}
|
|
stepCompleted(d, d.T("Sectors verified. %d sector locations found.", i))
|
|
}
|
|
|
|
func yugabyteConnect(d *MigrationData) {
|
|
harmonyCfg := config.DefaultStorageMiner().HarmonyDB //copy the config to a local variable
|
|
if d.MinerConfig != nil {
|
|
harmonyCfg = d.MinerConfig.HarmonyDB //copy the config to a local variable
|
|
}
|
|
var err error
|
|
d.DB, err = harmonydb.NewFromConfig(harmonyCfg)
|
|
if err != nil {
|
|
hcfg := getDBDetails(d)
|
|
harmonyCfg = *hcfg
|
|
}
|
|
|
|
d.say(plain, "Connected to Yugabyte. Schema is current.")
|
|
if !reflect.DeepEqual(harmonyCfg, d.MinerConfig.HarmonyDB) || !d.MinerConfig.Subsystems.EnableSectorIndexDB {
|
|
d.MinerConfig.HarmonyDB = harmonyCfg
|
|
d.MinerConfig.Subsystems.EnableSectorIndexDB = true
|
|
|
|
d.say(plain, "Enabling Sector Indexing in the database.")
|
|
buf, err := config.ConfigUpdate(d.MinerConfig, config.DefaultStorageMiner())
|
|
if err != nil {
|
|
d.say(notice, "Error encoding config.toml: %s", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
_, err = (&promptui.Prompt{
|
|
Label: d.T("Press return to update %s with Yugabyte info. A Backup file will be written to that folder before changes are made.", "config.toml")}).Run()
|
|
if err != nil {
|
|
os.Exit(1)
|
|
}
|
|
p, err := homedir.Expand(d.MinerConfigPath)
|
|
if err != nil {
|
|
d.say(notice, "Error expanding path: %s", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
tomlPath := path.Join(p, "config.toml")
|
|
stat, err := os.Stat(tomlPath)
|
|
if err != nil {
|
|
d.say(notice, "Error reading filemode of config.toml: %s", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
fBackup, err := os.CreateTemp(p, "config-backup-*.toml")
|
|
if err != nil {
|
|
d.say(notice, "Error creating backup file: %s", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
fBackupContents, err := os.ReadFile(tomlPath)
|
|
if err != nil {
|
|
d.say(notice, "Error reading config.toml: %s", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
_, err = fBackup.Write(fBackupContents)
|
|
if err != nil {
|
|
d.say(notice, "Error writing backup file: %s", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
err = fBackup.Close()
|
|
if err != nil {
|
|
d.say(notice, "Error closing backup file: %s", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
filemode := stat.Mode()
|
|
err = os.WriteFile(path.Join(p, "config.toml"), buf, filemode)
|
|
if err != nil {
|
|
d.say(notice, "Error writing config.toml: %s", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
d.say(section, "Restart Lotus Miner. ")
|
|
}
|
|
stepCompleted(d, d.T("Connected to Yugabyte"))
|
|
}
|
|
|
|
func readMinerConfig(d *MigrationData) {
|
|
d.say(plain, "To start, ensure your sealing pipeline is drained and shut-down lotus-miner.")
|
|
|
|
verifyPath := func(dir string) (*config.StorageMiner, error) {
|
|
cfg := config.DefaultStorageMiner()
|
|
dir, err := homedir.Expand(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = toml.DecodeFile(path.Join(dir, "config.toml"), &cfg)
|
|
return cfg, err
|
|
}
|
|
|
|
dirs := map[string]*config.StorageMiner{"~/.lotusminer": nil, "~/.lotus-miner-local-net": nil}
|
|
if v := os.Getenv("LOTUS_MINER_PATH"); v != "" {
|
|
dirs[v] = nil
|
|
}
|
|
for dir := range dirs {
|
|
cfg, err := verifyPath(dir)
|
|
if err != nil {
|
|
delete(dirs, dir)
|
|
}
|
|
dirs[dir] = cfg
|
|
}
|
|
|
|
var otherPath bool
|
|
if len(dirs) > 0 {
|
|
_, str, err := (&promptui.Select{
|
|
Label: d.T("Select the location of your lotus-miner config directory?"),
|
|
Items: append(lo.Keys(dirs), d.T("Other")),
|
|
Templates: d.selectTemplates,
|
|
}).Run()
|
|
if err != nil {
|
|
if err.Error() == "^C" {
|
|
os.Exit(1)
|
|
}
|
|
otherPath = true
|
|
} else {
|
|
if str == d.T("Other") {
|
|
otherPath = true
|
|
} else {
|
|
d.MinerConfigPath = str
|
|
d.MinerConfig = dirs[str]
|
|
}
|
|
}
|
|
}
|
|
if otherPath {
|
|
minerPathEntry:
|
|
str, err := (&promptui.Prompt{
|
|
Label: d.T("Enter the path to the configuration directory used by %s", "lotus-miner"),
|
|
}).Run()
|
|
if err != nil {
|
|
d.say(notice, "No path provided, abandoning migration ")
|
|
os.Exit(1)
|
|
}
|
|
cfg, err := verifyPath(str)
|
|
if err != nil {
|
|
d.say(notice, "Cannot read the config.toml file in the provided directory, Error: %s", err.Error())
|
|
goto minerPathEntry
|
|
}
|
|
d.MinerConfigPath = str
|
|
d.MinerConfig = cfg
|
|
}
|
|
|
|
// Try to lock Miner repo to verify that lotus-miner is not running
|
|
{
|
|
r, err := repo.NewFS(d.MinerConfigPath)
|
|
if err != nil {
|
|
d.say(plain, "Could not create repo from directory: %s. Aborting migration", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
lr, err := r.Lock(repo.StorageMiner)
|
|
if err != nil {
|
|
d.say(plain, "Could not lock miner repo. Your miner must be stopped: %s\n Aborting migration", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
_ = lr.Close()
|
|
}
|
|
|
|
stepCompleted(d, d.T("Read Miner Config"))
|
|
}
|
|
func stepCompleted(d *MigrationData, step string) {
|
|
fmt.Print(green.Render("✔ "))
|
|
d.say(plain, "Step Complete: %s\n", step)
|
|
}
|
|
|
|
func stepCreateActor(d *MigrationData) {
|
|
d.say(plain, "Initializing a new miner actor.")
|
|
|
|
for {
|
|
i, _, err := (&promptui.Select{
|
|
Label: d.T("Enter the info to create a new miner"),
|
|
Items: []string{
|
|
d.T("Owner Address: %s", d.owner.String()),
|
|
d.T("Worker Address: %s", d.worker.String()),
|
|
d.T("Sender Address: %s", d.sender.String()),
|
|
d.T("Sector Size: %d", d.ssize),
|
|
d.T("Confidence epochs: %d", d.confidence),
|
|
d.T("Continue to verify the addresses and create a new miner actor.")},
|
|
Size: 6,
|
|
Templates: d.selectTemplates,
|
|
}).Run()
|
|
if err != nil {
|
|
d.say(notice, "Miner creation error occurred: %s ", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
switch i {
|
|
case 0:
|
|
owner, err := (&promptui.Prompt{
|
|
Label: d.T("Enter the owner address"),
|
|
}).Run()
|
|
if err != nil {
|
|
d.say(notice, "No address provided")
|
|
continue
|
|
}
|
|
ownerAddr, err := address.NewFromString(owner)
|
|
if err != nil {
|
|
d.say(notice, "Failed to parse the address: %s", err.Error())
|
|
}
|
|
d.owner = ownerAddr
|
|
case 1, 2:
|
|
val, err := (&promptui.Prompt{
|
|
Label: d.T("Enter %s address", []string{"worker", "sender"}[i-1]),
|
|
Default: d.owner.String(),
|
|
}).Run()
|
|
if err != nil {
|
|
d.say(notice, err.Error())
|
|
continue
|
|
}
|
|
addr, err := address.NewFromString(val)
|
|
if err != nil {
|
|
d.say(notice, "Failed to parse the address: %s", err.Error())
|
|
}
|
|
switch i {
|
|
case 1:
|
|
d.worker = addr
|
|
case 2:
|
|
d.sender = addr
|
|
}
|
|
continue
|
|
case 3:
|
|
val, err := (&promptui.Prompt{
|
|
Label: d.T("Enter the sector size"),
|
|
}).Run()
|
|
if err != nil {
|
|
d.say(notice, "No value provided")
|
|
continue
|
|
}
|
|
sectorSize, err := units.RAMInBytes(val)
|
|
if err != nil {
|
|
d.say(notice, "Failed to parse sector size: %s", err.Error())
|
|
continue
|
|
}
|
|
d.ssize = abi.SectorSize(sectorSize)
|
|
continue
|
|
case 4:
|
|
confidenceStr, err := (&promptui.Prompt{
|
|
Label: d.T("Confidence epochs"),
|
|
Default: strconv.Itoa(5),
|
|
}).Run()
|
|
if err != nil {
|
|
d.say(notice, err.Error())
|
|
continue
|
|
}
|
|
confidence, err := strconv.ParseUint(confidenceStr, 10, 64)
|
|
if err != nil {
|
|
d.say(notice, "Failed to parse confidence: %s", err.Error())
|
|
continue
|
|
}
|
|
d.confidence = confidence
|
|
goto minerInit // break out of the for loop once we have all the values
|
|
}
|
|
}
|
|
|
|
minerInit:
|
|
miner, err := spcli.CreateStorageMiner(d.ctx, d.full, d.owner, d.worker, d.sender, d.ssize, d.confidence)
|
|
if err != nil {
|
|
d.say(notice, "Failed to create the miner actor: %s", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
d.MinerID = miner
|
|
stepCompleted(d, d.T("Miner %s created successfully", miner.String()))
|
|
}
|
|
|
|
func stepPresteps(d *MigrationData) {
|
|
|
|
// Setup and connect to YugabyteDB
|
|
_ = getDBDetails(d)
|
|
|
|
// Verify HarmonyDB connection
|
|
var titles []string
|
|
err := d.DB.Select(d.ctx, &titles, `SELECT title FROM harmony_config WHERE LENGTH(config) > 0`)
|
|
if err != nil {
|
|
d.say(notice, "Cannot reach the DB: %s", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Get full node API
|
|
full, closer, err := cliutil.GetFullNodeAPIV1(d.cctx)
|
|
if err != nil {
|
|
d.say(notice, "Error connecting to full node API: %s", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
d.full = full
|
|
d.closers = append(d.closers, closer)
|
|
stepCompleted(d, d.T("Pre-initialization steps complete"))
|
|
}
|
|
|
|
func stepNewMinerConfig(d *MigrationData) {
|
|
curioCfg := config.DefaultCurioConfig()
|
|
curioCfg.Addresses = append(curioCfg.Addresses, config.CurioAddresses{
|
|
PreCommitControl: []string{},
|
|
CommitControl: []string{},
|
|
TerminateControl: []string{},
|
|
DisableOwnerFallback: false,
|
|
DisableWorkerFallback: false,
|
|
MinerAddresses: []string{d.MinerID.String()},
|
|
})
|
|
|
|
sk, err := io.ReadAll(io.LimitReader(rand.Reader, 32))
|
|
if err != nil {
|
|
d.say(notice, "Failed to generate random bytes for secret: %s", err.Error())
|
|
d.say(notice, "Please do not run guided-setup again as miner creation is not idempotent. You need to run 'curio config new-cluster %s' to finish the configuration", d.MinerID.String())
|
|
os.Exit(1)
|
|
}
|
|
|
|
curioCfg.Apis.StorageRPCSecret = base64.StdEncoding.EncodeToString(sk)
|
|
|
|
ainfo, err := cliutil.GetAPIInfo(d.cctx, repo.FullNode)
|
|
if err != nil {
|
|
d.say(notice, "Failed to get API info for FullNode: %w", err)
|
|
d.say(notice, "Please do not run guided-setup again as miner creation is not idempotent. You need to run 'curio config new-cluster %s' to finish the configuration", d.MinerID.String())
|
|
os.Exit(1)
|
|
}
|
|
|
|
token, err := d.full.AuthNew(d.ctx, api.AllPermissions)
|
|
if err != nil {
|
|
d.say(notice, "Failed to verify the auth token from daemon node: %s", err.Error())
|
|
d.say(notice, "Please do not run guided-setup again as miner creation is not idempotent. You need to run 'curio config new-cluster %s' to finish the configuration", d.MinerID.String())
|
|
os.Exit(1)
|
|
}
|
|
|
|
curioCfg.Apis.ChainApiInfo = append(curioCfg.Apis.ChainApiInfo, fmt.Sprintf("%s:%s", string(token), ainfo.Addr))
|
|
|
|
// write config
|
|
var titles []string
|
|
err = d.DB.Select(d.ctx, &titles, `SELECT title FROM harmony_config WHERE LENGTH(config) > 0`)
|
|
if err != nil {
|
|
d.say(notice, "Cannot reach the DB: %s", err.Error())
|
|
d.say(notice, "Please do not run guided-setup again as miner creation is not idempotent. You need to run 'curio config new-cluster %s' to finish the configuration", d.MinerID.String())
|
|
os.Exit(1)
|
|
}
|
|
|
|
// If 'base' layer is not present
|
|
if !lo.Contains(titles, "base") {
|
|
curioCfg.Addresses = lo.Filter(curioCfg.Addresses, func(a config.CurioAddresses, _ int) bool {
|
|
return len(a.MinerAddresses) > 0
|
|
})
|
|
cb, err := config.ConfigUpdate(curioCfg, config.DefaultCurioConfig(), config.Commented(true), config.DefaultKeepUncommented(), config.NoEnv())
|
|
if err != nil {
|
|
d.say(notice, "Failed to generate default config: %s", err.Error())
|
|
d.say(notice, "Please do not run guided-setup again as miner creation is not idempotent. You need to run 'curio config new-cluster %s' to finish the configuration", d.MinerID.String())
|
|
os.Exit(1)
|
|
}
|
|
_, err = d.DB.Exec(d.ctx, "INSERT INTO harmony_config (title, config) VALUES ('base', $1)", string(cb))
|
|
if err != nil {
|
|
d.say(notice, "Failed to insert 'base' config layer in database: %s", err.Error())
|
|
d.say(notice, "Please do not run guided-setup again as miner creation is not idempotent. You need to run 'curio config new-cluster %s' to finish the configuration", d.MinerID.String())
|
|
os.Exit(1)
|
|
}
|
|
stepCompleted(d, d.T("Configuration 'base' was updated to include this miner's address"))
|
|
return
|
|
}
|
|
|
|
// If base layer is already present
|
|
baseCfg := config.DefaultCurioConfig()
|
|
var baseText string
|
|
|
|
err = d.DB.QueryRow(d.ctx, "SELECT config FROM harmony_config WHERE title='base'").Scan(&baseText)
|
|
if err != nil {
|
|
d.say(notice, "Failed to load base config from database: %s", err.Error())
|
|
d.say(notice, "Please do not run guided-setup again as miner creation is not idempotent. You need to run 'curio config new-cluster %s' to finish the configuration", d.MinerID.String())
|
|
os.Exit(1)
|
|
}
|
|
_, err = deps.LoadConfigWithUpgrades(baseText, baseCfg)
|
|
if err != nil {
|
|
d.say(notice, "Failed to parse base config: %s", err.Error())
|
|
d.say(notice, "Please do not run guided-setup again as miner creation is not idempotent. You need to run 'curio config new-cluster %s' to finish the configuration", d.MinerID.String())
|
|
os.Exit(1)
|
|
}
|
|
|
|
baseCfg.Addresses = append(baseCfg.Addresses, curioCfg.Addresses...)
|
|
baseCfg.Addresses = lo.Filter(baseCfg.Addresses, func(a config.CurioAddresses, _ int) bool {
|
|
return len(a.MinerAddresses) > 0
|
|
})
|
|
|
|
cb, err := config.ConfigUpdate(baseCfg, config.DefaultCurioConfig(), config.Commented(true), config.DefaultKeepUncommented(), config.NoEnv())
|
|
if err != nil {
|
|
d.say(notice, "Failed to regenerate base config: %s", err.Error())
|
|
d.say(notice, "Please do not run guided-setup again as miner creation is not idempotent. You need to run 'curio config new-cluster %s' to finish the configuration", d.MinerID.String())
|
|
os.Exit(1)
|
|
}
|
|
_, err = d.DB.Exec(d.ctx, "UPDATE harmony_config SET config=$1 WHERE title='base'", string(cb))
|
|
if err != nil {
|
|
d.say(notice, "Failed to insert 'base' config layer in database: %s", err.Error())
|
|
d.say(notice, "Please do not run guided-setup again as miner creation is not idempotent. You need to run 'curio config new-cluster %s' to finish the configuration", d.MinerID.String())
|
|
os.Exit(1)
|
|
}
|
|
|
|
stepCompleted(d, d.T("Configuration 'base' was updated to include this miner's address"))
|
|
}
|
|
|
|
func getDBDetails(d *MigrationData) *config.HarmonyDB {
|
|
harmonyCfg := config.DefaultStorageMiner().HarmonyDB
|
|
for {
|
|
i, _, err := (&promptui.Select{
|
|
Label: d.T("Enter the info to connect to your Yugabyte database installation (https://download.yugabyte.com/)"),
|
|
Items: []string{
|
|
d.T("Host: %s", strings.Join(harmonyCfg.Hosts, ",")),
|
|
d.T("Port: %s", harmonyCfg.Port),
|
|
d.T("Username: %s", harmonyCfg.Username),
|
|
d.T("Password: %s", harmonyCfg.Password),
|
|
d.T("Database: %s", harmonyCfg.Database),
|
|
d.T("Continue to connect and update schema.")},
|
|
Size: 6,
|
|
Templates: d.selectTemplates,
|
|
}).Run()
|
|
if err != nil {
|
|
d.say(notice, "Database config error occurred, abandoning migration: %s ", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
switch i {
|
|
case 0:
|
|
host, err := (&promptui.Prompt{
|
|
Label: d.T("Enter the Yugabyte database host(s)"),
|
|
}).Run()
|
|
if err != nil {
|
|
d.say(notice, "No host provided")
|
|
continue
|
|
}
|
|
harmonyCfg.Hosts = strings.Split(host, ",")
|
|
case 1, 2, 3, 4:
|
|
val, err := (&promptui.Prompt{
|
|
Label: d.T("Enter the Yugabyte database %s", []string{"port", "username", "password", "database"}[i-1]),
|
|
}).Run()
|
|
if err != nil {
|
|
d.say(notice, "No value provided")
|
|
continue
|
|
}
|
|
switch i {
|
|
case 1:
|
|
harmonyCfg.Port = val
|
|
case 2:
|
|
harmonyCfg.Username = val
|
|
case 3:
|
|
harmonyCfg.Password = val
|
|
case 4:
|
|
harmonyCfg.Database = val
|
|
}
|
|
continue
|
|
case 5:
|
|
db, err := harmonydb.NewFromConfig(harmonyCfg)
|
|
if err != nil {
|
|
if err.Error() == "^C" {
|
|
os.Exit(1)
|
|
}
|
|
d.say(notice, "Error connecting to Yugabyte database: %s", err.Error())
|
|
continue
|
|
}
|
|
d.DB = db
|
|
return &harmonyCfg
|
|
}
|
|
}
|
|
}
|