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