leap_cli 1.2.5 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/leap +5 -0
- data/lib/leap/platform.rb +4 -1
- data/lib/leap_cli/commands/compile.rb +50 -0
- data/lib/leap_cli/commands/deploy.rb +7 -2
- data/lib/leap_cli/commands/inspect.rb +6 -2
- data/lib/leap_cli/commands/node.rb +5 -1
- data/lib/leap_cli/commands/pre.rb +8 -0
- data/lib/leap_cli/commands/shell.rb +26 -5
- data/lib/leap_cli/commands/test.rb +14 -4
- data/lib/leap_cli/commands/user.rb +9 -21
- data/lib/leap_cli/commands/vagrant.rb +2 -2
- data/lib/leap_cli/config/macros.rb +74 -37
- data/lib/leap_cli/config/manager.rb +23 -5
- data/lib/leap_cli/config/node.rb +8 -0
- data/lib/leap_cli/config/object.rb +51 -42
- data/lib/leap_cli/config/object_list.rb +21 -1
- data/lib/leap_cli/config/provider.rb +11 -0
- data/lib/leap_cli/config/secrets.rb +14 -9
- data/lib/leap_cli/log.rb +8 -2
- data/lib/leap_cli/logger.rb +1 -1
- data/lib/leap_cli/path.rb +4 -0
- data/lib/leap_cli/remote/tasks.rb +24 -0
- data/lib/leap_cli/util/remote_command.rb +7 -0
- data/lib/leap_cli/version.rb +2 -2
- data/lib/leap_cli.rb +1 -0
- metadata +3 -18
data/bin/leap
CHANGED
data/lib/leap/platform.rb
CHANGED
@@ -13,9 +13,12 @@ module Leap
|
|
13
13
|
attr_accessor :facts
|
14
14
|
attr_accessor :paths
|
15
15
|
attr_accessor :node_files
|
16
|
-
attr_accessor :
|
16
|
+
attr_accessor :monitor_username
|
17
|
+
attr_accessor :reserved_usernames
|
17
18
|
|
18
19
|
def define(&block)
|
20
|
+
# some sanity defaults:
|
21
|
+
@reserved_usernames = []
|
19
22
|
self.instance_eval(&block)
|
20
23
|
end
|
21
24
|
|
@@ -33,10 +33,60 @@ module LeapCli
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def update_compiled_ssh_configs
|
36
|
+
generate_monitor_ssh_keys
|
36
37
|
update_authorized_keys
|
37
38
|
update_known_hosts
|
38
39
|
end
|
39
40
|
|
41
|
+
##
|
42
|
+
## SSH
|
43
|
+
##
|
44
|
+
|
45
|
+
#
|
46
|
+
# generates a ssh key pair that is used only by remote monitors
|
47
|
+
# to connect to nodes and run certain allowed commands.
|
48
|
+
#
|
49
|
+
# every node has the public monitor key added to their authorized
|
50
|
+
# keys, and every monitor node has a copy of the private monitor key.
|
51
|
+
#
|
52
|
+
def generate_monitor_ssh_keys
|
53
|
+
priv_key_file = :monitor_priv_key
|
54
|
+
pub_key_file = :monitor_pub_key
|
55
|
+
unless file_exists?(priv_key_file, pub_key_file)
|
56
|
+
cmd = %(ssh-keygen -N '' -C 'monitor' -t ecdsa -b 521 -f '%s') % path(priv_key_file)
|
57
|
+
assert_run! cmd
|
58
|
+
if file_exists?(priv_key_file, pub_key_file)
|
59
|
+
log :created, path(priv_key_file)
|
60
|
+
log :created, path(pub_key_file)
|
61
|
+
else
|
62
|
+
log :failed, 'to create monitor ssh keys'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Compiles the authorized keys file, which gets installed on every during init.
|
69
|
+
# Afterwards, puppet installs an authorized keys file that is generated differently
|
70
|
+
# (see authorized_keys() in macros.rb)
|
71
|
+
#
|
72
|
+
def update_authorized_keys
|
73
|
+
buffer = StringIO.new
|
74
|
+
keys = Dir.glob(path([:user_ssh, '*']))
|
75
|
+
if keys.empty?
|
76
|
+
bail! "You must have at least one public SSH user key configured in order to proceed. See `leap help add-user`."
|
77
|
+
end
|
78
|
+
keys.sort.each do |keyfile|
|
79
|
+
ssh_type, ssh_key = File.read(keyfile).strip.split(" ")
|
80
|
+
buffer << ssh_type
|
81
|
+
buffer << " "
|
82
|
+
buffer << ssh_key
|
83
|
+
buffer << " "
|
84
|
+
buffer << Path.relative_path(keyfile)
|
85
|
+
buffer << "\n"
|
86
|
+
end
|
87
|
+
write_file!(:authorized_keys, buffer.string)
|
88
|
+
end
|
89
|
+
|
40
90
|
##
|
41
91
|
## ZONE FILE
|
42
92
|
##
|
@@ -11,6 +11,9 @@ module LeapCli
|
|
11
11
|
c.switch :fast, :desc => 'Makes the deploy command faster by skipping some slow steps. A "fast" deploy can be used safely if you recently completed a normal deploy.',
|
12
12
|
:negatable => false
|
13
13
|
|
14
|
+
# --sync
|
15
|
+
c.switch :sync, :desc => "Sync files, but don't actually apply recipes."
|
16
|
+
|
14
17
|
# --force
|
15
18
|
c.switch :force, :desc => 'Deploy even if there is a lockfile.', :negatable => false
|
16
19
|
|
@@ -49,8 +52,10 @@ module LeapCli
|
|
49
52
|
ssh.leap.log :synching, "puppet manifests" do
|
50
53
|
sync_puppet_files(ssh)
|
51
54
|
end
|
52
|
-
|
53
|
-
ssh.
|
55
|
+
unless options[:sync]
|
56
|
+
ssh.leap.log :applying, "puppet" do
|
57
|
+
ssh.puppet.apply(:verbosity => LeapCli.log_level, :tags => tags(options), :force => options[:force])
|
58
|
+
end
|
54
59
|
end
|
55
60
|
end
|
56
61
|
end
|
@@ -39,7 +39,7 @@ module LeapCli; module Commands
|
|
39
39
|
:inspect_service
|
40
40
|
elsif path_match?(:tag_config, full_path)
|
41
41
|
:inspect_tag
|
42
|
-
elsif path_match?(:provider_config, full_path)
|
42
|
+
elsif path_match?(:provider_config, full_path) || path_match?(:provider_env_config, full_path)
|
43
43
|
:inspect_provider
|
44
44
|
elsif path_match?(:common_config, full_path)
|
45
45
|
:inspect_common
|
@@ -108,6 +108,8 @@ module LeapCli; module Commands
|
|
108
108
|
def inspect_provider(arg, options)
|
109
109
|
if options[:base]
|
110
110
|
inspect_json manager.base_provider
|
111
|
+
elsif arg =~ /provider\.(.*)\.json/
|
112
|
+
inspect_json manager.providers[$1]
|
111
113
|
else
|
112
114
|
inspect_json manager.provider
|
113
115
|
end
|
@@ -130,7 +132,9 @@ module LeapCli; module Commands
|
|
130
132
|
end
|
131
133
|
|
132
134
|
def inspect_json(config)
|
133
|
-
|
135
|
+
if config
|
136
|
+
puts JSON.sorted_generate(config)
|
137
|
+
end
|
134
138
|
end
|
135
139
|
|
136
140
|
def path_match?(path_symbol, path)
|
@@ -63,7 +63,11 @@ module LeapCli; module Commands
|
|
63
63
|
update_compiled_ssh_configs
|
64
64
|
ssh_connect_options = connect_options(options).merge({:bootstrap => true, :echo => options[:echo]})
|
65
65
|
ssh_connect(node, ssh_connect_options) do |ssh|
|
66
|
-
|
66
|
+
if node.vagrant?
|
67
|
+
ssh.install_authorized_keys2
|
68
|
+
else
|
69
|
+
ssh.install_authorized_keys
|
70
|
+
end
|
67
71
|
ssh.install_prerequisites
|
68
72
|
ssh.leap.capture(facter_cmd) do |response|
|
69
73
|
if response[:exitcode] == 0
|
@@ -20,6 +20,13 @@ module LeapCli; module Commands
|
|
20
20
|
desc 'Skip prompts and assume "yes"'
|
21
21
|
switch :yes, :negatable => false
|
22
22
|
|
23
|
+
desc 'Enable debugging library (leap_cli development only)'
|
24
|
+
switch :debug, :negatable => false
|
25
|
+
|
26
|
+
desc 'Disable colors in output'
|
27
|
+
default_value true
|
28
|
+
switch 'color', :negatable => true
|
29
|
+
|
23
30
|
pre do |global,command,options,args|
|
24
31
|
#
|
25
32
|
# set verbosity
|
@@ -59,6 +66,7 @@ module LeapCli; module Commands
|
|
59
66
|
LeapCli.log_file = global[:log] || LeapCli.leapfile.log
|
60
67
|
LeapCli::Util.log_raw(:log) { $0 + ' ' + ORIGINAL_ARGV.join(' ')}
|
61
68
|
log_version
|
69
|
+
LeapCli.log_in_color = global[:color]
|
62
70
|
|
63
71
|
#
|
64
72
|
# load all the nodes everything
|
@@ -32,18 +32,28 @@ module LeapCli; module Commands
|
|
32
32
|
return connect_options
|
33
33
|
end
|
34
34
|
|
35
|
+
def ssh_config_help_message
|
36
|
+
puts ""
|
37
|
+
puts "Are 'too many authentication failures' getting you down?"
|
38
|
+
puts "Then we have the solution for you! Add something like this to your ~/.ssh/config file:"
|
39
|
+
puts " Host *.#{manager.provider.domain}"
|
40
|
+
puts " IdentityFile ~/.ssh/id_rsa"
|
41
|
+
puts " IdentitiesOnly=yes"
|
42
|
+
puts "(replace `id_rsa` with the actual private key filename that you use for this provider)"
|
43
|
+
end
|
44
|
+
|
35
45
|
private
|
36
46
|
|
37
47
|
def exec_ssh(cmd, args)
|
38
48
|
node = get_node_from_args(args, :include_disabled => true)
|
39
49
|
options = [
|
40
|
-
"-o 'HostName=#{node.
|
50
|
+
"-o 'HostName=#{node.domain.full}'",
|
41
51
|
# "-o 'HostKeyAlias=#{node.name}'", << oddly incompatible with ports in known_hosts file, so we must not use this or non-standard ports break.
|
42
52
|
"-o 'GlobalKnownHostsFile=#{path(:known_hosts)}'",
|
43
53
|
"-o 'UserKnownHostsFile=/dev/null'"
|
44
54
|
]
|
45
55
|
if node.vagrant?
|
46
|
-
options << "-i #{vagrant_ssh_key_file}"
|
56
|
+
options << "-i #{vagrant_ssh_key_file}" # use the universal vagrant insecure key
|
47
57
|
options << "-o 'StrictHostKeyChecking=no'" # blindly accept host key and don't save it (since userknownhostsfile is /dev/null)
|
48
58
|
else
|
49
59
|
options << "-o 'StrictHostKeyChecking=yes'"
|
@@ -56,12 +66,23 @@ module LeapCli; module Commands
|
|
56
66
|
end
|
57
67
|
ssh = "ssh -l #{username} -p #{node.ssh.port} #{options.join(' ')}"
|
58
68
|
if cmd == :ssh
|
59
|
-
command = "#{ssh} #{node.
|
69
|
+
command = "#{ssh} #{node.domain.full}"
|
60
70
|
elsif cmd == :mosh
|
61
|
-
command = "MOSH_TITLE_NOPREFIX=1 mosh --ssh \"#{ssh}\" #{node.
|
71
|
+
command = "MOSH_TITLE_NOPREFIX=1 mosh --ssh \"#{ssh}\" #{node.domain.full}"
|
62
72
|
end
|
63
73
|
log 2, command
|
64
|
-
|
74
|
+
|
75
|
+
# exec the shell command in a subprocess
|
76
|
+
pid = fork { exec "#{command}" }
|
77
|
+
|
78
|
+
# wait for shell to exit so we can grab the exit status
|
79
|
+
_, status = Process.waitpid2(pid)
|
80
|
+
|
81
|
+
if status.exitstatus == 255
|
82
|
+
ssh_config_help_message
|
83
|
+
elsif status.exitstatus != 0
|
84
|
+
exit_now! status.exitstatus, status.exitstatus
|
85
|
+
end
|
65
86
|
end
|
66
87
|
|
67
88
|
end; end
|
@@ -11,10 +11,16 @@ module LeapCli; module Commands
|
|
11
11
|
|
12
12
|
test.desc 'Run tests.'
|
13
13
|
test.command :run do |run|
|
14
|
+
run.switch 'continue', :desc => 'Continue over errors and failures (default is --no-continue).', :negatable => true
|
14
15
|
run.action do |global_options,options,args|
|
15
|
-
|
16
|
+
test_order = File.join(Path.platform, 'tests/order.rb')
|
17
|
+
if File.exists?(test_order)
|
18
|
+
require test_order
|
19
|
+
end
|
20
|
+
manager.filter!(args).names_in_test_dependency_order.each do |node_name|
|
21
|
+
node = manager.nodes[node_name]
|
16
22
|
ssh_connect(node) do |ssh|
|
17
|
-
ssh.run(test_cmd)
|
23
|
+
ssh.run(test_cmd(options))
|
18
24
|
end
|
19
25
|
end
|
20
26
|
end
|
@@ -25,8 +31,12 @@ module LeapCli; module Commands
|
|
25
31
|
|
26
32
|
private
|
27
33
|
|
28
|
-
def test_cmd
|
29
|
-
|
34
|
+
def test_cmd(options)
|
35
|
+
if options[:continue]
|
36
|
+
"#{PUPPET_DESTINATION}/bin/run_tests --continue"
|
37
|
+
else
|
38
|
+
"#{PUPPET_DESTINATION}/bin/run_tests"
|
39
|
+
end
|
30
40
|
end
|
31
41
|
|
32
42
|
#
|
@@ -24,8 +24,15 @@ module LeapCli
|
|
24
24
|
|
25
25
|
c.action do |global_options,options,args|
|
26
26
|
username = args.first
|
27
|
-
if !username.any?
|
28
|
-
|
27
|
+
if !username.any?
|
28
|
+
if options[:self]
|
29
|
+
username ||= `whoami`.strip
|
30
|
+
else
|
31
|
+
help! "Either USERNAME argument or --self flag is required."
|
32
|
+
end
|
33
|
+
end
|
34
|
+
if Leap::Platform.reserved_usernames.include? username
|
35
|
+
bail! %(The username "#{username}" is reserved. Sorry, pick another.)
|
29
36
|
end
|
30
37
|
|
31
38
|
ssh_pub_key = nil
|
@@ -39,7 +46,6 @@ module LeapCli
|
|
39
46
|
end
|
40
47
|
|
41
48
|
if options[:self]
|
42
|
-
username ||= `whoami`.strip
|
43
49
|
ssh_pub_key ||= pick_ssh_key.to_s
|
44
50
|
pgp_pub_key ||= pick_pgp_key
|
45
51
|
end
|
@@ -118,23 +124,5 @@ module LeapCli
|
|
118
124
|
return `gpg --armor --export-options export-minimal --export #{key_id}`.strip
|
119
125
|
end
|
120
126
|
|
121
|
-
def update_authorized_keys
|
122
|
-
buffer = StringIO.new
|
123
|
-
keys = Dir.glob(path([:user_ssh, '*']))
|
124
|
-
if keys.empty?
|
125
|
-
bail! "You must have at least one public SSH user key configured in order to proceed. See `leap help add-user`."
|
126
|
-
end
|
127
|
-
keys.sort.each do |keyfile|
|
128
|
-
ssh_type, ssh_key = File.read(keyfile).strip.split(" ")
|
129
|
-
buffer << ssh_type
|
130
|
-
buffer << " "
|
131
|
-
buffer << ssh_key
|
132
|
-
buffer << " "
|
133
|
-
buffer << Path.relative_path(keyfile)
|
134
|
-
buffer << "\n"
|
135
|
-
end
|
136
|
-
write_file!(:authorized_keys, buffer.string)
|
137
|
-
end
|
138
|
-
|
139
127
|
end
|
140
128
|
end
|
@@ -156,7 +156,7 @@ module LeapCli; module Commands
|
|
156
156
|
if node.vagrant?
|
157
157
|
lines << %[ config.vm.define :#{node.name} do |config|]
|
158
158
|
lines << %[ config.vm.box = "leap-wheezy"]
|
159
|
-
lines << %[ config.vm.box_url = "
|
159
|
+
lines << %[ config.vm.box_url = "https://downloads.leap.se/leap-debian.box"]
|
160
160
|
lines << %[ config.vm.network :hostonly, "#{node.ip_address}", :netmask => "#{netmask}"]
|
161
161
|
lines << %[ config.vm.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]]
|
162
162
|
lines << %[ config.vm.customize ["modifyvm", :id, "--name", "#{node.name}"]]
|
@@ -170,7 +170,7 @@ module LeapCli; module Commands
|
|
170
170
|
if node.vagrant?
|
171
171
|
lines << %[ config.vm.define :#{node.name} do |config|]
|
172
172
|
lines << %[ config.vm.box = "leap-wheezy"]
|
173
|
-
lines << %[ config.vm.box_url = "
|
173
|
+
lines << %[ config.vm.box_url = "https://downloads.leap.se/leap-debian.box"]
|
174
174
|
lines << %[ config.vm.network :private_network, ip: "#{node.ip_address}"]
|
175
175
|
lines << %[ config.vm.provider "virtualbox" do |v|]
|
176
176
|
lines << %[ v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]]
|
@@ -18,6 +18,13 @@ module LeapCli; module Config
|
|
18
18
|
global.nodes
|
19
19
|
end
|
20
20
|
|
21
|
+
#
|
22
|
+
# grab an environment appropriate provider
|
23
|
+
#
|
24
|
+
def provider
|
25
|
+
global.providers[@node.environment] || global.provider
|
26
|
+
end
|
27
|
+
|
21
28
|
#
|
22
29
|
# returns a list of nodes that match the same environment
|
23
30
|
#
|
@@ -119,7 +126,7 @@ module LeapCli; module Config
|
|
119
126
|
# +length+ is the character length of the generated password.
|
120
127
|
#
|
121
128
|
def secret(name, length=32)
|
122
|
-
@manager.secrets.set(name, Util::Secret.generate(length))
|
129
|
+
@manager.secrets.set(name, Util::Secret.generate(length), @node[:environment])
|
123
130
|
end
|
124
131
|
|
125
132
|
#
|
@@ -128,7 +135,7 @@ module LeapCli; module Config
|
|
128
135
|
# +bit_length+ is the bits in the secret, (ie length of resulting hex string will be bit_length/4)
|
129
136
|
#
|
130
137
|
def hex_secret(name, bit_length=128)
|
131
|
-
@manager.secrets.set(name, Util::Secret.generate_hex(bit_length))
|
138
|
+
@manager.secrets.set(name, Util::Secret.generate_hex(bit_length), @node[:environment])
|
132
139
|
end
|
133
140
|
|
134
141
|
#
|
@@ -157,31 +164,43 @@ module LeapCli; module Config
|
|
157
164
|
end
|
158
165
|
|
159
166
|
#
|
160
|
-
# Generates entries needed for updating /etc/hosts on a node
|
161
|
-
#
|
162
|
-
#
|
163
|
-
#
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
167
|
+
# Generates entries needed for updating /etc/hosts on a node (as a hash).
|
168
|
+
#
|
169
|
+
# Argument `nodes` can be nil or a list of nodes. If nil, only include the
|
170
|
+
# IPs of the other nodes this @node as has encountered (plus all mx nodes).
|
171
|
+
#
|
172
|
+
# Also, for virtual machines, we use the local address if this @node is in
|
173
|
+
# the same location as the node in question.
|
174
|
+
#
|
175
|
+
# We include the ssh public key for each host, so that the hash can also
|
176
|
+
# be used to generate the /etc/ssh/known_hosts
|
177
|
+
#
|
178
|
+
def hosts_file(nodes=nil)
|
179
|
+
if nodes.nil?
|
180
|
+
if @referenced_nodes && @referenced_nodes.any?
|
181
|
+
nodes = @referenced_nodes
|
182
|
+
nodes = nodes.merge(nodes_like_me[:services => 'mx']) # all nodes always need to communicate with mx nodes.
|
183
|
+
end
|
184
|
+
end
|
185
|
+
return nil unless nodes
|
186
|
+
hosts = {}
|
187
|
+
my_location = @node['location'] ? @node['location']['name'] : nil
|
188
|
+
nodes.each_node do |node|
|
189
|
+
hosts[node.name] = {'ip_address' => node.ip_address, 'domain_internal' => node.domain.internal, 'domain_full' => node.domain.full}
|
190
|
+
node_location = node['location'] ? node['location']['name'] : nil
|
191
|
+
if my_location == node_location
|
192
|
+
if facts = @node.manager.facts[node.name]
|
193
|
+
if facts['ec2_public_ipv4']
|
194
|
+
hosts[node.name]['ip_address'] = facts['ec2_public_ipv4']
|
177
195
|
end
|
178
196
|
end
|
179
197
|
end
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
198
|
+
host_pub_key = Util::read_file([:node_ssh_pub_key,node.name])
|
199
|
+
if host_pub_key
|
200
|
+
hosts[node.name]['host_pub_key'] = host_pub_key
|
201
|
+
end
|
184
202
|
end
|
203
|
+
hosts
|
185
204
|
end
|
186
205
|
|
187
206
|
##
|
@@ -315,11 +334,15 @@ module LeapCli; module Config
|
|
315
334
|
##
|
316
335
|
|
317
336
|
#
|
318
|
-
#
|
337
|
+
# Creates a hash from the ssh key info in users directory, for use in
|
338
|
+
# updating authorized_keys file. Additionally, the 'monitor' public key is
|
339
|
+
# included, which is used by the monitor nodes to run particular commands
|
340
|
+
# remotely.
|
319
341
|
#
|
320
342
|
def authorized_keys
|
321
343
|
hash = {}
|
322
|
-
Dir.glob(Path.named_path([:user_ssh, '*']))
|
344
|
+
keys = Dir.glob(Path.named_path([:user_ssh, '*']))
|
345
|
+
keys.sort.each do |keyfile|
|
323
346
|
ssh_type, ssh_key = File.read(keyfile).strip.split(" ")
|
324
347
|
name = File.basename(File.dirname(keyfile))
|
325
348
|
hash[name] = {
|
@@ -327,21 +350,35 @@ module LeapCli; module Config
|
|
327
350
|
"key" => ssh_key
|
328
351
|
}
|
329
352
|
end
|
353
|
+
ssh_type, ssh_key = File.read(Path.named_path(:monitor_pub_key)).strip.split(" ")
|
354
|
+
hash[Leap::Platform.monitor_username] = {
|
355
|
+
"type" => ssh_type,
|
356
|
+
"key" => ssh_key
|
357
|
+
}
|
330
358
|
hash
|
331
359
|
end
|
332
360
|
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
361
|
+
#
|
362
|
+
# this is not currently used, because we put key information in the 'hosts' hash.
|
363
|
+
# see 'hosts_file()'
|
364
|
+
#
|
365
|
+
# def known_hosts_file(nodes=nil)
|
366
|
+
# if nodes.nil?
|
367
|
+
# if @referenced_nodes && @referenced_nodes.any?
|
368
|
+
# nodes = @referenced_nodes
|
369
|
+
# end
|
370
|
+
# end
|
371
|
+
# return nil unless nodes
|
372
|
+
# entries = []
|
373
|
+
# nodes.each_node do |node|
|
374
|
+
# hostnames = [node.name, node.domain.internal, node.domain.full, node.ip_address].join(',')
|
375
|
+
# pub_key = Util::read_file([:node_ssh_pub_key,node.name])
|
376
|
+
# if pub_key
|
377
|
+
# entries << [hostnames, pub_key].join(' ')
|
378
|
+
# end
|
379
|
+
# end
|
380
|
+
# entries.join("\n")
|
381
|
+
# end
|
345
382
|
|
346
383
|
##
|
347
384
|
## UTILITY
|
@@ -16,7 +16,7 @@ module LeapCli
|
|
16
16
|
## ATTRIBUTES
|
17
17
|
##
|
18
18
|
|
19
|
-
attr_reader :services, :tags, :nodes, :provider, :common, :secrets
|
19
|
+
attr_reader :services, :tags, :nodes, :provider, :providers, :common, :secrets
|
20
20
|
attr_reader :base_services, :base_tags, :base_provider, :base_common
|
21
21
|
|
22
22
|
#
|
@@ -48,7 +48,7 @@ module LeapCli
|
|
48
48
|
@base_services = load_all_json(Path.named_path([:service_config, '*'], Path.provider_base), Config::Tag)
|
49
49
|
@base_tags = load_all_json(Path.named_path([:tag_config, '*'], Path.provider_base), Config::Tag)
|
50
50
|
@base_common = load_json(Path.named_path(:common_config, Path.provider_base), Config::Object)
|
51
|
-
@base_provider = load_json(Path.named_path(:provider_config, Path.provider_base), Config::
|
51
|
+
@base_provider = load_json(Path.named_path(:provider_config, Path.provider_base), Config::Provider)
|
52
52
|
|
53
53
|
# load provider
|
54
54
|
provider_path = Path.named_path(:provider_config, @provider_dir)
|
@@ -58,9 +58,17 @@ module LeapCli
|
|
58
58
|
@tags = load_all_json(Path.named_path([:tag_config, '*'], @provider_dir), Config::Tag)
|
59
59
|
@nodes = load_all_json(Path.named_path([:node_config, '*'], @provider_dir), Config::Node)
|
60
60
|
@common = load_json(common_path, Config::Object)
|
61
|
-
@provider = load_json(provider_path, Config::
|
61
|
+
@provider = load_json(provider_path, Config::Provider)
|
62
62
|
@secrets = load_json(Path.named_path(:secrets_config, @provider_dir), Config::Secrets)
|
63
63
|
|
64
|
+
### BEGIN HACK
|
65
|
+
### remove this after it is likely that no one has any old-style secrets.json
|
66
|
+
if @secrets['webapp_secret_token']
|
67
|
+
@secrets = Config::Secrets.new
|
68
|
+
Util::log :warning, "Creating all new secrets.json (new version is scoped by environment). Make sure to do a full deploy so that new secrets take effect."
|
69
|
+
end
|
70
|
+
### END HACK
|
71
|
+
|
64
72
|
# inherit
|
65
73
|
@services.inherit_from! base_services
|
66
74
|
@tags.inherit_from! base_tags
|
@@ -75,8 +83,18 @@ module LeapCli
|
|
75
83
|
remove_disabled_nodes
|
76
84
|
end
|
77
85
|
|
78
|
-
#
|
86
|
+
# load optional environment specific providers
|
79
87
|
validate_provider(@provider)
|
88
|
+
@providers = {}
|
89
|
+
environments.each do |env|
|
90
|
+
if Path.defined?(:provider_env_config)
|
91
|
+
provider_path = Path.named_path([:provider_env_config, env], @provider_dir)
|
92
|
+
providers[env] = load_json(provider_path, Config::Provider)
|
93
|
+
providers[env].inherit_from! @provider
|
94
|
+
validate_provider(providers[env])
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
80
98
|
end
|
81
99
|
|
82
100
|
#
|
@@ -100,7 +118,7 @@ module LeapCli
|
|
100
118
|
node_list.each_node do |node|
|
101
119
|
filepath = Path.named_path([:node_files_dir, node.name], @provider_dir)
|
102
120
|
hierapath = Path.named_path([:hiera, node.name], @provider_dir)
|
103
|
-
Util::write_file!(hierapath, node.
|
121
|
+
Util::write_file!(hierapath, node.dump_yaml)
|
104
122
|
updated_files << filepath
|
105
123
|
updated_hiera << hierapath
|
106
124
|
end
|
data/lib/leap_cli/config/node.rb
CHANGED
@@ -32,6 +32,14 @@ module LeapCli; module Config
|
|
32
32
|
end
|
33
33
|
return vagrant_range.include?(ip_address)
|
34
34
|
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# can be overridden by the platform.
|
38
|
+
# returns a list of node names that should be tested before this node
|
39
|
+
#
|
40
|
+
def test_dependencies
|
41
|
+
[]
|
42
|
+
end
|
35
43
|
end
|
36
44
|
|
37
45
|
end; end
|
@@ -33,24 +33,29 @@ module LeapCli
|
|
33
33
|
@node = node || self
|
34
34
|
end
|
35
35
|
|
36
|
+
#
|
37
|
+
# export YAML
|
36
38
|
#
|
37
39
|
# We use pure ruby yaml exporter ya2yaml instead of SYCK or PSYCH because it
|
38
40
|
# allows us greater compatibility regardless of installed ruby version and
|
39
41
|
# greater control over how the yaml is exported (sorted keys, in particular).
|
40
42
|
#
|
41
|
-
def
|
42
|
-
evaluate
|
43
|
+
def dump_yaml
|
44
|
+
evaluate(@node)
|
43
45
|
ya2yaml(:syck_compatible => true)
|
44
46
|
end
|
45
47
|
|
48
|
+
#
|
49
|
+
# export JSON
|
50
|
+
#
|
46
51
|
def dump_json
|
47
|
-
evaluate
|
52
|
+
evaluate(@node)
|
48
53
|
JSON.sorted_generate(self)
|
49
54
|
end
|
50
55
|
|
51
|
-
def evaluate
|
52
|
-
evaluate_everything
|
53
|
-
late_evaluate_everything
|
56
|
+
def evaluate(context=@node)
|
57
|
+
evaluate_everything(context)
|
58
|
+
late_evaluate_everything(context)
|
54
59
|
end
|
55
60
|
|
56
61
|
##
|
@@ -204,13 +209,13 @@ module LeapCli
|
|
204
209
|
#
|
205
210
|
# walks the object tree, eval'ing all the attributes that are dynamic ruby (e.g. value starts with '= ')
|
206
211
|
#
|
207
|
-
def evaluate_everything
|
212
|
+
def evaluate_everything(context)
|
208
213
|
keys.each do |key|
|
209
|
-
obj = fetch_value(key)
|
214
|
+
obj = fetch_value(key, context)
|
210
215
|
if is_required_value_not_set?(obj)
|
211
216
|
Util::log 0, :warning, "required key \"#{key}\" is not set in node \"#{node.name}\"."
|
212
217
|
elsif obj.is_a? Config::Object
|
213
|
-
obj.evaluate_everything
|
218
|
+
obj.evaluate_everything(context)
|
214
219
|
end
|
215
220
|
end
|
216
221
|
end
|
@@ -218,10 +223,10 @@ module LeapCli
|
|
218
223
|
#
|
219
224
|
# some keys need to be evaluated 'late', after all the other keys have been evaluated.
|
220
225
|
#
|
221
|
-
def late_evaluate_everything
|
226
|
+
def late_evaluate_everything(context)
|
222
227
|
if @late_eval_list
|
223
228
|
@late_eval_list.each do |key, value|
|
224
|
-
self[key] =
|
229
|
+
self[key] = context.evaluate_ruby(key, value)
|
225
230
|
if is_required_value_not_set?(self[key])
|
226
231
|
Util::log 0, :warning, "required key \"#{key}\" is not set in node \"#{node.name}\"."
|
227
232
|
end
|
@@ -229,44 +234,24 @@ module LeapCli
|
|
229
234
|
end
|
230
235
|
values.each do |obj|
|
231
236
|
if obj.is_a? Config::Object
|
232
|
-
obj.late_evaluate_everything
|
237
|
+
obj.late_evaluate_everything(context)
|
233
238
|
end
|
234
239
|
end
|
235
240
|
end
|
236
241
|
|
237
|
-
private
|
238
|
-
|
239
242
|
#
|
240
|
-
#
|
243
|
+
# evaluates the string `value` as ruby in the context of self.
|
244
|
+
# (`key` is just passed for debugging purposes)
|
241
245
|
#
|
242
|
-
def
|
243
|
-
value = fetch(key, nil)
|
244
|
-
if value.is_a?(String) && value =~ /^=/
|
245
|
-
if value =~ /^=> (.*)$/
|
246
|
-
value = evaluate_later(key, $1)
|
247
|
-
elsif value =~ /^= (.*)$/
|
248
|
-
value = evaluate_now(key, $1)
|
249
|
-
end
|
250
|
-
self[key] = value
|
251
|
-
end
|
252
|
-
return value
|
253
|
-
end
|
254
|
-
|
255
|
-
def evaluate_later(key, value)
|
256
|
-
@late_eval_list ||= []
|
257
|
-
@late_eval_list << [key, value]
|
258
|
-
'<evaluate later>'
|
259
|
-
end
|
260
|
-
|
261
|
-
def evaluate_now(key, value)
|
246
|
+
def evaluate_ruby(key, value)
|
262
247
|
result = nil
|
263
248
|
if LeapCli.log_level >= 2
|
264
|
-
result =
|
249
|
+
result = self.instance_eval(value)
|
265
250
|
else
|
266
251
|
begin
|
267
|
-
result =
|
252
|
+
result = self.instance_eval(value)
|
268
253
|
rescue SystemStackError => exc
|
269
|
-
Util::log 0, :error, "while evaluating node '#{
|
254
|
+
Util::log 0, :error, "while evaluating node '#{self.name}'"
|
270
255
|
Util::log 0, "offending key: #{key}", :indent => 1
|
271
256
|
Util::log 0, "offending string: #{value}", :indent => 1
|
272
257
|
Util::log 0, "STACK OVERFLOW, BAILING OUT. There must be an eval loop of death (variables with circular dependencies).", :indent => 1
|
@@ -274,9 +259,9 @@ module LeapCli
|
|
274
259
|
rescue FileMissing => exc
|
275
260
|
Util::bail! do
|
276
261
|
if exc.options[:missing]
|
277
|
-
Util::log :missing, exc.options[:missing].gsub('$node',
|
262
|
+
Util::log :missing, exc.options[:missing].gsub('$node', self.name)
|
278
263
|
else
|
279
|
-
Util::log :error, "while evaluating node '#{
|
264
|
+
Util::log :error, "while evaluating node '#{self.name}'"
|
280
265
|
Util::log "offending key: #{key}", :indent => 1
|
281
266
|
Util::log "offending string: #{value}", :indent => 1
|
282
267
|
Util::log "error message: no file '#{exc}'", :indent => 1
|
@@ -284,13 +269,13 @@ module LeapCli
|
|
284
269
|
end
|
285
270
|
rescue AssertionFailed => exc
|
286
271
|
Util.bail! do
|
287
|
-
Util::log :failed, "assertion while evaluating node '#{
|
272
|
+
Util::log :failed, "assertion while evaluating node '#{self.name}'"
|
288
273
|
Util::log 'assertion: %s' % exc.assertion, :indent => 1
|
289
274
|
Util::log "offending key: #{key}", :indent => 1
|
290
275
|
end
|
291
276
|
rescue SyntaxError, StandardError => exc
|
292
277
|
Util::bail! do
|
293
|
-
Util::log :error, "while evaluating node '#{
|
278
|
+
Util::log :error, "while evaluating node '#{self.name}'"
|
294
279
|
Util::log "offending key: #{key}", :indent => 1
|
295
280
|
Util::log "offending string: #{value}", :indent => 1
|
296
281
|
Util::log "error message: #{exc.inspect}", :indent => 1
|
@@ -300,6 +285,30 @@ module LeapCli
|
|
300
285
|
return result
|
301
286
|
end
|
302
287
|
|
288
|
+
private
|
289
|
+
|
290
|
+
#
|
291
|
+
# fetches the value for the key, evaluating the value as ruby if it begins with '='
|
292
|
+
#
|
293
|
+
def fetch_value(key, context=@node)
|
294
|
+
value = fetch(key, nil)
|
295
|
+
if value.is_a?(String) && value =~ /^=/
|
296
|
+
if value =~ /^=> (.*)$/
|
297
|
+
value = evaluate_later(key, $1)
|
298
|
+
elsif value =~ /^= (.*)$/
|
299
|
+
value = context.evaluate_ruby(key, $1)
|
300
|
+
end
|
301
|
+
self[key] = value
|
302
|
+
end
|
303
|
+
return value
|
304
|
+
end
|
305
|
+
|
306
|
+
def evaluate_later(key, value)
|
307
|
+
@late_eval_list ||= []
|
308
|
+
@late_eval_list << [key, value]
|
309
|
+
'<evaluate later>'
|
310
|
+
end
|
311
|
+
|
303
312
|
#
|
304
313
|
# when merging, we raise an error if this method returns true for the two values.
|
305
314
|
#
|
@@ -1,9 +1,12 @@
|
|
1
|
+
require 'tsort'
|
2
|
+
|
1
3
|
module LeapCli
|
2
4
|
module Config
|
3
5
|
#
|
4
6
|
# A list of Config::Object instances (internally stored as a hash)
|
5
7
|
#
|
6
8
|
class ObjectList < Hash
|
9
|
+
include TSort
|
7
10
|
|
8
11
|
def initialize(config=nil)
|
9
12
|
if config
|
@@ -46,7 +49,9 @@ module LeapCli
|
|
46
49
|
each do |name, config|
|
47
50
|
value = config[field]
|
48
51
|
if value.is_a? Array
|
49
|
-
if value.include?(match_value)
|
52
|
+
if operator == :equal && value.include?(match_value)
|
53
|
+
results[name] = config
|
54
|
+
elsif operator == :not_equal && !value.include?(match_value)
|
50
55
|
results[name] = config
|
51
56
|
end
|
52
57
|
else
|
@@ -169,6 +174,21 @@ module LeapCli
|
|
169
174
|
end
|
170
175
|
end
|
171
176
|
|
177
|
+
#
|
178
|
+
# topographical sort based on test dependency
|
179
|
+
#
|
180
|
+
def tsort_each_node(&block)
|
181
|
+
self.each_key(&block)
|
182
|
+
end
|
183
|
+
|
184
|
+
def tsort_each_child(node_name, &block)
|
185
|
+
self[node_name].test_dependencies.each(&block)
|
186
|
+
end
|
187
|
+
|
188
|
+
def names_in_test_dependency_order
|
189
|
+
self.tsort
|
190
|
+
end
|
191
|
+
|
172
192
|
end
|
173
193
|
end
|
174
194
|
end
|
@@ -1,8 +1,6 @@
|
|
1
1
|
#
|
2
|
-
#
|
3
2
|
# A class for the secrets.json file
|
4
3
|
#
|
5
|
-
#
|
6
4
|
|
7
5
|
module LeapCli; module Config
|
8
6
|
|
@@ -14,10 +12,13 @@ module LeapCli; module Config
|
|
14
12
|
@discovered_keys = {}
|
15
13
|
end
|
16
14
|
|
17
|
-
def set(key, value)
|
15
|
+
def set(key, value, environment=nil)
|
16
|
+
environment ||= 'default'
|
18
17
|
key = key.to_s
|
19
|
-
@discovered_keys[
|
20
|
-
|
18
|
+
@discovered_keys[environment] ||= {}
|
19
|
+
@discovered_keys[environment][key] = true
|
20
|
+
self[environment] ||= {}
|
21
|
+
self[environment][key] ||= value
|
21
22
|
end
|
22
23
|
|
23
24
|
#
|
@@ -27,12 +28,16 @@ module LeapCli; module Config
|
|
27
28
|
# this should only be triggered when all nodes have been processed, otherwise
|
28
29
|
# secrets that are actually in use will get mistakenly removed.
|
29
30
|
#
|
30
|
-
#
|
31
31
|
def dump_json(only_discovered_keys=false)
|
32
32
|
if only_discovered_keys
|
33
|
-
self.each_key do |
|
34
|
-
|
35
|
-
|
33
|
+
self.each_key do |environment|
|
34
|
+
self[environment].each_key do |key|
|
35
|
+
unless @discovered_keys[environment] && @discovered_keys[environment][key]
|
36
|
+
self[environment].delete(key)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
if self[environment].empty?
|
40
|
+
self.delete(environment)
|
36
41
|
end
|
37
42
|
end
|
38
43
|
end
|
data/lib/leap_cli/log.rb
CHANGED
@@ -9,6 +9,8 @@ require 'paint'
|
|
9
9
|
module LeapCli
|
10
10
|
extend self
|
11
11
|
|
12
|
+
attr_accessor :log_in_color
|
13
|
+
|
12
14
|
# logging options
|
13
15
|
def log_level
|
14
16
|
@log_level ||= 1
|
@@ -112,8 +114,12 @@ module LeapCli
|
|
112
114
|
message = LeapCli::Path.relative_path(message)
|
113
115
|
end
|
114
116
|
|
115
|
-
log_raw(:log, nil)
|
116
|
-
|
117
|
+
log_raw(:log, nil) { [clear_prefix, message].join }
|
118
|
+
if LeapCli.log_in_color
|
119
|
+
log_raw(:stdout, options[:indent]) { [colored_prefix, message].join }
|
120
|
+
else
|
121
|
+
log_raw(:stdout, options[:indent]) { [clear_prefix, message].join }
|
122
|
+
end
|
117
123
|
|
118
124
|
# run block, if given
|
119
125
|
if block_given?
|
data/lib/leap_cli/logger.rb
CHANGED
@@ -198,7 +198,7 @@ module LeapCli
|
|
198
198
|
|
199
199
|
if color == :hide
|
200
200
|
return nil
|
201
|
-
elsif mode == :log || (color == :none && style.nil?)
|
201
|
+
elsif mode == :log || (color == :none && style.nil?) || !LeapCli.log_in_color
|
202
202
|
return [message, line_prefix, options]
|
203
203
|
else
|
204
204
|
term_color = COLORS[color]
|
data/lib/leap_cli/path.rb
CHANGED
@@ -72,6 +72,10 @@ module LeapCli; module Path
|
|
72
72
|
File.exists?(named_path(name, provider_dir))
|
73
73
|
end
|
74
74
|
|
75
|
+
def self.defined?(name)
|
76
|
+
Leap::Platform.paths[name]
|
77
|
+
end
|
78
|
+
|
75
79
|
def self.relative_path(path, provider_dir=Path.provider)
|
76
80
|
if provider_dir
|
77
81
|
path = named_path(path, provider_dir)
|
@@ -12,6 +12,30 @@ task :install_authorized_keys, :max_hosts => MAX_HOSTS do
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
+
#
|
16
|
+
# for vagrant nodes, we don't overwrite authorized_keys, because we want to keep the insecure vagrant key.
|
17
|
+
# instead we install to authorized_keys2, which is also used by sshd.
|
18
|
+
#
|
19
|
+
# why?
|
20
|
+
# without it, it might be impossible to re-initialize a node.
|
21
|
+
#
|
22
|
+
# ok, why is that?
|
23
|
+
# when we init a vagrant node, we force it to use the insecure vagrant key, and not the user's keys
|
24
|
+
# (so re-initialization would be impossible if authorized_keys doesn't include insecure key).
|
25
|
+
#
|
26
|
+
# ok, why force the insecure vagrant key in the first place?
|
27
|
+
# if we don't do this, then first time initialization might fail if the user has many keys
|
28
|
+
# (ssh will bomb out before it gets to the vagrant key).
|
29
|
+
# and it really doesn't make sense to ask users to pin the insecure vagrant key in their
|
30
|
+
# .ssh/config files.
|
31
|
+
#
|
32
|
+
task :install_authorized_keys2, :max_hosts => MAX_HOSTS do
|
33
|
+
leap.log :updating, "authorized_keys2" do
|
34
|
+
leap.mkdirs '/root/.ssh'
|
35
|
+
upload LeapCli::Path.named_path(:authorized_keys), '/root/.ssh/authorized_keys2', :mode => '600'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
15
39
|
task :install_prerequisites, :max_hosts => MAX_HOSTS do
|
16
40
|
leap.mkdirs LeapCli::PUPPET_DESTINATION
|
17
41
|
leap.log :updating, "package list" do
|
@@ -35,6 +35,12 @@ module LeapCli; module Util; module RemoteCommand
|
|
35
35
|
end
|
36
36
|
|
37
37
|
yield cap
|
38
|
+
rescue Capistrano::ConnectionError => exc
|
39
|
+
# not sure if this will work if english is not the locale??
|
40
|
+
if exc.message =~ /Too many authentication failures/
|
41
|
+
at_exit {ssh_config_help_message}
|
42
|
+
end
|
43
|
+
raise exc
|
38
44
|
end
|
39
45
|
|
40
46
|
private
|
@@ -99,6 +105,7 @@ module LeapCli; module Util; module RemoteCommand
|
|
99
105
|
opts = {}
|
100
106
|
if node.vagrant?
|
101
107
|
opts[:keys] = [vagrant_ssh_key_file]
|
108
|
+
opts[:keys_only] = true # only use the keys specified above, and ignore whatever keys the ssh-agent is aware of.
|
102
109
|
opts[:paranoid] = false # we skip host checking for vagrant nodes, because fingerprint is different for everyone.
|
103
110
|
if LeapCli::log_level <= 1
|
104
111
|
opts[:verbose] = :error # suppress all the warnings about adding host keys to known_hosts, since it is not actually doing that.
|
data/lib/leap_cli/version.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module LeapCli
|
2
2
|
unless defined?(LeapCli::VERSION)
|
3
|
-
VERSION = '1.
|
4
|
-
COMPATIBLE_PLATFORM_VERSION = '0.
|
3
|
+
VERSION = '1.5.0'
|
4
|
+
COMPATIBLE_PLATFORM_VERSION = '0.3.0'..'1.99'
|
5
5
|
SUMMARY = 'Command line interface to the LEAP platform'
|
6
6
|
DESCRIPTION = 'The command "leap" can be used to manage a bevy of servers running the LEAP platform from the comfort of your own home.'
|
7
7
|
LOAD_PATHS = ['lib', 'vendor/certificate_authority/lib', 'vendor/rsync_command/lib']
|
data/lib/leap_cli.rb
CHANGED
@@ -27,6 +27,7 @@ require 'leap_cli/ssh_key'
|
|
27
27
|
require 'leap_cli/config/object'
|
28
28
|
require 'leap_cli/config/node'
|
29
29
|
require 'leap_cli/config/tag'
|
30
|
+
require 'leap_cli/config/provider'
|
30
31
|
require 'leap_cli/config/secrets'
|
31
32
|
require 'leap_cli/config/object_list'
|
32
33
|
require 'leap_cli/config/manager'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: leap_cli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,24 +9,8 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2014-03-16 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
-
- !ruby/object:Gem::Dependency
|
15
|
-
name: rake
|
16
|
-
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
|
-
requirements:
|
19
|
-
- - ! '>='
|
20
|
-
- !ruby/object:Gem::Version
|
21
|
-
version: 10.0.3
|
22
|
-
type: :development
|
23
|
-
prerelease: false
|
24
|
-
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
|
-
requirements:
|
27
|
-
- - ! '>='
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
version: 10.0.3
|
30
14
|
- !ruby/object:Gem::Dependency
|
31
15
|
name: minitest
|
32
16
|
requirement: !ruby/object:Gem::Requirement
|
@@ -287,6 +271,7 @@ files:
|
|
287
271
|
- lib/leap_cli/config/object.rb
|
288
272
|
- lib/leap_cli/config/tag.rb
|
289
273
|
- lib/leap_cli/config/object_list.rb
|
274
|
+
- lib/leap_cli/config/provider.rb
|
290
275
|
- lib/leap_cli/config/secrets.rb
|
291
276
|
- lib/leap_cli/config/node.rb
|
292
277
|
- lib/leap_cli/config/macros.rb
|