Initial commit
This commit is contained in:
commit
60a560e976
16 changed files with 347 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.pyc
|
19
bin/dotbot
Executable file
19
bin/dotbot
Executable file
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import sys, os
|
||||
|
||||
PROJECT_ROOT_DIRECTORY = os.path.dirname(
|
||||
os.path.dirname(os.path.realpath(__file__)))
|
||||
|
||||
if os.path.exists(os.path.join(PROJECT_ROOT_DIRECTORY, 'dotbot')):
|
||||
if PROJECT_ROOT_DIRECTORY not in sys.path:
|
||||
sys.path.insert(0, PROJECT_ROOT_DIRECTORY)
|
||||
os.putenv('PYTHONPATH', PROJECT_ROOT_DIRECTORY)
|
||||
|
||||
import dotbot
|
||||
|
||||
def main():
|
||||
dotbot.cli.main()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
1
dotbot/__init__.py
Normal file
1
dotbot/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from .cli import main
|
46
dotbot/cli.py
Normal file
46
dotbot/cli.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
from argparse import ArgumentParser
|
||||
from .config import ConfigReader, ReadingError
|
||||
from .dispatcher import Dispatcher, DispatchError
|
||||
from .messenger import Messenger
|
||||
from .messenger import Level
|
||||
|
||||
def add_options(parser):
|
||||
parser.add_argument('-Q', '--super-quiet', dest = 'super_quiet', action = 'store_true',
|
||||
help = 'suppress almost all output')
|
||||
parser.add_argument('-q', '--quiet', dest = 'quiet', action = 'store_true',
|
||||
help = 'suppress most output')
|
||||
parser.add_argument('-v', '--verbose', dest = 'verbose', action = 'store_true',
|
||||
help = 'enable verbose output')
|
||||
parser.add_argument('-d', '--base-directory', nargs = 1,
|
||||
dest = 'base_directory', help = 'execute commands from within BASEDIR',
|
||||
metavar = 'BASEDIR', required = True)
|
||||
parser.add_argument('-c', '--config-file', nargs = 1, dest = 'config_file',
|
||||
help = 'run commands given in CONFIGFILE', metavar = 'CONFIGFILE',
|
||||
required = True)
|
||||
|
||||
def read_config(config_file):
|
||||
reader = ConfigReader(config_file)
|
||||
return reader.get_config()
|
||||
|
||||
def main():
|
||||
log = Messenger()
|
||||
try:
|
||||
parser = ArgumentParser()
|
||||
add_options(parser)
|
||||
options = parser.parse_args()
|
||||
if (options.super_quiet):
|
||||
log.set_level(Level.WARNING)
|
||||
if (options.quiet):
|
||||
log.set_level(Level.INFO)
|
||||
if (options.verbose):
|
||||
log.set_level(Level.DEBUG)
|
||||
tasks = read_config(options.config_file[0])
|
||||
dispatcher = Dispatcher(options.base_directory[0])
|
||||
success = dispatcher.dispatch(tasks)
|
||||
if success:
|
||||
log.info('\n==> All tasks executed successfully')
|
||||
else:
|
||||
raise DispatchError('\n==> Some tasks were not executed successfully')
|
||||
except (ReadingError, DispatchError) as e:
|
||||
log.error('%s' % e)
|
||||
exit(1)
|
19
dotbot/config.py
Normal file
19
dotbot/config.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
import json
|
||||
|
||||
class ConfigReader(object):
|
||||
def __init__(self, config_file_path):
|
||||
self._config = self._read(config_file_path)
|
||||
|
||||
def _read(self, config_file_path):
|
||||
try:
|
||||
with open(config_file_path) as fin:
|
||||
data = json.load(fin)
|
||||
return data
|
||||
except Exception:
|
||||
raise ReadingError('Could not read config file')
|
||||
|
||||
def get_config(self):
|
||||
return self._config
|
||||
|
||||
class ReadingError(Exception):
|
||||
pass
|
46
dotbot/dispatcher.py
Normal file
46
dotbot/dispatcher.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
import os
|
||||
from .executor import *
|
||||
from .messenger import Messenger
|
||||
|
||||
class Dispatcher(object):
|
||||
PLUGIN_CLASS = Executor
|
||||
PLUGIN_DIR = 'dotbot/executor'
|
||||
|
||||
def __init__(self, base_directory):
|
||||
self._log = Messenger()
|
||||
self._set_base_directory(base_directory)
|
||||
self._load_plugins()
|
||||
|
||||
def _set_base_directory(self, base_directory):
|
||||
path = os.path.abspath(os.path.realpath(
|
||||
os.path.expanduser(base_directory)))
|
||||
if os.path.exists(path):
|
||||
self._base_directory = path
|
||||
else:
|
||||
raise DispatchError('Nonexistant base directory')
|
||||
|
||||
def dispatch(self, tasks):
|
||||
success = True
|
||||
for task in tasks:
|
||||
for action in task:
|
||||
handled = False
|
||||
for plugin in self._plugins:
|
||||
if plugin.can_handle(action):
|
||||
try:
|
||||
success &= plugin.handle(action, task[action])
|
||||
handled = True
|
||||
except Exception:
|
||||
self._log.error(
|
||||
'An error was encountered while executing action %s' %
|
||||
action)
|
||||
if not handled:
|
||||
success = False
|
||||
self._log.error('Action %s not handled' % action)
|
||||
return success
|
||||
|
||||
def _load_plugins(self):
|
||||
self._plugins = [plugin(self._base_directory)
|
||||
for plugin in Executor.__subclasses__()]
|
||||
|
||||
class DispatchError(Exception):
|
||||
pass
|
3
dotbot/executor/__init__.py
Normal file
3
dotbot/executor/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from .executor import Executor
|
||||
from .linker import Linker
|
||||
from .commandrunner import CommandRunner
|
32
dotbot/executor/commandrunner.py
Normal file
32
dotbot/executor/commandrunner.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
import os, subprocess
|
||||
from . import Executor
|
||||
|
||||
class CommandRunner(Executor):
|
||||
'''
|
||||
Run arbitrary shell commands.
|
||||
'''
|
||||
|
||||
def can_handle(self, directive):
|
||||
return directive == 'shell'
|
||||
|
||||
def handle(self, directive, data):
|
||||
if directive != 'shell':
|
||||
raise ValueError('CommandRunner cannot handle directive %s' %
|
||||
directive)
|
||||
return self._process_commands(data)
|
||||
|
||||
def _process_commands(self, data):
|
||||
success = True
|
||||
with open(os.devnull, 'w') as devnull:
|
||||
for cmd, msg in data:
|
||||
self._log.lowinfo('%s [%s]' % (msg, cmd))
|
||||
ret = subprocess.call(cmd, shell = True, stdout = devnull,
|
||||
stderr = devnull, cwd = self._base_directory)
|
||||
if ret != 0:
|
||||
success = False
|
||||
self._log.warning('Command [%s] failed' % cmd)
|
||||
if success:
|
||||
self._log.info('All commands have been executed')
|
||||
else:
|
||||
self._log.error('Some commands were not sucessfully executed')
|
||||
return success
|
24
dotbot/executor/executor.py
Normal file
24
dotbot/executor/executor.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from ..messenger import Messenger
|
||||
|
||||
class Executor(object):
|
||||
'''
|
||||
Abstract base class for commands that process directives.
|
||||
'''
|
||||
|
||||
def __init__(self, base_directory):
|
||||
self._base_directory = base_directory
|
||||
self._log = Messenger()
|
||||
|
||||
def can_handle(self, directive):
|
||||
'''
|
||||
Returns true if the Executor can handle the directive.
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
def handle(self, directive, data):
|
||||
'''
|
||||
Executes the directive.
|
||||
|
||||
Returns true if the Executor successfully handled the directive.
|
||||
'''
|
||||
raise NotImplementedError
|
73
dotbot/executor/linker.py
Normal file
73
dotbot/executor/linker.py
Normal file
|
@ -0,0 +1,73 @@
|
|||
import os
|
||||
from . import Executor
|
||||
|
||||
class Linker(Executor):
|
||||
'''
|
||||
Symbolically links dotfiles.
|
||||
'''
|
||||
|
||||
def can_handle(self, directive):
|
||||
return directive == 'link'
|
||||
|
||||
def handle(self, directive, data):
|
||||
if directive != 'link':
|
||||
raise ValueError('Linker cannot handle directive %s' % directive)
|
||||
return self._process_links(data)
|
||||
|
||||
def _process_links(self, links):
|
||||
success = True
|
||||
for destination, source in links.items():
|
||||
success &= self._link(source, destination)
|
||||
if success:
|
||||
self._log.info('All links have been set up')
|
||||
else:
|
||||
self._log.error('Some links were not successfully set up')
|
||||
return success
|
||||
|
||||
def _is_link(self, path):
|
||||
'''
|
||||
Returns true if the path is a symbolic link.
|
||||
'''
|
||||
return os.path.islink(os.path.expanduser(path))
|
||||
|
||||
def _link_destination(self, path):
|
||||
'''
|
||||
Returns the absolute path to the destination of the symbolic link.
|
||||
'''
|
||||
path = os.path.expanduser(path)
|
||||
rel_dest = os.readlink(path)
|
||||
return os.path.join(os.path.dirname(path), rel_dest)
|
||||
|
||||
def _exists(self, path):
|
||||
'''
|
||||
Returns true if the path exists.
|
||||
'''
|
||||
path = os.path.expanduser(path)
|
||||
return os.path.exists(path)
|
||||
|
||||
def _link(self, source, link_name):
|
||||
'''
|
||||
Links link_name to source.
|
||||
|
||||
Returns true if successfully linked files.
|
||||
'''
|
||||
success = False
|
||||
source = os.path.join(self._base_directory, source)
|
||||
if not self._exists(link_name) and self._is_link(link_name):
|
||||
self._log.warning('Invalid link %s -> %s' %
|
||||
(link_name, self._link_destination(link_name)))
|
||||
elif not self._exists(link_name):
|
||||
self._log.lowinfo('Creating link %s -> %s' % (link_name, source))
|
||||
os.symlink(source, os.path.expanduser(link_name))
|
||||
success = True
|
||||
elif self._exists(link_name) and not self._is_link(link_name):
|
||||
self._log.warning(
|
||||
'%s already exists but is a regular file or directory' %
|
||||
link_name)
|
||||
elif self._link_destination(link_name) != source:
|
||||
self._log.warning('Incorrect link %s -> %s' %
|
||||
(link_name, self._link_destination(link_name)))
|
||||
else:
|
||||
self._log.lowinfo('Link exists %s -> %s' % (link_name, source))
|
||||
success = True
|
||||
return success
|
2
dotbot/messenger/__init__.py
Normal file
2
dotbot/messenger/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
from .messenger import Messenger
|
||||
from .level import Level
|
8
dotbot/messenger/color.py
Normal file
8
dotbot/messenger/color.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
class Color(object):
|
||||
NONE = ''
|
||||
RESET = '\033[0m'
|
||||
RED = '\033[91m'
|
||||
GREEN = '\033[92m'
|
||||
YELLOW = '\033[93m'
|
||||
BLUE = '\033[94m'
|
||||
MAGENTA = '\033[95m'
|
7
dotbot/messenger/level.py
Normal file
7
dotbot/messenger/level.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
class Level(object):
|
||||
NOTSET = 0
|
||||
DEBUG = 10
|
||||
LOWINFO = 15
|
||||
INFO = 20
|
||||
WARNING = 30
|
||||
ERROR = 40
|
60
dotbot/messenger/messenger.py
Normal file
60
dotbot/messenger/messenger.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
import sys
|
||||
from ..util.singleton import Singleton
|
||||
from .color import Color
|
||||
from .level import Level
|
||||
|
||||
class Messenger(object):
|
||||
__metaclass__ = Singleton
|
||||
|
||||
def __init__(self, level = Level.LOWINFO):
|
||||
self.set_level(level)
|
||||
|
||||
def set_level(self, level):
|
||||
self._level = level
|
||||
|
||||
def log(self, level, message):
|
||||
if (level >= self._level):
|
||||
print '%s%s%s' % (self._color(level), message, self._reset())
|
||||
|
||||
def debug(self, message):
|
||||
self.log(Level.DEBUG, message)
|
||||
|
||||
def lowinfo(self, message):
|
||||
self.log(Level.LOWINFO, message)
|
||||
|
||||
def info(self, message):
|
||||
self.log(Level.INFO, message)
|
||||
|
||||
def warning(self, message):
|
||||
self.log(Level.WARNING, message)
|
||||
|
||||
def error(self, message):
|
||||
self.log(Level.ERROR, message)
|
||||
|
||||
def _color(self, level):
|
||||
'''
|
||||
Get a color (terminal escape sequence) according to a level.
|
||||
'''
|
||||
if not sys.stdout.isatty():
|
||||
return ''
|
||||
elif level < Level.DEBUG:
|
||||
return ''
|
||||
elif Level.DEBUG <= level < Level.LOWINFO:
|
||||
return Color.YELLOW
|
||||
elif Level.LOWINFO <= level < Level.INFO:
|
||||
return Color.BLUE
|
||||
elif Level.INFO <= level < Level.WARNING:
|
||||
return Color.GREEN
|
||||
elif Level.WARNING <= level < Level.ERROR:
|
||||
return Color.MAGENTA
|
||||
elif Level.ERROR <= level:
|
||||
return Color.RED
|
||||
|
||||
def _reset(self):
|
||||
'''
|
||||
Get a reset color (terminal escape sequence).
|
||||
'''
|
||||
if not sys.stdout.isatty():
|
||||
return ''
|
||||
else:
|
||||
return Color.RESET
|
0
dotbot/util/__init__.py
Normal file
0
dotbot/util/__init__.py
Normal file
6
dotbot/util/singleton.py
Normal file
6
dotbot/util/singleton.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
class Singleton(type):
|
||||
_instances = {}
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls not in cls._instances:
|
||||
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
||||
return cls._instances[cls]
|
Loading…
Reference in a new issue