blob: a0c272943ccb2ec89725abb46ae6e2e3dbc2e46e [file] [log] [blame]
Yilin Yang19da6932019-12-10 13:39:28 +08001#!/usr/bin/env python3
Joel Kitching3e72e8b2016-07-04 17:03:00 +08002#
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
7from __future__ import print_function
8
9import argparse
chuntsenf2e3ebd2019-10-28 16:49:57 +080010import json
Joel Kitching3e72e8b2016-07-04 17:03:00 +080011import logging
12import os
13import signal
14import sys
chuntsen4cf110a2018-02-13 18:35:20 +080015import tarfile
Joel Kitching3e72e8b2016-07-04 17:03:00 +080016
Wei-Han Chend665b002019-10-03 16:16:09 +080017from cros.factory.instalog import core
18from cros.factory.instalog import daemon_utils
Wei-Han Chen12edbb22019-10-03 16:40:00 +080019from cros.factory.instalog import instalog_common
Wei-Han Chend665b002019-10-03 16:16:09 +080020from cros.factory.instalog import log_utils
21from cros.factory.instalog.utils import file_utils
22from cros.factory.instalog.utils import sync_utils
23from cros.factory.instalog.utils import type_utils
Joel Kitching3e72e8b2016-07-04 17:03:00 +080024
Wei-Han Chend665b002019-10-03 16:16:09 +080025from cros.factory.instalog.external import jsonrpclib
26from cros.factory.instalog.external import yaml
Joel Kitching3a5e76c2016-12-21 13:22:22 +080027
Joel Kitching3e72e8b2016-07-04 17:03:00 +080028
Joel Kitching860649d2016-11-21 14:53:02 +080029# The default number of seconds to wait before giving up on a flush.
chuntsendc33ae82017-01-17 15:22:16 +080030_DEFAULT_FLUSH_TIMEOUT = 30
31_DEFAULT_STOP_TIMEOUT = 10
Joel Kitching860649d2016-11-21 14:53:02 +080032
33
Joel Kitching3e72e8b2016-07-04 17:03:00 +080034class InstalogService(daemon_utils.Daemon):
35 """Represents the Instalog daemon service."""
36
37 def __init__(self, config, logging_level):
38 self._config = config
39 self._logging_level = logging_level
40 self._core = None
Joel Kitching3e72e8b2016-07-04 17:03:00 +080041 super(InstalogService, self).__init__(
42 pidfile=config['instalog']['pid_file'])
43
44 def _SignalHandler(self, signal_num, frame):
Joel Kitching8e514ce2017-03-29 14:04:33 +080045 """Signal handler to stop Instalog on SIGINT or SIGTERM."""
Joel Kitching3e72e8b2016-07-04 17:03:00 +080046 del frame
Joel Kitching8e514ce2017-03-29 14:04:33 +080047 logging.debug('_SignalHandler called with signalnum=%s', signal_num)
48 if signal_num not in [signal.SIGINT, signal.SIGTERM]:
Joel Kitching3e72e8b2016-07-04 17:03:00 +080049 return
Joel Kitching3e72e8b2016-07-04 17:03:00 +080050 if self._core:
Joel Kitching8e514ce2017-03-29 14:04:33 +080051 # No need for a lock since _SignalHandler will only ever be called from
52 # Instalog's main thread.
53 signal_string = 'SIGINT' if signal_num == signal.SIGINT else 'SIGTERM'
54 logging.warning('%s detected, stopping', signal_string)
Joel Kitching3e72e8b2016-07-04 17:03:00 +080055 self._core.Stop()
56 self._core = None
Joel Kitching3e72e8b2016-07-04 17:03:00 +080057
chuntsenc8e2a572018-01-11 19:30:40 +080058 def _InitLogging(self, foreground):
Joel Kitching3e72e8b2016-07-04 17:03:00 +080059 """Sets up logging."""
chuntsenc8e2a572018-01-11 19:30:40 +080060 handlers = []
Joel Kitching3e72e8b2016-07-04 17:03:00 +080061
62 # Save logging calls to log file.
Peter Shihb4e49352017-05-25 17:35:11 +080063 log_file = self._config['instalog']['log_file']
64 file_utils.TryMakeDirs(os.path.dirname(log_file))
chuntsenc8e2a572018-01-11 19:30:40 +080065 handlers.append(log_utils.GetFileHandler(log_file, self._logging_level))
Joel Kitching3e72e8b2016-07-04 17:03:00 +080066
chuntsenc8e2a572018-01-11 19:30:40 +080067 # Output logging calls to console when foreground is set.
68 if foreground:
69 handlers.append(log_utils.GetStreamHandler(self._logging_level))
70
71 log_utils.InitLogging(handlers)
Joel Kitching3e72e8b2016-07-04 17:03:00 +080072
Chun-Tsen Kuoa874f782018-03-26 14:26:13 +080073 def Run(self, foreground, rpc_ready=None):
Joel Kitching3e72e8b2016-07-04 17:03:00 +080074 """Starts Instalog."""
chuntsenc8e2a572018-01-11 19:30:40 +080075 self._InitLogging(foreground)
Joel Kitching3e72e8b2016-07-04 17:03:00 +080076
Joel Kitching8e514ce2017-03-29 14:04:33 +080077 signal.signal(signal.SIGINT, self._SignalHandler)
78 signal.signal(signal.SIGTERM, self._SignalHandler)
Joel Kitching3e72e8b2016-07-04 17:03:00 +080079
80 self._core = core.Instalog(
81 node_id=self._config['instalog']['node_id'],
Joel Kitching9edac3b2016-10-23 10:40:24 +070082 data_dir=self._config['instalog']['data_dir'],
Joel Kitching3e72e8b2016-07-04 17:03:00 +080083 cli_hostname=self._config['instalog']['cli_hostname'],
84 cli_port=self._config['instalog']['cli_port'],
85 buffer_plugin=self._config['buffer'],
86 input_plugins=self._config['input'],
87 output_plugins=self._config['output'])
Chun-Tsen Kuoa874f782018-03-26 14:26:13 +080088 # After the core initialized, the RPC server is ready.
89 if rpc_ready:
90 rpc_ready.set()
Joel Kitching3e72e8b2016-07-04 17:03:00 +080091 self._core.Run()
92
93
Fei Shaobd07c9a2020-06-15 19:04:50 +080094class InstalogCLI:
Joel Kitching3e72e8b2016-07-04 17:03:00 +080095 """Represents the CLI interface used to control Instalog."""
96
97 def __init__(self, args):
98 # Read config file.
99 config_path = self._LocateConfigFile(args.config)
100 if config_path is None:
101 exit('No config file found')
102 with open(config_path) as f:
103 config = yaml.load(f)
chuntsen4cf110a2018-02-13 18:35:20 +0800104 self._CheckDataDir(config)
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800105
106 # logging.WARNING = 30, logging.INFO = 20, logging.DEBUG = 10
107 logging_level = logging.INFO - ((args.verbose - args.quiet) * 10)
108
109 self._service = InstalogService(config, logging_level)
110 self._core = jsonrpclib.Server(
111 'http://%s:%s' % (config['instalog']['cli_hostname'],
112 config['instalog']['cli_port']))
113
114 if args.cmd == 'start':
115 self.Start(args.foreground)
116 elif args.cmd == 'stop':
chuntsendc33ae82017-01-17 15:22:16 +0800117 self.Stop(args.timeout)
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800118 elif args.cmd == 'restart':
119 self.Restart()
120 elif args.cmd == 'status':
121 self.Status()
Joel Kitching23293972016-11-18 17:24:47 +0800122 elif args.cmd == 'inspect':
123 self.Inspect(args.plugin_id, args.json_path)
Joel Kitching860649d2016-11-21 14:53:02 +0800124 elif args.cmd == 'flush':
125 self.Flush(args.plugin_id, args.timeout)
chuntsen4cf110a2018-02-13 18:35:20 +0800126 elif args.cmd == 'archive':
chuntsenaabb2422018-03-07 16:16:09 +0800127 self.Archive(config_path, config['instalog']['data_dir'],
chuntsen4cf110a2018-02-13 18:35:20 +0800128 args.archive_path, args.details)
chuntsen65e9f382018-07-09 18:24:32 +0800129 elif args.cmd == 'progress':
130 self.Progress(args.plugin_id, args.details)
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800131
132 def _LocateConfigFile(self, user_path):
133 """Locates the config file that should be used by Instalog."""
134 if user_path:
135 return user_path
136 paths = [
137 os.path.join(os.getcwd(), 'instalog.yaml'),
138 os.path.join(os.path.dirname(os.path.realpath(__file__)),
139 'instalog.yaml'),
140 os.path.join(os.path.expanduser('~'), '.instalog.yaml'),
Joel Kitching24e01242016-11-25 15:17:29 +0800141 os.path.join(os.sep, 'etc', 'instalog.yaml'),
Hung-Te Linb0a32bb2017-01-05 15:11:06 +0800142 os.path.join(os.sep, 'run', 'instalog.yaml')]
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800143 for path in paths:
144 logging.debug('Checking %s for config file...', path)
145 if os.path.exists(path):
146 logging.info('Config file found at %s', path)
147 return path
chuntsenf2e3ebd2019-10-28 16:49:57 +0800148 return None
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800149
chuntsen4cf110a2018-02-13 18:35:20 +0800150 def _CheckDataDir(self, config):
151 data_dir = config['instalog']['data_dir']
152 if not os.path.exists(data_dir):
153 os.makedirs(data_dir)
154 instalog_dir = instalog_common.INSTALOG_DIR
155 for path, unused_dirs, unused_files in os.walk(
156 instalog_dir, followlinks=True):
157 if not os.path.islink(path) and os.path.samefile(path, data_dir):
158 print('You should not put the data_dir in the Instalog source code')
159 sys.exit(1)
160
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800161 def Restart(self):
162 """Restarts the daemon."""
chuntsendc33ae82017-01-17 15:22:16 +0800163 self.Stop(_DEFAULT_STOP_TIMEOUT)
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800164 self.Start(False)
165
166 def Start(self, foreground):
167 """Starts the daemon.
168
169 Args:
170 foreground: Does not detach the daemon.
171 """
172 print('Starting...')
Chun-Tsen Kuoa874f782018-03-26 14:26:13 +0800173 if not self._service.Start(foreground):
174 return
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800175 if foreground:
176 return
Joel Kitching11bbbe72016-12-07 11:43:38 +0800177
178 # First, wait for the daemon process to start.
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800179 try:
180 sync_utils.WaitFor(self._service.IsRunning, 10)
181 except type_utils.TimeoutError:
Joel Kitching11bbbe72016-12-07 11:43:38 +0800182 print('Daemon could not be brought up, check the logs')
183 sys.exit(1)
184
185 def TryIsUp():
186 try:
187 # Perform the real check to see if Instalog is up internally.
188 return self._core.IsUp()
189 except Exception:
190 raise type_utils.TimeoutError('Could not call core IsUp')
191
192 try:
Chun-Tsen Kuoa874f782018-03-26 14:26:13 +0800193 print('Waiting for the core is up...')
Joel Kitching11bbbe72016-12-07 11:43:38 +0800194 if sync_utils.WaitFor(TryIsUp, 10):
195 print('DONE')
196 return
197 except type_utils.TimeoutError:
198 pass
199 print('Daemon could not be brought up, check the logs')
200 sys.exit(1)
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800201
chuntsendc33ae82017-01-17 15:22:16 +0800202 def Stop(self, timeout):
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800203 """Stops the daemon."""
204 # First, send the "stop" instruction to the daemon.
205 print('Stopping...')
206 try:
207 self._core.Stop()
208 except Exception:
209 print('Could not connect to daemon, is it running?')
210 sys.exit(1)
211
212 # Then, wait for the process to come down.
213 try:
chuntsendc33ae82017-01-17 15:22:16 +0800214 sync_utils.WaitFor(self._service.IsStopped, timeout)
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800215 except type_utils.TimeoutError:
216 print('Still shutting down?')
217 sys.exit(1)
218 else:
219 print('DONE')
220
221 def Status(self):
222 """Prints the status of the daemon."""
223 running = self._service.IsRunning()
Joel Kitchingbed7d452017-04-01 00:21:08 -0700224 if running:
225 up = self._core.IsUp()
226 print('UP' if up else 'STARTING')
227 else:
228 print('DOWN')
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800229
Joel Kitching23293972016-11-18 17:24:47 +0800230 def Inspect(self, plugin_id, json_path):
231 """Inspects the store of a given plugin."""
232 success, value = self._core.Inspect(plugin_id, json_path)
233 print(value)
234 if not success:
235 sys.exit(1)
236
Joel Kitching860649d2016-11-21 14:53:02 +0800237 def Flush(self, plugin_id, timeout):
238 """Flushes the given plugin with given timeout."""
239 success, value = self._core.Flush(plugin_id, timeout)
chuntsenf2e3ebd2019-10-28 16:49:57 +0800240 print(json.dumps(value))
Joel Kitching860649d2016-11-21 14:53:02 +0800241 if not success:
242 sys.exit(1)
243
chuntsenaabb2422018-03-07 16:16:09 +0800244 def Archive(self, config_path, data_dir, archive_path, details):
chuntsen65e9f382018-07-09 18:24:32 +0800245 """Archives the whole Instalog."""
chuntsen4cf110a2018-02-13 18:35:20 +0800246 if self._service.IsRunning():
247 print('Is the Instalog running? You need to stop the Instalog first')
248 sys.exit(1)
249 if os.path.isdir(archive_path):
250 archive_path = os.path.join(archive_path, 'archived_instalog.tar.gz')
251 if not os.path.isdir(os.path.dirname(archive_path)):
252 print('The directory of `%s` does not exist' %
253 os.path.realpath(archive_path))
254 sys.exit(1)
255
256 print('Archiving to %s ...' % os.path.realpath(archive_path))
257 with tarfile.open(archive_path, 'w') as tar:
258 data_dir = os.path.realpath(data_dir)
259 instalog_dir = instalog_common.INSTALOG_DIR
260 instalog_parent_dir = instalog_common.INSTALOG_PARENT_DIR
261 instalog_virtual_env_dir = instalog_common.INSTALOG_VIRTUAL_ENV_DIR
262
263 if os.path.exists(data_dir):
264 print('Archiving data_dir from %s' % os.path.realpath(data_dir))
265 tar.add(data_dir, 'data')
chuntsenaabb2422018-03-07 16:16:09 +0800266 print('Archiving config file from %s' % os.path.realpath(config_path))
267 tar.add(config_path, 'instalog.yaml')
chuntsen4cf110a2018-02-13 18:35:20 +0800268 if details >= 1:
269 def VirtualEnvFilter(tarinfo):
270 if tarinfo.name == 'instalog/virtual_env':
271 return None
272 return tarinfo
273 print('Archiving Instalog source code')
274 tar.add(instalog_dir, 'instalog', filter=VirtualEnvFilter)
275 tar.add(os.path.join(instalog_parent_dir, 'utils'), 'utils')
276 tar.add(os.path.join(instalog_parent_dir, 'testlog'), 'testlog')
277 tar.add(os.path.join(instalog_parent_dir, 'external'), 'external')
278 if details >= 2:
279 if os.path.exists(instalog_virtual_env_dir):
280 print('Archiving virtual_env')
281 tar.add(instalog_virtual_env_dir, 'instalog/virtual_env')
282 print('DONE')
283
chuntsen65e9f382018-07-09 18:24:32 +0800284 def Progress(self, plugin_id, details):
285 """Shows the progress of output plugins"""
286 progress_dict = self._core.GetAllProgress(details)
287 for name in sorted(progress_dict):
288 if plugin_id is None or name.startswith(plugin_id):
289 completed, total = progress_dict[name]
290 print('%s completed %d of %d events, and remaining %d events' %
291 (name, completed, total, total - completed))
292
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800293
Peter Shihb4e49352017-05-25 17:35:11 +0800294def main():
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800295 parser = argparse.ArgumentParser()
296 parser.add_argument(
297 '--config', '-c',
298 help='config file path; by default, searches: \n'
299 '$PWD/instalog.yaml py/instalog/instalog.yaml '
Hung-Te Linb0a32bb2017-01-05 15:11:06 +0800300 '~/.instalog.yaml /etc/instalog.yaml /run/instalog.yaml')
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800301 parser.add_argument(
302 '--verbose', '-v', action='count', default=0,
303 help='increase verbosity')
304 parser.add_argument(
305 '--quiet', '-q', action='count', default=0,
306 help='decrease verbosity')
307
308 subparsers = parser.add_subparsers(title='commands')
309
310 start_parser = subparsers.add_parser('start', help='start Instalog')
311 start_parser.set_defaults(cmd='start')
312 start_parser.add_argument(
313 '--no-daemon', '-n', dest='foreground', action='store_true',
314 help='keep in foreground')
315
316 stop_parser = subparsers.add_parser('stop', help='stop Instalog')
317 stop_parser.set_defaults(cmd='stop')
chuntsendc33ae82017-01-17 15:22:16 +0800318 stop_parser.add_argument(
319 '--timeout', '-w', type=float,
320 required=False, default=_DEFAULT_STOP_TIMEOUT,
321 help='time to wait before giving up')
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800322
323 restart_parser = subparsers.add_parser('restart', help='restart Instalog')
324 restart_parser.set_defaults(cmd='restart')
325
326 status_parser = subparsers.add_parser('status', help='print Instalog status')
327 status_parser.set_defaults(cmd='status')
328
Joel Kitching23293972016-11-18 17:24:47 +0800329 inspect_parser = subparsers.add_parser('inspect', help='inspect plugin store')
330 inspect_parser.set_defaults(cmd='inspect')
331 inspect_parser.add_argument(
332 'plugin_id', type=str, help='ID of plugin to inspect')
333 inspect_parser.add_argument(
334 'json_path', type=str, nargs='?', default='.',
335 help='path of store JSON to print')
336
Joel Kitching860649d2016-11-21 14:53:02 +0800337 flush_parser = subparsers.add_parser('flush', help='flush plugin')
338 flush_parser.set_defaults(cmd='flush')
339 flush_parser.add_argument(
340 '--timeout', '-w', type=float,
chuntsendc33ae82017-01-17 15:22:16 +0800341 required=False, default=_DEFAULT_FLUSH_TIMEOUT,
Joel Kitching860649d2016-11-21 14:53:02 +0800342 help='time to wait before giving up')
343 flush_parser.add_argument(
344 'plugin_id', type=str, nargs='?', default=None,
345 help='ID of plugin to flush')
346
chuntsen4cf110a2018-02-13 18:35:20 +0800347 archive_parser = subparsers.add_parser('archive', help='archive the Instalog')
348 archive_parser.set_defaults(cmd='archive')
349 archive_parser.add_argument(
350 '--output', '-o', dest='archive_path', type=str,
351 required=False, default='.',
352 help='path to put the archive file')
353 archive_parser.add_argument(
354 '--details', '-d', action='count', default=0,
355 help='archive more details (instalog code / virtual_env)')
356
chuntsen65e9f382018-07-09 18:24:32 +0800357 progress_parser = subparsers.add_parser(
358 'progress', help='print the progress of plugin')
359 progress_parser.set_defaults(cmd='progress')
360 progress_parser.add_argument(
361 'plugin_id', type=str, nargs='?', default=None,
362 help='ID of plugin\'s progress to print')
363 progress_parser.add_argument(
364 '--details', '-d', action='count', default=0,
365 help='print more details')
366
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800367 args = parser.parse_args()
368
369 InstalogCLI(args)
Peter Shihb4e49352017-05-25 17:35:11 +0800370
371if __name__ == '__main__':
372 main()