The Assimilation Project  based on Assimilation version 1.1.7.1474836767
cmaconfig.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # vim: smartindent tabstop=4 shiftwidth=4 expandtab number colorcolumn=100
3 #
4 # This file is part of the Assimilation Project.
5 #
6 # Author: Alan Robertson <alanr@unix.sh>
7 # Copyright (C) 2014 - Assimilation Systems Limited
8 #
9 # Free support is available from the Assimilation Project community - http://assimproj.org
10 # Paid support is available from Assimilation Systems Limited - http://assimilationsystems.com
11 #
12 # The Assimilation software is free software: you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation, either version 3 of the License, or
15 # (at your option) any later version.
16 #
17 # The Assimilation software is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
21 #
22 # You should have received a copy of the GNU General Public License
23 # along with the Assimilation Project software. If not, see http://www.gnu.org/licenses/
24 #
25 '''
26 This file implements things related to Configuration files for the CMA.
27 Not quite sure what all it will do, but hey, this comment is slightly better than nothing.
28 '''
29 from types import ClassType
30 from AssimCclasses import pyConfigContext, pyNetAddr, pySignFrame, pyCompressFrame
31 from consts import CMAconsts
32 
33 class ConfigFile(object):
34  '''
35  This class implements configuration file management, including validation
36  and default values for parameters.
37  '''
38  callbacks = []
39  # A template is a pattern for how to validate a dict-like object
40  # like those that come from pyConfigContexts -- which in turn model JSON
41  default_template = {
42  'OUI': {str: str}, # Addendum for locally-known OUI mappings
43  'optional_modules': [ # List of optional modules to be included
44  # Below is the list of all known optional modules
45  {'linkdiscovery', # listens for CMA/LLDP packets
46  'checksumdiscovery', # Checksums network-facing files
47  'monitoringdiscovery', # Automatically monitors services
48  'arpdiscovery', # Listens for ARP packets for
49  # network mapping...
50  'procsysdiscovery', # Discovers content of /proc/sys
51  }
52  ],
53  'contrib_modules': [str], # List of contrib modules to be included
54  # We have no idea what contrib modules there might be
55  'initial_discovery': # Below is the list of known discovery agents...
56  {'auditd_conf', # /etc/audit/auditd.conf config
57  'commands', # Discovers installed commands
58  'cpu', # CPU details
59  'docker', # Docker host & container configuration
60  'vagrant', # Vagrant host & VM configuration
61  'login_defs', # /etc/login.defs configuration
62  'pam', # PAM configuration
63  'findmnt', # Discovers mounted filesystems (Linux)
64  'packages', # Discovers installed packages
65  'monitoringagents', # Discovers monitoring agents
66  'nsswitch', # Discovers nsswitch configuration (GNU)
67  'os', # Discovers OS version information
68  'sshd', # Discovers sshd configuration
69  'sudoers', # Discovers /etc/sudoers configuration
70  'tcpdiscovery', # Discovers network-facing processes
71  'ulimit', # Discovers ulimit settings
72  },
73  'cmaport': {int, long},# CMA listening port
74  'cmainit': pyNetAddr, # Initial contact address for the CMA
75  'cmaaddr': pyNetAddr, # CMA's base address...
76  'cmadisc': pyNetAddr, # Address to send discovery information
77  'cmafail': pyNetAddr, # Address to send failure reports
78  'outsig': pySignFrame,# Packet signature frame
79  'compress': pyCompressFrame,# Packet compression frame
80  'compression_method': {'zlib'}, # Packet compression method
81  'compression_threshold':{int,long}, # Threshold for when to start compressing
82  'score_severity_map': {str: {'high': float, 'medium': float, 'low': float}},
83  'discovery': {
84  'repeat': {int,long}, # how often to repeat a discovery action
85  'warn': {int,long}, # How long to wait when issuing a slow discovery warning
86  'timeout': {int,long}, # how long wait before declaring failure
87  'nice': {int,long}, # Optional UNIX-style 'nice' value
88  'agents': { # Configuration information for individual agent types,
89  # optionally including machine
90  str:{
91  'repeat': {int,long}, # repeat for this particular agent
92  'warn': {int,long}, # How long before slow discovery warning
93  'timeout': {int,long}, # timeout for this particular agent
94  'nice': {int,long}, # UNIX-style nice value
95  },
96  },
97  },
98  'monitoring': {
99  'repeat': {int,long}, # Default repeat interval in seconds
100  'warn': {int,long}, # How long to wait when issuing a slow monitoring warning
101  'timeout': {int,long}, # How long to wait before declaring failure
102  'nice': {int,long}, # Optional UNIX-style 'nice' value
103  'agents': { # Configuration information for individual agent types,
104  # optionally including machine
105  str:{
106  'repeat': {int,long}, # repeat for this particular agent
107  'warn': {int,long}, # How long before slow warning
108  'timeout': {int,long}, # timeout for this particular agent
109  'nice': {int,long}, # UNIX-style nice value
110  'argv': [str],
111  'env': {str: str},
112  },
113  },
114  'nagiospath': [str],
115  },
116  'heartbeats': {
117  'repeat': {int,long}, # how frequently to heartbeat - in seconds
118  'warn': {int,long}, # How long to wait when issuing a late heartbeat warning
119  'timeout': {int,long}, # How long to wait before declaring a system dead
120  },
121  'bprulesbydomain': {str: str}, # Which best practice rule sets to use by default?
122  'allbpdiscoverytypes': [str], # List of all best practice discovery types
123  'checksum_cmds': [str], # Ordered List of checksum commands to use
124  'checksum_files': [str], # Files to always perform the checksum of
125  'permission_files': [str], # Files to always check the permissions of
126  'sysfile_fileattrs':[str], # Standard/common system files perms to discover
127  'libmodules_fileattrs':[str], # Standard/common library directories perms to discover
128  'lib_fileattrs':[str], # Standard/common library perms to discover
129  'lib64_fileattrs':[str], # Standard/common library perms to discover
130  'libgnu_fileattrs':[str], # Standard/common library perms to discover
131  'libgnu64_fileattrs':[str], # Standard/common library perms to discover
132  'usrlib_fileattrs':[str], # Standard/common library perms to discover
133  'usrlib64_fileattrs':[str], # Standard/common library perms to discover
134  'usrlibgnu_fileattrs':[str], # Standard/common library perms to discover
135  'usrlibgnu64_fileattrs':[str], # Standard/common library perms to discover
136  'bin_fileattrs':[str], # Standard/common command directories perms to discover
137  'sbin_fileattrs': [str], # Standard/common command directories perms to discover
138  'usr_bin_fileattrs': [str], # Standard/common command directories perms to discover
139  'usr_local_fileattrs': [str], # Standard/common command directories perms to discover
140  'usr_sbin_fileattrs': [str], # Standard/common command directories perms to discover
141  'perm_discovery_lists': [str], # List of all the collections of perms to discover
142  'containers': {
143  'docker': {
144  },
145  'vagrant': {
146  },
147  }
148  }
149 
150  @staticmethod
151  def register_callback(function, **args):
152  'Register a callback to let someone know when we create or modify this configuration'
153  ConfigFile.callbacks.append((function, args))
154 
155  # This is the default configuration for the Assimilation project CMA
156  # It should/must conform to the default_template above
157  @staticmethod
158  def default_defaults():
159  '''This is our default - for defaults
160  Sounds kinda weird, but it makes sense - and is handy for our tests to not have to
161  have the current defaults updated all the time...
162  '''
163  retval = {
164  'OUI': {
165  # Addendum of locally-known OUIs - feel free to contribute ones you find...
166  # Python includes lots of them, but is missing newer ones.
167  # Note that they have to be in lower case with '-' separators.
168  # You can find the latest data here:
169  # http://standards.ieee.org/cgi-bin/ouisearch
170  '18-0c-ac': 'Canon, Inc.',
171  '28-d2-44': 'LCFC(HeFei) Electronics Technology Co., Ltd.',
172  '56-84-7a': '(linux bridge)',
173  '64-bc-0c': 'LG Electronics',
174  '84-7a-88': 'HTC Corporation',
175  'a8-66-7f': 'Apple, Inc.',
176  'b0-79-3c': 'Revolv, Inc.',
177  'b8-ee-65': 'Liteon Technology Corporation',
178  'bc-ee-7b': 'ASUSTek Computer, Inc.',
179  'c8-b5-b7': 'Apple, Inc.',
180  'cc-3a-61': 'SAMSUNG ELECTRO MECHANICS CO., LTD.',
181  'd8-50-e6': 'ASUSTek COMPUTER INC.',
182  'd8-cb-8a': 'Micro-Star INTL CO., LTD.',
183  'e8-ab-fa': 'Shenzhen Reecam Tech.Ltd.',
184  },
185  #
186  # Below is the set of modules that we import before starting up
187  # Each of them triggers different kinds of conditional discovery
188  # as per its design...
189  'optional_modules': [ # List of optional modules to be included
190  'linkdiscovery', # Perform CDP/LLDP monitoring
191  'checksumdiscovery', # Perform tripwire-like checksum monitoring
192  'monitoringdiscovery', # Initiates monitoring based on service
193  # discovery
194  'arpdiscovery', # Listen for ARP packets: IPs and MACs
195  'procsysdiscovery', # Discovers content of /proc/sys
196  ],
197  'contrib_modules': [], # List of contrib modules to be imported
198  #
199  # Always start these discovery plugins below when a Drone comes online
200  #
201  'initial_discovery':['os', # OS properties
202  'cpu', # CPU properties
203  'packages', # What packages are installed?
204  'commands', # Discovers installed commands
205  'monitoringagents',# What monitoring agents are installed?
206  'login_defs', # /etc/login.defs configuration
207  'auditd_conf', # /etc/audit/auditd.conf config
208  'pam', # PAM configuration
209  'sudoers', # Discovers /etc/sudoers configuration
210  'ulimit', # What are current ulimit values?
211  'nsswitch', # Discovers nsswitch configuration (Linux)
212  'findmnt', # Discovers mounted filesystems (Linux)
213  'sshd', # Discovers sshd configuration
214  'docker', # Docker host & container configuration
215  'vagrant', # Vagrant host & VM configuration
216  'tcpdiscovery' # Discover services
217  ],
218  'cmaport': 1984, # Our listening port
219  'cmainit': pyNetAddr("0.0.0.0:1984"), # Our listening address
220  'compression_threshold': 20000, # Compress packets >= 20 kbytes
221  'compression_method': "zlib", # Compression method
222  'score_severity_map': {'security': {'high': 3.0, 'medium': 2.0, 'low': 1.0},
223  'networking': {'high': 3.0, 'medium': 2.0, 'low': 1.0},
224  },
225  'discovery': {
226  'repeat': 60, # Default repeat interval in seconds
227  'warn': 120, # Default slow discovery warning time
228  'timeout': 300, # Default timeout interval in seconds
229  'agents': { # Configuration information for individual agent types,
230  # optionally including machine
231  "checksums": {'repeat':3600*8, 'timeout': 10*60
232  , 'warn':5*60},
233  "os": {'repeat': 0, 'timeout': 60, 'warn': 5},
234  "cpu": {'repeat': 0, 'timeout': 60, 'warn': 5}
235  },
236  },
237  'monitoring': {
238  'repeat': 15, # Default repeat interval in seconds
239  'warn': 60, # Default slow monitoring warning time
240  'timeout': 180, # Default repeat interval in seconds
241  'agents': { # Configuration information for individual agent types,
242  # optionally including machine
243  # "lsb::ssh": {'repeat': int, 'timeout': int},
244  # "ocf::Neo4j/servidor": {'repeat': int, 'timeout': int},
245  #
246  "nagios::check_load": {'repeat': 60, 'timeout': 30,
247  #
248  # I would really rather have a pure run queue length
249  # but there's no agent for that. Sigh...
250  #
251  # -r == scale load average by by number of CPUs
252  # -w == floating point warning load averages
253  # (1,5,15 minute values)
254  # -c == floating point critical load averages
255  # (1,5,15 minute values)
256  #
257  'argv': ['-r', '-w', '4,3,2', '-c', '4,3,2']},
258  },
259  'nagiospath': [ "/usr/lib/nagios/plugins", # places to look for Nagios agents
260  "/usr/local/nagios/libexec",
261  "/usr/nagios/libexec",
262  "/opt/nrpe/libexec"
263  ],
264  },
265  'heartbeats': {
266  'repeat': 1, # how frequently to heartbeat - in seconds
267  'warn': 5, # How long to wait when issuing a late heartbeat warning
268  'timeout': 30, # How long to wait before declaring a system dead
269  },
270  'bprulesbydomain': {# Default best practice rule sets by domain
271  # Default the global domain to the base rule set
272  CMAconsts.globaldomain: CMAconsts.BASERULESETNAME,
273  # If you want different defaults for the global domain or
274  # for any other domain, put them in your local config file.
275  # I suspect these default rules will turn out to be a bit
276  # harsh for many sites.
277  },
278  # List of all the known best practice discovery types
279  'allbpdiscoverytypes': ['auditd_conf', 'auditd_fileattrs', 'fileattrs',
280  'login_defs', 'pam', 'proc_sys', 'sshd'],
281  # Prioritized list of checksum commands to use
282  # we use the first one that's installed.
283  'checksum_cmds': [
284  '/usr/bin/sha256sum',
285  '/usr/bin/sha224sum',
286  '/usr/bin/sha384sum',
287  '/usr/bin/sha512sum',
288  '/usr/bin/sha1sum',
289  '/usr/bin/md5sum',
290  '/usr/bin/cksum',
291  '/usr/bin/crc32'],
292  # Files we *always* checksum
293  'checksum_files': [
294  '/bin/sh',
295  '/bin/bash',
296  '/bin/login',
297  '/usr/bin/passwd',
298  ],
299  # Files/directories to always get the permissions of
300  # Directories ending in / also have their contained files checked.
301  'sysfile_fileattrs':[
302  '/',
303  '/etc/audit/',
304  '/etc/bash.bashrc',
305  '/etc/bashrc',
306  '/etc/bash_completion',
307  '/etc/bash_completion.d/',
308  '/etc/default/grub',
309  '/etc/group',
310  '/etc/grub.conf',
311  '/etc/grub.d/',
312  '/etc/gshadow',
313  '/etc/init.d/',
314  '/etc/login.defs',
315  '/etc/passwd',
316  '/etc/profile',
317  '/etc/profile.d/',
318  '/etc/csh.cshrc',
319  '/etc/selinux/',
320  '/etc/shadow',
321  '/usr/',
322  '/usr/local/',
323  '/var/',
324  ],
325  # A collection of directories to gather the permissions of
326  # Note that all these discovery names are in 'perm_discovery_lists' below
327  # That's what triggers them to be discovered.
328  # They are all broken up into several lists because the output is verbose
329  # Both UDP and Neo4j hate large discovery blobs
330  'libmodules_fileattrs':[ '/lib/modules/' ],
331  'lib_fileattrs':['/lib/', '/lib/i386-linux-gnu/'],
332  'lib64_fileattrs':['/lib64', '/lib/x86_64-linux-gnu/'],
333  'usrlib_fileattrs':['/usr/lib/', '/usr/lib/i386-linux-gnu/' ],
334  'usrlib64_fileattrs':['/usr/lib64/', '/usr/lib/x86_64-linux-gnu/'],
335  'bin_fileattrs':['/bin/', '/sbin/'],
336  'usr_bin_fileattrs': ['/usr/bin/', '/usr/sbin/'],
337  'usr_local_fileattrs': [ '/usr/local/bin/', '/usr/local/sbin/', '/usr/local/lib/'],
338  'perm_discovery_lists': [
339  'bin_fileattrs',
340  'lib64_fileattrs',
341  'libmodules_fileattrs',
342  'lib_fileattrs',
343  'sysfile_fileattrs',
344  'usrlib64_fileattrs',
345  'usrlib_fileattrs',
346  'usr_bin_fileattrs',
347  'usr_local_fileattrs',
348  ],
349  'containers': {
350  'docker': {
351  'initial_discovery':[
352  'os', # OS properties
353  'packages', # What packages are installed?
354  'commands', # Discovers installed commands
355  'ulimit', # What are current ulimit values?
356  ],
357  },
358  'vagrant': {
359  'initial_discovery':[
360  'os', # CPU properties
361  'netconfig', # Network configuration
362  'packages', # What packages are installed?
363  'commands', # Discovers installed commands
364  'ulimit', # What are current ulimit values?
365  'tcpdiscovery' # Discover services
366  ],
367  },
368  }
369  } # End of return value
370  retval['allbpdiscoverytypes'].extend(retval['perm_discovery_lists'])
371  return retval
372 
373  def __init__(self, filename=None, template=None, defaults=None):
374  'Init function for ConfigFile class, give us a filename - or None!'
375  if template is None:
376  template = ConfigFile.default_template
377  self.template = template
378  if defaults is None:
379  defaults = ConfigFile.default_defaults()
380  self.defaults = defaults
381  if filename is None:
382  self.config = pyConfigContext(self.defaults)
383  else:
384  self.config = pyConfigContext(filename=filename)
385  # Call any registered callbacks
386  for callbacktuple in ConfigFile.callbacks:
387  function, args = callbacktuple
388  function(self, None, args)
389 
390 
391  def __contains__(self, name):
392  "We're basically a dict lookalike - implement __contains__"
393  return name in self.config
394 
395  def __getitem__(self, name):
396  "We're basically a dict lookalike - implement __getitem__"
397  return self.config[name]
398 
399  def __delitem__(self, name):
400  "We're basically a dict lookalike - implement __delitem__"
401  del self.config[name]
402 
403  def __len__(self):
404  "We're basically a dict lookalike - implement __len__"
405  return len(self.config)
406 
407  def __setitem__(self, name, value):
408  "We're basically a dict lookalike - implement __setitem__"
409  self.config[name] = value
410  valid = self.isvalid()
411  if not valid[0]:
412  raise ValueError(valid[1])
413  for callbacktuple in ConfigFile.callbacks:
414  function, args = callbacktuple
415  function(self, name, args)
416 
417 
418  @staticmethod
419  def _merge_config_elems(defaults, config):
420  '''Merge data from our defaults into the configuration.
421  Any element which is not included in the specified configuration is pulled
422  from the default value for that element.
423  NOTE that if you override a name which has an array for a value, you are
424  eliminating all the array values. The arrays are NOT somehow merged.
425  '''
426  for elem in defaults:
427  delem = defaults[elem]
428  if isinstance(delem, (dict, pyConfigContext)):
429  if elem not in config:
430  config[elem] = pyConfigContext()
431  ConfigFile._merge_config_elems(delem, config[elem])
432  elif elem not in config:
433  #print 'SETTING elem %s to delem %s' % (elem, delem)
434  config[elem] = delem
435 
436  def complete_config(self):
437  '''Create a complete configuration by merging with defaults
438  and validating the merged config against our template.'''
439  ConfigFile._merge_config_elems(self.defaults, self.config)
440  ret = self.isvalid(self.config)
441  if ret[0]:
442  return self.config
443  else:
444  raise ValueError(ret[1])
445 
446 
447  def isvalid(self, config=None):
448  '''Validate the given configuration against our template.
449  Return is a Tuple (True/False, 'explanation of errors')'''
450  if config is None:
451  config = self.default_defaults()
452  return ConfigFile._check_validity(self.template, config)
453 
454  @staticmethod
455  def _check_validity(template, configobj):
456  '''Recursively validate a complex dict-like object against a complex template object
457  This is an interesting, but somewhat complex operation.
458 
459  Return value is a Tuple (True/False, 'explanation of errors')
460  '''
461 
462  if isinstance(template, (type, ClassType)):
463  return ConfigFile._check_validity_type(template, configobj)
464  if isinstance(template, dict):
465  return ConfigFile._check_validity_dict(template, configobj)
466  if isinstance(template, (list, tuple)):
467  return ConfigFile._check_validity_list(template, configobj)
468  if isinstance(template, set):
469  return ConfigFile._check_validity_set(template, configobj)
470 
471  return (False, "Case we didn't allow for: %s vs %s" % (str(template), str(configobj)))
472 
473  @staticmethod
474  def _check_validity_type(template, configobj):
475  'Make sure the configobj is of the given type'
476  if (not isinstance(configobj, template)):
477  return (False, '%s is not of %s' % (configobj, template))
478  return (True, '')
479 
480  @staticmethod
481  def _check_validity_set(template, configobj):
482  'Make sure the configobj is of a string matching something in the set'
483  if isinstance(configobj, list):
484  # Or maybe a list of things all of which have to be in the set...
485  for elem in configobj:
486  ret = ConfigFile._check_validity_set(template, elem)
487  if not ret[0]:
488  return (False, 'Element %s: %s' % (elem, ret[1]))
489  return (True, '')
490 
491  # Pylint doesn't like our type matching - so I hid it from pylint through 'configtype'
492  configtype = type(configobj)
493  if configobj not in template and configtype not in template:
494  return (False, '%s is not in %s' % (configobj, template))
495  return (True, '')
496 
497  @staticmethod
498  def _check_validity_dict(template, configobj):
499  'Check a configobj for validity against a "dict" template'
500  try:
501  keys = configobj.keys()
502  except AttributeError:
503  return (False, '%s is not a dict' % (configobj))
504  # Were we just given "str" as a key value?
505  # If so, then any names are legal, but the values all have to be the "correct" type
506  if str in template:
507  validatetype = template[str]
508  # Any key is fine, but elements have to match the given type
509  for configkey in keys:
510  ret = ConfigFile._check_validity(validatetype, configobj[configkey])
511  if not ret[0]:
512  return (False, 'Element %s: %s' % (configkey, ret[1]))
513  else:
514  # Every key in the configobj must also be in the template
515  for elemname in keys:
516  if elemname not in template:
517  return (False, ('%s is not one of %s' % (elemname, str(template.keys()))))
518  ret = ConfigFile._check_validity(template[elemname], configobj[elemname])
519  if not ret[0]:
520  return (False, 'Element %s: %s' % (elemname, ret[1]))
521  return (True, '')
522 
523 
524  @staticmethod
525  def _check_validity_list(template, configobj):
526  'Check a configobj for validity against a list/tuple template'
527  # When the template element is a list or tuple, then the item has to be a
528  # list or tuple and every element of the item has to match the given template
529  # Note that all lists (currently) have to be of the same type...
530  if not isinstance(configobj, (list, tuple)):
531  return (False, ('%s is not a list or tuple' % (configobj)))
532  checktype = template[0]
533  if isinstance(checktype, set):
534  for elem in configobj:
535  if elem not in checktype:
536  return (False, ('%s is not in %s' % (elem, checktype)))
537  else:
538  for elem in configobj:
539  ret = ConfigFile._check_validity(checktype, elem)
540  if not ret[0]:
541  return (False, ('Array element: %s' % ret[1]))
542  return (True, '')
543 
544  @staticmethod
545  def agent_params(config, agenttype, agentname, dronedesignation):
546  '''We return the agent parameters for the given type, agent name and drone
547  The most specific values take priority over the less specific values
548  creating a 3-level value inheritance scheme.
549  - Top level is for all agents.
550  - Second level is for specific agents.
551  - Third level is for specific agents on specific machines.
552  agenttype should be one of 'monitoring' or 'discovery'
553  agentname for discovery:
554  name of discovery agent
555  agentname for monitoring:
556  monitoring-class::provider:monitortype for OCF
557  monitoring-class::monitortype for non-OCF
558 
559  We implement this.
560  '''
561  compoundname = '%s/%s' % (agentname, dronedesignation)
562  subconfig = config[agenttype]
563  result = pyConfigContext('{"type": "%s", "parameters":{}}' % agentname)
564  if compoundname in subconfig:
565  result['parameters'] = subconfig[compoundname]
566  if 'agents' in subconfig and agentname in subconfig['agents']:
567  agentlist = subconfig['agents']
568  for tag in agentlist[agentname]:
569  if tag not in result:
570  subval = agentlist[agentname][tag]
571  result['parameters'][tag] = agentlist[agentname][tag]
572  for tag in subconfig:
573  if tag not in result:
574  subval = subconfig[tag]
575  if not hasattr(subval, 'keys'):
576  result['parameters'][tag] = subconfig[tag]
577  return result
578 
579 # Simplify setting up initial discovery validation for our container types
580 for container in ConfigFile.default_template['containers']:
581  ConfigFile.default_template['containers'][container]['initial_discovery'] \
582  = ConfigFile.default_template['initial_discovery']
583 ConfigFile.default_template['containers']['vagrant']['initial_discovery'].add('netconfig')
584 
585 if __name__ == '__main__':
586  # pylint: disable=C0411,C0413
587  import subprocess
588  cf = ConfigFile()
589  assert cf.isvalid()
590  # Make sure it's making correct JSON...
591  pyConfigContext(str(cf.complete_config()))
592  #print 'Complete config:', cf.complete_config() # also checks for validity
593  lint = subprocess.Popen(('jsonlint', '-f'), stdin=subprocess.PIPE)
594  print >> lint.stdin, '%s\n' % cf.complete_config()
595  lint.stdin.close()
596  assert lint.wait() == 0