pennyworth-tool 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.hound.yml +3 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +18 -0
  6. data/CONTRIBUTING.md +67 -0
  7. data/COPYING +674 -0
  8. data/Gemfile +28 -0
  9. data/README.md +339 -0
  10. data/Rakefile +33 -0
  11. data/bin/pennyworth +26 -0
  12. data/config/setup.yml +17 -0
  13. data/examples/README.md +23 -0
  14. data/examples/kiwi/definitions/base_opensuse13.1_kvm/config.sh +87 -0
  15. data/examples/kiwi/definitions/base_opensuse13.1_kvm/config.xml +64 -0
  16. data/examples/kiwi/definitions/base_opensuse13.1_kvm/root/etc/sysconfig/network/ifcfg-eth0 +2 -0
  17. data/examples/kiwi/definitions/base_opensuse13.1_kvm/root/home/vagrant/.ssh/authorized_keys +1 -0
  18. data/examples/vagrant/Vagrantfile +14 -0
  19. data/files/99-libvirt.rules +2 -0
  20. data/files/image_test-template.xml +43 -0
  21. data/files/pool-default.xml +6 -0
  22. data/lib/image_runner.rb +89 -0
  23. data/lib/pennyworth.rb +65 -0
  24. data/lib/pennyworth/cli.rb +339 -0
  25. data/lib/pennyworth/cli_host_controller.rb +107 -0
  26. data/lib/pennyworth/commands/base_command.rb +96 -0
  27. data/lib/pennyworth/commands/boot_command.rb +29 -0
  28. data/lib/pennyworth/commands/build_base_command.rb +103 -0
  29. data/lib/pennyworth/commands/command.rb +43 -0
  30. data/lib/pennyworth/commands/down_command.rb +25 -0
  31. data/lib/pennyworth/commands/import_base_command.rb +112 -0
  32. data/lib/pennyworth/commands/import_ssh_keys_command.rb +27 -0
  33. data/lib/pennyworth/commands/list_command.rb +41 -0
  34. data/lib/pennyworth/commands/setup_command.rb +209 -0
  35. data/lib/pennyworth/commands/shutdown_command.rb +28 -0
  36. data/lib/pennyworth/commands/status_command.rb +26 -0
  37. data/lib/pennyworth/commands/up_command.rb +27 -0
  38. data/lib/pennyworth/exceptions.rb +39 -0
  39. data/lib/pennyworth/helper.rb +39 -0
  40. data/lib/pennyworth/host_config.rb +86 -0
  41. data/lib/pennyworth/host_runner.rb +133 -0
  42. data/lib/pennyworth/image_runner.rb +89 -0
  43. data/lib/pennyworth/libvirt.rb +93 -0
  44. data/lib/pennyworth/local_command_runner.rb +77 -0
  45. data/lib/pennyworth/local_runner.rb +34 -0
  46. data/lib/pennyworth/lock_service.rb +87 -0
  47. data/lib/pennyworth/remote_command_runner.rb +144 -0
  48. data/lib/pennyworth/runner.rb +27 -0
  49. data/lib/pennyworth/settings.rb +42 -0
  50. data/lib/pennyworth/spec.rb +96 -0
  51. data/lib/pennyworth/spec_profiler.rb +85 -0
  52. data/lib/pennyworth/ssh_keys_importer.rb +107 -0
  53. data/lib/pennyworth/urls.rb +28 -0
  54. data/lib/pennyworth/vagrant.rb +81 -0
  55. data/lib/pennyworth/vagrant_command.rb +120 -0
  56. data/lib/pennyworth/vagrant_runner.rb +44 -0
  57. data/lib/pennyworth/version.rb +22 -0
  58. data/lib/pennyworth/vm.rb +62 -0
  59. data/man/.gitignore +2 -0
  60. data/man/pennyworth.1.md +28 -0
  61. data/pennyworth.gemspec +57 -0
  62. data/prophet/Gemfile +3 -0
  63. data/prophet/prophet.rb +82 -0
  64. data/spec/base_command_spec.rb +30 -0
  65. data/spec/build_base_command_spec.rb +147 -0
  66. data/spec/cli_host_controller_spec.rb +113 -0
  67. data/spec/data/hosts.yaml +10 -0
  68. data/spec/data/kiwi/base_opensuse12.3_kvm.box +1 -0
  69. data/spec/data/kiwi/base_opensuse13.1_kvm.box +1 -0
  70. data/spec/data/kiwi/definitions/base_opensuse12.3_kvm/config.sh +1 -0
  71. data/spec/data/kiwi/definitions/base_opensuse12.3_kvm/config.xml +1 -0
  72. data/spec/data/kiwi/definitions/base_opensuse12.3_kvm/root/home/vagrant/.ssh/authorized_keys +1 -0
  73. data/spec/data/kiwi/definitions/base_opensuse13.1_kvm/config.sh +1 -0
  74. data/spec/data/kiwi/definitions/base_opensuse13.1_kvm/config.xml +1 -0
  75. data/spec/data/kiwi/definitions/base_opensuse13.1_kvm/root/home/vagrant/.ssh/authorized_keys +1 -0
  76. data/spec/data/kiwi2/box_state.yaml +14 -0
  77. data/spec/data/kiwi2/definitions/base_opensuse12.3_kvm/config.sh +1 -0
  78. data/spec/data/kiwi2/definitions/base_opensuse12.3_kvm/config.xml +1 -0
  79. data/spec/data/kiwi2/definitions/base_opensuse12.3_kvm/root/home/vagrant/.ssh/authorized_keys +1 -0
  80. data/spec/data/kiwi2/definitions/base_opensuse13.1_kvm/config.sh +1 -0
  81. data/spec/data/kiwi2/definitions/base_opensuse13.1_kvm/config.xml +1 -0
  82. data/spec/data/kiwi2/definitions/base_opensuse13.1_kvm/root/home/vagrant/.ssh/authorized_keys +1 -0
  83. data/spec/data/kiwi3/box_state.yaml +13 -0
  84. data/spec/data/kiwi3/definitions/base_opensuse12.3_kvm/.gitkeep +0 -0
  85. data/spec/data/kiwi3/definitions/base_opensuse13.1_kvm/.gitkeep +0 -0
  86. data/spec/data/kiwi3/import_state.yaml +3 -0
  87. data/spec/data/kiwi4/definitions/base_opensuse12.3_kvm/.gitkeep +0 -0
  88. data/spec/data/kiwi4/definitions/base_opensuse13.1_kvm/.gitkeep +0 -0
  89. data/spec/data/kiwi4/import_state.yaml +3 -0
  90. data/spec/data/kiwi5/import_state.yaml +3 -0
  91. data/spec/data/vagrant/.gitkeep +0 -0
  92. data/spec/host_config_spec.rb +197 -0
  93. data/spec/host_runner_spec.rb +112 -0
  94. data/spec/image_runner_spec.rb +62 -0
  95. data/spec/import_base_command_spec.rb +189 -0
  96. data/spec/local_command_runner_spec.rb +117 -0
  97. data/spec/local_runner_spec.rb +42 -0
  98. data/spec/lock_service_spec.rb +95 -0
  99. data/spec/remote_command_runner_spec.rb +115 -0
  100. data/spec/settings_spec.rb +26 -0
  101. data/spec/setup_command_spec.rb +49 -0
  102. data/spec/spec_helper.rb +50 -0
  103. data/spec/spec_profiler_spec.rb +63 -0
  104. data/spec/spec_spec.rb +99 -0
  105. data/spec/support/command_runner_examples.rb +29 -0
  106. data/spec/support/runner_examples.rb +34 -0
  107. data/spec/urls_spec.rb +46 -0
  108. data/spec/vagrant_command_spec.rb +51 -0
  109. data/spec/vagrant_runner_spec.rb +40 -0
  110. data/spec/vagrant_spec.rb +288 -0
  111. data/spec/vm_spec.rb +56 -0
  112. metadata +257 -0
@@ -0,0 +1,86 @@
1
+ # Copyright (c) 2013-2015 SUSE LLC
2
+ #
3
+ # This program is free software; you can redistribute it and/or
4
+ # modify it under the terms of version 3 of the GNU General Public License as
5
+ # published by the Free Software Foundation.
6
+ #
7
+ # This program is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ # GNU General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU General Public License
13
+ # along with this program; if not, contact SUSE LLC.
14
+ #
15
+ # To contact SUSE about this file by physical or electronic mail,
16
+ # you may find current contact information at www.suse.com
17
+
18
+ module Pennyworth
19
+ class HostConfig
20
+ attr_reader :config_file, :lock_server_address
21
+
22
+ def self.for_directory(config_dir)
23
+ HostConfig.new(File.join(config_dir, "hosts.yaml"))
24
+ end
25
+
26
+ def initialize(config_file)
27
+ @config_file = File.expand_path(config_file)
28
+ end
29
+
30
+ def parse(yaml_string)
31
+ yaml = YAML.load(yaml_string)
32
+ if !yaml
33
+ raise HostFileError.new("Could not parse YAML in file '#{config_file}'")
34
+ end
35
+
36
+ if yaml["include"]
37
+ begin
38
+ open(yaml["include"], "rb") do |u|
39
+ parse(u.read)
40
+ end
41
+ rescue OpenURI::HTTPError, Errno::ENOENT
42
+ raise HostFileError.new("Unable to include '#{yaml["include"]}'")
43
+ end
44
+ end
45
+
46
+ if yaml["hosts"]
47
+ if !@hosts
48
+ @hosts = yaml["hosts"]
49
+ else
50
+ yaml["hosts"].each do |key, value|
51
+ @hosts[key] = value
52
+ end
53
+ end
54
+ end
55
+
56
+ if yaml["lock_server_address"]
57
+ @lock_server_address = yaml["lock_server_address"]
58
+ end
59
+ end
60
+
61
+ def read
62
+ parse(File.read(config_file))
63
+ end
64
+
65
+ def hosts
66
+ @hosts.keys
67
+ end
68
+
69
+ def host(host_name)
70
+ @hosts[host_name]
71
+ end
72
+
73
+ def setup(url)
74
+ if File.exist?(config_file)
75
+ raise HostFileError.new("Config file '#{config_file}' already exists." +
76
+ " Canceling setup.")
77
+ end
78
+
79
+ FileUtils.mkdir_p(File.dirname(config_file))
80
+ File.open(config_file, "w") do |f|
81
+ f.puts("---")
82
+ f.puts("include: #{url}")
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,133 @@
1
+ # Copyright (c) 2013-2014 SUSE LLC
2
+ #
3
+ # This program is free software; you can redistribute it and/or
4
+ # modify it under the terms of version 3 of the GNU General Public License as
5
+ # published by the Free Software Foundation.
6
+ #
7
+ # This program is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ # GNU General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU General Public License
13
+ # along with this program; if not, contact SUSE LLC.
14
+ #
15
+ # To contact SUSE about this file by physical or electronic mail,
16
+ # you may find current contact information at www.suse.com
17
+
18
+ module Pennyworth
19
+ class HostRunner < Runner
20
+ def initialize(host_name, host_config, username = "root")
21
+ @host_name = host_name
22
+ @username = username
23
+ config_file = host_config.config_file
24
+
25
+ host = host_config.host(host_name)
26
+ if !host
27
+ raise InvalidHostError.new("Host '#{host_name}' is not defined in '#{config_file}'")
28
+ end
29
+
30
+ @ip = host["address"]
31
+ @base_snapshot_id = host["base_snapshot_id"]
32
+ if !@ip
33
+ raise InvalidHostError.new(
34
+ "Missing 'address' field for host '#{host_name}' in '#{config_file}'"
35
+ )
36
+ end
37
+ if should_cleanup && !@base_snapshot_id
38
+ raise InvalidHostError.new(
39
+ "Missing 'base_snapshot_id' field for host '#{host_name}' in '#{config_file}'"
40
+ )
41
+ end
42
+
43
+ @command_runner = RemoteCommandRunner.new(@ip, @username)
44
+ @locker = LockService.new(host_config.lock_server_address)
45
+ end
46
+
47
+ def start
48
+ if !@locker.request_lock(@host_name)
49
+ raise LockError.new("Host '#{@host_name}' already locked")
50
+ end
51
+
52
+ connect
53
+
54
+ if should_cleanup
55
+ check_cleanup_capabilities
56
+ install_cleanup_interrupt_handler
57
+ end
58
+
59
+ @ip
60
+ end
61
+
62
+ def cleanup
63
+ return if @cleaned_up || !@connected
64
+
65
+ remote = RemoteCommandRunner.new(@ip, @username)
66
+ remote.run "snapper", "create", "-c", "number", "--pre-number", @base_snapshot_id.to_s,
67
+ "--description", "pennyworth_snapshot"
68
+ remote.run "snapper", "cleanup", "number"
69
+ remote.run "snapper", "undochange", "#{@base_snapshot_id}..0"
70
+ remote.run "bash", "-c", "reboot &"
71
+ @cleaned_up = true
72
+ end
73
+
74
+ def stop
75
+ if should_cleanup
76
+ cleanup
77
+ uninstall_cleanup_interrupt_handler
78
+ end
79
+
80
+ @locker.release_lock(@host_name)
81
+ end
82
+
83
+ private
84
+
85
+ # Makes sure the we can connect to the remote system as root (without a
86
+ # password or passphrase)
87
+ def connect
88
+ Cheetah.run "ssh", "-q", "-o", "BatchMode=yes", "root@#{@ip}"
89
+ rescue Cheetah::ExecutionFailed
90
+ raise SshConnectionFailed.new(
91
+ "Could not establish SSH connection to host '#{@ip}'. Please make sure that " \
92
+ "you can connect non-interactively as root, e.g. using ssh-agent.\n\n" \
93
+ "To copy your default ssh key to the machine run:\n" \
94
+ "ssh-copy-id root@#{@ip}"
95
+ )
96
+ end
97
+
98
+ def check_cleanup_capabilities
99
+ begin
100
+ RemoteCommandRunner.new(@ip, @username).run "snapper", "--help"
101
+ rescue Cheetah::ExecutionFailed
102
+ raise CommandNotFoundError.new(
103
+ "Snapper needs to be installed on the test system '#{@ip}' for the automatic cleanup."
104
+ )
105
+ end
106
+ @connected = true
107
+ end
108
+
109
+ def should_cleanup
110
+ !ENV["SKIP_HOST_CLEANUP"]
111
+ end
112
+
113
+ def install_cleanup_interrupt_handler
114
+ @old_interrupt_handler = trap("INT") do
115
+ trap("INT") do
116
+ exit!(1)
117
+ end
118
+
119
+ puts "RSpec is shutting down. Resetting test host '#{@ip}'." \
120
+ "Interrupt again to force exit."
121
+ cleanup
122
+ puts "Done."
123
+
124
+ @old_interrupt_handler.call
125
+ end
126
+ end
127
+
128
+ def uninstall_cleanup_interrupt_handler
129
+ # Restore old interrupt handler
130
+ trap("INT", @old_interrupt_handler) if defined?(@old_interrupt_handler)
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,89 @@
1
+ # Copyright (c) 2013-2014 SUSE LLC
2
+ #
3
+ # This program is free software; you can redistribute it and/or
4
+ # modify it under the terms of version 3 of the GNU General Public License as
5
+ # published by the Free Software Foundation.
6
+ #
7
+ # This program is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ # GNU General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU General Public License
13
+ # along with this program; if not, contact SUSE LLC.
14
+ #
15
+ # To contact SUSE about this file by physical or electronic mail,
16
+ # you may find current contact information at www.suse.com
17
+
18
+ module Pennyworth
19
+ class ImageRunner < Runner
20
+ DOMAIN_TEMPLATE = File.join(File.dirname(__FILE__) + "/../../files/image_test-template.xml")
21
+
22
+ attr_accessor :name
23
+
24
+ def initialize(image, username)
25
+ @image = image
26
+ @name = File.basename(image)
27
+ @username = username
28
+
29
+ @connection = ::Libvirt::open("qemu:///system")
30
+ end
31
+
32
+ def start
33
+ cleanup
34
+
35
+ ip = start_built_image
36
+ @command_runner = RemoteCommandRunner.new(ip, @username)
37
+
38
+ ip
39
+ end
40
+
41
+ def stop
42
+ system = @connection.lookup_domain_by_name(@name)
43
+ system.destroy
44
+ end
45
+
46
+ def cleanup_directory(_dir)
47
+ # The machine will be reset anyway after the tests, so this is is a NOP
48
+ end
49
+
50
+ private
51
+
52
+ def cleanup
53
+ system = @connection.lookup_domain_by_name(@name)
54
+ system.destroy
55
+ rescue
56
+ end
57
+
58
+ # Creates a transient kvm domain from the predefined image_test-domain.xml
59
+ # file and returns the ip address for further interaction.
60
+ def start_built_image
61
+ domain_config = File.read(DOMAIN_TEMPLATE)
62
+ domain_config.gsub!("@@image@@", @image)
63
+ domain_config.gsub!("@@name@@", @name)
64
+
65
+ @connection.create_domain_xml(domain_config)
66
+ system = @connection.lookup_domain_by_name(@name)
67
+
68
+ domain_xml = Nokogiri::XML(system.xml_desc)
69
+ mac = domain_xml.xpath("//domain/devices/interface/mac").attr("address")
70
+ ip_address = nil
71
+
72
+ # Loop until the VM has got an IP address we can return
73
+ lease_file = "/var/lib/libvirt/dnsmasq/default.leases"
74
+ 300.times do
75
+ match = File.readlines(lease_file).grep(/#{mac}/).first
76
+ if match
77
+ ip_address = match.split[2]
78
+ break
79
+ end
80
+
81
+ sleep 1
82
+ end
83
+
84
+ ip_address
85
+ end
86
+ end
87
+
88
+
89
+ end
@@ -0,0 +1,93 @@
1
+ # Copyright (c) 2013-2014 SUSE LLC
2
+ #
3
+ # This program is free software; you can redistribute it and/or
4
+ # modify it under the terms of version 3 of the GNU General Public License as
5
+ # published by the Free Software Foundation.
6
+ #
7
+ # This program is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ # GNU General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU General Public License
13
+ # along with this program; if not, contact SUSE LLC.
14
+ #
15
+ # To contact SUSE about this file by physical or electronic mail,
16
+ # you may find current contact information at www.suse.com
17
+
18
+ module Pennyworth
19
+ class Libvirt
20
+ LIBVIRT_NET_NAME = "default"
21
+ LIBVIRT_POOL_NAME = "default"
22
+
23
+ class << self
24
+ def ensure_libvirt_env_started
25
+ # The check here is unnecessary for technical reasons ("sysctl start" does
26
+ # not fail if the service is already running), but it avoids an unnecessary
27
+ # sudo password prompt.
28
+ libvirtd_start unless libvirtd_active?
29
+
30
+ libvirt_net_start unless libvirt_net_active?
31
+
32
+ # As default pool is not always available but gets sometimes created by
33
+ # tools like virt-manager, we need to create it ourself if it does not yet
34
+ # exist.
35
+ libvirt_pool_create unless libvirt_pool_exists?
36
+ end
37
+
38
+ private
39
+
40
+ def libvirtd_start
41
+ Cheetah.run "sudo", "systemctl", "start", "libvirtd"
42
+
43
+ sleep 0.1 until File.exists?("/var/run/libvirt/libvirt-sock")
44
+ end
45
+
46
+ def libvirtd_active?
47
+ output = Cheetah.run "systemctl", "show", "--property", "ActiveState", "libvirtd", :stdout => :capture
48
+
49
+ output == "ActiveState=active"
50
+ end
51
+
52
+ def libvirt_net_start
53
+ Cheetah.run "virsh", "-c", "qemu:///system", "net-start", LIBVIRT_NET_NAME
54
+ end
55
+
56
+ def libvirt_net_active?
57
+ output = with_c_locale do
58
+ Cheetah.run "virsh", "-c", "qemu:///system", "net-list", "--all", :stdout => :capture
59
+ end
60
+
61
+ # The output looks like this:
62
+ #
63
+ # Name State Autostart Persistent
64
+ # ----------------------------------------------------------
65
+ # default active no yes
66
+ # vagrant0 active yes yes
67
+ #
68
+ output.split("\n")[2..-1].find do |line|
69
+ line =~ /^\s*#{LIBVIRT_NET_NAME}\s+active/
70
+ end
71
+ end
72
+
73
+ def libvirt_pool_create
74
+ pool_config_file = File.dirname(__FILE__) + "/../../files/pool-default.xml"
75
+ Cheetah.run "virsh", "-c", "qemu:///system", "pool-create", pool_config_file
76
+ end
77
+
78
+ def libvirt_pool_exists?
79
+ output = with_c_locale do
80
+ Cheetah.run "virsh", "-c", "qemu:///system", "pool-list", "--all", :stdout => :capture
81
+ end
82
+
83
+ # The output looks like this:
84
+ #
85
+ # Name State Autostart
86
+ # -----------------------------------------
87
+ # default active no
88
+ #
89
+ output.split("\n")[2..-1].detect { |line| line =~ /^\s*#{LIBVIRT_POOL_NAME}/ }
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,77 @@
1
+ # Copyright (c) 2013-2014 SUSE LLC
2
+ #
3
+ # This program is free software; you can redistribute it and/or
4
+ # modify it under the terms of version 3 of the GNU General Public License as
5
+ # published by the Free Software Foundation.
6
+ #
7
+ # This program is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ # GNU General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU General Public License
13
+ # along with this program; if not, contact SUSE LLC.
14
+ #
15
+ # To contact SUSE about this file by physical or electronic mail,
16
+ # you may find current contact information at www.suse.com
17
+
18
+ module Pennyworth
19
+ # LocalCommandRunner is used for executing commands on the local machine
20
+ class LocalCommandRunner
21
+ # Initialize the command runner
22
+ #
23
+ # +opts+:: Options to modify how and which the commands are run.
24
+ #
25
+ # Available options:
26
+ # [env]:: Hash of environment options to set for the command, e.g.
27
+ # {
28
+ # "MACHINERY_DIR" => "/tmp/machinery"
29
+ # }
30
+ def initialize(opts = {})
31
+ @env = opts[:env] || {}
32
+ end
33
+
34
+ def run(*args)
35
+ options = args.last.is_a?(Hash) ? args.pop : {}
36
+
37
+ with_env(@env) do
38
+ Cheetah.run(
39
+ "bash", "-c", *args, options
40
+ )
41
+ end
42
+ rescue Cheetah::ExecutionFailed => e
43
+ raise ExecutionFailed.new(e)
44
+ end
45
+
46
+ # Copy a local file to the remote system.
47
+ #
48
+ # +source+:: Path to the local file
49
+ # +destination+:: Path to the remote file or directory. If +destination+ is a
50
+ # path, the same filename as +source+ will be used.
51
+ # +opts+:: Options to modify the attributes of the remote file.
52
+ #
53
+ # Available options:
54
+ # [owner]:: Owner of the file, e.g. "tux"
55
+ # [group]:: Group of the file, e.g. "users"
56
+ # [mode]:: Mode of the file, e.g. "600"
57
+ def inject_file(source, destination, opts = {})
58
+ # Append filename (taken from +source+) to destination if it is a path, so
59
+ # that +destination+ is always the full target path including the filename.
60
+ destination = File.join(destination, File.basename(source)) if File.directory?(destination)
61
+
62
+ FileUtils.cp(source, destination)
63
+ FileUtils.chown(opts[:owner], opts[:group], destination)
64
+ FileUtils.chmod(opts[:mode], destination) if opts[:mode]
65
+ end
66
+
67
+ def extract_file(source, destination)
68
+ FileUtils.cp(source, destination)
69
+ end
70
+
71
+ def inject_directory(source, destination, opts = {})
72
+ FileUtils.mkdir_p(destination)
73
+ FileUtils.cp_r(source, destination)
74
+ FileUtils.chown_R(opts[:owner], opts[:group], destination)
75
+ end
76
+ end
77
+ end