nucleon 0.1.3 → 0.1.4

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