|
1 #! /usr/bin/python |
|
2 # |
|
3 # CDDL HEADER START |
|
4 # |
|
5 # The contents of this file are subject to the terms of the |
|
6 # Common Development and Distribution License (the "License"). |
|
7 # You may not use this file except in compliance with the License. |
|
8 # |
|
9 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE |
|
10 # or http://www.opensolaris.org/os/licensing. |
|
11 # See the License for the specific language governing permissions |
|
12 # and limitations under the License. |
|
13 # |
|
14 # When distributing Covered Code, include this CDDL HEADER in each |
|
15 # file and include the License file at usr/src/OPENSOLARIS.LICENSE. |
|
16 # If applicable, add the following below this CDDL HEADER, with the |
|
17 # fields enclosed by brackets "[]" replaced with your own identifying |
|
18 # information: Portions Copyright [yyyy] [name of copyright owner] |
|
19 # |
|
20 # CDDL HEADER END |
|
21 # |
|
22 |
|
23 # |
|
24 # Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. |
|
25 # |
|
26 |
|
27 ''' |
|
28 Utility program for helping with the upgrade of puppet to a newer |
|
29 version. This program will take a puppet configuration file that |
|
30 has been generated via the command sequence |
|
31 |
|
32 puppet agent --genconfig > puppet.conf |
|
33 |
|
34 and use the data in that configuration file to replace the |
|
35 associated Puppet SMF user configuratable properties with the |
|
36 properties that are allowed in the new version of puppet |
|
37 |
|
38 NOTE: This file should not be included with the puppet release |
|
39 ''' |
|
40 |
|
41 import os |
|
42 import re |
|
43 import sys |
|
44 |
|
45 from lxml import etree |
|
46 from optparse import OptionParser |
|
47 |
|
48 |
|
49 COMMENT_PATTERN = re.compile(".*# ?(.*)") |
|
50 CONFIG_VALUE_PATTERN = re.compile("([\S]+)\s*=\s*(\S*)") |
|
51 |
|
52 DEFAULT_VALUE_STR = "The default value is " |
|
53 |
|
54 # SMF defined property types. For a list of |
|
55 # all available types see |
|
56 # /usr/share/lib/xml/dtd/service_bundle.dtd.1 |
|
57 TYPE_ASTRING = "astring" |
|
58 TYPE_BOOLEAN = "boolean" |
|
59 TYPE_INTEGER = "integer" |
|
60 TYPE_HOST = "host" |
|
61 TYPE_HOSTNAME = "hostname" |
|
62 TYPE_NETADDRESS = "net_address" |
|
63 TYPE_URI = "uri" |
|
64 |
|
65 |
|
66 # Dictionary of currently defined property types to associate |
|
67 # with a specified property. Any property not defined here |
|
68 # is assumed to have a property type of astring, integer, |
|
69 # or boolean |
|
70 PROP_TYPE = { |
|
71 'server': TYPE_HOST, |
|
72 'archive_file_server': TYPE_HOST, |
|
73 'bindaddress': TYPE_NETADDRESS, |
|
74 'ca_server': TYPE_HOST, |
|
75 'certname': TYPE_HOSTNAME, |
|
76 'couchdb_url': TYPE_URI, |
|
77 'dbserver': TYPE_HOST, |
|
78 'dns_alt_names': TYPE_HOST, |
|
79 'http_proxy_host': TYPE_HOST, |
|
80 'inventory_server': TYPE_HOST, |
|
81 'ldapserver': TYPE_HOST, |
|
82 'ldapuser': TYPE_HOSTNAME, |
|
83 'module_repository': TYPE_URI, |
|
84 'queue_source': TYPE_URI, |
|
85 'report_server': TYPE_HOST, |
|
86 'reporturl': TYPE_URI, |
|
87 'smtpserver': TYPE_HOST, |
|
88 'srv_domain': TYPE_HOST, |
|
89 } |
|
90 |
|
91 # Dictionary used to hold properites and the resulting xml code |
|
92 PUPPET_CONFIG_DICT = dict() |
|
93 |
|
94 |
|
95 def err(msg): |
|
96 '''Output standard error message''' |
|
97 # Duplicate the syntax of the parser.error |
|
98 sys.stderr.write("%(prog)s: error: %(msg)s\n" % |
|
99 {"prog": os.path.basename(sys.argv[0]), "msg": msg}) |
|
100 |
|
101 |
|
102 def create_config_element(key, key_type, desc_text): |
|
103 '''Create a basic xml entry following the basic pattern of |
|
104 |
|
105 <prop_pattern name='${key}' type='${key_type}' |
|
106 required='false'> |
|
107 <description> <loctext xml:lang='C'> |
|
108 ${desc_text} |
|
109 </loctext> </description> |
|
110 </prop_pattern> |
|
111 ''' |
|
112 prop_pattern = etree.Element( |
|
113 "prop_pattern", |
|
114 name=key, |
|
115 type=key_type, |
|
116 required="false") |
|
117 desc = etree.SubElement(prop_pattern, "description") |
|
118 loctext = etree.SubElement(desc, "loctext") |
|
119 loctext.text = "\n%s\n\t " % desc_text |
|
120 loctext.set('{http://www.w3.org/XML/1998/namespace}lang', 'C') |
|
121 return prop_pattern |
|
122 |
|
123 |
|
124 def determine_type(key, value): |
|
125 '''Determine the xml property type to associate with the |
|
126 specified key |
|
127 ''' |
|
128 |
|
129 # Does the key have a specified xml property type |
|
130 # already defined? |
|
131 try: |
|
132 return PROP_TYPE[key] |
|
133 except KeyError: |
|
134 pass |
|
135 |
|
136 # Use the value to determine the xml property type |
|
137 if value.isdigit(): |
|
138 return TYPE_INTEGER |
|
139 if value.lower() in ['false', 'true']: |
|
140 return TYPE_BOOLEAN |
|
141 return TYPE_ASTRING |
|
142 |
|
143 |
|
144 def process_grouping(lines): |
|
145 '''Process the lines in the list. The last entry should be |
|
146 a 'key=value' entry |
|
147 ''' |
|
148 |
|
149 # The last field should be a key = value pair |
|
150 # If it's not then the format of the file is not matching |
|
151 # the expected format of |
|
152 # |
|
153 # Description |
|
154 # The default value is "xxxx" |
|
155 # key = value |
|
156 # |
|
157 key_value = lines.pop() |
|
158 match = CONFIG_VALUE_PATTERN.match(key_value) |
|
159 if not match: |
|
160 raise TypeError("Last line in grouping is not in expected " |
|
161 "format of 'key = value'\n%s" % |
|
162 "\n".join(lines)) |
|
163 key = match.group(1) |
|
164 value = match.group(2) |
|
165 |
|
166 default_value_line = lines.pop() |
|
167 if not default_value_line.startswith(DEFAULT_VALUE_STR): |
|
168 # Not a match. Last line was still part of the description |
|
169 lines.append(default_value_line) |
|
170 |
|
171 key_type = determine_type(key, value) |
|
172 |
|
173 # remaining lines are the descriptor field |
|
174 desc = '\n'.join(lines) |
|
175 PUPPET_CONFIG_DICT[key] = (key, key_type, desc) |
|
176 |
|
177 |
|
178 def parse_puppet_config(filename): |
|
179 '''Parse the puppet configuration file that is generated by |
|
180 puppet agent --genconfig |
|
181 ''' |
|
182 parameter_list = [] |
|
183 agent_check = True |
|
184 with open(filename, 'r') as f_handle: |
|
185 for line in f_handle: |
|
186 if agent_check: |
|
187 if line.startswith("[agent]"): |
|
188 # Throw away the initial starting block code in the |
|
189 del parameter_list[:] |
|
190 agent_check = False |
|
191 continue |
|
192 line = line.strip().replace("\n", "") |
|
193 if not line: |
|
194 # If parameter_list is not empty, process the data and |
|
195 # generate an xml structure |
|
196 process_grouping(parameter_list) |
|
197 # Done processing, delete all the saved entries |
|
198 del parameter_list[:] |
|
199 continue |
|
200 |
|
201 match = COMMENT_PATTERN.match(line) |
|
202 if match: |
|
203 line = match.group(1) |
|
204 parameter_list.append(line) |
|
205 f_handle.close() |
|
206 |
|
207 |
|
208 def update_smf_file(smf_xml_file, output_file, version): |
|
209 '''Replace the puppet property definitions in the specified SMF |
|
210 file with those that are stored in PUPPET_CONFIG_DICT |
|
211 ''' |
|
212 |
|
213 try: |
|
214 parser = etree.XMLParser(remove_blank_text=True) |
|
215 tree = etree.parse(smf_xml_file, parser) |
|
216 root = tree.getroot() |
|
217 template = root.find("service/template") |
|
218 puppet_desc = template.find("common_name/loctext") |
|
219 puppet_desc.text = "Puppet version %s" % version |
|
220 |
|
221 pg_pattern = template.find("pg_pattern") |
|
222 except IOError as msg: |
|
223 err(msg) |
|
224 return -1 |
|
225 except etree.XMLSyntaxError as msg: |
|
226 err(msg) |
|
227 return -1 |
|
228 except NameError as msg: |
|
229 err("XML file %s does not match expected formated" % smf_xml_file) |
|
230 |
|
231 # Delete the pg_pattern nodes and it's children |
|
232 # This is the structure that will be rebuilt based |
|
233 # on the genconfig information that was read in |
|
234 if pg_pattern is not None: |
|
235 template.remove(pg_pattern) |
|
236 |
|
237 # <pg_pattern name='config' type='application' required='false'> |
|
238 pg_pattern = etree.SubElement( |
|
239 template, |
|
240 "pg_pattern", |
|
241 name="config", |
|
242 type="application", |
|
243 required="false") |
|
244 for key in sorted(PUPPET_CONFIG_DICT.iterkeys()): |
|
245 values = PUPPET_CONFIG_DICT[key] |
|
246 element = create_config_element(values[0], values[1], values[2]) |
|
247 pg_pattern.append(element) |
|
248 |
|
249 # Write out the contents of the updated puppet SMF config file |
|
250 print "Writting out contents of new SMF configuration file to: %s" % \ |
|
251 output_file |
|
252 with open(output_file, "w") as f_handle: |
|
253 f_handle.write(etree.tostring(tree, pretty_print=True)) |
|
254 f_handle.close() |
|
255 |
|
256 |
|
257 def option_list(): |
|
258 '''Build the option list for this utility''' |
|
259 desc = "Utility for assisting in the upgrading of Solaris Puppet SMF file" |
|
260 usage = "usage: %prog -c <puppet_config_file> -s <smf_confilg_file> " \ |
|
261 "-v <puppet_version> [-o <output_file>]\n" |
|
262 opt_list = OptionParser(description=desc, usage=usage) |
|
263 |
|
264 opt_list.add_option("-c", "--config", dest="config", default=None, |
|
265 action="store", type="string", nargs=1, |
|
266 metavar="<puppet_config_file>", |
|
267 help="Puppet configuration file generated via" |
|
268 "genconfig option to puppet. i.e. " |
|
269 "puppet agent --genconfig > puppet.conf") |
|
270 opt_list.add_option("-s", "--smf", dest="smf_xml", default=None, |
|
271 action="store", type="string", nargs=1, |
|
272 metavar="<smf_config_file>", |
|
273 help="Current solaris Puppet SMF XML configuration" |
|
274 " file. This file is located in <userland_tree>" |
|
275 "/components/puppet/files/puppet.xml") |
|
276 opt_list.add_option("-o", "--output", dest="output", default=None, |
|
277 action="store", type="string", nargs=1, |
|
278 metavar="<output_file>", |
|
279 help="The name of the new puppet.xml file ") |
|
280 opt_list.add_option("-v", "--version", dest="version", default="None", |
|
281 action="store", type="string", nargs=1, |
|
282 metavar="<puppet_version>", |
|
283 help="Puppet Version of update") |
|
284 |
|
285 return opt_list |
|
286 |
|
287 |
|
288 def main(): |
|
289 '''Execute this utility based on the options supplied by the user''' |
|
290 parser = option_list() |
|
291 |
|
292 (options, _args) = parser.parse_args() |
|
293 |
|
294 if not options.output and options.version: |
|
295 options.output = "puppet.%s.xml" % options.version |
|
296 |
|
297 if not options.config or not options.smf_xml or \ |
|
298 not options.output or not options.version: |
|
299 err("Required options not specified") |
|
300 parser.print_help() |
|
301 sys.exit(-1) |
|
302 |
|
303 if not os.path.isfile(options.config): |
|
304 err("%s does not exist or is not a regular file\n" |
|
305 % options.config) |
|
306 sys.exit(-1) |
|
307 if not os.path.isfile(options.smf_xml): |
|
308 err("%s does not exist or is not a regular file\n" |
|
309 % options.smf_xml) |
|
310 sys.exit(-1) |
|
311 if os.path.exists(options.output): |
|
312 err("specified file %s already exist\n" |
|
313 % options.output) |
|
314 sys.exit(-1) |
|
315 |
|
316 parse_puppet_config(options.config) |
|
317 update_smf_file(options.smf_xml, options.output, options.version) |
|
318 |
|
319 if __name__ == '__main__': |
|
320 main() |