-
Giorgos Korfiatis authored
Use new statuses 'SETTINGS READY' and 'READY'.
ab41bf0c
cli.py 19.25 KiB
# 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/>.
import cmd
import os
import sys
import logging
import argparse
import errno
import socket
from agkyra import protocol, protocol_client, gui, config
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)
logging.getLogger('astakosclient').addHandler(logging.NullHandler())
LOGGER = logging.getLogger(__name__)
STATUS = protocol.STATUS
NOTIFICATION = protocol.COMMON['NOTIFICATION']
remaining = lambda st: st['unsynced'] - (st['synced'] + st['failed'])
class ConfigError(protocol_client.UIClientError):
"""Error with config settings"""
class ConfigCommands(object):
"""Commands for handling Agkyra config options"""
cnf = config.AgkyraConfig()
def _validate_section(
self, section, err_msg='"%s" is not a valid section'):
""":raises ConfigError: if the section is invalid"""
if not self.cnf.has_section(section):
raise ConfigError(err_msg % section)
def _assert_section_name(self, section, name, err_msg='%s "%s" not found'):
""":raises ConfigError: if the section name does not exist"""
if name not in self.cnf.keys(section):
raise ConfigError(err_msg % (section, name))
def _assert_has_option(
self, section, name, option, err_msg='%s "%s" has no option "%s"'):
""":raises ConfigError: if the option does not exist"""
if option not in self.cnf.get(section, name):
raise ConfigError(err_msg % (section, name, option))
def print_option(self, section, name, option):
"""Print a configuration option"""
self._validate_section(section)
self._assert_section_name(section, name or option)
if name:
self._assert_has_option(section, name, option)
section = '%s.%s' % (section, name) if name else section
value = self.cnf.get(section, option)
sys.stdout.write(' %s: %s\n' % (option, value))
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:
self._assert_section_name(section, 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)
if not names:
raise ConfigError('Section %s not found' % section)
for name in names:
sys.stdout.write('%s %s\n' % (section, name))
sys.stdout.flush()
self.list_section(section, name)
def list_sections(self):
"""List all configuration sections"""
for section in self.cnf.sections():
self.list_section_type(section)
def set_global_setting(self, section, option, value):
assert section == 'global', 'section %s should be global"' % section
self.cnf.set(section, option, value)
self.cnf.write()
return True
def set_setting(self, section, name, option, value):
self._validate_section(section)
self.cnf.set('%s.%s' % (section, name), option, value)
self.cnf.write()
return True
def delete_global_option(self, section, option, yes=False):
"""Delete global option"""
assert section == 'global', 'Section must be global, not %s' % section
self._assert_section_name(
section, option, '%s option "%s" does not exist')
if (not yes and 'y' != raw_input(
'Delete %s option %s? [y|N]: ' % (section, option))):
sys.stderr.write('Aborted\n')
return False
self.cnf.remove_option(section, option)
self.cnf.write()
return True
def delete_section_option(self, section, name, option, yes=False):
"""Delete a section (sync or cloud) option"""
self._validate_section(section)
self._assert_section_name(section, name)
self._assert_has_option(section, name, option)
if (not yes and 'y' != raw_input(
'Delete %s of %s "%s"? [y|N]: ' % (option, section, name))):
sys.stderr.write('Aborted\n')
return False
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()
return True
def delete_section(self, section, name, yes=False):
"""Delete a section (sync or cloud)"""
self._validate_section(section)
self._assert_section_name(section, name)
if (not yes and 'y' != raw_input(
'Delete %s "%s"? [y|N]: ' % (section, name))):
sys.stderr.write('Aborted\n')
return False
self.cnf.remove_option(section, name)
self.cnf.write()
return True
def handle_UI_error(func):
def inner(*args, **kw):
try:
return func(*args, **kw)
except protocol_client.UIClientError as uice:
msg = '%s' % uice
LOGGER.debug(msg)
sys.stderr.write(msg)
sys.stderr.flush()
return inner
class AgkyraCLI(cmd.Cmd):
"""The CLI for Agkyra is connected to a protocol server"""
cnf_cmds = ConfigCommands()
helper = protocol.SessionHelper()
def __init__(self, *args, **kwargs):
self.callback = kwargs.pop('callback', os.path.realpath(sys.argv[0]))
self.args = kwargs.pop('parsed_args', None)
AGKYRA_LOGGER.setLevel(logging.DEBUG
if self.args.debug else logging.INFO)
LOGGER.debug("Callback is %s" % self.callback)
cmd.Cmd.__init__(self, *args, **kwargs)
@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="*")
if not any([a for a in sys.argv[1:] if not a.startswith('-')]):
parser.add_argument(
'--version',
action='store_true', help='Output agkyra version and exit')
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"""
LOGGER.info('Starting the agkyra daemon')
session_daemon = self.helper.create_session_daemon()
if session_daemon:
session_daemon.start()
LOGGER.info('Daemon is shut down')
else:
LOGGER.info('Another daemon is running, aborting')
@property
def client(self):
"""Return the helper client instace or None"""
self._client = getattr(self, '_client', None)
if self._client is None:
self._client = self.try_make_client()
return self._client
def try_make_client(self):
session = self.helper.load_active_session()
if session:
client = protocol_client.UIClient(session)
try:
client.connect()
return client
except socket.error as e:
if e.errno == errno.ECONNREFUSED:
sys.stderr.write(
"It seems that a previous Agkyra execution hasn't "
"exited properly.\nPlease, try again after a few "
"seconds.\n")
exit(1)
else:
raise
else:
return None
def do_help(self, line):
"""Help on agkyra GUI and CLI
agkyra Run agkyra with GUI (equivalent to "agkyra gui")
agkyra <cmd> Run a command through agkyra CLI
agkyra --version Print agkyra version and exit
To get help on agkyra commands:
help <cmd> for an individual command
help <--list | -l> for all commands
"""
if getattr(self.args, 'list', None):
self.args.list = None
prefix = 'do_'
for c in self.get_names():
if c.startswith(prefix):
actual_name = c[len(prefix):]
sys.stderr.write('- %s -\n' % actual_name)
self.do_help(actual_name)
else:
if not line:
cmd.Cmd.do_help(self, 'help')
cmd.Cmd.do_help(self, line)
def emptyline(self):
if self.args.version:
from agkyra import __version__
sys.stdout.write('%s\n' % __version__)
sys.stdout.flush()
elif not self.must_help(''):
return self.do_gui('')
def default(self, line):
self.do_help(line)
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
"""
try:
{
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
}[len(args)](*args)
except Exception as e:
LOGGER.debug('%s\n' % e)
if isinstance(e, ConfigError):
raise
sys.stderr.write(self.config_list.__doc__ + '\n')
def _warn_user_about_setting_updates(self):
if self.helper.load_active_session():
sys.stderr.write(
'Done\n'
'WARNING: Setting updates will take effect after agkyra is '
'shutdown and started again. To do this:\n'
'\t$ agkyra shutdown\n\t$ agkyra start\n');
sys.stderr.flush()
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
"""
r = False
try:
r = {
3: self.cnf_cmds.set_global_setting,
4: self.cnf_cmds.set_setting
}[len(args)](*args)
except Exception as e:
LOGGER.debug('%s\n' % e)
if isinstance(e, ConfigError):
raise
sys.stderr.write(self.config_set.__doc__ + '\n')
if r:
self._warn_user_about_setting_updates()
def config_delete(self, args):
"""Delete an option
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
"""
args.append(self.args.yes)
r = False
try:
r = {
3: self.cnf_cmds.delete_global_option if (
args[0] == 'global') else self.cnf_cmds.delete_section,
4: self.cnf_cmds.delete_section_option
}[len(args)](*args)
except Exception as e:
LOGGER.debug('%s\n' % e)
if isinstance(e, ConfigError):
raise
sys.stderr.write(self.config_delete.__doc__ + '\n')
if r:
self._warn_user_about_setting_updates()
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
"""
if self.must_help('config'):
return
args = line.split(' ')
try:
method = getattr(self, 'config_' + args[0])
method(args[1:])
except AttributeError:
self.do_help('config')
except ConfigError as ce:
sys.stderr.write('%s\n' % ce)
sys.stderr.flush()
def do_status(self, line):
"""Get Agkyra client status. Status may be one of the following:
Syncing There is a process syncing right now
Paused Notifiers are active but syncing is paused
Not running No active processes
"""
self._status(line)
@handle_UI_error
def _status(self, line):
if self.must_help('status'):
return
client = self.client
status, msg = client.get_status() if client else None, 'Not running'
if status:
msg = NOTIFICATION[str(status['code'])]
diff = remaining(status)
if diff:
msg = '%s, %s remaining' % (msg, diff)
sys.stdout.write('%s\n' % msg)
sys.stdout.flush()
def do_start(self, line):
"""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
"""
self._start(line)
def make_server_if_needed(self):
client = self.client
if not client:
sys.stderr.write('No Agkyra daemons running, starting one')
protocol.launch_server(self.callback, self.args.debug)
sys.stderr.write(' ... ')
self.helper.wait_session_to_load()
sys.stderr.write('OK\n')
@handle_UI_error
def _start(self, line):
if self.must_help('start'):
return
if line in ['daemon']:
return self.launch_daemon()
if line:
sys.stderr.write("Unrecognized subcommand '%s'.\n" % line)
sys.stderr.flush()
return
self.make_server_if_needed()
client = self.client
status = client.get_status()
if status['code'] == STATUS['SETTINGS READY']:
client.init()
status = client.get_status()
if status['code'] in [STATUS['PAUSED'], STATUS['READY']]:
client.start()
sys.stderr.write('Starting syncer ... ')
try:
client.wait_until_syncing()
sys.stderr.write('OK\n')
except protocol_client.UIClientError as uice:
sys.stderr.write('%s\n' % uice)
elif status['code'] == STATUS['SYNCING']:
sys.stderr.write('Already ')
sys.stderr.flush()
self.do_status(line)
def do_pause(self, line):
"""Pause a session (stop it from syncing, but keep it running)"""
self._pause(line)
@handle_UI_error
def _pause(self, line):
if self.must_help('pause'):
return
client = self.client
if client:
status = client.get_status()
if status['code'] == STATUS['PAUSED']:
sys.stderr.write('Already ')
else:
client.pause()
sys.stderr.write('Pausing syncer ... ')
try:
client.wait_until_paused()
sys.stderr.write('OK\n')
except protocol_client.UIClientError as uice:
sys.stderr.write('%s\n' % uice)
sys.stderr.flush()
self.do_status(line)
def do_shutdown(self, line):
"""Shutdown Agkyra, if it is running"""
self._shutdown(line)
@handle_UI_error
def _shutdown(self, line):
if self.must_help('shutdown'):
return
client = self.client
if client:
client.shutdown()
sys.stderr.write('Shutting down Agkyra ... ')
success = self.helper.wait_session_to_stop()
sys.stderr.write('Stopped' if success else 'Still up (timed out)')
sys.stderr.write('\n')
else:
sys.stderr.write('Not running\n')
sys.stderr.flush()
# 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
session = self.helper.load_active_session()
if not session:
self.make_server_if_needed()
session = self.helper.wait_session_to_load()
if session:
LOGGER.info('Start new GUI')
new_gui = gui.GUI(session, debug=self.args.debug)
try:
new_gui.start()
except KeyboardInterrupt:
LOGGER.info('GUI interrupted by user')
sys.stderr.write('GUI interrupted by user, exiting\n')
sys.stderr.flush()
new_gui.clean_exit()
if self.client:
self._shutdown('')
except socket.error as e:
if e.errno == errno.ECONNREFUSED:
sys.stderr.write(
"It seems that a previous Agkyra execution hasn't "
"exited properly.\nPlease, try again after a few "
"seconds.\n")
new_gui.clean_exit()
exit(1)
else:
raise
else:
sys.stderr.write('Session failed to load\n')
sys.stderr.flush()