leap_cli 1.5.6 → 1.6.2

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.
Files changed (62) hide show
  1. data/bin/leap +29 -6
  2. data/lib/leap/platform.rb +36 -1
  3. data/lib/leap_cli/commands/ca.rb +97 -20
  4. data/lib/leap_cli/commands/compile.rb +49 -8
  5. data/lib/leap_cli/commands/db.rb +13 -4
  6. data/lib/leap_cli/commands/deploy.rb +138 -29
  7. data/lib/leap_cli/commands/env.rb +76 -0
  8. data/lib/leap_cli/commands/facts.rb +10 -3
  9. data/lib/leap_cli/commands/inspect.rb +2 -2
  10. data/lib/leap_cli/commands/list.rb +10 -10
  11. data/lib/leap_cli/commands/node.rb +7 -132
  12. data/lib/leap_cli/commands/node_init.rb +169 -0
  13. data/lib/leap_cli/commands/pre.rb +4 -27
  14. data/lib/leap_cli/commands/ssh.rb +152 -0
  15. data/lib/leap_cli/commands/test.rb +22 -13
  16. data/lib/leap_cli/commands/user.rb +12 -4
  17. data/lib/leap_cli/commands/vagrant.rb +4 -4
  18. data/lib/leap_cli/config/filter.rb +175 -0
  19. data/lib/leap_cli/config/manager.rb +130 -61
  20. data/lib/leap_cli/config/node.rb +32 -0
  21. data/lib/leap_cli/config/object.rb +69 -44
  22. data/lib/leap_cli/config/object_list.rb +44 -39
  23. data/lib/leap_cli/config/secrets.rb +24 -12
  24. data/lib/leap_cli/config/tag.rb +7 -0
  25. data/lib/{core_ext → leap_cli/core_ext}/boolean.rb +0 -0
  26. data/lib/{core_ext → leap_cli/core_ext}/hash.rb +0 -0
  27. data/lib/{core_ext → leap_cli/core_ext}/json.rb +0 -0
  28. data/lib/{core_ext → leap_cli/core_ext}/nil.rb +0 -0
  29. data/lib/{core_ext → leap_cli/core_ext}/string.rb +0 -0
  30. data/lib/leap_cli/core_ext/yaml.rb +29 -0
  31. data/lib/leap_cli/exceptions.rb +24 -0
  32. data/lib/leap_cli/leapfile.rb +60 -10
  33. data/lib/{lib_ext → leap_cli/lib_ext}/capistrano_connections.rb +0 -0
  34. data/lib/{lib_ext → leap_cli/lib_ext}/gli.rb +0 -0
  35. data/lib/leap_cli/log.rb +1 -1
  36. data/lib/leap_cli/logger.rb +18 -1
  37. data/lib/leap_cli/markdown_document_listener.rb +1 -1
  38. data/lib/leap_cli/override/json.rb +11 -0
  39. data/lib/leap_cli/path.rb +20 -6
  40. data/lib/leap_cli/remote/leap_plugin.rb +2 -2
  41. data/lib/leap_cli/remote/puppet_plugin.rb +1 -1
  42. data/lib/leap_cli/remote/rsync_plugin.rb +1 -1
  43. data/lib/leap_cli/remote/tasks.rb +1 -1
  44. data/lib/leap_cli/ssh_key.rb +63 -1
  45. data/lib/leap_cli/util/remote_command.rb +19 -2
  46. data/lib/leap_cli/util/secret.rb +1 -1
  47. data/lib/leap_cli/util/x509.rb +3 -2
  48. data/lib/leap_cli/util.rb +11 -3
  49. data/lib/leap_cli/version.rb +2 -2
  50. data/lib/leap_cli.rb +24 -14
  51. data/vendor/certificate_authority/lib/certificate_authority/certificate.rb +85 -29
  52. data/vendor/certificate_authority/lib/certificate_authority/distinguished_name.rb +5 -0
  53. data/vendor/certificate_authority/lib/certificate_authority/extensions.rb +406 -41
  54. data/vendor/certificate_authority/lib/certificate_authority/key_material.rb +0 -34
  55. data/vendor/certificate_authority/lib/certificate_authority/serial_number.rb +6 -0
  56. data/vendor/certificate_authority/lib/certificate_authority/signing_request.rb +36 -1
  57. metadata +25 -24
  58. data/lib/leap_cli/commands/shell.rb +0 -89
  59. data/lib/leap_cli/config/macros.rb +0 -430
  60. data/lib/leap_cli/constants.rb +0 -7
  61. data/lib/leap_cli/requirements.rb +0 -19
  62. data/lib/lib_ext/markdown_document_listener.rb +0 -122
@@ -5,7 +5,7 @@ module LeapCli
5
5
  desc 'Apply recipes to a node or set of nodes.'
6
6
  long_desc 'The FILTER can be the name of a node, service, or tag.'
7
7
  arg_name 'FILTER'
8
- command :deploy do |c|
8
+ command [:deploy, :d] do |c|
9
9
 
10
10
  # --fast
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.',
@@ -17,9 +17,12 @@ module LeapCli
17
17
  # --force
18
18
  c.switch :force, :desc => 'Deploy even if there is a lockfile.', :negatable => false
19
19
 
20
+ # --dev
21
+ c.switch :dev, :desc => "Development mode: don't run 'git submodule update' before deploy.", :negatable => false
22
+
20
23
  # --tags
21
24
  c.flag :tags, :desc => 'Specify tags to pass through to puppet (overriding the default).',
22
- :default_value => DEFAULT_TAGS.join(','), :arg_name => 'TAG[,TAG]'
25
+ :arg_name => 'TAG[,TAG]'
23
26
 
24
27
  c.flag :port, :desc => 'Override the default SSH port.',
25
28
  :arg_name => 'PORT'
@@ -28,9 +31,12 @@ module LeapCli
28
31
  :arg_name => 'IPADDRESS'
29
32
 
30
33
  c.action do |global,options,args|
31
- init_submodules
32
34
 
33
- nodes = filter_deploy_nodes(args)
35
+ if options[:dev] != true
36
+ init_submodules
37
+ end
38
+
39
+ nodes = manager.filter!(args)
34
40
  if nodes.size > 1
35
41
  say "Deploying to these nodes: #{nodes.keys.join(', ')}"
36
42
  if !global[:yes] && !agree("Continue? ")
@@ -38,7 +44,16 @@ module LeapCli
38
44
  end
39
45
  end
40
46
 
41
- compile_hiera_files
47
+ environments = nodes.field('environment').uniq
48
+ if environments.empty?
49
+ environments = [nil]
50
+ end
51
+ environments.each do |env|
52
+ check_platform_pinning(env)
53
+ end
54
+ # compile hiera files for all the nodes in every environment that is
55
+ # being deployed and only those environments.
56
+ compile_hiera_files(manager.filter(environments))
42
57
 
43
58
  ssh_connect(nodes, connect_options(options)) do |ssh|
44
59
  ssh.leap.log :checking, 'node' do
@@ -58,39 +73,130 @@ module LeapCli
58
73
  end
59
74
  end
60
75
  end
76
+ if !Util.exit_status.nil? && Util.exit_status != 0
77
+ log :warning, "puppet did not finish successfully."
78
+ end
61
79
  end
62
80
  end
63
81
 
64
82
  private
65
83
 
84
+ #
85
+ # The currently activated provider.json could have loaded some pinning
86
+ # information for the platform. If this is the case, refuse to deploy
87
+ # if there is a mismatch.
88
+ #
89
+ # For example:
90
+ #
91
+ # "platform": {
92
+ # "branch": "develop"
93
+ # "version": "1.0..99"
94
+ # "commit": "e1d6280e0a8c565b7fb1a4ed3969ea6fea31a5e2..HEAD"
95
+ # }
96
+ #
97
+ def check_platform_pinning(environment)
98
+ provider = manager.env(environment).provider
99
+ return unless provider['platform']
100
+
101
+ if environment.nil? || environment == 'default'
102
+ provider_json = 'provider.json'
103
+ else
104
+ provider_json = 'provider.' + environment + '.json'
105
+ end
106
+
107
+ # can we have json schema verification already?
108
+ unless provider.platform.is_a? Hash
109
+ bail!('`platform` attribute in #{provider_json} must be a hash (was %s).' % provider.platform.inspect)
110
+ end
111
+
112
+ # check version
113
+ if provider.platform['version']
114
+ if !Leap::Platform.version_in_range?(provider.platform.version)
115
+ say("The platform is pinned to a version range of '#{provider.platform.version}' "+
116
+ "by the `platform.version` property in #{provider_json}, but the platform "+
117
+ "(#{Path.platform}) has version #{Leap::Platform.version}.")
118
+ quit!("OK. Bye.") unless agree("Do you really want to deploy from the wrong version? ")
119
+ end
120
+ end
121
+
122
+ # check branch
123
+ if provider.platform['branch']
124
+ if !is_git_directory?(Path.platform)
125
+ say("The platform is pinned to a particular branch by the `platform.branch` property "+
126
+ "in #{provider_json}, but the platform directory (#{Path.platform}) is not a git repository.")
127
+ quit!("OK. Bye.") unless agree("Do you really want to deploy anyway? ")
128
+ end
129
+ unless provider.platform.branch == current_git_branch(Path.platform)
130
+ say("The platform is pinned to branch '#{provider.platform.branch}' by the `platform.branch` property "+
131
+ "in #{provider_json}, but the current branch is '#{current_git_branch(Path.platform)}' " +
132
+ "(for directory '#{Path.platform}')")
133
+ quit!("OK. Bye.") unless agree("Do you really want to deploy from the wrong branch? ")
134
+ end
135
+ end
136
+
137
+ # check commit
138
+ if provider.platform['commit']
139
+ if !is_git_directory?(Path.platform)
140
+ say("The platform is pinned to a particular commit range by the `platform.commit` property "+
141
+ "in #{provider_json}, but the platform directory (#{Path.platform}) is not a git repository.")
142
+ quit!("OK. Bye.") unless agree("Do you really want to deploy anyway? ")
143
+ end
144
+ current_commit = current_git_commit(Path.platform)
145
+ Dir.chdir(Path.platform) do
146
+ commit_range = assert_run!("git log --pretty='format:%H' '#{provider.platform.commit}'",
147
+ "The platform is pinned to a particular commit range by the `platform.commit` property "+
148
+ "in #{provider_json}, but git was not able to find commits in the range specified "+
149
+ "(#{provider.platform.commit}).")
150
+ commit_range = commit_range.split("\n")
151
+ if !commit_range.include?(current_commit) &&
152
+ provider.platform.commit.split('..').first != current_commit
153
+ say("The platform is pinned via the `platform.commit` property in #{provider_json} " +
154
+ "to a commit in the range #{provider.platform.commit}, but the current HEAD " +
155
+ "(#{current_commit}) is not in that range.")
156
+ quit!("OK. Bye.") unless agree("Do you really want to deploy from the wrong commit? ")
157
+ end
158
+ end
159
+ end
160
+ end
161
+
66
162
  def sync_hiera_config(ssh)
67
- dest_dir = provider.hiera_sync_destination
68
163
  ssh.rsync.update do |server|
69
164
  node = manager.node(server.host)
70
165
  hiera_file = Path.relative_path([:hiera, node.name])
71
- ssh.leap.log hiera_file + ' -> ' + node.name + ':' + dest_dir + '/hiera.yaml'
166
+ ssh.leap.log hiera_file + ' -> ' + node.name + ':' + Leap::Platform.hiera_path
72
167
  {
73
168
  :source => hiera_file,
74
- :dest => dest_dir + '/hiera.yaml',
169
+ :dest => Leap::Platform.hiera_path,
75
170
  :flags => "-rltp --chmod=u+rX,go-rwx"
76
171
  }
77
172
  end
78
173
  end
79
174
 
175
+ #
176
+ # sync various support files.
177
+ #
80
178
  def sync_support_files(ssh)
81
- dest_dir = provider.hiera_sync_destination
179
+ dest_dir = Leap::Platform.files_dir
180
+ source_files = []
181
+ if Path.defined?(:custom_puppet_dir) && file_exists?(:custom_puppet_dir)
182
+ source_files += [:custom_puppet_dir, :custom_puppet_modules_dir, :custom_puppet_manifests_dir].collect{|path|
183
+ Path.relative_path(path, Path.provider) + '/' # rsync needs trailing slash
184
+ }
185
+ ensure_dir :custom_puppet_modules_dir
186
+ end
82
187
  ssh.rsync.update do |server|
83
188
  node = manager.node(server.host)
84
189
  files_to_sync = node.file_paths.collect {|path| Path.relative_path(path, Path.provider) }
190
+ files_to_sync += source_files
85
191
  if files_to_sync.any?
86
192
  ssh.leap.log(files_to_sync.join(', ') + ' -> ' + node.name + ':' + dest_dir)
87
193
  {
88
- :chdir => Path.provider,
194
+ :chdir => Path.named_path(:files_dir),
89
195
  :source => ".",
90
196
  :dest => dest_dir,
91
197
  :excludes => "*",
92
- :includes => calculate_includes_from_files(files_to_sync),
93
- :flags => "-rltp --chmod=u+rX,go-rwx --relative --delete --delete-excluded --filter='protect hiera.yaml' --copy-links"
198
+ :includes => calculate_includes_from_files(files_to_sync, '/files'),
199
+ :flags => "-rltp --chmod=u+rX,go-rwx --relative --delete --delete-excluded --copy-links"
94
200
  }
95
201
  else
96
202
  nil
@@ -100,9 +206,9 @@ module LeapCli
100
206
 
101
207
  def sync_puppet_files(ssh)
102
208
  ssh.rsync.update do |server|
103
- ssh.leap.log(Path.platform + '/[bin,tests,puppet] -> ' + server.host + ':' + LeapCli::PUPPET_DESTINATION)
209
+ ssh.leap.log(Path.platform + '/[bin,tests,puppet] -> ' + server.host + ':' + Leap::Platform.leap_dir)
104
210
  {
105
- :dest => LeapCli::PUPPET_DESTINATION,
211
+ :dest => Leap::Platform.leap_dir,
106
212
  :source => '.',
107
213
  :chdir => Path.platform,
108
214
  :excludes => '*',
@@ -112,7 +218,12 @@ module LeapCli
112
218
  end
113
219
  end
114
220
 
221
+ #
222
+ # ensure submodules are up to date, if the platform is a git
223
+ # repository.
224
+ #
115
225
  def init_submodules
226
+ return unless is_git_directory?(Path.platform)
116
227
  Dir.chdir Path.platform do
117
228
  assert_run! "git submodule sync"
118
229
  statuses = assert_run! "git submodule status"
@@ -126,11 +237,17 @@ module LeapCli
126
237
  end
127
238
  end
128
239
 
129
- def calculate_includes_from_files(files)
240
+ #
241
+ # converts an array of file paths into an array
242
+ # suitable for --include of rsync
243
+ #
244
+ # if set, `prefix` is stripped off.
245
+ #
246
+ def calculate_includes_from_files(files, prefix=nil)
130
247
  return nil unless files and files.any?
131
248
 
132
249
  # prepend '/' (kind of like ^ for rsync)
133
- includes = files.collect {|file| '/' + file}
250
+ includes = files.collect {|file| file =~ /^\// ? file : '/' + file }
134
251
 
135
252
  # include all sub files of specified directories
136
253
  includes.size.times do |i|
@@ -148,6 +265,10 @@ module LeapCli
148
265
  end
149
266
  end
150
267
 
268
+ if prefix
269
+ includes.map! {|path| path.sub(/^#{Regexp.escape(prefix)}\//, '/')}
270
+ end
271
+
151
272
  return includes
152
273
  end
153
274
 
@@ -155,23 +276,11 @@ module LeapCli
155
276
  if options[:tags]
156
277
  tags = options[:tags].split(',')
157
278
  else
158
- tags = LeapCli::DEFAULT_TAGS.dup
279
+ tags = Leap::Platform.default_puppet_tags.dup
159
280
  end
160
281
  tags << 'leap_slow' unless options[:fast]
161
282
  tags.join(',')
162
283
  end
163
284
 
164
- #
165
- # for safety, we allow production deploys to be turned off in the Leapfile.
166
- #
167
- def filter_deploy_nodes(filter)
168
- nodes = manager.filter!(filter)
169
- if !leapfile.allow_production_deploy
170
- nodes = nodes[:environment => "!production"]
171
- assert! nodes.any?, "Skipping deploy because @allow_production_deploy is disabled."
172
- end
173
- nodes
174
- end
175
-
176
285
  end
177
286
  end
@@ -0,0 +1,76 @@
1
+ module LeapCli
2
+ module Commands
3
+
4
+ desc "Manipulate and query environment information."
5
+ long_desc "The 'environment' node property can be used to isolate sets of nodes into entirely separate environments. "+
6
+ "A node in one environment will never interact with a node from another environment. "+
7
+ "Environment pinning works by modifying your ~/.leaprc file and is dependent on the "+
8
+ "absolute file path of your provider directory (pins don't apply if you move the directory)"
9
+ command [:env, :e] do |c|
10
+ c.desc "List the available environments. The pinned environment, if any, will be marked with '*'. Will also set the pin if run with an environment argument."
11
+ c.arg_name 'ENVIRONMENT', :optional => true
12
+ c.command :ls do |ls|
13
+ ls.action do |global_options, options, args|
14
+ environment = get_env_from_args(args)
15
+ if environment
16
+ pin(environment)
17
+ LeapCli.leapfile.load
18
+ end
19
+ print_envs
20
+ end
21
+ end
22
+
23
+ c.desc 'Pin the environment to ENVIRONMENT. All subsequent commands will only apply to nodes in this environment.'
24
+ c.arg_name 'ENVIRONMENT'
25
+ c.command :pin do |pin|
26
+ pin.action do |global_options,options,args|
27
+ environment = get_env_from_args(args)
28
+ if environment
29
+ pin(environment)
30
+ else
31
+ bail! "There is no environment `#{environment}`"
32
+ end
33
+ end
34
+ end
35
+
36
+ c.desc "Unpin the environment. All subsequent commands will apply to all nodes."
37
+ c.command :unpin do |unpin|
38
+ unpin.action do |global_options, options, args|
39
+ LeapCli.leapfile.unset('environment')
40
+ log 0, :saved, "~/.leaprc, removing environment property."
41
+ end
42
+ end
43
+
44
+ c.default_command :ls
45
+ end
46
+
47
+ protected
48
+
49
+ def get_env_from_args(args)
50
+ environment = args.first
51
+ if environment == 'default' || (environment && manager.environment_names.include?(environment))
52
+ return environment
53
+ else
54
+ return nil
55
+ end
56
+ end
57
+
58
+ def pin(environment)
59
+ LeapCli.leapfile.set('environment', environment)
60
+ log 0, :saved, "~/.leaprc with environment set to #{environment}."
61
+ end
62
+
63
+ def print_envs
64
+ envs = ["default"] + manager.environment_names.compact.sort
65
+ envs.each do |env|
66
+ if env
67
+ if LeapCli.leapfile.environment == env
68
+ puts "* #{env}"
69
+ else
70
+ puts " #{env}"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -79,14 +79,21 @@ module LeapCli; module Commands
79
79
  private
80
80
 
81
81
  def update_facts(global_options, options, args)
82
- nodes = manager.filter(args)
82
+ nodes = manager.filter(args, :local => false)
83
83
  new_facts = {}
84
84
  ssh_connect(nodes) do |ssh|
85
85
  ssh.leap.run_with_progress(facter_cmd) do |response|
86
- new_facts[response[:host]] = response[:data].strip
86
+ node = manager.node(response[:host])
87
+ if node
88
+ new_facts[node.name] = response[:data].strip
89
+ else
90
+ log :warning, 'Could not find node for hostname %s' % response[:host]
91
+ end
87
92
  end
88
93
  end
89
- overwrite_existing = args.empty?
94
+ # only overwrite the entire facts file if and only if we are gathering facts
95
+ # for all nodes in all environments.
96
+ overwrite_existing = args.empty? && LeapCli.leapfile.environment.nil?
90
97
  update_facts_file(new_facts, overwrite_existing)
91
98
  end
92
99
 
@@ -2,7 +2,7 @@ module LeapCli; module Commands
2
2
 
3
3
  desc 'Prints details about a file. Alternately, the argument FILE can be the name of a node, service or tag.'
4
4
  arg_name 'FILE'
5
- command :inspect do |c|
5
+ command [:inspect, :i] do |c|
6
6
  c.switch 'base', :desc => 'Inspect the FILE from the provider_base (i.e. without local inheritance).', :negatable => false
7
7
  c.action do |global_options,options,args|
8
8
  object = args.first
@@ -109,7 +109,7 @@ module LeapCli; module Commands
109
109
  if options[:base]
110
110
  inspect_json manager.base_provider
111
111
  elsif arg =~ /provider\.(.*)\.json/
112
- inspect_json manager.providers[$1]
112
+ inspect_json manager.env($1).provider
113
113
  else
114
114
  inspect_json manager.provider
115
115
  end
@@ -11,28 +11,29 @@ module LeapCli; module Commands
11
11
  "`leap list openvpn +local` matches all nodes with service \"openvpn\" AND tag \"local\""
12
12
 
13
13
  arg_name 'FILTER', :optional => true
14
- command :list do |c|
14
+ command [:list,:ls] do |c|
15
15
  c.flag 'print', :desc => 'What attributes to print (optional)'
16
16
  c.switch 'disabled', :desc => 'Include disabled nodes in the list.', :negatable => false
17
17
  c.action do |global_options,options,args|
18
+ # don't rely on default manager(), because we want to pass custom options to load()
19
+ manager = LeapCli::Config::Manager.new
18
20
  if global_options[:color]
19
21
  colors = ['cyan', 'white']
20
22
  else
21
23
  colors = [nil, nil]
22
24
  end
23
25
  puts
24
- if options['disabled']
25
- manager.load(:include_disabled => true) # reload, with disabled nodes
26
- end
26
+ manager.load(:include_disabled => options['disabled'], :continue_on_error => true)
27
27
  if options['print']
28
28
  print_node_properties(manager.filter(args), options['print'])
29
29
  else
30
30
  if args.any?
31
31
  NodeTable.new(manager.filter(args), colors).run
32
32
  else
33
- TagTable.new('SERVICES', manager.services, colors).run
34
- TagTable.new('TAGS', manager.tags, colors).run
35
- NodeTable.new(manager.nodes, colors).run
33
+ environment = LeapCli.leapfile.environment || '_all_'
34
+ TagTable.new('SERVICES', manager.env(environment).services, colors).run
35
+ TagTable.new('TAGS', manager.env(environment).tags, colors).run
36
+ NodeTable.new(manager.filter(), colors).run
36
37
  end
37
38
  end
38
39
  end
@@ -41,11 +42,9 @@ module LeapCli; module Commands
41
42
  private
42
43
 
43
44
  def self.print_node_properties(nodes, properties)
44
- node_list = manager.nodes
45
45
  properties = properties.split(',')
46
46
  max_width = nodes.keys.inject(0) {|max,i| [i.size,max].max}
47
47
  nodes.each_node do |node|
48
- node.evaluate
49
48
  value = properties.collect{|prop|
50
49
  if node[prop].nil?
51
50
  "null"
@@ -68,7 +67,7 @@ module LeapCli; module Commands
68
67
  @colors = colors
69
68
  end
70
69
  def run
71
- tags = @tag_list.keys.sort
70
+ tags = @tag_list.keys.select{|tag| tag !~ /^_/}.sort # sorted list of tags, excluding _partials
72
71
  max_width = [20, (tags+[@heading]).inject(0) {|max,i| [i.size,max].max}].max
73
72
  table :border => false do
74
73
  row :color => @colors[0] do
@@ -76,6 +75,7 @@ module LeapCli; module Commands
76
75
  column "NODES", :width => HighLine::SystemExtensions.terminal_size.first - max_width - 2, :padding => 2
77
76
  end
78
77
  tags.each do |tag|
78
+ next if @tag_list[tag].node_list.empty?
79
79
  row :color => @colors[1] do
80
80
  column tag
81
81
  column @tag_list[tag].node_list.keys.sort.join(', ')
@@ -1,6 +1,9 @@
1
- require 'net/ssh/known_hosts'
2
- require 'tempfile'
3
- require 'ipaddr'
1
+ #
2
+ # fyi: the `node init` command lives in node_init.rb,
3
+ # but all other `node x` commands live here.
4
+ #
5
+
6
+ autoload :IPAddr, 'ipaddr'
4
7
 
5
8
  module LeapCli; module Commands
6
9
 
@@ -9,7 +12,7 @@ module LeapCli; module Commands
9
12
  ##
10
13
 
11
14
  desc 'Node management'
12
- command :node do |node|
15
+ command [:node, :n] do |node|
13
16
  node.desc 'Create a new configuration file for a node named NAME.'
14
17
  node.long_desc ["If specified, the optional argument SEED can be used to seed values in the node configuration file.",
15
18
  "The format is property_name:value.",
@@ -44,45 +47,6 @@ module LeapCli; module Commands
44
47
  end
45
48
  end
46
49
 
47
- node.desc 'Bootstraps a node or nodes, setting up SSH keys and installing prerequisite packages'
48
- node.long_desc "This command prepares a server to be used with the LEAP Platform by saving the server's SSH host key, " +
49
- "copying the authorized_keys file, installing packages that are required for deploying, and registering important facts. " +
50
- "Node init must be run before deploying to a server, and the server must be running and available via the network. " +
51
- "This command only needs to be run once, but there is no harm in running it multiple times."
52
- node.arg_name 'FILTER' #, :optional => false, :multiple => false
53
- node.command :init do |init|
54
- init.switch 'echo', :desc => 'If set, passwords are visible as you type them (default is hidden)', :negatable => false
55
- init.flag :port, :desc => 'Override the default SSH port.', :arg_name => 'PORT'
56
- init.flag :ip, :desc => 'Override the default SSH IP address.', :arg_name => 'IPADDRESS'
57
-
58
- init.action do |global,options,args|
59
- assert! args.any?, 'You must specify a FILTER'
60
- finished = []
61
- manager.filter!(args).each_node do |node|
62
- is_node_alive(node, options)
63
- save_public_host_key(node, global, options) unless node.vagrant?
64
- update_compiled_ssh_configs
65
- ssh_connect_options = connect_options(options).merge({:bootstrap => true, :echo => options[:echo]})
66
- ssh_connect(node, ssh_connect_options) do |ssh|
67
- if node.vagrant?
68
- ssh.install_insecure_vagrant_key
69
- end
70
- ssh.install_authorized_keys
71
- ssh.install_prerequisites
72
- ssh.leap.capture(facter_cmd) do |response|
73
- if response[:exitcode] == 0
74
- update_node_facts(node.name, response[:data])
75
- else
76
- log :failed, "to run facter on #{node.name}"
77
- end
78
- end
79
- end
80
- finished << node.name
81
- end
82
- log :completed, "initialization of nodes #{finished.join(', ')}"
83
- end
84
- end
85
-
86
50
  node.desc 'Renames a node file, and all its related files.'
87
51
  node.arg_name 'OLD_NAME NEW_NAME'
88
52
  node.command :mv do |mv|
@@ -117,30 +81,6 @@ module LeapCli; module Commands
117
81
  ## PUBLIC HELPERS
118
82
  ##
119
83
 
120
- #
121
- # generates the known_hosts file.
122
- #
123
- # we do a 'late' binding on the hostnames and ip part of the ssh pub key record in order to allow
124
- # for the possibility that the hostnames or ip has changed in the node configuration.
125
- #
126
- def update_known_hosts
127
- buffer = StringIO.new
128
- buffer << "#\n"
129
- buffer << "# This file is automatically generated by the command `leap`. You should NOT modify this file.\n"
130
- buffer << "# Instead, rerun `leap node init` on whatever node is causing SSH problems.\n"
131
- buffer << "#\n"
132
- manager.nodes.keys.sort.each do |node_name|
133
- node = manager.nodes[node_name]
134
- hostnames = [node.name, node.domain.internal, node.domain.full, node.ip_address].join(',')
135
- pub_key = read_file([:node_ssh_pub_key,node.name])
136
- if pub_key
137
- buffer << [hostnames, pub_key].join(' ')
138
- buffer << "\n"
139
- end
140
- end
141
- write_file!(:known_hosts, buffer.string)
142
- end
143
-
144
84
  def get_node_from_args(args, options={})
145
85
  node_name = args.first
146
86
  node = manager.node(node_name)
@@ -151,71 +91,6 @@ module LeapCli; module Commands
151
91
  node
152
92
  end
153
93
 
154
- private
155
-
156
- ##
157
- ## PRIVATE HELPERS
158
- ##
159
-
160
- #
161
- # saves the public ssh host key for node into the provider directory.
162
- #
163
- # see `man sshd` for the format of known_hosts
164
- #
165
- def save_public_host_key(node, global, options)
166
- log :fetching, "public SSH host key for #{node.name}"
167
- address = options[:ip] || node.ip_address
168
- port = options[:port] || node.ssh.port
169
- public_key = get_public_key_for_ip(address, port)
170
- pub_key_path = Path.named_path([:node_ssh_pub_key, node.name])
171
- if Path.exists?(pub_key_path)
172
- if public_key == SshKey.load(pub_key_path)
173
- log :trusted, "- Public SSH host key for #{node.name} matches previously saved key", :indent => 1
174
- else
175
- bail! do
176
- log :error, "The public SSH host key we just fetched for #{node.name} doesn't match what we have saved previously.", :indent => 1
177
- log "Remove the file #{pub_key_path} if you really want to change it.", :indent => 2
178
- end
179
- end
180
- elsif public_key.in_known_hosts?(node.name, node.ip_address, node.domain.name)
181
- log :trusted, "- Public SSH host key for #{node.name} is trusted (key found in your ~/.ssh/known_hosts)"
182
- else
183
- puts
184
- say("This is the SSH host key you got back from node \"#{node.name}\"")
185
- say("Type -- #{public_key.bits} bit #{public_key.type.upcase}")
186
- say("Fingerprint -- " + public_key.fingerprint)
187
- say("Public Key -- " + public_key.key)
188
- if !global[:yes] && !agree("Is this correct? ")
189
- bail!
190
- else
191
- puts
192
- write_file! [:node_ssh_pub_key, node.name], public_key.to_s
193
- end
194
- end
195
- end
196
-
197
- def get_public_key_for_ip(address, port=22)
198
- assert_bin!('ssh-keyscan')
199
- output = assert_run! "ssh-keyscan -p #{port} -t ecdsa #{address}", "Could not get the public host key from #{address}:#{port}. Maybe sshd is not running?"
200
- line = output.split("\n").grep(/^[^#]/).first
201
- if line =~ /No route to host/
202
- bail! :failed, 'ssh-keyscan: no route to %s' % address
203
- elsif line =~ /no hostkey alg/
204
- bail! :failed, 'ssh-keyscan: no hostkey alg (must be missing an ecdsa public host key)'
205
- end
206
- assert! line, "Got zero host keys back!"
207
- ip, key_type, public_key = line.split(' ')
208
- return SshKey.load(public_key, key_type)
209
- end
210
-
211
- def is_node_alive(node, options)
212
- address = options[:ip] || node.ip_address
213
- port = options[:port] || node.ssh.port
214
- log :connecting, "to node #{node.name}"
215
- assert_run! "nc -zw3 #{address} #{port}",
216
- "Failed to reach #{node.name} (address #{address}, port #{port}). You can override the configured IP address and port with --ip or --port."
217
- end
218
-
219
94
  def seed_node_data(node, args)
220
95
  args.each do |seed|
221
96
  key, value = seed.split(':')