puppet 3.5.0.rc3 → 3.5.1.rc1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of puppet might be problematic. Click here for more details.

Files changed (54) hide show
  1. data/lib/puppet.rb +11 -2
  2. data/lib/puppet/defaults.rb +3 -2
  3. data/lib/puppet/environments.rb +16 -0
  4. data/lib/puppet/network/http/api/v1.rb +6 -2
  5. data/lib/puppet/network/http/issues.rb +1 -0
  6. data/lib/puppet/node/environment.rb +9 -5
  7. data/lib/puppet/pops/parser/interpolation_support.rb +8 -4
  8. data/lib/puppet/provider/yumrepo/inifile.rb +60 -20
  9. data/lib/puppet/test/test_helper.rb +1 -8
  10. data/lib/puppet/type/yumrepo.rb +43 -9
  11. data/lib/puppet/util/inifile.rb +209 -86
  12. data/lib/puppet/version.rb +1 -1
  13. data/spec/integration/application/apply_spec.rb +3 -1
  14. data/spec/integration/directory_environments_spec.rb +2 -1
  15. data/spec/integration/indirector/file_content/file_server_spec.rb +1 -1
  16. data/spec/integration/node/environment_spec.rb +2 -2
  17. data/spec/integration/resource/type_collection_spec.rb +1 -1
  18. data/spec/unit/application/apply_spec.rb +1 -1
  19. data/spec/unit/environments_spec.rb +35 -36
  20. data/spec/unit/face/module/install_spec.rb +1 -1
  21. data/spec/unit/face/module/list_spec.rb +3 -3
  22. data/spec/unit/face/module/uninstall_spec.rb +1 -1
  23. data/spec/unit/face/parser_spec.rb +1 -1
  24. data/spec/unit/indirector/node/active_record_spec.rb +1 -1
  25. data/spec/unit/indirector/node/ldap_spec.rb +4 -4
  26. data/spec/unit/indirector/node/plain_spec.rb +1 -1
  27. data/spec/unit/indirector/request_spec.rb +3 -3
  28. data/spec/unit/module_spec.rb +6 -6
  29. data/spec/unit/module_tool/applications/installer_spec.rb +1 -1
  30. data/spec/unit/module_tool/applications/uninstaller_spec.rb +1 -1
  31. data/spec/unit/module_tool_spec.rb +1 -1
  32. data/spec/unit/network/http/api/v1_spec.rb +11 -1
  33. data/spec/unit/network/http/api/v2/environments_spec.rb +1 -1
  34. data/spec/unit/node/environment_spec.rb +1 -1
  35. data/spec/unit/node_spec.rb +1 -1
  36. data/spec/unit/parser/ast/collection_spec.rb +1 -1
  37. data/spec/unit/parser/compiler_spec.rb +1 -1
  38. data/spec/unit/parser/files_spec.rb +2 -2
  39. data/spec/unit/parser/functions_spec.rb +2 -2
  40. data/spec/unit/parser/parser_spec.rb +2 -2
  41. data/spec/unit/parser/resource_spec.rb +1 -1
  42. data/spec/unit/parser/scope_spec.rb +3 -3
  43. data/spec/unit/pops/parser/lexer2_spec.rb +11 -0
  44. data/spec/unit/pops/parser/parse_heredoc_spec.rb +15 -1
  45. data/spec/unit/provider/yumrepo/inifile_spec.rb +71 -23
  46. data/spec/unit/rails/host_spec.rb +1 -1
  47. data/spec/unit/resource/type_collection_spec.rb +1 -1
  48. data/spec/unit/resource/type_spec.rb +1 -1
  49. data/spec/unit/resource_spec.rb +1 -1
  50. data/spec/unit/type/yumrepo_spec.rb +214 -49
  51. data/spec/unit/util/autoload_spec.rb +1 -1
  52. data/spec/unit/util/inifile_spec.rb +492 -0
  53. data/spec/unit/util/rdoc/parser_spec.rb +2 -2
  54. metadata +3009 -3030
@@ -177,8 +177,17 @@ module Puppet
177
177
  environments = settings[:environmentpath]
178
178
  modulepath = Puppet::Node::Environment.split_path(settings[:basemodulepath])
179
179
 
180
- loaders = Puppet::Environments::Directories.from_path(environments, modulepath)
181
- loaders << Puppet::Environments::Legacy.new
180
+ if environments.empty?
181
+ loaders = [Puppet::Environments::Legacy.new]
182
+ else
183
+ loaders = Puppet::Environments::Directories.from_path(environments, modulepath)
184
+ # in case the configured environment (used for the default sometimes)
185
+ # doesn't exist
186
+ loaders << Puppet::Environments::StaticPrivate.new(
187
+ Puppet::Node::Environment.create(Puppet[:environment].to_sym,
188
+ [],
189
+ Puppet::Node::Environment::NO_MANIFEST))
190
+ end
182
191
 
183
192
  {
184
193
  :environments => Puppet::Environments::Combined.new(*loaders)
@@ -197,8 +197,9 @@ module Puppet
197
197
  this provides the default environment for nodes we know nothing about."
198
198
  },
199
199
  :environmentpath => {
200
- :default => "$confdir/environments",
201
- :desc => "A path of environment directories",
200
+ :default => "",
201
+ :desc => "A search path for environment directories, as a list of directories separated by the system
202
+ path separator character.",
202
203
  :type => :path,
203
204
  },
204
205
  :diff_args => {
@@ -60,6 +60,22 @@ module Puppet::Environments
60
60
  end
61
61
  end
62
62
 
63
+ # A source of unlisted pre-defined environments.
64
+ #
65
+ # Used only for internal bootstrapping environments which are not relevant
66
+ # to an end user (such as the fall back 'configured' environment).
67
+ #
68
+ # @api private
69
+ class StaticPrivate < Static
70
+
71
+ # Unlisted
72
+ #
73
+ # @!macro loader_list
74
+ def list
75
+ []
76
+ end
77
+ end
78
+
63
79
  # Old-style environments that come either from explicit stanzas in
64
80
  # puppet.conf or from dynamic environments created from use of `$environment`
65
81
  # in puppet.conf.
@@ -64,8 +64,12 @@ class Puppet::Network::HTTP::API::V1
64
64
  method = indirection_method(http_method, indirection)
65
65
 
66
66
  configured_environment = Puppet.lookup(:environments).get(environment)
67
- configured_environment = configured_environment.override_from_commandline(Puppet.settings)
68
- params[:environment] = configured_environment
67
+ if configured_environment.nil?
68
+ raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new("Could not find #{environment}", Puppet::Network::HTTP::Issues::ENVIRONMENT_NOT_FOUND)
69
+ else
70
+ configured_environment = configured_environment.override_from_commandline(Puppet.settings)
71
+ params[:environment] = configured_environment
72
+ end
69
73
 
70
74
  params.delete(:bucket_path)
71
75
 
@@ -2,6 +2,7 @@ module Puppet::Network::HTTP::Issues
2
2
  NO_INDIRECTION_REMOTE_REQUESTS = :NO_INDIRECTION_REMOTE_REQUESTS
3
3
  HANDLER_NOT_FOUND = :HANDLER_NOT_FOUND
4
4
  RESOURCE_NOT_FOUND = :RESOURCE_NOT_FOUND
5
+ ENVIRONMENT_NOT_FOUND = :ENVIRONMENT_NOT_FOUND
5
6
  RUNTIME_ERROR = :RUNTIME_ERROR
6
7
  MISSING_HEADER_FIELD = :MISSING_HEADER_FIELD
7
8
  UNSUPPORTED_FORMAT = :UNSUPPORTED_FORMAT
@@ -22,9 +22,10 @@ end
22
22
  # logging functions. Logging functions are attached to the 'root' environment
23
23
  # when {Puppet::Parser::Functions.reset} is called.
24
24
  class Puppet::Node::Environment
25
-
26
25
  include Puppet::Util::Cacher
27
26
 
27
+ NO_MANIFEST = :no_manifest
28
+
28
29
  # @api private
29
30
  def self.seen
30
31
  @seen ||= {}
@@ -72,16 +73,17 @@ class Puppet::Node::Environment
72
73
  #
73
74
  # @param name [Symbol] the name of the
74
75
  # @param modulepath [Array<String>] the list of paths from which to load modules
75
- # @param manifest [String] the path to the manifest for the environment
76
+ # @param manifest [String] the path to the manifest for the environment or
77
+ # the constant Puppet::Node::Environment::NO_MANIFEST if there is none.
76
78
  # @return [Puppet::Node::Environment]
77
79
  #
78
80
  # @api public
79
- def self.create(name, modulepath, manifest)
81
+ def self.create(name, modulepath, manifest = NO_MANIFEST)
80
82
  obj = self.allocate
81
83
  obj.send(:initialize,
82
84
  name,
83
85
  expand_dirs(extralibs() + modulepath),
84
- File.expand_path(manifest))
86
+ manifest == NO_MANIFEST ? manifest : File.expand_path(manifest))
85
87
  obj
86
88
  end
87
89
 
@@ -459,7 +461,9 @@ class Puppet::Node::Environment
459
461
  file = self.manifest
460
462
  # if the manifest file is a reference to a directory, parse and combine all .pp files in that
461
463
  # directory
462
- if File.directory?(file)
464
+ if file == NO_MANIFEST
465
+ Puppet::Parser::AST::Hostclass.new('')
466
+ elsif File.directory?(file)
463
467
  parse_results = Dir.entries(file).find_all { |f| f =~ /\.pp$/ }.sort.map do |pp_file|
464
468
  parser.file = File.join(file, pp_file)
465
469
  parser.parse
@@ -44,8 +44,9 @@ module Puppet::Pops::Parser::InterpolationSupport
44
44
  break
45
45
  else
46
46
  # false $ variable start
47
- text += value
47
+ text += terminator
48
48
  value,terminator = slurp_dqstring()
49
+ text += value
49
50
  after = scn.pos
50
51
  end
51
52
  end
@@ -84,8 +85,9 @@ module Puppet::Pops::Parser::InterpolationSupport
84
85
  break
85
86
  else
86
87
  # false $ variable start
88
+ text += terminator
89
+ value,terminator = slurp_dqstring
87
90
  text += value
88
- value,terminator = self.send(slurpfunc)
89
91
  after = scn.pos
90
92
  end
91
93
  end
@@ -127,8 +129,9 @@ module Puppet::Pops::Parser::InterpolationSupport
127
129
  break
128
130
  else
129
131
  # false $ variable start
130
- text += value
132
+ text += terminator
131
133
  value,terminator = slurp_uqstring()
134
+ text += value
132
135
  after = scn.pos
133
136
  end
134
137
  end
@@ -166,8 +169,9 @@ module Puppet::Pops::Parser::InterpolationSupport
166
169
  break
167
170
  else
168
171
  # false $ variable start
169
- text += value
172
+ text += terminator
170
173
  value,terminator = slurp_uqstring
174
+ text += value
171
175
  after = scn.pos
172
176
  end
173
177
  end
@@ -18,7 +18,9 @@ Puppet::Type.type(:yumrepo).provide(:inifile) do
18
18
  if valid_property?(key)
19
19
  # We strip the values here to handle cases where distros set values
20
20
  # like enabled = 1 with spaces.
21
- attributes_hash[key] = value.strip
21
+ attributes_hash[key] = value
22
+ elsif key == :name
23
+ attributes_hash[:descr] = value
22
24
  end
23
25
  end
24
26
  instances << new(attributes_hash)
@@ -132,7 +134,7 @@ Puppet::Type.type(:yumrepo).provide(:inifile) do
132
134
  def create
133
135
  @property_hash[:ensure] = :present
134
136
 
135
- new_section = section(@resource[:name])
137
+ new_section = current_section
136
138
 
137
139
  # We fetch a list of properties from the type, then iterate
138
140
  # over them, avoiding ensure. We're relying on .should to
@@ -140,19 +142,25 @@ Puppet::Type.type(:yumrepo).provide(:inifile) do
140
142
  # and if so we set it in the virtual inifile.
141
143
  PROPERTIES.each do |property|
142
144
  next if property == :ensure
145
+
146
+
143
147
  if value = @resource.should(property)
144
- new_section[property.to_s] = value
145
- @property_hash[property] = value
148
+ self.send("#{property}=", value)
146
149
  end
147
150
  end
148
151
  end
149
152
 
153
+ # @return [Boolean] Returns true if ensure => present.
154
+ def exists?
155
+ @property_hash[:ensure] == :present
156
+ end
157
+
150
158
  # We don't actually destroy the file here, merely mark it for
151
159
  # destruction in the section.
152
160
  # @return [void]
153
161
  def destroy
154
162
  # Flag file for deletion on flush.
155
- section(@resource[:name]).destroy=(true)
163
+ current_section.destroy=(true)
156
164
 
157
165
  @property_hash.clear
158
166
  end
@@ -162,26 +170,58 @@ Puppet::Type.type(:yumrepo).provide(:inifile) do
162
170
  self.class.store
163
171
  end
164
172
 
165
- # @return [void]
166
- def section(name)
167
- self.class.section(name)
168
- end
169
-
170
- # Create all of our setters.
171
- mk_resource_methods
173
+ # Generate setters and getters for our INI properties.
172
174
  PROPERTIES.each do |property|
173
- # Exclude ensure, as we don't need to create an ensure=
175
+ # The ensure property uses #create, #exists, and #destroy we can't generate
176
+ # meaningful setters and getters for this
174
177
  next if property == :ensure
175
- # Builds the property= method.
176
- define_method("#{property.to_s}=") do |value|
177
- section(@property_hash[:name])[property.to_s] = value
178
- @property_hash[property] = value
178
+
179
+ define_method(property) do
180
+ get_property(property)
181
+ end
182
+
183
+ define_method("#{property}=") do |value|
184
+ set_property(property, value)
179
185
  end
180
186
  end
181
187
 
182
- # @return [Boolean] Returns true if ensure => present.
183
- def exists?
184
- @property_hash[:ensure] == :present
188
+ # Map the yumrepo 'descr' type property to the 'name' INI property.
189
+ def descr
190
+ if ! @property_hash.has_key?(:descr)
191
+ @property_hash[:descr] = current_section['name']
192
+ end
193
+ value = @property_hash[:descr]
194
+ value.nil? ? :absent : value
195
+ end
196
+
197
+ def descr=(value)
198
+ value = (value == :absent ? nil : value)
199
+ current_section['name'] = value
200
+ @property_hash[:descr] = value
201
+ end
202
+
203
+ private
204
+
205
+ def get_property(property)
206
+ if ! @property_hash.has_key?(property)
207
+ @property_hash[property] = current_section[property.to_s]
208
+ end
209
+ value = @property_hash[property]
210
+ value.nil? ? :absent : value
185
211
  end
186
212
 
213
+ def set_property(property, value)
214
+ value = (value == :absent ? nil : value)
215
+ current_section[property.to_s] = value
216
+ @property_hash[property] = value
217
+ end
218
+
219
+ # @return [void]
220
+ def section(name)
221
+ self.class.section(name)
222
+ end
223
+
224
+ def current_section
225
+ self.class.section(self.name)
226
+ end
187
227
  end
@@ -37,19 +37,12 @@ module Puppet::Test
37
37
  # @return nil
38
38
  def self.initialize()
39
39
  owner = Process.pid
40
- @environmentdir = Dir.mktmpdir('environments')
41
40
  Puppet.push_context(Puppet.base_context({
42
- :environmentpath => @environmentdir,
41
+ :environmentpath => "",
43
42
  :basemodulepath => "",
44
43
  :manifest => "/dev/null"
45
44
  }), "Initial for specs")
46
45
  Puppet::Parser::Functions.reset
47
-
48
- ObjectSpace.define_finalizer(Puppet.lookup(:environments), proc {
49
- if Process.pid == owner
50
- FileUtils.rm_rf(@environmentdir)
51
- end
52
- })
53
46
  end
54
47
 
55
48
  # Call this method once, when beginning a test run--prior to running
@@ -18,9 +18,11 @@ Puppet::Type.newtype(:yumrepo) do
18
18
  # Doc string for properties that can be made 'absent'
19
19
  ABSENT_DOC="Set this to `absent` to remove it from the file completely."
20
20
  # False can be false/0/no and True can be true/1/yes in yum.
21
- YUM_BOOLEAN=/(True|False|0|1|No|Yes)/
21
+ YUM_BOOLEAN=/(True|False|0|1|No|Yes)/i
22
22
  YUM_BOOLEAN_DOC="Valid values are: False/0/No or True/1/Yes."
23
23
 
24
+ VALID_SCHEMES = %w[file http https ftp]
25
+
24
26
  newparam(:name, :namevar => true) do
25
27
  desc "The name of the repository. This corresponds to the
26
28
  `repositoryid` parameter in `yum.conf(5)`."
@@ -46,8 +48,12 @@ Puppet::Type.newtype(:yumrepo) do
46
48
 
47
49
  newvalues(/.*/, :absent)
48
50
  validate do |value|
51
+ next if value.to_s == 'absent'
49
52
  parsed = URI.parse(value)
50
- fail("Must be a valid URL") unless ['file', 'http', 'https', 'ftp'].include?(parsed.scheme)
53
+
54
+ unless VALID_SCHEMES.include?(parsed.scheme)
55
+ raise "Must be a valid URL"
56
+ end
51
57
  end
52
58
  end
53
59
 
@@ -56,8 +62,16 @@ Puppet::Type.newtype(:yumrepo) do
56
62
 
57
63
  newvalues(/.*/, :absent)
58
64
  validate do |value|
59
- parsed = URI.parse(value)
60
- fail("Must be a valid URL") unless ['file', 'http', 'https', 'ftp'].include?(parsed.scheme)
65
+ next if value.to_s == 'absent'
66
+
67
+ value.split(/\s+/).each do |uri|
68
+
69
+ parsed = URI.parse(uri)
70
+
71
+ unless VALID_SCHEMES.include?(parsed.scheme)
72
+ raise "Must be a valid URL"
73
+ end
74
+ end
61
75
  end
62
76
  end
63
77
 
@@ -92,8 +106,16 @@ Puppet::Type.newtype(:yumrepo) do
92
106
 
93
107
  newvalues(/.*/, :absent)
94
108
  validate do |value|
95
- parsed = URI.parse(value)
96
- fail("Must be a valid URL") unless ['file', 'http', 'https', 'ftp'].include?(parsed.scheme)
109
+ next if value.to_s == 'absent'
110
+
111
+ value.split(/\s+/).each do |uri|
112
+
113
+ parsed = URI.parse(uri)
114
+
115
+ unless VALID_SCHEMES.include?(parsed.scheme)
116
+ raise "Must be a valid URL"
117
+ end
118
+ end
97
119
  end
98
120
  end
99
121
 
@@ -104,8 +126,12 @@ Puppet::Type.newtype(:yumrepo) do
104
126
 
105
127
  newvalues(/.*/, :absent)
106
128
  validate do |value|
129
+ next if value.to_s == 'absent'
107
130
  parsed = URI.parse(value)
108
- fail("Must be a valid URL") unless ['file', 'http', 'https', 'ftp'].include?(parsed.scheme)
131
+
132
+ unless VALID_SCHEMES.include?(parsed.scheme)
133
+ raise "Must be a valid URL"
134
+ end
109
135
  end
110
136
  end
111
137
 
@@ -202,8 +228,12 @@ Puppet::Type.newtype(:yumrepo) do
202
228
 
203
229
  newvalues(/.*/, :absent)
204
230
  validate do |value|
231
+ next if value.to_s == 'absent'
205
232
  parsed = URI.parse(value)
206
- fail("Must be a valid URL") unless ['file', 'http', 'https', 'ftp'].include?(parsed.scheme)
233
+
234
+ unless VALID_SCHEMES.include?(parsed.scheme)
235
+ raise "Must be a valid URL"
236
+ end
207
237
  end
208
238
  end
209
239
 
@@ -262,8 +292,12 @@ Puppet::Type.newtype(:yumrepo) do
262
292
 
263
293
  newvalues(/.*/, :absent)
264
294
  validate do |value|
295
+ next if value.to_s == 'absent'
265
296
  parsed = URI.parse(value)
266
- fail("Must be a valid URL") unless ['file', 'http', 'https', 'ftp'].include?(parsed.scheme)
297
+
298
+ unless VALID_SCHEMES.include?(parsed.scheme)
299
+ raise "Must be a valid URL"
300
+ end
267
301
  end
268
302
  end
269
303
 
@@ -9,6 +9,7 @@
9
9
  # The parsing tries to stay close to python's ConfigParser
10
10
 
11
11
  require 'puppet/util/filetype'
12
+ require 'puppet/error'
12
13
 
13
14
  module Puppet::Util::IniConfig
14
15
  # A section in a .ini file
@@ -24,10 +25,17 @@ module Puppet::Util::IniConfig
24
25
  @destroy = false
25
26
  end
26
27
 
27
- # Has this section been modified since it's been read in
28
- # or written back to disk
28
+ # Does this section need to be updated in/removed from the associated file?
29
+ #
30
+ # @note This section is dirty if a key has been modified _or_ if the
31
+ # section has been modified so the associated file can be rewritten
32
+ # without this section.
29
33
  def dirty?
30
- @dirty
34
+ @dirty or @destroy
35
+ end
36
+
37
+ def mark_dirty
38
+ @dirty = true
31
39
  end
32
40
 
33
41
  # Should only be used internally
@@ -69,13 +77,17 @@ module Puppet::Util::IniConfig
69
77
  # Format the section as text in the way it should be
70
78
  # written to file
71
79
  def format
72
- text = "[#{name}]\n"
73
- @entries.each do |entry|
74
- if entry.is_a?(Array)
75
- key, value = entry
76
- text << "#{key}=#{value}\n" unless value.nil?
77
- else
78
- text << entry
80
+ if @destroy
81
+ text = ""
82
+ else
83
+ text = "[#{name}]\n"
84
+ @entries.each do |entry|
85
+ if entry.is_a?(Array)
86
+ key, value = entry
87
+ text << "#{key}=#{value}\n" unless value.nil?
88
+ else
89
+ text << entry
90
+ end
79
91
  end
80
92
  end
81
93
  text
@@ -91,128 +103,239 @@ module Puppet::Util::IniConfig
91
103
 
92
104
  end
93
105
 
94
- # A logical .ini-file that can be spread across several physical
95
- # files. For each physical file, call #read with the filename
96
- class File
97
- def initialize
98
- @files = {}
106
+ class PhysicalFile
107
+
108
+ # @!attribute [r] filetype
109
+ # @api private
110
+ # @return [Puppet::Util::FileType::FileTypeFlat]
111
+ attr_reader :filetype
112
+
113
+ # @!attribute [r] contents
114
+ # @api private
115
+ # @return [Array<String, Puppet::Util::IniConfig::Section>]
116
+ attr_reader :contents
117
+
118
+ # @!attribute [rw] destroy_empty
119
+ # Whether empty files should be removed if no sections are defined.
120
+ # Defaults to false
121
+ attr_accessor :destroy_empty
122
+
123
+ # @!attribute [rw] file_collection
124
+ # @return [Puppet::Util::IniConfig::FileCollection]
125
+ attr_accessor :file_collection
126
+
127
+ def initialize(file, options = {})
128
+ @file = file
129
+ @contents = []
130
+ @filetype = Puppet::Util::FileType.filetype(:flat).new(file)
131
+
132
+ @destroy_empty = options.fetch(:destroy_empty, false)
99
133
  end
100
134
 
101
- # Add the contents of the file with name FILE to the
102
- # already existing sections
103
- def read(file)
104
- text = Puppet::Util::FileType.filetype(:flat).new(file).read
105
- raise "Could not find #{file}" if text.nil?
135
+ # Read and parse the on-disk file associated with this object
136
+ def read
137
+ text = @filetype.read
138
+ if text.nil?
139
+ raise IniParseError, "Cannot read nonexistent file #{@file.inspect}"
140
+ end
141
+ parse(text)
142
+ end
143
+
144
+ INI_COMMENT = Regexp.union(
145
+ /^\s*$/,
146
+ /^[#;]/,
147
+ /^\s*rem\s/i
148
+ )
149
+ INI_CONTINUATION = /^[ \t\r\n\f]/
150
+ INI_SECTION_NAME = /^\[([^\]]+)\]/
151
+ INI_PROPERTY = /^\s*([^\s=]+)\s*\=(.*)$/
106
152
 
153
+ # @api private
154
+ def parse(text)
107
155
  section = nil # The name of the current section
108
156
  optname = nil # The name of the last option in section
109
- line = 0
110
- @files[file] = []
157
+ line_num = 0
158
+
111
159
  text.each_line do |l|
112
- line += 1
113
- if l.strip.empty? || "#;".include?(l[0,1]) ||
114
- (l.split(nil, 2)[0].downcase == "rem" && l[0,1].downcase == "r")
160
+ line_num += 1
161
+ if l.match(INI_COMMENT)
115
162
  # Whitespace or comment
116
163
  if section.nil?
117
- @files[file] << l
164
+ @contents << l
118
165
  else
119
166
  section.add_line(l)
120
167
  end
121
- elsif " \t\r\n\f".include?(l[0,1]) && section && optname
168
+ elsif l.match(INI_CONTINUATION) && section && optname
122
169
  # continuation line
123
170
  section[optname] += "\n#{l.chomp}"
124
- elsif l =~ /^\[([^\]]+)\]/
171
+ elsif (match = l.match(INI_SECTION_NAME))
125
172
  # section heading
126
- section.mark_clean unless section.nil?
127
- section = add_section($1, file)
173
+ section.mark_clean if section
174
+
175
+ section_name = match[1]
176
+
177
+ section = add_section(section_name)
128
178
  optname = nil
129
- elsif l =~ /^\s*([^\s=]+)\s*\=(.*)$/
179
+ elsif (match = l.match(INI_PROPERTY))
130
180
  # We allow space around the keys, but not the values
131
181
  # For the values, we don't know if space is significant
182
+ key = match[1]
183
+ val = match[2]
184
+
132
185
  if section.nil?
133
- raise "#{file}:#{line}:Key/value pair outside of a section for key #{$1}"
134
- else
135
- section[$1] = $2
136
- optname = $1
186
+ raise IniParseError.new("Property with key #{key.inspect} outside of a section")
137
187
  end
188
+
189
+ section[key] = val
190
+ optname = key
138
191
  else
139
- raise "#{file}:#{line}: Can't parse '#{l.chomp}'"
192
+ raise IniParseError.new("Can't parse line '#{l.chomp}'", @file, line_num)
140
193
  end
141
194
  end
142
195
  section.mark_clean unless section.nil?
143
196
  end
144
197
 
145
- # Store all modifications made to sections in this file back
146
- # to the physical files. If no modifications were made to
147
- # a physical file, nothing is written
148
- def store
149
- @files.each do |file, lines|
150
- text = ""
151
- dirty = false
152
- destroy = false
153
- lines.each do |l|
154
- if l.is_a?(Section)
155
- destroy ||= l.destroy?
156
- dirty ||= l.dirty?
157
- text << l.format
158
- l.mark_clean
159
- else
160
- text << l
161
- end
162
- end
163
- # We delete the file and then remove it from the list of files.
164
- if destroy
165
- ::File.unlink(file)
166
- @files.delete(file)
198
+ # @return [Array<Puppet::Util::IniConfig::Section>] All sections defined in
199
+ # this file.
200
+ def sections
201
+ @contents.select { |entry| entry.is_a? Section }
202
+ end
203
+
204
+ # @return [Puppet::Util::IniConfig::Section, nil] The section with the
205
+ # given name if it exists, else nil.
206
+ def get_section(name)
207
+ @contents.find { |entry| entry.is_a? Section and entry.name == name }
208
+ end
209
+
210
+ def format
211
+ text = ""
212
+
213
+ @contents.each do |content|
214
+ if content.is_a? Section
215
+ text << content.format
167
216
  else
168
- if dirty
169
- Puppet::Util::FileType.filetype(:flat).new(file).write(text)
170
- return file
171
- end
217
+ text << content
172
218
  end
173
219
  end
220
+
221
+ text
222
+ end
223
+
224
+ def store
225
+ if @destroy_empty and (sections.empty? or sections.all?(&:destroy?))
226
+ ::File.unlink(@file)
227
+ elsif sections.any?(&:dirty?)
228
+ text = self.format
229
+ @filetype.write(text)
230
+ end
231
+ sections.each(&:mark_clean)
232
+ end
233
+
234
+ # Create a new section and store it in the file contents
235
+ #
236
+ # @api private
237
+ # @param name [String] The name of the section to create
238
+ # @return [Puppet::Util::IniConfig::Section]
239
+ def add_section(name)
240
+ if section_exists?(name)
241
+ raise IniParseError.new("Section #{name.inspect} is already defined, cannot redefine", @file)
242
+ end
243
+
244
+ section = Section.new(name, @file)
245
+ @contents << section
246
+
247
+ section
248
+ end
249
+
250
+ private
251
+
252
+ def section_exists?(name)
253
+ if self.get_section(name)
254
+ true
255
+ elsif @file_collection and @file_collection.get_section(name)
256
+ true
257
+ else
258
+ false
259
+ end
260
+ end
261
+ end
262
+
263
+ class FileCollection
264
+
265
+ attr_reader :files
266
+
267
+ def initialize
268
+ @files = {}
269
+ end
270
+
271
+ # Read and parse a file and store it in the collection. If the file has
272
+ # already been read it will be destroyed and re-read.
273
+ def read(file)
274
+ new_physical_file(file).read
275
+ end
276
+
277
+ def store
278
+ @files.values.each do |file|
279
+ file.store
280
+ end
174
281
  end
175
282
 
176
- # Execute BLOCK, passing each section in this file
177
- # as an argument
178
283
  def each_section(&block)
179
- @files.each do |file, list|
180
- list.each do |entry|
181
- yield(entry) if entry.is_a?(Section)
284
+ @files.values.each do |file|
285
+ file.sections.each do |section|
286
+ yield section
182
287
  end
183
288
  end
184
289
  end
185
290
 
186
- # Execute BLOCK, passing each file constituting this inifile
187
- # as an argument
188
291
  def each_file(&block)
189
- @files.keys.each do |file|
190
- yield(file)
292
+ @files.keys.each do |path|
293
+ yield path
191
294
  end
192
295
  end
193
296
 
194
- # Return the Section with the given name or nil
195
- def [](name)
196
- name = name.to_s
197
- each_section do |section|
198
- return section if section.name == name
297
+ def get_section(name)
298
+ sect = nil
299
+ @files.values.each do |file|
300
+ if (current = file.get_section(name))
301
+ sect = current
302
+ end
199
303
  end
200
- nil
304
+ sect
201
305
  end
306
+ alias [] get_section
202
307
 
203
- # Return true if the file contains a section with name NAME
204
308
  def include?(name)
205
- ! self[name].nil?
309
+ !! get_section(name)
206
310
  end
207
311
 
208
- # Add a section to be stored in FILE when store is called
209
312
  def add_section(name, file)
210
- raise "A section with name #{name} already exists" if include?(name)
211
- result = Section.new(name, file)
212
- @files[file] ||= []
213
- @files[file] << result
214
- result
313
+ get_physical_file(file).add_section(name)
314
+ end
315
+
316
+ private
317
+
318
+ # Return a file if it's already been defined, create a new file if it hasn't
319
+ # been defined.
320
+ def get_physical_file(file)
321
+ if @files[file]
322
+ @files[file]
323
+ else
324
+ new_physical_file(file)
325
+ end
326
+ end
327
+
328
+ # Create a new physical file and set required attributes on that file.
329
+ def new_physical_file(file)
330
+ @files[file] = PhysicalFile.new(file)
331
+ @files[file].file_collection = self
332
+ @files[file]
215
333
  end
216
334
  end
217
- end
218
335
 
336
+ File = FileCollection
337
+
338
+ class IniParseError < Puppet::Error
339
+ include Puppet::ExternalFileError
340
+ end
341
+ end