-
Notifications
You must be signed in to change notification settings - Fork 0
/
cmd_profile.go
449 lines (392 loc) · 11.5 KB
/
cmd_profile.go
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
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
package main
import (
"fmt"
"io/ioutil"
"os"
"path"
"strings"
"github.com/brsuite/bronutil"
"github.com/brsuite/lnd/lncfg"
"github.com/urfave/cli"
"gopkg.in/macaroon.v2"
)
var (
// defaultLncliDir is the default directory to store the profile file
// in. This defaults to:
// C:\Users\<username>\AppData\Local\Lncli\ on Windows
// ~/.lncli/ on Linux
// ~/Library/Application Support/Lncli/ on MacOS
defaultLncliDir = bronutil.AppDataDir("lncli", false)
// defaultProfileFile is the full, absolute path of the profile file.
defaultProfileFile = path.Join(defaultLncliDir, "profiles.json")
)
var profileSubCommand = cli.Command{
Name: "profile",
Category: "Profiles",
Usage: "Create and manage lncli profiles.",
Description: `
Profiles for lncli are an easy and comfortable way to manage multiple
nodes from the command line by storing node specific parameters like RPC
host, network, TLS certificate path or macaroons in a named profile.
To use a predefined profile, just use the '--profile=myprofile' (or
short version '-p=myprofile') with any lncli command.
A default profile can also be defined, lncli will then always use the
connection/node parameters from that profile instead of the default
values.
WARNING: Setting a default profile changes the default behavior of
lncli! To disable the use of the default profile for a single command,
set '--profile= '.
The profiles are stored in a file called profiles.json in the user's
home directory, for example:
C:\Users\<username>\AppData\Local\Lncli\profiles.json on Windows
~/.lncli/profiles.json on Linux
~/Library/Application Support/Lncli/profiles.json on MacOS
`,
Subcommands: []cli.Command{
profileListCommand,
profileAddCommand,
profileRemoveCommand,
profileSetDefaultCommand,
profileUnsetDefaultCommand,
profileAddMacaroonCommand,
},
}
var profileListCommand = cli.Command{
Name: "list",
Usage: "Lists all lncli profiles",
Action: profileList,
}
func profileList(_ *cli.Context) error {
f, err := loadProfileFile(defaultProfileFile)
if err != nil {
return err
}
printJSON(f)
return nil
}
var profileAddCommand = cli.Command{
Name: "add",
Usage: "Add a new profile.",
ArgsUsage: "name",
Description: `
Add a new named profile to the main profiles.json. All global options
(see 'lncli --help') passed into this command are stored in that named
profile.
`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "name",
Usage: "the name of the new profile",
},
cli.BoolFlag{
Name: "default",
Usage: "set the new profile to be the default profile",
},
},
Action: profileAdd,
}
func profileAdd(ctx *cli.Context) error {
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
return cli.ShowCommandHelp(ctx, "add")
}
// Load the default profile file or create a new one if it doesn't exist
// yet.
f, err := loadProfileFile(defaultProfileFile)
switch {
case err == errNoProfileFile:
f = &profileFile{}
_ = os.MkdirAll(path.Dir(defaultProfileFile), 0700)
case err != nil:
return err
}
// Create a profile struct from all the global options.
profile, err := profileFromContext(ctx, true, false)
if err != nil {
return fmt.Errorf("could not load global options: %v", err)
}
// Finally, all that's left is to get the profile name from either
// positional argument or flag.
args := ctx.Args()
switch {
case ctx.IsSet("name"):
profile.Name = ctx.String("name")
case args.Present():
profile.Name = args.First()
default:
return fmt.Errorf("name argument missing")
}
// Is there already a profile with that name?
for _, p := range f.Profiles {
if p.Name == profile.Name {
return fmt.Errorf("a profile with the name %s already "+
"exists", profile.Name)
}
}
// Do we need to update the default entry to be this one?
if ctx.Bool("default") {
f.Default = profile.Name
}
// All done, store the updated profile file.
f.Profiles = append(f.Profiles, profile)
if err = saveProfileFile(defaultProfileFile, f); err != nil {
return fmt.Errorf("error writing profile file %s: %v",
defaultProfileFile, err)
}
fmt.Printf("Profile %s added to file %s.\n", profile.Name,
defaultProfileFile)
return nil
}
var profileRemoveCommand = cli.Command{
Name: "remove",
Usage: "Remove a profile",
ArgsUsage: "name",
Description: `Remove the specified profile from the profile file.`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "name",
Usage: "the name of the profile to delete",
},
},
Action: profileRemove,
}
func profileRemove(ctx *cli.Context) error {
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
return cli.ShowCommandHelp(ctx, "remove")
}
// Load the default profile file.
f, err := loadProfileFile(defaultProfileFile)
if err != nil {
return fmt.Errorf("could not load profile file: %v", err)
}
// Get the profile name from either positional argument or flag.
var (
args = ctx.Args()
name string
found = false
)
switch {
case ctx.IsSet("name"):
name = ctx.String("name")
case args.Present():
name = args.First()
default:
return fmt.Errorf("name argument missing")
}
// Create a copy of all profiles but don't include the one to delete.
newProfiles := make([]*profileEntry, 0, len(f.Profiles)-1)
for _, p := range f.Profiles {
// Skip the one we want to delete.
if p.Name == name {
found = true
if p.Name == f.Default {
fmt.Println("Warning: removing default profile.")
}
continue
}
// Keep all others.
newProfiles = append(newProfiles, p)
}
// If what we were looking for didn't exist in the first place, there's
// no need for updating the file.
if !found {
return fmt.Errorf("profile with name %s not found in file",
name)
}
// Great, everything updated, now let's save the file.
f.Profiles = newProfiles
return saveProfileFile(defaultProfileFile, f)
}
var profileSetDefaultCommand = cli.Command{
Name: "setdefault",
Usage: "Set the default profile.",
ArgsUsage: "name",
Description: `
Set a specified profile to be used as the default profile.
WARNING: Setting a default profile changes the default behavior of
lncli! To disable the use of the default profile for a single command,
set '--profile= '.
`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "name",
Usage: "the name of the profile to set as default",
},
},
Action: profileSetDefault,
}
func profileSetDefault(ctx *cli.Context) error {
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
return cli.ShowCommandHelp(ctx, "setdefault")
}
// Load the default profile file.
f, err := loadProfileFile(defaultProfileFile)
if err != nil {
return fmt.Errorf("could not load profile file: %v", err)
}
// Get the profile name from either positional argument or flag.
var (
args = ctx.Args()
name string
found = false
)
switch {
case ctx.IsSet("name"):
name = ctx.String("name")
case args.Present():
name = args.First()
default:
return fmt.Errorf("name argument missing")
}
// Make sure the new default profile actually exists.
for _, p := range f.Profiles {
if p.Name == name {
found = true
f.Default = p.Name
break
}
}
// If the default profile doesn't exist, there's no need for updating
// the file.
if !found {
return fmt.Errorf("profile with name %s not found in file",
name)
}
// Great, everything updated, now let's save the file.
return saveProfileFile(defaultProfileFile, f)
}
var profileUnsetDefaultCommand = cli.Command{
Name: "unsetdefault",
Usage: "Unsets the default profile.",
Description: `
Disables the use of a default profile and restores lncli to its original
behavior.
`,
Action: profileUnsetDefault,
}
func profileUnsetDefault(_ *cli.Context) error {
// Load the default profile file.
f, err := loadProfileFile(defaultProfileFile)
if err != nil {
return fmt.Errorf("could not load profile file: %v", err)
}
// Save the file with the flag disabled.
f.Default = ""
return saveProfileFile(defaultProfileFile, f)
}
var profileAddMacaroonCommand = cli.Command{
Name: "addmacaroon",
Usage: "Add a macaroon to a profile's macaroon jar.",
ArgsUsage: "macaroon-name",
Description: `
Add an additional macaroon specified by the global option --macaroonpath
to an existing profile's macaroon jar.
If no profile is selected, the macaroon is added to the default profile
(if one exists). To add a macaroon to a specific profile, use the global
--profile=myprofile option.
If multiple macaroons exist in a profile's macaroon jar, the one to use
can be specified with the global option --macfromjar=xyz.
`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "name",
Usage: "the name of the macaroon",
},
cli.BoolFlag{
Name: "default",
Usage: "set the new macaroon to be the default " +
"macaroon in the jar",
},
},
Action: profileAddMacaroon,
}
func profileAddMacaroon(ctx *cli.Context) error {
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
return cli.ShowCommandHelp(ctx, "addmacaroon")
}
// Load the default profile file or create a new one if it doesn't exist
// yet.
f, err := loadProfileFile(defaultProfileFile)
if err != nil {
return fmt.Errorf("could not load profile file: %v", err)
}
// Finally, all that's left is to get the profile name from either
// positional argument or flag.
var (
args = ctx.Args()
profileName string
macName string
)
switch {
case ctx.IsSet("name"):
macName = ctx.String("name")
case args.Present():
macName = args.First()
default:
return fmt.Errorf("name argument missing")
}
// Make sure the user actually set a macaroon path to use.
if !ctx.GlobalIsSet("macaroonpath") {
return fmt.Errorf("macaroonpath global option missing")
}
// Find out which profile we should add the macaroon. The global flag
// takes precedence over the default profile.
if f.Default != "" {
profileName = f.Default
}
if ctx.GlobalIsSet("profile") {
profileName = ctx.GlobalString("profile")
}
if len(strings.TrimSpace(profileName)) == 0 {
return fmt.Errorf("no profile specified and no default " +
"profile exists")
}
// Is there a profile with that name?
var selectedProfile *profileEntry
for _, p := range f.Profiles {
if p.Name == profileName {
selectedProfile = p
break
}
}
if selectedProfile == nil {
return fmt.Errorf("profile with name %s not found", profileName)
}
// Does a macaroon with that name already exist?
for _, m := range selectedProfile.Macaroons.Jar {
if m.Name == macName {
return fmt.Errorf("a macaroon with the name %s "+
"already exists", macName)
}
}
// Do we need to update the default entry to be this one?
if ctx.Bool("default") {
selectedProfile.Macaroons.Default = macName
}
// Now load and possibly encrypt the macaroon file.
macPath := lncfg.CleanAndExpandPath(ctx.GlobalString("macaroonpath"))
macBytes, err := ioutil.ReadFile(macPath)
if err != nil {
return fmt.Errorf("unable to read macaroon path: %v", err)
}
mac := &macaroon.Macaroon{}
if err = mac.UnmarshalBinary(macBytes); err != nil {
return fmt.Errorf("unable to decode macaroon: %v", err)
}
macEntry := &macaroonEntry{
Name: macName,
}
if err = macEntry.storeMacaroon(mac, nil); err != nil {
return fmt.Errorf("unable to store macaroon: %v", err)
}
// All done, store the updated profile file.
selectedProfile.Macaroons.Jar = append(
selectedProfile.Macaroons.Jar, macEntry,
)
if err = saveProfileFile(defaultProfileFile, f); err != nil {
return fmt.Errorf("error writing profile file %s: %v",
defaultProfileFile, err)
}
fmt.Printf("Macaroon %s added to profile %s in file %s.\n", macName,
selectedProfile.Name, defaultProfileFile)
return nil
}