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 CHANGED
@@ -1,4 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
+
3
+ if ARGV.include?('--debug')
4
+ require 'debugger'
5
+ end
6
+
2
7
  begin
3
8
  require 'leap_cli'
4
9
  rescue LoadError
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 :puppet_destination
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
- ssh.leap.log :applying, "puppet" do
53
- ssh.puppet.apply(:verbosity => LeapCli.log_level, :tags => tags(options), :force => options[:force])
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
- puts JSON.sorted_generate(config)
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
- ssh.install_authorized_keys
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.ip_address}'",
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.name}"
69
+ command = "#{ssh} #{node.domain.full}"
60
70
  elsif cmd == :mosh
61
- command = "MOSH_TITLE_NOPREFIX=1 mosh --ssh \"#{ssh}\" #{node.name}"
71
+ command = "MOSH_TITLE_NOPREFIX=1 mosh --ssh \"#{ssh}\" #{node.domain.full}"
62
72
  end
63
73
  log 2, command
64
- exec "#{command}"
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
- manager.filter!(args).each_node do |node|
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
- "#{PUPPET_DESTINATION}/bin/run_tests"
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? && !options[:self]
28
- help! "Either 'username' or --self is required."
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 = "http://download.leap.se/leap-debian.box"]
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 = "http://download.leap.se/leap-debian.box"]
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, but only including the IPs of the
161
- # other nodes we have encountered. Also, for virtual machines, use the local address if this
162
- # @node is in the same location.
163
- #
164
- def hosts_file
165
- if @referenced_nodes && @referenced_nodes.any?
166
- hosts = {}
167
- my_location = @node['location'] ? @node['location']['name'] : nil
168
- @referenced_nodes.each_node do |node|
169
- next if node.name == @node.name
170
- hosts[node.name] = {'ip_address' => node.ip_address, 'domain_internal' => node.domain.internal, 'domain_full' => node.domain.full}
171
- node_location = node['location'] ? node['location']['name'] : nil
172
- if my_location == node_location
173
- if facts = @node.manager.facts[node.name]
174
- if facts['ec2_public_ipv4']
175
- hosts[node.name]['ip_address'] = facts['ec2_public_ipv4']
176
- end
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
- #hosts = @referenced_nodes.pick_fields("ip_address", "domain.internal", "domain.full")
181
- return hosts
182
- else
183
- return nil
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
- # creates a hash from the ssh key info in users directory, for use in updating authorized_keys file
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, '*'])).sort.each do |keyfile|
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
- def known_hosts_file
334
- return nil unless @referenced_nodes
335
- entries = []
336
- @referenced_nodes.each_node do |node|
337
- hostnames = [node.name, node.domain.internal, node.domain.full, node.ip_address].join(',')
338
- pub_key = Util::read_file([:node_ssh_pub_key,node.name])
339
- if pub_key
340
- entries << [hostnames, pub_key].join(' ')
341
- end
342
- end
343
- entries.join("\n")
344
- end
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::Object)
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::Object)
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
- # validate
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.dump)
121
+ Util::write_file!(hierapath, node.dump_yaml)
104
122
  updated_files << filepath
105
123
  updated_hiera << hierapath
106
124
  end
@@ -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 dump
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] = evaluate_now(key, value)
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
- # fetches the value for the key, evaluating the value as ruby if it begins with '='
243
+ # evaluates the string `value` as ruby in the context of self.
244
+ # (`key` is just passed for debugging purposes)
241
245
  #
242
- def fetch_value(key)
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 = @node.instance_eval(value)
249
+ result = self.instance_eval(value)
265
250
  else
266
251
  begin
267
- result = @node.instance_eval(value)
252
+ result = self.instance_eval(value)
268
253
  rescue SystemStackError => exc
269
- Util::log 0, :error, "while evaluating node '#{@node.name}'"
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', @node.name)
262
+ Util::log :missing, exc.options[:missing].gsub('$node', self.name)
278
263
  else
279
- Util::log :error, "while evaluating node '#{@node.name}'"
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 '#{@node.name}'"
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 '#{@node.name}'"
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
@@ -0,0 +1,11 @@
1
+ #
2
+ # Configuration class for provider.json
3
+ #
4
+
5
+ module LeapCli; module Config
6
+ class Provider < Object
7
+ def provider
8
+ self
9
+ end
10
+ end
11
+ end; 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[key] = true
20
- self[key] ||= value
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 |key|
34
- unless @discovered_keys[key]
35
- self.delete(key)
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) { [clear_prefix, message].join }
116
- log_raw(:stdout, options[:indent]) { [colored_prefix, message].join }
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?
@@ -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.
@@ -1,7 +1,7 @@
1
1
  module LeapCli
2
2
  unless defined?(LeapCli::VERSION)
3
- VERSION = '1.2.5'
4
- COMPATIBLE_PLATFORM_VERSION = '0.2.4'..'1.99'
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.2.5
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: 2013-12-06 00:00:00.000000000 Z
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