#!/usr/bin/env ruby # Copyright (c) 2009, Keith Salisbury (www.globalkeith.com) # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, # are permitted provided that the following conditions are met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # Neither the name of the original author nor the names of contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # You may need these... # # sudo apt-get install ruby rubygems # sudo gem sources -a http://gems.github.com # sudo gem install visionmedia-commander # sudo gem install yaml erb fileutils # require 'yaml' require 'erb' require 'fileutils' require 'subtrac/version' require 'subtrac/commands' module Subtrac SUBTRAC_ROOT = "#{File.dirname(__FILE__)}/" unless defined?(SUBTRAC_ROOT) SUBTRAC_ENV = (ENV['SUBTRAC_ENV'] || 'development').dup unless defined?(SUBTRAC_ENV) USER_CONFIG = 'config/user.yml' class << self # Filesystem directories def root Pathname.new(SUBTRAC_ROOT) if defined?(SUBTRAC_ROOT) end def public_path @public_path ||= self.root ? File.join(self.root, "public") : "public" end def public_path=(path) @public_path = path end def subtrac_path @subtrac_path ||= self.root ? File.join(self.root, "subtrac") : "subtrac" end def install_dir @install_dir ||= File.expand_path(@APP_CONFIG[:installation_dir]) end def install_dir=(name) puts "Setting new install_dir variable to #{name}" @install_dir = name end def apache_conf_dir @apache_conf_dir ||= @APP_CONFIG[:apache_conf_dir] end def apache_conf_dir=(name) @APP_CONFIG[:apache_conf_dir] = @@apache_conf_dir = name end def docs_dir @docs_dir ||= File.join(install_dir,@APP_CONFIG[:dirs][:docs]) end def svn_dir @svn_dir ||= File.join(install_dir,@APP_CONFIG[:dirs][:svn]) end def trac_dir @trac_dir ||= File.join(install_dir,@APP_CONFIG[:dirs][:trac]) end def temp_dir @temp_dir ||= File.join(install_dir,@APP_CONFIG[:dirs][:temp]) end def log_dir @log_dir ||= File.join(install_dir,@APP_CONFIG[:dirs][:log]) end def locations_dir @locations_dir ||= File.join(install_dir,@APP_CONFIG[:dirs][:locations]) end def server_name @server_name ||= @APP_CONFIG[:server_name] end def server_name=(name) @APP_CONFIG[:server_name] = @@server_name = name end def server_hostname @server_hostname ||= @APP_CONFIG[:server_hostname] end def server_hostname=(name) @APP_CONFIG[:server_hostname] = @@server_hostname = name end def default_client @default_client ||= @APP_CONFIG[:default_client] end def default_client=(name) @APP_CONFIG[:default_client] = @@default_client = name end def default_project @default_project ||= @APP_CONFIG[:default_project] end def default_project=(name) puts "Updating default_project to #{name}" @APP_CONFIG[:default_project] = @@default_project = name end # LDAP def enable_ldap @enable_ldap ||= @APP_CONFIG[:ldap][:enable] end def enable_ldap=(value) @APP_CONFIG[:ldap][:enable] = @@enable_ldap = value end def ldap_bind_dn @ldap_bind_dn ||= @APP_CONFIG[:ldap][:bind_dn] end def ldap_bind_dn=(value) @APP_CONFIG[:ldap][:bind_dn] = @@ldap_bind_dn = value end def ldap_bind_password @ldap_bind_password ||= @APP_CONFIG[:ldap][:bind_password] end def ldap_bind_password=(value) @APP_CONFIG[:ldap][:bind_password] = @@ldap_bind_password = value end def ldap_bind_host @ldap_bind_host ||= @APP_CONFIG[:ldap][:bind_host] end def ldap_bind_host=(value) @APP_CONFIG[:ldap][:bind_host] = @@ldap_bind_password = value end end # Loads the configuration YML file def self.load_config # TODO: We need to refactor this code so it will load the default configuration only if one has not been created puts "\n==== Loading configuration file ====" # load configuration file file_path = File.join(subtrac_path, USER_CONFIG) file_path = File.join(subtrac_path,"/config/config.yml") if not File.exists?(file_path) puts "Attempting to read config file: #{file_path}" begin yamlFile = YAML.load(File.read(file_path)) rescue Exception => e raise StandardError, "Config #{file_path} could not be loaded." end if yamlFile if yamlFile[SUBTRAC_ENV] @APP_CONFIG = yamlFile[SUBTRAC_ENV] else raise StandardError, "config/config.yml exists, but doesn't have a configuration for #{SUBTRAC_ENV}." end else raise StandardError, "config/config.yml does not exist." end say("Configuration loaded successfully...") end # Install def self.install(args,options) puts "\n==== Installing development server files ====" if options.defaults then overwrite = options.clean confirm_default_client = true else # check where we are installing confirm_or_replace_value(:install_dir,"install_dir") unless !File.directory?(install_dir) # Ask if the user agrees (yes or no) confirm_clean = agree("Err, it seems there's some stuff in there. You sure you want me to overwrite? [Y/n]") if options.clean overwrite = agree("Doubly sure? I can't undo this....[Y/n]") if confirm_clean end # confirm server confirm_or_replace_value(:server_name,"server_name") # ask for hostname confirm_or_replace_value(:server_hostname,"server_hostname") # default client/project name confirm_or_replace_value(:default_client,"default_client") confirm_or_replace_value(:default_project,"default_project") end say("Ok we're about to install now, these are the options you have chosen: installation directory: #{install_dir} overwrite: #{overwrite} server name: #{server_name} server hostname: #{server_hostname} default client: #{default_client} default project: #{default_project}") confirm = agree("Is this ok? [y/n]") exit 0 if !confirm create_environment_directories(overwrite) create_virtual_host() install_common_files() install_default_theme() configure_admin_user() create_project(default_project,default_client) save_config() end private # confirms the current value with the user or accepts a new value if required def self.confirm_or_replace_value(prop, name) method(prop).call # initialise the property current_value = instance_variable_get("@#{prop}") accept_default = agree("The default value for #{prop} is \"#{current_value}\". Is this ok? [y/n]") if !accept_default then answer = ask("What would you like to change it to?") send("#{prop}=", answer) #instance_variable_set(prop, answer) end end # Write the changes configuration to disk def self.save_config # save the things that might have changed file_path = File.join(subtrac_path, USER_CONFIG) open(file_path, 'w') {|f| YAML.dump({SUBTRAC_ENV => @APP_CONFIG}, f)} end # creates a directory if it does not exist def self.create_if_missing(*names) names.each do |name| unless File.directory?(name) puts "Creating directory called #{names}..." FileUtils.mkdir_p(name) end end end # publishes an erb template def self.parse_template(infile,outfile,binding) file = File.open(outfile, 'w+') if file template = ERB.new(IO.read(infile)) if template file.syswrite(template.result(binding)) else raise "Could not read template. file #{infile}" end else raise "Unable to open file for writing. file #{outfile}" end end # creates a new virtual host and reloads apache and enables the new virtual host def self.create_virtual_host puts "\n==== Creating new virtual host ====" vhost_template = File.join(subtrac_path, @APP_CONFIG[:templates][:virtual_host]) puts "group apache virtual host template: #{vhost_template}" # TODO: Copy this to the conf folder and map a file with an Include to it. # this way the vhost file will get backed up as well. new_vhost = File.join(apache_conf_dir,server_hostname) puts "group apache file: #{new_vhost}" parse_template(vhost_template,new_vhost,binding) if SUBTRAC_ENV != 'test' # reload apache configuration `/etc/init.d/apache2 force-reload` if SUBTRAC_ENV != 'test' `a2ensite #{server_hostname}` if SUBTRAC_ENV != 'test' end def self.install_common_files puts "\n==== Installing common files ====" # TODO: implement a mask for .svn folders # TODO: refactor /common to the app config FileUtils.cp_r(Dir.glob(File.join(subtrac_path, "common/.")),docs_dir) FileUtils.cp_r(Dir.glob(File.join(subtrac_path, "shared/.")),create_if_missing(File.join(trac_dir, ".shared"))) end def self.configure_admin_user puts "\n==== Configure admin user ====" # create admin user passwd_file = File.join(install_dir, ".passwd") admin_user = ask("New admin user: ") { |q| q.echo = true } admin_pass = ask("New password: ") { |q| q.echo = "*" } admin_pass_confirm = ask("Re-type new password: ") { |q| q.echo = "*" } if (admin_pass == admin_pass_confirm) then `htpasswd -c -b #{passwd_file} #{admin_user} #{admin_pass}` @APP_CONFIG[:admin_user] = admin_user @APP_CONFIG[:admin_pass] = admin_pass # ensure this guy is added to trac admin group @APP_CONFIG[:trac][:permissions][admin_user] = "admins" else # call the password chooser again end end def self.create_environment_directories(overwrite=false) puts "\n==== Creating new environment directories ====" FileUtils.rm_rf install_dir if overwrite create_if_missing install_dir # create the environment directories @APP_CONFIG[:dirs].each do |key, value| dir = File.join(install_dir,value) create_if_missing dir end end def self.create_client(name) puts "\n==== Create a new client called #{name} ====" client_name = name.downcase # create apache configuration puts "subtrac_path: #{subtrac_path}" puts "templates_location: #{@APP_CONFIG[:templates][:location]}" location_template = File.join(subtrac_path, @APP_CONFIG[:templates][:location]) puts "location_template: #{location_template}" location_conf = File.join(locations_dir,"#{client_name}.conf") puts "location_conf: #{location_conf}" parse_template(location_template,location_conf,binding) `/etc/init.d/apache2 force-reload` if SUBTRAC_ENV != 'test' # create svn+trac directory create_if_missing File.join(svn_dir,client_name) create_if_missing File.join(trac_dir,client_name) end def self.install_default_theme puts "\n==== Create a new client called #{name} ====" # create a project for this clients trac theme #create_project("trac_theme", client_name,@APP_CONFIG[:default_theme_template]) # check the theme project out #client_theme_dir = File.join(trac_dir,client_name,".theme") #create_if_missing(client_theme_dir) #FileUtils.chown_R('www-data', 'www-data', client_theme_dir, :verbose => true) if SUBTRAC_ENV != 'test' #puts "Attempting checkout of theme project..." #`sudo svn co --username #{@APP_CONFIG[:admin_user]} --password #{@APP_CONFIG[:admin_pass]} \ # file://#{svn_dir}/#{client_name}/trac_theme/trunk #{client_theme_dir}` if SUBTRAC_ENV != 'test' # TODO: Need to find a way to test before trying template_dir = File.join(subtrac_path, "templates","trac","themes","subtractheme") say(`sudo easy_install #{template_dir}`) end def self.create_project(project, client, project_type=@APP_CONFIG[:default_project_template]) puts "\n==== Create a new project called #{project} for #{client} ====" client_name = client.downcase project_name = project.downcase # create client directory if needed create_client(client_name) if (!File.directory? File.join(svn_dir,client_name)) project_svn_dir = File.join(svn_dir,client_name,project_name) project_trac_dir = File.join(trac_dir,client_name,project_name) # TODO: Need to handle this exception... if (File.directory? project_svn_dir) then raise StandardError, "A project called #{project} already exists in the #{client} repository. Would you like to replace it?" end # create new project directories say("Create project directories...") create_if_missing project_svn_dir create_if_missing project_trac_dir project_template = File.join(subtrac_path, @APP_CONFIG[:templates][:projects],project_type) # copy template svn project to a temp folder, then svn import it into the repo say("Create temporary project directory and copy template files...") svn_template_dir = File.join(project_template,"svn") project_temp_dir = File.join(temp_dir,project_name) FileUtils.cp_r(svn_template_dir,project_temp_dir) # create a new subversion repository say("Creating a new subversion repository...") `svnadmin create #{project_svn_dir}` if SUBTRAC_ENV != 'test' # import into svn say("Importing temporary project into the new subversion repository...") `svn import #{project_temp_dir} file:///#{project_svn_dir} --message "initial import"` if SUBTRAC_ENV != 'test' # delete the temporary directory FileUtils.rm_r(project_temp_dir, :force => true) # create a new trac site say("Creating a new trac site...") result = `trac-admin #{project_trac_dir} initenv #{project_name} sqlite:#{project_trac_dir}/db/trac.db svn #{project_svn_dir}` if SUBTRAC_ENV != 'test' FileUtils.chown_R('www-data', 'www-data', project_trac_dir, :verbose => false) if SUBTRAC_ENV != 'test' FileUtils.mkdir_p("#{project_trac_dir}/conf") if SUBTRAC_ENV == 'test' # fake the folder for tests say("Installing trac configuration...") # install shared trac.ini trac_ini_template = File.join(subtrac_path, @APP_CONFIG[:templates][:trac]) parse_template(trac_ini_template,File.join(project_trac_dir,"/conf/trac.ini"),binding) # remove custom templates directory so trac uses the shared location (while we wait for trac patch) #FileUtils.rm_rf("#{project_trac_dir}/templates") say("Setting up default trac permissions...") # set up trac permissions @APP_CONFIG[:trac][:permissions].each do |key, value| `sudo trac-admin #{project_trac_dir} permission add #{key} #{value}` if SUBTRAC_ENV != 'test' end say("Adding default trac wiki pages...") # loop through the directory and import all pages Dir.foreach("#{project_template}/trac/wiki/.") do |file| # do something with the file here unless ['.', '..','.svn'].include? file temp_file = File.join(temp_dir,file) puts = binding parse_template(File.join(project_template,"trac/wiki",file), temp_file, binding) `trac-admin #{project_trac_dir} wiki import #{file} #{temp_file}` if SUBTRAC_ENV != 'test' FileUtils.rm(temp_file) end end # chown the whole directory `sudo chown -R www-data:www-data #{install_dir}` #FileUtils.chown_R('www-data', 'www-data', File.join(install_dir,"*"), :verbose => true) if SUBTRAC_ENV != 'test' # run trac upgrade say("Upgrading the trac installation...") `trac-admin #{project_trac_dir} upgrade` if SUBTRAC_ENV != 'test' end end