mirror of
https://github.com/ViViDboarder/shoestrap.git
synced 2024-12-03 17:56:47 +00:00
initial commit
This commit is contained in:
commit
dfab18817e
150
README.md
Normal file
150
README.md
Normal 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
0
assets/default/.gitkeep
Normal file
5
helpers/custom
Normal file
5
helpers/custom
Normal file
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
##############################################################################
|
||||
# Add your custom helpers here. Remember, this is just Bash!
|
||||
##############################################################################
|
360
helpers/default
Normal file
360
helpers/default
Normal 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
14
helpers/initialize
Normal 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
34
my-cookbook
Executable 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
0
recipes/default/.gitkeep
Normal file
Loading…
Reference in New Issue
Block a user