cli.py 13.8 KB
Newer Older
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Copyright (C) 2015 GRNET S.A.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

16
import cmd
17
import os
18
19
import sys
import logging
20
import argparse
21

22
from agkyra import protocol, protocol_client, gui, config
23
24
25
26
27
28
29
30
31
32
33
34


AGKYRA_DIR = config.AGKYRA_DIR
LOGGERFILE = os.path.join(AGKYRA_DIR, 'agkyra.log')
AGKYRA_LOGGER = logging.getLogger('agkyra')
HANDLER = logging.FileHandler(LOGGERFILE)
FORMATTER = logging.Formatter(
    "%(name)s:%(lineno)s %(levelname)s:%(asctime)s:%(message)s")
HANDLER.setFormatter(FORMATTER)
AGKYRA_LOGGER.addHandler(HANDLER)

LOGGER = logging.getLogger(__name__)
35
STATUS = protocol.STATUS
36
NOTIFICATION = protocol.COMMON['NOTIFICATION']
37

38
39
remaining = lambda st: st['unsynced'] - (st['synced'] + st['failed'])

40

41
class ConfigCommands(object):
42
    """Commands for handling Agkyra config options"""
43
44
45
46
47
48
    cnf = config.AgkyraConfig()

    def print_option(self, section, name, option):
        """Print a configuration option"""
        section = '%s.%s' % (section, name) if name else section
        value = self.cnf.get(section, option)
49
        sys.stdout.write('  %s: %s\n' % (option, value))
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

    def list_section(self, section, name):
        """list contents of a section"""
        content = dict(self.cnf.items(section))
        if section in 'global' and name:
            self.print_option(section, '', name)
        else:
            if name:
                content = content[name]
            for option in content.keys():
                self.print_option(section, name, option)

    def list_section_type(self, section):
        """print the contents of a configuration section"""
        names = ['', ] if section in ('global', ) else self.cnf.keys(section)
        assert names, 'Section %s not found' % section
        for name in names:
            print section, name
            self.list_section(section, name)

    def list_sections(self):
        """List all configuration sections"""
        for section in self.cnf.sections():
            self.list_section_type(section)

75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
    def set_global_setting(self, section, option, value):
        assert section in ('global'), 'Syntax error'
        self.cnf.set(section, option, value)
        self.cnf.write()

    def set_setting(self, section, name, option, value):
        assert section in self.cnf.sections(), 'Syntax error'
        self.cnf.set('%s.%s' % (section, name), option, value)
        self.cnf.write()

    def delete_global_option(self, section, option, yes=False):
        """Delete global option"""
        if (not yes and 'y' != raw_input(
                'Delete %s option %s? [y|N]: ' % (section, option))):
            sys.stderr.write('Aborted\n')
        else:
            self.cnf.remove_option(section, option)
            self.cnf.write()

    def delete_section_option(self, section, name, option, yes=False):
        """Delete a section (sync or cloud) option"""
        assert section in self.cnf.sections(), 'Syntax error'
        if (not yes and 'y' != raw_input(
                'Delete %s of %s "%s"? [y|N]: ' % (option, section, name))):
            sys.stderr.write('Aborted\n')
        else:
            if section == config.CLOUD_PREFIX:
                self.cnf.remove_from_cloud(name, option)
            elif section == config.SYNC_PREFIX:
                self.cnf.remove_from_sync(name, option)
            else:
                self.cnf.remove_option('%s.%s' % (section, name), option)
            self.cnf.write()

    def delete_section(self, section, name, yes=False):
        """Delete a section (sync or cloud)"""
        if (not yes and 'y' != raw_input(
                'Delete %s "%s"? [y|N]: ' % (section, name))):
            sys.stderr.write('Aborted\n')
        else:
            self.cnf.remove_option(section, name)
            self.cnf.write()


119
class AgkyraCLI(cmd.Cmd):
120
121
    """The CLI for Agkyra is connected to a protocol server"""
    cnf_cmds = ConfigCommands()
122
123
    helper = protocol.SessionHelper()

124
    def __init__(self, *args, **kwargs):
125
126
        self.callback = kwargs.pop('callback', sys.argv[0])
        self.args = kwargs.pop('parsed_args', None)
127
128
        AGKYRA_LOGGER.setLevel(logging.DEBUG
                               if self.args.debug else logging.INFO)
129
130
        cmd.Cmd.__init__(self, *args, **kwargs)

131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
    @staticmethod
    def parse_args():
        parser = argparse.ArgumentParser(
            description='Agkyra syncer launcher', add_help=False)
        parser.add_argument(
            '--help', '-h',
            action='store_true', help='Help on agkyra syntax and usage')
        parser.add_argument(
            '--debug', '-d',
            action='store_true', help='set logging level to "debug"')
        parser.add_argument('cmd', nargs="*")

        for terms in (['help', ], ['config', 'delete']):
            if not set(terms).difference(sys.argv):
                {
                    'help': lambda: parser.add_argument(
                        '--list', '-l',
                        action='store_true', help='List all commands'),
                    'config_delete': lambda: parser.add_argument(
                        '--yes', '-y',
                        action='store_true', help='Yes to all questions')
                }['_'.join(terms)]()

        return parser.parse_args()

    def must_help(self, command):
        if self.args.help:
            self.do_help(command)
            return True
        return False

    def launch_daemon(self):
        """Launch the agkyra protocol server"""
164
        LOGGER.info('Starting the agkyra daemon')
165
166
167
        if not self.helper.load_active_session():
            self.helper.create_session()
            self.helper.start()
168
            LOGGER.info('Daemon is shut down')
169
        else:
170
            LOGGER.info('Another daemon is running, aborting')
171

172
173
174
175
176
    @property
    def client(self):
        """Return the helper client instace or None"""
        self._client = getattr(self, '_client', None)
        if not self._client:
177
178
            session = protocol.retry_on_locked_db(
                self.helper.load_active_session)
179
180
181
182
            if session:
                self._client = protocol_client.UIClient(session)
                self._client.connect()
        return self._client
183

184
    def do_help(self, line):
185
186
187
188
189
        """Help on agkyra GUI and CLI
        agkyra         Run agkyra with GUI (equivalent to "agkyra gui")
        agkyra <cmd>   Run a command through agkyra CLI

        To get help on agkyra commands:
190
191
192
            help <cmd>            for an individual command
            help <--list | -l>    for all commands
        """
193
194
        if getattr(self.args, 'list', None):
            self.args.list = None
195
196
197
198
199
200
201
202
203
204
205
            prefix = 'do_'
            for c in self.get_names():
                if c.startswith(prefix):
                    actual_name = c[len(prefix):]
                    print '-', actual_name, '-'
                    self.do_help(actual_name)
                    print
        else:
            if not line:
                cmd.Cmd.do_help(self, 'help')
            cmd.Cmd.do_help(self, line)
206

207
208
209
210
211
212
213
214
    def emptyline(self):
        if self.must_help(''):
            return
        return self.do_gui('')

    def default(self, line):
        self.do_help(line)

215
216
217
218
219
220
221
    def config_list(self, args):
        """List (all or some) options
        list                                List all options
        list <global | cloud | sync>        List global | cloud | sync options
        list global OPTION                  Get global option
        list <cloud | sync> NAME            List options a cloud or sync
        list <cloud | sync> NAME OPTION     List an option from a cloud or sync
222
223
224
        """
        try:
            {
225
226
227
228
                0: self.cnf_cmds.list_sections,
                1: self.cnf_cmds.list_section_type,
                2: self.cnf_cmds.list_section,
                3: self.cnf_cmds.print_option
229
230
            }[len(args)](*args)
        except Exception as e:
231
            LOGGER.debug('%s\n' % e)
232
            sys.stderr.write(self.config_list.__doc__ + '\n')
233

234
235
236
237
238
239
240
    def config_set(self, args):
        """Set an option
        set global OPTION VALUE                 Set a global option
        set <cloud | sync> NAME OPTION VALUE    Set an option on cloud or sync
                                                Creates a sync or cloud, if it
                                                does not exist
        """
241
242
        try:
            {
243
244
                3: self.cnf_cmds.set_global_setting,
                4: self.cnf_cmds.set_setting
245
246
            }[len(args)](*args)
        except Exception as e:
247
            LOGGER.debug('%s\n' % e)
248
249
250
251
            sys.stderr.write(self.config_set.__doc__ + '\n')

    def config_delete(self, args):
        """Delete an option
252
253
254
        delete global OPTION [-y]               Delete a global option
        delete <cloud | sync> NAME [-y]         Delete a sync or cloud
        delete <cloud |sync> NAME OPTION [-y]   Delete a sync or cloud option
255
        """
256
        args.append(self.args.yes)
257
258
        try:
            {
259
260
261
                3: self.cnf_cmds.delete_global_option if (
                    args[0] == 'global') else self.cnf_cmds.delete_section,
                4: self.cnf_cmds.delete_section_option
262
263
            }[len(args)](*args)
        except Exception as e:
264
            LOGGER.debug('%s\n' % e)
265
            sys.stderr.write(self.config_delete.__doc__ + '\n')
266

267
268
269
270
271
272
    def do_config(self, line):
        """Commands for managing the agkyra settings
        list   [global|cloud|sync [setting]]          List all or some settings
        set    <global|cloud|sync> <setting> <value>  Set a setting
        delete <global|cloud|sync> [setting]          Delete a setting or group
        """
273
274
        if self.must_help('config'):
            return
275
276
277
278
279
280
281
        args = line.split(' ')
        try:
            method = getattr(self, 'config_' + args[0])
            method(args[1:])
        except AttributeError:
            self.do_help('config')

282
283
    def do_status(self, line):
        """Get Agkyra client status. Status may be one of the following:
284
285
286
        Syncing     There is a process syncing right now
        Paused      Notifiers are active but syncing is paused
        Not running No active processes
287
        """
288
289
        if self.must_help('status'):
            return
290
        client = self.client
291
292
        status, msg = client.get_status() if client else None, 'Not running'
        if status:
293
            msg = NOTIFICATION[str(status['code'])]
294
            diff = remaining(status)
295
296
297
            if diff:
                msg = '%s, %s remaining' % (msg, diff)
        sys.stdout.write('%s\n' % msg)
298
        sys.stdout.flush()
299

300
    def do_start(self, line):
301
302
303
304
305
306
307
308
        """Start the session, set it in syncing mode
        start         Start syncing. If daemon is down, start it up
        start daemon  Start the agkyra daemon and wait
        """
        if self.must_help('start'):
            return
        if line in ['daemon']:
            return self.launch_daemon()
309
310
311
312
        if line:
            sys.stderr.write("Unrecognized subcommand '%s'.\n" % line)
            sys.stderr.flush()
            return
313
314
        client = self.client
        if not client:
315
            sys.stderr.write('No Agkyra daemons running, starting one')
316
            protocol.launch_server(self.callback, self.args.debug)
317
            sys.stderr.write(' ... ')
318
            self.helper.wait_session_to_load()
319
320
321
            sys.stderr.write('OK\n')
        else:
            status = client.get_status()
322
            if status['code'] == STATUS['PAUSED']:
323
324
325
326
                client.start()
                sys.stderr.write('Starting syncer ... ')
                try:
                    client.wait_until_syncing()
327
328
329
                    sys.stderr.write('OK\n')
                except AssertionError as ae:
                    sys.stderr.write('%s\n' % ae)
330
            else:
331
                sys.stderr.write('Already ')
332
        sys.stderr.flush()
333
        self.do_status(line)
334
335
336

    def do_pause(self, line):
        """Pause a session (stop it from syncing, but keep it running)"""
337
338
        if self.must_help('pause'):
            return
339
340
341
        client = self.client
        if client:
            status = client.get_status()
342
            if status['code'] == STATUS['PAUSED']:
343
                sys.stderr.write('Already ')
344
345
346
347
348
            else:
                client.pause()
                sys.stderr.write('Pausing syncer ... ')
                try:
                    client.wait_until_paused()
349
350
351
                    sys.stderr.write('OK\n')
                except AssertionError as ae:
                    sys.stderr.write('%s\n' % ae)
352
        sys.stderr.flush()
353
        self.do_status(line)
354

355
356
    def do_shutdown(self, line):
        """Shutdown Agkyra, if it is running"""
357
358
        if self.must_help('shutdown'):
            return
359
360
361
        client = self.client
        if client:
            client.shutdown()
362
            sys.stderr.write('Shutting down Agkyra ... ')
363
364
365
366
            success = self.helper.wait_session_to_stop()
            sys.stderr.write('Stopped' if success else 'Still up (timed out)')
            sys.stderr.write('\n')
        else:
367
            sys.stderr.write('Not running\n')
368
        sys.stderr.flush()
369
370
371
372
373
374
375
376
377
378
379


    # Systemic commands
    def do_gui(self, line):
        """Launch the agkyra GUI
        Only one GUI instance can run at a time.
        If an agkyra daemon is already running, the GUI will use it.
        """
        if self.must_help('gui'):
            return
        gui.run(callback=self.callback, debug=self.args.debug)