public inbox for gentoo-catalyst@lists.gentoo.org
 help / color / mirror / Atom feed
* [gentoo-catalyst] [PATCH 1/3] main: group related command line flags
@ 2015-10-09  5:57 Mike Frysinger
  2015-10-09  5:57 ` [gentoo-catalyst] [PATCH 2/3] log: new logging module to standardize catalyst output Mike Frysinger
                   ` (2 more replies)
  0 siblings, 3 replies; 6+ messages in thread
From: Mike Frysinger @ 2015-10-09  5:57 UTC (permalink / raw
  To: gentoo-catalyst

This makes the --help output more manageable so people can quickly
scan and skip options that they don't care about.
---
 catalyst/main.py | 48 ++++++++++++++++++++++++++++--------------------
 1 file changed, 28 insertions(+), 20 deletions(-)

diff --git a/catalyst/main.py b/catalyst/main.py
index 03c13c0..e6b6447 100644
--- a/catalyst/main.py
+++ b/catalyst/main.py
@@ -168,41 +168,49 @@ Using the specfile option (-f, --file) to build a stage target:
 $ catalyst -f stage1-specfile.spec"""
 
 	parser = argparse.ArgumentParser(epilog=epilog, formatter_class=argparse.RawDescriptionHelpFormatter)
-	parser.add_argument('-d', '--debug',
+
+	parser.add_argument('-V', '--version',
+		action='version', version=get_version(),
+		help='display version information')
+
+	group = parser.add_argument_group('Program output options')
+	group.add_argument('-d', '--debug',
 		default=False, action='store_true',
 		help='enable debugging')
-	parser.add_argument('-v', '--verbose',
+	group.add_argument('-v', '--verbose',
 		default=False, action='store_true',
 		help='verbose output')
-	parser.add_argument('-c', '--config',
-		type=FilePath(),
-		help='use specified configuration file')
-	parser.add_argument('-f', '--file',
-		type=FilePath(),
-		help='read specfile')
-	parser.add_argument('-F', '--fetchonly',
-		default=False, action='store_true',
-		help='fetch files only')
-	parser.add_argument('-a', '--clear-autoresume',
+
+	group = parser.add_argument_group('Temporary file management')
+	group.add_argument('-a', '--clear-autoresume',
 		default=False, action='store_true',
 		help='clear autoresume flags')
-	parser.add_argument('-p', '--purge',
+	group.add_argument('-p', '--purge',
 		default=False, action='store_true',
 		help='clear tmp dirs, package cache, autoresume flags')
-	parser.add_argument('-P', '--purgeonly',
+	group.add_argument('-P', '--purgeonly',
 		default=False, action='store_true',
 		help='clear tmp dirs, package cache, autoresume flags and exit')
-	parser.add_argument('-T', '--purgetmponly',
+	group.add_argument('-T', '--purgetmponly',
 		default=False, action='store_true',
 		help='clear tmp dirs and autoresume flags and exit')
-	parser.add_argument('-s', '--snapshot',
+
+	group = parser.add_argument_group('Target/config file management')
+	group.add_argument('-F', '--fetchonly',
+		default=False, action='store_true',
+		help='fetch files only')
+	group.add_argument('-c', '--config',
+		type=FilePath(),
+		help='use specified configuration file')
+	group.add_argument('-f', '--file',
+		type=FilePath(),
+		help='read specfile')
+	group.add_argument('-s', '--snapshot',
 		help='generate a release snapshot')
-	parser.add_argument('-V', '--version',
-		action='version', version=get_version(),
-		help='display version information')
-	parser.add_argument('-C', '--cli',
+	group.add_argument('-C', '--cli',
 		default=[], nargs=argparse.REMAINDER,
 		help='catalyst commandline (MUST BE LAST OPTION)')
+
 	return parser
 
 
-- 
2.5.2



^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [gentoo-catalyst] [PATCH 2/3] log: new logging module to standardize catalyst output
  2015-10-09  5:57 [gentoo-catalyst] [PATCH 1/3] main: group related command line flags Mike Frysinger
@ 2015-10-09  5:57 ` Mike Frysinger
  2015-10-09 16:30   ` Brian Dolbec
  2015-10-09  5:57 ` [gentoo-catalyst] [PATCH 3/3] main: convert to new logging module Mike Frysinger
  2015-10-09 16:19 ` [gentoo-catalyst] [PATCH 1/3] main: group related command line flags Brian Dolbec
  2 siblings, 1 reply; 6+ messages in thread
From: Mike Frysinger @ 2015-10-09  5:57 UTC (permalink / raw
  To: gentoo-catalyst

This has everything you could ever want:
- control where output is sent (stdout or a file)
- control over log level
- automatic exit when CRITICAL is used
- automatic colorization of critical/error/warning messages
- explicit control over use of colors
- automatic handling of embedded newlines
- standardized output format
- all logging routed through a single logger (not the root)
- additional log level between warning & info: notice

This just lays the groundwork -- no code is actually converted over
to using this.  That will be done in follow up commit(s).

Note: The default output level has changed from "info" to "notice".
That means the default display won't spam w/irrelevant details.

Example output (only the main.py module is converted):
$ ./bin/wrapper.py -s test
09 Oct 2015 01:34:56 EDT: NOTICE  : Using default Catalyst configuration file: /etc/catalyst/catalyst.conf
ContentsMap: __init__(), search_order = ['pixz', 'lbzip2', 'isoinfo_l', 'squashfs', 'gzip', 'xz', 'bzip2', 'tar', 'isoinfo_f']
Creating Portage tree snapshot test from /usr/portage...
^C

$ ./bin/wrapper.py -s test --debug
09 Oct 2015 01:35:43 EDT: INFO    : main.py:version: Catalyst git
09 Oct 2015 01:35:43 EDT: INFO    : main.py:version: vcs version 4440e8908c677d8764e29b0f127e2dd6c02b7621, date Fri Oct 9 01:28:19 2015 -0400
09 Oct 2015 01:35:43 EDT: INFO    : main.py:version: Copyright 2003-2015 Gentoo Foundation
09 Oct 2015 01:35:43 EDT: INFO    : main.py:version: Copyright 2008-2012 various authors
09 Oct 2015 01:35:43 EDT: INFO    : main.py:version: Distributed under the GNU General Public License version 2.1
09 Oct 2015 01:35:43 EDT: NOTICE  : log.py:notice: Using default Catalyst configuration file: /etc/catalyst/catalyst.conf
09 Oct 2015 01:35:43 EDT: INFO    : main.py:parse_config: Snapshot cache support enabled.
09 Oct 2015 01:35:43 EDT: INFO    : main.py:parse_config: Kernel cache support enabled.
09 Oct 2015 01:35:43 EDT: INFO    : main.py:parse_config: Autoresuming support enabled.
09 Oct 2015 01:35:43 EDT: INFO    : main.py:parse_config: Package cache support enabled.
09 Oct 2015 01:35:43 EDT: INFO    : main.py:parse_config: Seed cache support enabled.
09 Oct 2015 01:35:43 EDT: INFO    : main.py:parse_config: Envscript support enabled.
09 Oct 2015 01:35:43 EDT: DEBUG   : main.py:main: conf_values[options] = set(['snapcache', 'kerncache', 'autoresume', 'pkgcache', 'bindist', 'seedcache'])
^C

$ ./bin/wrapper.py -s test -C /asdf
09 Oct 2015 01:36:59 EDT: NOTICE  : Using default Catalyst configuration file: /etc/catalyst/catalyst.conf
ContentsMap: __init__(), search_order = ['pixz', 'lbzip2', 'isoinfo_l', 'squashfs', 'gzip', 'xz', 'bzip2', 'tar', 'isoinfo_f']

!!! catalyst: Syntax error: 0

09 Oct 2015 01:36:59 EDT: CRITICAL: Could not parse commandline
---
 catalyst/log.py  | 130 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 catalyst/main.py |  30 ++++++++++++-
 2 files changed, 158 insertions(+), 2 deletions(-)
 create mode 100644 catalyst/log.py

diff --git a/catalyst/log.py b/catalyst/log.py
new file mode 100644
index 0000000..bf39116
--- /dev/null
+++ b/catalyst/log.py
@@ -0,0 +1,130 @@
+# Copyright 2003-2015 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+
+"""Logging related code
+
+This largely exposes the same interface as the logging module except we add
+another level "notice" between warning & info, and all output goes through
+the "catalyst" logger.
+"""
+
+from __future__ import print_function
+
+import logging
+import logging.handlers
+import os
+import sys
+import time
+
+
+class CatalystLogger(logging.Logger):
+	"""Override the _log member to autosplit on new lines"""
+
+	def _log(self, level, msg, args, **kwargs):
+		"""If given a multiline message, split it"""
+		# We have to interpolate it first in case they spread things out
+		# over multiple lines like: Bad Thing:\n%s\nGoodbye!
+		msg %= args
+		for line in msg.splitlines():
+			super(CatalystLogger, self)._log(level, line, (), **kwargs)
+
+
+# The logger that all output should go through.
+# This is ugly because we want to not perturb the logging module state.
+_klass = logging.getLoggerClass()
+logging.setLoggerClass(CatalystLogger)
+logger = logging.getLogger('catalyst')
+logging.setLoggerClass(_klass)
+del _klass
+
+
+# Set the notice level between warning and info.
+NOTICE = (logging.WARNING + logging.INFO) / 2
+logging.addLevelName(NOTICE, 'NOTICE')
+
+
+# The API we expose to consumers.
+def notice(msg, *args, **kwargs):
+	"""Log a notice message"""
+	logger.log(NOTICE, msg, *args, **kwargs)
+
+def critical(msg, *args, **kwargs):
+	"""Log a critical message and then exit"""
+	status = kwargs.pop('status', 1)
+	logger.critical(msg, *args, **kwargs)
+	sys.exit(status)
+
+error = logger.error
+warning = logger.warning
+info = logger.info
+debug = logger.debug
+
+
+class CatalystFormatter(logging.Formatter):
+	"""Mark bad messages with colors automatically"""
+
+	_COLORS = {
+		'CRITICAL':	'\033[1;35m',
+		'ERROR':	'\033[1;31m',
+		'WARNING':	'\033[1;33m',
+		'DEBUG':	'\033[1;34m',
+	}
+	_NORMAL = '\033[0m'
+
+	@staticmethod
+	def detect_color():
+		"""Figure out whether the runtime env wants color"""
+		if 'NOCOLOR' is os.environ:
+			return False
+		return os.isatty(sys.stdout.fileno())
+
+	def __init__(self, *args, **kwargs):
+		"""Initialize"""
+		color = kwargs.pop('color', None)
+		if color is None:
+			color = self.detect_color()
+		if not color:
+			self._COLORS = {}
+
+		super(CatalystFormatter, self).__init__(*args, **kwargs)
+
+	def format(self, record, **kwargs):
+		"""Format the |record| with our color settings"""
+		#print(dir(record))
+		#print(record.getMessage())
+		msg = super(CatalystFormatter, self).format(record, **kwargs)
+		#print('{', msg, '}')
+		color = self._COLORS.get(record.levelname)
+		if color:
+			return color + msg + self._NORMAL
+		else:
+			return msg
+
+
+def setup_logging(level, output=None, debug=False, color=None):
+	"""Initialize the logging module using the |level| level"""
+	# The incoming level will be things like "info", but setLevel wants
+	# the numeric constant.  Convert it here.
+	level = logging.getLevelName(level.upper())
+
+	# The good stuff.
+	fmt = '%(asctime)s: %(levelname)-8s: '
+	if debug:
+		fmt += '%(filename)s:%(funcName)s: '
+	fmt += '%(message)s'
+
+	# Figure out where to send the log output.
+	if output is None:
+		handler = logging.StreamHandler(stream=sys.stdout)
+	else:
+		handler = logging.FileHandler(output)
+
+	# Use a date format that is readable by humans & machines.
+	# Think e-mail/RFC 2822: 05 Oct 2013 18:58:50 EST
+	tzname = time.strftime('%Z', time.localtime())
+	datefmt = '%d %b %Y %H:%M:%S ' + tzname
+	formatter = CatalystFormatter(fmt, datefmt, color=color)
+	handler.setFormatter(formatter)
+
+	logger.addHandler(handler)
+	logger.setLevel(level)
diff --git a/catalyst/main.py b/catalyst/main.py
index e6b6447..c9a2219 100644
--- a/catalyst/main.py
+++ b/catalyst/main.py
@@ -18,6 +18,7 @@ from DeComp.definitions import (COMPRESS_DEFINITIONS, DECOMPRESS_DEFINITIONS,
 	CONTENTS_DEFINITIONS)
 from DeComp.contents import ContentsMap
 
+from catalyst import log
 import catalyst.config
 import catalyst.util
 from catalyst.defaults import confdefaults, option_messages
@@ -176,10 +177,23 @@ $ catalyst -f stage1-specfile.spec"""
 	group = parser.add_argument_group('Program output options')
 	group.add_argument('-d', '--debug',
 		default=False, action='store_true',
-		help='enable debugging')
+		help='enable debugging (and default --log-level debug)')
 	group.add_argument('-v', '--verbose',
 		default=False, action='store_true',
-		help='verbose output')
+		help='verbose output (and default --log-level info)')
+	group.add_argument('--log-level',
+		default=None,
+		choices=('critical', 'error', 'warning', 'notice', 'info', 'debug'),
+		help='set verbosity of output')
+	group.add_argument('--log-file',
+		type=FilePath(exists=False),
+		help='write all output to this file (instead of stdout)')
+	group.add_argument('--color',
+		default=None, action='store_true',
+		help='colorize output all the time (default: detect)')
+	group.add_argument('--nocolor',
+		dest='color', action='store_false',
+		help='never colorize output all the time (default: detect)')
 
 	group = parser.add_argument_group('Temporary file management')
 	group.add_argument('-a', '--clear-autoresume',
@@ -218,6 +232,18 @@ def main():
 	parser = get_parser()
 	opts = parser.parse_args(sys.argv[1:])
 
+	# Initialize the logger before anything else.
+	log_level = opts.log_level
+	if log_level is None:
+		if opts.debug:
+			log_level = 'debug'
+		elif opts.verbose:
+			log_level = 'info'
+		else:
+			log_level = 'notice'
+	log.setup_logging(log_level, output=opts.log_file, debug=opts.debug,
+		color=opts.color)
+
 	# Parse the command line options.
 	myconfig = opts.config
 	myspecfile = opts.file
-- 
2.5.2



^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [gentoo-catalyst] [PATCH 3/3] main: convert to new logging module
  2015-10-09  5:57 [gentoo-catalyst] [PATCH 1/3] main: group related command line flags Mike Frysinger
  2015-10-09  5:57 ` [gentoo-catalyst] [PATCH 2/3] log: new logging module to standardize catalyst output Mike Frysinger
@ 2015-10-09  5:57 ` Mike Frysinger
  2015-10-09 16:32   ` Brian Dolbec
  2015-10-09 16:19 ` [gentoo-catalyst] [PATCH 1/3] main: group related command line flags Brian Dolbec
  2 siblings, 1 reply; 6+ messages in thread
From: Mike Frysinger @ 2015-10-09  5:57 UTC (permalink / raw
  To: gentoo-catalyst

---
 catalyst/main.py | 104 +++++++++++++++++++++++--------------------------------
 1 file changed, 43 insertions(+), 61 deletions(-)

diff --git a/catalyst/main.py b/catalyst/main.py
index c9a2219..bc77c59 100644
--- a/catalyst/main.py
+++ b/catalyst/main.py
@@ -7,6 +7,7 @@
 # $Id$
 
 import argparse
+import datetime
 import os
 import sys
 
@@ -31,36 +32,36 @@ conf_values={}
 
 
 def version():
-	print get_version()
-	print "Copyright 2003-2008 Gentoo Foundation"
-	print "Copyright 2008-2012 various authors"
-	print "Distributed under the GNU General Public License version 2.1\n"
+	log.info(get_version())
+	log.info('Copyright 2003-%s Gentoo Foundation', datetime.datetime.now().year)
+	log.info('Copyright 2008-2012 various authors')
+	log.info('Distributed under the GNU General Public License version 2.1')
 
 def parse_config(myconfig):
 	# search a couple of different areas for the main config file
 	myconf={}
 	config_file=""
+	default_config_file = '/etc/catalyst/catalyst.conf'
 
 	# first, try the one passed (presumably from the cmdline)
 	if myconfig:
 		if os.path.exists(myconfig):
-			print "Using command line specified Catalyst configuration file, "+myconfig
+			log.notice('Using command line specified Catalyst configuration file: %s',
+				myconfig)
 			config_file=myconfig
 
 		else:
-			print "!!! catalyst: Could not use specified configuration file "+\
-				myconfig
-			sys.exit(1)
+			log.critical('Specified configuration file does not exist: %s', myconfig)
 
 	# next, try the default location
-	elif os.path.exists("/etc/catalyst/catalyst.conf"):
-		print "Using default Catalyst configuration file, /etc/catalyst/catalyst.conf"
-		config_file="/etc/catalyst/catalyst.conf"
+	elif os.path.exists(default_config_file):
+		log.notice('Using default Catalyst configuration file: %s',
+			default_config_file)
+		config_file = default_config_file
 
 	# can't find a config file (we are screwed), so bail out
 	else:
-		print "!!! catalyst: Could not find a suitable configuration file"
-		sys.exit(1)
+		log.critical('Could not find a suitable configuration file')
 
 	# now, try and parse the config file "config_file"
 	try:
@@ -69,8 +70,7 @@ def parse_config(myconfig):
 		myconf.update(myconfig.get_values())
 
 	except Exception:
-		print "!!! catalyst: Unable to parse configuration file, "+myconfig
-		sys.exit(1)
+		log.critical('Could not find parse configuration file: %s', myconfig)
 
 	# now, load up the values into conf_values so that we can use them
 	for x in list(confdefaults):
@@ -90,7 +90,7 @@ def parse_config(myconfig):
 	# print out any options messages
 	for opt in conf_values['options']:
 		if opt in option_messages:
-			print option_messages[opt]
+			log.info(option_messages[opt])
 
 	for key in ["digests", "envscript", "var_tmpfs_portage", "port_logdir",
 				"local_overlay"]:
@@ -102,7 +102,7 @@ def parse_config(myconfig):
 		conf_values["contents"] = myconf["contents"].replace("-", '_')
 
 	if "envscript" in myconf:
-		print "Envscript support enabled."
+		log.info('Envscript support enabled.')
 
 	# take care of any variable substitutions that may be left
 	for x in list(conf_values):
@@ -118,11 +118,8 @@ def import_module(target):
 	try:
 		mod_name = "catalyst.targets." + target
 		module = __import__(mod_name, [],[], ["not empty"])
-	except ImportError as e:
-		print "!!! catalyst: Python module import error: %s " % target + \
-			"in catalyst/targets/ ... exiting."
-		print "ERROR was: ", e
-		sys.exit(1)
+	except ImportError:
+		log.critical('Python module import error: %s', target, exc_info=True)
 	return module
 
 
@@ -278,7 +275,7 @@ def main():
 	parse_config(myconfig)
 
 	conf_values["options"].update(options)
-	#print "MAIN: conf_values['options'] =", conf_values["options"]
+	log.debug('conf_values[options] = %s', conf_values['options'])
 
 	# initialize our contents generator
 	contents_map = ContentsMap(CONTENTS_DEFINITIONS)
@@ -308,14 +305,13 @@ def main():
 
 		# First validate all the requested digests are valid keys.
 		if digests - valid_digests:
-			print
-			print "These are not a valid digest entries:"
-			print ', '.join(digests - valid_digests)
-			print "Valid digest entries:"
-			print ', '.join(sorted(valid_digests))
-			print
-			print "Catalyst aborting...."
-			sys.exit(2)
+			log.critical(
+				'These are not valid digest entries:\n'
+				'%s\n'
+				'Valid digest entries:\n'
+				'%s',
+				', '.join(digests - valid_digests),
+				', '.join(sorted(valid_digests)))
 
 		# Then check for any programs that the hash func requires.
 		for digest in digests:
@@ -326,37 +322,28 @@ def main():
 				if skip_missing:
 					digests.remove(digest)
 					continue
-				print
-				print "digest=" + digest
-				print "\tThe " + hash_map.hash_map[digest].cmd + \
-					" binary was not found. It needs to be in your system path"
-				print
-				print "Catalyst aborting...."
-				sys.exit(2)
+				log.critical(
+					'The "%s" binary needed by digest "%s" was not found. '
+					'It needs to be in your system path.',
+					hash_map.hash_map[digest].cmd, digest)
 
 		# Now reload the config with our updated value.
 		conf_values['digests'] = ' '.join(digests)
 
 	if "hash_function" in conf_values:
 		if conf_values["hash_function"] not in HASH_DEFINITIONS:
-			print
-			print conf_values["hash_function"]+\
-				" is not a valid hash_function entry"
-			print "Valid hash_function entries:"
-			print HASH_DEFINITIONS.keys()
-			print
-			print "Catalyst aborting...."
-			sys.exit(2)
+			log.critical(
+				'%s is not a valid hash_function entry\n'
+				'Valid hash_function entries:\n'
+				'%s', HASH_DEFINITIONS.keys())
 		try:
 			process.find_binary(hash_map.hash_map[conf_values["hash_function"]].cmd)
 		except process.CommandNotFound:
-			print
-			print "hash_function="+conf_values["hash_function"]
-			print "\tThe "+hash_map.hash_map[conf_values["hash_function"]].cmd + \
-				" binary was not found. It needs to be in your system path"
-			print
-			print "Catalyst aborting...."
-			sys.exit(2)
+			log.critical(
+				'The "%s" binary needed by hash_function "%s" was not found. '
+				'It needs to be in your system path.',
+				hash_map.hash_map[conf_values['hash_function']].cmd,
+				conf_values['hash_function'])
 
 	addlargs={}
 
@@ -370,25 +357,20 @@ def main():
 			cmdline.parse_lines(mycmdline)
 			addlargs.update(cmdline.get_values())
 		except CatalystError:
-			print "!!! catalyst: Could not parse commandline, exiting."
-			sys.exit(1)
+			log.critical('Could not parse commandline')
 
 	if "target" not in addlargs:
 		raise CatalystError("Required value \"target\" not specified.")
 
 	if os.getuid() != 0:
 		# catalyst cannot be run as a normal user due to chroots, mounts, etc
-		print "!!! catalyst: This script requires root privileges to operate"
-		sys.exit(2)
+		log.critical('This script requires root privileges to operate')
 
 	# everything is setup, so the build is a go
 	try:
 		success = build_target(addlargs)
 	except KeyboardInterrupt:
-		print "\nCatalyst build aborted due to user interrupt ( Ctrl-C )"
-		print
-		print "Catalyst aborting...."
-		sys.exit(2)
+		log.critical('Catalyst build aborted due to user interrupt (Ctrl-C)')
 	if not success:
 		sys.exit(2)
 	sys.exit(0)
-- 
2.5.2



^ permalink raw reply related	[flat|nested] 6+ messages in thread

* Re: [gentoo-catalyst] [PATCH 1/3] main: group related command line flags
  2015-10-09  5:57 [gentoo-catalyst] [PATCH 1/3] main: group related command line flags Mike Frysinger
  2015-10-09  5:57 ` [gentoo-catalyst] [PATCH 2/3] log: new logging module to standardize catalyst output Mike Frysinger
  2015-10-09  5:57 ` [gentoo-catalyst] [PATCH 3/3] main: convert to new logging module Mike Frysinger
@ 2015-10-09 16:19 ` Brian Dolbec
  2 siblings, 0 replies; 6+ messages in thread
From: Brian Dolbec @ 2015-10-09 16:19 UTC (permalink / raw
  To: gentoo-catalyst

On Fri,  9 Oct 2015 01:57:26 -0400
Mike Frysinger <vapier@gentoo.org> wrote:

> This makes the --help output more manageable so people can quickly
> scan and skip options that they don't care about.
> ---
>  catalyst/main.py | 48
> ++++++++++++++++++++++++++++-------------------- 1 file changed, 28
> insertions(+), 20 deletions(-)
> 

Looks good

-- 
Brian Dolbec <dolsen>



^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [gentoo-catalyst] [PATCH 2/3] log: new logging module to standardize catalyst output
  2015-10-09  5:57 ` [gentoo-catalyst] [PATCH 2/3] log: new logging module to standardize catalyst output Mike Frysinger
@ 2015-10-09 16:30   ` Brian Dolbec
  0 siblings, 0 replies; 6+ messages in thread
From: Brian Dolbec @ 2015-10-09 16:30 UTC (permalink / raw
  To: gentoo-catalyst

On Fri,  9 Oct 2015 01:57:27 -0400
Mike Frysinger <vapier@gentoo.org> wrote:

> This has everything you could ever want:
> - control where output is sent (stdout or a file)
> - control over log level
> - automatic exit when CRITICAL is used
> - automatic colorization of critical/error/warning messages
> - explicit control over use of colors
> - automatic handling of embedded newlines
> - standardized output format
> - all logging routed through a single logger (not the root)
> - additional log level between warning & info: notice
> 
> This just lays the groundwork -- no code is actually converted over
> to using this.  That will be done in follow up commit(s).
> 
> Note: The default output level has changed from "info" to "notice".
> That means the default display won't spam w/irrelevant details.
> 
> Example output (only the main.py module is converted):
> $ ./bin/wrapper.py -s test
> 09 Oct 2015 01:34:56 EDT: NOTICE  : Using default Catalyst
> configuration file: /etc/catalyst/catalyst.conf ContentsMap:
> __init__(), search_order = ['pixz', 'lbzip2', 'isoinfo_l',
> 'squashfs', 'gzip', 'xz', 'bzip2', 'tar', 'isoinfo_f'] Creating
> Portage tree snapshot test from /usr/portage... ^C
> 
> $ ./bin/wrapper.py -s test --debug
> 09 Oct 2015 01:35:43 EDT: INFO    : main.py:version: Catalyst git
> 09 Oct 2015 01:35:43 EDT: INFO    : main.py:version: vcs version
> 4440e8908c677d8764e29b0f127e2dd6c02b7621, date Fri Oct 9 01:28:19
> 2015 -0400 09 Oct 2015 01:35:43 EDT: INFO    : main.py:version:
> Copyright 2003-2015 Gentoo Foundation 09 Oct 2015 01:35:43 EDT:
> INFO    : main.py:version: Copyright 2008-2012 various authors 09 Oct
> 2015 01:35:43 EDT: INFO    : main.py:version: Distributed under the
> GNU General Public License version 2.1 09 Oct 2015 01:35:43 EDT:
> NOTICE  : log.py:notice: Using default Catalyst configuration
> file: /etc/catalyst/catalyst.conf 09 Oct 2015 01:35:43 EDT: INFO    :
> main.py:parse_config: Snapshot cache support enabled. 09 Oct 2015
> 01:35:43 EDT: INFO    : main.py:parse_config: Kernel cache support
> enabled. 09 Oct 2015 01:35:43 EDT: INFO    : main.py:parse_config:
> Autoresuming support enabled. 09 Oct 2015 01:35:43 EDT: INFO    :
> main.py:parse_config: Package cache support enabled. 09 Oct 2015
> 01:35:43 EDT: INFO    : main.py:parse_config: Seed cache support
> enabled. 09 Oct 2015 01:35:43 EDT: INFO    : main.py:parse_config:
> Envscript support enabled. 09 Oct 2015 01:35:43 EDT: DEBUG   :
> main.py:main: conf_values[options] = set(['snapcache', 'kerncache',
> 'autoresume', 'pkgcache', 'bindist', 'seedcache']) ^C
> 
> $ ./bin/wrapper.py -s test -C /asdf
> 09 Oct 2015 01:36:59 EDT: NOTICE  : Using default Catalyst
> configuration file: /etc/catalyst/catalyst.conf ContentsMap:
> __init__(), search_order = ['pixz', 'lbzip2', 'isoinfo_l',
> 'squashfs', 'gzip', 'xz', 'bzip2', 'tar', 'isoinfo_f']
> 
> !!! catalyst: Syntax error: 0
> 
> 09 Oct 2015 01:36:59 EDT: CRITICAL: Could not parse commandline
> ---
>  catalyst/log.py  | 130
> +++++++++++++++++++++++++++++++++++++++++++++++++++++++
> catalyst/main.py |  30 ++++++++++++- 2 files changed, 158
> insertions(+), 2 deletions(-) create mode 100644 catalyst/log.py
> 
> diff --git a/catalyst/log.py b/catalyst/log.py
> new file mode 100644
> index 0000000..bf39116
> --- /dev/null
> +++ b/catalyst/log.py
> @@ -0,0 +1,130 @@
> +# Copyright 2003-2015 Gentoo Foundation
> +# Distributed under the terms of the GNU General Public License v2
> +
> +"""Logging related code
> +
> +This largely exposes the same interface as the logging module except
> we add +another level "notice" between warning & info, and all output
> goes through +the "catalyst" logger.
> +"""
> +
> +from __future__ import print_function
> +
> +import logging
> +import logging.handlers
> +import os
> +import sys
> +import time
> +
> +
> +class CatalystLogger(logging.Logger):
> +	"""Override the _log member to autosplit on new lines"""
> +
> +	def _log(self, level, msg, args, **kwargs):
> +		"""If given a multiline message, split it"""
> +		# We have to interpolate it first in case they
> spread things out
> +		# over multiple lines like: Bad Thing:\n%s\nGoodbye!
> +		msg %= args
> +		for line in msg.splitlines():
> +			super(CatalystLogger, self)._log(level,
> line, (), **kwargs) +
> +
> +# The logger that all output should go through.
> +# This is ugly because we want to not perturb the logging module
> state. +_klass = logging.getLoggerClass()
> +logging.setLoggerClass(CatalystLogger)
> +logger = logging.getLogger('catalyst')
> +logging.setLoggerClass(_klass)
> +del _klass
> +
> +
> +# Set the notice level between warning and info.
> +NOTICE = (logging.WARNING + logging.INFO) / 2
> +logging.addLevelName(NOTICE, 'NOTICE')
> +
> +
> +# The API we expose to consumers.
> +def notice(msg, *args, **kwargs):
> +	"""Log a notice message"""
> +	logger.log(NOTICE, msg, *args, **kwargs)
> +
> +def critical(msg, *args, **kwargs):
> +	"""Log a critical message and then exit"""
> +	status = kwargs.pop('status', 1)
> +	logger.critical(msg, *args, **kwargs)
> +	sys.exit(status)
> +
> +error = logger.error
> +warning = logger.warning
> +info = logger.info
> +debug = logger.debug
> +
> +
> +class CatalystFormatter(logging.Formatter):
> +	"""Mark bad messages with colors automatically"""
> +
> +	_COLORS = {
> +		'CRITICAL':	'\033[1;35m',
> +		'ERROR':	'\033[1;31m',
> +		'WARNING':	'\033[1;33m',
> +		'DEBUG':	'\033[1;34m',
> +	}
> +	_NORMAL = '\033[0m'
> +
> +	@staticmethod
> +	def detect_color():
> +		"""Figure out whether the runtime env wants color"""
> +		if 'NOCOLOR' is os.environ:
> +			return False
> +		return os.isatty(sys.stdout.fileno())
> +
> +	def __init__(self, *args, **kwargs):
> +		"""Initialize"""
> +		color = kwargs.pop('color', None)
> +		if color is None:
> +			color = self.detect_color()
> +		if not color:
> +			self._COLORS = {}
> +
> +		super(CatalystFormatter, self).__init__(*args,
> **kwargs) +
> +	def format(self, record, **kwargs):
> +		"""Format the |record| with our color settings"""
> +		#print(dir(record))
> +		#print(record.getMessage())
> +		msg = super(CatalystFormatter, self).format(record,
> **kwargs)
> +		#print('{', msg, '}')
> +		color = self._COLORS.get(record.levelname)
> +		if color:
> +			return color + msg + self._NORMAL
> +		else:
> +			return msg
> +
> +
> +def setup_logging(level, output=None, debug=False, color=None):
> +	"""Initialize the logging module using the |level| level"""
> +	# The incoming level will be things like "info", but
> setLevel wants
> +	# the numeric constant.  Convert it here.
> +	level = logging.getLevelName(level.upper())
> +
> +	# The good stuff.
> +	fmt = '%(asctime)s: %(levelname)-8s: '
> +	if debug:
> +		fmt += '%(filename)s:%(funcName)s: '
> +	fmt += '%(message)s'
> +
> +	# Figure out where to send the log output.
> +	if output is None:
> +		handler = logging.StreamHandler(stream=sys.stdout)
> +	else:
> +		handler = logging.FileHandler(output)
> +
> +	# Use a date format that is readable by humans & machines.
> +	# Think e-mail/RFC 2822: 05 Oct 2013 18:58:50 EST
> +	tzname = time.strftime('%Z', time.localtime())
> +	datefmt = '%d %b %Y %H:%M:%S ' + tzname
> +	formatter = CatalystFormatter(fmt, datefmt, color=color)
> +	handler.setFormatter(formatter)
> +
> +	logger.addHandler(handler)
> +	logger.setLevel(level)
> diff --git a/catalyst/main.py b/catalyst/main.py
> index e6b6447..c9a2219 100644
> --- a/catalyst/main.py
> +++ b/catalyst/main.py
> @@ -18,6 +18,7 @@ from DeComp.definitions import
> (COMPRESS_DEFINITIONS, DECOMPRESS_DEFINITIONS, CONTENTS_DEFINITIONS)
>  from DeComp.contents import ContentsMap
>  
> +from catalyst import log
>  import catalyst.config
>  import catalyst.util
>  from catalyst.defaults import confdefaults, option_messages
> @@ -176,10 +177,23 @@ $ catalyst -f stage1-specfile.spec"""
>  	group = parser.add_argument_group('Program output options')
>  	group.add_argument('-d', '--debug',
>  		default=False, action='store_true',
> -		help='enable debugging')
> +		help='enable debugging (and default --log-level
> debug)') group.add_argument('-v', '--verbose',
>  		default=False, action='store_true',
> -		help='verbose output')
> +		help='verbose output (and default --log-level info)')
> +	group.add_argument('--log-level',
> +		default=None,
> +		choices=('critical', 'error', 'warning', 'notice',
> 'info', 'debug'),
> +		help='set verbosity of output')
> +	group.add_argument('--log-file',
> +		type=FilePath(exists=False),
> +		help='write all output to this file (instead of
> stdout)')
> +	group.add_argument('--color',
> +		default=None, action='store_true',
> +		help='colorize output all the time (default:
> detect)')
> +	group.add_argument('--nocolor',
> +		dest='color', action='store_false',
> +		help='never colorize output all the time (default:
> detect)') 
>  	group = parser.add_argument_group('Temporary file
> management') group.add_argument('-a', '--clear-autoresume',
> @@ -218,6 +232,18 @@ def main():
>  	parser = get_parser()
>  	opts = parser.parse_args(sys.argv[1:])
>  
> +	# Initialize the logger before anything else.
> +	log_level = opts.log_level
> +	if log_level is None:
> +		if opts.debug:
> +			log_level = 'debug'
> +		elif opts.verbose:
> +			log_level = 'info'
> +		else:
> +			log_level = 'notice'
> +	log.setup_logging(log_level, output=opts.log_file,
> debug=opts.debug,
> +		color=opts.color)
> +
>  	# Parse the command line options.
>  	myconfig = opts.config
>  	myspecfile = opts.file

Very nice. :)   I learned a bit in here ;)

In gkeys I had it log to a file always, but didn't add color to the
console output.

-- 
Brian Dolbec <dolsen>



^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [gentoo-catalyst] [PATCH 3/3] main: convert to new logging module
  2015-10-09  5:57 ` [gentoo-catalyst] [PATCH 3/3] main: convert to new logging module Mike Frysinger
@ 2015-10-09 16:32   ` Brian Dolbec
  0 siblings, 0 replies; 6+ messages in thread
From: Brian Dolbec @ 2015-10-09 16:32 UTC (permalink / raw
  To: gentoo-catalyst

On Fri,  9 Oct 2015 01:57:28 -0400
Mike Frysinger <vapier@gentoo.org> wrote:

> ---
>  catalyst/main.py | 104
> +++++++++++++++++++++++-------------------------------- 1 file
> changed, 43 insertions(+), 61 deletions(-)
> 
> diff --git a/catalyst/main.py b/catalyst/main.py
> index c9a2219..bc77c59 100644
> --- a/catalyst/main.py
> +++ b/catalyst/main.py
> @@ -7,6 +7,7 @@
>  # $Id$
>  
>  import argparse
> +import datetime
>  import os
>  import sys
>  
> @@ -31,36 +32,36 @@ conf_values={}
>  
>  
>  def version():
> -	print get_version()
> -	print "Copyright 2003-2008 Gentoo Foundation"
> -	print "Copyright 2008-2012 various authors"
> -	print "Distributed under the GNU General Public License
> version 2.1\n"
> +	log.info(get_version())
> +	log.info('Copyright 2003-%s Gentoo Foundation',
> datetime.datetime.now().year)
> +	log.info('Copyright 2008-2012 various authors')
> +	log.info('Distributed under the GNU General Public License
> version 2.1') 
>  def parse_config(myconfig):
>  	# search a couple of different areas for the main config file
>  	myconf={}
>  	config_file=""
> +	default_config_file = '/etc/catalyst/catalyst.conf'
>  
>  	# first, try the one passed (presumably from the cmdline)
>  	if myconfig:
>  		if os.path.exists(myconfig):
> -			print "Using command line specified Catalyst
> configuration file, "+myconfig
> +			log.notice('Using command line specified
> Catalyst configuration file: %s',
> +				myconfig)
>  			config_file=myconfig
>  
>  		else:
> -			print "!!! catalyst: Could not use specified
> configuration file "+\
> -				myconfig
> -			sys.exit(1)
> +			log.critical('Specified configuration file
> does not exist: %s', myconfig) 
>  	# next, try the default location
> -	elif os.path.exists("/etc/catalyst/catalyst.conf"):
> -		print "Using default Catalyst configuration
> file, /etc/catalyst/catalyst.conf"
> -		config_file="/etc/catalyst/catalyst.conf"
> +	elif os.path.exists(default_config_file):
> +		log.notice('Using default Catalyst configuration
> file: %s',
> +			default_config_file)
> +		config_file = default_config_file
>  
>  	# can't find a config file (we are screwed), so bail out
>  	else:
> -		print "!!! catalyst: Could not find a suitable
> configuration file"
> -		sys.exit(1)
> +		log.critical('Could not find a suitable
> configuration file') 
>  	# now, try and parse the config file "config_file"
>  	try:
> @@ -69,8 +70,7 @@ def parse_config(myconfig):
>  		myconf.update(myconfig.get_values())
>  
>  	except Exception:
> -		print "!!! catalyst: Unable to parse configuration
> file, "+myconfig
> -		sys.exit(1)
> +		log.critical('Could not find parse configuration
> file: %s', myconfig) 
>  	# now, load up the values into conf_values so that we can
> use them for x in list(confdefaults):
> @@ -90,7 +90,7 @@ def parse_config(myconfig):
>  	# print out any options messages
>  	for opt in conf_values['options']:
>  		if opt in option_messages:
> -			print option_messages[opt]
> +			log.info(option_messages[opt])
>  
>  	for key in ["digests", "envscript", "var_tmpfs_portage",
> "port_logdir", "local_overlay"]:
> @@ -102,7 +102,7 @@ def parse_config(myconfig):
>  		conf_values["contents"] =
> myconf["contents"].replace("-", '_') 
>  	if "envscript" in myconf:
> -		print "Envscript support enabled."
> +		log.info('Envscript support enabled.')
>  
>  	# take care of any variable substitutions that may be left
>  	for x in list(conf_values):
> @@ -118,11 +118,8 @@ def import_module(target):
>  	try:
>  		mod_name = "catalyst.targets." + target
>  		module = __import__(mod_name, [],[], ["not empty"])
> -	except ImportError as e:
> -		print "!!! catalyst: Python module import error: %s
> " % target + \
> -			"in catalyst/targets/ ... exiting."
> -		print "ERROR was: ", e
> -		sys.exit(1)
> +	except ImportError:
> +		log.critical('Python module import error: %s',
> target, exc_info=True) return module
>  
>  
> @@ -278,7 +275,7 @@ def main():
>  	parse_config(myconfig)
>  
>  	conf_values["options"].update(options)
> -	#print "MAIN: conf_values['options'] =",
> conf_values["options"]
> +	log.debug('conf_values[options] = %s',
> conf_values['options']) 
>  	# initialize our contents generator
>  	contents_map = ContentsMap(CONTENTS_DEFINITIONS)
> @@ -308,14 +305,13 @@ def main():
>  
>  		# First validate all the requested digests are valid
> keys. if digests - valid_digests:
> -			print
> -			print "These are not a valid digest entries:"
> -			print ', '.join(digests - valid_digests)
> -			print "Valid digest entries:"
> -			print ', '.join(sorted(valid_digests))
> -			print
> -			print "Catalyst aborting...."
> -			sys.exit(2)
> +			log.critical(
> +				'These are not valid digest
> entries:\n'
> +				'%s\n'
> +				'Valid digest entries:\n'
> +				'%s',
> +				', '.join(digests - valid_digests),
> +				', '.join(sorted(valid_digests)))
>  
>  		# Then check for any programs that the hash func
> requires. for digest in digests:
> @@ -326,37 +322,28 @@ def main():
>  				if skip_missing:
>  					digests.remove(digest)
>  					continue
> -				print
> -				print "digest=" + digest
> -				print "\tThe " +
> hash_map.hash_map[digest].cmd + \
> -					" binary was not found. It
> needs to be in your system path"
> -				print
> -				print "Catalyst aborting...."
> -				sys.exit(2)
> +				log.critical(
> +					'The "%s" binary needed by
> digest "%s" was not found. '
> +					'It needs to be in your
> system path.',
> +
> hash_map.hash_map[digest].cmd, digest) 
>  		# Now reload the config with our updated value.
>  		conf_values['digests'] = ' '.join(digests)
>  
>  	if "hash_function" in conf_values:
>  		if conf_values["hash_function"] not in
> HASH_DEFINITIONS:
> -			print
> -			print conf_values["hash_function"]+\
> -				" is not a valid hash_function entry"
> -			print "Valid hash_function entries:"
> -			print HASH_DEFINITIONS.keys()
> -			print
> -			print "Catalyst aborting...."
> -			sys.exit(2)
> +			log.critical(
> +				'%s is not a valid hash_function
> entry\n'
> +				'Valid hash_function entries:\n'
> +				'%s', HASH_DEFINITIONS.keys())
>  		try:
>  			process.find_binary(hash_map.hash_map[conf_values["hash_function"]].cmd)
>  		except process.CommandNotFound:
> -			print
> -			print
> "hash_function="+conf_values["hash_function"]
> -			print "\tThe
> "+hash_map.hash_map[conf_values["hash_function"]].cmd + \
> -				" binary was not found. It needs to
> be in your system path"
> -			print
> -			print "Catalyst aborting...."
> -			sys.exit(2)
> +			log.critical(
> +				'The "%s" binary needed by
> hash_function "%s" was not found. '
> +				'It needs to be in your system
> path.',
> +
> hash_map.hash_map[conf_values['hash_function']].cmd,
> +				conf_values['hash_function'])
>  
>  	addlargs={}
>  
> @@ -370,25 +357,20 @@ def main():
>  			cmdline.parse_lines(mycmdline)
>  			addlargs.update(cmdline.get_values())
>  		except CatalystError:
> -			print "!!! catalyst: Could not parse
> commandline, exiting."
> -			sys.exit(1)
> +			log.critical('Could not parse commandline')
>  
>  	if "target" not in addlargs:
>  		raise CatalystError("Required value \"target\" not
> specified.") 
>  	if os.getuid() != 0:
>  		# catalyst cannot be run as a normal user due to
> chroots, mounts, etc
> -		print "!!! catalyst: This script requires root
> privileges to operate"
> -		sys.exit(2)
> +		log.critical('This script requires root privileges
> to operate') 
>  	# everything is setup, so the build is a go
>  	try:
>  		success = build_target(addlargs)
>  	except KeyboardInterrupt:
> -		print "\nCatalyst build aborted due to user
> interrupt ( Ctrl-C )"
> -		print
> -		print "Catalyst aborting...."
> -		sys.exit(2)
> +		log.critical('Catalyst build aborted due to user
> interrupt (Ctrl-C)') if not success:
>  		sys.exit(2)
>  	sys.exit(0)


Nice :)


-- 
Brian Dolbec <dolsen>



^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2015-10-09 16:33 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2015-10-09  5:57 [gentoo-catalyst] [PATCH 1/3] main: group related command line flags Mike Frysinger
2015-10-09  5:57 ` [gentoo-catalyst] [PATCH 2/3] log: new logging module to standardize catalyst output Mike Frysinger
2015-10-09 16:30   ` Brian Dolbec
2015-10-09  5:57 ` [gentoo-catalyst] [PATCH 3/3] main: convert to new logging module Mike Frysinger
2015-10-09 16:32   ` Brian Dolbec
2015-10-09 16:19 ` [gentoo-catalyst] [PATCH 1/3] main: group related command line flags Brian Dolbec

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox