pennyworth-tool 0.0.1

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.
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