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