leap_cli 1.2.5 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|