cdcab193393eaac7ed2f62c984c01c6c9e40d9a3
[puppet_sudo.git] / lib / puppet / provider / sudoers / parsed.rb
1 require 'puppet/provider/parsedfile'
2 sudoers = "/etc/sudoers"
3
4 #
5 # crontab does the same thing, it uses a comment to specify uniqueness
6 #
7  
8 Puppet::Type.type(:sudoers).provide(
9   :parsed, 
10   :parent => Puppet::Provider::ParsedFile, 
11   :default_target => '/etc/sudoers', 
12   # what the heck does this mean?
13   :filetype => :flat
14 ) do
15  
16   desc "The sudoers provider that uses the ParsedFile class"
17
18   commands :visudo => 'visudo'
19
20   # this is just copied from hosts
21   text_line :comment, :match => %r{^#}, 
22     :post_parse => proc { |record|
23     # shameful NAMEVAR hack :(
24       if record[:line] =~ /Puppet NAMEVAR (.+)\s*$/
25         record[:name] = $1
26       end
27     } 
28
29   text_line :blank, :match => /^\s*$/;
30
31   #
32   # parse lines as either a Defaults, Alias, or User spec.
33   #
34   # match everything and process entire line
35   record_line :parsed, :fields => %w{line},
36     :match => /(.*)/,
37     :post_parse => proc { |hash|
38       Puppet.debug("sudoers post_parse for line #{hash[:line]}")
39       # create records for aliases
40       if (hash[:line] =~ /^\s*((User|Runas|Host|Cmnd)_Alias)\s+(\S+)\s*=\s*(.+)$/)
41         Puppet.debug("parsed line as Alias")
42         Puppet::Type.type(:sudoers).provider(:parsed).parse_alias($1, $3, $4, hash)
43       elsif (hash[:line] =~ /^\s*(Defaults\S*)\s*(.*)$/)
44         Puppet.debug("parsed line as Defaults")
45         Puppet::Type.type(:sudoers).provider(:parsed).parse_defaults($1, $2, hash)
46       elsif (hash[:line] =~ /^\s*(.*)?=(.*)$/)
47         Puppet.debug("parsed line as User Spec")
48         Puppet::Type.type(:sudoers).provider(:parsed).parse_user_spec($1, $2, hash)
49       else 
50         raise Puppet::Error, "invalid line #{hash[:line]}"
51       end
52 #      puts hash.to_yaml
53 #      hash
54     }
55
56   # parse alias lines
57   def self.parse_alias(sudo_alias, name, items, hash)
58     hash[:type] = 'alias'
59     hash[:sudo_alias] = sudo_alias
60     hash[:name] = name
61     hash[:items] = clean_list(items) 
62     hash 
63   end
64
65   # parse existing user spec lines from sudoers
66   def self.parse_user_spec(users_hosts, commands, hash) 
67     hash[:type] = 'user_spec'
68     #hash[:name] = user
69     #hash[:hosts] = hosts.gsub(/\s/, '').split(',')
70     hash[:commands] = clean_list(commands)
71     hash_array = users_hosts.split(',')  
72     # every element will be a user until the hit the delim
73     currentsymbol = :users
74     hash[:users] = Array.new
75     hash[:hosts] = Array.new
76     # parsing users and hosts is kind of complicated, sorry
77     hash_array.each do |element|
78 #puts "!! #{element}"
79     # the element that splits users and hosts will be 2 white space delimited strings 
80       if element =~ /^\s*(\S+)\s+(\S+)\s*$/
81         user, host  = $1, $2
82         if currentsymbol == :hosts
83           raise Exception, 'found more than one whitespace delim in users_hosts' 
84         end
85         # sweet we found the delim between user and host
86         hash[currentsymbol] << user.gsub(/\s/, '')
87         # now everything else will be a host
88         currentsymbol=:hosts
89         hash[currentsymbol] << host.gsub(/\s/, '')
90       elsif element =~ /\s*\S+\s*/
91         hash[currentsymbol] << element.gsub(/\s/, '')
92       else
93         raise Exception, "Malformed user spec line lhs: #{lhs}"
94       end
95     end
96   end 
97
98   # create record for defaults line
99   def self.parse_defaults(default, parameters, hash)
100     hash[:name] = default
101     hash[:type] = 'default'
102     hash[:parameters] = parameters.gsub(/\s/, '').split(',')
103   end
104   
105   # I could use prefetch_hook to support multi-line entries
106   # will use the prefetch_hook to determine if
107   # the line before us is a commented namevar line
108   # only used for user spec.
109   # Most of this code is shameless taken from provider crontab.rb
110   # NAMEVAR comments leave me in need of a shower, but it seems to be the only way.
111   def self.prefetch_hook(records)
112     # store comment name vars when we find them
113     name=nil
114     results = records.each do |record|
115       if(record[:record_type] == :comment)
116         # if we are a namevar comment
117 #puts "found a comment: #{record.to_yaml}"
118         if record[:name]
119 #puts "found a comment with :name"
120           name = record[:name]
121           record[:skip] = true
122         end
123        # if we are a spec record, check the namevar
124       elsif record[:type] == 'user_spec'
125         if name
126 #puts "adding to a record"
127           record[:name] = name
128           name = nil
129         else
130           puts "spec record not created by puppet"
131           # probably a pre-exting record not created by puppet
132         end 
133       end
134     end.reject{|record| record[:skip]}
135     results
136   end
137
138  # overriding how lines are written to the file
139   def self.to_line(hash) 
140 #    puts "\nEntering self.to_line for #{hash[:name]}"
141     #puts "\n#{hash.to_yaml}\n"
142 #    # dynamically call a function based on the value of hash[:type]
143     if(hash[:record_type] == :blank || hash[:record_type] == :comment)
144       hash[:line]
145     elsif(hash[:sudo_alias])
146       self.alias_to_line(hash) 
147     elsif(hash[:commands])
148       self.spec_to_line(hash)
149     elsif(hash[:parameters])
150       self.default_to_line(hash)
151     else
152       raise Puppet::Error, "dont understand how to write out record \n|#{hash.to_yaml}\n|"
153     end
154   end
155
156   # write line for user spec records
157   def self.spec_to_line(hash)
158 #puts hash.to_yaml
159     #required
160     #users=self.array_convert(hash[:users])
161     # required
162     hosts=self.array_convert(hash[:hosts])
163     users=self.array_convert(hash[:users])
164     # required
165     commands=self.array_convert(hash[:commands])
166     str = "#Puppet NAMEVAR #{hash[:name]}"
167     str << "\n#{users} #{hosts}=#{commands}"
168     Puppet.notice "adding line:  #{str}"
169     str
170   end
171
172   # write line for alias records
173   def self.alias_to_line(hash) 
174     # do I need to ensure that the required elements are here?
175     # shouldnt the type do that? check file, its similar
176     # since different attributes make sense based on ensure value (dir/file/symlink)
177     items=self.array_convert(hash[:items])
178     str = "#{hash[:sudo_alias]} #{hash[:name]}=#{items}"
179     Puppet.notice "adding line: #{str}"
180     str
181   end
182
183   # write line for default records
184   # this is not implemented yet.
185   def self.default_to_line(hash)
186     parameters=self.array_convert(hash[:parameters])
187     str = "#{hash[:name]} #{parameters}"
188     Puppet.notice "Adding line #{str}"
189     str
190   end
191
192   # convert arrays into to , joined lists
193   def self.array_convert(list)
194     if list.class == Array
195       list.join(',')
196     else
197       list
198     end
199   end
200
201   # split a list on delim,  trim leading and trailing white-spaces
202   def self.clean_list(list, delim=',')
203     list.split(delim).collect do |x| 
204       x.gsub(/\s*(.*)?\s*/, '\1')
205     end
206   end
207
208   # used to verify files with visudo before they are flushed 
209   # flush seems to be called more than one time?
210   def self.flush_target(target)
211     Puppet.info("We are flushing #{target}")
212   #  a little pre-flush hot visudo action
213 #puts File.read(target)
214   visudo("-cf", target) unless (File.zero?(target) or !File.exists?(target))
215     super(target)
216   end
217 end