Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add service-setup subcommand #186

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ docker/curio:
--build-arg BUILD_VERSION=dev .
.PHONY: docker/curio

docker/devnet: $(lotus_build_cmd) docker/curio-all-in-one docker/lotus docker/lotus-miner docker/curio docker/yugabyte
docker/devnet: $(lotus_build_cmd) docker/curio-all-in-one docker/lotus docker/lotus-miner docker/curio
.PHONY: docker/devnet

devnet/up:
Expand Down
2 changes: 1 addition & 1 deletion apt/make_debs.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func part2(base, product, extra string) {
OrPanic(os.MkdirAll(base, 0755))
OrPanic(copyFile("curio", path.Join(base, "curio")))
OrPanic(copyFile("sptool", path.Join(base, "sptool")))
base = path.Join(dir, "lib", "systemd", "system")
base = path.Join(dir, "etc", "systemd", "system")
OrPanic(os.MkdirAll(base, 0755))
OrPanic(copyFile("apt/curio.service", path.Join(base, "curio.service")))
base = path.Join(dir, "usr", "share", "bash-completion", "completions")
Expand Down
96 changes: 71 additions & 25 deletions cmd/curio/guidedsetup/guidedsetup.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package guidedsetup

import (
"bufio"
"bytes"
"context"
"crypto/rand"
Expand All @@ -17,7 +18,9 @@ import (
"net/http"
"os"
"os/signal"
"os/user"
"path"
"path/filepath"
"strings"
"syscall"

Expand All @@ -26,7 +29,6 @@ import (
"github.com/manifoldco/promptui"
"github.com/mitchellh/go-homedir"
"github.com/samber/lo"
"github.com/snadrus/must"
"github.com/urfave/cli/v2"
"golang.org/x/text/language"
"golang.org/x/text/message"
Expand Down Expand Up @@ -244,41 +246,85 @@ func complete(d *MigrationData) {
stepCompleted(d, d.T("Lotus-Miner to Curio Migration."))
}

var EnvFiles = []string{"/etc/curio.env", "./curio/curio.env", "~/config/curio.env"}

func afterRan(d *MigrationData) {
// Write curio.env file.
// Inform users they need to copy this to /etc/curio.env or ~/.config/curio.env to run Curio.
places := append([]string{"/tmp/curio.env",
must.One(os.Getwd()) + "/curio.env",
must.One(os.UserHomeDir()) + "/curio.env"}, EnvFiles...)
saveConfigFile:
_, where, err := (&promptui.Select{
Label: d.T("Where should we save your database config file?"),
Items: places,
i, _, err := (&promptui.Select{
Label: d.T("Do you wish to add Harmony DB credentials to your ~/.bashrc or ~/.zshrc file?"),
Items: []string{
d.T("Yes"),
d.T("No")},
Templates: d.selectTemplates,
}).Run()
if err != nil {
d.say(notice, "Aborting migration.", err.Error())
d.say(notice, "Aborting remaining steps.", err.Error())
os.Exit(1)
}
if i == 0 {
// Determine the current user's shell
shell := os.Getenv("SHELL")
if shell == "" {
fmt.Println("Unable to determine the user's shell.")
return
}

args := []string{fmt.Sprintf("CURIO_DB=postgres://%s:%s@%s:%s/%s",
d.HarmonyCfg.Username,
d.HarmonyCfg.Password,
d.HarmonyCfg.Hosts[0],
d.HarmonyCfg.Port,
d.HarmonyCfg.Database)}
var rcFile string
if strings.Contains(shell, "bash") {
rcFile = ".bashrc"
} else if strings.Contains(shell, "zsh") {
rcFile = ".zshrc"
} else {
d.say(notice, "Not adding DB variables to RC file as shell %s is not BASH or ZSH.", shell)
os.Exit(1)
}

// Write the file
err = os.WriteFile(where, []byte(strings.Join(args, "\n")), 0644)
if err != nil {
d.say(notice, "Error writing file: %s", err.Error())
goto saveConfigFile
// Get the current user's home directory
usr, err := user.Current()
if err != nil {
d.say(notice, "Error getting user home directory:", err)
os.Exit(1)
}
rcFilePath := filepath.Join(usr.HomeDir, rcFile)

lines := []string{
fmt.Sprintf("export CURIO_DB_HOST=%s", strings.Join(d.HarmonyCfg.Hosts, ",")),
fmt.Sprintf("export CURIO_DB_USER=%s", d.HarmonyCfg.Username),
fmt.Sprintf("export CURIO_DB_PASSWORD=%s", d.HarmonyCfg.Password),
fmt.Sprintf("export CURIO_DB_PORT=%s", d.HarmonyCfg.Port),
fmt.Sprintf("export CURIO_DB_NAME=%s", d.HarmonyCfg.Database),
}

// Reopen the file in write mode to overwrite with updated content
file, err := os.OpenFile(rcFilePath, os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
d.say(notice, "Error opening %s file in write mode:", rcFile, err)
return
}
defer func(file *os.File) {
_ = file.Close()
}(file)

// Write all lines back to the rc file
writer := bufio.NewWriter(file)
for _, line := range lines {
if _, err := writer.WriteString(line + "\n"); err != nil {
d.say(notice, "Error writing to %s file: %s", rcFile, err)
return
}
}

for i := 1; i < 6; i++ {
err := writer.Flush()
if err != nil {
d.say(notice, "Failed to flush the writes to file %s: %s", rcFile, err)
d.say(notice, "Retrying.......(%d/5)", i)
continue
}
d.say(notice, "Finished updating the %s file", rcFile)
break
}
}

d.say(plain, "Try the web interface with %s ", code.Render("curio run --layers=gui"))
d.say(plain, "For more servers, make /etc/curio.env with the curio.env database env and add the CURIO_LAYERS env to assign purposes.")
d.say(plain, "Execute 'curio service-setup' to setup the Curio service on this node and proceed to do the same on the other nodes")
d.say(plain, "You can now migrate your market node (%s), if applicable.", "Boost")
d.say(plain, "Additional info is at http://docs.curiostorage.org")
}
Expand Down
245 changes: 245 additions & 0 deletions cmd/curio/guidedsetup/servicesetup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package guidedsetup

import (
"bufio"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/manifoldco/promptui"
"github.com/urfave/cli/v2"
)

// This should always match apt/curio.service
var serviceContent = `
[Unit]
Description=Curio
After=network.target

[Service]
ExecStart=/usr/local/bin/curio run
Environment=GOLOG_FILE="/var/log/curio/curio.log"
Environment=GOLOG_LOG_FMT="json"
LimitNOFILE=1000000
Restart=always
RestartSec=10
EnvironmentFile=/etc/curio.env

[Install]
WantedBy=multi-user.target
`

var ServicesetupCmd = &cli.Command{
Name: "service-setup",
Usage: "Run the service setup for a new Curio node. This command will take input from user and create/modify the service files",
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,
}

say(header, "This interactive tool creates/replace Curio service file and creates the basic env file for it.")
for _, step := range newServiceSteps {
step(&migrationData)
}

return nil
},
}

type newServiceStep func(data *MigrationData)

var newServiceSteps = []newServiceStep{
getDBDetails,
createServiceFile,
createEnvFile,
}

func createServiceFile(d *MigrationData) {
// Define the service file name
serviceName := "curio.service"

// Service file paths to check
servicePaths := []string{
filepath.Join("/etc/systemd/system/", serviceName),
filepath.Join("/usr/lib/systemd/system/", serviceName),
}

// Check if the service file already exists in any of the standard locations
for _, servicePath := range servicePaths {
if _, err := os.Stat(servicePath); err == nil {
d.say(notice, "Service file %s already exists at %s, no changes made.", serviceName, servicePath)
return //early return
} else if !os.IsNotExist(err) {
d.say(notice, "Error checking for service file at %s: %s", servicePath, err)
os.Exit(1)
}
}

// If the service file doesn't exist, create it in /etc/systemd/system/
targetServicePath := filepath.Join("/etc/systemd/system/", serviceName)
err := os.WriteFile(targetServicePath, []byte(serviceContent), 0644)
if err != nil {
d.say(notice, "Error writing service file: %s", err)
os.Exit(1)
}

d.say(notice, "Service file %s created successfully at %s", serviceName, targetServicePath)

// Reload systemd to recognize the new service
cmd := exec.Command("systemctl", "daemon-reload")
if err := cmd.Run(); err != nil {
d.say(notice, "Error reloading systemd: %s", err)
os.Exit(1)
}

// Enable the service to start on boot
cmd = exec.Command("systemctl", "enable", serviceName)
if err := cmd.Run(); err != nil {
d.say(notice, "Error enabling service: %s", err)
os.Exit(1)
}
d.say(notice, "Service %s enabled.\n", serviceName)
}

func createEnvFile(d *MigrationData) {
// Define the path to the environment file
envFilePath := "/etc/curio.env"

var layers, additionalEnvVars, envVars []string

repoPath := "~/.curio"
nodeName := "ChangeME"

// Take user input to remaining env vars
for {
i, _, err := (&promptui.Select{
Label: d.T("Enter the info to configure your Curio node"),
Items: []string{
d.T("CURIO_LAYERS: %s", layers),
d.T("CURIO_REPO_PATH: %s", repoPath),
d.T("CURIO_NODE_NAME: %s", nodeName),
d.T("Add additional variables like FIL_PROOFS_PARAMETER_CACHE: %s", envVars),
d.T("Continue update the env file.")},
Size: 6,
Templates: d.selectTemplates,
}).Run()
if err != nil {
d.say(notice, "Env config error occurred, abandoning: %s ", err.Error())
os.Exit(1)
}
switch i {
case 0:
host, err := (&promptui.Prompt{
Label: d.T("Enter the config layer name to use for this node. Base is already included by default"),
}).Run()
if err != nil {
d.say(notice, "No layer provided")
continue
}
layers = strings.Split(host, ",")
case 1, 2:
val, err := (&promptui.Prompt{
Label: d.T("Enter the %s", []string{"repo path", "node name"}[i-1]),
}).Run()
if err != nil {
d.say(notice, "No value provided")
continue
}
switch i {
case 1:
repoPath = val
case 2:
nodeName = val
}
continue
case 3:
// Ask if the user wants to add additional variables
additionalVars, err := (&promptui.Prompt{
Label: d.T("Do you want to add additional variables like FIL_PROOFS_PARAMETER_CACHE? (y/n)"),
Validate: func(input string) error {
if strings.EqualFold(input, "y") || strings.EqualFold(input, "yes") {
return nil
}
if strings.EqualFold(input, "n") || strings.EqualFold(input, "no") {
return nil
}
return errors.New("incorrect input")
},
}).Run()
if err != nil || strings.Contains(strings.ToLower(additionalVars), "n") {
d.say(notice, "No additional variables added")
continue
}

// Capture multiline input for additional variables
d.say(plain, "Start typing your additional environment variables one variable per line. Use Ctrl+D to finish:")
reader := bufio.NewReader(os.Stdin)

for {
text, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
break // End of input when Ctrl+D is pressed
}
d.say(notice, "Error reading input: %s", err)
os.Exit(1)
}
additionalEnvVars = append(additionalEnvVars, text)
}

for _, envVar := range additionalEnvVars {
parts := strings.SplitN(envVar, "=", 2)
if len(parts) == 2 {
envVars = append(envVars, fmt.Sprintf("%s=%s", strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])))
} else {
d.say(notice, "Skipping invalid input: %s", envVar)
}
}
continue
case 4:
// Define the environment variables to be added or updated
defenvVars := []string{
fmt.Sprintf("CURIO_LAYERS=%s", strings.Join(layers, ",")),
"CURIO_ALL_REMAINING_FIELDS_ARE_OPTIONAL=true",
fmt.Sprintf("CURIO_DB_HOST=%s", strings.Join(d.HarmonyCfg.Hosts, ",")),
fmt.Sprintf("CURIO_DB_USER=%s", d.HarmonyCfg.Username),
fmt.Sprintf("CURIO_DB_PASSWORD=%s", d.HarmonyCfg.Password),
fmt.Sprintf("CURIO_DB_PORT=%s", d.HarmonyCfg.Port),
fmt.Sprintf("CURIO_DB_NAME=%s", d.HarmonyCfg.Database),
fmt.Sprintf("CURIO_REPO_PATH=%s", repoPath),
fmt.Sprintf("CURIO_NODE_NAME=%s", nodeName),
"FIL_PROOFS_USE_MULTICORE_SDR=1",
}
var w string
for _, s := range defenvVars {
w += fmt.Sprintf("%s\n", s)
}
for _, s := range envVars {
w += fmt.Sprintf("%s\n", s)
}

// Write the new environment variables to the file
err = os.WriteFile(envFilePath, []byte(w), 0644)
if err != nil {
d.say(notice, "Error writing to file %s: %s", envFilePath, err)
os.Exit(1)
}
d.say(notice, "Service env file /etc/curio.env created successfully.")
return
}
}
}
Loading