#!/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 # The Configuration instance used to configure the Subtrac environment def configuration @@configuration end def configuration=(configuration) @@configuration = configuration end def initialized? @initialized || false end def initialized=(initialized) @initialized ||= initialized end 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 subtrac_path @@subtrac_path ||= self.root ? File.join(self.root, "subtrac") : "subtrac" end def public_path=(path) @@public_path = path end def docs_dir @@docs_dir ||= File.join(@install_dir,@APP_CONFIG[:dirs][:docs]) end def docs_dir=(dir) @@docs_dir = dir end def svn_dir @@svn_dir ||= File.join(@install_dir,@APP_CONFIG[:dirs][:svn]) end def svn_dir=(dir) @@svn_dir = dir end def trac_dir @@trac_dir ||= File.join(@install_dir,@APP_CONFIG[:dirs][:trac]) end def trac_dir=(dir) @@trac_dir = dir end def temp_dir @@temp_dir ||= File.join(@install_dir,@APP_CONFIG[:dirs][:temp]) end def temp_dir=(dir) @@temp_dir = dir end def log_dir @@log_dir ||= File.join(@install_dir,@APP_CONFIG[:dirs][:log]) end def log_dir=(dir) @@log_dir = dir end def locations_dir @@locations_dir ||= File.join(@install_dir,@APP_CONFIG[:dirs][:locations]) end def locations_dir=(dir) @@locations_dir = dir 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 raw_config = File.read(file_path) yamlFile = YAML.load(raw_config) 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 puts "\n==== Installation options ====" say("Setting up default configuration...") @server_name = @APP_CONFIG[:server_name] @server_hostname = @APP_CONFIG[:server_hostname] @server_ip = @APP_CONFIG[:server_ip] @default_client = @APP_CONFIG[:default_client] @default_project = @APP_CONFIG[:default_project] @install_dir = File.expand_path(@APP_CONFIG[:installation_dir]) @conf_dir = @APP_CONFIG[:apache2_conf] @ldap_enable = @APP_CONFIG[:ldap][:enable] if (@ldap_enable) then @ldap_bind_dn = @APP_CONFIG[:ldap][:bind_dn] @ldap_bind_password = @APP_CONFIG[:ldap][:bind_password] @ldap_url = @APP_CONFIG[:ldap][:host] end end # Install def self.install(args,options) puts "\n==== Installing development server files ====" # check where we are installing change_install_dir = agree("The default installation directory is \"#{@install_dir}\". Would you like to change this? [Y/n]") @install_dir = ask("Where would you like to install this server?") if change_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 confirm_clean = agree("Doubly sure? I can't undo this....[Y/n]") if confirm_clean end # right lets install change_server_name = agree("The default server name is \"#{@server_name}\". Would you like to change this? [Y/n]") @server_name = ask("What would you like to call your new development server? ") if change_server_name create_environment_directories(confirm_clean) install_common_files() configure_admin_user() # TODO: Need to check for default client/project before creating virtual host # otherwise we need remove the rewrite rule create_virtual_host() confirm_default_client = agree("Do you want to create a default client project? [Y/n]") # create default project and client if confirm_default_client change_client = agree("The default client name is \"#{@default_client}\". Would you like to change this? [Y/n]") @default_client = ask("What client name would you like to use? ") if change_client change_project = agree("The default project name is \"#{@default_project}\". Would you like to change this? [Y/n]") @default_project = ask("What would you like to call your new project? ") if change_project create_project(@default_project,@default_client) end save_config() end private # Write the changes configuration to disk def self.save_config # save the things that might have changed @APP_CONFIG[:server_name] = @server_name @APP_CONFIG[:server_hostname] = @server_hostname @APP_CONFIG[:server_ip] = @server_ip @APP_CONFIG[:default_client] = @default_client @APP_CONFIG[:default_project] = @default_project @APP_CONFIG[:installation_dir] = @install_dir @APP_CONFIG[:apache2_conf] = @conf_dir @APP_CONFIG[:ldap][:bind_dn] = @ldap_bind_dn @APP_CONFIG[:ldap][:bind_password] = @ldap_bind_password @APP_CONFIG[:ldap][:host] = @ldap_url 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 tempalte: #{vhost_template}" new_vhost = File.join(@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"))) # this need to be replaced with a question/answer session #FileUtils.cp(,@install_dir) end def self.configure_admin_user puts "\n==== Configure admin user ====" # create admin user passwd_file = File.join(@install_dir, ".passwd") admin_user = ask("Pick an admin username: ") { |q| q.echo = true } `htpasswd -c #{passwd_file} #{admin_user}` @APP_CONFIG[:admin_user] = admin_user # ensure this guy is added to trac admin group @APP_CONFIG[:trac][:permissions][admin_user] = "admins" #FileUtils.chown_R('www-data', 'www-data', passwd_file, :verbose => true) if SUBTRAC_ENV != 'test' 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) # 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][:username]} --password #{@APP_CONFIG[:admin][:password]} \ file://#{svn_dir}/#{client_name}/trac_theme/trunk #{client_theme_dir}` if SUBTRAC_ENV != 'test' 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 change this to use a 'groups' yml file 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 => true) 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 # run trac upgrade say("Upgrading the trac installation...") `trac-admin #{project_trac_dir} upgrade` if SUBTRAC_ENV != 'test' end end