Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 1 | #!/usr/bin/python2 |
| 2 | # |
| 3 | # Copyright 2016 The Chromium OS Authors. All rights reserved. |
| 4 | # Use of this source code is governed by a BSD-style license that can be |
| 5 | # found in the LICENSE file. |
| 6 | |
| 7 | from __future__ import print_function |
| 8 | |
| 9 | import argparse |
| 10 | import logging |
| 11 | import os |
| 12 | import signal |
| 13 | import sys |
| 14 | import threading |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 15 | |
| 16 | import instalog_common # pylint: disable=W0611 |
| 17 | from instalog import core |
| 18 | from instalog import daemon_utils |
Joel Kitching | d01336a | 2016-08-03 08:14:59 +0800 | [diff] [blame] | 19 | from instalog import log_utils |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 20 | from instalog.utils import sync_utils |
| 21 | from instalog.utils import type_utils |
| 22 | |
Joel Kitching | 3a5e76c | 2016-12-21 13:22:22 +0800 | [diff] [blame] | 23 | from instalog.external import jsonrpclib |
| 24 | from instalog.external import yaml |
| 25 | |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 26 | |
Joel Kitching | 860649d | 2016-11-21 14:53:02 +0800 | [diff] [blame] | 27 | # The default number of seconds to wait before giving up on a flush. |
chuntsen | dc33ae8 | 2017-01-17 15:22:16 +0800 | [diff] [blame^] | 28 | _DEFAULT_FLUSH_TIMEOUT = 30 |
| 29 | _DEFAULT_STOP_TIMEOUT = 10 |
Joel Kitching | 860649d | 2016-11-21 14:53:02 +0800 | [diff] [blame] | 30 | |
| 31 | |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 32 | class InstalogService(daemon_utils.Daemon): |
| 33 | """Represents the Instalog daemon service.""" |
| 34 | |
| 35 | def __init__(self, config, logging_level): |
| 36 | self._config = config |
| 37 | self._logging_level = logging_level |
| 38 | self._core = None |
| 39 | self._signal_handler_lock = threading.Lock() |
| 40 | super(InstalogService, self).__init__( |
| 41 | pidfile=config['instalog']['pid_file']) |
| 42 | |
| 43 | def _SignalHandler(self, signal_num, frame): |
| 44 | """Signal handler to stop Instalog on SIGINT.""" |
| 45 | logging.debug('_SignalHandler called with signalnum=%s', signal_num) |
| 46 | del frame |
| 47 | if signal_num is not signal.SIGINT: |
| 48 | return |
| 49 | self._signal_handler_lock.acquire(False) |
| 50 | if self._core: |
| 51 | logging.warning('SIGINT detected, stopping') |
| 52 | self._core.Stop() |
| 53 | self._core = None |
| 54 | self._signal_handler_lock.release() |
| 55 | |
| 56 | def _InitLogging(self): |
| 57 | """Sets up logging.""" |
Joel Kitching | d01336a | 2016-08-03 08:14:59 +0800 | [diff] [blame] | 58 | # TODO(kitching): Refactor (some of) this code to log_utils module. |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 59 | # Get logger. |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 60 | logger = logging.getLogger() |
| 61 | logger.setLevel(logging.DEBUG) |
| 62 | |
Joel Kitching | 24e0124 | 2016-11-25 15:17:29 +0800 | [diff] [blame] | 63 | # In certain situations, a StreamHandler to stdout is created implicitly. |
| 64 | # Since we want to create our own, we need to remove the default one if it |
| 65 | # exists. |
| 66 | if logger.handlers: |
| 67 | logger.removeHandler(logger.handlers[0]) |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 68 | |
| 69 | # Create formatter. |
Joel Kitching | d01336a | 2016-08-03 08:14:59 +0800 | [diff] [blame] | 70 | formatter = logging.Formatter(log_utils.LOG_FORMAT) |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 71 | |
| 72 | # Save logging calls to log file. |
| 73 | fh = logging.FileHandler(self._config['instalog']['log_file']) |
| 74 | fh.setFormatter(formatter) |
| 75 | fh.setLevel(self._logging_level) |
| 76 | logger.addHandler(fh) |
| 77 | |
| 78 | # Output logging calls to console (for when foreground=True). |
| 79 | sh = logging.StreamHandler() |
| 80 | sh.setFormatter(formatter) |
| 81 | sh.setLevel(self._logging_level) |
| 82 | logger.addHandler(sh) |
| 83 | |
| 84 | def Run(self, foreground): |
| 85 | """Starts Instalog.""" |
| 86 | self._InitLogging() |
| 87 | |
| 88 | if foreground: |
| 89 | signal.signal(signal.SIGINT, self._SignalHandler) |
| 90 | |
| 91 | self._core = core.Instalog( |
| 92 | node_id=self._config['instalog']['node_id'], |
Joel Kitching | 9edac3b | 2016-10-23 10:40:24 +0700 | [diff] [blame] | 93 | data_dir=self._config['instalog']['data_dir'], |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 94 | cli_hostname=self._config['instalog']['cli_hostname'], |
| 95 | cli_port=self._config['instalog']['cli_port'], |
| 96 | buffer_plugin=self._config['buffer'], |
| 97 | input_plugins=self._config['input'], |
| 98 | output_plugins=self._config['output']) |
| 99 | self._core.Run() |
| 100 | |
| 101 | |
| 102 | class InstalogCLI(object): |
| 103 | """Represents the CLI interface used to control Instalog.""" |
| 104 | |
| 105 | def __init__(self, args): |
| 106 | # Read config file. |
| 107 | config_path = self._LocateConfigFile(args.config) |
| 108 | if config_path is None: |
| 109 | exit('No config file found') |
| 110 | with open(config_path) as f: |
| 111 | config = yaml.load(f) |
| 112 | |
| 113 | # logging.WARNING = 30, logging.INFO = 20, logging.DEBUG = 10 |
| 114 | logging_level = logging.INFO - ((args.verbose - args.quiet) * 10) |
| 115 | |
| 116 | self._service = InstalogService(config, logging_level) |
| 117 | self._core = jsonrpclib.Server( |
| 118 | 'http://%s:%s' % (config['instalog']['cli_hostname'], |
| 119 | config['instalog']['cli_port'])) |
| 120 | |
| 121 | if args.cmd == 'start': |
| 122 | self.Start(args.foreground) |
| 123 | elif args.cmd == 'stop': |
chuntsen | dc33ae8 | 2017-01-17 15:22:16 +0800 | [diff] [blame^] | 124 | self.Stop(args.timeout) |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 125 | elif args.cmd == 'restart': |
| 126 | self.Restart() |
| 127 | elif args.cmd == 'status': |
| 128 | self.Status() |
Joel Kitching | 2329397 | 2016-11-18 17:24:47 +0800 | [diff] [blame] | 129 | elif args.cmd == 'inspect': |
| 130 | self.Inspect(args.plugin_id, args.json_path) |
Joel Kitching | 860649d | 2016-11-21 14:53:02 +0800 | [diff] [blame] | 131 | elif args.cmd == 'flush': |
| 132 | self.Flush(args.plugin_id, args.timeout) |
Joel Kitching | 2329397 | 2016-11-18 17:24:47 +0800 | [diff] [blame] | 133 | |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 134 | |
| 135 | def _LocateConfigFile(self, user_path): |
| 136 | """Locates the config file that should be used by Instalog.""" |
| 137 | if user_path: |
| 138 | return user_path |
| 139 | paths = [ |
| 140 | os.path.join(os.getcwd(), 'instalog.yaml'), |
| 141 | os.path.join(os.path.dirname(os.path.realpath(__file__)), |
| 142 | 'instalog.yaml'), |
| 143 | os.path.join(os.path.expanduser('~'), '.instalog.yaml'), |
Joel Kitching | 24e0124 | 2016-11-25 15:17:29 +0800 | [diff] [blame] | 144 | os.path.join(os.sep, 'etc', 'instalog.yaml'), |
Hung-Te Lin | b0a32bb | 2017-01-05 15:11:06 +0800 | [diff] [blame] | 145 | os.path.join(os.sep, 'run', 'instalog.yaml')] |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 146 | for path in paths: |
| 147 | logging.debug('Checking %s for config file...', path) |
| 148 | if os.path.exists(path): |
| 149 | logging.info('Config file found at %s', path) |
| 150 | return path |
| 151 | |
| 152 | def Restart(self): |
| 153 | """Restarts the daemon.""" |
chuntsen | dc33ae8 | 2017-01-17 15:22:16 +0800 | [diff] [blame^] | 154 | self.Stop(_DEFAULT_STOP_TIMEOUT) |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 155 | self.Start(False) |
| 156 | |
| 157 | def Start(self, foreground): |
| 158 | """Starts the daemon. |
| 159 | |
| 160 | Args: |
| 161 | foreground: Does not detach the daemon. |
| 162 | """ |
| 163 | print('Starting...') |
| 164 | self._service.Start(foreground) |
| 165 | if foreground: |
| 166 | return |
Joel Kitching | 11bbbe7 | 2016-12-07 11:43:38 +0800 | [diff] [blame] | 167 | |
| 168 | # First, wait for the daemon process to start. |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 169 | try: |
| 170 | sync_utils.WaitFor(self._service.IsRunning, 10) |
| 171 | except type_utils.TimeoutError: |
Joel Kitching | 11bbbe7 | 2016-12-07 11:43:38 +0800 | [diff] [blame] | 172 | print('Daemon could not be brought up, check the logs') |
| 173 | sys.exit(1) |
| 174 | |
| 175 | def TryIsUp(): |
| 176 | try: |
| 177 | # Perform the real check to see if Instalog is up internally. |
| 178 | return self._core.IsUp() |
| 179 | except Exception: |
| 180 | raise type_utils.TimeoutError('Could not call core IsUp') |
| 181 | |
| 182 | try: |
| 183 | if sync_utils.WaitFor(TryIsUp, 10): |
| 184 | print('DONE') |
| 185 | return |
| 186 | except type_utils.TimeoutError: |
| 187 | pass |
| 188 | print('Daemon could not be brought up, check the logs') |
| 189 | sys.exit(1) |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 190 | |
chuntsen | dc33ae8 | 2017-01-17 15:22:16 +0800 | [diff] [blame^] | 191 | def Stop(self, timeout): |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 192 | """Stops the daemon.""" |
| 193 | # First, send the "stop" instruction to the daemon. |
| 194 | print('Stopping...') |
| 195 | try: |
| 196 | self._core.Stop() |
| 197 | except Exception: |
| 198 | print('Could not connect to daemon, is it running?') |
| 199 | sys.exit(1) |
| 200 | |
| 201 | # Then, wait for the process to come down. |
| 202 | try: |
chuntsen | dc33ae8 | 2017-01-17 15:22:16 +0800 | [diff] [blame^] | 203 | sync_utils.WaitFor(self._service.IsStopped, timeout) |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 204 | except type_utils.TimeoutError: |
| 205 | print('Still shutting down?') |
| 206 | sys.exit(1) |
| 207 | else: |
| 208 | print('DONE') |
| 209 | |
| 210 | def Status(self): |
| 211 | """Prints the status of the daemon.""" |
| 212 | running = self._service.IsRunning() |
| 213 | print('UP' if running else 'DOWN') |
| 214 | |
Joel Kitching | 2329397 | 2016-11-18 17:24:47 +0800 | [diff] [blame] | 215 | def Inspect(self, plugin_id, json_path): |
| 216 | """Inspects the store of a given plugin.""" |
| 217 | success, value = self._core.Inspect(plugin_id, json_path) |
| 218 | print(value) |
| 219 | if not success: |
| 220 | sys.exit(1) |
| 221 | |
Joel Kitching | 860649d | 2016-11-21 14:53:02 +0800 | [diff] [blame] | 222 | def Flush(self, plugin_id, timeout): |
| 223 | """Flushes the given plugin with given timeout.""" |
| 224 | success, value = self._core.Flush(plugin_id, timeout) |
| 225 | print(value) |
| 226 | if not success: |
| 227 | sys.exit(1) |
| 228 | |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 229 | |
| 230 | if __name__ == '__main__': |
| 231 | parser = argparse.ArgumentParser() |
| 232 | parser.add_argument( |
| 233 | '--config', '-c', |
| 234 | help='config file path; by default, searches: \n' |
| 235 | '$PWD/instalog.yaml py/instalog/instalog.yaml ' |
Hung-Te Lin | b0a32bb | 2017-01-05 15:11:06 +0800 | [diff] [blame] | 236 | '~/.instalog.yaml /etc/instalog.yaml /run/instalog.yaml') |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 237 | parser.add_argument( |
| 238 | '--verbose', '-v', action='count', default=0, |
| 239 | help='increase verbosity') |
| 240 | parser.add_argument( |
| 241 | '--quiet', '-q', action='count', default=0, |
| 242 | help='decrease verbosity') |
| 243 | |
| 244 | subparsers = parser.add_subparsers(title='commands') |
| 245 | |
| 246 | start_parser = subparsers.add_parser('start', help='start Instalog') |
| 247 | start_parser.set_defaults(cmd='start') |
| 248 | start_parser.add_argument( |
| 249 | '--no-daemon', '-n', dest='foreground', action='store_true', |
| 250 | help='keep in foreground') |
| 251 | |
| 252 | stop_parser = subparsers.add_parser('stop', help='stop Instalog') |
| 253 | stop_parser.set_defaults(cmd='stop') |
chuntsen | dc33ae8 | 2017-01-17 15:22:16 +0800 | [diff] [blame^] | 254 | stop_parser.add_argument( |
| 255 | '--timeout', '-w', type=float, |
| 256 | required=False, default=_DEFAULT_STOP_TIMEOUT, |
| 257 | help='time to wait before giving up') |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 258 | |
| 259 | restart_parser = subparsers.add_parser('restart', help='restart Instalog') |
| 260 | restart_parser.set_defaults(cmd='restart') |
| 261 | |
| 262 | status_parser = subparsers.add_parser('status', help='print Instalog status') |
| 263 | status_parser.set_defaults(cmd='status') |
| 264 | |
Joel Kitching | 2329397 | 2016-11-18 17:24:47 +0800 | [diff] [blame] | 265 | inspect_parser = subparsers.add_parser('inspect', help='inspect plugin store') |
| 266 | inspect_parser.set_defaults(cmd='inspect') |
| 267 | inspect_parser.add_argument( |
| 268 | 'plugin_id', type=str, help='ID of plugin to inspect') |
| 269 | inspect_parser.add_argument( |
| 270 | 'json_path', type=str, nargs='?', default='.', |
| 271 | help='path of store JSON to print') |
| 272 | |
Joel Kitching | 860649d | 2016-11-21 14:53:02 +0800 | [diff] [blame] | 273 | flush_parser = subparsers.add_parser('flush', help='flush plugin') |
| 274 | flush_parser.set_defaults(cmd='flush') |
| 275 | flush_parser.add_argument( |
| 276 | '--timeout', '-w', type=float, |
chuntsen | dc33ae8 | 2017-01-17 15:22:16 +0800 | [diff] [blame^] | 277 | required=False, default=_DEFAULT_FLUSH_TIMEOUT, |
Joel Kitching | 860649d | 2016-11-21 14:53:02 +0800 | [diff] [blame] | 278 | help='time to wait before giving up') |
| 279 | flush_parser.add_argument( |
| 280 | 'plugin_id', type=str, nargs='?', default=None, |
| 281 | help='ID of plugin to flush') |
| 282 | |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 283 | args = parser.parse_args() |
| 284 | |
| 285 | InstalogCLI(args) |