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 |
chuntsen | 4cf110a | 2018-02-13 18:35:20 +0800 | [diff] [blame] | 14 | import tarfile |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 15 | |
Peter Shih | fdf1768 | 2017-05-26 11:38:39 +0800 | [diff] [blame] | 16 | import instalog_common # pylint: disable=unused-import |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 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 |
Peter Shih | b4e4935 | 2017-05-25 17:35:11 +0800 | [diff] [blame] | 20 | from instalog.utils import file_utils |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 21 | from instalog.utils import sync_utils |
| 22 | from instalog.utils import type_utils |
| 23 | |
Joel Kitching | 3a5e76c | 2016-12-21 13:22:22 +0800 | [diff] [blame] | 24 | from instalog.external import jsonrpclib |
| 25 | from instalog.external import yaml |
| 26 | |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 27 | |
Joel Kitching | 860649d | 2016-11-21 14:53:02 +0800 | [diff] [blame] | 28 | # The default number of seconds to wait before giving up on a flush. |
chuntsen | dc33ae8 | 2017-01-17 15:22:16 +0800 | [diff] [blame] | 29 | _DEFAULT_FLUSH_TIMEOUT = 30 |
| 30 | _DEFAULT_STOP_TIMEOUT = 10 |
Joel Kitching | 860649d | 2016-11-21 14:53:02 +0800 | [diff] [blame] | 31 | |
| 32 | |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 33 | class 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 Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 40 | super(InstalogService, self).__init__( |
| 41 | pidfile=config['instalog']['pid_file']) |
| 42 | |
| 43 | def _SignalHandler(self, signal_num, frame): |
Joel Kitching | 8e514ce | 2017-03-29 14:04:33 +0800 | [diff] [blame] | 44 | """Signal handler to stop Instalog on SIGINT or SIGTERM.""" |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 45 | del frame |
Joel Kitching | 8e514ce | 2017-03-29 14:04:33 +0800 | [diff] [blame] | 46 | logging.debug('_SignalHandler called with signalnum=%s', signal_num) |
| 47 | if signal_num not in [signal.SIGINT, signal.SIGTERM]: |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 48 | return |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 49 | if self._core: |
Joel Kitching | 8e514ce | 2017-03-29 14:04:33 +0800 | [diff] [blame] | 50 | # 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 Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 54 | self._core.Stop() |
| 55 | self._core = None |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 56 | |
chuntsen | c8e2a57 | 2018-01-11 19:30:40 +0800 | [diff] [blame] | 57 | def _InitLogging(self, foreground): |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 58 | """Sets up logging.""" |
chuntsen | c8e2a57 | 2018-01-11 19:30:40 +0800 | [diff] [blame] | 59 | handlers = [] |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 60 | |
| 61 | # Save logging calls to log file. |
Peter Shih | b4e4935 | 2017-05-25 17:35:11 +0800 | [diff] [blame] | 62 | log_file = self._config['instalog']['log_file'] |
| 63 | file_utils.TryMakeDirs(os.path.dirname(log_file)) |
chuntsen | c8e2a57 | 2018-01-11 19:30:40 +0800 | [diff] [blame] | 64 | handlers.append(log_utils.GetFileHandler(log_file, self._logging_level)) |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 65 | |
chuntsen | c8e2a57 | 2018-01-11 19:30:40 +0800 | [diff] [blame] | 66 | # 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 Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 71 | |
Chun-Tsen Kuo | a874f78 | 2018-03-26 14:26:13 +0800 | [diff] [blame^] | 72 | def Run(self, foreground, rpc_ready=None): |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 73 | """Starts Instalog.""" |
chuntsen | c8e2a57 | 2018-01-11 19:30:40 +0800 | [diff] [blame] | 74 | self._InitLogging(foreground) |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 75 | |
Joel Kitching | 8e514ce | 2017-03-29 14:04:33 +0800 | [diff] [blame] | 76 | signal.signal(signal.SIGINT, self._SignalHandler) |
| 77 | signal.signal(signal.SIGTERM, self._SignalHandler) |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 78 | |
| 79 | self._core = core.Instalog( |
| 80 | node_id=self._config['instalog']['node_id'], |
Joel Kitching | 9edac3b | 2016-10-23 10:40:24 +0700 | [diff] [blame] | 81 | data_dir=self._config['instalog']['data_dir'], |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 82 | 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 Kuo | a874f78 | 2018-03-26 14:26:13 +0800 | [diff] [blame^] | 87 | # After the core initialized, the RPC server is ready. |
| 88 | if rpc_ready: |
| 89 | rpc_ready.set() |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 90 | self._core.Run() |
| 91 | |
| 92 | |
| 93 | class 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) |
chuntsen | 4cf110a | 2018-02-13 18:35:20 +0800 | [diff] [blame] | 103 | self._CheckDataDir(config) |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 104 | |
| 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': |
chuntsen | dc33ae8 | 2017-01-17 15:22:16 +0800 | [diff] [blame] | 116 | self.Stop(args.timeout) |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 117 | elif args.cmd == 'restart': |
| 118 | self.Restart() |
| 119 | elif args.cmd == 'status': |
| 120 | self.Status() |
Joel Kitching | 2329397 | 2016-11-18 17:24:47 +0800 | [diff] [blame] | 121 | elif args.cmd == 'inspect': |
| 122 | self.Inspect(args.plugin_id, args.json_path) |
Joel Kitching | 860649d | 2016-11-21 14:53:02 +0800 | [diff] [blame] | 123 | elif args.cmd == 'flush': |
| 124 | self.Flush(args.plugin_id, args.timeout) |
chuntsen | 4cf110a | 2018-02-13 18:35:20 +0800 | [diff] [blame] | 125 | elif args.cmd == 'archive': |
chuntsen | aabb242 | 2018-03-07 16:16:09 +0800 | [diff] [blame] | 126 | self.Archive(config_path, config['instalog']['data_dir'], |
chuntsen | 4cf110a | 2018-02-13 18:35:20 +0800 | [diff] [blame] | 127 | args.archive_path, args.details) |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 128 | |
| 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 Kitching | 24e0124 | 2016-11-25 15:17:29 +0800 | [diff] [blame] | 138 | os.path.join(os.sep, 'etc', 'instalog.yaml'), |
Hung-Te Lin | b0a32bb | 2017-01-05 15:11:06 +0800 | [diff] [blame] | 139 | os.path.join(os.sep, 'run', 'instalog.yaml')] |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 140 | 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 | |
chuntsen | 4cf110a | 2018-02-13 18:35:20 +0800 | [diff] [blame] | 146 | 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 Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 157 | def Restart(self): |
| 158 | """Restarts the daemon.""" |
chuntsen | dc33ae8 | 2017-01-17 15:22:16 +0800 | [diff] [blame] | 159 | self.Stop(_DEFAULT_STOP_TIMEOUT) |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 160 | 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 Kuo | a874f78 | 2018-03-26 14:26:13 +0800 | [diff] [blame^] | 169 | if not self._service.Start(foreground): |
| 170 | return |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 171 | if foreground: |
| 172 | return |
Joel Kitching | 11bbbe7 | 2016-12-07 11:43:38 +0800 | [diff] [blame] | 173 | |
| 174 | # First, wait for the daemon process to start. |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 175 | try: |
| 176 | sync_utils.WaitFor(self._service.IsRunning, 10) |
| 177 | except type_utils.TimeoutError: |
Joel Kitching | 11bbbe7 | 2016-12-07 11:43:38 +0800 | [diff] [blame] | 178 | 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 Kuo | a874f78 | 2018-03-26 14:26:13 +0800 | [diff] [blame^] | 189 | print('Waiting for the core is up...') |
Joel Kitching | 11bbbe7 | 2016-12-07 11:43:38 +0800 | [diff] [blame] | 190 | 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 Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 197 | |
chuntsen | dc33ae8 | 2017-01-17 15:22:16 +0800 | [diff] [blame] | 198 | def Stop(self, timeout): |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 199 | """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: |
chuntsen | dc33ae8 | 2017-01-17 15:22:16 +0800 | [diff] [blame] | 210 | sync_utils.WaitFor(self._service.IsStopped, timeout) |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 211 | 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 Kitching | bed7d45 | 2017-04-01 00:21:08 -0700 | [diff] [blame] | 220 | if running: |
| 221 | up = self._core.IsUp() |
| 222 | print('UP' if up else 'STARTING') |
| 223 | else: |
| 224 | print('DOWN') |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 225 | |
Joel Kitching | 2329397 | 2016-11-18 17:24:47 +0800 | [diff] [blame] | 226 | 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 Kitching | 860649d | 2016-11-21 14:53:02 +0800 | [diff] [blame] | 233 | 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 | |
chuntsen | aabb242 | 2018-03-07 16:16:09 +0800 | [diff] [blame] | 240 | def Archive(self, config_path, data_dir, archive_path, details): |
chuntsen | 4cf110a | 2018-02-13 18:35:20 +0800 | [diff] [blame] | 241 | 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') |
chuntsen | aabb242 | 2018-03-07 16:16:09 +0800 | [diff] [blame] | 261 | print('Archiving config file from %s' % os.path.realpath(config_path)) |
| 262 | tar.add(config_path, 'instalog.yaml') |
chuntsen | 4cf110a | 2018-02-13 18:35:20 +0800 | [diff] [blame] | 263 | 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 Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 279 | |
Peter Shih | b4e4935 | 2017-05-25 17:35:11 +0800 | [diff] [blame] | 280 | def main(): |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 281 | 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 Lin | b0a32bb | 2017-01-05 15:11:06 +0800 | [diff] [blame] | 286 | '~/.instalog.yaml /etc/instalog.yaml /run/instalog.yaml') |
Joel Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 287 | 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') |
chuntsen | dc33ae8 | 2017-01-17 15:22:16 +0800 | [diff] [blame] | 304 | 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 Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 308 | |
| 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 Kitching | 2329397 | 2016-11-18 17:24:47 +0800 | [diff] [blame] | 315 | 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 Kitching | 860649d | 2016-11-21 14:53:02 +0800 | [diff] [blame] | 323 | 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, |
chuntsen | dc33ae8 | 2017-01-17 15:22:16 +0800 | [diff] [blame] | 327 | required=False, default=_DEFAULT_FLUSH_TIMEOUT, |
Joel Kitching | 860649d | 2016-11-21 14:53:02 +0800 | [diff] [blame] | 328 | 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 | |
chuntsen | 4cf110a | 2018-02-13 18:35:20 +0800 | [diff] [blame] | 333 | 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 Kitching | 3e72e8b | 2016-07-04 17:03:00 +0800 | [diff] [blame] | 343 | args = parser.parse_args() |
| 344 | |
| 345 | InstalogCLI(args) |
Peter Shih | b4e4935 | 2017-05-25 17:35:11 +0800 | [diff] [blame] | 346 | |
| 347 | if __name__ == '__main__': |
| 348 | main() |