commit dfab18817e941af32dc66295f64fb903ade8e054 Author: Carl Mercier Date: Wed Mar 21 16:05:28 2012 -0400 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec82e86 --- /dev/null +++ b/README.md @@ -0,0 +1,150 @@ +# Shoestrap + +Shoestrap is a simple framework to bootstrap *nix machines. + +It speaks Bash so there's virtually no learning curve. More importantly, you +won't have to learn yet another DSL. Shoestrap aims to get out of your way. + +You should be able to get up and running in minutes, not hours. + + +## What about Chef, Puppet and co.? + +Chef and Puppet are great tools, but they are too complex for most use cases. +The learning curve for these tools is quite steep as they each have their own +DSL. On the other end, Shoestrap is just Bash. It does not require any +'Bash to config files' translation. + +I believe Shoestrap is a great simple alternative to Chef or Puppet that will +fulfill the needs of most people. + + +## Terminology + +Shoestrap uses some of the Chef terminology since I couldn't come up with +better names or analogies. + +### Cookbook + +A cookbook is a Bash script that executes different actions. For example, +it may install packages, run 'recipes'. Think of it as a dispatcher. + +Cookbooks live at the root of your Shoestrap project. You can have multiple +cookbooks per project. + +### Recipes + +Recipes are snippets of Bash code that can be executed from a Cookbook. For +example, you may have a recipe to install `memcached`, or a recipe to setup +SSH keys on the target machine. Remember, it's just Bash, so anything goes. + +### Assets + +An asset is a file that will be needed by the target machine. For example, +a configuration file or an init script. + + +## Helpers + +Shoestrap ships with many Bash helpers functions. They can be found in +`helpers/default`. You do NOT need to use the built-in helper functions, +but they will simplify many of the most common tasks you'll need to perform. + +Helper functions can be used from cookbooks or recipes. You may also pass +arguments to these functions. + +You may add your own helper functions in `helpers/custom`. + +Here are some of the most commonly used helpers: + +#### `add_line` +Concatenate a line to a text file if it's not already there. + +#### `add_user` +Add a user to the system. + +#### `copy` +Copy an asset file. It first looks in the assets/{cookbook} directory and falls back to assets/default if file doesn't exist. + +#### `error` +Write an error to the screen and halt execution. + +#### `is_installed` +Check if an element has already been installed. Useful to prevent code from running more than once. Also see `set_installed`. + +#### `log` +Write a line to the screen. + +#### `package` +Install a package (ie: apt-get install {package-name}). + +#### `package_update` +Update packages in package manager (ie: apt-get update). + +#### `recipe` +Run a recipe. It first looks in the recipes/{cookbook} directory and falls back to recipes/default if file doesn't exist. + +#### `set_installed` +Sets an element as 'installed'. + + +## Getting Started + +1. Clone the `shoestrap` repo to your local machine. + `git clone https://github.com/cmer/shoestrap.git` + +2. Rename `./my-cookbook` to something a little bit more meaningful. For example, + you might want to call your cookbook `web` if it bootstraps a web server. Make + sure it is executable (`chmod +x {my-cookbook}`). + +3. Specify actions to take in the cookbook. For example, which recipes to run, which + packages to install or which user(s) to add. For example: `recipe 'nginx'`. + +4. Create a recipe file under `recipes/default`. For example: `recipes/default/nginx`. The recipe + is the code to execute. In our example, it would be the code to run to install `nginx`. + +5. Add assets (if needed) under `assets/default/{recipe}`. For example: `assets/default/nginx/nginx.conf`. + +6. Upload your project to the target machine. You can use `scp`, Capistrano, Git or whatever you feel + comfortable with. + +7. Run your cookbook from the target machine. For example: `sudo ./web`. + + +## Example + +You can see a sample project at http://github.com/cmer/shoestrap-example + +Browse the source code, it's the best way to familiarize yourself with Shoestrap. It's also a great starting +point for your own Shoestrap project. + + +## Example: Directory Structure of a Shoestrap Project + + [assets] + [default] # Assets to be used by default + [recipe1] # Assets for 'recipe1'. + foo.conf + bar.conf + [cookbook1] # Assets for 'cookbook1'. If asset cannot be found here, fallback is 'default' + [recipe1] # Assets for 'recipe1' when executed from 'cookbook1'. Overrides anything in [default]. + foo.conf + [helpers] + custom # Your custom Bash functions and helpers + default # Shoestrap's default helpers + initialize # Initialize script. + [recipes] + [default] # Recipes to be used by default + recipe1 + recipe2 + recipe3 + [cookbook1] # Recipes for 'cookbook1'. Overrides anything in [default]. + recipe1 + cookbook1 # The cookbook script itself. This is your point of entry to Shoestrap + + +## Compatibility + +Shoestrap has only been tested with Ubuntu Oneiric 11.10 but should work with any/most Unix-like +operating systems. My goal is to support Ubuntu/Debian, CentOS/Red Hat and Mac OS X. I will need +help from the community to achieve this, however. diff --git a/assets/default/.gitkeep b/assets/default/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/helpers/custom b/helpers/custom new file mode 100644 index 0000000..308df5a --- /dev/null +++ b/helpers/custom @@ -0,0 +1,5 @@ +#!/bin/bash + +############################################################################## +# Add your custom helpers here. Remember, this is just Bash! +############################################################################## diff --git a/helpers/default b/helpers/default new file mode 100644 index 0000000..2126388 --- /dev/null +++ b/helpers/default @@ -0,0 +1,360 @@ +#!/bin/bash + +############################################################################## +# DO NOT MODIFY THIS FILE. Instead, modify 'helpers/custom'. +############################################################################## + +COOKBOOK_NAME="$(basename $0)" +DIR="$( cd "$( dirname "$0" )" && pwd )" + +# +# Run a given recipe. +# +# Arguments can be passed to the 'recipe' function. They will be accessible by +# the recipe as $2, $3, $4, etc. +# +recipe () { + CURRENT_RECIPE_NAME=$1 + DEFAULT_ASSETS_PATH="$DIR/assets/default/$CURRENT_RECIPE_NAME" + COOKBOOK_ASSETS_PATH="$DIR/assets/$COOKBOOK_NAME/$CURRENT_RECIPE_NAME" + local custom_recipe="$DIR/recipes/custom/$CURRENT_RECIPE_NAME" + local default_recipe="$DIR/recipes/default/$CURRENT_RECIPE_NAME" + + if [ -f $custom_recipe ]; then + log "Running recipe '$custom_recipe'..." 1 + separator + . $custom_recipe + + elif [ -f $default_recipe ]; then + log "Running recipe '$default_recipe'..." 1 + separator + . $default_recipe + else + error "Could not find recipe for '$CURRENT_RECIPE_NAME'. Fail!" + fi + + cd $DIR +} + +# +# Prints the 'finished' banner. +# +finished () { + spacer 1 + separator "=" + echo " FINISHED: '$COOKBOOK_NAME'" + separator "=" + spacer 1 +} + +# +# Writes a log line to the screen +# +# If specified, the first parameter is the number of empty lines to print +# before the log message. +# +# If specified, the second parameter is the number of empty lines to print +# before the log message. +# +log () { + if [[ $2 -gt 0 ]]; then + spacer $2 + fi + + echo " * $1" + + if [[ $3 -gt 0 ]]; then + spacer $3 + fi +} + +# +# Writes an error log line to the screen and exit with an error code. +# +error () { + spacer 2 + echo " -> $1" + spacer 2 + exit 1 +} + +# +# Write one or many empty lines to the screen. +# +spacer () { + if [ $1 ]; then + local spaces=$1 + else + local spaces=1 + fi + + for (( i=0; i<$spaces; i++ )) do + echo "" + done +} + +# +# noop +# +noop () { + return 0 +} + +# +# Write a separator to the screen. +# +# You can optionally specify the separator character. Default is '-'. +# +separator () { + if [ $1 ]; then + local char=$1 + else + local char='-' + fi + + local width=$(tput cols) + + for (( i=0; i < $width-2; i++ )) do + local output="$output$char" + done + echo $output +} + +# +# Update packages in package manager. +# +package_update () { + log "Updating package manager..." 0 1 + detect_package_manager + + if [ "$PACKAGE_MANAGER" == 'apt-get' ]; then + apt-get update -y + elif [ "$PACKAGE_MANAGER" == 'yum' ]; then + yum check-update -y + elif [ "$PACKAGE_MANAGER" == 'brew' ]; then + brew update + else + error "Unknown package manager: $PACKAGE_MANAGER" + fi + + if [ $? -ne 0 ]; then + error "An error occured while updating packages. Fail!" + else + spacer 2 + fi +} + +# +# Install a package through package manager +# +package () { + log "Installing package '$1'..." + detect_package_manager + + test_package_installed $1 > /dev/null 2>&1 + + if [ $? -eq 0 ]; then + log "Package '$1' is already installed. Skipping." + return 0 + fi + + if [ "$PACKAGE_MANAGER" == 'apt-get' ]; then + DEBIAN_FRONTEND=noninteractive apt-get install -y $1 + elif [ "$PACKAGE_MANAGER" == 'yum' ]; then + yum install -y $1 + elif [ "$PACKAGE_MANAGER" == 'brew' ]; then + brew install $1 + else + error "Unknown package manager: $PACKAGE_MANAGER" + fi + + if [ $? -ne 0 ]; then + error "An error occured while installing package '$1'. Fail!" + else + spacer 2 + fi +} + +# +# Determine if a package is installed or not. +# +# If package is installed, function will return 0. If not, it will return 1. +# +test_package_installed () { + detect_package_manager + + # When many packages are specified, skip test. + if [ $# -gt 1 ]; then + return 1 + fi + + if [ "$PACKAGE_MANAGER" == 'apt-get' ]; then + dpkg -l $1 + return $? + fi + + # Don't know how to detect if a package is installed with other package managers. + return 1 +} + +# +# Determine which package manager is in use on the system. +# +detect_package_manager () { + if [ "$PACKAGE_MANAGER" != "" ]; then + return 0 + fi + + if command_exist apt-get; then + PACKAGE_MANAGER='apt-get' + elif command_exist yum; then + PACKAGE_MANAGER='yum' + elif command_exist brew; then + PACKAGE_MANAGER='brew' + else + error "Could not find a package manager. Fail!" + fi + + log "Detected package manager: $PACKAGE_MANAGER" + return 0 +} + +# +# Determines if a command exist on the system. +# +command_exist () { + command -v "$1" > /dev/null 2>&1; +} + +# +# Copy a file from the assets folder to the specified location. +# +copy () { + local cookbook_assets_source="$COOKBOOK_ASSETS_PATH/$1" + local default_assets_source="$DEFAULT_ASSETS_PATH/$1" + local target=$2 + + if [ -f $cookbook_assets_source ]; then + log "Copying $cookbook_assets_source to $target..." + cp $cookbook_assets_source $target + elif [ -f $default_assets_source ]; then + log "Copying $default_assets_source to $target..." + cp $default_assets_source $target + else + error "Could not find '$1' to copy. Fail!" + fi +} + +# +# Add a user to the system. +# +add_user () { + local user=$1 + local pass=$2 + local args=$3 + + id $user > /dev/null 2>&1 + + if [ $? -eq 0 ]; then + log "User $user already exists. Skipped creation." + else + log "Adding user $user..." + [ "$pass" == "" ] && pass=generate_password + + if [[ "$args" != *nohome* ]]; then + /usr/sbin/useradd --password `openssl passwd -crypt $pass` --create-home $user + else + /usr/sbin/useradd --password `openssl passwd -crypt $pass` $user + fi + fi +} + +# +# Generate a random password. +# +generate_password() { + local l=$1 + [ "$l" == "" ] && l=8 + tr -dc A-Za-z0-9_ < /dev/urandom | head -c ${l} | xargs +} + +# +# Run a command as another user +# +run_as () { + local user=$1 + local cmd=$2 + log "Running command as '$user'..." + log "$cmd" + # sudo -u $user -H -s /bin/bash -c "$cmd" + # sudo -u $user -s /bin/bash -i "$cmd" + su -c "$cmd" -s /bin/bash $user +} + +# +# Add line to a file if line is not already present +# +add_line () { + local line=$1 + local file=$2 + grep "$line" $file > /dev/null 2>&1 + + if [ $? -ne 0 ]; then + log "Adding '$line' to '$file'..." + echo "$line" >> $file + else + log "'$line' already in '$file'. Skipping." + fi +} + +# +# Write a warning if user is not root. +# +warn_if_not_root () { + uid=`id -u` && [ "$uid" = "0" ] || + { echo "WARNING: You are NOT running this script as 'root'. You might want to consider that..."; } +} + +# +# Stops the execution of the script if user is not root. +# +fail_if_not_root () { + uid=`id -u` && [ "$uid" = "0" ] || + { echo "You must run this as 'root'. Exiting."; exit 1; } +} + +# +# Checks if a certain element has already been installed. +# +function is_installed () { + if [ $# -gt 1 ]; then + local args=$* + local name=${args// /-} + else + local name=$1 + fi + + if [[ -f ~/.shoestrap/installed/$name ]]; then + log "'$name' is already installed." + return 0 + else + log "'$name' is not installed." + return 1 + fi +} + +# +# Sets an element as installed. +# +function set_installed () { + if [ $# -gt 1 ]; then + local args=$* + local name=${args// /-} + else + local name=$1 + fi + + mkdir -p ~/.shoestrap/installed + touch ~/.shoestrap/installed/$name +} + diff --git a/helpers/initialize b/helpers/initialize new file mode 100644 index 0000000..82e0543 --- /dev/null +++ b/helpers/initialize @@ -0,0 +1,14 @@ +#!/bin/bash + +############################################################################## +# DO NOT MODIFY THIS FILE. Instead, modify 'helpers/custom'. +############################################################################## + +. helpers/default +. helpers/custom + +warn_if_not_root + +spacer 1; separator "=" +echo " BOOTSTRAPPING '$COOKBOOK_NAME'..." +separator "="; spacer 1 \ No newline at end of file diff --git a/my-cookbook b/my-cookbook new file mode 100755 index 0000000..dad17d3 --- /dev/null +++ b/my-cookbook @@ -0,0 +1,34 @@ +#!/bin/bash + +# Initialization - DO NOT REMOVE +. helpers/initialize + +############################################################## +### Customizations start here ################################ +############################################################## + +fail_if_not_root # Comment out if 'root' is not required. + +### Install packages +# package_update +# package 'git-core' +# package 'vim screen htop curl wget traceroute' +# package 'build-essential' +# package 'libjpeg-progs' +# package 'libmagickwand-dev imagemagick' +# package 'libsqlite3-dev' + +### Users +# add_user 'deploy' ; recipe 'setup_keys' 'deploy' ; recipe 'customize_bash' 'deploy' ; recipe 'add_sudoer' 'deploy' + +### Run recipes +# recipe 'secure_ssh' +# recipe 'rbenv' +# recipe 'ruby' '1.9.3-p125' +# recipe 'nginx' +# recipe 'memcached' '1.4.13' +# recipe 'mariadb' + +### Show the Finished banner +finished + diff --git a/recipes/default/.gitkeep b/recipes/default/.gitkeep new file mode 100644 index 0000000..e69de29