19
19
20
20
21
21
__author__ = "Elliot Jordan"
22
- __version__ = "1.1 .0"
22
+ __version__ = "1.2 .0"
23
23
24
24
import argparse
25
25
import json
26
+ import logging
26
27
import os
27
28
import plistlib
28
29
import shutil
29
30
import sys
30
- import xml
31
+ import xml .parsers .expat
32
+ from typing import Any
31
33
32
34
33
- def build_argument_parser ():
35
+ def setup_logging (verbosity : int = 0 ) -> logging .Logger :
36
+ """Set up logging configuration based on verbosity level."""
37
+ if verbosity == 0 :
38
+ level = logging .WARNING
39
+ elif verbosity == 1 :
40
+ level = logging .INFO
41
+ else :
42
+ level = logging .DEBUG
43
+
44
+ # Configure logging format
45
+ log_format = "%(levelname)s: %(message)s"
46
+ if verbosity >= 2 :
47
+ log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
48
+
49
+ logging .basicConfig (level = level , format = log_format , datefmt = "%Y-%m-%d %H:%M:%S" )
50
+
51
+ return logging .getLogger (__name__ )
52
+
53
+
54
+ def build_argument_parser () -> argparse .ArgumentParser :
34
55
"""Build and return the argument parser."""
35
56
parser = argparse .ArgumentParser (
36
57
description = __doc__ , formatter_class = argparse .RawDescriptionHelpFormatter
@@ -78,14 +99,15 @@ def build_argument_parser():
78
99
return parser
79
100
80
101
81
- def validate_args (args ) :
102
+ def validate_args (args : argparse . Namespace ) -> argparse . Namespace :
82
103
"""Do sanity checking and validation on provided input arguments."""
104
+ logger = logging .getLogger (__name__ )
83
105
84
106
if not os .path .isdir (os .path .expanduser (args .input_dir )):
85
107
sys .exit ("Input path provided is not a directory: %s" % args .input_dir )
86
108
if os .path .exists (os .path .expanduser (args .output_dir )):
87
109
if args .overwrite :
88
- print ( "WARNING: Will overwrite output dir: %s" % args .output_dir )
110
+ logger . warning ( " Will overwrite output dir: %s", args .output_dir )
89
111
else :
90
112
sys .exit (
91
113
"Output path already exists: %s\n Use --overwrite to replace contents "
@@ -99,18 +121,22 @@ def validate_args(args):
99
121
return args
100
122
101
123
102
- def read_manifest_plist (path ) :
124
+ def read_manifest_plist (path : str ) -> dict [ str , Any ] :
103
125
"""Given a path to a profile manifest plist, return the contents of
104
126
the plist."""
127
+ logger = logging .getLogger (__name__ )
128
+ plist = {}
105
129
with open (path , "rb" ) as openfile :
106
130
try :
107
- return plistlib .load (openfile )
131
+ plist = plistlib .load (openfile )
108
132
except xml .parsers .expat .ExpatError :
109
- print ("Error reading %s" % path )
133
+ logger .error ("Error reading %s" , path )
134
+ return plist
110
135
111
136
112
- def process_subkeys (subkeys ) :
137
+ def process_subkeys (subkeys : list [ dict [ str , Any ]]) -> dict [ str , dict [ str , Any ]] :
113
138
"""Given a list of subkeys, return equivalent JSON schema manifest properties."""
139
+ logger = logging .getLogger (__name__ )
114
140
115
141
# Skip keys that describe the payload instead of the setting
116
142
meta_keys = (
@@ -132,15 +158,15 @@ def process_subkeys(subkeys):
132
158
)
133
159
134
160
properties = {}
135
- for idx , subkey in enumerate ( subkeys ) :
161
+ for subkey in subkeys :
136
162
# Get subkey name
137
163
name = ""
138
164
try :
139
165
if subkey .get ("pfm_name" , "" ) != "" :
140
166
name = subkey ["pfm_name" ]
141
167
except AttributeError :
142
- print ( "WARNING: Syntax error. Skipping." )
143
- return
168
+ logger . warning ( " Syntax error. Skipping." )
169
+ return {}
144
170
145
171
# Skip specific names
146
172
if name in meta_keys :
@@ -195,7 +221,7 @@ def process_subkeys(subkeys):
195
221
196
222
# Recurse into sub-sub-keys
197
223
if "pfm_subkeys" in subkey and not isinstance (subkey ["pfm_subkeys" ], list ):
198
- print ( "WARNING: Not a list: %s" % subkey ["pfm_subkeys" ])
224
+ logger . warning ( " Not a list: %s", subkey ["pfm_subkeys" ])
199
225
if isinstance (subkey .get ("pfm_subkeys" ), list ):
200
226
subprop = process_subkeys (subkey ["pfm_subkeys" ])
201
227
if "items" in properties [name ]:
@@ -204,12 +230,13 @@ def process_subkeys(subkeys):
204
230
# TODO: Validate this assumption. Some warnings seen in the wild.
205
231
subprop_keys = list (subprop .keys ())
206
232
if len (subprop_keys ) > 1 :
207
- print (
208
- "WARNING: Array type should only have one subproperty "
209
- "key. Skipping all but the first: %s" % subprop_keys
233
+ logger .warning (
234
+ "Array type should only have one subproperty "
235
+ "key. Skipping all but the first: %s" ,
236
+ subprop_keys ,
210
237
)
211
238
elif len (subprop_keys ) == 0 :
212
- print ( "WARNING: No subproperty keys found in %s key." % name )
239
+ logger . warning ( " No subproperty keys found in %s key.", name )
213
240
continue
214
241
array_props = subprop [subprop_keys [0 ]]
215
242
properties [name ]["items" ] = array_props
@@ -219,11 +246,14 @@ def process_subkeys(subkeys):
219
246
return properties
220
247
221
248
222
- def convert_to_jamf_manifest (data , property_order_increment = 5 ):
249
+ def convert_to_jamf_manifest (
250
+ data : dict [str , Any ], property_order_increment : int = 5
251
+ ) -> dict [str , Any ] | None :
223
252
"""Convert a profile manifest plist object to a Jamf JSON schema manifest.
224
253
225
254
Reference: https://docs.jamf.com/technical-papers/jamf-pro/json-schema/10.19.0/Understanding_the_Structure_of_a_JSON_Schema_Manifest.html
226
255
"""
256
+ logger = logging .getLogger (__name__ )
227
257
228
258
# Create schema object
229
259
try :
@@ -233,20 +263,20 @@ def convert_to_jamf_manifest(data, property_order_increment=5):
233
263
"properties" : process_subkeys (data ["pfm_subkeys" ]),
234
264
}
235
265
except KeyError :
236
- print ( "ERROR: Manifest is missing a title, domain, or description." )
237
- return
266
+ logger . error ( " Manifest is missing a title, domain, or description." )
267
+ return None
238
268
239
269
# Lock property order
240
270
if property_order_increment > 0 :
241
271
order = property_order_increment
242
- for property in schema ["properties" ]:
243
- schema ["properties" ][property ]["property_order" ] = order
272
+ for prop_name in schema ["properties" ]:
273
+ schema ["properties" ][prop_name ]["property_order" ] = order
244
274
order += property_order_increment
245
275
246
276
return schema
247
277
248
278
249
- def write_to_file (path , data ) :
279
+ def write_to_file (path : str , data : dict [ str , Any ]) -> None :
250
280
"""Given a path to a file and JSON data, write the file."""
251
281
path_head , path_tail = os .path .split (path )
252
282
@@ -267,8 +297,9 @@ def write_to_file(path, data):
267
297
)
268
298
269
299
270
- def update_readme (count ) :
300
+ def update_readme (count : int ) -> None :
271
301
"""Updates README.md file with latest manifest count."""
302
+ logger = logging .getLogger (__name__ )
272
303
273
304
with open ("README.md" , encoding = "utf-8" ) as f :
274
305
readme = f .readlines ()
@@ -281,16 +312,19 @@ def update_readme(count):
281
312
break
282
313
with open ("README.md" , "w" , encoding = "utf-8" ) as f :
283
314
f .write ("" .join (readme ))
284
- print ("Updated README.md" )
315
+ logger . info ("Updated README.md" )
285
316
286
317
287
- def main ():
318
+ def main () -> None :
288
319
"""Main process."""
289
320
290
321
# Parse command line arguments.
291
322
argparser = build_argument_parser ()
292
323
args = validate_args (argparser .parse_args ())
293
324
325
+ # Set up logging based on verbosity
326
+ logger = setup_logging (args .verbose )
327
+
294
328
# Expand to full paths
295
329
input_dir = os .path .expanduser (args .input_dir )
296
330
output_dir = os .path .expanduser (args .output_dir )
@@ -303,14 +337,14 @@ def main():
303
337
304
338
# Iterate through manifests in the input path
305
339
count = {"done" : 0 , "skipped" : 0 }
306
- for root , dirs , files in os .walk (input_dir ):
340
+ for root , _dirs , files in os .walk (input_dir ):
307
341
for name in files :
308
342
if name .endswith (".plist" ):
309
343
relpath = os .path .relpath (os .path .join (root , name ), start = input_dir )
310
344
311
345
# Output filename if in verbose mode
312
346
if args .verbose > 0 :
313
- print ("Processing %s" % relpath )
347
+ logger . info ("Processing %s" , relpath )
314
348
315
349
# Load manifest
316
350
pfm_data = read_manifest_plist (os .path .join (root , name ))
@@ -333,7 +367,9 @@ def main():
333
367
write_to_file (output_path , manifest )
334
368
count ["done" ] += 1
335
369
336
- print ("Converted %d files. Skipped %d files." % (count ["done" ], count ["skipped" ]))
370
+ logger .info (
371
+ "Converted %d files. Skipped %d files." , count ["done" ], count ["skipped" ]
372
+ )
337
373
update_readme (count ["done" ])
338
374
339
375
0 commit comments