leap_cli 1.5.6 → 1.6.2

Sign up to get free protection for your applications and to get access to all the features.
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(':')