commit
dfab18817e
7 changed files with 563 additions and 0 deletions
@ -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,0 +1,5 @@
|
||||
#!/bin/bash |
||||
|
||||
############################################################################## |
||||
# Add your custom helpers here. Remember, this is just Bash! |
||||
############################################################################## |
@ -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 |
||||
} |
||||
|
@ -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 |
@ -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 |
||||
|
Loading…
Reference in new issue