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