Script to Quickly Setup WebApp Environment and Domain
Sat, Oct 11, 2008
Automation, Code, Linux, Ruby
Just sharing a script I wrote to quickly deploy WordPress (and eventually a few other webapps) sites, which somebody might find useful. This uses Linode‘s API* to add the domain name to the DNS server along with some subdomains. If you’re using another server, (Slicehost, your own, etc), you can alter the dns class to use that API, or just ignore the DNS stuff completely; Its optional.
This will be updated periodically as I refactor and add support for more apps (notably Joomla and Clipshare – though this would violate their terms unless you have the unlimited license). This was written primarily because I couldn’t stand setting up another vhost and WordPress installation. There are plenty of existing deployers but I plan on adding very specific features and tweaking this for in-house work. I also wanted to try Rio (Ruby-IO). GPL license. Go nuts.
* As of 10/11, the apicore.rb file on the site has some syntactic errors in the domainResourceSave method. I sent an email out to the author about it. Problems aren’t major. You can get my apicore.rb here.
This won’t run unless you create the appropriate folder structure in /etc/mksite/. I’ll get going on this in a bit. See the code below:
#!/usr/bin/env ruby
########################################################
# Isam M.
# http://biodegradablegeek.com
# MKSITE.RB (0.2) Last updated Oct 19th, 2008
#
# mksite makes it quicker to setup sites and web
# apps by doing most of the tedious work.
#
#
# UNSTABLE
# Run it in your imagination,
# not on your system!
#
#
########################################################
require 'rubygems'
require 'rio'
require 'yaml'
require 'mysql'
require 'highline/import'
HighLine.track_eof = false
begin
require 'apicore'
rescue LoadError
puts "NOTICE: Unable to load apicore.rb - domains will not be added automatically"
end
Apache_sites = '/etc/apache2/sites-available/'
Subdomains = ['', 'www', 'mail', 'blog', "dev#{(rand*10).floor}"]
Testing = false
Homedir = ENV['HOME']
Username = ENV['USER']
Applications = [:Skeleton, :Clipshare, :Joomla, :PHPMotion, 'PmWiki (N/A)', :Wordpress]
$config = nil
$log = nil
def say(txt)
super txt
# $log << txt if $log
end
# flash('message', :notice || :error) to output msg
def flash(msg, message_type = :notice)
if message_type.eql? :notice
say("INFORMATION: #{msg}")
elsif message_type.eql? :emphasize
say("***************************************************")
say("NOTICE: #{msg}n")
say("***************************************************")
elsif message_type.eql? :error
STDERR.puts("FATAL ERROR: #{msg}n")
end
end
# DZone 2111
def genAlpha(size=64)
s=''
size.times {
s << (i = Kernel.rand(62); i += ((i < 10) ? 48 : ((i < 36) ? 55 : 61 ))).chr
}
s
end
if defined? ApiCore
class DNSAPI < ApiCore
# This depends on the Linode API Ruby bindings (apicore.rb)
def initialize(key, debug=false, batching=false)
super
@batching=false
end
# Add domain. Return ID on success, nil on failure
def addDomain domain
return nil if !domain
params = {}
params[:DomainID] = 0
params[:Domain] = domain
params[:Type] = 'master'
params[:Status] = 1
params[:SOA_Email] = getVal('email') || ask("Enter SOA email for the domain: ")
domainSave params
end
def addDomainResource(domain, resource, target, record_type = 'A')
return nil if (did = getDomainIdByName domain).nil?
params = {}
params[:ResourceID] = 0
params[:DomainID] = did
params[:Name] = resource
params[:Type] = record_type
params[:Target] = target
domainResourceSave params
end
def getDomainIdByName domain
domainList.find do |dom|
return dom["DOMAINID"] if dom["DOMAIN"].downcase == domain.downcase
end
end
end
else
flash('ApiCore not loaded. Skipping DNS stuff', :notice)
end
class App
class << self; attr_reader :message, :name, :version, :description, :vhost; end
@message = nil
@name = nil
@version = nil
@description = 'Just Another Web App'
@vhost = 'generic'
def initialize(rootdir, domain, db)
flash("Initializing application (dir=#{rootdir}, domain=#{domain})")
@rootdir = (rootdir[-1].chr.eql? '/') ? rootdir.chop : rootdir
@domain = domain
@db = db # hash of database info
# System stuff
#if Testing
#@templates = '/home/kiwi/Code/mksite/templates'
#@configs = '/home/kiwi/Code/mksite/configs'
#@vhosts = '/home/kiwi/Code/mksite/vhosts'
#else
@templates = '/etc/mksite/templates'
@configs = '/etc/mksite/configs'
@vhosts = '/etc/mksite/vhosts'
#end
end
# This generally does not need to be overriden.
# It does 'generic shit' like creating the rootdir
# and setting permissions
def envSetup
if !rio(@rootdir).exist?
flash("Creating directory #{@rootdir}")
rio(@rootdir).mkpath
end
rio(@rootdir).chdir do |root|
# Copy the generic public/private/log apache
# structure to rootdir
flash("Changing working dir to #{@rootdir}")
flash("Working inside '#{root.to_s}'")
rio(@templates,'skeleton.www').each { |df|
# Overwrite existing files? .. yes.
#while rio(root, df).exist? do
df > root
#flash("Copied #{df.to_s} to #{root.to_s}")
}
# Set permissions (a+w on logs, etc)
flash('Setting permissions...')
flash('666 ./log/*.log')
rio('./log/access.log').chmod(00666)
rio('./log/error.log').chmod(00666)
flash('700 ./private')
rio('./private/').chmod(00700)
end
end
def setup
# envSetup()
# databaseSetup()
raise 'OVERRIDE ME'
#flash("setup() template function invoked. 'OVERRIDE ME'", :log)
end
def databaseSetup
# Create database if it doesn't already exist
# This usually doesn't need to be overriden
#Mysql.server_connect(@db['name'])
# flash('Checking database connection... ')
begin
dbo = Mysql.real_connect(@db['host'], @db['user'], @db['pass'])
flash("Creating database '#{@db['name']}'");
res = dbo.query("CREATE DATABASE IF NOT EXISTS #{@db['name']};")
flash("Database server returned #{res}") if res
rescue Mysql::Error => err
flash('Unable to connect to access/create database', :error)
flash("Error returned (#{err.errno}) = '#{err.error}'", :error)
exit 1
ensure
dbo.close if dbo
end
end
def dnsSetup
return if !defined? ApiCore
# Setup DNS - currently uses Linode API
flash("Setting up DNS for '#{@domain}'")
flash("This requires a Linode API keynLogin to linode.com and find it under 'My Profile'", :emphasize)
dns = nil
api_key = getVal('apikey')
loop do
api_key = ask('Paste your Linode API key (or 'skip'): ') if api_key.nil? || api_key.empty?
break if api_key.downcase.eql? 'skip'
# Check API key
begin
dns = DNSAPI.new api_key
dns.domainList
break
rescue RuntimeError
flash("API key invalid (or service down?). Learn to paste and try again.n")
api_key = nil
end
end
unless api_key.nil? or api_key.downcase.eql? 'skip'
flash("Adding master domain '#{@domain}'")
begin
begin
dns.addDomain(@domain)
rescue RuntimeError
flash('Unable to add domain (exists already?). Attempting to add subdomains...', :error)
end
server = getVal('server') ||
ask("Enter IP subdomains should point to (or 'skip'): ") { |q| q.default = 'skip' }
unless server.downcase.eql? 'skip'
Subdomains.each do |sub|
flash("Adding subdomain '#{sub}.#{@domain}' (points to #{server})")
flash('Could not add subdomain', :error) if !dns.addDomainResource(@domain, sub, server)
end
end
rescue
raise
flash("Unable to add domain/subdomain. Skipping", :error)
end
end
end
def serverSetup
# Set Apache vhost
vhost_file = (defined? self.class.vhost) ? self.class.vhost : 'generic'
flash("Generating vhost file (#{@vhosts}/#{vhost_file}) for Apache 2.x")
vhost = ''
email = getVal('email') || ask("Enter a valid email for tech support: ")
rio(@vhosts, vhost_file) > vhost
vhost.gsub!('_MKS_DOMAIN_', @domain)
vhost.gsub!('_MKS_EMAIL_', email)
vhost.gsub!('_MKS_ROOT_', "#{@rootdir}")
vhost.gsub!('_MKS_PUBLIC_', "#{@rootdir}/public")
#rio(Testing ? '/tmp/' : Apache_sites, @domain).puts(vhost)
rio('/tmp/', @domain).puts(vhost)
# Your enemies should not read this
rio('/tmp/', @domain).chmod(00600)
flash("vhost file has been generated as /tmp/#{@domain}n
It is YOUR responsibility to move this to #{Apache_sites}n
Site will not work until you 'a2ensite && apache2ctl restart'", :emphasize)
end
def postInstall()
flash("Finished!", :emphasize)
end
end
class Skeleton < App
@name = 'Skeleton'
@description = 'Generic WWW directory structure'
@message = ''
@vhost = 'generic'
def setup
self.envSetup()
self.dnsSetup()
self.serverSetup()
end
end
class WordPress < App
@name = 'Wordpress'
@version = '2.6.2'
@description = 'A popular blogging platform'
@message = 'Trying to make monies on the Internets?'
@vhost = 'generic'
def setup
# This sets up the initial environment / permissions
self.envSetup()
# Copy the wordpress skeleton directory to the new dir
wproot = rio(@rootdir,'public')
flash("Wordpress root will be #{wproot}")
flash('Copying WordPress data over... (may take awhile)')
rio(@templates,'wordpress').each { |df| df > wproot }
rio(wproot).chdir do
rio('wp-config-sample.php').rm()
# Generate and output wp config
flash('Generating wp-config.php based on your DB settings...')
wpcfg = rio(@configs, 'wordpress.cfg')
if !wpcfg.exist? || !wpcfg.readable?
flash("Wordpress config template missing or unreadable, quitting", :error)
exit 2
end
# Copy the config into a string, do things with it and then write it to disk
config = ''
rio(@configs, 'wordpress.cfg') > config
flash("Warning: config file '#{@configs}/wordpress.cfg' is empty") if config.empty?
config['_MKS_DB_HOST_'] = @db['host']
config['_MKS_DB_USER_'] = @db['user']
config['_MKS_DB_PASS_'] = @db['pass']
config['_MKS_DB_NAME_'] = @db['name']
config.gsub!('_MKS_SECRET_', genAlpha())
flash("Writing #{wpcfg} data")
rio('wp-config.php').w!.puts(config)
end
# Setup the database
self.databaseSetup()
# Add DNS info
self.dnsSetup()
# Setup the server/vhost
self.serverSetup()
end
end
class Clipshare < App
def setup
raise 'Clipshare support is currently not available. sowwie'
end
end
class Joomla < App
@name = 'Joomla'
@version = '1.5.7'
@description = 'A widely used Content Management System'
@message = ''
@vhost = 'generic'
def setup
# This sets up the initial environment / permissions
self.envSetup()
# Copy the wordpress skeleton directory to the new dir
flash('Copying Joomla data over... (may take awhile)')
jooroot = rio(@rootdir,'public')
flash("Joomla root will be #{jooroot}")
rio(@templates,'joomla-1.5.7').each { |df| df > jooroot }
rio(jooroot).chdir do
flash('Removing stock Joomla config...')
rio('configuration.php-dist').rm()
# Generate and output joomla config
flash('Generating configuration.php based on provided settings...')
joocfg = rio(@configs, 'joomla.cfg')
if !joocfg.exist? || !joocfg.readable?
flash("Joomla config template missing or unreadable, quitting", :error)
exit 2
end
# Copy the config into a string, do things with it and then write it to disk
config = ''
rio(@configs, 'joomla.cfg') > config
flash("Warning: config file '#{@configs}/joomla.cfg' is empty") if config.empty?
config['_MKS_DB_HOST_'] = @db['host']
config['_MKS_DB_USER_'] = @db['user']
config['_MKS_DB_PASS_'] = @db['pass']
config['_MKS_DB_NAME_'] = @db['name']
config.gsub!('_MKS_DOMAIN_', @domain)
config.gsub!('_MKS_SECRET_', genAlpha())
flash("Writing ./configuration.php")
rio('configuration.php').w!.puts(config)
flash("Making configuration.php world writable (0666)")
rio('configuration.php').chmod(0666)
end
# Setup the database
self.databaseSetup()
# Add DNS info
self.dnsSetup()
# Setup the server/vhost
self.serverSetup()
end
def postInstall()
flash('Joomla is ready to be setup using the web interface', :emphasize)
flash("Go to #{@domain} where the Joomla! web based installer will
guide you through the rest of the installation")
flash("Here's the database information:n
USERNAME: #{@db['user']}n
DB HOST : #{@db['host']}n
DB NAME : #{@db['name']}")
flash("nAdmin panel is located @ #{@domain}/administrator ")
flash("You can log into Admin using the username 'admin' along with the
password that was generated or you chose during the web based install.")
super
end
end
class PHPMotion < App
def setup
raise 'PHPMotion support is currently not available. sowwie'
end
end
class Log
def initialize(filename="/tmp/#{Username}_mksite.log")
@log = rio(filename)
n=0; @log = rio("#{filename}.#{n+=1}") while @log.exist?
@log.puts("# GENERATED BY MKS - BEGAN @ #{Time.now.to_i}")
@log.chmod(00600)
flash("Log file generated (important): #{@log.to_s} (no worries, set to 600)", :emphasize)
return @log
end
# append to log
def <<(data)
#super <<(data)
@log.puts(data)
@log.puts("n")
#@log << data << "n"
end
end
def loadConfig()
cfgpath = "#{ENV['HOME']}/.mksite"
return YAML.load(rio(cfgpath).read()) if rio(cfgpath).exist?
nil
end
def getVal(key, default=nil)
return $config[key] if $config and defined? $config[key]
default
end
def main
exit 1 if 'root'==ENV['USER']
if ($config = loadConfig())
flash('~/.mksite config loaded')
else
flash('~/.mksite not found. It's fine, I'll annoy you with questions.')
end
puts $config.inspect if Testing
flash('Press ^C (CTRL+C) at any time quit', :emphasize)
# Let's ask neutral questions about the new site.
if Testing
rootdir = '/tmp/sandbox9/'
domain = 'domain.cxm'
else
domain = ask('Enter site's domain name (no http:// or www): ') #{ |d| d.validate = !! /^www.|^http:/// }
rootdir = ask('Root site directory (leave blank for default): ') { |q|
q.default = "/home/#{Username}/www/#{domain}/"
q.validate = /^/home/#{Username}//
}
end
flash("Domain has been set to "#{domain}"")
#$log = Log.new("/tmp/mks_#{domain}.log")
flash("Site will reside in "#{rootdir}", and the index/script")
flash(" files (index.php, .htaccess etc) will go in "#{rootdir}/public/"nn")
# Ask the user what app she wants to install and specific questions about that app
app = nil
choose do |menu|
menu.prompt = 'Choose the software for your new site: '
Applications.each do |app|
menu.choice app do |a|
app = a
end
end
end
flash("You chose #{app}. #{Kernel.const_get(app).message}n")
# Fetch DB info if this app needs a database
# XXX Should just have a db flag in the App's class
db = nil
unless app.eql? :Skeleton
flash('This app requires a database (only MySQL supported)')
flash('It will be created if it doesn't exist')
db = {}
db['host'] = 'localhost'
loop do
if Testing
db['user'] = 'kiwi'
db['pass'] = ''
db['name'] = 'kiwi_sandbox9'
else
db['user'] = getVal('db_user') || ask('Enter MySQL username: ')
db['pass'] = getVal('db_pass') || ask('Enter MySQL password: ') { |q| q.echo = '*'}
db['name'] = ask('Enter database name (should begin w/ your 'username_')') { |dbn| dbn.validate = /^#{Username}_/}
puts db.inspect
end
flash("Please double check: mysql://#{db['user']}:#{'*' * (db['pass']).size}@#{db['host']}/#{db['name']}")
break if Testing || agree('Is this correct?')
flash("Please re-enter database infon")
end
end
# Check permissions and database login
flash('Doing a preliminary check', :emphasize)
flash('Checking installation environment')
# Quit if the directory exists and the user does not want to go ahead
unless Testing
exit 1 if rio(rootdir).exist? && !agree("Directory '#{rootdir}' exists! Continue? (Y/N)")
end
# Is the parent directory available and writable?
parentdir = rio(rootdir).dirname
unless parentdir.exist? && parentdir.writable?
flash("Directory '#{parentdir.to_s}' either doesn't exist or is not writable. Check permissions", :error)
exit 1
end
unless db.nil? || false==getVal('db_confirm')
# Check DB connection - but this does not check if user has any privileges
flash('Checking database connection')
begin
dbo = Mysql.real_connect(db['host'], db['user'], db['pass'])
flash("Successfully connected to '#{dbo.get_server_info}'")
rescue Mysql::Error => err
flash('Unable to connect to database server', :error)
flash("Error returned (#{err.errno}) = '#{err.error}'", :error)
exit 1
ensure
dbo.close if dbo
end
end
# Begin installation
flash("Word on the server racks is... you're good to go", :emphasize)
klass = Kernel.const_get(app)
webapp = klass.new(rootdir, domain, db)
webapp.setup()
webapp.postInstall()
end
main()
Download the ASCII mksite.rb file here.
Why not subscribe to the feed?. If you’re on a mobile device I suggest Viigo
Tags: Automation, code, deployment, joomla, Linux, phpmotion, rio, Ruby, scripting, Scripts, warez
Leave a Reply