summaryrefslogtreecommitdiff
path: root/netdeploy/remote/nodecfg/nodeConfigurator.go
blob: 5ab43d5ff7cf35e9d5131e3198045ef1f7767140 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
// Copyright (C) 2019-2024 Algorand, Inc.
// This file is part of go-algorand
//
// go-algorand is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// go-algorand is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with go-algorand.  If not, see <https://www.gnu.org/licenses/>.

package nodecfg

import (
	"context"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/algorand/go-algorand/config"
	"github.com/algorand/go-algorand/data/bookkeeping"
	"github.com/algorand/go-algorand/netdeploy/remote"
	"github.com/algorand/go-algorand/tools/network/cloudflare"
	"github.com/algorand/go-algorand/util"
)

type nodeConfigurator struct {
	config                  remote.HostConfig
	dnsName                 string
	genesisFile             string
	genesisData             bookkeeping.Genesis
	bootstrappedBlockFile   string
	bootstrappedTrackerFile string
	bootstrappedTrackerDir  string
	relayEndpoints          []srvEntry
	metricsEndpoints        []srvEntry
}

type srvEntry struct {
	srvName string
	port    string
}

// ApplyConfigurationToHost attempts to apply the provided configuration to the local host,
// based on the configuration specified for the provided hostName, with node
// directories being created / updated under the specified rootNodeDir
func ApplyConfigurationToHost(cfg remote.HostConfig, rootConfigDir, rootNodeDir string, dnsName string) (err error) {
	nc := nodeConfigurator{
		config:  cfg,
		dnsName: dnsName,
	}

	return nc.apply(rootConfigDir, rootNodeDir)
}

// Apply the configuration.  For now, assume initial installation - not an update.
//
// Copy node directories from configuration folder to the rootNodeDir
// Then configure
func (nc *nodeConfigurator) apply(rootConfigDir, rootNodeDir string) (err error) {

	blockFile := filepath.Join(rootConfigDir, "genesisdata", "bootstrapped.block.sqlite")
	blockFileExists := util.FileExists(blockFile)
	if blockFileExists {
		nc.bootstrappedBlockFile = blockFile
	}

	trackerFile := filepath.Join(rootConfigDir, "genesisdata", "bootstrapped.tracker.sqlite")
	trackerFileExists := util.FileExists(trackerFile)
	if trackerFileExists {
		nc.bootstrappedTrackerFile = trackerFile
	}

	trackerDir := filepath.Join(rootConfigDir, "genesisdata", "bootstrapped")
	trackerDirExists := util.FileExists(trackerDir)
	if trackerDirExists {
		nc.bootstrappedTrackerDir = trackerDir
	}

	nc.genesisFile = filepath.Join(rootConfigDir, "genesisdata", config.GenesisJSONFile)
	nc.genesisData, err = bookkeeping.LoadGenesisFromFile(nc.genesisFile)
	nodeDirs, err := nc.prepareNodeDirs(nc.config.Nodes, rootConfigDir, rootNodeDir)
	if err != nil {
		return fmt.Errorf("error preparing node directories: %v", err)
	}

	for _, nodeDir := range nodeDirs {
		nodeDir.delaySave = true
		err = nodeDir.configure()
		if err != nil {
			break
		}
		nodeDir.delaySave = false
		fmt.Fprint(os.Stdout, "... saving config\n")
		nodeDir.saveConfig()
	}

	if err == nil && nc.dnsName != "" {
		fmt.Fprint(os.Stdout, "... registering DNS / SRV records\n")
		err = nc.registerDNSRecords()
	}

	return
}

func (nc *nodeConfigurator) prepareNodeDirs(configs []remote.NodeConfig, rootConfigDir, rootNodeDir string) (nodeDirs []nodeDir, err error) {
	rootHostDir := filepath.Join(rootConfigDir, "hosts", nc.config.Name)
	if err != nil {
		err = fmt.Errorf("error loading genesis data: %v", err)
		return
	}
	genesisDir := nc.genesisData.ID()

	// Importing root keys is complicated - just use goal's support for it
	goalPath, err := filepath.Abs(filepath.Join(rootNodeDir, ".."))
	if err != nil {
		return
	}
	importKeysCmd := filepath.Join(goalPath, "goal")

	for _, node := range configs {
		nodeSrc := filepath.Join(rootHostDir, node.Name)
		nodeDest := filepath.Join(rootNodeDir, node.Name)

		fmt.Fprintf(os.Stdout, "Creating node %s in %s...\n", node.Name, nodeDest)
		err = util.CopyFolder(nodeSrc, nodeDest)
		if err != nil {
			return
		}

		// Copy the genesis.json file
		_, err = util.CopyFile(nc.genesisFile, filepath.Join(nodeDest, config.GenesisJSONFile))
		if err != nil {
			return
		}

		// Copy wallet files into current ledger folder and import the wallets
		//
		fmt.Fprintf(os.Stdout, "... copying wallets to ledger folder ...\n")
		err = util.CopyFolderWithFilter(nodeDest, filepath.Join(nodeDest, genesisDir), filterWalletFiles)
		if err != nil {
			return
		}

		fmt.Fprintf(os.Stdout, "... importing wallets into kmd ...\n")
		err = importWalletFiles(importKeysCmd, nodeDest)
		if err != nil {
			return
		}

		// Copy the bootstrapped files into current ledger folder
		if nc.bootstrappedBlockFile != "" &&
			(nc.bootstrappedTrackerFile != "" || nc.bootstrappedTrackerDir != "") {
			fmt.Fprintf(os.Stdout, "... copying block database file to ledger folder ...\n")
			dest := filepath.Join(nodeDest, genesisDir, fmt.Sprintf("%s.block.sqlite", config.LedgerFilenamePrefix))
			_, err = util.CopyFile(nc.bootstrappedBlockFile, dest)
			if err != nil {
				return nil, fmt.Errorf("failed to copy database file %s from %s to %s : %w", "bootstrapped.block.sqlite", filepath.Dir(nc.bootstrappedBlockFile), dest, err)
			}
			if nc.bootstrappedTrackerFile != "" {
				fmt.Fprintf(os.Stdout, "... copying tracker database file to ledger folder ...\n")
				dest = filepath.Join(nodeDest, genesisDir, fmt.Sprintf("%s.tracker.sqlite", config.LedgerFilenamePrefix))
				_, err = util.CopyFile(nc.bootstrappedTrackerFile, dest)
				if err != nil {
					return nil, fmt.Errorf("failed to copy database file %s from %s to %s : %w", filepath.Base(nc.bootstrappedBlockFile), filepath.Dir(nc.bootstrappedBlockFile), dest, err)
				}
			}
			if nc.bootstrappedTrackerDir != "" {
				fmt.Fprintf(os.Stdout, "... copying tracker database directory to ledger folder ...\n")
				dest = filepath.Join(nodeDest, genesisDir, config.LedgerFilenamePrefix)
				err = util.CopyFolder(nc.bootstrappedTrackerDir, dest)
				if err != nil {
					return nil, fmt.Errorf("failed to copy database directory from %s to %s : %w", nc.bootstrappedTrackerDir, dest, err)
				}
			}
		}

		nodeDirs = append(nodeDirs, nodeDir{
			NodeConfig:   node,
			dataDir:      nodeDest,
			configurator: nc,
		})
	}
	return
}

func (nc *nodeConfigurator) registerDNSRecords() (err error) {
	cfZoneID, cfToken, err := getClouldflareCredentials()
	if err != nil {
		return fmt.Errorf("error getting DNS credentials: %v", err)
	}

	cloudflareDNS := cloudflare.NewDNS(cfZoneID, cfToken)

	const priority = 1
	const weight = 1
	const relayBootstrap = "_algobootstrap"
	const metricsSrv = "_metrics"
	const proxied = false

	// If we need to register anything, first register a DNS entry
	// to map our network DNS name to our public name (or IP) provided to nodecfg
	// Network HostName = eg r1.testnet.algodev.network
	networkHostName := nc.config.Name + "." + string(nc.genesisData.Network) + ".algodev.network"
	isIP := net.ParseIP(nc.dnsName) != nil
	var recordType string
	if isIP {
		recordType = "A"
	} else {
		recordType = "CNAME"
	}

	fmt.Fprintf(os.Stdout, "...... Adding DNS Record '%s' -> '%s' .\n", networkHostName, nc.dnsName)
	cloudflareDNS.SetDNSRecord(context.Background(), recordType, networkHostName, nc.dnsName, cloudflare.AutomaticTTL, priority, proxied)

	for _, entry := range nc.relayEndpoints {
		port, parseErr := strconv.ParseInt(strings.Split(entry.port, ":")[1], 10, 64)
		if parseErr != nil {
			return parseErr
		}
		fmt.Fprintf(os.Stdout, "...... Adding Relay SRV Record '%s' -> '%s' .\n", entry.srvName, networkHostName)
		err = cloudflareDNS.SetSRVRecord(context.Background(), entry.srvName, networkHostName,
			cloudflare.AutomaticTTL, priority, uint(port), relayBootstrap, "_tcp", weight)
		if err != nil {
			return
		}
	}

	for _, entry := range nc.metricsEndpoints {
		port, parseErr := strconv.ParseInt(strings.Split(entry.port, ":")[1], 10, 64)
		if parseErr != nil {
			fmt.Fprintf(os.Stdout, "Error parsing port for srv record: %s (port %v)\n", parseErr, entry)
			return parseErr
		}
		fmt.Fprintf(os.Stdout, "...... Adding Metrics SRV Record '%s' -> '%s' .\n", entry.srvName, networkHostName)
		err = cloudflareDNS.SetSRVRecord(context.Background(), entry.srvName, networkHostName,
			cloudflare.AutomaticTTL, priority, uint(port), metricsSrv, "_tcp", weight)
		if err != nil {
			fmt.Fprintf(os.Stdout, "Error creating srv record: %s (%v)\n", err, entry)
			return
		}
	}
	return
}

func getClouldflareCredentials() (zoneID string, token string, err error) {
	zoneID = os.Getenv("CLOUDFLARE_ZONE_ID")
	token = os.Getenv("CLOUDFLARE_API_TOKEN")
	if zoneID == "" || token == "" {
		err = fmt.Errorf("one or more credentials missing from ENV")
	}
	return
}

func importWalletFiles(importKeysCmd string, nodeDir string) error {
	_, _, err := util.ExecAndCaptureOutput(importKeysCmd, "account", "importrootkey", "-d", nodeDir, "-u")
	return err
}

func filterWalletFiles(name string, info os.FileInfo) bool {
	if info.IsDir() {
		return false
	}

	ext := filepath.Ext(info.Name())
	return ext == ".partkey" || ext == ".rootkey"
}

func (nc *nodeConfigurator) addRelaySrv(srvRecord string, port string) {
	nc.relayEndpoints = append(nc.relayEndpoints, srvEntry{srvRecord, port})
}

func (nc *nodeConfigurator) registerMetricsSrv(srvRecord string, port string) {
	nc.metricsEndpoints = append(nc.metricsEndpoints, srvEntry{srvRecord, port})
}