leap_cli 1.2.5
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.
- data/bin/leap +81 -0
- data/lib/core_ext/boolean.rb +14 -0
- data/lib/core_ext/hash.rb +35 -0
- data/lib/core_ext/json.rb +42 -0
- data/lib/core_ext/nil.rb +5 -0
- data/lib/core_ext/string.rb +14 -0
- data/lib/leap/platform.rb +52 -0
- data/lib/leap_cli/commands/ca.rb +430 -0
- data/lib/leap_cli/commands/clean.rb +16 -0
- data/lib/leap_cli/commands/compile.rb +134 -0
- data/lib/leap_cli/commands/deploy.rb +172 -0
- data/lib/leap_cli/commands/facts.rb +93 -0
- data/lib/leap_cli/commands/inspect.rb +140 -0
- data/lib/leap_cli/commands/list.rb +122 -0
- data/lib/leap_cli/commands/new.rb +126 -0
- data/lib/leap_cli/commands/node.rb +272 -0
- data/lib/leap_cli/commands/pre.rb +99 -0
- data/lib/leap_cli/commands/shell.rb +67 -0
- data/lib/leap_cli/commands/test.rb +55 -0
- data/lib/leap_cli/commands/user.rb +140 -0
- data/lib/leap_cli/commands/util.rb +50 -0
- data/lib/leap_cli/commands/vagrant.rb +201 -0
- data/lib/leap_cli/config/macros.rb +369 -0
- data/lib/leap_cli/config/manager.rb +369 -0
- data/lib/leap_cli/config/node.rb +37 -0
- data/lib/leap_cli/config/object.rb +336 -0
- data/lib/leap_cli/config/object_list.rb +174 -0
- data/lib/leap_cli/config/secrets.rb +43 -0
- data/lib/leap_cli/config/tag.rb +18 -0
- data/lib/leap_cli/constants.rb +7 -0
- data/lib/leap_cli/leapfile.rb +97 -0
- data/lib/leap_cli/load_paths.rb +15 -0
- data/lib/leap_cli/log.rb +166 -0
- data/lib/leap_cli/logger.rb +216 -0
- data/lib/leap_cli/markdown_document_listener.rb +134 -0
- data/lib/leap_cli/path.rb +84 -0
- data/lib/leap_cli/remote/leap_plugin.rb +204 -0
- data/lib/leap_cli/remote/puppet_plugin.rb +66 -0
- data/lib/leap_cli/remote/rsync_plugin.rb +35 -0
- data/lib/leap_cli/remote/tasks.rb +36 -0
- data/lib/leap_cli/requirements.rb +19 -0
- data/lib/leap_cli/ssh_key.rb +130 -0
- data/lib/leap_cli/util/remote_command.rb +110 -0
- data/lib/leap_cli/util/secret.rb +54 -0
- data/lib/leap_cli/util/x509.rb +32 -0
- data/lib/leap_cli/util.rb +431 -0
- data/lib/leap_cli/version.rb +9 -0
- data/lib/leap_cli.rb +46 -0
- data/lib/lib_ext/capistrano_connections.rb +16 -0
- data/lib/lib_ext/gli.rb +52 -0
- data/lib/lib_ext/markdown_document_listener.rb +122 -0
- data/vendor/certificate_authority/lib/certificate_authority/certificate.rb +200 -0
- data/vendor/certificate_authority/lib/certificate_authority/certificate_revocation_list.rb +77 -0
- data/vendor/certificate_authority/lib/certificate_authority/distinguished_name.rb +97 -0
- data/vendor/certificate_authority/lib/certificate_authority/extensions.rb +266 -0
- data/vendor/certificate_authority/lib/certificate_authority/key_material.rb +148 -0
- data/vendor/certificate_authority/lib/certificate_authority/ocsp_handler.rb +144 -0
- data/vendor/certificate_authority/lib/certificate_authority/pkcs11_key_material.rb +65 -0
- data/vendor/certificate_authority/lib/certificate_authority/revocable.rb +14 -0
- data/vendor/certificate_authority/lib/certificate_authority/serial_number.rb +10 -0
- data/vendor/certificate_authority/lib/certificate_authority/signing_entity.rb +16 -0
- data/vendor/certificate_authority/lib/certificate_authority/signing_request.rb +56 -0
- data/vendor/certificate_authority/lib/certificate_authority.rb +21 -0
- data/vendor/rsync_command/lib/rsync_command/ssh_options.rb +159 -0
- data/vendor/rsync_command/lib/rsync_command/thread_pool.rb +36 -0
- data/vendor/rsync_command/lib/rsync_command/version.rb +3 -0
- data/vendor/rsync_command/lib/rsync_command.rb +96 -0
- data/vendor/rsync_command/test/rsync_test.rb +74 -0
- data/vendor/rsync_command/test/ssh_options_test.rb +61 -0
- data/vendor/vagrant_ssh_keys/vagrant.key +27 -0
- data/vendor/vagrant_ssh_keys/vagrant.pub +1 -0
- metadata +345 -0
@@ -0,0 +1,204 @@
|
|
1
|
+
#
|
2
|
+
# these methods are made available in capistrano tasks as 'leap.method_name'
|
3
|
+
# (see RemoteCommand::new_capistrano)
|
4
|
+
#
|
5
|
+
|
6
|
+
module LeapCli; module Remote; module LeapPlugin
|
7
|
+
|
8
|
+
def required_packages
|
9
|
+
"puppet ruby-hiera-puppet rsync lsb-release locales"
|
10
|
+
end
|
11
|
+
|
12
|
+
def log(*args, &block)
|
13
|
+
LeapCli::Util::log(*args, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
#
|
17
|
+
# creates directories that are owned by root and 700 permissions
|
18
|
+
#
|
19
|
+
def mkdirs(*dirs)
|
20
|
+
raise ArgumentError.new('illegal dir name') if dirs.grep(/[\' ]/).any?
|
21
|
+
run dirs.collect{|dir| "mkdir -m 700 -p #{dir}; "}.join
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# echos "ok" if the node has been initialized and the required packages are installed, bails out otherwise.
|
26
|
+
#
|
27
|
+
def assert_initialized
|
28
|
+
begin
|
29
|
+
test_initialized_file = "test -f #{INITIALIZED_FILE}"
|
30
|
+
check_required_packages = "! dpkg-query -W --showformat='${Status}\n' #{required_packages} 2>&1 | grep -q -E '(deinstall|no packages)'"
|
31
|
+
run "#{test_initialized_file} && #{check_required_packages} && echo ok"
|
32
|
+
rescue Capistrano::CommandError => exc
|
33
|
+
LeapCli::Util.bail! do
|
34
|
+
exc.hosts.each do |host|
|
35
|
+
LeapCli::Util.log :error, "running deploy: node not initialized. Run 'leap node init #{host}'", :host => host
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# bails out the deploy if the file /etc/leap/no-deploy exists.
|
43
|
+
# This kind of sucks, because it would be better to skip over nodes that have no-deploy set instead
|
44
|
+
# halting the entire deploy. As far as I know, with capistrano, there is no way to close one of the
|
45
|
+
# ssh connections in the pool and make sure it gets no further commands.
|
46
|
+
#
|
47
|
+
def check_for_no_deploy
|
48
|
+
begin
|
49
|
+
run "test ! -f /etc/leap/no-deploy"
|
50
|
+
rescue Capistrano::CommandError => exc
|
51
|
+
LeapCli::Util.bail! do
|
52
|
+
exc.hosts.each do |host|
|
53
|
+
LeapCli::Util.log "Can't continue because file /etc/leap/no-deploy exists", :host => host
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def mark_initialized
|
60
|
+
run "touch #{INITIALIZED_FILE}"
|
61
|
+
end
|
62
|
+
|
63
|
+
#
|
64
|
+
# This is a hairy ugly hack, exactly the kind of stuff that makes ruby
|
65
|
+
# dangerous and too much fun for its own good.
|
66
|
+
#
|
67
|
+
# In most places, we run remote ssh without a current 'task'. This works fine,
|
68
|
+
# except that in a few places, the behavior of capistrano ssh is controlled by
|
69
|
+
# the options of the current task.
|
70
|
+
#
|
71
|
+
# We don't want to create an actual current task, because tasks are no fun
|
72
|
+
# and can't take arguments or return values. So, when we need to configure
|
73
|
+
# things that can only be configured in a task, we use this handy hack to
|
74
|
+
# fake the current task.
|
75
|
+
#
|
76
|
+
# This is NOT thread safe, but could be made to be so with some extra work.
|
77
|
+
#
|
78
|
+
def with_task(name)
|
79
|
+
task = @config.tasks[name]
|
80
|
+
@config.class.send(:alias_method, :original_current_task, :current_task)
|
81
|
+
@config.class.send(:define_method, :current_task, Proc.new(){ task })
|
82
|
+
begin
|
83
|
+
yield
|
84
|
+
ensure
|
85
|
+
@config.class.send(:remove_method, :current_task)
|
86
|
+
@config.class.send(:alias_method, :current_task, :original_current_task)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# similar to run(cmd, &block), but with:
|
92
|
+
#
|
93
|
+
# * exit codes
|
94
|
+
# * stdout and stderr are combined
|
95
|
+
#
|
96
|
+
def stream(cmd, &block)
|
97
|
+
command = '%s 2>&1; echo "exitcode=$?"' % cmd
|
98
|
+
run(command) do |channel, stream, data|
|
99
|
+
exitcode = nil
|
100
|
+
if data =~ /exitcode=(\d+)\n/
|
101
|
+
exitcode = $1.to_i
|
102
|
+
data.sub!(/exitcode=(\d+)\n/,'')
|
103
|
+
end
|
104
|
+
yield({:host => channel[:host], :data => data, :exitcode => exitcode})
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
#
|
109
|
+
# like stream, but capture all the output before returning
|
110
|
+
#
|
111
|
+
def capture(cmd, &block)
|
112
|
+
command = '%s 2>&1; echo "exitcode=$?" 2>&1;' % cmd
|
113
|
+
host_data = {}
|
114
|
+
run(command) do |channel, stream, data|
|
115
|
+
host_data[channel[:host]] ||= ""
|
116
|
+
if data =~ /exitcode=(\d+)\n/
|
117
|
+
exitcode = $1.to_i
|
118
|
+
data.sub!(/exitcode=(\d+)\n/,'')
|
119
|
+
host_data[channel[:host]] += data
|
120
|
+
yield({:host => channel[:host], :data => host_data[channel[:host]], :exitcode => exitcode})
|
121
|
+
else
|
122
|
+
host_data[channel[:host]] += data
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
#
|
128
|
+
# Run a command, with a nice status report and progress indicator.
|
129
|
+
# Only successful results are returned, errors are printed.
|
130
|
+
#
|
131
|
+
# For each successful run on each host, block is yielded with a hash like so:
|
132
|
+
#
|
133
|
+
# {:host => 'bluejay', :exitcode => 0, :data => 'shell output'}
|
134
|
+
#
|
135
|
+
def run_with_progress(cmd, &block)
|
136
|
+
ssh_failures = []
|
137
|
+
exitcode_failures = []
|
138
|
+
succeeded = []
|
139
|
+
task = LeapCli.log_level > 1 ? :standard_task : :skip_errors_task
|
140
|
+
with_task(task) do
|
141
|
+
log :querying, 'facts' do
|
142
|
+
progress " "
|
143
|
+
call_on_failure do |host|
|
144
|
+
ssh_failures << host
|
145
|
+
progress 'F'
|
146
|
+
end
|
147
|
+
capture(cmd) do |response|
|
148
|
+
if response[:exitcode] == 0
|
149
|
+
progress '.'
|
150
|
+
yield response
|
151
|
+
else
|
152
|
+
exitcode_failures << response
|
153
|
+
progress 'F'
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
puts "done"
|
159
|
+
if ssh_failures.any?
|
160
|
+
log :failed, 'to connect to nodes: ' + ssh_failures.join(' ')
|
161
|
+
end
|
162
|
+
if exitcode_failures.any?
|
163
|
+
log :failed, 'to run successfully:' do
|
164
|
+
exitcode_failures.each do |response|
|
165
|
+
log "[%s] exit %s - %s" % [response[:host], response[:exitcode], response[:data].strip]
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
rescue Capistrano::RemoteError => err
|
170
|
+
log :error, err.to_s
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
def progress(str='.')
|
176
|
+
$stdout.print str; $stdout.flush;
|
177
|
+
end
|
178
|
+
|
179
|
+
#def mkdir(dir)
|
180
|
+
# run "mkdir -p #{dir}"
|
181
|
+
#end
|
182
|
+
|
183
|
+
#def chown_root(dir)
|
184
|
+
# run "chown root -R #{dir} && chmod -R ag-rwx,u+rwX #{dir}"
|
185
|
+
#end
|
186
|
+
|
187
|
+
#def logrun(cmd)
|
188
|
+
# @streamer ||= LeapCli::Remote::LogStreamer.new
|
189
|
+
# run cmd do |channel, stream, data|
|
190
|
+
# @streamer.collect_output(channel[:host], data)
|
191
|
+
# end
|
192
|
+
#end
|
193
|
+
|
194
|
+
# return_code = nil
|
195
|
+
# run "something; echo return code: $?" do |channel, stream, data|
|
196
|
+
# if data =~ /return code: (\d+)/
|
197
|
+
# return_code = $1.to_i
|
198
|
+
# else
|
199
|
+
# Capistrano::Configuration.default_io_proc.call(channel, stream, data)
|
200
|
+
# end
|
201
|
+
# end
|
202
|
+
# puts "finished with return code: #{return_code}"
|
203
|
+
|
204
|
+
end; end; end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
#
|
2
|
+
# these methods are made available in capistrano tasks as 'puppet.method_name'
|
3
|
+
# (see RemoteCommand::new_capistrano)
|
4
|
+
#
|
5
|
+
|
6
|
+
module LeapCli; module Remote; module PuppetPlugin
|
7
|
+
|
8
|
+
def apply(options)
|
9
|
+
run "#{PUPPET_DESTINATION}/bin/puppet_command set_hostname apply #{flagize(options)}"
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def flagize(hsh)
|
15
|
+
hsh.inject([]) {|str, item|
|
16
|
+
if item[1] === false
|
17
|
+
str
|
18
|
+
elsif item[1] === true
|
19
|
+
str << "--" + item[0].to_s
|
20
|
+
else
|
21
|
+
str << "--" + item[0].to_s + " " + item[1].to_s
|
22
|
+
end
|
23
|
+
}.join(' ')
|
24
|
+
end
|
25
|
+
|
26
|
+
end; end; end
|
27
|
+
|
28
|
+
|
29
|
+
# def puppet(command = :noop)
|
30
|
+
# #puppet_cmd = "cd #{puppet_destination} && #{sudo_cmd} #{puppet_command} --modulepath=#{puppet_lib} #{puppet_parameters}"
|
31
|
+
# puppet_cmd = "cd #{puppet_destination} && #{sudo_cmd} #{puppet_command} #{puppet_parameters}"
|
32
|
+
# flag = command == :noop ? '--noop' : ''
|
33
|
+
|
34
|
+
# writer = if puppet_stream_output
|
35
|
+
# SupplyDrop::Writer::Streaming.new(logger)
|
36
|
+
# else
|
37
|
+
# SupplyDrop::Writer::Batched.new(logger)
|
38
|
+
# end
|
39
|
+
|
40
|
+
# writer = SupplyDrop::Writer::File.new(writer, puppet_write_to_file) unless puppet_write_to_file.nil?
|
41
|
+
|
42
|
+
# begin
|
43
|
+
# exitcode = nil
|
44
|
+
# run "#{puppet_cmd} #{flag}; echo exitcode:$?" do |channel, stream, data|
|
45
|
+
# if data =~ /exitcode:(\d+)/
|
46
|
+
# exitcode = $1
|
47
|
+
# writer.collect_output(channel[:host], "Puppet #{command} complete (#{exitcode_description(exitcode)}).\n")
|
48
|
+
# else
|
49
|
+
# writer.collect_output(channel[:host], data)
|
50
|
+
# end
|
51
|
+
# end
|
52
|
+
# ensure
|
53
|
+
# writer.all_output_collected
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
|
57
|
+
# def exitcode_description(code)
|
58
|
+
# case code
|
59
|
+
# when "0" then "no changes"
|
60
|
+
# when "2" then "changes made"
|
61
|
+
# when "4" then "failed"
|
62
|
+
# when "6" then "changes and failures"
|
63
|
+
# else code
|
64
|
+
# end
|
65
|
+
# end
|
66
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
#
|
2
|
+
# these methods are made available in capistrano tasks as 'rsync.method_name'
|
3
|
+
# (see RemoteCommand::new_capistrano)
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'rsync_command'
|
7
|
+
|
8
|
+
module LeapCli; module Remote; module RsyncPlugin
|
9
|
+
|
10
|
+
#
|
11
|
+
# takes a block, yielded a server, that should return a hash with various rsync options.
|
12
|
+
# supported options include:
|
13
|
+
#
|
14
|
+
# {:source => '', :dest => '', :flags => '', :includes => [], :excludes => []}
|
15
|
+
#
|
16
|
+
def update
|
17
|
+
rsync = RsyncCommand.new(:logger => logger)
|
18
|
+
rsync.asynchronously(find_servers) do |server|
|
19
|
+
options = yield server
|
20
|
+
next unless options
|
21
|
+
remote_user = server.user || fetch(:user, ENV['USER'])
|
22
|
+
src = options[:source]
|
23
|
+
dest = {:user => remote_user, :host => server.host, :path => options[:dest]}
|
24
|
+
options[:ssh] = ssh_options.merge(server.options[:ssh_options]||{})
|
25
|
+
options[:chdir] ||= Path.provider
|
26
|
+
rsync.exec(src, dest, options)
|
27
|
+
end
|
28
|
+
if rsync.failed?
|
29
|
+
LeapCli::Util.bail! do
|
30
|
+
LeapCli::Util.log :failed, "to rsync to #{rsync.failures.map{|f|f[:dest][:host]}.join(' ')}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end; end; end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#
|
2
|
+
# This file is evaluated just the same as a typical capistrano "deploy.rb"
|
3
|
+
# For DSL manual, see https://github.com/capistrano/capistrano/wiki
|
4
|
+
#
|
5
|
+
|
6
|
+
MAX_HOSTS = 10
|
7
|
+
|
8
|
+
task :install_authorized_keys, :max_hosts => MAX_HOSTS do
|
9
|
+
leap.log :updating, "authorized_keys" do
|
10
|
+
leap.mkdirs '/root/.ssh'
|
11
|
+
upload LeapCli::Path.named_path(:authorized_keys), '/root/.ssh/authorized_keys', :mode => '600'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
task :install_prerequisites, :max_hosts => MAX_HOSTS do
|
16
|
+
leap.mkdirs LeapCli::PUPPET_DESTINATION
|
17
|
+
leap.log :updating, "package list" do
|
18
|
+
run "apt-get update"
|
19
|
+
end
|
20
|
+
leap.log :installing, "required packages" do
|
21
|
+
run "DEBIAN_FRONTEND=noninteractive apt-get -q -y -o DPkg::Options::=--force-confold install #{leap.required_packages}"
|
22
|
+
end
|
23
|
+
run "echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen; locale-gen"
|
24
|
+
leap.mkdirs("/etc/leap", "/srv/leap")
|
25
|
+
leap.mark_initialized
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# just dummies, used to capture task options
|
30
|
+
#
|
31
|
+
|
32
|
+
task :skip_errors_task, :on_error => :continue, :max_hosts => MAX_HOSTS do
|
33
|
+
end
|
34
|
+
|
35
|
+
task :standard_task, :max_hosts => MAX_HOSTS do
|
36
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# run 'rake update-requirements' to generate this file.
|
2
|
+
module LeapCli
|
3
|
+
REQUIREMENTS = [
|
4
|
+
"provider.ca.name",
|
5
|
+
"provider.ca.server_certificates.bit_size",
|
6
|
+
"provider.ca.server_certificates.digest",
|
7
|
+
"provider.ca.server_certificates.life_span",
|
8
|
+
"common.x509.use",
|
9
|
+
"provider.domain",
|
10
|
+
"provider.name",
|
11
|
+
"provider.ca.server_certificates.bit_size",
|
12
|
+
"provider.ca.server_certificates.digest",
|
13
|
+
"provider.ca.name",
|
14
|
+
"provider.ca.bit_size",
|
15
|
+
"provider.ca.life_span",
|
16
|
+
"provider.ca.client_certificates.unlimited_prefix",
|
17
|
+
"provider.ca.client_certificates.limited_prefix"
|
18
|
+
]
|
19
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
#
|
2
|
+
# A wrapper around OpenSSL::PKey::RSA instances to provide a better api for dealing with SSH keys.
|
3
|
+
#
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'net/ssh'
|
7
|
+
require 'forwardable'
|
8
|
+
|
9
|
+
module LeapCli
|
10
|
+
class SshKey
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
attr_accessor :filename
|
14
|
+
attr_accessor :comment
|
15
|
+
|
16
|
+
##
|
17
|
+
## CLASS METHODS
|
18
|
+
##
|
19
|
+
|
20
|
+
def self.load(arg1, arg2=nil)
|
21
|
+
key = nil
|
22
|
+
if arg1.is_a? OpenSSL::PKey::RSA
|
23
|
+
key = SshKey.new arg1
|
24
|
+
elsif arg1.is_a? String
|
25
|
+
if arg1 =~ /^ssh-/
|
26
|
+
type, data = arg1.split(' ')
|
27
|
+
key = SshKey.new load_from_data(data, type)
|
28
|
+
elsif File.exists? arg1
|
29
|
+
key = SshKey.new load_from_file(arg1)
|
30
|
+
key.filename = arg1
|
31
|
+
else
|
32
|
+
key = SshKey.new load_from_data(arg1, arg2)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
return key
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.load_from_file(filename)
|
39
|
+
public_key = nil
|
40
|
+
private_key = nil
|
41
|
+
begin
|
42
|
+
public_key = Net::SSH::KeyFactory.load_public_key(filename)
|
43
|
+
rescue NotImplementedError, Net::SSH::Exception, OpenSSL::PKey::PKeyError
|
44
|
+
begin
|
45
|
+
private_key = Net::SSH::KeyFactory.load_private_key(filename)
|
46
|
+
rescue NotImplementedError, Net::SSH::Exception, OpenSSL::PKey::PKeyError
|
47
|
+
end
|
48
|
+
end
|
49
|
+
public_key || private_key
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.load_from_data(data, type='ssh-rsa')
|
53
|
+
public_key = nil
|
54
|
+
private_key = nil
|
55
|
+
begin
|
56
|
+
public_key = Net::SSH::KeyFactory.load_data_public_key("#{type} #{data}")
|
57
|
+
rescue NotImplementedError, Net::SSH::Exception, OpenSSL::PKey::PKeyError
|
58
|
+
begin
|
59
|
+
private_key = Net::SSH::KeyFactory.load_data_private_key("#{type} #{data}")
|
60
|
+
rescue NotImplementedError, Net::SSH::Exception, OpenSSL::PKey::PKeyError
|
61
|
+
end
|
62
|
+
end
|
63
|
+
public_key || private_key
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
## INSTANCE METHODS
|
68
|
+
##
|
69
|
+
|
70
|
+
public
|
71
|
+
|
72
|
+
def initialize(rsa_key)
|
73
|
+
@key = rsa_key
|
74
|
+
end
|
75
|
+
|
76
|
+
def_delegator :@key, :fingerprint, :fingerprint
|
77
|
+
def_delegator :@key, :public?, :public?
|
78
|
+
def_delegator :@key, :private?, :private?
|
79
|
+
def_delegator :@key, :ssh_type, :type
|
80
|
+
def_delegator :@key, :public_encrypt, :public_encrypt
|
81
|
+
def_delegator :@key, :public_decrypt, :public_decrypt
|
82
|
+
def_delegator :@key, :private_encrypt, :private_encrypt
|
83
|
+
def_delegator :@key, :private_decrypt, :private_decrypt
|
84
|
+
def_delegator :@key, :params, :params
|
85
|
+
def_delegator :@key, :to_text, :to_text
|
86
|
+
|
87
|
+
def public_key
|
88
|
+
SshKey.new(@key.public_key)
|
89
|
+
end
|
90
|
+
|
91
|
+
def private_key
|
92
|
+
SshKey.new(@key.private_key)
|
93
|
+
end
|
94
|
+
|
95
|
+
#
|
96
|
+
# not sure if this will always work, but is seems to for now.
|
97
|
+
#
|
98
|
+
def bits
|
99
|
+
Net::SSH::Buffer.from(:key, @key).to_s.split("\001\000").last.size * 8
|
100
|
+
end
|
101
|
+
|
102
|
+
def summary
|
103
|
+
"%s %s %s (%s)" % [self.type, self.bits, self.fingerprint, self.filename || self.comment || '']
|
104
|
+
end
|
105
|
+
|
106
|
+
def to_s
|
107
|
+
self.type + " " + self.key
|
108
|
+
end
|
109
|
+
|
110
|
+
def key
|
111
|
+
[Net::SSH::Buffer.from(:key, @key).to_s].pack("m*").gsub(/\s/, "")
|
112
|
+
end
|
113
|
+
|
114
|
+
def ==(other_key)
|
115
|
+
return false if other_key.nil?
|
116
|
+
return false if self.class != other_key.class
|
117
|
+
return self.to_text == other_key.to_text
|
118
|
+
end
|
119
|
+
|
120
|
+
def in_known_hosts?(*identifiers)
|
121
|
+
identifiers.each do |identifier|
|
122
|
+
Net::SSH::KnownHosts.search_for(identifier).each do |key|
|
123
|
+
return true if self == key
|
124
|
+
end
|
125
|
+
end
|
126
|
+
return false
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module LeapCli; module Util; module RemoteCommand
|
2
|
+
extend self
|
3
|
+
|
4
|
+
#
|
5
|
+
# FYI
|
6
|
+
# Capistrano::Logger::IMPORTANT = 0
|
7
|
+
# Capistrano::Logger::INFO = 1
|
8
|
+
# Capistrano::Logger::DEBUG = 2
|
9
|
+
# Capistrano::Logger::TRACE = 3
|
10
|
+
#
|
11
|
+
def ssh_connect(nodes, options={}, &block)
|
12
|
+
options ||= {}
|
13
|
+
node_list = parse_node_list(nodes)
|
14
|
+
|
15
|
+
cap = new_capistrano
|
16
|
+
cap.logger = LeapCli::Logger.new(:level => LeapCli.log_level)
|
17
|
+
user = options[:user] || 'root'
|
18
|
+
cap.set :user, user
|
19
|
+
cap.set :ssh_options, ssh_options # ssh options common to all nodes
|
20
|
+
cap.set :use_sudo, false # we may want to change this in the future
|
21
|
+
|
22
|
+
# Allow password authentication when we are bootstraping a single node
|
23
|
+
# (and key authentication fails).
|
24
|
+
if options[:bootstrap] && node_list.size == 1
|
25
|
+
hostname = node_list.values.first.name
|
26
|
+
if options[:echo]
|
27
|
+
cap.set(:password) { ask "Root SSH password for #{user}@#{hostname}> " }
|
28
|
+
else
|
29
|
+
cap.set(:password) { Capistrano::CLI.password_prompt " * Typed password will be hidden (use --echo to make it visible)\nRoot SSH password for #{user}@#{hostname}> " }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
node_list.each do |name, node|
|
34
|
+
cap.server node.name, :dummy_arg, node_options(node, options[:ssh_options])
|
35
|
+
end
|
36
|
+
|
37
|
+
yield cap
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
#
|
43
|
+
# For available options, see http://net-ssh.github.com/net-ssh/classes/Net/SSH.html#method-c-start
|
44
|
+
#
|
45
|
+
def ssh_options
|
46
|
+
{
|
47
|
+
:config => false,
|
48
|
+
:global_known_hosts_file => path(:known_hosts),
|
49
|
+
:user_known_hosts_file => '/dev/null',
|
50
|
+
:paranoid => true
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# For notes on advanced ways to set server-specific options, see
|
56
|
+
# http://railsware.com/blog/2011/11/02/advanced-server-definitions-in-capistrano/
|
57
|
+
#
|
58
|
+
# if, in the future, we want to do per-node password options, it would be done like so:
|
59
|
+
#
|
60
|
+
# password_proc = Proc.new {Capistrano::CLI.password_prompt "Root SSH password for #{node.name}"}
|
61
|
+
# return {:password => password_proc}
|
62
|
+
#
|
63
|
+
def node_options(node, ssh_options_override=nil)
|
64
|
+
ssh_options_override ||= {}
|
65
|
+
{
|
66
|
+
:ssh_options => {
|
67
|
+
# :host_key_alias => node.name, << incompatible with ports in known_hosts
|
68
|
+
:host_name => node.ip_address,
|
69
|
+
:port => node.ssh.port
|
70
|
+
}.merge(contingent_ssh_options_for_node(node)).merge(ssh_options_override)
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
def new_capistrano
|
75
|
+
# load once the library files
|
76
|
+
@capistrano_enabled ||= begin
|
77
|
+
require 'capistrano'
|
78
|
+
require 'capistrano/cli'
|
79
|
+
require 'lib_ext/capistrano_connections'
|
80
|
+
require 'leap_cli/remote/leap_plugin'
|
81
|
+
require 'leap_cli/remote/puppet_plugin'
|
82
|
+
require 'leap_cli/remote/rsync_plugin'
|
83
|
+
Capistrano.plugin :leap, LeapCli::Remote::LeapPlugin
|
84
|
+
Capistrano.plugin :puppet, LeapCli::Remote::PuppetPlugin
|
85
|
+
Capistrano.plugin :rsync, LeapCli::Remote::RsyncPlugin
|
86
|
+
true
|
87
|
+
end
|
88
|
+
|
89
|
+
# create capistrano instance
|
90
|
+
cap = Capistrano::Configuration.new
|
91
|
+
|
92
|
+
# add tasks to capistrano instance
|
93
|
+
cap.load File.dirname(__FILE__) + '/../remote/tasks.rb'
|
94
|
+
|
95
|
+
return cap
|
96
|
+
end
|
97
|
+
|
98
|
+
def contingent_ssh_options_for_node(node)
|
99
|
+
opts = {}
|
100
|
+
if node.vagrant?
|
101
|
+
opts[:keys] = [vagrant_ssh_key_file]
|
102
|
+
opts[:paranoid] = false # we skip host checking for vagrant nodes, because fingerprint is different for everyone.
|
103
|
+
if LeapCli::log_level <= 1
|
104
|
+
opts[:verbose] = :error # suppress all the warnings about adding host keys to known_hosts, since it is not actually doing that.
|
105
|
+
end
|
106
|
+
end
|
107
|
+
return opts
|
108
|
+
end
|
109
|
+
|
110
|
+
end; end; end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
#
|
2
|
+
# A simple secret generator
|
3
|
+
#
|
4
|
+
# Uses OpenSSL random number generator instead of Ruby's rand function
|
5
|
+
#
|
6
|
+
require 'openssl'
|
7
|
+
|
8
|
+
module LeapCli; module Util
|
9
|
+
class Secret
|
10
|
+
CHARS = ('A'..'Z').to_a + ('a'..'z').to_a + ('0'..'9').to_a + "_".split(//u) - "io01lO".split(//u)
|
11
|
+
HEX = (0..9).to_a + ('a'..'f').to_a
|
12
|
+
|
13
|
+
#
|
14
|
+
# generate a secret with with no ambiguous characters.
|
15
|
+
#
|
16
|
+
# +length+ is in chars
|
17
|
+
#
|
18
|
+
# Only alphanumerics are allowed, in order to make these passwords work
|
19
|
+
# for REST url calls and to allow you to easily copy and paste them.
|
20
|
+
#
|
21
|
+
def self.generate(length = 16)
|
22
|
+
seed
|
23
|
+
OpenSSL::Random.random_bytes(length).bytes.to_a.collect { |byte|
|
24
|
+
CHARS[ byte % CHARS.length ]
|
25
|
+
}.join
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# generates a hex secret, instead of an alphanumeric on.
|
30
|
+
#
|
31
|
+
# length is in bits
|
32
|
+
#
|
33
|
+
def self.generate_hex(length = 128)
|
34
|
+
seed
|
35
|
+
OpenSSL::Random.random_bytes(length/4).bytes.to_a.collect { |byte|
|
36
|
+
HEX[ byte % HEX.length ]
|
37
|
+
}.join
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def self.seed
|
43
|
+
@pid ||= 0
|
44
|
+
pid = $$
|
45
|
+
if @pid != pid
|
46
|
+
now = Time.now
|
47
|
+
ary = [now.to_i, now.nsec, @pid, pid]
|
48
|
+
OpenSSL::Random.seed(ary.to_s)
|
49
|
+
@pid = pid
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end; end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'certificate_authority'
|
3
|
+
require 'digest'
|
4
|
+
require 'digest/md5'
|
5
|
+
require 'digest/sha1'
|
6
|
+
|
7
|
+
module LeapCli; module X509
|
8
|
+
extend self
|
9
|
+
|
10
|
+
#
|
11
|
+
# returns a fingerprint of a x509 certificate
|
12
|
+
#
|
13
|
+
def fingerprint(digest, cert_file)
|
14
|
+
if cert_file.is_a? String
|
15
|
+
cert = OpenSSL::X509::Certificate.new(Util.read_file!(cert_file))
|
16
|
+
elsif cert_file.is_a? OpenSSL::X509::Certificate
|
17
|
+
cert = cert_file
|
18
|
+
elsif cert_file.is_a? CertificateAuthority::Certificate
|
19
|
+
cert = cert_file.openssl_body
|
20
|
+
end
|
21
|
+
digester = case digest
|
22
|
+
when "MD5" then Digest::MD5.new
|
23
|
+
when "SHA1" then Digest::SHA1.new
|
24
|
+
when "SHA256" then Digest::SHA256.new
|
25
|
+
when "SHA384" then Digest::SHA384.new
|
26
|
+
when "SHA512" then Digest::SHA512.new
|
27
|
+
end
|
28
|
+
digester.hexdigest(cert.to_der)
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
end; end
|