blob: 60a139d77d4182b89559df74e54f0be3890225a8 [file] [log] [blame]
Joel Kitching3e72e8b2016-07-04 17:03:00 +08001#!/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
7from __future__ import print_function
8
9import argparse
10import logging
11import os
12import signal
13import sys
chuntsen4cf110a2018-02-13 18:35:20 +080014import tarfile
Joel Kitching3e72e8b2016-07-04 17:03:00 +080015
Peter Shihfdf17682017-05-26 11:38:39 +080016import instalog_common # pylint: disable=unused-import
Joel Kitching3e72e8b2016-07-04 17:03:00 +080017from instalog import core
18from instalog import daemon_utils
Joel Kitchingd01336a2016-08-03 08:14:59 +080019from instalog import log_utils
Peter Shihb4e49352017-05-25 17:35:11 +080020from instalog.utils import file_utils
Joel Kitching3e72e8b2016-07-04 17:03:00 +080021from instalog.utils import sync_utils
22from instalog.utils import type_utils
23
Joel Kitching3a5e76c2016-12-21 13:22:22 +080024from instalog.external import jsonrpclib
25from instalog.external import yaml
26
Joel Kitching3e72e8b2016-07-04 17:03:00 +080027
Joel Kitching860649d2016-11-21 14:53:02 +080028# The default number of seconds to wait before giving up on a flush.
chuntsendc33ae82017-01-17 15:22:16 +080029_DEFAULT_FLUSH_TIMEOUT = 30
30_DEFAULT_STOP_TIMEOUT = 10
Joel Kitching860649d2016-11-21 14:53:02 +080031
32
Joel Kitching3e72e8b2016-07-04 17:03:00 +080033class InstalogService(daemon_utils.Daemon):
34 """Represents the Instalog daemon service."""
35
36 def __init__(self, config, logging_level):
37 self._config = config
38 self._logging_level = logging_level
39 self._core = None
Joel Kitching3e72e8b2016-07-04 17:03:00 +080040 super(InstalogService, self).__init__(
41 pidfile=config['instalog']['pid_file'])
42
43 def _SignalHandler(self, signal_num, frame):
Joel Kitching8e514ce2017-03-29 14:04:33 +080044 """Signal handler to stop Instalog on SIGINT or SIGTERM."""
Joel Kitching3e72e8b2016-07-04 17:03:00 +080045 del frame
Joel Kitching8e514ce2017-03-29 14:04:33 +080046 logging.debug('_SignalHandler called with signalnum=%s', signal_num)
47 if signal_num not in [signal.SIGINT, signal.SIGTERM]:
Joel Kitching3e72e8b2016-07-04 17:03:00 +080048 return
Joel Kitching3e72e8b2016-07-04 17:03:00 +080049 if self._core:
Joel Kitching8e514ce2017-03-29 14:04:33 +080050 # No need for a lock since _SignalHandler will only ever be called from
51 # Instalog's main thread.
52 signal_string = 'SIGINT' if signal_num == signal.SIGINT else 'SIGTERM'
53 logging.warning('%s detected, stopping', signal_string)
Joel Kitching3e72e8b2016-07-04 17:03:00 +080054 self._core.Stop()
55 self._core = None
Joel Kitching3e72e8b2016-07-04 17:03:00 +080056
chuntsenc8e2a572018-01-11 19:30:40 +080057 def _InitLogging(self, foreground):
Joel Kitching3e72e8b2016-07-04 17:03:00 +080058 """Sets up logging."""
chuntsenc8e2a572018-01-11 19:30:40 +080059 handlers = []
Joel Kitching3e72e8b2016-07-04 17:03:00 +080060
61 # Save logging calls to log file.
Peter Shihb4e49352017-05-25 17:35:11 +080062 log_file = self._config['instalog']['log_file']
63 file_utils.TryMakeDirs(os.path.dirname(log_file))
chuntsenc8e2a572018-01-11 19:30:40 +080064 handlers.append(log_utils.GetFileHandler(log_file, self._logging_level))
Joel Kitching3e72e8b2016-07-04 17:03:00 +080065
chuntsenc8e2a572018-01-11 19:30:40 +080066 # Output logging calls to console when foreground is set.
67 if foreground:
68 handlers.append(log_utils.GetStreamHandler(self._logging_level))
69
70 log_utils.InitLogging(handlers)
Joel Kitching3e72e8b2016-07-04 17:03:00 +080071
Chun-Tsen Kuoa874f782018-03-26 14:26:13 +080072 def Run(self, foreground, rpc_ready=None):
Joel Kitching3e72e8b2016-07-04 17:03:00 +080073 """Starts Instalog."""
chuntsenc8e2a572018-01-11 19:30:40 +080074 self._InitLogging(foreground)
Joel Kitching3e72e8b2016-07-04 17:03:00 +080075
Joel Kitching8e514ce2017-03-29 14:04:33 +080076 signal.signal(signal.SIGINT, self._SignalHandler)
77 signal.signal(signal.SIGTERM, self._SignalHandler)
Joel Kitching3e72e8b2016-07-04 17:03:00 +080078
79 self._core = core.Instalog(
80 node_id=self._config['instalog']['node_id'],
Joel Kitching9edac3b2016-10-23 10:40:24 +070081 data_dir=self._config['instalog']['data_dir'],
Joel Kitching3e72e8b2016-07-04 17:03:00 +080082 cli_hostname=self._config['instalog']['cli_hostname'],
83 cli_port=self._config['instalog']['cli_port'],
84 buffer_plugin=self._config['buffer'],
85 input_plugins=self._config['input'],
86 output_plugins=self._config['output'])
Chun-Tsen Kuoa874f782018-03-26 14:26:13 +080087 # After the core initialized, the RPC server is ready.
88 if rpc_ready:
89 rpc_ready.set()
Joel Kitching3e72e8b2016-07-04 17:03:00 +080090 self._core.Run()
91
92
93class InstalogCLI(object):
94 """Represents the CLI interface used to control Instalog."""
95
96 def __init__(self, args):
97 # Read config file.
98 config_path = self._LocateConfigFile(args.config)
99 if config_path is None:
100 exit('No config file found')
101 with open(config_path) as f:
102 config = yaml.load(f)
chuntsen4cf110a2018-02-13 18:35:20 +0800103 self._CheckDataDir(config)
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800104
105 # logging.WARNING = 30, logging.INFO = 20, logging.DEBUG = 10
106 logging_level = logging.INFO - ((args.verbose - args.quiet) * 10)
107
108 self._service = InstalogService(config, logging_level)
109 self._core = jsonrpclib.Server(
110 'http://%s:%s' % (config['instalog']['cli_hostname'],
111 config['instalog']['cli_port']))
112
113 if args.cmd == 'start':
114 self.Start(args.foreground)
115 elif args.cmd == 'stop':
chuntsendc33ae82017-01-17 15:22:16 +0800116 self.Stop(args.timeout)
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800117 elif args.cmd == 'restart':
118 self.Restart()
119 elif args.cmd == 'status':
120 self.Status()
Joel Kitching23293972016-11-18 17:24:47 +0800121 elif args.cmd == 'inspect':
122 self.Inspect(args.plugin_id, args.json_path)
Joel Kitching860649d2016-11-21 14:53:02 +0800123 elif args.cmd == 'flush':
124 self.Flush(args.plugin_id, args.timeout)
chuntsen4cf110a2018-02-13 18:35:20 +0800125 elif args.cmd == 'archive':
chuntsenaabb2422018-03-07 16:16:09 +0800126 self.Archive(config_path, config['instalog']['data_dir'],
chuntsen4cf110a2018-02-13 18:35:20 +0800127 args.archive_path, args.details)
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800128
129 def _LocateConfigFile(self, user_path):
130 """Locates the config file that should be used by Instalog."""
131 if user_path:
132 return user_path
133 paths = [
134 os.path.join(os.getcwd(), 'instalog.yaml'),
135 os.path.join(os.path.dirname(os.path.realpath(__file__)),
136 'instalog.yaml'),
137 os.path.join(os.path.expanduser('~'), '.instalog.yaml'),
Joel Kitching24e01242016-11-25 15:17:29 +0800138 os.path.join(os.sep, 'etc', 'instalog.yaml'),
Hung-Te Linb0a32bb2017-01-05 15:11:06 +0800139 os.path.join(os.sep, 'run', 'instalog.yaml')]
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800140 for path in paths:
141 logging.debug('Checking %s for config file...', path)
142 if os.path.exists(path):
143 logging.info('Config file found at %s', path)
144 return path
145
chuntsen4cf110a2018-02-13 18:35:20 +0800146 def _CheckDataDir(self, config):
147 data_dir = config['instalog']['data_dir']
148 if not os.path.exists(data_dir):
149 os.makedirs(data_dir)
150 instalog_dir = instalog_common.INSTALOG_DIR
151 for path, unused_dirs, unused_files in os.walk(
152 instalog_dir, followlinks=True):
153 if not os.path.islink(path) and os.path.samefile(path, data_dir):
154 print('You should not put the data_dir in the Instalog source code')
155 sys.exit(1)
156
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800157 def Restart(self):
158 """Restarts the daemon."""
chuntsendc33ae82017-01-17 15:22:16 +0800159 self.Stop(_DEFAULT_STOP_TIMEOUT)
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800160 self.Start(False)
161
162 def Start(self, foreground):
163 """Starts the daemon.
164
165 Args:
166 foreground: Does not detach the daemon.
167 """
168 print('Starting...')
Chun-Tsen Kuoa874f782018-03-26 14:26:13 +0800169 if not self._service.Start(foreground):
170 return
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800171 if foreground:
172 return
Joel Kitching11bbbe72016-12-07 11:43:38 +0800173
174 # First, wait for the daemon process to start.
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800175 try:
176 sync_utils.WaitFor(self._service.IsRunning, 10)
177 except type_utils.TimeoutError:
Joel Kitching11bbbe72016-12-07 11:43:38 +0800178 print('Daemon could not be brought up, check the logs')
179 sys.exit(1)
180
181 def TryIsUp():
182 try:
183 # Perform the real check to see if Instalog is up internally.
184 return self._core.IsUp()
185 except Exception:
186 raise type_utils.TimeoutError('Could not call core IsUp')
187
188 try:
Chun-Tsen Kuoa874f782018-03-26 14:26:13 +0800189 print('Waiting for the core is up...')
Joel Kitching11bbbe72016-12-07 11:43:38 +0800190 if sync_utils.WaitFor(TryIsUp, 10):
191 print('DONE')
192 return
193 except type_utils.TimeoutError:
194 pass
195 print('Daemon could not be brought up, check the logs')
196 sys.exit(1)
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800197
chuntsendc33ae82017-01-17 15:22:16 +0800198 def Stop(self, timeout):
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800199 """Stops the daemon."""
200 # First, send the "stop" instruction to the daemon.
201 print('Stopping...')
202 try:
203 self._core.Stop()
204 except Exception:
205 print('Could not connect to daemon, is it running?')
206 sys.exit(1)
207
208 # Then, wait for the process to come down.
209 try:
chuntsendc33ae82017-01-17 15:22:16 +0800210 sync_utils.WaitFor(self._service.IsStopped, timeout)
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800211 except type_utils.TimeoutError:
212 print('Still shutting down?')
213 sys.exit(1)
214 else:
215 print('DONE')
216
217 def Status(self):
218 """Prints the status of the daemon."""
219 running = self._service.IsRunning()
Joel Kitchingbed7d452017-04-01 00:21:08 -0700220 if running:
221 up = self._core.IsUp()
222 print('UP' if up else 'STARTING')
223 else:
224 print('DOWN')
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800225
Joel Kitching23293972016-11-18 17:24:47 +0800226 def Inspect(self, plugin_id, json_path):
227 """Inspects the store of a given plugin."""
228 success, value = self._core.Inspect(plugin_id, json_path)
229 print(value)
230 if not success:
231 sys.exit(1)
232
Joel Kitching860649d2016-11-21 14:53:02 +0800233 def Flush(self, plugin_id, timeout):
234 """Flushes the given plugin with given timeout."""
235 success, value = self._core.Flush(plugin_id, timeout)
236 print(value)
237 if not success:
238 sys.exit(1)
239
chuntsenaabb2422018-03-07 16:16:09 +0800240 def Archive(self, config_path, data_dir, archive_path, details):
chuntsen4cf110a2018-02-13 18:35:20 +0800241 if self._service.IsRunning():
242 print('Is the Instalog running? You need to stop the Instalog first')
243 sys.exit(1)
244 if os.path.isdir(archive_path):
245 archive_path = os.path.join(archive_path, 'archived_instalog.tar.gz')
246 if not os.path.isdir(os.path.dirname(archive_path)):
247 print('The directory of `%s` does not exist' %
248 os.path.realpath(archive_path))
249 sys.exit(1)
250
251 print('Archiving to %s ...' % os.path.realpath(archive_path))
252 with tarfile.open(archive_path, 'w') as tar:
253 data_dir = os.path.realpath(data_dir)
254 instalog_dir = instalog_common.INSTALOG_DIR
255 instalog_parent_dir = instalog_common.INSTALOG_PARENT_DIR
256 instalog_virtual_env_dir = instalog_common.INSTALOG_VIRTUAL_ENV_DIR
257
258 if os.path.exists(data_dir):
259 print('Archiving data_dir from %s' % os.path.realpath(data_dir))
260 tar.add(data_dir, 'data')
chuntsenaabb2422018-03-07 16:16:09 +0800261 print('Archiving config file from %s' % os.path.realpath(config_path))
262 tar.add(config_path, 'instalog.yaml')
chuntsen4cf110a2018-02-13 18:35:20 +0800263 if details >= 1:
264 def VirtualEnvFilter(tarinfo):
265 if tarinfo.name == 'instalog/virtual_env':
266 return None
267 return tarinfo
268 print('Archiving Instalog source code')
269 tar.add(instalog_dir, 'instalog', filter=VirtualEnvFilter)
270 tar.add(os.path.join(instalog_parent_dir, 'utils'), 'utils')
271 tar.add(os.path.join(instalog_parent_dir, 'testlog'), 'testlog')
272 tar.add(os.path.join(instalog_parent_dir, 'external'), 'external')
273 if details >= 2:
274 if os.path.exists(instalog_virtual_env_dir):
275 print('Archiving virtual_env')
276 tar.add(instalog_virtual_env_dir, 'instalog/virtual_env')
277 print('DONE')
278
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800279
Peter Shihb4e49352017-05-25 17:35:11 +0800280def main():
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800281 parser = argparse.ArgumentParser()
282 parser.add_argument(
283 '--config', '-c',
284 help='config file path; by default, searches: \n'
285 '$PWD/instalog.yaml py/instalog/instalog.yaml '
Hung-Te Linb0a32bb2017-01-05 15:11:06 +0800286 '~/.instalog.yaml /etc/instalog.yaml /run/instalog.yaml')
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800287 parser.add_argument(
288 '--verbose', '-v', action='count', default=0,
289 help='increase verbosity')
290 parser.add_argument(
291 '--quiet', '-q', action='count', default=0,
292 help='decrease verbosity')
293
294 subparsers = parser.add_subparsers(title='commands')
295
296 start_parser = subparsers.add_parser('start', help='start Instalog')
297 start_parser.set_defaults(cmd='start')
298 start_parser.add_argument(
299 '--no-daemon', '-n', dest='foreground', action='store_true',
300 help='keep in foreground')
301
302 stop_parser = subparsers.add_parser('stop', help='stop Instalog')
303 stop_parser.set_defaults(cmd='stop')
chuntsendc33ae82017-01-17 15:22:16 +0800304 stop_parser.add_argument(
305 '--timeout', '-w', type=float,
306 required=False, default=_DEFAULT_STOP_TIMEOUT,
307 help='time to wait before giving up')
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800308
309 restart_parser = subparsers.add_parser('restart', help='restart Instalog')
310 restart_parser.set_defaults(cmd='restart')
311
312 status_parser = subparsers.add_parser('status', help='print Instalog status')
313 status_parser.set_defaults(cmd='status')
314
Joel Kitching23293972016-11-18 17:24:47 +0800315 inspect_parser = subparsers.add_parser('inspect', help='inspect plugin store')
316 inspect_parser.set_defaults(cmd='inspect')
317 inspect_parser.add_argument(
318 'plugin_id', type=str, help='ID of plugin to inspect')
319 inspect_parser.add_argument(
320 'json_path', type=str, nargs='?', default='.',
321 help='path of store JSON to print')
322
Joel Kitching860649d2016-11-21 14:53:02 +0800323 flush_parser = subparsers.add_parser('flush', help='flush plugin')
324 flush_parser.set_defaults(cmd='flush')
325 flush_parser.add_argument(
326 '--timeout', '-w', type=float,
chuntsendc33ae82017-01-17 15:22:16 +0800327 required=False, default=_DEFAULT_FLUSH_TIMEOUT,
Joel Kitching860649d2016-11-21 14:53:02 +0800328 help='time to wait before giving up')
329 flush_parser.add_argument(
330 'plugin_id', type=str, nargs='?', default=None,
331 help='ID of plugin to flush')
332
chuntsen4cf110a2018-02-13 18:35:20 +0800333 archive_parser = subparsers.add_parser('archive', help='archive the Instalog')
334 archive_parser.set_defaults(cmd='archive')
335 archive_parser.add_argument(
336 '--output', '-o', dest='archive_path', type=str,
337 required=False, default='.',
338 help='path to put the archive file')
339 archive_parser.add_argument(
340 '--details', '-d', action='count', default=0,
341 help='archive more details (instalog code / virtual_env)')
342
Joel Kitching3e72e8b2016-07-04 17:03:00 +0800343 args = parser.parse_args()
344
345 InstalogCLI(args)
Peter Shihb4e49352017-05-25 17:35:11 +0800346
347if __name__ == '__main__':
348 main()