diff --git a/atomicapp/__init__.py b/atomicapp/__init__.py index 24dd79f8..e69de29b 100644 --- a/atomicapp/__init__.py +++ b/atomicapp/__init__.py @@ -1,41 +0,0 @@ -""" - Copyright 2015 Red Hat, Inc. - - This file is part of Atomic App. - - Atomic App is free software: you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Atomic App is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License - along with Atomic App. If not, see . -""" - -import logging - - -def set_logging(name="atomicapp", level=logging.DEBUG): - # create logger - logger = logging.getLogger() - logger.handlers = [] - logger.setLevel(level) - - # create console handler - ch = logging.StreamHandler() - - # create formatter - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - - # add formatter to ch - ch.setFormatter(formatter) - - # add ch to logger - logger.addHandler(ch) - -set_logging(level=logging.DEBUG) # override this however you want diff --git a/atomicapp/applogging.py b/atomicapp/applogging.py new file mode 100644 index 00000000..ad5030a6 --- /dev/null +++ b/atomicapp/applogging.py @@ -0,0 +1,152 @@ +""" + Copyright 2015 Red Hat, Inc. + + This file is part of Atomic App. + + Atomic App is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Atomic App is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Atomic App. If not, see . +""" + +import sys +import logging + +from atomicapp.constants import (LOGGER_COCKPIT, + LOGGER_DEFAULT) + + +class colorizeOutputFormatter(logging.Formatter): + """ + A class to colorize the log msgs based on log level + """ + + def format(self, record): + # Call the parent class to do formatting. + msg = super(colorizeOutputFormatter, self).format(record) + + # Now post process and colorize if needed + if record.levelno == logging.DEBUG: + msg = self._colorize(msg, 'cyan') + elif record.levelno == logging.WARNING: + msg = self._colorize(msg, 'yellow') + elif record.levelno == logging.INFO: + msg = self._colorize(msg, 'white') + elif record.levelno == logging.ERROR: + msg = self._colorize(msg, 'red') + else: + raise Exception("Invalid logging level {}".format(record.levelno)) + return self._make_unicode(msg) + + def _colorize(self, text, color): + """ + Colorize based upon the color codes indicated. + """ + # Console color codes + colorCodes = { + 'white': '0', 'bright white': '1;37', + 'blue': '0;34', 'bright blue': '1;34', + 'green': '0;32', 'bright green': '1;32', + 'cyan': '0;36', 'bright cyan': '1;36', + 'red': '0;31', 'bright red': '1;31', + 'purple': '0;35', 'bright purple': '1;35', + 'yellow': '0;33', 'bright yellow': '1;33', + } + return "\033[" + colorCodes[color] + "m" + text + "\033[0m" + + def _make_unicode(self, input): + """ + Convert all input to utf-8 for multi language support + """ + if type(input) != unicode: + input = input.decode('utf-8') + return input + + +class Logging: + + @staticmethod + def setup_logging(verbose=None, quiet=None, logtype=None): + """ + This function sets up logging based on the logtype requested. + The 'none' level outputs no logs at all + The 'cockpit' level outputs just logs for the cockpit logger + The 'nocolor' level prints out normal log msgs (no cockpit) without color + The 'color' level prints out normal log msgs (no cockpit) with color + """ + + # If no logtype was set then let's have a sane default + # If connected to a tty, then default to color, else, no color + if not logtype: + if sys.stdout.isatty(): + logtype = 'color' + else: + logtype = 'nocolor' + + # Determine what logging level we should use + if verbose: + logging_level = logging.DEBUG + elif quiet: + logging_level = logging.WARNING + else: + logging_level = logging.INFO + + # Get the loggers and clear out the handlers (allows this function + # to be ran more than once) + logger = logging.getLogger(LOGGER_DEFAULT) + logger.handlers = [] + cockpit_logger = logging.getLogger(LOGGER_COCKPIT) + cockpit_logger.handlers = [] + + if logtype == 'none': + # blank out both loggers + logger.addHandler(logging.NullHandler()) + cockpit_logger.addHandler(logging.NullHandler()) + return + + if logtype == 'cockpit': + # blank out normal log messages + logger.addHandler(logging.NullHandler()) + + # configure cockpit logger + handler = logging.StreamHandler(stream=sys.stdout) + formatter = logging.Formatter('atomicapp.status.%(levelname)s.message=%(message)s') + handler.setFormatter(formatter) + cockpit_logger.addHandler(handler) + cockpit_logger.setLevel(logging_level) + return + + if logtype == 'nocolor': + # blank out cockpit log messages + cockpit_logger.addHandler(logging.NullHandler()) + + # configure logger for basic no color printing to stdout + handler = logging.StreamHandler(stream=sys.stdout) + formatter = logging.Formatter('%(asctime)s - [%(levelname)s] - %(filename)s - %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + logger.setLevel(logging_level) + return + + if logtype == 'color': + # blank out cockpit log messages + cockpit_logger.addHandler(logging.NullHandler()) + + # configure logger for color printing to stdout + handler = logging.StreamHandler(stream=sys.stdout) + formatter = colorizeOutputFormatter('%(asctime)s - [%(levelname)s] - %(filename)s - %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + logger.setLevel(logging_level) + return + + # If we made it here then there is an error + raise Exception("Invalid logging output type: {}".format(logtype)) diff --git a/atomicapp/cli/main.py b/atomicapp/cli/main.py index d1fc4d35..2cf50ed6 100644 --- a/atomicapp/cli/main.py +++ b/atomicapp/cli/main.py @@ -25,7 +25,7 @@ from lockfile import LockFile from lockfile import AlreadyLocked -from atomicapp import set_logging +from atomicapp.applogging import Logging from atomicapp.constants import (__ATOMICAPPVERSION__, __NULECULESPECVERSION__, ANSWERS_FILE, @@ -34,12 +34,13 @@ CACHE_DIR, HOST_DIR, LOCK_FILE, + LOGGER_DEFAULT, PROVIDERS) from atomicapp.nulecule import NuleculeManager from atomicapp.nulecule.exceptions import NuleculeException from atomicapp.utils import Utils -logger = logging.getLogger(__name__) +logger = logging.getLogger(LOGGER_DEFAULT) def print_app_location(app_path): @@ -218,7 +219,7 @@ def create_parser(self): action="store_true", help=( "Don't actually call provider. The commands that should be " - "run will be sent to stdout but not run.")) + "run will be logged but not run.")) globals_parser.add_argument( "--answers-format", dest="answers_format", @@ -249,6 +250,18 @@ def create_parser(self): "--providerapi", dest="providerapi", help='Value for providerapi answers option.') + globals_parser.add_argument( + "--logtype", + dest="logtype", + choices=['cockpit', 'color', 'nocolor', 'none'], + help=""" + Override the default logging output. The options are: + nocolor: we will only log to stdout; + color: log to stdout with color; + cockpit: used with cockpit integration; + none: atomicapp will disable any logging. + If nothing is set and logging to file then 'nocolor' by default. + If nothing is set and logging to tty then 'color' by default.""") # === "run" SUBPARSER === run_subparser = toplevel_subparsers.add_parser( @@ -367,6 +380,9 @@ def create_parser(self): def run(self): cmdline = sys.argv[1:] # Grab args from cmdline + # Initial setup of logging (to allow for a few early debug statements) + Logging.setup_logging(verbose=True, quiet=False) + # If we are running in an openshift pod (via `oc new-app`) then # there is no cmdline but we want to default to "atomicapp run". if Utils.running_on_openshift(): @@ -399,11 +415,15 @@ def run(self): if args.mode: args.action = args.mode # Allow mode to override 'action' cmdline.insert(0, args.action) # Place 'action' at front - logger.info("Action/Mode Selected is: %s" % args.action) # Finally, parse args and give error if necessary args = self.parser.parse_args(cmdline) + # Setup logging (now with arguments from cmdline) and log a few msgs + Logging.setup_logging(args.verbose, args.quiet, args.logtype) + logger.info("Action/Mode Selected is: %s" % args.action) + logger.debug("Final parsed cmdline: {}".format(' '.join(cmdline))) + # In the case of Atomic CLI we want to allow the user to specify # a directory if they want to for "run". For that reason we won't # default the RUN label for Atomic App to provide an app_spec argument. @@ -425,17 +445,6 @@ def run(self): if hasattr(args, item) and getattr(args, item) is not None: args.cli_answers[item] = getattr(args, item) - # Set logging level - if args.verbose: - set_logging(level=logging.DEBUG) - elif args.quiet: - set_logging(level=logging.WARNING) - else: - set_logging(level=logging.INFO) - - # Now that we have set the logging level let's print out the cmdline - logger.debug("Final parsed cmdline: {}".format(' '.join(cmdline))) - lock = LockFile(os.path.join(Utils.getRoot(), LOCK_FILE)) try: lock.acquire(timeout=-1) diff --git a/atomicapp/constants.py b/atomicapp/constants.py index 04c61806..ab2a89cd 100644 --- a/atomicapp/constants.py +++ b/atomicapp/constants.py @@ -54,6 +54,10 @@ ANSWERS_FILE_SAMPLE_FORMAT = 'ini' WORKDIR = ".workdir" LOCK_FILE = "/run/lock/atomicapp.lock" + +LOGGER_DEFAULT = "atomicapp" +LOGGER_COCKPIT = "cockpit" + HOST_DIR = "/host" DEFAULT_PROVIDER = "kubernetes" diff --git a/atomicapp/nulecule/base.py b/atomicapp/nulecule/base.py index f7efca7b..fbac1ec2 100644 --- a/atomicapp/nulecule/base.py +++ b/atomicapp/nulecule/base.py @@ -10,6 +10,8 @@ from atomicapp.constants import (APP_ENT_PATH, EXTERNAL_APP_DIR, GLOBAL_CONF, + LOGGER_COCKPIT, + LOGGER_DEFAULT, MAIN_FILE, RESOURCE_KEY, PARAMS_KEY, @@ -27,7 +29,8 @@ from jsonpointer import resolve_pointer, set_pointer, JsonPointerException -logger = logging.getLogger(__name__) +cockpit_logger = logging.getLogger(LOGGER_COCKPIT) +logger = logging.getLogger(LOGGER_DEFAULT) class Nulecule(NuleculeBase): @@ -100,6 +103,7 @@ def unpack(cls, image, dest, config=None, namespace=GLOBAL_CONF, docker_handler = DockerHandler(dryrun=dryrun) docker_handler.pull(image) docker_handler.extract(image, APP_ENT_PATH, dest, update) + cockpit_logger.info("All dependencies installed successfully.") return cls.load_from_path( dest, config=config, namespace=namespace, nodeps=nodeps, dryrun=dryrun, update=update) @@ -159,6 +163,7 @@ def run(self, provider_key=None, dryrun=False): # Process components for component in self.components: component.run(provider_key, dryrun) + cockpit_logger.info("Component %s installed successfully" % provider_key) def stop(self, provider_key=None, dryrun=False): """ @@ -271,6 +276,7 @@ def load(self, nodeps=False, dryrun=False): """ Load external application of the Nulecule component. """ + cockpit_logger.info("Loading app %s ." % self.name) if self.source: if nodeps: logger.info( @@ -282,6 +288,7 @@ def run(self, provider_key, dryrun=False): """ Run the Nulecule component with the specified provider, """ + cockpit_logger.info("Deploying component %s ..." % self.name) if self._app: self._app.run(provider_key, dryrun) return @@ -346,6 +353,7 @@ def load_external_application(self, dryrun=False, update=False): update=update ) self._app = nulecule + cockpit_logger.info("Copied app successfully.") @property def components(self): diff --git a/atomicapp/nulecule/container.py b/atomicapp/nulecule/container.py index cb50c80f..cc40b426 100644 --- a/atomicapp/nulecule/container.py +++ b/atomicapp/nulecule/container.py @@ -4,11 +4,14 @@ import logging from atomicapp.constants import (APP_ENT_PATH, + LOGGER_COCKPIT, + LOGGER_DEFAULT, MAIN_FILE) from atomicapp.utils import Utils from atomicapp.nulecule.exceptions import NuleculeException -logger = logging.getLogger(__name__) +cockpit_logger = logging.getLogger(LOGGER_COCKPIT) +logger = logging.getLogger(LOGGER_DEFAULT) class DockerHandler(object): @@ -47,6 +50,7 @@ def pull(self, image, update=False): """ if not self.is_image_present(image) or update: logger.info('Pulling Docker image: %s' % image) + cockpit_logger.info('Pulling Docker image: %s' % image) pull_cmd = [self.docker_cli, 'pull', image] logger.debug(' '.join(pull_cmd)) else: @@ -58,6 +62,8 @@ def pull(self, image, update=False): elif subprocess.call(pull_cmd) != 0: raise Exception("Could not pull Docker image %s" % image) + cockpit_logger.info('Skipping pulling Docker image: %s' % image) + def extract(self, image, source, dest, update=False): """ Extracts content from a directory in a Docker image to specified @@ -109,6 +115,7 @@ def extract(self, image, source, dest, update=False): if os.path.exists(mainfile): existing_id = Utils.getAppId(mainfile) new_id = Utils.getAppId(tmpmainfile) + cockpit_logger.info("Loading app_id %s ." % new_id) if existing_id != new_id: raise NuleculeException( "Existing app (%s) and requested app (%s) differ" % diff --git a/atomicapp/nulecule/lib.py b/atomicapp/nulecule/lib.py index 4dd41f39..c1e052da 100644 --- a/atomicapp/nulecule/lib.py +++ b/atomicapp/nulecule/lib.py @@ -1,11 +1,16 @@ # -*- coding: utf-8 -*- +import logging + from atomicapp.constants import (GLOBAL_CONF, + LOGGER_COCKPIT, NAME_KEY, DEFAULTNAME_KEY, PROVIDER_KEY) from atomicapp.utils import Utils from atomicapp.plugin import Plugin +cockpit_logger = logging.getLogger(LOGGER_COCKPIT) + class NuleculeBase(object): @@ -44,6 +49,7 @@ def load_config(self, config, ask=False, skip_asking=False): config.get(GLOBAL_CONF, {}).get(param[NAME_KEY]) if value is None and (ask or ( not skip_asking and param.get(DEFAULTNAME_KEY) is None)): + cockpit_logger.info("%s is missing in answers.conf." % param[NAME_KEY]) value = Utils.askFor(param[NAME_KEY], param) elif value is None: value = param.get(DEFAULTNAME_KEY) diff --git a/atomicapp/nulecule/main.py b/atomicapp/nulecule/main.py index 6c38ace5..79a5a723 100644 --- a/atomicapp/nulecule/main.py +++ b/atomicapp/nulecule/main.py @@ -14,13 +14,16 @@ ANSWERS_FILE_SAMPLE, ANSWERS_RUNTIME_FILE, DEFAULT_ANSWERS, + LOGGER_COCKPIT, + LOGGER_DEFAULT, MAIN_FILE, PROVIDER_KEY) from atomicapp.nulecule.base import Nulecule from atomicapp.nulecule.exceptions import NuleculeException from atomicapp.utils import Utils -logger = logging.getLogger(__name__) +cockpit_logger = logging.getLogger(LOGGER_COCKPIT) +logger = logging.getLogger(LOGGER_DEFAULT) class NuleculeManager(object): @@ -191,6 +194,8 @@ def fetch(self, nodeps=False, update=False, dryrun=False, os.path.join(self.app_path, ANSWERS_FILE_SAMPLE), runtime_answers, answers_format) + cockpit_logger.info("Install Successful.") + def run(self, cli_provider, answers_output, ask, answers_format=ANSWERS_FILE_SAMPLE_FORMAT, **kwargs): """ diff --git a/atomicapp/plugin.py b/atomicapp/plugin.py index 85f52e2e..96a43f11 100644 --- a/atomicapp/plugin.py +++ b/atomicapp/plugin.py @@ -26,9 +26,11 @@ import logging from utils import Utils -from constants import HOST_DIR, PROVIDER_CONFIG_KEY +from constants import (HOST_DIR, + LOGGER_DEFAULT, + PROVIDER_CONFIG_KEY) -logger = logging.getLogger(__name__) +logger = logging.getLogger(LOGGER_DEFAULT) class Provider(object): diff --git a/atomicapp/providers/docker.py b/atomicapp/providers/docker.py index 6b116c8a..a157ccc7 100644 --- a/atomicapp/providers/docker.py +++ b/atomicapp/providers/docker.py @@ -21,11 +21,13 @@ import subprocess import re import logging -from atomicapp.constants import DEFAULT_CONTAINER_NAME, DEFAULT_NAMESPACE +from atomicapp.constants import (DEFAULT_CONTAINER_NAME, + DEFAULT_NAMESPACE, + LOGGER_DEFAULT) from atomicapp.plugin import Provider, ProviderFailedException from atomicapp.utils import Utils -logger = logging.getLogger(__name__) +logger = logging.getLogger(LOGGER_DEFAULT) class DockerProvider(Provider): diff --git a/atomicapp/providers/kubernetes.py b/atomicapp/providers/kubernetes.py index 600e28fa..ad6c8be8 100644 --- a/atomicapp/providers/kubernetes.py +++ b/atomicapp/providers/kubernetes.py @@ -22,10 +22,13 @@ import os from string import Template +from atomicapp.constants import (LOGGER_COCKPIT, + LOGGER_DEFAULT) from atomicapp.plugin import Provider, ProviderFailedException -from atomicapp.utils import printErrorStatus, Utils +from atomicapp.utils import Utils -logger = logging.getLogger(__name__) +cockpit_logger = logging.getLogger(LOGGER_COCKPIT) +logger = logging.getLogger(LOGGER_DEFAULT) class KubernetesProvider(Provider): @@ -123,7 +126,7 @@ def process_k8s_artifacts(self): except Exception: msg = "Error processing %s artifcats, Error:" % os.path.join( self.path, artifact) - printErrorStatus(msg) + cockpit_logger.error(msg) raise if "kind" in data: self.k8s_manifests.append((data["kind"].lower(), artifact)) diff --git a/atomicapp/providers/marathon.py b/atomicapp/providers/marathon.py index 40d2d7f7..c22165d8 100644 --- a/atomicapp/providers/marathon.py +++ b/atomicapp/providers/marathon.py @@ -21,12 +21,14 @@ import urlparse import logging import os +from atomicapp.constants import (LOGGER_COCKPIT, + LOGGER_DEFAULT) from atomicapp.plugin import Provider, ProviderFailedException -from atomicapp.utils import printErrorStatus from atomicapp.utils import Utils from atomicapp.constants import PROVIDER_API_KEY -logger = logging.getLogger(__name__) +cockpit_logger = logging.getLogger(LOGGER_COCKPIT) +logger = logging.getLogger(LOGGER_DEFAULT) class Marathon(Provider): @@ -115,10 +117,10 @@ def _process_artifacts(self): # every marathon app has to have id. 'id' key is also used for showing messages if "id" not in data.keys(): msg = "Error processing %s artifact. There is no id" % artifact - printErrorStatus(msg) + cockpit_logger.error(msg) raise ProviderFailedException(msg) except anymarkup.AnyMarkupError, e: msg = "Error processing artifact - %s" % e - printErrorStatus(msg) + cockpit_logger.error(msg) raise ProviderFailedException(msg) self.marathon_artifacts.append(data) diff --git a/atomicapp/providers/openshift.py b/atomicapp/providers/openshift.py index 3695d12f..0a73b811 100644 --- a/atomicapp/providers/openshift.py +++ b/atomicapp/providers/openshift.py @@ -32,13 +32,14 @@ from atomicapp.constants import (ACCESS_TOKEN_KEY, ANSWERS_FILE, DEFAULT_NAMESPACE, + LOGGER_DEFAULT, NAMESPACE_KEY, PROVIDER_API_KEY, PROVIDER_TLS_VERIFY_KEY, PROVIDER_CA_KEY) from requests.exceptions import SSLError import logging -logger = logging.getLogger(__name__) +logger = logging.getLogger(LOGGER_DEFAULT) # If running in an openshift POD via `oc new-app`, the ca file is here OPENSHIFT_POD_CA_FILE = "/run/secrets/kubernetes.io/serviceaccount/ca.crt" diff --git a/atomicapp/requirements.py b/atomicapp/requirements.py index 899b26d2..f618ffbc 100644 --- a/atomicapp/requirements.py +++ b/atomicapp/requirements.py @@ -1,9 +1,10 @@ import logging -from atomicapp.constants import REQUIREMENT_FUNCTIONS +from atomicapp.constants import (LOGGER_DEFAULT, + REQUIREMENT_FUNCTIONS) from atomicapp.plugin import Plugin -logger = logging.getLogger(__name__) +logger = logging.getLogger(LOGGER_DEFAULT) class Requirements: diff --git a/atomicapp/utils.py b/atomicapp/utils.py index d25bd6d8..bb7a77c3 100644 --- a/atomicapp/utils.py +++ b/atomicapp/utils.py @@ -36,28 +36,19 @@ CACHE_DIR, EXTERNAL_APP_DIR, HOST_DIR, + LOGGER_COCKPIT, + LOGGER_DEFAULT, WORKDIR) __all__ = ('Utils') -logger = logging.getLogger(__name__) +cockpit_logger = logging.getLogger(LOGGER_COCKPIT) +logger = logging.getLogger(LOGGER_DEFAULT) class AtomicAppUtilsException(Exception): pass -# Following Methods(printStatus, printErrorStatus) -# are required for Cockpit or thirdparty management tool integration -# DONOT change the atomicapp.status.* prefix in the logger method. - - -def printStatus(message): - logger.info("atomicapp.status.info.message=" + str(message)) - - -def printErrorStatus(message): - logger.info("atomicapp.status.error.message=" + str(message)) - def find_binary(executable, path=None): """Tries to find 'executable' in the directories listed in 'path'. @@ -270,7 +261,7 @@ def run_cmd(cmd, checkexitcode=True, stdin=None): # we were asked not to. if checkexitcode: if ec != 0: - printErrorStatus("cmd failed: %s" % str(cmd)) # For cockpit + cockpit_logger.error("cmd failed: %s" % str(cmd)) raise AtomicAppUtilsException( "cmd: %s failed: \n%s" % (str(cmd), stderr)) diff --git a/tests/units/cli/test_default_provider.py b/tests/units/cli/test_default_provider.py index 73437966..cb16a9b2 100644 --- a/tests/units/cli/test_default_provider.py +++ b/tests/units/cli/test_default_provider.py @@ -61,16 +61,16 @@ def test_run_helloapache_app(self, capsys): # Run the dry-run command with pytest.raises(SystemExit) as exec_info: self.exec_cli(command) - nil, out = capsys.readouterr() + stdout, stderr = capsys.readouterr() # Tear down and remove all those useless generated files self.tear_down() # Print out what we've captured just in case the test fails - print out + print stdout # Since this a Docker-only provider test, docker *should* be in it, NOT Kubernetes - assert "u'provider': u'docker'" in out - assert "Deploying to Kubernetes" not in out + assert "u'provider': u'docker'" in stdout + assert "Deploying to Kubernetes" not in stdout assert exec_info.value.code == 0