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 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