initial commit

This commit is contained in:
Carl Mercier 2012-03-21 16:05:28 -04:00
commit dfab18817e
7 changed files with 563 additions and 0 deletions

150
README.md Normal file
View File

@ -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.

0
assets/default/.gitkeep Normal file
View File

5
helpers/custom Normal file
View File

@ -0,0 +1,5 @@
#!/bin/bash
##############################################################################
# Add your custom helpers here. Remember, this is just Bash!
##############################################################################

360
helpers/default Normal file
View File

@ -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
}

14
helpers/initialize Normal file
View File

@ -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

34
my-cookbook Executable file
View File

@ -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

0
recipes/default/.gitkeep Normal file
View File