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,27 @@
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 ImportSshKeysCommand < Command
20
+ def execute(ip, options)
21
+ password = options[:password] || "linux"
22
+ SshKeysImporter.import(ip, password)
23
+ rescue => e
24
+ log e.message
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,41 @@
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 ListCommand < BaseCommand
20
+ def execute
21
+ if @kiwi_dir
22
+ puts "Vagrant box definitions managed by pennyworth:"
23
+ local_base_images.each do |b|
24
+ puts " #{b}"
25
+ end
26
+ puts
27
+ end
28
+
29
+ puts "Available Vagrant boxes:"
30
+ VagrantCommand.new.list.each do |box|
31
+ puts " #{box}"
32
+ end
33
+ puts
34
+
35
+ puts "Available VMs:"
36
+ VagrantCommand.new.status.each do |vm|
37
+ puts " #{vm}"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,209 @@
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 SetupCommand < Command
20
+ def initialize
21
+ super
22
+
23
+ @user = ENV["LOGNAME"]
24
+ end
25
+
26
+ def execute
27
+ # Install dependencies
28
+ show_warning_for_unsupported_platforms
29
+ install_packages
30
+ reload_udev_rules
31
+ install_vagrant_plugin
32
+
33
+ # Set up permissions
34
+ add_user_to_groups
35
+ disable_libvirt_policykit_auth
36
+ allow_libvirt_access
37
+ allow_qemu_kvm_access
38
+ allow_arp_access
39
+ end
40
+
41
+ def show_warning_for_unsupported_platforms
42
+ supported_os = ["openSUSE 13.2", "SLES 12"]
43
+
44
+ os_release = read_os_release_file
45
+
46
+ if os_release
47
+ version = os_release[/^VERSION_ID="(.*)"/, 1]
48
+ distribution = os_release[/^NAME="(.*)"/, 1]
49
+ end
50
+
51
+ if !os_release || !supported_os.include?("#{distribution} #{version}")
52
+ log "Warning: Pennyworth is not tested upstream on this platform. " \
53
+ "Use at your own risk."
54
+ end
55
+ end
56
+
57
+ def read_os_release_file
58
+ os_release_file = "/etc/os-release"
59
+ File.read(os_release_file) if File.exist?(os_release_file)
60
+ end
61
+
62
+ private
63
+
64
+ def zypper_install(package)
65
+ Cheetah.run(
66
+ "sudo",
67
+ "zypper",
68
+ "--non-interactive",
69
+ "install",
70
+ "--auto-agree-with-licenses",
71
+ "--name",
72
+ package
73
+ )
74
+ end
75
+
76
+ def install_packages
77
+ log "Installing packages:"
78
+
79
+ packages = config["packages"]["local"]
80
+
81
+ if config["packages"][base_system]
82
+ packages += config["packages"][base_system]
83
+ end
84
+
85
+ packages.each do |name|
86
+ log " * Installing #{name}..."
87
+ zypper_install(name)
88
+ end
89
+
90
+ config["packages"]["remote"].each do |url|
91
+ log " * Downloading and installing #{url}..."
92
+ zypper_install(url)
93
+ end
94
+ end
95
+
96
+ # The kvm package does come with a udev rule file to adjust ownership of
97
+ # /dev/kvm. However, the installation of the package does not trigger a
98
+ # reload of the udev rules. In order for /dev/kvm to have the right
99
+ # ownership we need to reload the udev rules ourself.
100
+ def reload_udev_rules
101
+ log "Reloading udev rules"
102
+ Cheetah.run "sudo", "/sbin/udevadm", "control", "--reload-rules"
103
+ Cheetah.run "sudo", "/sbin/udevadm", "trigger"
104
+ end
105
+
106
+ def install_vagrant_plugin
107
+ log "Installing libvirt plugin for Vagrant..."
108
+
109
+ Cheetah.run "vagrant", "plugin", "install", "vagrant-libvirt"
110
+ end
111
+
112
+ def add_user_to_groups
113
+ log "Adding user #@user to groups:"
114
+
115
+ ["libvirt", "qemu", "kvm"].each do |group|
116
+ log " * Adding to group #{group}..."
117
+ Cheetah.run "sudo", "/usr/sbin/usermod", "-a", "-G", group, @user
118
+ end
119
+ end
120
+
121
+ # Without this, PlicyKit would pop-up a dialog asking for root password every
122
+ # time you do something with libvirt as a normal user. This would break the
123
+ # setup workflow and fail on headless machines.
124
+ def disable_libvirt_policykit_auth
125
+ log "Disabling PolicyKit authentication for libvirt..."
126
+
127
+ policykit_config = File.dirname(__FILE__) + "/../../../files/99-libvirt.rules"
128
+ Cheetah.run "sudo", "cp", policykit_config, "/etc/polkit-1/rules.d/99-libvirt.rules"
129
+ end
130
+
131
+ def allow_libvirt_access
132
+ log "Allowing libvirt access for normal users..."
133
+
134
+ adapt_config_file "/etc/libvirt/libvirtd.conf",
135
+ :unix_sock_group => "libvirt",
136
+ :unix_sock_ro_perms => "0777",
137
+ :unix_sock_rw_perms => "0770",
138
+ :auth_unix_rw => "none",
139
+ # By default, libvirt logs to syslog. We'd like to have the logs
140
+ # separated.
141
+ :log_outputs => "1:file:/var/log/libvirt/libvirt.log"
142
+ end
143
+
144
+ def allow_qemu_kvm_access
145
+ log "Allowing qemu-kvm access for user #@user..."
146
+
147
+ adapt_config_file "/etc/libvirt/qemu.conf",
148
+ :user => @user,
149
+ :group => "qemu"
150
+ end
151
+
152
+ # Pennyworth build fails because Pennyworth can't find arp when run as a normal user.
153
+ # This is a crude workaround.
154
+ def allow_arp_access
155
+ log "Making arp command available for normal users..."
156
+
157
+ Cheetah.run "sudo", "ln", "-sf", "/sbin/arp", "/usr/bin"
158
+ end
159
+
160
+ def adapt_config_file(file, adaptations)
161
+ # Create a backup.
162
+ Cheetah.run "sudo", "cp", file, "#{file}.pennyworth_save"
163
+
164
+ # Create a temporary copy with permissions that allow us to modify it.
165
+ temp_file = "/tmp/#{File.basename(file)}.pennyworth"
166
+ Cheetah.run "sudo", "cp", file, temp_file
167
+ Cheetah.run "sudo", "chmod", "a+rw", temp_file
168
+
169
+ # Do the adaptations.
170
+ content = File.read(temp_file)
171
+ adaptations.each_pair do |key, value|
172
+ regexp_uncommented = /^(\s*)#{key}(\s*)=(\s*).*$/
173
+ regexp_commented = /^(\s*)\#?#{key}(\s*)=(\s*).*$/
174
+ replacement = "\\1#{key}\\2=\\3#{value.inspect}"
175
+
176
+ # We need to be careful here because sometimes there are both commented
177
+ # and uncommented lines in the file. In that case, we want to modify the
178
+ # first uncommented line and keep the commented ones intact. On the other
179
+ # hand, if there are just commented lines, we want to uncomment and modify
180
+ # the first one.
181
+ if content =~ regexp_uncommented
182
+ content.sub!(regexp_uncommented, replacement)
183
+ elsif content =~ regexp_commented
184
+ content.sub!(regexp_commented, replacement)
185
+ else
186
+ raise "#{file} No #{key.inspect} option to adapt."
187
+ end
188
+ end
189
+ File.write(temp_file, content)
190
+
191
+ # Replace the original file with the temporary copy, keep its permissions
192
+ # intact.
193
+ permissions = Cheetah.run(
194
+ "sudo",
195
+ "stat",
196
+ "--printf",
197
+ "%a",
198
+ file,
199
+ :stdout => :capture
200
+ )
201
+ Cheetah.run "sudo", "mv", temp_file, file
202
+ Cheetah.run "sudo", "chmod", permissions, file
203
+ end
204
+
205
+ def base_system
206
+ Cheetah.run(["lsb_release", "--release"], :stdout => :capture).split[1]
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,28 @@
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 ShutdownCommand < Command
20
+ def execute(name)
21
+ runner = ImageRunner.new(name)
22
+
23
+ VM.new(runner).stop
24
+ rescue
25
+ log "Could not shutdown image #{runner.name}. Was it running?"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,26 @@
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 StatusCommand < Command
20
+ def execute(vm_name = nil)
21
+ v = VagrantCommand.new
22
+
23
+ print_ssh_config(v.vagrant, vm_name)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,27 @@
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 UpCommand < Command
20
+ def execute(vm_name = nil, options = {})
21
+ v = VagrantCommand.new
22
+ v.up vm_name, options[:destroy]
23
+
24
+ print_ssh_config(v.vagrant, vm_name)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,39 @@
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 WrongPasswordException < StandardError; end
20
+ class SshKeysAlreadyExistsException < StandardError; end
21
+ class SshConnectionFailed < StandardError; end
22
+ class CommandNotFoundError < StandardError; end
23
+ class BuildFailed < StandardError; end
24
+ class InvalidHostError < StandardError; end
25
+ class HostFileError < StandardError; end
26
+ class LockError < StandardError; end
27
+
28
+ class ExecutionFailed < StandardError
29
+ def initialize(e)
30
+ @message = e.message
31
+ @message += "\nStandard output:\n #{e.stdout}\n"
32
+ @message += "\nError output:\n #{e.stderr}\n"
33
+ end
34
+
35
+ def to_s
36
+ @message
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,39 @@
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
+ # This file contains global helper methods
19
+
20
+ def log s = nil
21
+ puts s unless Pennyworth::Cli.settings.silent
22
+ end
23
+
24
+ def with_c_locale(&block)
25
+ with_env "LC_ALL" => "C", &block
26
+ end
27
+
28
+ def with_env(env)
29
+ # ENV isn't a Hash, but a weird Hash-like object. Calling #to_hash on it
30
+ # will copy its items into a newly created Hash instance. This approach
31
+ # ensures that any modifications of ENV won't affect the stored value.
32
+ saved_env = ENV.to_hash
33
+ begin
34
+ ENV.replace(saved_env.merge(env))
35
+ yield
36
+ ensure
37
+ ENV.replace(saved_env)
38
+ end
39
+ end