leap_cli 1.5.6 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/bin/leap +29 -6
- data/lib/leap/platform.rb +36 -1
- data/lib/leap_cli/commands/ca.rb +97 -20
- data/lib/leap_cli/commands/compile.rb +49 -8
- data/lib/leap_cli/commands/db.rb +13 -4
- data/lib/leap_cli/commands/deploy.rb +138 -29
- data/lib/leap_cli/commands/env.rb +76 -0
- data/lib/leap_cli/commands/facts.rb +10 -3
- data/lib/leap_cli/commands/inspect.rb +2 -2
- data/lib/leap_cli/commands/list.rb +10 -10
- data/lib/leap_cli/commands/node.rb +7 -132
- data/lib/leap_cli/commands/node_init.rb +169 -0
- data/lib/leap_cli/commands/pre.rb +4 -27
- data/lib/leap_cli/commands/ssh.rb +152 -0
- data/lib/leap_cli/commands/test.rb +22 -13
- data/lib/leap_cli/commands/user.rb +12 -4
- data/lib/leap_cli/commands/vagrant.rb +4 -4
- data/lib/leap_cli/config/filter.rb +175 -0
- data/lib/leap_cli/config/manager.rb +130 -61
- data/lib/leap_cli/config/node.rb +32 -0
- data/lib/leap_cli/config/object.rb +69 -44
- data/lib/leap_cli/config/object_list.rb +44 -39
- data/lib/leap_cli/config/secrets.rb +24 -12
- data/lib/leap_cli/config/tag.rb +7 -0
- data/lib/{core_ext → leap_cli/core_ext}/boolean.rb +0 -0
- data/lib/{core_ext → leap_cli/core_ext}/hash.rb +0 -0
- data/lib/{core_ext → leap_cli/core_ext}/json.rb +0 -0
- data/lib/{core_ext → leap_cli/core_ext}/nil.rb +0 -0
- data/lib/{core_ext → leap_cli/core_ext}/string.rb +0 -0
- data/lib/leap_cli/core_ext/yaml.rb +29 -0
- data/lib/leap_cli/exceptions.rb +24 -0
- data/lib/leap_cli/leapfile.rb +60 -10
- data/lib/{lib_ext → leap_cli/lib_ext}/capistrano_connections.rb +0 -0
- data/lib/{lib_ext → leap_cli/lib_ext}/gli.rb +0 -0
- data/lib/leap_cli/log.rb +1 -1
- data/lib/leap_cli/logger.rb +18 -1
- data/lib/leap_cli/markdown_document_listener.rb +1 -1
- data/lib/leap_cli/override/json.rb +11 -0
- data/lib/leap_cli/path.rb +20 -6
- data/lib/leap_cli/remote/leap_plugin.rb +2 -2
- data/lib/leap_cli/remote/puppet_plugin.rb +1 -1
- data/lib/leap_cli/remote/rsync_plugin.rb +1 -1
- data/lib/leap_cli/remote/tasks.rb +1 -1
- data/lib/leap_cli/ssh_key.rb +63 -1
- data/lib/leap_cli/util/remote_command.rb +19 -2
- data/lib/leap_cli/util/secret.rb +1 -1
- data/lib/leap_cli/util/x509.rb +3 -2
- data/lib/leap_cli/util.rb +11 -3
- data/lib/leap_cli/version.rb +2 -2
- data/lib/leap_cli.rb +24 -14
- data/vendor/certificate_authority/lib/certificate_authority/certificate.rb +85 -29
- data/vendor/certificate_authority/lib/certificate_authority/distinguished_name.rb +5 -0
- data/vendor/certificate_authority/lib/certificate_authority/extensions.rb +406 -41
- data/vendor/certificate_authority/lib/certificate_authority/key_material.rb +0 -34
- data/vendor/certificate_authority/lib/certificate_authority/serial_number.rb +6 -0
- data/vendor/certificate_authority/lib/certificate_authority/signing_request.rb +36 -1
- metadata +25 -24
- data/lib/leap_cli/commands/shell.rb +0 -89
- data/lib/leap_cli/config/macros.rb +0 -430
- data/lib/leap_cli/constants.rb +0 -7
- data/lib/leap_cli/requirements.rb +0 -19
- data/lib/lib_ext/markdown_document_listener.rb +0 -122
@@ -0,0 +1,169 @@
|
|
1
|
+
#
|
2
|
+
# Node initialization.
|
3
|
+
# Most of the fun stuff is in tasks.rb.
|
4
|
+
#
|
5
|
+
|
6
|
+
module LeapCli; module Commands
|
7
|
+
|
8
|
+
desc 'Node management'
|
9
|
+
command :node do |node|
|
10
|
+
node.desc 'Bootstraps a node or nodes, setting up SSH keys and installing prerequisite packages'
|
11
|
+
node.long_desc "This command prepares a server to be used with the LEAP Platform by saving the server's SSH host key, " +
|
12
|
+
"copying the authorized_keys file, installing packages that are required for deploying, and registering important facts. " +
|
13
|
+
"Node init must be run before deploying to a server, and the server must be running and available via the network. " +
|
14
|
+
"This command only needs to be run once, but there is no harm in running it multiple times."
|
15
|
+
node.arg_name 'FILTER'
|
16
|
+
node.command :init do |init|
|
17
|
+
init.switch 'echo', :desc => 'If set, passwords are visible as you type them (default is hidden)', :negatable => false
|
18
|
+
init.flag :port, :desc => 'Override the default SSH port.', :arg_name => 'PORT'
|
19
|
+
init.flag :ip, :desc => 'Override the default SSH IP address.', :arg_name => 'IPADDRESS'
|
20
|
+
|
21
|
+
init.action do |global,options,args|
|
22
|
+
assert! args.any?, 'You must specify a FILTER'
|
23
|
+
finished = []
|
24
|
+
manager.filter!(args).each_node do |node|
|
25
|
+
is_node_alive(node, options)
|
26
|
+
save_public_host_key(node, global, options) unless node.vagrant?
|
27
|
+
update_compiled_ssh_configs
|
28
|
+
ssh_connect_options = connect_options(options).merge({:bootstrap => true, :echo => options[:echo]})
|
29
|
+
ssh_connect(node, ssh_connect_options) do |ssh|
|
30
|
+
if node.vagrant?
|
31
|
+
ssh.install_insecure_vagrant_key
|
32
|
+
end
|
33
|
+
ssh.install_authorized_keys
|
34
|
+
ssh.install_prerequisites
|
35
|
+
unless node.vagrant?
|
36
|
+
ssh.leap.log(:checking, "SSH host keys") do
|
37
|
+
ssh.leap.capture(get_ssh_keys_cmd) do |response|
|
38
|
+
update_local_ssh_host_keys(node, response[:data]) if response[:exitcode] == 0
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
ssh.leap.log(:updating, "facts") do
|
43
|
+
ssh.leap.capture(facter_cmd) do |response|
|
44
|
+
if response[:exitcode] == 0
|
45
|
+
update_node_facts(node.name, response[:data])
|
46
|
+
else
|
47
|
+
log :failed, "to run facter on #{node.name}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
finished << node.name
|
53
|
+
end
|
54
|
+
log :completed, "initialization of nodes #{finished.join(', ')}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
##
|
62
|
+
## PRIVATE HELPERS
|
63
|
+
##
|
64
|
+
|
65
|
+
def is_node_alive(node, options)
|
66
|
+
address = options[:ip] || node.ip_address
|
67
|
+
port = options[:port] || node.ssh.port
|
68
|
+
log :connecting, "to node #{node.name}"
|
69
|
+
assert_run! "nc -zw3 #{address} #{port}",
|
70
|
+
"Failed to reach #{node.name} (address #{address}, port #{port}). You can override the configured IP address and port with --ip or --port."
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# saves the public ssh host key for node into the provider directory.
|
75
|
+
#
|
76
|
+
# see `man sshd` for the format of known_hosts
|
77
|
+
#
|
78
|
+
def save_public_host_key(node, global, options)
|
79
|
+
log :fetching, "public SSH host key for #{node.name}"
|
80
|
+
address = options[:ip] || node.ip_address
|
81
|
+
port = options[:port] || node.ssh.port
|
82
|
+
host_keys = get_public_keys_for_ip(address, port)
|
83
|
+
pub_key_path = Path.named_path([:node_ssh_pub_key, node.name])
|
84
|
+
|
85
|
+
if Path.exists?(pub_key_path)
|
86
|
+
if host_keys.include? SshKey.load(pub_key_path)
|
87
|
+
log :trusted, "- Public SSH host key for #{node.name} matches previously saved key", :indent => 1
|
88
|
+
else
|
89
|
+
bail! do
|
90
|
+
log :error, "The public SSH host keys we just fetched for #{node.name} doesn't match what we have saved previously.", :indent => 1
|
91
|
+
log "Delete the file #{pub_key_path} if you really want to remove the trusted SSH host key.", :indent => 2
|
92
|
+
end
|
93
|
+
end
|
94
|
+
else
|
95
|
+
known_key = host_keys.detect{|k|k.in_known_hosts?(node.name, node.ip_address, node.domain.name)}
|
96
|
+
if known_key
|
97
|
+
log :trusted, "- Public SSH host key for #{node.name} is trusted (key found in your ~/.ssh/known_hosts)"
|
98
|
+
else
|
99
|
+
public_key = SshKey.pick_best_key(host_keys)
|
100
|
+
if public_key.nil?
|
101
|
+
bail!("We got back #{host_keys.size} host keys from #{node.name}, but we can't support any of them.")
|
102
|
+
else
|
103
|
+
say(" This is the SSH host key you got back from node \"#{node.name}\"")
|
104
|
+
say(" Type -- #{public_key.bits} bit #{public_key.type.upcase}")
|
105
|
+
say(" Fingerprint -- " + public_key.fingerprint)
|
106
|
+
say(" Public Key -- " + public_key.key)
|
107
|
+
if !global[:yes] && !agree(" Is this correct? ")
|
108
|
+
bail!
|
109
|
+
else
|
110
|
+
known_key = public_key
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
puts
|
115
|
+
write_file! [:node_ssh_pub_key, node.name], known_key.to_s
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
#
|
120
|
+
# Get the public host keys for a host using ssh-keyscan.
|
121
|
+
# Return an array of SshKey objects, one for each key.
|
122
|
+
#
|
123
|
+
def get_public_keys_for_ip(address, port=22)
|
124
|
+
assert_bin!('ssh-keyscan')
|
125
|
+
output = assert_run! "ssh-keyscan -p #{port} #{address}", "Could not get the public host key from #{address}:#{port}. Maybe sshd is not running?"
|
126
|
+
if output.empty?
|
127
|
+
bail! :failed, "ssh-keyscan returned empty output."
|
128
|
+
end
|
129
|
+
|
130
|
+
if output =~ /No route to host/
|
131
|
+
bail! :failed, 'ssh-keyscan: no route to %s' % address
|
132
|
+
else
|
133
|
+
keys = SshKey.parse_keys(output)
|
134
|
+
if keys.empty?
|
135
|
+
bail! "ssh-keyscan got zero host keys back (that we understand)! Output was: #{output}"
|
136
|
+
else
|
137
|
+
return keys
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# run on the server to generate a string suitable for passing to SshKey.parse_keys()
|
143
|
+
def get_ssh_keys_cmd
|
144
|
+
"/bin/grep ^HostKey /etc/ssh/sshd_config | /usr/bin/awk '{print $2 \".pub\"}' | /usr/bin/xargs /bin/cat"
|
145
|
+
end
|
146
|
+
|
147
|
+
#
|
148
|
+
# Sometimes the ssh host keys on the server will be better than what we have
|
149
|
+
# stored locally. In these cases, ask the user if they want to upgrade.
|
150
|
+
#
|
151
|
+
def update_local_ssh_host_keys(node, remote_keys_string)
|
152
|
+
remote_keys = SshKey.parse_keys(remote_keys_string)
|
153
|
+
return unless remote_keys.any?
|
154
|
+
current_key = SshKey.load(Path.named_path([:node_ssh_pub_key, node.name]))
|
155
|
+
best_key = SshKey.pick_best_key(remote_keys)
|
156
|
+
return unless best_key && current_key
|
157
|
+
if current_key != best_key
|
158
|
+
say(" One of the SSH host keys for node '#{node.name}' is better than what you currently have trusted.")
|
159
|
+
say(" Current key: #{current_key.summary}")
|
160
|
+
say(" Better key: #{best_key.summary}")
|
161
|
+
if agree(" Do you want to use the better key? ")
|
162
|
+
write_file! [:node_ssh_pub_key, node.name], best_key.to_s
|
163
|
+
end
|
164
|
+
else
|
165
|
+
log(3, "current host key does not need updating")
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
end; end
|
@@ -20,8 +20,8 @@ module LeapCli; module Commands
|
|
20
20
|
desc 'Skip prompts and assume "yes"'
|
21
21
|
switch :yes, :negatable => false
|
22
22
|
|
23
|
-
desc '
|
24
|
-
switch :debug, :negatable => false
|
23
|
+
desc 'Print full stack trace for exceptions and load `debugger` gem if installed.'
|
24
|
+
switch [:d, :debug], :negatable => false
|
25
25
|
|
26
26
|
desc 'Disable colors in output'
|
27
27
|
default_value true
|
@@ -31,12 +31,7 @@ module LeapCli; module Commands
|
|
31
31
|
#
|
32
32
|
# set verbosity
|
33
33
|
#
|
34
|
-
LeapCli.
|
35
|
-
if LeapCli.log_level > 1
|
36
|
-
ENV['GLI_DEBUG'] = "true"
|
37
|
-
else
|
38
|
-
ENV['GLI_DEBUG'] = "false"
|
39
|
-
end
|
34
|
+
LeapCli.set_log_level(global[:verbose].to_i)
|
40
35
|
|
41
36
|
#
|
42
37
|
# load Leapfile
|
@@ -53,13 +48,6 @@ module LeapCli; module Commands
|
|
53
48
|
bail! { log :missing, "platform directory '#{Path.platform}'" }
|
54
49
|
end
|
55
50
|
|
56
|
-
if LeapCli.leapfile.platform_branch && LeapCli::Util.is_git_directory?(Path.platform)
|
57
|
-
branch = LeapCli::Util.current_git_branch(Path.platform)
|
58
|
-
if branch != LeapCli.leapfile.platform_branch
|
59
|
-
bail! "Wrong branch for #{Path.platform}. Was '#{branch}', should be '#{LeapCli.leapfile.platform_branch}'. Edit Leapfile to disable this check."
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
51
|
#
|
64
52
|
# set log file
|
65
53
|
#
|
@@ -68,18 +56,7 @@ module LeapCli; module Commands
|
|
68
56
|
log_version
|
69
57
|
LeapCli.log_in_color = global[:color]
|
70
58
|
|
71
|
-
|
72
|
-
# load all the nodes everything
|
73
|
-
#
|
74
|
-
manager
|
75
|
-
|
76
|
-
#
|
77
|
-
# check requirements
|
78
|
-
#
|
79
|
-
REQUIREMENTS.each do |key|
|
80
|
-
assert_config! key
|
81
|
-
end
|
82
|
-
|
59
|
+
true
|
83
60
|
end
|
84
61
|
|
85
62
|
private
|
@@ -0,0 +1,152 @@
|
|
1
|
+
module LeapCli; module Commands
|
2
|
+
|
3
|
+
desc 'Log in to the specified node with an interactive shell.'
|
4
|
+
arg_name 'NAME' #, :optional => false, :multiple => false
|
5
|
+
command :ssh do |c|
|
6
|
+
c.flag 'ssh', :desc => "Pass through raw options to ssh (e.g. `--ssh '-F ~/sshconfig'`)."
|
7
|
+
c.flag 'port', :arg_name => 'SSH_PORT', :desc => 'Override default SSH port used when trying to connect to the server. Same as `--ssh "-p SSH_PORT"`.'
|
8
|
+
c.action do |global_options,options,args|
|
9
|
+
exec_ssh(:ssh, options, args)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
desc 'Log in to the specified node with an interactive shell using mosh (requires node to have mosh.enabled set to true).'
|
14
|
+
arg_name 'NAME'
|
15
|
+
command :mosh do |c|
|
16
|
+
c.flag 'ssh', :desc => "Pass through raw options to ssh (e.g. `--ssh '-F ~/sshconfig'`)."
|
17
|
+
c.flag 'port', :arg_name => 'SSH_PORT', :desc => 'Override default SSH port used when trying to connect to the server. Same as `--ssh "-p SSH_PORT"`.'
|
18
|
+
c.action do |global_options,options,args|
|
19
|
+
exec_ssh(:mosh, options, args)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
desc 'Creates an SSH port forward (tunnel) to the node NAME. REMOTE_PORT is the port on the remote node that the tunnel will connect to. LOCAL_PORT is the optional port on your local machine. For example: `leap tunnel couch1:5984`.'
|
24
|
+
arg_name '[LOCAL_PORT:]NAME:REMOTE_PORT'
|
25
|
+
command :tunnel do |c|
|
26
|
+
c.flag 'ssh', :desc => "Pass through raw options to ssh (e.g. --ssh '-F ~/sshconfig')."
|
27
|
+
c.flag 'port', :arg_name => 'SSH_PORT', :desc => 'Override default SSH port used when trying to connect to the server. Same as `--ssh "-p SSH_PORT"`.'
|
28
|
+
c.action do |global_options,options,args|
|
29
|
+
local_port, node, remote_port = parse_tunnel_arg(args.first)
|
30
|
+
options[:ssh] = [options[:ssh], "-N -L 127.0.0.1:#{local_port}:0.0.0.0:#{remote_port}"].join(' ')
|
31
|
+
log("Forward port localhost:#{local_port} to #{node.name}:#{remote_port}")
|
32
|
+
if is_port_available?(local_port)
|
33
|
+
exec_ssh(:ssh, options, [node.name])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
#
|
41
|
+
# allow for ssh overrides of all commands that use ssh_connect
|
42
|
+
#
|
43
|
+
def connect_options(options)
|
44
|
+
connect_options = {:ssh_options=>{}}
|
45
|
+
if options[:port]
|
46
|
+
connect_options[:ssh_options][:port] = options[:port]
|
47
|
+
end
|
48
|
+
if options[:ip]
|
49
|
+
connect_options[:ssh_options][:host_name] = options[:ip]
|
50
|
+
end
|
51
|
+
return connect_options
|
52
|
+
end
|
53
|
+
|
54
|
+
def ssh_config_help_message
|
55
|
+
puts ""
|
56
|
+
puts "Are 'too many authentication failures' getting you down?"
|
57
|
+
puts "Then we have the solution for you! Add something like this to your ~/.ssh/config file:"
|
58
|
+
puts " Host *.#{manager.provider.domain}"
|
59
|
+
puts " IdentityFile ~/.ssh/id_rsa"
|
60
|
+
puts " IdentitiesOnly=yes"
|
61
|
+
puts "(replace `id_rsa` with the actual private key filename that you use for this provider)"
|
62
|
+
end
|
63
|
+
|
64
|
+
require 'socket'
|
65
|
+
def is_port_available?(port)
|
66
|
+
TCPServer.open('127.0.0.1', port) {}
|
67
|
+
true
|
68
|
+
rescue Errno::EACCES
|
69
|
+
bail!("You don't have permission to bind to port #{port}.")
|
70
|
+
rescue Errno::EADDRINUSE
|
71
|
+
bail!("Local port #{port} is already in use. Specify LOCAL_PORT to pick another.")
|
72
|
+
rescue Exception => exc
|
73
|
+
bail!(exc.to_s)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def exec_ssh(cmd, cli_options, args)
|
79
|
+
node = get_node_from_args(args, :include_disabled => true)
|
80
|
+
port = node.ssh.port
|
81
|
+
options = [
|
82
|
+
"-o 'HostName=#{node.ip_address}'",
|
83
|
+
# "-o 'HostKeyAlias=#{node.name}'", << oddly incompatible with ports in known_hosts file, so we must not use this or non-standard ports break.
|
84
|
+
"-o 'GlobalKnownHostsFile=#{path(:known_hosts)}'",
|
85
|
+
"-o 'UserKnownHostsFile=/dev/null'"
|
86
|
+
]
|
87
|
+
if node.vagrant?
|
88
|
+
options << "-i #{vagrant_ssh_key_file}" # use the universal vagrant insecure key
|
89
|
+
options << "-o IdentitiesOnly=yes" # force the use of the insecure vagrant key
|
90
|
+
options << "-o 'StrictHostKeyChecking=no'" # blindly accept host key and don't save it (since userknownhostsfile is /dev/null)
|
91
|
+
else
|
92
|
+
options << "-o 'StrictHostKeyChecking=yes'"
|
93
|
+
end
|
94
|
+
if !node.supported_ssh_host_key_algorithms.empty?
|
95
|
+
options << "-o 'HostKeyAlgorithms=#{node.supported_ssh_host_key_algorithms}'"
|
96
|
+
end
|
97
|
+
username = 'root'
|
98
|
+
if LeapCli.log_level >= 3
|
99
|
+
options << "-vv"
|
100
|
+
elsif LeapCli.log_level >= 2
|
101
|
+
options << "-v"
|
102
|
+
end
|
103
|
+
if cli_options[:port]
|
104
|
+
port = cli_options[:port]
|
105
|
+
end
|
106
|
+
if cli_options[:ssh]
|
107
|
+
options << cli_options[:ssh]
|
108
|
+
end
|
109
|
+
ssh = "ssh -l #{username} -p #{port} #{options.join(' ')}"
|
110
|
+
if cmd == :ssh
|
111
|
+
command = "#{ssh} #{node.domain.full}"
|
112
|
+
elsif cmd == :mosh
|
113
|
+
command = "MOSH_TITLE_NOPREFIX=1 mosh --ssh \"#{ssh}\" #{node.domain.full}"
|
114
|
+
end
|
115
|
+
log 2, command
|
116
|
+
|
117
|
+
# exec the shell command in a subprocess
|
118
|
+
pid = fork { exec "#{command}" }
|
119
|
+
|
120
|
+
Signal.trap("SIGINT") do
|
121
|
+
Process.kill("KILL", pid)
|
122
|
+
Process.wait(pid)
|
123
|
+
exit(0)
|
124
|
+
end
|
125
|
+
|
126
|
+
# wait for shell to exit so we can grab the exit status
|
127
|
+
_, status = Process.waitpid2(pid)
|
128
|
+
|
129
|
+
if status.exitstatus == 255
|
130
|
+
ssh_config_help_message
|
131
|
+
elsif status.exitstatus != 0
|
132
|
+
exit(status.exitstatus)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def parse_tunnel_arg(arg)
|
137
|
+
if arg.count(':') == 1
|
138
|
+
node_name, remote = arg.split(':')
|
139
|
+
local = nil
|
140
|
+
elsif arg.count(':') == 2
|
141
|
+
local, node_name, remote = arg.split(':')
|
142
|
+
else
|
143
|
+
bail!('Argument NAME:REMOTE_PORT required.')
|
144
|
+
end
|
145
|
+
node = get_node_from_args([node_name], :include_disabled => true)
|
146
|
+
remote = remote.to_i
|
147
|
+
local = local || remote
|
148
|
+
local = local.to_i
|
149
|
+
return [local, node, remote]
|
150
|
+
end
|
151
|
+
|
152
|
+
end; end
|
@@ -1,15 +1,9 @@
|
|
1
1
|
module LeapCli; module Commands
|
2
2
|
|
3
3
|
desc 'Run tests.'
|
4
|
-
command :test do |test|
|
5
|
-
test.desc '
|
6
|
-
test.
|
7
|
-
init.action do |global_options,options,args|
|
8
|
-
generate_test_client_openvpn_configs
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
test.desc 'Run tests.'
|
4
|
+
command [:test, :t] do |test|
|
5
|
+
test.desc 'Run the test suit on FILTER nodes.'
|
6
|
+
test.arg_name 'FILTER', :optional => true
|
13
7
|
test.command :run do |run|
|
14
8
|
run.switch 'continue', :desc => 'Continue over errors and failures (default is --no-continue).', :negatable => true
|
15
9
|
run.action do |global_options,options,args|
|
@@ -19,13 +13,28 @@ module LeapCli; module Commands
|
|
19
13
|
end
|
20
14
|
manager.filter!(args).names_in_test_dependency_order.each do |node_name|
|
21
15
|
node = manager.nodes[node_name]
|
22
|
-
|
23
|
-
|
16
|
+
begin
|
17
|
+
ssh_connect(node) do |ssh|
|
18
|
+
ssh.run(test_cmd(options))
|
19
|
+
end
|
20
|
+
rescue Capistrano::CommandError => exc
|
21
|
+
if options[:continue]
|
22
|
+
exit_status(1)
|
23
|
+
else
|
24
|
+
bail!
|
25
|
+
end
|
24
26
|
end
|
25
27
|
end
|
26
28
|
end
|
27
29
|
end
|
28
30
|
|
31
|
+
test.desc 'Creates files needed to run tests.'
|
32
|
+
test.command :init do |init|
|
33
|
+
init.action do |global_options,options,args|
|
34
|
+
generate_test_client_openvpn_configs
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
29
38
|
test.default_command :run
|
30
39
|
end
|
31
40
|
|
@@ -33,9 +42,9 @@ module LeapCli; module Commands
|
|
33
42
|
|
34
43
|
def test_cmd(options)
|
35
44
|
if options[:continue]
|
36
|
-
"#{
|
45
|
+
"#{Leap::Platform.leap_dir}/bin/run_tests --continue"
|
37
46
|
else
|
38
|
-
"#{
|
47
|
+
"#{Leap::Platform.leap_dir}/bin/run_tests"
|
39
48
|
end
|
40
49
|
end
|
41
50
|
|
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'gpgme'
|
2
1
|
|
3
2
|
#
|
4
3
|
# perhaps we want to verify that the key files are actually the key files we expect.
|
@@ -75,8 +74,10 @@ module LeapCli
|
|
75
74
|
if `which ssh-add`.strip.any?
|
76
75
|
`ssh-add -L 2> /dev/null`.split("\n").compact.each do |line|
|
77
76
|
key = SshKey.load(line)
|
78
|
-
key
|
79
|
-
|
77
|
+
if key
|
78
|
+
key.comment = 'ssh-agent'
|
79
|
+
ssh_keys << key unless ssh_keys.include?(key)
|
80
|
+
end
|
80
81
|
end
|
81
82
|
end
|
82
83
|
ssh_keys.compact!
|
@@ -98,13 +99,20 @@ module LeapCli
|
|
98
99
|
# let the the user choose among the gpg public keys that we encounter, or just pick the key if there is only one.
|
99
100
|
#
|
100
101
|
def pick_pgp_key
|
102
|
+
begin
|
103
|
+
return unless `which gpg`.strip.any?
|
104
|
+
require 'gpgme'
|
105
|
+
rescue LoadError
|
106
|
+
return
|
107
|
+
end
|
108
|
+
|
101
109
|
secret_keys = GPGME::Key.find(:secret)
|
102
110
|
if secret_keys.empty?
|
103
111
|
log "Skipping OpenPGP setup because I could not find any OpenPGP keys for you"
|
104
112
|
return nil
|
105
113
|
end
|
106
114
|
|
107
|
-
|
115
|
+
secret_keys.select!{|key| !key.expired}
|
108
116
|
|
109
117
|
if secret_keys.length > 1
|
110
118
|
key_index = numbered_choice_menu('Choose your OpenPGP public key', secret_keys) do |key, i|
|
@@ -1,11 +1,11 @@
|
|
1
|
-
|
1
|
+
autoload :IPAddr, 'ipaddr'
|
2
2
|
require 'fileutils'
|
3
3
|
|
4
4
|
module LeapCli; module Commands
|
5
5
|
|
6
6
|
desc "Manage local virtual machines."
|
7
7
|
long_desc "This command provides a convient way to manage Vagrant-based virtual machines. If FILTER argument is missing, the command runs on all local virtual machines. The Vagrantfile is automatically generated in 'test/Vagrantfile'. If you want to run vagrant commands manually, cd to 'test'."
|
8
|
-
command :local do |local|
|
8
|
+
command [:local, :l] do |local|
|
9
9
|
local.desc 'Starts up the virtual machine(s)'
|
10
10
|
local.arg_name 'FILTER', :optional => true #, :multiple => false
|
11
11
|
local.command :start do |start|
|
@@ -156,7 +156,7 @@ module LeapCli; module Commands
|
|
156
156
|
if node.vagrant?
|
157
157
|
lines << %[ config.vm.define :#{node.name} do |config|]
|
158
158
|
lines << %[ config.vm.box = "leap-wheezy"]
|
159
|
-
lines << %[ config.vm.box_url = "https://downloads.leap.se/platform/leap-
|
159
|
+
lines << %[ config.vm.box_url = "https://downloads.leap.se/platform/vagrant/virtualbox/leap-wheezy.box"]
|
160
160
|
lines << %[ config.vm.network :hostonly, "#{node.ip_address}", :netmask => "#{netmask}"]
|
161
161
|
lines << %[ config.vm.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]]
|
162
162
|
lines << %[ config.vm.customize ["modifyvm", :id, "--name", "#{node.name}"]]
|
@@ -170,7 +170,7 @@ module LeapCli; module Commands
|
|
170
170
|
if node.vagrant?
|
171
171
|
lines << %[ config.vm.define :#{node.name} do |config|]
|
172
172
|
lines << %[ config.vm.box = "leap-wheezy"]
|
173
|
-
lines << %[ config.vm.box_url = "https://downloads.leap.se/platform/leap-
|
173
|
+
lines << %[ config.vm.box_url = "https://downloads.leap.se/platform/vagrant/virtualbox/leap-wheezy.box"]
|
174
174
|
lines << %[ config.vm.network :private_network, ip: "#{node.ip_address}"]
|
175
175
|
lines << %[ config.vm.provider "virtualbox" do |v|]
|
176
176
|
lines << %[ v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]]
|
@@ -0,0 +1,175 @@
|
|
1
|
+
#
|
2
|
+
# Many leap_cli commands accept a list of filters to select a subset of nodes for the command to
|
3
|
+
# be applied to. This class is a helper for manager to run these filters.
|
4
|
+
#
|
5
|
+
# Classes other than Manager should not use this class.
|
6
|
+
#
|
7
|
+
# Filter rules:
|
8
|
+
#
|
9
|
+
# * A filter consists of a list of tokens
|
10
|
+
# * A token may be a service name, tag name, environment name, or node name.
|
11
|
+
# * Each token may be optionally prefixed with a plus sign.
|
12
|
+
# * Multiple tokens with a plus are treated as an OR condition,
|
13
|
+
# but treated as an AND condition with the plus sign.
|
14
|
+
#
|
15
|
+
# For example
|
16
|
+
#
|
17
|
+
# * openvpn +development => all nodes with service 'openvpn' AND environment 'development'
|
18
|
+
# * openvpn seattle => all nodes with service 'openvpn' OR tag 'seattle'.
|
19
|
+
#
|
20
|
+
# There can only be one environment specified. Typically, there are also tags
|
21
|
+
# for each environment name. These name are treated as environments, not tags.
|
22
|
+
#
|
23
|
+
module LeapCli
|
24
|
+
module Config
|
25
|
+
class Filter
|
26
|
+
|
27
|
+
#
|
28
|
+
# filter -- array of strings, each one a filter
|
29
|
+
# options -- hash, possible keys include
|
30
|
+
# :nopin -- disregard environment pinning
|
31
|
+
# :local -- if false, disallow local nodes
|
32
|
+
#
|
33
|
+
# A nil value in the filters array indicates
|
34
|
+
# the default environment. This is in order to support
|
35
|
+
# calls like `manager.filter(environments)`
|
36
|
+
#
|
37
|
+
def initialize(filters, options, manager)
|
38
|
+
@filters = filters.nil? ? [] : filters.dup
|
39
|
+
@environments = []
|
40
|
+
@options = options
|
41
|
+
@manager = manager
|
42
|
+
|
43
|
+
# split filters by pulling out items that happen
|
44
|
+
# to be environment names.
|
45
|
+
if LeapCli.leapfile.environment.nil? || @options[:nopin]
|
46
|
+
@environments = []
|
47
|
+
else
|
48
|
+
@environments = [LeapCli.leapfile.environment]
|
49
|
+
end
|
50
|
+
@filters.select! do |filter|
|
51
|
+
if filter.nil?
|
52
|
+
@environments << nil unless @environments.include?(nil)
|
53
|
+
false
|
54
|
+
else
|
55
|
+
filter_text = filter.sub(/^\+/,'')
|
56
|
+
if is_environment?(filter_text)
|
57
|
+
if filter_text == LeapCli.leapfile.environment
|
58
|
+
# silently ignore already pinned environments
|
59
|
+
elsif (filter =~ /^\+/ || @filters.first == filter) && !@environments.empty?
|
60
|
+
LeapCli::Util.bail! do
|
61
|
+
LeapCli::Util.log "Environments are exclusive: no node is in two environments." do
|
62
|
+
LeapCli::Util.log "Tried to filter on '#{@environments.join('\' AND \'')}' AND '#{filter_text}'"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
else
|
66
|
+
@environments << filter_text
|
67
|
+
end
|
68
|
+
false
|
69
|
+
else
|
70
|
+
true
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# don't let the first filter have a + prefix
|
76
|
+
if @filters[0] =~ /^\+/
|
77
|
+
@filters[0] = @filters[0][1..-1]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# actually run the filter, returns a filtered list of nodes
|
82
|
+
def nodes()
|
83
|
+
if @filters.empty?
|
84
|
+
return nodes_for_empty_filter
|
85
|
+
else
|
86
|
+
return nodes_for_filter
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def nodes_for_empty_filter
|
93
|
+
node_list = @manager.nodes
|
94
|
+
if @environments.any?
|
95
|
+
node_list = node_list[ @environments.collect{|e|[:environment, env_to_filter(e)]} ]
|
96
|
+
end
|
97
|
+
if @options[:local] === false
|
98
|
+
node_list = node_list[:environment => '!local']
|
99
|
+
end
|
100
|
+
node_list
|
101
|
+
end
|
102
|
+
|
103
|
+
def nodes_for_filter
|
104
|
+
node_list = Config::ObjectList.new
|
105
|
+
@filters.each do |filter|
|
106
|
+
if filter =~ /^\+/
|
107
|
+
keep_list = nodes_for_name(filter[1..-1])
|
108
|
+
node_list.delete_if do |name, node|
|
109
|
+
if keep_list[name]
|
110
|
+
false
|
111
|
+
else
|
112
|
+
true
|
113
|
+
end
|
114
|
+
end
|
115
|
+
else
|
116
|
+
node_list.merge!(nodes_for_name(filter))
|
117
|
+
end
|
118
|
+
end
|
119
|
+
node_list
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
#
|
125
|
+
# returns a set of nodes corresponding to a single name,
|
126
|
+
# where name could be a node name, service name, or tag name.
|
127
|
+
#
|
128
|
+
# For services and tags, we only include nodes for the
|
129
|
+
# environments that are active
|
130
|
+
#
|
131
|
+
def nodes_for_name(name)
|
132
|
+
if node = @manager.nodes[name]
|
133
|
+
return Config::ObjectList.new(node)
|
134
|
+
elsif @environments.empty?
|
135
|
+
if @manager.services[name]
|
136
|
+
return @manager.env('_all_').services[name].node_list
|
137
|
+
elsif @manager.tags[name]
|
138
|
+
return @manager.env('_all_').tags[name].node_list
|
139
|
+
else
|
140
|
+
LeapCli::Util.log :warning, "filter '#{name}' does not match any node names, tags, services, or environments."
|
141
|
+
return Config::ObjectList.new
|
142
|
+
end
|
143
|
+
else
|
144
|
+
node_list = Config::ObjectList.new
|
145
|
+
if @manager.services[name]
|
146
|
+
@environments.each do |env|
|
147
|
+
node_list.merge!(@manager.env(env).services[name].node_list)
|
148
|
+
end
|
149
|
+
elsif @manager.tags[name]
|
150
|
+
@environments.each do |env|
|
151
|
+
node_list.merge!(@manager.env(env).tags[name].node_list)
|
152
|
+
end
|
153
|
+
else
|
154
|
+
LeapCli::Util.log :warning, "filter '#{name}' does not match any node names, tags, services, or environments."
|
155
|
+
end
|
156
|
+
return node_list
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
#
|
161
|
+
# when pinning, we use the name 'default' to specify nodes
|
162
|
+
# without an environment set, but when filtering, we need to filter
|
163
|
+
# on :environment => nil.
|
164
|
+
#
|
165
|
+
def env_to_filter(environment)
|
166
|
+
environment == 'default' ? nil : environment
|
167
|
+
end
|
168
|
+
|
169
|
+
def is_environment?(text)
|
170
|
+
text == 'default' || @manager.environment_names.include?(text)
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|