nucleon 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -74,7 +74,7 @@ class Action < Base
74
74
 
75
75
  #---
76
76
 
77
- def normalize
77
+ def normalize(reload)
78
78
  args = array(delete(:args, []))
79
79
 
80
80
  @action_interface = Util::Liquid.new do |method, method_args|
@@ -101,7 +101,7 @@ class Action < Base
101
101
  set(:settings, Config.new)
102
102
  configure
103
103
  parse_base(args)
104
- end
104
+ end
105
105
  end
106
106
 
107
107
  #-----------------------------------------------------------------------------
@@ -303,11 +303,13 @@ class Action < Base
303
303
  # Validate all of the configurations
304
304
  success = true
305
305
  config.export.each do |name, option|
306
- success = false unless option.validate(settings[name], *args)
306
+ unless ignore.include?(name)
307
+ success = false unless option.validate(settings[name], *args)
308
+ end
307
309
  end
308
310
  if success
309
311
  # Check for missing arguments (in case of internal execution mode)
310
- arguments.each do |name|
312
+ arguments.each do |name|
311
313
  if settings[name.to_sym].nil?
312
314
  warn('nucleon.core.exec.errors.missing_argument', { :name => name })
313
315
  success = false
@@ -6,7 +6,7 @@ class Base < Core
6
6
  # All Plugin classes should directly or indirectly extend Base
7
7
 
8
8
  def initialize(type, provider, options)
9
- config = Util::Data.clean(Config.ensure(options))
9
+ config = Util::Data.clean(Config.ensure(options), false)
10
10
  name = Util::Data.ensure_value(config.delete(:plugin_name), config.delete(:name, provider))
11
11
 
12
12
  @quiet = config.delete(:quiet, false)
@@ -18,7 +18,7 @@ class Base < Core
18
18
  myself.plugin_name = name
19
19
 
20
20
  logger.debug("Normalizing #{plugin_type} plugin #{plugin_name} with meta data: #{meta.inspect}")
21
- normalize
21
+ normalize(false)
22
22
  end
23
23
 
24
24
  #---
@@ -147,7 +147,7 @@ class Base < Core
147
147
  #-----------------------------------------------------------------------------
148
148
  # Plugin operations
149
149
 
150
- def normalize
150
+ def normalize(reload)
151
151
  # Implement in sub classes
152
152
  end
153
153
 
@@ -6,7 +6,7 @@ class Command < Base
6
6
  #-----------------------------------------------------------------------------
7
7
  # Command plugin interface
8
8
 
9
- def normalize
9
+ def normalize(reload)
10
10
  super
11
11
  end
12
12
 
@@ -36,21 +36,26 @@ class Project < Base
36
36
  #-----------------------------------------------------------------------------
37
37
  # Project plugin interface
38
38
 
39
- def normalize
39
+ def normalize(reload)
40
40
  super
41
41
 
42
- extension(:normalize)
43
-
44
42
  set_directory(Util::Disk.filename(get(:directory, Dir.pwd)))
43
+ register
44
+
45
45
  set_url(get(:url)) if get(:url, false)
46
46
 
47
47
  myself.plugin_name = path if myself.plugin_name == plugin_provider
48
48
 
49
+ ui.resource = plugin_name
50
+ logger = plugin_name
51
+
49
52
  if keys = delete(:keys, nil)
50
53
  set(:private_key, keys[:private_key])
51
54
  set(:public_key, keys[:public_key])
52
55
  end
53
56
 
57
+ extension(:normalize)
58
+
54
59
  init_project
55
60
  extension(:init)
56
61
 
@@ -63,7 +68,7 @@ class Project < Base
63
68
  init_auth
64
69
  init_parent
65
70
  init_remotes
66
- load_revision
71
+ load_revision
67
72
  end
68
73
 
69
74
  #-----------------------------------------------------------------------------
@@ -71,7 +76,12 @@ class Project < Base
71
76
 
72
77
  def register
73
78
  super
74
- # TODO: Scan project directory looking for plugins
79
+ if directory
80
+ lib_path = File.join(directory, 'lib')
81
+ if File.directory?(lib_path)
82
+ CORL.register(lib_path)
83
+ end
84
+ end
75
85
  end
76
86
 
77
87
  #-----------------------------------------------------------------------------
@@ -560,7 +570,7 @@ class Project < Base
560
570
 
561
571
  #---
562
572
 
563
- def foreach!
573
+ def each
564
574
  if can_persist?
565
575
  localize do
566
576
  logger.info("Iterating through all sub projects of project #{name}")
@@ -614,14 +624,20 @@ class Project < Base
614
624
 
615
625
  #---
616
626
 
617
- def set_remote(name, url)
627
+ def set_remote(name, url, options = {})
628
+ config = Config.ensure(options)
629
+
618
630
  if can_persist?
619
631
  localize do
620
- if ! url.strip.empty? && url = extension_set(:set_remote, url, { :name => name })
621
- delete_remote(name)
632
+ unless url.strip.empty?
633
+ if url = extension_set(:set_remote, url, { :name => name })
634
+ delete_remote(name)
635
+
636
+ url = translate_edit_url(url) if name == :edit && config.get(:translate, true)
622
637
 
623
- logger.info("Setting project remote #{name} to #{url}")
624
- yield(url) if block_given?
638
+ logger.info("Setting project remote #{name} to #{url}")
639
+ yield(url) if block_given?
640
+ end
625
641
  end
626
642
  end
627
643
  else
@@ -637,6 +653,8 @@ class Project < Base
637
653
  config = Config.ensure(options)
638
654
 
639
655
  if url = extension_set(:add_remote_url, url, { :name => name, :config => config })
656
+ url = translate_edit_url(url) if name == :edit && config.get(:translate, true)
657
+
640
658
  logger.info("Adding project remote url #{url} to #{name}")
641
659
  yield(config, url) if block_given?
642
660
  end
@@ -651,7 +669,7 @@ class Project < Base
651
669
  def set_host_remote(name, hosts, path, options = {})
652
670
  if can_persist?
653
671
  localize do
654
- config = Config.ensure(options).import({ :path => path })
672
+ config = Config.ensure(options).import({ :path => path, :translate => false })
655
673
  hosts = array(hosts)
656
674
 
657
675
  unless hosts.empty?
@@ -660,7 +678,7 @@ class Project < Base
660
678
  path = config.delete(:path)
661
679
 
662
680
  logger.info("Setting host remote #{name} for #{hosts.inspect} at #{path}")
663
- set_remote(name, translate_url(hosts.shift, path, config.export))
681
+ set_remote(name, translate_url(hosts.shift, path, config.export), config)
664
682
 
665
683
  hosts.each do |host|
666
684
  logger.debug("Adding remote url to #{host}")
@@ -16,11 +16,11 @@ module CLI
16
16
  #---
17
17
 
18
18
  def self.encode(data)
19
- Base64.encode64(Util::Data.to_json(data, false))
19
+ Base64.urlsafe_encode64(Util::Data.to_json(data, false))
20
20
  end
21
21
 
22
22
  def self.decode(encoded_string)
23
- Util::Data.symbol_map(Util::Data.parse_json(Base64.decode64(encoded_string)))
23
+ Util::Data.symbol_map(Util::Data.parse_json(Base64.urlsafe_decode64(encoded_string)))
24
24
  end
25
25
 
26
26
  #-------------------------------------------------------------------------
@@ -75,7 +75,7 @@ module CLI
75
75
  if sub_command
76
76
  results << [ sub_command, sub_args ]
77
77
  end
78
-
78
+
79
79
  return results.flatten
80
80
  end
81
81
 
@@ -130,14 +130,14 @@ module CLI
130
130
  options[:help] = true
131
131
  end
132
132
  end
133
-
134
133
  parser.parse!(args)
135
134
 
136
- # Now we can act on options given
137
- Nucleon.log_level = options[:log_level] if options[:log_level]
135
+ # Now we can act on options given
138
136
  return if options[:help]
139
137
 
140
138
  parse_encoded
139
+
140
+ Nucleon.log_level = options[:log_level] if options[:log_level]
141
141
 
142
142
  remaining_args = args.dup
143
143
  arg_messages = []
@@ -217,7 +217,7 @@ module CLI
217
217
  end
218
218
 
219
219
  encoded_properties.each do |name, value|
220
- self.options[name] = value unless options.has_key?(name)
220
+ self.options[name] = value
221
221
  end
222
222
  end
223
223
  options.delete(:encoded_params)
@@ -234,9 +234,10 @@ class Data
234
234
  #-----------------------------------------------------------------------------
235
235
  # Operations
236
236
 
237
- def self.clean(data)
237
+ def self.clean(data, remove_empty = true)
238
238
  data.keys.each do |key|
239
- data.delete(key) if data[key].nil?
239
+ obj = data[key]
240
+ data.delete(key) if obj.nil? || ( remove_empty && obj.is_a?(Hash) && obj.empty? )
240
241
  end
241
242
  data
242
243
  end
@@ -71,7 +71,10 @@ class Disk
71
71
  if file
72
72
  file.pos = 0 if options[:mode] == 'w'
73
73
  success = file.write(data)
74
- file.flush
74
+ begin
75
+ file.flush
76
+ rescue # In case the file is already closed
77
+ end
75
78
  return success
76
79
  end
77
80
  return nil
@@ -91,20 +91,22 @@ class Shell < Core
91
91
  begin
92
92
  t1, output_new, output_orig, output_reader = pipe_exec_stream($stdout, conditions, {
93
93
  :prefix => info_prefix,
94
- :suffix => info_suffix,
94
+ :suffix => info_suffix,
95
+ :quiet => config.get(:quiet, false)
95
96
  }, 'output') do |data|
96
97
  system_result.append_output(data)
97
- block_given? ? code.call(:output, command, data) : true
98
+ code ? code.call(:output, command, data) : true
98
99
  end
99
100
 
100
101
  t2, error_new, error_orig, error_reader = pipe_exec_stream($stderr, conditions, {
101
102
  :prefix => error_prefix,
102
- :suffix => error_suffix,
103
+ :suffix => error_suffix,
104
+ :quiet => config.get(:quiet, false)
103
105
  }, 'error') do |data|
104
106
  system_result.append_errors(data)
105
- block_given? ? code.call(:error, command, data) : true
107
+ code ? code.call(:error, command, data) : true
106
108
  end
107
-
109
+
108
110
  system_success = system(command)
109
111
  system_result.status = $?.exitstatus
110
112
 
@@ -131,7 +133,7 @@ class Shell < Core
131
133
 
132
134
  thread = process_stream(read, original, options, label) do |data|
133
135
  check_conditions(data, conditions, match_prefix) do
134
- block_given? ? code.call(data) : true
136
+ code ? code.call(data) : true
135
137
  end
136
138
  end
137
139
 
@@ -170,7 +172,7 @@ class Shell < Core
170
172
  end
171
173
 
172
174
  result = true
173
- if block_given?
175
+ if code
174
176
  result = code.call
175
177
 
176
178
  unless prefix.empty?
@@ -206,7 +208,7 @@ class Shell < Core
206
208
  suffix = default_suffix
207
209
 
208
210
  unless line.empty?
209
- if block_given?
211
+ if code
210
212
  result = code.call(line)
211
213
 
212
214
  if result && result.is_a?(Hash)
@@ -217,11 +219,13 @@ class Shell < Core
217
219
  success = result if success
218
220
  end
219
221
 
220
- prefix = ( prefix && ! prefix.empty? ? "#{prefix}: " : '' )
222
+ prefix = ( prefix && ! prefix.empty? ? prefix : '' )
221
223
  suffix = ( suffix && ! suffix.empty? ? suffix : '' )
222
224
  eol = ( index < lines.length - 1 || newline ? "\n" : ' ' )
223
-
224
- output.write(prefix.lstrip + line + suffix.rstrip + eol)
225
+
226
+ unless options[:quiet]
227
+ output.write(prefix.lstrip + line + suffix.rstrip + eol)
228
+ end
225
229
  end
226
230
  end
227
231
  end
@@ -0,0 +1,356 @@
1
+
2
+ module Nucleon
3
+ module Util
4
+ class SSH < Core
5
+
6
+ #-----------------------------------------------------------------------------
7
+ # User key home
8
+
9
+ @@key_path = nil
10
+
11
+ #---
12
+
13
+ def self.key_path
14
+ unless @@key_path
15
+ home_path = ( ENV['USER'] == 'root' ? '/root' : ENV['HOME'] ) # In case we are using sudo
16
+ @@key_path = File.join(home_path, '.ssh')
17
+
18
+ FileUtils.mkdir(@@key_path) unless File.directory?(@@key_path)
19
+ end
20
+ @@key_path
21
+ end
22
+
23
+ #-----------------------------------------------------------------------------
24
+ # Instance generators
25
+
26
+ def self.generate(options = {})
27
+ config = Config.ensure(options)
28
+
29
+ private_key = config.get(:private_key, nil)
30
+ original_key = nil
31
+ key_comment = config.get(:comment, '')
32
+
33
+ if private_key.nil?
34
+ key_type = config.get(:type, "RSA")
35
+ key_bits = config.get(:bits, 2048)
36
+ passphrase = config.get(:passphrase, nil)
37
+
38
+ key_data = SSHKey.generate(
39
+ :type => key_type,
40
+ :bits => key_bits,
41
+ :comment => key_comment,
42
+ :passphrase => passphrase
43
+ )
44
+ is_new = true
45
+
46
+ else
47
+ if private_key.include?('PRIVATE KEY')
48
+ original_key = private_key
49
+ else
50
+ original_key = Disk.read(private_key)
51
+ end
52
+
53
+ key_data = SSHKey.new(original_key, :comment => key_comment) if original_key
54
+ is_new = false
55
+ end
56
+
57
+ return nil unless key_data && ! key_data.ssh_public_key.empty?
58
+ Keypair.new(key_data, is_new, original_key)
59
+ end
60
+
61
+ #-----------------------------------------------------------------------------
62
+ # Checks
63
+
64
+ def self.valid?(public_ssh_key)
65
+ SSHKey.valid_ssh_public_key?(public_ssh_key)
66
+ end
67
+
68
+ #-----------------------------------------------------------------------------
69
+ # Keypair interface
70
+
71
+ class Keypair
72
+ attr_reader :type, :private_key, :encrypted_key, :public_key, :ssh_key
73
+
74
+ def initialize(key_data, is_new, original_key)
75
+ @type = key_data.type
76
+ @private_key = key_data.private_key
77
+ @encrypted_key = is_new ? key_data.encrypted_private_key : original_key
78
+ @public_key = key_data.public_key
79
+ @ssh_key = key_data.ssh_public_key
80
+ end
81
+
82
+ #---
83
+
84
+ def store(key_path = nil, key_base = 'id')
85
+ key_path = SSH.key_path if key_path.nil?
86
+ private_key_file = File.join(key_path, "#{key_base}_#{type.downcase}")
87
+ public_key_file = File.join(key_path, "#{key_base}_#{type.downcase}.pub")
88
+
89
+ private_success = Disk.write(private_key_file, encrypted_key)
90
+ FileUtils.chmod(0600, private_key_file) if private_success
91
+
92
+ public_success = Disk.write(public_key_file, ssh_key)
93
+
94
+ if private_success && public_success
95
+ return { :private_key => private_key_file, :public_key => public_key_file }
96
+ end
97
+ false
98
+ end
99
+ end
100
+
101
+ #-----------------------------------------------------------------------------
102
+ # SSH Execution interface
103
+
104
+ @@sessions = {}
105
+
106
+ #---
107
+
108
+ def self.session_id(hostname, user)
109
+ "#{hostname}-#{user}"
110
+ end
111
+
112
+ #---
113
+
114
+ def self.session(hostname, user, port = 22, private_key = nil, reset = false, options = {})
115
+ require 'net/ssh'
116
+
117
+ ssh_options = Config.new({
118
+ :user_known_hosts_file => [ File.join(key_path, 'known_hosts'), File.join(key_path, 'known_hosts2') ],
119
+ :key_data => [],
120
+ :keys_only => false,
121
+ :auth_methods => [ 'publickey' ],
122
+ :paranoid => :very
123
+ }).import(options)
124
+
125
+ ssh_options[:port] = port
126
+ ssh_options[:keys] = private_key.nil? ? [] : [ private_key ]
127
+
128
+ session_id = session_id(hostname, user)
129
+
130
+ if reset || ! @@sessions.has_key?(session_id)
131
+ @@sessions[session_id] = Net::SSH.start(hostname, user, ssh_options.export)
132
+ end
133
+ yield(@@sessions[session_id]) if block_given? && @@sessions[session_id]
134
+ @@sessions[session_id]
135
+ end
136
+
137
+ def self.init_session(hostname, user, port = 22, private_key = nil, options = {})
138
+ session(hostname, user, port, private_key, true, options)
139
+ end
140
+
141
+ #---
142
+
143
+ def self.close_session(hostname, user)
144
+ session_id = session_id(hostname, user)
145
+
146
+ if @@sessions.has_key?(session_id)
147
+ begin # Don't care about errors here
148
+ @@sessions[session_id].close
149
+ rescue
150
+ end
151
+ @@sessions.delete(session_id)
152
+ end
153
+ end
154
+
155
+ #---
156
+
157
+ def self.close(hostname = nil, user = nil)
158
+ if hostname && user.nil? # Assume we entered a session id
159
+ if @@sessions.has_key?(hostname)
160
+ @@sessions[hostname].close
161
+ @@sessions.delete(hostname)
162
+ end
163
+
164
+ elsif hostname && user # Generate session id from args
165
+ session_id = session_id(hostname, user)
166
+
167
+ if @@sessions.has_key?(session_id)
168
+ @@sessions[session_id].close
169
+ @@sessions.delete(session_id)
170
+ end
171
+
172
+ else # Close all connections
173
+ @@sessions.keys.each do |id|
174
+ @@sessions[id].close
175
+ @@sessions.delete(id)
176
+ end
177
+ end
178
+ end
179
+
180
+ #---
181
+
182
+ def self.exec(hostname, user, commands)
183
+ results = []
184
+
185
+ begin
186
+ session(hostname, user) do |ssh|
187
+ Data.array(commands).each do |command|
188
+ command = command.flatten.join(' ') if command.is_a?(Array)
189
+ command = command.to_s
190
+ result = Shell::Result.new(command)
191
+
192
+ ssh.open_channel do |ssh_channel|
193
+ ssh_channel.request_pty
194
+ ssh_channel.exec(command) do |channel, success|
195
+ unless success
196
+ raise "Could not execute command: #{command.inspect}"
197
+ end
198
+
199
+ channel.on_data do |ch, data|
200
+ result.append_output(data)
201
+ yield(:output, command, data) if block_given?
202
+ end
203
+
204
+ channel.on_extended_data do |ch, type, data|
205
+ next unless type == 1
206
+ result.append_errors(data)
207
+ yield(:error, command, data) if block_given?
208
+ end
209
+
210
+ channel.on_request('exit-status') do |ch, data|
211
+ result.status = data.read_long
212
+ end
213
+
214
+ channel.on_request('exit-signal') do |ch, data|
215
+ result.status = 255
216
+ end
217
+ end
218
+ end
219
+ ssh.loop
220
+ results << result
221
+ end
222
+ end
223
+ rescue Net::SSH::HostKeyMismatch => error
224
+ error.remember_host!
225
+ sleep 0.2
226
+ retry
227
+ end
228
+ results
229
+ end
230
+
231
+ #---
232
+
233
+ def self.download(hostname, user, remote_path, local_path, options = {})
234
+ config = Config.ensure(options)
235
+
236
+ require 'net/scp'
237
+
238
+ # Accepted options:
239
+ # * :recursive - the +remote+ parameter refers to a remote directory, which
240
+ # should be downloaded to a new directory named +local+ on the local
241
+ # machine.
242
+ # * :preserve - the atime and mtime of the file should be preserved.
243
+ # * :verbose - the process should result in verbose output on the server
244
+ # end (useful for debugging).
245
+ #
246
+ config.init(:recursive, true)
247
+ config.init(:preserve, true)
248
+ config.init(:verbose, true)
249
+
250
+ blocking = config.delete(:blocking, true)
251
+
252
+ session(hostname, user) do |ssh|
253
+ if blocking
254
+ ssh.scp.download!(remote_path, local_path, config.export) do |ch, name, received, total|
255
+ yield(name, received, total) if block_given?
256
+ end
257
+ else
258
+ ssh.scp.download(remote_path, local_path, config.export)
259
+ end
260
+ end
261
+ end
262
+
263
+ #---
264
+
265
+ def self.upload(hostname, user, local_path, remote_path, options = {})
266
+ config = Config.ensure(options)
267
+
268
+ require 'net/scp'
269
+
270
+ # Accepted options:
271
+ # * :recursive - the +local+ parameter refers to a local directory, which
272
+ # should be uploaded to a new directory named +remote+ on the remote
273
+ # server.
274
+ # * :preserve - the atime and mtime of the file should be preserved.
275
+ # * :verbose - the process should result in verbose output on the server
276
+ # end (useful for debugging).
277
+ # * :chunk_size - the size of each "chunk" that should be sent. Defaults
278
+ # to 2048. Changing this value may improve throughput at the expense
279
+ # of decreasing interactivity.
280
+ #
281
+ config.init(:recursive, true)
282
+ config.init(:preserve, true)
283
+ config.init(:verbose, true)
284
+ config.init(:chunk_size, 2048)
285
+
286
+ blocking = config.delete(:blocking, true)
287
+
288
+ session(hostname, user) do |ssh|
289
+ if blocking
290
+ ssh.scp.upload!(local_path, remote_path, config.export) do |ch, name, sent, total|
291
+ yield(name, sent, total) if block_given?
292
+ end
293
+ else
294
+ ssh.scp.upload(local_path, remote_path, config.export)
295
+ end
296
+ end
297
+ end
298
+
299
+ #---
300
+
301
+ #
302
+ # Inspired by vagrant ssh implementation
303
+ #
304
+ # See: https://github.com/mitchellh/vagrant/blob/master/lib/vagrant/util/ssh.rb
305
+ #
306
+
307
+ def self.terminal(hostname, user, options = {})
308
+ config = Config.ensure(options)
309
+ ssh_path = nucleon_locate("ssh")
310
+
311
+ raise Errors::SSHUnavailable unless ssh_path
312
+
313
+ port = config.get(:port, 22)
314
+ private_keys = config.get(:private_keys, File.join(ENV['HOME'], '.ssh', 'id_rsa'))
315
+
316
+ command_options = [
317
+ "#{user}@#{hostname}",
318
+ "-p", port.to_s,
319
+ "-o", "Compression=yes",
320
+ "-o", "DSAAuthentication=yes",
321
+ "-o", "LogLevel=FATAL",
322
+ "-o", "StrictHostKeyChecking=no",
323
+ "-o", "UserKnownHostsFile=/dev/null",
324
+ "-o", "IdentitiesOnly=yes"
325
+ ]
326
+
327
+ Util::Data.array(private_keys).each do |path|
328
+ command_options += [ "-i", File.expand_path(path) ]
329
+ end
330
+
331
+ if config.get(:forward_x11, false)
332
+ command_options += [
333
+ "-o", "ForwardX11=yes",
334
+ "-o", "ForwardX11Trusted=yes"
335
+ ]
336
+ end
337
+
338
+ command_options += [ "-o", "ProxyCommand=#{config[:proxy_command]}" ] if config.get(:proxy_command, false)
339
+ command_options += [ "-o", "ForwardAgent=yes" ] if config.get(:forward_agent, false)
340
+
341
+ command_options.concat(Util::Data.array(config[:extra_args])) if config.get(:extra_args, false)
342
+
343
+ #---
344
+
345
+ logger.info("Executing SSH in subprocess: #{command_options.inspect}")
346
+
347
+ process = ChildProcess.build('ssh', *command_options)
348
+ process.io.inherit!
349
+
350
+ process.start
351
+ process.wait
352
+ process.exit_code
353
+ end
354
+ end
355
+ end
356
+ end