leap_cli 1.5.6 → 1.6.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|