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