summaryrefslogtreecommitdiffstats
path: root/features/support/helpers
diff options
context:
space:
mode:
authorintrigeri <intrigeri@boum.org>2017-02-07 01:46:02 +0000
committerintrigeri <intrigeri@boum.org>2017-02-07 01:46:02 +0000
commita125665e9ca875c16465f014c3cafe002eef1572 (patch)
tree375e19d545131856a70684c5fce29bb415eb299b /features/support/helpers
parent0bab7e30681f7b481f5967d38686770460533b4c (diff)
parent4bb09f3837bd8864afd085e69654cd497690fe70 (diff)
Merge remote-tracking branch 'origin/devel' into feature/tor-nightly-master
Diffstat (limited to 'features/support/helpers')
-rw-r--r--features/support/helpers/dogtail.rb14
-rw-r--r--features/support/helpers/exec_helper.rb69
-rw-r--r--features/support/helpers/firewall_helper.rb26
-rw-r--r--features/support/helpers/misc_helpers.rb19
-rw-r--r--features/support/helpers/remote_shell.rb132
-rw-r--r--features/support/helpers/storage_helper.rb32
-rw-r--r--features/support/helpers/vm_helper.rb40
7 files changed, 208 insertions, 124 deletions
diff --git a/features/support/helpers/dogtail.rb b/features/support/helpers/dogtail.rb
index 59fac3c..2d81205 100644
--- a/features/support/helpers/dogtail.rb
+++ b/features/support/helpers/dogtail.rb
@@ -85,15 +85,19 @@ module Dogtail
lines = [lines] if lines.class != Array
script = build_script(lines)
script_path = $vm.execute_successfully('mktemp', @opts).stdout.chomp
- $vm.file_overwrite(script_path, script, @opts[:user])
+ $vm.file_overwrite(script_path, script)
args = ["/usr/bin/python '#{script_path}'", @opts]
if @opts[:allow_failure]
- ret = $vm.execute(*args)
+ return $vm.execute(*args)
else
- ret = $vm.execute_successfully(*args)
+ begin
+ return $vm.execute_successfully(*args)
+ rescue Exception => e
+ debug_log("Failing Dogtail script (#{script_path}):")
+ script.split("\n").each { |line| debug_log(" "*4 + line) }
+ raise e
+ end
end
- $vm.execute("rm -f '#{script_path}'")
- ret
end
def self.value_to_s(v)
diff --git a/features/support/helpers/exec_helper.rb b/features/support/helpers/exec_helper.rb
deleted file mode 100644
index 6f221de..0000000
--- a/features/support/helpers/exec_helper.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-require 'json'
-require 'socket'
-
-class VMCommand
-
- @@request_id ||= 0
-
- attr_reader :cmd, :returncode, :stdout, :stderr
-
- def initialize(vm, cmd, options = {})
- @cmd = cmd
- @returncode, @stdout, @stderr = VMCommand.execute(vm, cmd, options)
- end
-
- def VMCommand.wait_until_remote_shell_is_up(vm, timeout = 90)
- try_for(timeout, :msg => "Remote shell seems to be down") do
- Timeout::timeout(3) do
- VMCommand.execute(vm, "echo 'hello?'")
- end
- end
- end
-
- # If `:spawn` is false the server will block until it has finished
- # executing `cmd`. If it's true the server won't block, and the
- # response will always be [0, "", ""] (only used as an
- # ACK). execute() will always block until a response is received,
- # though. Spawning is useful when starting processes in the
- # background (or running scripts that does the same) like our
- # onioncircuits wrapper, or any application we want to interact with.
- def VMCommand.execute(vm, cmd, options = {})
- options[:user] ||= "root"
- options[:spawn] ||= false
- type = options[:spawn] ? "spawn" : "call"
- id = (@@request_id += 1)
- socket = TCPSocket.new("127.0.0.1", vm.get_remote_shell_port)
- debug_log("#{type}ing as #{options[:user]}: #{cmd}")
- socket.puts(JSON.dump([id, type, options[:user], cmd]))
- loop do
- s = socket.readline(sep = "\0").chomp("\0")
- response_id, *rest = JSON.load(s)
- if response_id == id
- debug_log("#{type} returned: #{s}") if not(options[:spawn])
- return rest
- else
- debug_log("Dropped out-of-order remote shell response: " +
- "got id #{response_id} but expected id #{id}")
- end
- end
- ensure
- socket.close if defined?(socket) && socket
- end
-
- def success?
- return @returncode == 0
- end
-
- def failure?
- return not(success?)
- end
-
- def to_s
- "Return status: #{@returncode}\n" +
- "STDOUT:\n" +
- @stdout +
- "STDERR:\n" +
- @stderr
- end
-
-end
diff --git a/features/support/helpers/firewall_helper.rb b/features/support/helpers/firewall_helper.rb
index ed2a09b..49e9853 100644
--- a/features/support/helpers/firewall_helper.rb
+++ b/features/support/helpers/firewall_helper.rb
@@ -3,7 +3,7 @@ require 'packetfu'
# Returns the unique edges (based on protocol, source/destination
# address/port) in the graph of all network flows.
def pcap_connections_helper(pcap_file, opts = {})
- opts[:ignore_dhcp] ||= true
+ opts[:ignore_dhcp] = true unless opts.has_key?(:ignore_dhcp)
connections = Array.new
packets = PacketFu::PcapFile.new.file_to_array(:filename => pcap_file)
packets.each do |p|
@@ -45,28 +45,40 @@ def pcap_connections_helper(pcap_file, opts = {})
next if opts[:ignore_dhcp]
end
- connections << {
+ packet_info = {
mac_saddr: eth_packet.eth_saddr,
mac_daddr: eth_packet.eth_daddr,
protocol: protocol,
- saddr: ip_packet.ip_saddr,
- daddr: ip_packet.ip_daddr,
sport: sport,
dport: dport,
}
+ # It seems *Packet.parse can return nil despite *Packet.can_parse?
+ # returning true.
+ if ip_packet
+ packet_info[:saddr] = ip_packet.ip_saddr
+ packet_info[:daddr] = ip_packet.ip_daddr
+ else
+ puts "We were hit by #11508. PacketFu bug? Packet info: #{packet_info}"
+ end
+ connections << packet_info
end
connections.uniq.map { |p| OpenStruct.new(p) }
end
+class FirewallAssertionFailedError < Test::Unit::AssertionFailedError
+end
+
# These assertions are made from the perspective of the system under
# testing when it comes to the concepts of "source" and "destination".
def assert_all_connections(pcap_file, opts = {}, &block)
all = pcap_connections_helper(pcap_file, opts)
good = all.find_all(&block)
bad = all - good
- save_failure_artifact("Network capture", pcap_file) unless bad.empty?
- assert(bad.empty?, "Unexpected connections were made:\n" +
- bad.map { |e| " #{e}" } .join("\n"))
+ unless bad.empty?
+ raise FirewallAssertionFailedError.new(
+ "Unexpected connections were made:\n" +
+ bad.map { |e| " #{e}" } .join("\n"))
+ end
end
def assert_no_connections(pcap_file, opts = {}, &block)
diff --git a/features/support/helpers/misc_helpers.rb b/features/support/helpers/misc_helpers.rb
index db907e5..ed9c54e 100644
--- a/features/support/helpers/misc_helpers.rb
+++ b/features/support/helpers/misc_helpers.rb
@@ -30,8 +30,12 @@ end
# Call block (ignoring any exceptions it may throw) repeatedly with
# one second breaks until it returns true, or until `timeout` seconds have
-# passed when we throw a Timeout::Error exception.
+# passed when we throw a Timeout::Error exception. If `timeout` is `nil`,
+# then we just run the code block with no timeout.
def try_for(timeout, options = {})
+ if block_given? && timeout.nil?
+ return yield
+ end
options[:delay] ||= 1
last_exception = nil
# Create a unique exception used only for this particular try_for
@@ -78,11 +82,12 @@ def try_for(timeout, options = {})
# ends up there immediately.
rescue unique_timeout_exception => e
msg = options[:msg] || 'try_for() timeout expired'
+ exc_class = options[:exception] || Timeout::Error
if last_exception
msg += "\nLast ignored exception was: " +
"#{last_exception.class}: #{last_exception}"
end
- raise Timeout::Error.new(msg)
+ raise exc_class.new(msg)
end
class TorFailure < StandardError
@@ -266,9 +271,19 @@ def info_log_artifact_location(type, path)
info_log("#{type.capitalize}: #{path}")
end
+def notify_user(message)
+ alarm_script = $config['NOTIFY_USER_COMMAND']
+ return if alarm_script.nil? || alarm_script.empty?
+ cmd_helper(alarm_script.gsub('%m', message))
+end
+
def pause(message = "Paused")
+ notify_user(message)
STDERR.puts
STDERR.puts message
+ # Ring the ASCII bell for a helpful notification in most terminal
+ # emulators.
+ STDOUT.write "\a"
STDERR.puts
loop do
STDERR.puts "Return: Continue; d: Debugging REPL"
diff --git a/features/support/helpers/remote_shell.rb b/features/support/helpers/remote_shell.rb
new file mode 100644
index 0000000..b2465a3
--- /dev/null
+++ b/features/support/helpers/remote_shell.rb
@@ -0,0 +1,132 @@
+require 'base64'
+require 'json'
+require 'socket'
+require 'timeout'
+
+module RemoteShell
+ class ServerFailure < StandardError
+ end
+
+ # Used to differentiate vs Timeout::Error, which is thrown by
+ # try_for() (by default) and often wraps around remote shell usage
+ # -- in that case we don't want to catch that "outer" exception in
+ # our handling of remote shell timeouts below.
+ class Timeout < ServerFailure
+ end
+
+ DEFAULT_TIMEOUT = 20*60
+
+ # Counter providing unique id:s for each communicate() call.
+ @@request_id ||= 0
+
+ def communicate(vm, *args, **opts)
+ opts[:timeout] ||= DEFAULT_TIMEOUT
+ socket = TCPSocket.new("127.0.0.1", vm.get_remote_shell_port)
+ id = (@@request_id += 1)
+ # Since we already have defined our own Timeout in the current
+ # scope, we have to be more careful when referring to the Timeout
+ # class from the 'timeout' module. However, note that we want it
+ # to throw our own Timeout exception.
+ Object::Timeout.timeout(opts[:timeout], Timeout) do
+ socket.puts(JSON.dump([id] + args))
+ socket.flush
+ loop do
+ line = socket.readline("\n").chomp("\n")
+ response_id, status, *rest = JSON.load(line)
+ if response_id == id
+ if status != "success"
+ if status == "error" and rest.class == Array and rest.size == 1
+ msg = rest.first
+ raise ServerFailure.new("#{msg}")
+ else
+ raise ServerFailure.new("Uncaught exception: #{status}: #{rest}")
+ end
+ end
+ return rest
+ else
+ debug_log("Dropped out-of-order remote shell response: " +
+ "got id #{response_id} but expected id #{id}")
+ end
+ end
+ end
+ ensure
+ socket.close if defined?(socket) && socket
+ end
+
+ module_function :communicate
+ private :communicate
+
+ class Command
+ # If `:spawn` is false the server will block until it has finished
+ # executing `cmd`. If it's true the server won't block, and the
+ # response will always be [0, "", ""] (only used as an
+ # ACK). execute() will always block until a response is received,
+ # though. Spawning is useful when starting processes in the
+ # background (or running scripts that does the same) or any
+ # application we want to interact with.
+ def self.execute(vm, cmd, **opts)
+ opts[:user] ||= "root"
+ opts[:spawn] = false unless opts.has_key?(:spawn)
+ type = opts[:spawn] ? "spawn" : "call"
+ debug_log("#{type}ing as #{opts[:user]}: #{cmd}")
+ ret = RemoteShell.communicate(vm, type, opts[:user], cmd, **opts)
+ debug_log("#{type} returned: #{ret}") if not(opts[:spawn])
+ return ret
+ end
+
+ attr_reader :cmd, :returncode, :stdout, :stderr
+
+ def initialize(vm, cmd, **opts)
+ @cmd = cmd
+ @returncode, @stdout, @stderr = self.class.execute(vm, cmd, **opts)
+ end
+
+ def success?
+ return @returncode == 0
+ end
+
+ def failure?
+ return not(success?)
+ end
+
+ def to_s
+ "Return status: #{@returncode}\n" +
+ "STDOUT:\n" +
+ @stdout +
+ "STDERR:\n" +
+ @stderr
+ end
+ end
+
+ # An IO-like object that is more or less equivalent to a File object
+ # opened in rw mode.
+ class File
+ def self.open(vm, mode, path, *args, **opts)
+ debug_log("opening file #{path} in '#{mode}' mode")
+ ret = RemoteShell.communicate(vm, mode, path, *args, **opts)
+ if ret.size != 1
+ raise ServerFailure.new("expected 1 value but got #{ret.size}")
+ end
+ debug_log("#{mode} complete")
+ return ret.first
+ end
+
+ attr_reader :vm, :path
+
+ def initialize(vm, path)
+ @vm, @path = vm, path
+ end
+
+ def read()
+ Base64.decode64(self.class.open(@vm, 'read', @path))
+ end
+
+ def write(data)
+ self.class.open(@vm, 'write', @path, Base64.encode64(data))
+ end
+
+ def append(data)
+ self.class.open(@vm, 'append', @path, Base64.encode64(data))
+ end
+ end
+end
diff --git a/features/support/helpers/storage_helper.rb b/features/support/helpers/storage_helper.rb
index 0e452ed..9cf0db2 100644
--- a/features/support/helpers/storage_helper.rb
+++ b/features/support/helpers/storage_helper.rb
@@ -144,13 +144,7 @@ class VMStorage
end
def disk_mklabel(name, parttype)
- disk = {
- :path => disk_path(name),
- :opts => {
- :format => disk_format(name)
- }
- }
- guestfs_disk_helper(disk) do |g, disk_handle|
+ guestfs_disk_helper(name) do |g, disk_handle|
g.part_init(disk_handle, parttype)
end
end
@@ -158,13 +152,7 @@ class VMStorage
def disk_mkpartfs(name, parttype, fstype, opts = {})
opts[:label] ||= nil
opts[:luks_password] ||= nil
- disk = {
- :path => disk_path(name),
- :opts => {
- :format => disk_format(name)
- }
- }
- guestfs_disk_helper(disk) do |g, disk_handle|
+ guestfs_disk_helper(name) do |g, disk_handle|
g.part_disk(disk_handle, parttype)
g.part_set_name(disk_handle, 1, opts[:label]) if opts[:label]
primary_partition = g.list_partitions()[0]
@@ -182,13 +170,7 @@ class VMStorage
end
def disk_mkswap(name, parttype)
- disk = {
- :path => disk_path(name),
- :opts => {
- :format => disk_format(name)
- }
- }
- guestfs_disk_helper(disk) do |g, disk_handle|
+ guestfs_disk_helper(name) do |g, disk_handle|
g.part_disk(disk_handle, parttype)
primary_partition = g.list_partitions()[0]
g.mkswap(primary_partition)
@@ -206,7 +188,13 @@ class VMStorage
Guestfs::EVENT_TRACE)
g.set_autosync(1)
disks.each do |disk|
- g.add_drive_opts(disk[:path], disk[:opts])
+ if disk.class == String
+ g.add_drive_opts(disk_path(disk), format: disk_format(disk))
+ elsif disk.class == Hash
+ g.add_drive_opts(disk[:path], disk[:opts])
+ else
+ raise "cannot handle type '#{disk.class}'"
+ end
end
g.launch()
yield(g, *g.list_devices())
diff --git a/features/support/helpers/vm_helper.rb b/features/support/helpers/vm_helper.rb
index 04cf7bc..3029acf 100644
--- a/features/support/helpers/vm_helper.rb
+++ b/features/support/helpers/vm_helper.rb
@@ -452,7 +452,7 @@ EOF
def execute(cmd, options = {})
options[:user] ||= "root"
- options[:spawn] ||= false
+ options[:spawn] = false unless options.has_key?(:spawn)
if options[:libs]
libs = options[:libs]
options.delete(:libs)
@@ -463,7 +463,7 @@ EOF
cmds << cmd
cmd = cmds.join(" && ")
end
- return VMCommand.new(self, cmd, options)
+ return RemoteShell::Command.new(self, cmd, options)
end
def execute_successfully(*args)
@@ -482,7 +482,12 @@ EOF
end
def wait_until_remote_shell_is_up(timeout = 90)
- VMCommand.wait_until_remote_shell_is_up(self, timeout)
+ msg = 'hello?'
+ try_for(timeout, :msg => "Remote shell seems to be down") do
+ Timeout::timeout(3) do
+ execute_successfully("echo '#{msg}'").stdout.chomp == msg
+ end
+ end
end
def host_to_guest_time_sync
@@ -540,27 +545,24 @@ EOF
execute("test -d '#{directory}'").success?
end
- def file_content(file, user = 'root')
- # We don't quote #{file} on purpose: we sometimes pass environment variables
- # or globs that we want to be interpreted by the shell.
- cmd = execute("cat #{file}", :user => user)
- assert(cmd.success?,
- "Could not cat '#{file}':\n#{cmd.stdout}\n#{cmd.stderr}")
- return cmd.stdout
+ def file_open(path)
+ f = RemoteShell::File.new(self, path)
+ yield f if block_given?
+ return f
end
- def file_append(file, lines, user = 'root')
+ def file_content(path)
+ file_open(path) { |f| return f.read() }
+ end
+
+ def file_overwrite(path, lines)
lines = lines.join("\n") if lines.class == Array
- # Use some tricky quoting to allow any character to be appended
- lines.gsub!("'", "'\"'\"'")
- cmd = execute("echo '#{lines}' >> '#{file}'", :user => user)
- assert(cmd.success?,
- "Could not append to '#{file}':\n#{cmd.stdout}\n#{cmd.stderr}")
+ file_open(path) { |f| return f.write(lines) }
end
- def file_overwrite(*args)
- execute_successfully("rm -f '#{args.first}'")
- file_append(*args)
+ def file_append(path, lines)
+ lines = lines.join("\n") if lines.class == Array
+ file_open(path) { |f| return f.append(lines) }
end
def set_clipboard(text)