class AnsibleAutodoc: def __init__(self): self.config = SingleConfig() self.log = SingleLog(self.config.debug_level) args = self._cli_args() self._parse_args(args) doc_parser = Parser() doc_generator = Generator(doc_parser) doc_generator.render() def _cli_args(self): """ use argparse for parsing CLI arguments :return: args objec """ usage = '''ansible-autodoc [project_directory] [options]''' parser = argparse.ArgumentParser( description= 'Generate documentation from annotated playbooks and roles using templates', usage=usage) # parser.add_argument("-f", "--force", help="Force online list", action="store_true") # parser.add_argument('-t','--type', nargs='+', help='<Required> Set flag', required=True) # parser.add_argument('-t','--type', nargs='+', help='<Required> Set flag') parser.add_argument('project_dir', nargs='?', default=os.getcwd(), help="Project directory to scan, " "if empty current working will be used.") parser.add_argument('-C', "--conf", nargs='?', default="", help="specify an configuration file") parser.add_argument('-o', action="store", dest="output", type=str, help='Define the destination ' 'folder of your documenation') parser.add_argument('-y', action='store_true', help='overwrite the output without asking') parser.add_argument('-D', "--dry", action='store_true', help='Dry runt without writing') parser.add_argument("--sample-config", action='store_true', help='Print the sample configuration yaml file') parser.add_argument( '-p', nargs='?', default="_unset_", help='use print template instead of writing to files, ' 'sections: all, info, tags, todo, var') parser.add_argument('-V', "--version", action='store_true', help='Get versions') debug_level = parser.add_mutually_exclusive_group() debug_level.add_argument('-v', action='store_true', help='Set debug level to info') debug_level.add_argument('-vv', action='store_true', help='Set debug level to debug') debug_level.add_argument('-vvv', action='store_true', help='Set debug level to trace') return parser.parse_args() def _parse_args(self, args): """ Use an args object to apply all the configuration combinations to the config object :param args: :return: None """ self.config.set_base_dir(os.path.abspath(args.project_dir)) # search for config file if args.conf != "": conf_file = os.path.abspath(args.conf) if os.path.isfile(conf_file) and os.path.basename( conf_file) == self.config.config_file_name: self.config.load_config_file(conf_file) # re apply log level based on config self.log.set_level(self.config.debug_level) else: self.log.warn("No configuration file found: " + conf_file) else: conf_file = self.config.get_base_dir( ) + "/" + self.config.config_file_name if os.path.isfile(conf_file): self.config.load_config_file(conf_file) # re apply log level based on config self.log.set_level(self.config.debug_level) # sample configuration if args.sample_config: print(self.config.sample_config) sys.exit() # version if args.version: print(__version__) sys.exit() # Debug levels if args.v is True: self.log.set_level("info") elif args.vv is True: self.log.set_level("debug") elif args.vvv is True: self.log.set_level("trace") # need to send the message after the log levels have been set self.log.debug("using configuration file: " + conf_file) # Overwrite if args.y is True: self.config.template_overwrite = True # Dry run if args.dry is True: self.config.dry_run = True if self.log.log_level > 1: self.log.set_level(1) self.log.info( "Running in Dry mode: Therefore setting log level at least to INFO" ) # Print template if args.p == "_unset_": pass elif args.p is None: self.config.use_print_template = "all" else: self.config.use_print_template = args.p # output dir if args.output is not None: self.config.output_dir = os.path.abspath(args.output) # some debug self.log.debug(args) self.log.info("Using base dir: " + self.config.get_base_dir()) if self.config.is_role: self.log.info("This is detected as: ROLE ") elif self.config.is_role is not None and not self.config.is_role: self.log.info("This is detected as: PLAYBOOK ") else: self.log.error([ self.config.get_base_dir() + "/roles", self.config.get_base_dir() + "/tasks" ], "No ansible root project found, checked for: ") sys.exit(1)
class Generator: template_files = [] extension = "j2" _parser = None def __init__(self, doc_parser): self.config = SingleConfig() self.log = SingleLog() self.log.info("Using template dir: " + self.config.get_template_base_dir()) self._parser = doc_parser self._scan_template() def _scan_template(self): """ Search for Jinja2 (.j2) files to apply to the destination :return: None """ base_dir = self.config.get_template_base_dir() for file in glob.iglob(base_dir + '/**/*.' + self.extension, recursive=True): relative_file = file[len(base_dir) + 1:] if ntpath.basename(file)[:1] != "_": self.log.trace("[GENERATOR] found template file: " + relative_file) self.template_files.append(relative_file) else: self.log.debug("[GENERATOR] ignoring template file: " + relative_file) def _create_dir(self, dir): if not self.config.dry_run: os.makedirs(dir, exist_ok=True) else: self.log.info("[GENERATOR][DRY] Creating dir: " + dir) def _write_doc(self): files_to_overwite = [] for file in self.template_files: doc_file = self.config.get_output_dir( ) + "/" + file[:-len(self.extension) - 1] if os.path.isfile(doc_file): files_to_overwite.append(doc_file) if len(files_to_overwite ) > 0 and self.config.template_overwrite is False: SingleLog.print("This files will be overwritten:", files_to_overwite) if not self.config.dry_run: resulst = FileUtils.query_yes_no("do you want to continue?") if resulst != "yes": sys.exit() for file in self.template_files: doc_file = self.config.get_output_dir( ) + "/" + file[:-len(self.extension) - 1] source_file = self.config.get_template_base_dir() + "/" + file self.log.trace("[GENERATOR] Writing doc output to: " + doc_file + " from: " + source_file) # make sure the directory exists self._create_dir(os.path.dirname(os.path.realpath(doc_file))) if os.path.exists(source_file) and os.path.isfile(source_file): with open(source_file, 'r') as template: data = template.read() if data is not None: try: data = Environment( loader=FileSystemLoader( self.config.get_template_base_dir()), lstrip_blocks=True, trim_blocks=True).from_string(data).render( self._parser.get_data(), r=self._parser) if not self.config.dry_run: with open(doc_file, 'w') as outfile: outfile.write(data) self.log.info("Writing to: " + doc_file) else: self.log.info("[GENERATOR][DRY] Writing to: " + doc_file) except jinja2.exceptions.UndefinedError as e: self.log.error( "Jinja2 templating error: <" + str(e) + "> when loading file: \"" + file + "\", run in debug mode to see full except") if self.log.log_level < 1: raise except UnicodeEncodeError as e: self.log.error( "At the moment I'm unable to print special chars: <" + str(e) + ">, run in debug mode to see full except") if self.log.log_level < 1: raise sys.exit() def print_to_cli(self): for file in self.template_files: source_file = self.config.get_template_base_dir() + "/" + file with open(source_file, 'r') as template: data = template.read() if data is not None: try: data = Environment( loader=FileSystemLoader( self.config.get_template_base_dir()), lstrip_blocks=True, trim_blocks=True).from_string(data).render( self._parser.get_data(), r=self._parser) print(data) except jinja2.exceptions.UndefinedError as e: self.log.error( "Jinja2 templating error: <" + str(e) + "> when loading file: \"" + file + "\", run in debug mode to see full except") if self.log.log_level < 1: raise except UnicodeEncodeError as e: self.log.error( "At the moment I'm unable to print special chars: <" + str(e) + ">, run in debug mode to see full except") if self.log.log_level < 1: raise except: print("Unexpected error:", sys.exc_info()[0]) raise def render(self): if self.config.use_print_template: self.print_to_cli() else: self.log.info("Using output dir: " + self.config.get_output_dir()) self._write_doc()