Skip to content
Browse files

initial commit

  • Loading branch information...
0 parents commit dfab18817e941af32dc66295f64fb903ade8e054 @cmer committed
Showing with 563 additions and 0 deletions.
  1. +150 −0 README.md
  2. 0 assets/default/.gitkeep
  3. +5 −0 helpers/custom
  4. +360 −0 helpers/default
  5. +14 −0 helpers/initialize
  6. +34 −0 my-cookbook
  7. 0 recipes/default/.gitkeep
150 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.
0 assets/default/.gitkeep
No changes.
5 helpers/custom
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+##############################################################################
+# Add your custom helpers here. Remember, this is just Bash!
+##############################################################################
360 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
+}
+
14 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
34 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
+
0 recipes/default/.gitkeep
No changes.

0 comments on commit dfab188

Please sign in to comment.
Something went wrong with that request. Please try again.