Add dcrd to VSP status.

This renames "Voting wallet status" to "VSP status" and it now includes the status of the local dcrd instance. This change impacts both the /admin page of the UI, and the response of the /admin/status API endpoint.
This commit is contained in:
jholdstock 2021-11-03 10:33:04 +00:00 committed by Jamie Holdstock
parent d337e0a321
commit 6acc5be10c
10 changed files with 216 additions and 108 deletions

View File

@ -71,7 +71,7 @@ func blockConnected() {
ctx := context.Background()
dcrdClient, err := dcrdRPC.Client(ctx, netParams)
dcrdClient, _, err := dcrdRPC.Client(ctx, netParams)
if err != nil {
log.Errorf("%s: %v", funcName, err)
return
@ -314,7 +314,7 @@ func (n *NotificationHandler) Close() error {
func connectNotifier(shutdownCtx context.Context, dcrdWithNotifs rpc.DcrdConnect) error {
notifierClosed = make(chan struct{})
dcrdClient, err := dcrdWithNotifs.Client(shutdownCtx, netParams)
dcrdClient, _, err := dcrdWithNotifs.Client(shutdownCtx, netParams)
if err != nil {
return err
}
@ -411,7 +411,7 @@ func checkWalletConsistency() {
ctx := context.Background()
dcrdClient, err := dcrdRPC.Client(ctx, netParams)
dcrdClient, _, err := dcrdRPC.Client(ctx, netParams)
if err != nil {
log.Errorf("%s: %v", funcName, err)
return

View File

@ -436,7 +436,7 @@ func (vdb *VspDatabase) CheckIntegrity(ctx context.Context, params *chaincfg.Par
return nil
}
dcrdClient, err := dcrd.Client(ctx, params)
dcrdClient, _, err := dcrd.Client(ctx, params)
if err != nil {
return err
}

View File

@ -118,13 +118,13 @@ The `[WRN]` level is used to indicate events which are of interest, but do not
necessarily require investigation (eg. bad requests from clients, recoverable
errors).
### Voting Wallet Status
### VSP Status
The current status of the voting wallets is displayed in a table on the `/admin`
The current status of the VSP is displayed in a table on the `/admin`
page, and the same information can be retrieved as a JSON object from
`/admin/status` for automated monitoring. This endpoint requires Basic HTTP
Authentication with the username `admin` and the password set in vspd
configuration. A 200 HTTP status will be returned if the voting wallets seem
configuration. A 200 HTTP status will be returned if the VSP seems
healthy, or a 500 status will be used to indicate something is wrong.
```bash
@ -133,16 +133,23 @@ $ curl --user admin:12345 --request GET http://localhost:8800/admin/status
```json
{
"wss://127.0.0.1:20111/ws":
{
"connected":true,
"infoerror":false,
"daemonconnected":true,
"voteversion":8,
"unlocked":true,
"voting":true,
"bestblockerror":false,
"bestblockheight":462345
"dcrd": {
"host": "wss://127.0.0.1:19109/ws",
"connected": true,
"bestblockerror": false,
"bestblockheight": 802572
},
"wallets": {
"wss://127.0.0.1:20111/ws": {
"connected": true,
"infoerror": false,
"daemonconnected": true,
"voteversion": 10,
"unlocked": true,
"voting": true,
"bestblockerror": false,
"bestblockheight": 802572
}
}
}
```

View File

@ -47,16 +47,16 @@ func SetupDcrd(user, pass, addr string, cert []byte, n wsrpc.Notifier) DcrdConne
// Client creates a new DcrdRPC client instance. Returns an error if dialing
// dcrd fails or if dcrd is misconfigured.
func (d *DcrdConnect) Client(ctx context.Context, netParams *chaincfg.Params) (*DcrdRPC, error) {
func (d *DcrdConnect) Client(ctx context.Context, netParams *chaincfg.Params) (*DcrdRPC, string, error) {
c, newConnection, err := d.dial(ctx)
if err != nil {
return nil, fmt.Errorf("dcrd connection error: %w", err)
return nil, d.addr, fmt.Errorf("dcrd connection error: %w", err)
}
// If this is a reused connection, we don't need to validate the dcrd config
// again.
if !newConnection {
return &DcrdRPC{c, ctx}, nil
return &DcrdRPC{c, ctx}, d.addr, nil
}
// Verify dcrd is at the required api version.
@ -64,19 +64,19 @@ func (d *DcrdConnect) Client(ctx context.Context, netParams *chaincfg.Params) (*
err = c.Call(ctx, "version", &verMap)
if err != nil {
d.Close()
return nil, fmt.Errorf("dcrd version check failed: %w", err)
return nil, d.addr, fmt.Errorf("dcrd version check failed: %w", err)
}
ver, exists := verMap["dcrdjsonrpcapi"]
if !exists {
d.Close()
return nil, fmt.Errorf("dcrd version response missing 'dcrdjsonrpcapi'")
return nil, d.addr, fmt.Errorf("dcrd version response missing 'dcrdjsonrpcapi'")
}
sVer := semver{ver.Major, ver.Minor, ver.Patch}
if !semverCompatible(requiredDcrdVersion, sVer) {
d.Close()
return nil, fmt.Errorf("dcrd has incompatible JSON-RPC version: got %s, expected %s",
return nil, d.addr, fmt.Errorf("dcrd has incompatible JSON-RPC version: got %s, expected %s",
sVer, requiredDcrdVersion)
}
@ -85,11 +85,11 @@ func (d *DcrdConnect) Client(ctx context.Context, netParams *chaincfg.Params) (*
err = c.Call(ctx, "getcurrentnet", &netID)
if err != nil {
d.Close()
return nil, fmt.Errorf("dcrd getcurrentnet check failed: %w", err)
return nil, d.addr, fmt.Errorf("dcrd getcurrentnet check failed: %w", err)
}
if netID != netParams.Net {
d.Close()
return nil, fmt.Errorf("dcrd running on %s, expected %s", netID, netParams.Net)
return nil, d.addr, fmt.Errorf("dcrd running on %s, expected %s", netID, netParams.Net)
}
// Verify dcrd has tx index enabled (required for getrawtransaction).
@ -97,14 +97,14 @@ func (d *DcrdConnect) Client(ctx context.Context, netParams *chaincfg.Params) (*
err = c.Call(ctx, "getinfo", &info)
if err != nil {
d.Close()
return nil, fmt.Errorf("dcrd getinfo check failed: %w", err)
return nil, d.addr, fmt.Errorf("dcrd getinfo check failed: %w", err)
}
if !info.TxIndex {
d.Close()
return nil, errors.New("dcrd does not have transaction index enabled (--txindex)")
return nil, d.addr, errors.New("dcrd does not have transaction index enabled (--txindex)")
}
return &DcrdRPC{c, ctx}, nil
return &DcrdRPC{c, ctx}, d.addr, nil
}
// GetRawTransaction uses getrawtransaction RPC to retrieve details about the

View File

@ -27,6 +27,16 @@ type WalletStatus struct {
BestBlockHeight int64 `json:"bestblockheight"`
}
// DcrdStatus describes the current status of the local instance of dcrd used by
// vspd. This is used by the admin.html template, and also serialized to JSON
// for the /admin/status endpoint.
type DcrdStatus struct {
Host string `json:"host"`
Connected bool `json:"connected"`
BestBlockError bool `json:"bestblockerror"`
BestBlockHeight uint32 `json:"bestblockheight"`
}
type searchResult struct {
Hash string
Found bool
@ -36,6 +46,31 @@ type searchResult struct {
MaxVoteChanges int
}
func dcrdStatus(c *gin.Context) DcrdStatus {
hostname := c.MustGet("DcrdHostname").(string)
status := DcrdStatus{Host: hostname}
dcrdClient := c.MustGet("DcrdClient").(*rpc.DcrdRPC)
dcrdErr := c.MustGet("DcrdClientErr")
if dcrdErr != nil {
log.Errorf("could not get dcrd client: %v", dcrdErr.(error))
return status
}
status.Connected = true
bestBlock, err := dcrdClient.GetBestBlockHeader()
if err != nil {
log.Errorf("could not get dcrd best block header: %v", err)
status.BestBlockError = true
return status
}
status.BestBlockHeight = bestBlock.Height
return status
}
func walletStatus(c *gin.Context) map[string]WalletStatus {
walletClients := c.MustGet("WalletClients").([]*rpc.WalletRPC)
failedWalletClients := c.MustGet("FailedWalletClients").([]string)
@ -92,7 +127,17 @@ func statusJSON(c *gin.Context) {
}
}
c.AbortWithStatusJSON(httpStatus, wallets)
dcrd := dcrdStatus(c)
// Respond with HTTP status 500 if dcrd has issues.
if !dcrd.Connected || dcrd.BestBlockError {
httpStatus = http.StatusInternalServerError
}
c.AbortWithStatusJSON(httpStatus, gin.H{
"wallets": wallets,
"dcrd": dcrd,
})
}
// adminPage is the handler for "GET /admin".
@ -101,6 +146,7 @@ func adminPage(c *gin.Context) {
"WebApiCache": getCache(),
"WebApiCfg": cfg,
"WalletStatus": walletStatus(c),
"DcrdStatus": dcrdStatus(c),
})
}
@ -147,6 +193,7 @@ func ticketSearch(c *gin.Context) {
"WebApiCache": getCache(),
"WebApiCfg": cfg,
"WalletStatus": walletStatus(c),
"DcrdStatus": dcrdStatus(c),
})
}

View File

@ -69,7 +69,7 @@ func updateCache(ctx context.Context, db *database.VspDatabase,
}
// Get latest best block height.
dcrdClient, err := dcrd.Client(ctx, netParams)
dcrdClient, _, err := dcrd.Client(ctx, netParams)
if err != nil {
return err
}

View File

@ -75,10 +75,11 @@ func requireAdmin() gin.HandlerFunc {
// downstream handlers to make use of.
func withDcrdClient(dcrd rpc.DcrdConnect) gin.HandlerFunc {
return func(c *gin.Context) {
client, err := dcrd.Client(c, cfg.NetParams)
client, hostname, err := dcrd.Client(c, cfg.NetParams)
// Don't handle the error here, add it to the context and let downstream
// handlers decide what to do with it.
c.Set("DcrdClient", client)
c.Set("DcrdHostname", hostname)
c.Set("DcrdClientErr", err)
}
}

View File

@ -151,34 +151,41 @@ footer .code {
font-size: 12px;
}
.vsp-status {
padding: 10px;
}
#status-table th,
#status-table td {
.vsp-status table {
margin-bottom: 28px;
}
.vsp-status table th,
.vsp-status table td {
border: 1px solid #edeff1;
vertical-align: middle;
text-align: center;
}
#status-table .center {
.vsp-status table .center {
display: flex;
justify-content: center;
align-items: center;
}
#status-table .status {
.vsp-status table .status {
height: 30px;
padding-left: 30px;
}
#status-table .good {
.vsp-status table .good {
background: url(/public/images/success-icon.svg) no-repeat center center;
}
#status-table .bad {
.vsp-status table .bad {
background: url(/public/images/error-icon.svg) no-repeat left center;
}
#status-table .with-text {
.vsp-status table .with-text {
padding-left: 40px;
}

View File

@ -46,7 +46,7 @@
hidden
>
<ul>
<li><label for="tabset_1_1">Wallet Status</label></li>
<li><label for="tabset_1_1">VSP Status</label></li>
<li><label for="tabset_1_2">Ticket Search</label></li>
<li><label for="tabset_1_3">Database</label></li>
<li><label for="tabset_1_4">Logout</label></li>
@ -54,15 +54,59 @@
<div>
<section>
<table id="status-table" class="w-100 mb-0">
<section class="d-flex row justify-content-center">
<div class="vsp-status">
<h1>Local dcrd</h1>
<table class="vsp-status">
<thead>
<th>URL</th>
<th>Height</th>
<th>Connected</th>
</thead>
<tbody>
<tr>
<td>{{ stripWss .DcrdStatus.Host }}</td>
{{ if .DcrdStatus.Connected }}
{{ if .DcrdStatus.BestBlockError }}
<td>
<div class="center">
<div class="status bad center with-text">
Error
</div>
</div>
</td>
{{ else }}
<td>{{ .DcrdStatus.BestBlockHeight }}</td>
{{ end }}
{{else}}
<td>
<div class="center">
<div class="status bad center with-text">
Cannot connect
</div>
</div>
</td>
{{end}}
</tr>
</tbody>
</table>
</div>
<div class="vsp-status">
<h1>Voting Wallets</h1>
<table class="vsp-status">
<thead>
<th>URL</th>
<th>Height</th>
<th>Connected<br />to dcrd</th>
<th>Unlocked</th>
<th>Voting</th>
<th>Vote Version</th>
<th>Vote<br />Version</th>
</thead>
<tbody>
{{ range $host, $status := .WalletStatus }}
@ -128,6 +172,8 @@
{{end}}
</tbody>
</table>
</div>
</section>
<section>

View File

@ -232,14 +232,14 @@ func router(debugMode bool, cookieSecret []byte, dcrd rpc.DcrdConnect, wallets r
admin := router.Group("/admin").Use(
withWalletClients(wallets), withSession(cookieStore), requireAdmin(),
)
admin.GET("", adminPage)
admin.POST("/ticket", ticketSearch)
admin.GET("", withDcrdClient(dcrd), adminPage)
admin.POST("/ticket", withDcrdClient(dcrd), ticketSearch)
admin.GET("/backup", downloadDatabaseBackup)
admin.POST("/logout", adminLogout)
// Require Basic HTTP Auth on /admin/status endpoint.
basic := router.Group("/admin").Use(
withWalletClients(wallets), gin.BasicAuth(gin.Accounts{
withDcrdClient(dcrd), withWalletClients(wallets), gin.BasicAuth(gin.Accounts{
"admin": cfg.AdminPass,
}),
)