From d4f744952e2eff28b9ced0674885a601801270e1 Mon Sep 17 00:00:00 2001 From: Morgan Courbet Date: Tue, 3 Feb 2015 18:08:57 +0100 Subject: [PATCH] Add multiple targets Allow the user to define multiple targets to execute a consistent group of tasks. --- README.md | 214 +++++++++++++++++++++++++++++------------------ dotbot/cli.py | 23 +++-- dotbot/config.py | 16 +++- 3 files changed, 161 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index bc20e9d1..74e20f11 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ without the annoyance of having to manually copy or link files. Dotbot itself is entirely self contained and requires no installation (it's self-bootstrapping), so it's not necessary to install any software before you provision a new machine! All you have to do is download your dotfiles and then -run `./install`. +run `./install -t `. Template -------- @@ -94,10 +94,10 @@ installer should be able to be run multiple times without causing any problems.** This makes a lot of things easier to do (in particular, syncing updates between machines becomes really easy). -Dotbot configuration files are YAML (or JSON) arrays of tasks, where each task -is a dictionary that contains a command name mapping to data for that command. -Tasks are run in the order in which they are specified. Commands within a task -do not have a defined ordering. +Dotbot configuration files are YAML (or JSON) dictionaries of targets with +arrays of tasks, where each task is a dictionary that contains a command name +mapping to data for that command. Tasks are run in the order in which they are +specified. Commands within a task do not have a defined ordering. ### Link @@ -122,34 +122,37 @@ should be forcibly linked. ##### Example (YAML) ```yaml -- link: - ~/.config/terminator: - create: true - path: config/terminator/ - ~/.vim: vim/ - ~/.vimrc: vimrc - ~/.zshrc: - force: true - path: zshrc +work: + - link: + ~/.config/terminator: + create: true + path: config/terminator/ + ~/.vim: vim/ + ~/.vimrc: vimrc + ~/.zshrc: + force: true + path: zshrc ``` ##### Example (JSON) ```json -[{ - "link": { - "~/.config/terminator": { - "create": true, - "path": "config/terminator/" - }, - "~/.vim": "vim/", - "~/.vimrc": "vimrc", - "~/.zshrc": { - "force": true, - "path": "zshrc" +{ + "work": [{ + "link": { + "~/.config/terminator": { + "create": true, + "path": "config/terminator/" + }, + "~/.vim": "vim/", + "~/.vimrc": "vimrc", + "~/.zshrc": { + "force": true, + "path": "zshrc" + } } - } -}] + }] +} ``` ### Shell @@ -171,36 +174,39 @@ this syntax, all keys are optional except for the command itself. ##### Example (YAML) ```yaml -- shell: - - mkdir -p ~/src - - [mkdir -p ~/downloads, Creating downloads directory] - - - command: read var && echo Your variable is $var - stdin: true - stdout: true - - - command: read fail - stderr: true +work: + - shell: + - mkdir -p ~/src + - [mkdir -p ~/downloads, Creating downloads directory] + - + command: read var && echo Your variable is $var + stdin: true + stdout: true + - + command: read fail + stderr: true ``` ##### Example (JSON) ```json -[{ - "shell": [ - "mkdir -p ~/src", - ["mkdir -p ~/downloads", "Creating downloads directory"], - { - "command": "read var && echo Your variable is $var", - "stdin": true, - "stdout": true - }, - { - "command": "read fail", - "stderr": true - } - ] -}] +{ + "work": [{ + "shell": [ + "mkdir -p ~/src", + ["mkdir -p ~/downloads", "Creating downloads directory"], + { + "command": "read var && echo Your variable is $var", + "stdin": true, + "stdout": true + }, + { + "command": "read fail", + "stderr": true + } + ] + }] +} ``` ### Clean @@ -216,15 +222,18 @@ Clean commands are specified as an array of directories to be cleaned. ##### Example (YAML) ```yaml -- clean: ['~'] +work: + - clean: ['~'] ``` ##### Example (JSON) ```json -[{ - "clean": ["~"] -}] +{ + "work": [{ + "clean": ["~"] + }] +} ``` ### Full Example @@ -234,16 +243,26 @@ configuration. The conventional name for the configuration file is `install.conf.yaml`. ```yaml -- clean: ['~'] - -- link: - ~/.dotfiles: '' - ~/.tmux.conf: tmux.conf - ~/.vim: vim/ - ~/.vimrc: vimrc - -- shell: - - [git update-submodules, Installing/updating submodules] +common: + - clean: ['~'] + + - link: + ~/.dotfiles: '' + ~/.tmux.conf: tmux.conf + ~/.vim: vim/ + ~/.vimrc: vimrc + + - shell: + - [git update-submodules, Installing/updating submodules] + +laptop: + - shell: + - [sudo apt-get install vim, Installing vim] + +server: + - shell: + - [sudo apt-get install tmux, Installing tmux] + - [echo 'Europe/Paris' | sudo tee /etc/timezone > /dev/null && sudo dpkg-reconfigure -f noninteractive tzdata, Configuring timezone] ``` The configuration file can also be written in JSON. Here is the JSON equivalent @@ -251,24 +270,53 @@ of the YAML configuration given above. The conventional name for this file is `install.conf.json`. ```json -[ - { - "clean": ["~"] - }, - { - "link": { - "~/.dotfiles": "", - "~/.tmux.conf": "tmux.conf", - "~/.vim": "vim/", - "~/.vimrc": "vimrc" +{ + "common": [ + { + "clean": ["~"] + }, + { + "link": { + "~/.dotfiles": "", + "~/.tmux.conf": "tmux.conf", + "~/.vim": "vim/", + "~/.vimrc": "vimrc" + } + }, + { + "shell": [ + ["git submodule update --init --recursive", "Installing submodules"] + ] } - }, - { - "shell": [ - ["git submodule update --init --recursive", "Installing submodules"] - ] - } -] + ], + "laptop": [ + { + "clean": [] + }, + { + "link": {} + }, + { + "shell": [ + ["sudo apt-get install vim", "Installing vim"] + ] + } + ], + "server": [ + { + "clean": [] + }, + { + "link": {} + }, + { + "shell": [ + ["sudo apt-get install tmux", "Installing tmux"], + ["echo 'Europe/Paris' | sudo tee /etc/timezone > /dev/null && sudo dpkg-reconfigure -f noninteractive tzdata", "Configuring timezone"] + ] + } + ] +} ``` Contributing diff --git a/dotbot/cli.py b/dotbot/cli.py index c280ca1b..9fee8360 100644 --- a/dotbot/cli.py +++ b/dotbot/cli.py @@ -1,5 +1,5 @@ from argparse import ArgumentParser -from .config import ConfigReader, ReadingError +from .config import ConfigReader, ReadingError, ConfigurationError from .dispatcher import Dispatcher, DispatchError from .messenger import Messenger from .messenger import Level @@ -17,9 +17,12 @@ def add_options(parser): parser.add_argument('-c', '--config-file', nargs = 1, dest = 'config_file', help = 'run commands given in CONFIGFILE', metavar = 'CONFIGFILE', required = True) + parser.add_argument('-t', '--targets', nargs = '*', dest = 'targets', + help = 'set the target environments defined in the configuration file', metavar = 'TARGET', + required = True) -def read_config(config_file): - reader = ConfigReader(config_file) +def read_config(config_file, target): + reader = ConfigReader(config_file, target) return reader.get_config() def main(): @@ -34,14 +37,20 @@ def main(): 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) + targets = options.targets + target_tasks = read_config(options.config_file[0], targets) + + success = True + for target, tasks in target_tasks.iteritems(): + log.info('\nExecuting tasks for target %s' % target) + 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: + except (ReadingError, DispatchError, ConfigurationError) as e: log.error('%s' % e) exit(1) except KeyboardInterrupt: diff --git a/dotbot/config.py b/dotbot/config.py index 49252d8a..d076a801 100644 --- a/dotbot/config.py +++ b/dotbot/config.py @@ -1,8 +1,17 @@ import yaml +from .messenger import Messenger class ConfigReader(object): - def __init__(self, config_file_path): - self._config = self._read(config_file_path) + def __init__(self, config_file_path, targets): + complete_config = self._read(config_file_path) + + target_configs = {} + for target in targets: + if not complete_config.has_key(target): + raise ConfigurationError('The target %s is not defined in the configuration file' % target) + target_configs[target] = complete_config.get(target) + + self._config = target_configs def _read(self, config_file_path): try: @@ -17,3 +26,6 @@ def get_config(self): class ReadingError(Exception): pass + +class ConfigurationError(Exception): + pass