vagrant-bolt 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +124 -0
  5. data/.travis.yml +28 -0
  6. data/.yardopts +1 -0
  7. data/Gemfile +37 -0
  8. data/LICENSE +12 -0
  9. data/Puppetfile +7 -0
  10. data/README.md +431 -0
  11. data/Rakefile +19 -0
  12. data/Vagrantfile +47 -0
  13. data/acceptance/artifacts/.keep +0 -0
  14. data/acceptance/components/bolt_spec.rb +98 -0
  15. data/acceptance/skeletons/advanced/Vagrantfile +26 -0
  16. data/acceptance/skeletons/base/Vagrantfile +11 -0
  17. data/acceptance/skeletons/base/modules/facts/CHANGELOG.md +26 -0
  18. data/acceptance/skeletons/base/modules/facts/CONTRIBUTING.md +279 -0
  19. data/acceptance/skeletons/base/modules/facts/Gemfile +98 -0
  20. data/acceptance/skeletons/base/modules/facts/LICENSE +201 -0
  21. data/acceptance/skeletons/base/modules/facts/README.md +45 -0
  22. data/acceptance/skeletons/base/modules/facts/Rakefile +8 -0
  23. data/acceptance/skeletons/base/modules/facts/checksums.json +42 -0
  24. data/acceptance/skeletons/base/modules/facts/lib/puppet/functions/facts/group_by.rb +14 -0
  25. data/acceptance/skeletons/base/modules/facts/metadata.json +62 -0
  26. data/acceptance/skeletons/base/modules/facts/plans/info.pp +16 -0
  27. data/acceptance/skeletons/base/modules/facts/plans/init.pp +13 -0
  28. data/acceptance/skeletons/base/modules/facts/tasks/bash.json +5 -0
  29. data/acceptance/skeletons/base/modules/facts/tasks/bash.sh +93 -0
  30. data/acceptance/skeletons/base/modules/facts/tasks/init.json +10 -0
  31. data/acceptance/skeletons/base/modules/facts/tasks/powershell.json +4 -0
  32. data/acceptance/skeletons/base/modules/facts/tasks/powershell.ps1 +56 -0
  33. data/acceptance/skeletons/base/modules/facts/tasks/ruby.json +4 -0
  34. data/acceptance/skeletons/base/modules/facts/tasks/ruby.rb +40 -0
  35. data/acceptance/skeletons/provisioner/Vagrantfile +19 -0
  36. data/acceptance/skeletons/trigger/Vagrantfile +22 -0
  37. data/acceptance/vagrant-spec.config.rb +22 -0
  38. data/lib/vagrant-bolt.rb +57 -0
  39. data/lib/vagrant-bolt/command.rb +65 -0
  40. data/lib/vagrant-bolt/config.rb +6 -0
  41. data/lib/vagrant-bolt/config/bolt.rb +135 -0
  42. data/lib/vagrant-bolt/config/global.rb +172 -0
  43. data/lib/vagrant-bolt/config_builder.rb +11 -0
  44. data/lib/vagrant-bolt/config_builder/config.rb +150 -0
  45. data/lib/vagrant-bolt/config_builder/monkey_patches.rb +71 -0
  46. data/lib/vagrant-bolt/config_builder/provisioner.rb +106 -0
  47. data/lib/vagrant-bolt/config_builder/triggers.rb +29 -0
  48. data/lib/vagrant-bolt/plugin.rb +39 -0
  49. data/lib/vagrant-bolt/provisioner.rb +18 -0
  50. data/lib/vagrant-bolt/runner.rb +88 -0
  51. data/lib/vagrant-bolt/util/bolt.rb +139 -0
  52. data/lib/vagrant-bolt/util/config.rb +43 -0
  53. data/lib/vagrant-bolt/util/machine.rb +73 -0
  54. data/lib/vagrant-bolt/version.rb +5 -0
  55. data/spec/spec_helper.rb +12 -0
  56. data/spec/unit/config/bolt_spec.rb +150 -0
  57. data/spec/unit/config/global_spec.rb +95 -0
  58. data/spec/unit/provisioner/bolt_spec.rb +39 -0
  59. data/spec/unit/runner/runner_spec.rb +122 -0
  60. data/spec/unit/util/bolt_spec.rb +148 -0
  61. data/spec/unit/util/config_spec.rb +53 -0
  62. data/spec/unit/vagrant_spec.rb +9 -0
  63. data/tasks/acceptance.rake +45 -0
  64. data/tasks/spec.rake +5 -0
  65. data/templates/locales/en.yml +24 -0
  66. data/vagrant-bolt.gemspec +24 -0
  67. metadata +109 -0
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ # VM level requires overriding to_proc to allow access to the node config object
4
+ # @!visibility private
5
+ module VagrantBolt::ConfigBuilder::MonkeyPatches
6
+ def to_proc
7
+ proc do |config|
8
+ super.call(config)
9
+ eval_bolt_root(config)
10
+ eval_bolt_triggers_root(config)
11
+ end
12
+ end
13
+
14
+ def eval_bolt(config)
15
+ # noop
16
+ end
17
+
18
+ def eval_bolt_root(vm_root_config)
19
+ # Configure the vm bolt object if the options exist
20
+ with_attr(:bolt) do |bolt_config|
21
+ f = VagrantBolt::ConfigBuilder::Config.new_from_hash(bolt_config)
22
+ f.call(vm_root_config)
23
+ end
24
+ end
25
+
26
+ def eval_bolt_triggers(config)
27
+ # noop
28
+ end
29
+
30
+ def eval_bolt_triggers_root(vm_root_config)
31
+ # Configure the vm bolt object if the options exist
32
+ triggers = attr(:bolt_triggers) || [] # rubocop:disable Style/Attr
33
+ triggers.each do |config|
34
+ f = VagrantBolt::ConfigBuilder::Triggers.new_from_hash(config)
35
+ f.call(vm_root_config)
36
+ end
37
+ end
38
+ end
39
+
40
+ class ConfigBuilder::Model::VM
41
+ def_model_delegator :bolt
42
+ def_model_delegator :bolt_triggers
43
+ end
44
+
45
+ ConfigBuilder::Model::VM.prepend(VagrantBolt::ConfigBuilder::MonkeyPatches)
46
+
47
+ # Allow for the role filter to handle bolt configs and bolt_triggers
48
+ # @!visibility private
49
+ module VagrantBolt::ConfigBuilder::MonkeyPatches::FilterRoles
50
+ def merge_nodes!(left, right)
51
+ super.tap do |result|
52
+ array_keys = ['bolt_triggers']
53
+ array_keys.each do |key|
54
+ next unless right.key?(key)
55
+
56
+ result[key] ||= []
57
+ result[key].unshift(*right[key])
58
+ end
59
+
60
+ hash_keys = ['bolt']
61
+ hash_keys.each do |key|
62
+ next unless right.key?(key)
63
+
64
+ result[key] ||= {}
65
+ result[key] = right[key].merge(result[key])
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ ConfigBuilder::Filter::Roles.prepend(VagrantBolt::ConfigBuilder::MonkeyPatches::FilterRoles)
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'config_builder/model'
4
+ require_relative 'monkey_patches'
5
+
6
+ class VagrantBolt::ConfigBuilder::Provisioner < ConfigBuilder::Model::Provisioner::Base
7
+ # @!attribute [rw] args
8
+ # @return [String] Additional arguments for the bolt command
9
+ def_model_attribute :args
10
+
11
+ # @!attribute [rw] bolt_exe
12
+ # @return [String] The full path to the bolt command. If not passed in, the default from PATH will be used.
13
+ def_model_attribute :bolt_exe
14
+
15
+ # @!attribute [rw] boltdir
16
+ # @return [String] The bolt working directory. Defaults to `.`
17
+ def_model_attribute :boltdir
18
+
19
+ # @!attribute [rw] debug
20
+ # @return [Boolean] Shows debug logging
21
+ def_model_attribute :debug
22
+
23
+ # @!attribute [rw] host_key_check
24
+ # @return [Boolean] If the connection should check the host key on the remote host (linux)
25
+ def_model_attribute :host_key_check
26
+
27
+ # @!attribute [rw] modulepath
28
+ # @return [String] The path to the modules. Defaults to `modules`.
29
+ def_model_attribute :modulepath
30
+
31
+ # @!attribute [rw] name
32
+ # @return [String] The name of task or plan to run
33
+ def_model_attribute :name
34
+
35
+ # @!attribute [rw] nodes
36
+ # Note: The `node_list` will override this setting.
37
+ # @return [Array<String, Symbol>, "ALL"] The nodes to run the task or plan on.
38
+ # Valid values are an array of machine names or the string "ALL".
39
+ def_model_attribute :nodes
40
+
41
+ # @!attribute [rw] noop
42
+ # @return [Boolean] If the command should be run with noop. Only valid with tasks and apply.
43
+ def_model_attribute :noop
44
+
45
+ # @!attribute [rw] excludes
46
+ # Note: The `node_list` will override this setting.
47
+ # Note: This will be merged with `nodes`, with `excludes` taking precidence.
48
+ # @return [Array<String, Symbol>] The nodes to exclude from running this task or plan on.
49
+ # Valid values are an array of machine names.
50
+ def_model_attribute :excludes
51
+
52
+ # @!attribute [rw] node_list
53
+ # This setting overrides `nodes` and needs to be in the `protocol://ipaddress:port` URI format
54
+ # @return [String] The bolt node list. This defaults to the currnet node.
55
+ def_model_attribute :node_list
56
+
57
+ # @!attribute [rw] params
58
+ # @return [Hash] The paramater hash for the task or plan
59
+ def_model_attribute :params
60
+
61
+ # @!attribute [rw] command
62
+ # @return [Symbol] Whether bolt should use a task or plan
63
+ def_model_attribute :command
64
+
65
+ # @!attribute [rw] user
66
+ # @return [String] The user to authenticate on the machine.
67
+ def_model_attribute :user
68
+
69
+ # @!attribute [rw] password
70
+ # @return [String] The password to authenticate on the machine.
71
+ def_model_attribute :password
72
+
73
+ # @!attribute [rw] port
74
+ # @return [String] The port to connect to the machine.
75
+ def_model_attribute :port
76
+
77
+ # @!attribute [rw] private_key
78
+ # @return [String] The path of the private_key to authenticate on the machine.
79
+ def_model_attribute :private_key
80
+
81
+ # @!attribute [rw] run_as
82
+ # @return [String] User to run as using privilege escalation.
83
+ def_model_attribute :run_as
84
+
85
+ # @!attribute [rw] sudo_password
86
+ # @return [String] The password to authenticate sudo on the machine.
87
+ def_model_attribute :sudo_password
88
+
89
+ # @!attribute [rw] ssl
90
+ # @return [Boolean] If the connection should use SSL on with WinRM (Windows)
91
+ def_model_attribute :ssl
92
+
93
+ # @!attribute [rw] ssl_verify
94
+ # @return [Boolean] If the connection should verify SSL on with WinRM (Windows)
95
+ def_model_attribute :ssl_verify
96
+
97
+ # @!attribute [rw] tmpdir
98
+ # @return [String] The directory to upload and execute temporary files on the target
99
+ def_model_attribute :tmpdir
100
+
101
+ # @!attribute [rw] verbose
102
+ # @return [Boolean] Shows verbose logging
103
+ def_model_attribute :verbose
104
+
105
+ ConfigBuilder::Model::Provisioner.register('bolt', self)
106
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'config_builder/model'
4
+ require_relative 'monkey_patches'
5
+
6
+ class VagrantBolt::ConfigBuilder::Triggers < VagrantBolt::ConfigBuilder::Config
7
+ # @!attribute [rw] trigger_type
8
+ # @return [Symbol] A symbol containing the the trigger action. Valid values are `:before` and `:after`
9
+ def_model_attribute :trigger_type
10
+
11
+ # @!attribute [rw] trigger_commands
12
+ # @return [Array<Symbol>] The commands that the trigger should run on. E.g. `[:up, :provision]`
13
+ def_model_attribute :trigger_commands
14
+
15
+ def to_proc
16
+ options = @attrs.dup
17
+ trigger_type = options.delete(:trigger_type)
18
+ trigger_commands = options.delete(:trigger_commands)
19
+ command = options.delete(:command)
20
+ name = options.delete(:name)
21
+ proc do |config|
22
+ config.trigger.send(trigger_type, trigger_commands) do |trigger|
23
+ trigger.ruby do |env, machine|
24
+ VagrantBolt.send(command, name, env, machine, **options)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'vagrant'
4
+ require_relative 'version'
5
+
6
+ raise "vagrant-bolt version #{VagrantBolt::VERSION} requires Vagrant 2.2 or later" if Vagrant::VERSION < "2.2.0"
7
+
8
+ class VagrantBolt::Plugin < Vagrant.plugin('2')
9
+ name 'bolt'
10
+
11
+ description <<-DESC
12
+ Vagrant provisioning with Puppet Bolt
13
+ DESC
14
+
15
+ config(:bolt) do
16
+ require_relative 'config'
17
+ VagrantBolt::Config::Global
18
+ end
19
+
20
+ config(:bolt, :provisioner) do
21
+ require_relative 'config'
22
+ VagrantBolt::Config::Bolt
23
+ end
24
+
25
+ provisioner(:bolt) do
26
+ require_relative 'provisioner'
27
+ VagrantBolt::Provisioner
28
+ end
29
+
30
+ command(:bolt) do
31
+ require_relative 'command'
32
+ VagrantBolt::Command
33
+ end
34
+
35
+ # Enables config builder loading of this plugin
36
+ def self.config_builder_hook
37
+ require_relative 'config_builder'
38
+ end
39
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'vagrant'
4
+ require 'vagrant/errors'
5
+
6
+ class VagrantBolt::Provisioner < Vagrant.plugin('2', :provisioner)
7
+ # Provision VMs with Bolt
8
+ # Creates a trigger for each bolt provisioner
9
+
10
+ def provision
11
+ runner = VagrantBolt::Runner.new(@machine.env, @machine, @config)
12
+ runner.run(@config.command, @config.name)
13
+ end
14
+
15
+ def cleanup
16
+ # We don't do any clean up for this provisioner
17
+ end
18
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'util/bolt'
4
+ require_relative 'util/config'
5
+ require_relative 'util/machine'
6
+
7
+ class VagrantBolt::Runner
8
+ def initialize(env, machine, boltconfig = nil)
9
+ @env = env
10
+ @machine = machine
11
+ @boltconfig = boltconfig.nil? ? VagrantBolt::Config::Bolt.new : boltconfig
12
+ @inventory_path = VagrantBolt::Util::Bolt.inventory_file(@env)
13
+ end
14
+
15
+ # Run a bolt task or plan
16
+ # @param [Symbol, String] command The command of bolt to run; task or plan
17
+ # @param [String] name The name of the bolt task or plan to run
18
+ # @param [Hash] args A optional hash of bolt config overrides. No merging will be done with the overrides
19
+ # @example run('task', 'facts', {node_list: "machinename"})
20
+ def run(command, name, **args)
21
+ @boltconfig = setup_overrides(command, name, **args)
22
+ # Don't run anything if there are nodes to run it on
23
+ # TODO: Gate this in a more efficient manner. It is possible to run plans without a node list.
24
+ return if @boltconfig.node_list.nil?
25
+
26
+ @inventory_path = VagrantBolt::Util::Bolt.update_inventory_file(@env)
27
+ validate
28
+ command = VagrantBolt::Util::Bolt.generate_bolt_command(@boltconfig, @inventory_path)
29
+ VagrantBolt::Util::Machine.run_command(command, @machine.ui)
30
+ end
31
+
32
+ private
33
+
34
+ # Set up config overrides
35
+ # @param [Symbol, String] command The command of bolt to run; task or plan
36
+ # @param [String] name The name of the bolt task or plan to run
37
+ # @param [Hash] args A optional hash of bolt config overrides; {run_as: "vagrant"}
38
+ # @return [Object] Bolt config with ssh info populated
39
+ def setup_overrides(command, name, **args)
40
+ config = @boltconfig.dup
41
+ config.command = command
42
+ config.name = name
43
+ # Merge the root config to get the defaults for the environment
44
+ config = VagrantBolt::Util::Config.merge_config(config, @env.vagrantfile.config.bolt)
45
+ # Add any additional arguments to the config object
46
+ config.set_options(args) unless args.nil?
47
+ # Configure the node_list based on the config
48
+ config.node_list ||= [config.nodes - config.excludes].flatten.join(',') unless config.nodes.empty? || config.nodes.to_s.casecmp("all").zero?
49
+ config.node_list ||= [VagrantBolt::Util::Machine.nodes_in_environment(@env).map(&:name) - config.excludes].flatten.join(',') if config.nodes.to_s.casecmp("all").zero?
50
+ config.node_list ||= @machine.name.to_s unless config.excludes.include?(@machine.name.to_s)
51
+
52
+ # Ensure these are absolute paths to allow for running vagrant commands outside of the root dir
53
+ config.modulepath = VagrantBolt::Util::Config.full_path(config.modulepath, @env.root_path)
54
+ config.boltdir = VagrantBolt::Util::Config.full_path(config.boltdir, @env.root_path)
55
+
56
+ config
57
+ end
58
+
59
+ # Validate the config object for configuration issues
60
+ # Print and raise an exception if errors exist
61
+ def validate
62
+ errors = {}
63
+ errors.merge!(@boltconfig.validate(@machine))
64
+ errors.merge!(validate_config)
65
+
66
+ errors.keys.each do |key|
67
+ errors.delete(key) if errors[key].empty?
68
+ end
69
+
70
+ # rubocop:disable Style/GuardClause
71
+ if errors && !errors.empty?
72
+ raise Vagrant::Errors::ConfigInvalid,
73
+ errors: Vagrant::Util::TemplateRenderer.render(
74
+ "config/validation_failed",
75
+ errors: errors,
76
+ )
77
+ end
78
+ # rubocop:enable Style/GuardClause
79
+ end
80
+
81
+ # Validate a bolt config object for logical errors
82
+ def validate_config
83
+ errors = []
84
+ errors << I18n.t('vagrant-bolt.config.bolt.errors.command_not_specified') if @boltconfig.command.nil?
85
+ errors << I18n.t('vagrant-bolt.config.bolt.errors.no_task_or_plan') if @boltconfig.name.nil?
86
+ { "Bolt" => errors }
87
+ end
88
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'machine'
4
+
5
+ module VagrantBolt::Util
6
+ module Bolt
7
+ # Bolt Centric Utility Functions
8
+
9
+ # Create a bolt command from the config
10
+ # @param config [Object] The config objects
11
+ # @param inventory_path [String] The path of the inventory file
12
+ # @return [String] The bolt command
13
+ def self.generate_bolt_command(config, inventory_path = nil)
14
+ command = []
15
+ command << config.bolt_exe
16
+ command << "#{config.command} run \'#{config.name}\'"
17
+
18
+ config.instance_variables_hash.each do |key, value|
19
+ next if key.to_s.start_with?('__')
20
+ next if config.blacklist.include?(key)
21
+ next if value.nil?
22
+
23
+ key = key.tr('_', '-')
24
+ case value
25
+ when TrueClass, FalseClass
26
+ # Verbose and debug do not have --no flags so exclude them
27
+ next if ['verbose', 'debug', 'noop'].include?(key) && !value
28
+
29
+ arg = value ? "--#{key}" : "--no-#{key}"
30
+ command << arg
31
+ when String
32
+ command << "--#{key} \'#{value}\'"
33
+ when Hash
34
+ command << "--#{key} \'#{value.to_json}\'" unless value.empty?
35
+ end
36
+ end
37
+
38
+ command << "--inventoryfile \'#{inventory_path}\'" unless inventory_path.nil?
39
+ command << "--nodes \'#{config.node_list}\'" unless config.node_list.nil?
40
+ command << config.args unless config.args.nil?
41
+ command.flatten.join(" ")
42
+ end
43
+
44
+ # Generate a bolt inventory hash for the environment
45
+ # @param env [Object] The env object
46
+ # @return [Hash] The hash of config options for the inventory.yaml
47
+ def self.generate_inventory_hash(env)
48
+ inventory = { 'nodes' => [] }
49
+ inventory.merge!(env.vagrantfile.config.bolt.inventory_config.compact)
50
+ VagrantBolt::Util::Machine.nodes_in_environment(env).each do |vm|
51
+ next unless VagrantBolt::Util::Machine.running?(vm)
52
+
53
+ inventory['nodes'] << generate_node_hash(vm)
54
+ end
55
+ inventory.compact
56
+ end
57
+
58
+ # Generate a bolt inventory node hash from the VM config
59
+ # @param machine [Object] The machine object
60
+ # @return [Hash] The hash of config options for the VM
61
+ def self.generate_node_hash(machine)
62
+ # Only call ssh_info once
63
+ node_hash = {}
64
+ ssh_info = machine.ssh_info
65
+ return node_hash if ssh_info.nil?
66
+
67
+ node_hash['alias'] = machine.name.to_s
68
+ machine_config = machine.config.bolt.inventory_config
69
+ node_hash['config'] = {}
70
+ transport = VagrantBolt::Util::Machine.windows?(machine) ? 'winrm' : 'ssh'
71
+ node_hash['config'][transport] = machine_transport_hash(machine, machine_config, ssh_info).compact
72
+ node_hash['config']['transport'] = transport
73
+ node_hash['name'] = "#{transport}://#{ssh_info[:host]}:#{node_hash['config'][transport]['port']}"
74
+ machine_config.each do |key, value|
75
+ next if key == 'config' || value.nil? || value.empty?
76
+
77
+ node_hash[key] = value
78
+ end
79
+ node_hash.compact
80
+ end
81
+
82
+ # Return a transport config hash for a node
83
+ # @param machine [Object] The machine
84
+ # @param machine_config [Hash] A hash of the machine config options
85
+ # @param ssh_info [Hash] The ssh hash for the machine
86
+ def self.machine_transport_hash(machine, machine_config = {}, ssh_info = nil)
87
+ config = {}
88
+ if VagrantBolt::Util::Machine.windows?(machine)
89
+ transport = 'winrm'
90
+ config['ssl'] = (machine.config.winrm.transport == :ssl)
91
+ config['ssl_verify'] = machine.config.winrm.ssl_peer_verification
92
+ config['port'] = machine.config.winrm.port
93
+ config['user'] = machine.config.winrm.username
94
+ config['password'] = machine.config.winrm.password
95
+ else
96
+ transport = 'ssh'
97
+ config['private-key'] = ssh_info[:private_key_path][0] unless ssh_info[:private_key_path].nil?
98
+ config['host-key-check'] = (ssh_info[:verify_host_key] == true)
99
+ config['port'] = ssh_info[:port]
100
+ config['user'] = ssh_info[:username]
101
+ config['password'] = ssh_info[:password]
102
+ end
103
+ config.merge!(machine_config['config'][transport]) if machine_config.dig('config', transport)
104
+ config
105
+ end
106
+
107
+ # Return the path to the inventory file
108
+ # @param env [Object] The environment
109
+ # @return [String] The path to the inventory file
110
+ def self.inventory_file(env)
111
+ File.join(env.local_data_path, 'bolt_inventory.yaml')
112
+ end
113
+
114
+ # Update and write the inventory file for the current running machines
115
+ # @param env [Object] The envionment object
116
+ # @param inventory_file [String] The path where the inventory_file should be written.
117
+ # @return path to the inventory file
118
+ def self.update_inventory_file(env, inventory_file = nil)
119
+ inventory = generate_inventory_hash(env).to_yaml
120
+ inventory_file ||= Pathname.new(inventory_file(env))
121
+ # TODO: This lock should be global
122
+ lock = Mutex.new
123
+ lock.synchronize do
124
+ if !File.exist?(inventory_file) || (inventory != File.read(inventory_file))
125
+ begin
126
+ inventory_tmpfile = Tempfile.new('.vagrant_bolt_inventory', env.local_data_path)
127
+ inventory_tmpfile.write(inventory)
128
+ inventory_tmpfile.close
129
+ File.rename(inventory_tmpfile.path, inventory_file)
130
+ ensure
131
+ inventory_tmpfile.close
132
+ inventory_tmpfile.unlink
133
+ end
134
+ end
135
+ end
136
+ inventory_file
137
+ end
138
+ end
139
+ end