From c643cc2970f249da2efbd3a908996719b1320dd0 Mon Sep 17 00:00:00 2001 From: jholdstock Date: Thu, 30 May 2024 09:37:45 +0100 Subject: [PATCH 1/2] vspadmin: New binary to create empty databases. vspadmin is a new binary which implements one-off admin tasks which are necessarily interactive and thus do not fit neatly into the long-lived vspd binary. The first behaviour added to vspadmin is the ability to create a new vspd database. This behaviour is removed from vspd with the associated config option deprecated. Documentation and scripts updated to reflect the change. --- cmd/vspadmin/README.md | 30 ++++++++++ cmd/vspadmin/main.go | 130 ++++++++++++++++++++++++++++++++++++++++ cmd/vspd/main.go | 6 ++ docs/deployment.md | 9 ++- harness.sh | 4 +- internal/vspd/config.go | 48 ++------------- 6 files changed, 177 insertions(+), 50 deletions(-) create mode 100644 cmd/vspadmin/README.md create mode 100644 cmd/vspadmin/main.go diff --git a/cmd/vspadmin/README.md b/cmd/vspadmin/README.md new file mode 100644 index 00000000..0cee4dc3 --- /dev/null +++ b/cmd/vspadmin/README.md @@ -0,0 +1,30 @@ +# vspadmin + +vspadmin is a tool to perform various VSP administration tasks. + +## Usage + +```no-highlight +vspadmin [OPTIONS] COMMAND +``` + +## Options + +```no-highlight +--homedir= Path to application home directory. (default: /home/user/.vspd) +--network=[mainnet|testnet|simnet] Decred network to use. (default: mainnet) +-h, --help Show help message +``` + +## Commands + +### `createdatabase` + +Creates a new database for a new deployment of vspd. Accepts the xpub key to be +used for collecting fees as a parameter. + +Example: + +```no-highlight + go run ./cmd/vspadmin createdatabase +``` diff --git a/cmd/vspadmin/main.go b/cmd/vspadmin/main.go new file mode 100644 index 00000000..97aedfbd --- /dev/null +++ b/cmd/vspadmin/main.go @@ -0,0 +1,130 @@ +// Copyright (c) 2024 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/decred/dcrd/dcrutil/v4" + "github.com/decred/dcrd/hdkeychain/v3" + "github.com/decred/vspd/database" + "github.com/decred/vspd/internal/config" + "github.com/jessevdk/go-flags" +) + +const ( + dbFilename = "vspd.db" +) + +type conf struct { + HomeDir string `long:"homedir" description:"Path to application home directory."` + Network string `long:"network" description:"Decred network to use." choice:"mainnet" choice:"testnet" choice:"simnet"` +} + +var defaultConf = conf{ + HomeDir: dcrutil.AppDataDir("vspd", false), + Network: "mainnet", +} + +func log(format string, a ...any) { + fmt.Printf(format+"\n", a...) +} + +// fileExists reports whether the named file or directory exists. +func fileExists(name string) bool { + if _, err := os.Stat(name); os.IsNotExist(err) { + return false + } + return true +} + +func createDatabase(homeDir string, feeXPub string, network *config.Network) error { + dataDir := filepath.Join(homeDir, "data", network.Name) + dbFile := filepath.Join(dataDir, dbFilename) + + // Return error if database already exists. + if fileExists(dbFile) { + return fmt.Errorf("%s database already exists in %s", network.Name, dataDir) + } + + // Ensure provided xpub is a valid key for the selected network. + _, err := hdkeychain.NewKeyFromString(feeXPub, network.Params) + if err != nil { + return fmt.Errorf("failed to parse feexpub: %w", err) + } + + // Ensure the data directory exists. + err = os.MkdirAll(dataDir, 0700) + if err != nil { + return fmt.Errorf("failed to create data directory: %w", err) + } + + // Create new database. + err = database.CreateNew(dbFile, feeXPub) + if err != nil { + return fmt.Errorf("error creating db file %s: %w", dbFile, err) + } + + return nil +} + +// run is the real main function for vspadmin. It is necessary to work around +// the fact that deferred functions do not run when os.Exit() is called. +func run() int { + cfg := defaultConf + + // If command line options are requesting help, write it to stdout and exit. + if config.WriteHelp(&cfg) { + return 0 + } + + // Parse command line options. + remainingArgs, err := flags.Parse(&cfg) + if err != nil { + // Don't need to log the error, flags lib has already done it. + return 1 + } + + network, err := config.NetworkFromName(cfg.Network) + if err != nil { + log("%v", err) + return 1 + } + + if len(remainingArgs) < 1 { + log("No command specified") + return 1 + } + + switch remainingArgs[0] { + case "createdatabase": + if len(remainingArgs) != 2 { + log("createdatabase has one required argument, fee xpub") + return 1 + } + + feeXPub := remainingArgs[1] + + err = createDatabase(cfg.HomeDir, feeXPub, network) + if err != nil { + log("createdatabase failed: %v", err) + return 1 + } + + log("New %s vspd database created in %s", network.Name, cfg.HomeDir) + + default: + log("%q is not a valid command", remainingArgs[0]) + return 1 + } + + return 0 +} + +func main() { + os.Exit(run()) +} diff --git a/cmd/vspd/main.go b/cmd/vspd/main.go index c1f52bc6..796a9bd9 100644 --- a/cmd/vspd/main.go +++ b/cmd/vspd/main.go @@ -102,6 +102,12 @@ func run() int { log.Warnf("") } + if cfg.FeeXPub != "" { + log.Warnf("") + log.Warnf("\tWARNING: Config --feexpub is set. This behavior has been moved into vspadmin and will be removed from vspd in a future release") + log.Warnf("") + } + // Open database. db, err := database.Open(cfg.DatabaseFile(), makeLogger(" DB"), maxVoteChangeRecords) if err != nil { diff --git a/docs/deployment.md b/docs/deployment.md index 3400b747..01382753 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -96,15 +96,14 @@ vspd. **Do not run a voting wallet on your webserver.** config file to set your dcrd and dcrwallet connection details, and any other required customization. -1. A vspd database must be initialized before vpsd can be started. To do this, - provide vspd with the xpub key it should use for collecting fees: +1. Use [vspadmin](./cmd/vspadmin) to initialize a vpsd database. The xpub key to + be used for collecting fees must be passed in as an argument. ```no-highlight - $ vspd --feexpub=tpubVppjaMjp8GEW... + $ go run ./cmd/vspadmin createdatabase tpubVppjaMjp8GEW... ``` -1. Once the database is initialized, vspd can be started for normal operation by - running it without the `--feexpub` flag. +1. Once the database is initialized, vspd can be started for normal operation. 1. Configure nginx with SSL and set up reverse proxy to forward requests to the vspd process. nginx must also set the `X-Forwarded-For` header to make vspd diff --git a/harness.sh b/harness.sh index 579e48d9..fd966241 100755 --- a/harness.sh +++ b/harness.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# Copyright (c) 2020-2023 The Decred developers +# Copyright (c) 2020-2024 The Decred developers # Use of this source code is governed by an ISC # license that can be found in the LICENSE file. # @@ -170,7 +170,7 @@ EOF tmux new-window -t $TMUX_SESSION -n "vspd" echo "Creating vspd database" -tmux send-keys "vspd --homedir=${HARNESS_ROOT}/vspd --feexpub=${VSPD_FEE_XPUB}" C-m +tmux send-keys "go run ./cmd/vspadmin --homedir=${HARNESS_ROOT}/vspd --network=testnet createdatabase ${VSPD_FEE_XPUB}" C-m sleep 3 # wait for database creation and ensure dcrwallet rpc listeners are started echo "Starting vspd" tmux send-keys "vspd --homedir=${HARNESS_ROOT}/vspd" C-m diff --git a/internal/vspd/config.go b/internal/vspd/config.go index ae95be06..cd55db78 100644 --- a/internal/vspd/config.go +++ b/internal/vspd/config.go @@ -17,8 +17,6 @@ import ( "time" "github.com/decred/dcrd/dcrutil/v4" - "github.com/decred/dcrd/hdkeychain/v3" - "github.com/decred/vspd/database" "github.com/decred/vspd/internal/config" "github.com/decred/vspd/internal/version" flags "github.com/jessevdk/go-flags" @@ -55,12 +53,11 @@ type Config struct { // The following flags should be set on CLI only, not via config file. ShowVersion bool `long:"version" no-ini:"true" description:"Display version information and exit."` - FeeXPub string `long:"feexpub" no-ini:"true" description:"Cold wallet xpub used for collecting fees. Should be provided once to initialize a vspd database."` + FeeXPub string `long:"feexpub" no-ini:"true" description:"DEPRECATED: This behavior has been moved into vspadmin and will be removed from vspd in a future version of the software."` HomeDir string `long:"homedir" no-ini:"true" description:"Path to application home directory. Used for storing VSP database and logs."` ConfigFile string `long:"configfile" no-ini:"true" description:"DEPRECATED: This behavior is no longer available and this option will be removed in a future version of the software."` // The following fields are derived from the above fields by LoadConfig(). - dataDir string network *config.Network dcrdDetails *DcrdDetails walletDetails *WalletDetails @@ -89,7 +86,7 @@ func (cfg *Config) LogDir() string { } func (cfg *Config) DatabaseFile() string { - return filepath.Join(cfg.dataDir, dbFilename) + return filepath.Join(cfg.HomeDir, "data", cfg.network.Name, dbFilename) } func (cfg *Config) DcrdDetails() *DcrdDetails { @@ -420,45 +417,10 @@ func LoadConfig() (*Config, error) { Certs: walletCerts, } - // Create the data directory. - cfg.dataDir = filepath.Join(cfg.HomeDir, "data", cfg.network.Name) - err = os.MkdirAll(cfg.dataDir, 0700) - if err != nil { - return nil, fmt.Errorf("failed to create data directory: %w", err) - } - - dbPath := cfg.DatabaseFile() - - // If xpub has been provided, create a new database and exit. - if cfg.FeeXPub != "" { - // If database already exists, return error. - if fileExists(dbPath) { - return nil, fmt.Errorf("database already initialized at %s, "+ - "--feexpub option is not needed", dbPath) - } - - // Ensure provided value is a valid key for the selected network. - _, err = hdkeychain.NewKeyFromString(cfg.FeeXPub, cfg.network.Params) - if err != nil { - return nil, fmt.Errorf("failed to parse feexpub: %w", err) - } - - // Create new database. - fmt.Printf("Initializing new database at %s\n", dbPath) - err = database.CreateNew(dbPath, cfg.FeeXPub) - if err != nil { - return nil, fmt.Errorf("error creating db file %s: %w", dbPath, err) - } - - // Exit with success - fmt.Printf("Database initialized\n") - os.Exit(0) - } - // If database does not exist, return error. - if !fileExists(dbPath) { - return nil, fmt.Errorf("no database exists in %s. Run vspd with the"+ - " --feexpub option to initialize one", cfg.dataDir) + if !fileExists(cfg.DatabaseFile()) { + return nil, fmt.Errorf("no %s database exists in %s. A new database can"+ + " be created with vspadmin", cfg.network.Name, cfg.HomeDir) } return &cfg, nil From 808c922701577f99eb83e9b48f8a56a857e8b8d7 Mon Sep 17 00:00:00 2001 From: jholdstock Date: Thu, 30 May 2024 09:39:15 +0100 Subject: [PATCH 2/2] vspadmin: Write default config file. A new command in vspadmin writes the default config file for a new vspd deployment. The behaviour is removed from vspd and documentation has been updated to reflect the change. --- cmd/vspadmin/README.md | 12 +++++++++++- cmd/vspadmin/main.go | 40 +++++++++++++++++++++++++++++++++++++++- docs/deployment.md | 13 +++++++++---- internal/vspd/config.go | 19 ++++--------------- 4 files changed, 63 insertions(+), 21 deletions(-) diff --git a/cmd/vspadmin/README.md b/cmd/vspadmin/README.md index 0cee4dc3..e5727a74 100644 --- a/cmd/vspadmin/README.md +++ b/cmd/vspadmin/README.md @@ -26,5 +26,15 @@ used for collecting fees as a parameter. Example: ```no-highlight - go run ./cmd/vspadmin createdatabase +$ go run ./cmd/vspadmin createdatabase +``` + +### `writeconfig` + +Writes a config file with default values to the application home directory. + +Example: + +```no-highlight +$ go run ./cmd/vspadmin writeconfig ``` diff --git a/cmd/vspadmin/main.go b/cmd/vspadmin/main.go index 97aedfbd..1b6ef27b 100644 --- a/cmd/vspadmin/main.go +++ b/cmd/vspadmin/main.go @@ -13,11 +13,13 @@ import ( "github.com/decred/dcrd/hdkeychain/v3" "github.com/decred/vspd/database" "github.com/decred/vspd/internal/config" + "github.com/decred/vspd/internal/vspd" "github.com/jessevdk/go-flags" ) const ( - dbFilename = "vspd.db" + configFilename = "vspd.conf" + dbFilename = "vspd.db" ) type conf struct { @@ -72,6 +74,32 @@ func createDatabase(homeDir string, feeXPub string, network *config.Network) err return nil } +func writeConfig(homeDir string) error { + configFile := filepath.Join(homeDir, configFilename) + + // Return an error if the config file already exists. + if fileExists(configFile) { + return fmt.Errorf("config file already exists in %s", homeDir) + } + + // Ensure the directory exists. + err := os.MkdirAll(homeDir, 0700) + if err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + + // Write a config file with default values to the provided home directory. + preParser := flags.NewParser(&vspd.DefaultConfig, flags.None) + preIni := flags.NewIniParser(preParser) + err = preIni.WriteFile(configFile, + flags.IniIncludeComments|flags.IniIncludeDefaults) + if err != nil { + return fmt.Errorf("failed to create config file: %w", err) + } + + return nil +} + // run is the real main function for vspadmin. It is necessary to work around // the fact that deferred functions do not run when os.Exit() is called. func run() int { @@ -117,6 +145,16 @@ func run() int { log("New %s vspd database created in %s", network.Name, cfg.HomeDir) + case "writeconfig": + err = writeConfig(cfg.HomeDir) + if err != nil { + log("writeconfig failed: %v", err) + return 1 + } + + log("Config file with default values written to %s", cfg.HomeDir) + log("Edit the file and fill in values specific to your vspd deployment") + default: log("%q is not a valid command", remainingArgs[0]) return 1 diff --git a/docs/deployment.md b/docs/deployment.md index 01382753..689b5289 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -92,9 +92,13 @@ vspd. **Do not run a voting wallet on your webserver.** receiving `blockconnected` notifications, and for broadcasting and checking the status of fee transactions. -1. Run `vspd` with no arguments to write a default config file. Modify the - config file to set your dcrd and dcrwallet connection details, and any other - required customization. +1. Use [vspadmin](./cmd/vspadmin) to write a config file containing default + values. Modify the config file to set your dcrd and dcrwallet connection + details, and any other required customization. + + ```no-highlight + $ go run ./cmd/vspadmin writeconfig + ``` 1. Use [vspadmin](./cmd/vspadmin) to initialize a vpsd database. The xpub key to be used for collecting fees must be passed in as an argument. @@ -103,7 +107,8 @@ vspd. **Do not run a voting wallet on your webserver.** $ go run ./cmd/vspadmin createdatabase tpubVppjaMjp8GEW... ``` -1. Once the database is initialized, vspd can be started for normal operation. +1. Once the database is initialized and required fields in the config file have + been entered, vspd can be started for normal operation. 1. Configure nginx with SSL and set up reverse proxy to forward requests to the vspd process. nginx must also set the `X-Forwarded-For` header to make vspd diff --git a/internal/vspd/config.go b/internal/vspd/config.go index cd55db78..8dc65e6a 100644 --- a/internal/vspd/config.go +++ b/internal/vspd/config.go @@ -240,25 +240,14 @@ func LoadConfig() (*Config, error) { return nil, err } - // Create a default config file when one does not exist and the user did - // not specify an override. + // Load additional config from file. configFile := filepath.Join(cfg.HomeDir, configFilename) if !fileExists(configFile) { - preIni := flags.NewIniParser(preParser) - err = preIni.WriteFile(configFile, - flags.IniIncludeComments|flags.IniIncludeDefaults) - if err != nil { - return nil, fmt.Errorf("error creating a default "+ - "config file: %w", err) - } - fmt.Printf("Config file with default values written to %s\n", configFile) - - // File created, user now has to fill in values. Proceeding with the - // default file just causes errors. - os.Exit(0) + err := fmt.Errorf("config file does not exist at %s", configFile) + fmt.Fprintln(os.Stderr, err) + return nil, err } - // Load additional config from file. parser := flags.NewParser(&cfg, flags.None) err = flags.NewIniParser(parser).ParseFile(configFile)