Pull to refresh

Лень — двигатель прогресса или мой вариант создания окружения для веб-разработки на основе VirtualBox

Virtualization *
Sandbox


Всем веб-разработчикам так или иначе нужен какой-то сервер для разработки своих веб-приложений. Кто-то использует «Денвер», кто-то OpenServer, более продвинутые берут виртуальный сервер (VPS), а еще более продвинутые используют Vagrant, а кто-то просто ленивый. Под катом я расскажу, как разворачиваю веб-приложение для разработки с помощью VirtualBox, баша и кой-каких костылей. Для тех, кто ленив и не хочет смотреть под кат: описан один баш-сценарий, который монтирует расшареные папки в гостевую ОС и полу-демон, который запускает первый сценарий после запуска перед остановкой системы и реализует интерфейс демона.

В качестве гостевой операционной системы был выбран дистрибутив линукса CentOS 6.5, в качестве веб-сервера — Apache 2.2.15.

Сразу оговорюсь: описание установки и настройки LAMP-сервера описывать не буду ибо манов в интернетах полно.

Самый первый вариант скрипта выглядел примерно так:

#!/bin/sh
mount -t vboxsf $1 $2


Постепенно он разросся в следующий скрипт:
Скрипт первый - рабочая лошадка - /root/scripts/vbox-sf.sh
#!/bin/sh

# Author:	Dmitry Vapelnik
# Email:	dvapelnik@gmail.com

# Местонахождение лог-файла
logfile='/var/log/vbox-sf.log'
# Место, куда мы будем маунтить папки
mountPrefix='/var/www/html/';
# Хостнейм-суффикс
hn='.'`hostname`
# Ищем все папки, которые были подмаунчены VirtualBox
sharedFolders=`df | egrep "\/media\/sf_\$2[^ ]*" -o | sed -e 's/\/media\/sf_//'`
#================ LOG ==========================================================#
function log {
    echo [`date +"%F %T"`] $1 $2 >> $logfile
}
#================ MOUNTING =====================================================#
function mountFn {
    echo "Mounting....";
    for f in $sharedFolders; do
	mountPath=$mountPrefix$f$hn
	if cat /proc/mounts | grep vbox | grep $mountPath &> /dev/null; then
	    echo Already mounted. Continue..;
	else
	    rm -rf $mountPath 2> /dev/null;
	    mkdir -p $mountPath;
	    chown apache:apache $mountPath;
	    if mount -t vboxsf $f $mountPath -o umask=0022,uid=apache,gid=apache; then
		echo Mounted $f
		log mounted $mountPath
		# Папки для сохранения результирующих файлов профилиования и 
		# трейсов для XDebug
		mkdir -p /var/www/html/$f$hn/xd_profile_$f$hn
		mkdir -p /var/www/html/$f$hn/xd_trace_$f$hn
		if [ -f $mountPath'/httpd.conf' ]; then
		    # Формирование конфига httpd из заранее подготовленного шаблона
		    cat $mountPath'/httpd.conf' | sed -e "s/<%domain%>/$f$hn/g" > /etc/httpd/conf/sf/$f$hn.conf
		    if [ -f $mountPath'/aftermount.sh' ]; then
			# Запуск заранее подготовленного скрипта, который будет
			# запускаться сразу после монтирования папки
			bash $mountPath'/aftermount.sh' $mountPath;
		    fi
		fi
	    fi
	fi
    done;
    # Перезапускаем веб-сервер
    service httpd restart
}
#================ UNMOUNTING ===================================================#
function umountFn {
    echo "Unmounting..."
    # Останавливаем веб-сервер
    service httpd stop
    for f in $sharedFolders; do
	mountPath=$mountPrefix$f$hn
	# Удаляем логи веб-сервера
	find $mountPath -type f -name httpd_"$f""$hn"_*.log -exec rm -f {} \;
	# Удаляем папки профилирования и трейсов
	rm -rf $mountPath/xd_profile_"$f""$hn"
	rm -rf $mountPath/xd_trace_"$f""$hn"
	# Выполняем заранее подготовленный стенарий, который должен
	# быть выполненым перед демонтированием папки
	if [ -f $mountPath'/beforeumount.sh' ]; then
	    bash $mountPath'/beforeumount.sh' $mountPath;
	fi
	# Демонтируем папку
	umount $mountPath
	# Подчищаем за собой
	if [[ $? -eq 0  ]]; then
	    rm -rf $mountPath 2> /dev/null
	    rm -f /etc/httpd/conf/sf/$f$hn.conf 2> /dev/null
	    echo "Unmounted and removed $f"
	    log umounted $mountPath
	else
	    echo "Not unmounted"
	fi
    done;
    # Запускаем веб-сервер
    service httpd start
}
#================ STATUS =======================================================#
function statusFn {
    com=0
    for f in $sharedFolders; do
	mountPath=$mountPrefix$f$hn
	if df | grep $mountPath &> /dev/null; then
	    com=`expr $com + 1`;
	    if [ $com -eq 1 ]; then
		echo List of mounted resources:
	    fi
	    df | grep $mountPath | egrep -o '\/.+$'
	fi
    done
    if [ $com -eq 0  ]; then
	echo No shared storage mounted
    fi
}
#===============================================================================#
if [ "$1" == "mount"  ]; then
    mountFn;
    exit 0
elif [ "$1" == "umount" ]; then
    umountFn;
    exit 0
elif [ "$1" == "status" ]; then
    statusFn;
    exit 0
else
    cat << EOF
    No arguments supplied
    -----------------------------------------------------------------------------
    Usage:
    -----------------------------------------------------------------------------
    Using with single argument one of:
	mount	: for mounting all shared folders under /var/www/html directory
	umount	: for unmounting all shared folders under /var/www/html directory
	status	: for checking mount status
    -----------------------------------------------------------------------------
    Using with two argument for mounting or unmounting single folder
    Example:
	vbox-sf mount foo	: will mount shared folder /media/sf_foo into /var/www/html/foo.domain.com
	vbox-sf umount foo	: will umount shared folder /media/sf_foo from /var/www/html/foo.domain.com
EOF
    exit 1
fi

exit 0


Что необходимо и как работает


  1. Необходимо установить на гостевеой машине веб-сервер (я выбрал apache).
  2. Для корректного монтирования расшаренных папок на гостевой ОС необходимо установить Guest Additions.
  3. Папку, которую мы монтируем следует назвать коротко, но уникально — это название будет поддоменом домена нашего сервера.
  4. В корень расшариваемой папки необходимо положить обязательно шаблон конфига виртуального хоста для веб-сервера.
    Приблизительный конфиг виртуального хоста для веб-сервера
    <VirtualHost *:80>
    	DocumentRoot /var/www/html/<%domain%>
    	ServerName <%domain%>
    	ServerAlias www.<%domain%>
    	DirectoryIndex index.php
    
    	<Directory /var/www/html/<%domain%>>
    		AllowOverride All
    		php_admin_value open_basedir /var/www/html/<%domain%>:/tmp:/usr/share:/var/lib
    	</Directory>
    
    	CustomLog	/var/www/html/<%domain%>/httpd_<%domain%>_access.log combined
    	ErrorLog	/var/www/html/<%domain%>/httpd_<%domain%>_error.log
    
    	php_admin_value xdebug.profiler_output_dir /var/www/html/<%domain%>/xd_profile_<%domain%>
    	php_admin_value xdebug.trace_output_dir /var/www/html/<%domain%>/xd_trace_<%domain%>
    </VirtualHost>

    Сам конфиг может изменяться, но основные моменты нужно сохранить: <%domain%> — маска, которая заменяется на домен для веб-приложения, файлы логов и папки для профилирования и трейсов (если необходимо). Все остальное по вкусу в зависимости от того, что необходимо в приложении.


  5. Папки необходимо расшаривать с автомаунтом и желательно с правом на запись в нее — мы же можем что-то и заливать через это наше приложение и потому файлы должны сохраняться. А кто-то может что-то и кеширует в файлы. Право на запись не помешает. Автомаунт важен — скрипт выбирает нужные папки по списку уже подмонтированных папок в /media/sf_* — именно поэтому папки должни монтироваться с автомаунтом. Должно выглядеть приблизительно так, как на скриншоте.
  6. На гостевой машине должен быть отключен SELinux. С включенным SELinux веб-сервер не видит, подмаунченых в /var/www/html/, папок — они маунтятся в контексте vbox, а не httpd. Я пока не нашел как это подправить и поэтому подпер костылем — отключил SELinux.
  7. Хостнейм у гостевой машины должен иметь вид домена. У меня это natty.nat и в итоге все веб-приложения, которые я маунчу в эту гостевую машину, будут иметь такой вид: [имя папки, которую расшариваем].[hostname]. например test.natty.nat. Лично мне удобно было вешать на такие домены.
  8. Необходимо создать папочку /etc/httpd/conf/sf,
    # mkdir -p /etc/httpd/conf/sf
    в которую будут складироваться конфиги виртуальных хостов веб-сервера. При этом в конце файла /etc/httpd/conf/httpd.conf необходимо инклудить все конфиги, которые мы будем складировать в вышеупомянутой папке:
    include "conf/sf/*.conf"


Если все хорошо, то теперь мы можем расшарить папку с нашим проектом на нашу виртуальную машину, запустить её и, выполнив следующую команду, получить готовое место для разработки:

# /path/to/vbox-sf.sh mount


Так мы подмаунтим все расшаренные папки. Если вторым аргументом указать имя нашей папки, то подмонтирует или демонтирует только её:

# /path/to/vbox-sf.sh mount test

# /path/to/vbox-sf.sh umount test


Демон


Но теперь было бы неплохо это дело как-то автоматизировать. Я выбрал путь демона и я считаю, что он будет более правильным, нежели руцями изменять /etc/rc.# файлы. Был написан следующий скрипт для /etc/init.d
Скрипт второй - демон - /etc/init.d/vboxsf
#!/bin/bash
#
# Author:	Dmitry Vapelnik
# Email:	dvapelnik@gmail.com

### BEGIN INIT INFO
# Required-Start: 	httpd mysqld vboxadd-service vboxadd
# Required-Stop:	httpd mysqld vboxadd-service vboxadd
# Default-Start:	3
# Default-Stop:		0 6
# Short-Description:	Mounting VirtualBox shared folders
# Description:		This file should be used to mount and umount 
#			VirtualBox shared folders
### END INIT INFO

# Get function from functions library
. /etc/init.d/functions

prog="VBoxSF"
lockfile='/var/lock/subsys/vboxsf'

# Start the service vbox-sf
start() {
#	initlog -c "echo -n Starting $prog server: "
	/root/bin/vbox-sf mount &> /dev/null && touch $lockfile
	success $"$prog: mounted"
	echo
}

# Restart the service vbox-sf
stop() {
#	initlog -c "echo -n Umounting $prog: "
	/root/bin/vbox-sf umount &> /dev/null && rm -f $lockfile
	success $"$prog: umounted"
	echo
}

status() {
	/root/bin/vbox-sf status
}

### main logic ###
case "$1" in
	start)
		start
		;;
	stop)
		stop
		;;
	status)
		status
		;;
	restart|reload|condrestart)
		stop
		start
		;;
	*)
		echo $"Usage: $0 {start|stop|restart|status}"
		exit 1
esac

exit 0


Этот скрипт необходимо положить в директорию /etc/init.d и назвать файл vboxsf. На самом деле здесь название файла некритично. Просто когда мы будем добавлять нового демона с помощью chkconfig, то нам необходимо будет указать имя этого файлика. Далее я буду использовать именно vboxsf.

Итак, мы добавили файл. Теперь нам необходимо перелинковать наш скрипт в /root/bin:

# mkdir -p /root/bin
# ln -s /root/scripts/vbox-sf.sh /root/bin/vbox-sf


Добавляем в chkconfig:

# chkconfig --add vboxsf


Проверяем все ли добавилось:

# chkconfig | grep vboxsf


Если все хорошо, нам должно показать, на каких уровнях будет запускаться наш скрипт.



В результате теперь мы можем просто использовать следующие команды:

# service vboxsf start
# service vboxsf stop
# service vboxsf restart
# service vboxsf status


Если нет, то смотрим что мы не так сделали. В принципе, на этом моменте все должно работать: монтироваться при запуске и демонтироваться перед выключением.

А теперь вкусняшки


Помните скрипты, которые выполняются после монтирования и перед демонтированием папки? Так вот, для более удобной работы я также разворачиваю базу из дампа в MySQL-сервер и перед тем, как произойдет демонтирование, сливаю базу обратно в дамп. Таким образом, мы имеем актуальный дамп базы после того, как выключим машину и имеем актуальную базу после включения машины.

Вот эти скрипты:
Скрипт третий - восстановление БД из дампа - aftermount.sh
#!/bin/sh

# Author:	Dmitry Vapelnik
# Email:	dvapelnik@gmail.com

if [ $# -eq 0  ]; then
    echo 'No arguments supplied';
    echo 'Exit';
    exit 0;
fi
######################################################
dbAdminUser='ourDbAdminLogin'
dbAdminPass='ourDbAdminPassword'

dbName='ourDbName'
dbUser='ourDbUser'
dbPass='ourDbPassword'
dbHost='localhost'
dbDump=$1'/db.sql'
######################################################

queryCreateUser="CREATE USER '$dbUser'@'$dbHost' IDENTIFIED BY '$dbPass';
CREATE DATABASE IF NOT EXISTS \`$dbName\`;
GRANT ALL PRIVILEGES ON \`$dbName\`.* TO '$dbUser'@'$dbHost';
FLUSH PRIVILEGES;"

echo Creating new user...
if mysql -u$dbAdminUser -p$dbAdminPass -e "$queryCreateUser"; then
    echo User added
    echo Using MySQL dump
    if mysql -u$dbAdminUser -p$dbAdminPass $dbName < $dbDump; then
	echo MySQL dump loaded into $dbName
    else
	echo MySQL dump not loaded
    fi
else
    echo Error: user not added exit
fi


Важно! Achtung! dbAdminUser и dbAdminPass — это логин и пароль администратора БД, который может создать/удалить пользователя, создать БД и залить в неё дамп.
ourDbName, ourDbUser и ourDbPassword — имя БД и логин и пароль пользователя MySQL, который используется в нашем монтируемом разрабатываемом веб-приложении.

Что делает этот скрипт:
  1. Создает пользователя;
  2. Создает БД;
  3. Заливает содержимое нашего дампа в эту базу


Что необходимо:

  1. Чтобы администратор БД мог добавить пользователя, создать БД и залить в неё наш дамп (я использую MySQL-пользователя root и некий короткий пароль — мне же никто не будет ломать мой сервер на VirtualBox);
  2. Дамп нашей БД должен лежать в корне расшариваемой папки в файле db.sql. Мы можем его назвать по-другому и положить в другое место, но тогда необходимо будет подправить и скрипт — это тоже не проблема при желании;
  3. Для соблюдения чистоты, ни создаваемого пользователя, ни БД не должно быть до разворачивания всего нашего дела.


Второй скрипт просто сохраняет содержимое нашей базы в файл, подчищает за собою юзера и удаляет БД.
Скрипт четвертый - сохранение БД в дамп - beforeumount.sh
#!/bin/sh

# Author:	Dmitry Vapelnik
# Email:	dvapelnik@gmail.com

if [ $# -eq 0  ]; then
    echo 'No arguments supplied';
    echo 'Exit';
    exit 0;
fi
######################################################
dbAdminUser='dbAdminUser'
dbAdminPass='dbAdminPass'

dbName='ourDbName'
dbUser='ourDbUser'
dbHost='localhost'
dbDump=$1'/db.sql'
######################################################

queryRemove="DROP DATABASE \`$dbName\`;
DROP USER '$dbUser'@'$dbHost';
FLUSH PRIVILEGES;"

echo Dumping MySQL DB into file..

if mysqldump -u$dbAdminUser -p$dbAdminPass $dbName > $dbDump; then
    echo DB dumped into file
    echo Removing user and database
    if mysql -u$dbAdminUser -p$dbAdminPass -e "$queryRemove"; then
	echo User and DB was removed
    else
	echo Error on removing
    fi
fi

Настройки как и в предыдущем скрипте. На этом этапе все должно быть прозрачно.

После всех этих манипуляций мы получаем следующее: у нас есть виртуальный сервер, работающий под VirtualBox; после того, как мы его включили, он автоматом маунтит наши папки, создает пользователя, базу данных в MySQL, заливает в неё наш заранее подготовленный дамп — можно работать — логи лежат фактически у нас на машине, файли профлирования и трейсы тоже у нас на машине; по завершении работы отключаем сервер: логи удаляются, файлы профилирования и трейсы удаляются вместе с папкой, содержимое базы данных дампится обратно в наш файл, папки демонтируются и машина выключается. Фактически, сервер у нас играет роль некоего суррогата или донора — на нем ничего окончательно не сохраняется и не валяется, а он только воспроизводит то, что мы ему подкидываем.

Приятный и местами полезный бонус: мы можем настроить несколько серверов с разными версиями ПО и одновременно проверять, как работает наше веб-приложение на каждой из версий php, MySQL, Apache. Логи, трейсы и файлы профилирования будут лежать в отдельных файлах в зависимости от имени домена, чтобы не мешать все в кучу.

При необходимости можно навесить других вкусняшек и удобностей, но это уже по вкусу и потребностям каждого разработчика отдельно. Все, что нужно, пихаем в скрипты, которые выполняются после монтирования и перед демонтированием.

Да, ещё важная деталь. Чтобы не прописывать весь зоопарк поддоменов, которые могут использоваться для разработки, необходимо установить на серверной машине (на машине, на которой мы ведем разработку и где установлен VirtualBox, у меня это ноутбук) Dnsmasq — это легковесный DNS, DHCP, TFTP (BOOTP, PXE) сервер. Настраивается он донельзя противного просто. Очищенный мой конфиг выглядит так (да, у меня три машины: 5.3, 5.4, 5.5):

$ cat /etc/dnsmasq.conf | egrep -v "(^#.*|^$)"
listen-address=127.0.0.1
address=/natty.nat/192.168.191.160
address=/ketty.nat/192.168.191.161
address=/betsy.nat/192.168.191.162


Таким образом, все поддомены этих доменов будут направляться на указанные IP-адреса.

Еще может кому будет полезен кусок /etc/php.ini, который отвечает за XDebug.
Кусок от /etc/php.ini
; этот путь может быть другим - смотрим куда установится XDebug
zend_extension="xdebug.so"
xdebug.cli_color=1
xdebug.remote_enable=true
xdebug.remote_host="192.168.191.1"
xdebug.remote_port=9000
xdebug.remote_handler="dbgp"
xdebug.remote_connect_back=1
; Profiler
xdebug.profiler_enable = 0
xdebug.profiler_enable_trigger = 1
xdebug.profiler_append = 0
; Trace options
xdebug.collect_includes = 1
xdebug.collect_params = 4
xdebug.collect_vars = 0
xdebug.dump.REQUEST = *
xdebug.dump.SESSION = *
;xdebug.dump.SERVER = REMOTE_ADDR,REQUEST_METHOD
xdebug.dump.SERVER = *
xdebug.dump_globals = 1
xdebug.dump_once = 1
xdebug.dump_undefined = 0
xdebug.show_mem_delta = Off
;xdebug.file_link_format = ''
xdebug.manual_url = http://www.php.net
xdebug.show_exception_trace = 1
xdebug.show_local_vars = 1
xdebug.show_mem_delta = 1
; Traces
xdebug.auto_trace = 0
xdebug.collect_assignments = 1
xdebug.collect_return = 1
xdebug.trace_enable_trigger = 1
; 0 for parsing
; 1 human readable
xdebug.trace_format = 1
xdebug.trace_options = 0
xdebug.trace_output_name = trace.%c


Если кто не знает, под огнелис есть удобное расширение для работы с XDebug — The easiest Xdebug, которое позволяет включать/отключаться остановку на брейкпоинтах, профилирование и трейс.

Я все создавал в сети 192.168.191.0/24 и потом, мои IP из этой подсети. Если у вас другая подсеть — изменяйтесь как вам удобно.
Tags:
Hubs:
Total votes 28: ↑20 and ↓8 +12
Views 26K
Comments Comments 32