565 lines
17 KiB
Bash
565 lines
17 KiB
Bash
#!/bin/bash
|
|
|
|
#####################################################################
|
|
####### Script instalador Ogclient
|
|
####### Autor: Luis Gerardo Romero <lguillen@unizar.es>
|
|
#####################################################################
|
|
|
|
function globalSetup() {
|
|
local current_dir
|
|
current_dir=$(dirname "$0")
|
|
PROGRAMDIR=$(readlink -e "$current_dir")
|
|
PROGRAMNAME=$(basename "$0")
|
|
OPENGNSYS_CLIENT_USER="ogdhcp"
|
|
|
|
# Comprobar si se ha descargado el paquete comprimido (REMOTE=0) o sólo el instalador (REMOTE=1).
|
|
if [ -d "$PROGRAMDIR/../installer" ]; then
|
|
echo "REMOTE=0"
|
|
REMOTE=0
|
|
else
|
|
echo "REMOTE=1"
|
|
REMOTE=1
|
|
fi
|
|
|
|
BRANCH=${1:-"main"}
|
|
|
|
GIT_REPO="ssh://git@ognproject.evlt.uma.es:21987/opengnsys/ogdhcp.git"
|
|
|
|
# Directorios de instalación y destino de OpenGnsys.
|
|
WORKDIR=/tmp/ogdhcp_installer
|
|
INSTALL_TARGET=/opt/ogdhcp
|
|
PATH=$PATH:$INSTALL_TARGET/bin
|
|
|
|
if command -v service &>/dev/null; then
|
|
STARTSERVICE="eval service \$service restart"
|
|
STOPSERVICE="eval service \$service stop"
|
|
else
|
|
STARTSERVICE="eval /etc/init.d/\$service restart"
|
|
STOPSERVICE="eval /etc/init.d/\$service stop"
|
|
fi
|
|
|
|
ENABLESERVICE="eval update-rc.d \$service defaults"
|
|
DISABLESERVICE="eval update-rc.d \$service disable"
|
|
|
|
# Variables globales
|
|
DEFAULTDEV=""
|
|
NGINX_TEMPLATE="$INSTALL_TARGET/etc/nginxServer.conf.tmpl"
|
|
NGINX_OUTPUT="/etc/nginx/sites-available/ogdhcp.conf"
|
|
NGINX_CONF_PATH="/etc/nginx/nginx.conf"
|
|
PHP_FPM_CONF_PATH="/etc/php/__PHPVERSION__/fpm/pool.d/www.conf"
|
|
NEW_FPM_CONF_PATH="/etc/php/__PHPVERSION__/fpm/pool.d/ogdhcp.conf"
|
|
SOCKET_PATH="/run/php/php__PHPVERSION__-fpm-ogdhcp.sock"
|
|
|
|
# Registro de incidencias.
|
|
OGLOGFILE="$INSTALL_TARGET/var/log/${PROGRAMNAME%.sh}.log"
|
|
LOG_FILE="/tmp/$(basename "$OGLOGFILE")"
|
|
}
|
|
|
|
|
|
function checkDependencies() {
|
|
echoAndLog "Checking dependencies..."
|
|
|
|
# Lista de dependencias
|
|
local DEPENDENCIES=(
|
|
php
|
|
php-cli
|
|
php-fpm
|
|
php-json
|
|
php-pdo
|
|
php-mysql
|
|
php-zip
|
|
php-gd
|
|
php-mbstring
|
|
php-curl
|
|
php-xml
|
|
php-pear
|
|
php-bcmath
|
|
composer
|
|
unzip
|
|
kea-dhcp4-server
|
|
kea-common
|
|
kea-ctrl-agent
|
|
jq
|
|
net-tools
|
|
nginx
|
|
|
|
)
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
|
|
# Comprobar cada dependencia
|
|
for dep in "${DEPENDENCIES[@]}"; do
|
|
if ! dpkg -s "$dep" >/dev/null 2>&1; then
|
|
echoAndLog "$dep is not installed. Installing..."
|
|
sudo apt-get install -y --no-install-recommends "$dep"
|
|
else
|
|
echoAndLog "$dep is already installed."
|
|
fi
|
|
done
|
|
sed -i '/ConditionFileNotEmpty=\/etc\/kea\/kea-api-password/d' /usr/lib/systemd/system/kea-ctrl-agent.service
|
|
systemctl restart kea-ctrl-agent.service
|
|
echoAndLog "Dependencies checked."
|
|
}
|
|
|
|
|
|
# Obtiene el código fuente del proyecto desde el repositorio de GitHub.
|
|
function downloadCode() {
|
|
if [ $# -ne 1 ]; then
|
|
errorAndLog "${FUNCNAME}(): invalid number of parameters"
|
|
exit 1
|
|
fi
|
|
|
|
local url="$1"
|
|
|
|
echoAndLog "${FUNCNAME}(): downloading code from '$url'..."
|
|
|
|
GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=accept-new" git archive --remote="$url" --format zip --output opengnsys.zip --prefix=opengnsys/ "$BRANCH" && unzip opengnsys.zip
|
|
if [ $? -ne 0 ]; then
|
|
errorAndLog "${FUNCNAME}(): error getting OpenGnsys code from $url"
|
|
return 1
|
|
fi
|
|
rm -f opengnsys.zip
|
|
echoAndLog "${FUNCNAME}(): code was downloaded"
|
|
return 0
|
|
}
|
|
|
|
# Crea la estructura base de la instalación de opengnsys
|
|
function createDirs() {
|
|
if [ $# -ne 1 ]; then
|
|
errorAndLog "${FUNCNAME}(): invalid number of parameters"
|
|
exit 1
|
|
fi
|
|
|
|
local path_opengnsys_base="$1"
|
|
|
|
# Crear estructura de directorios.
|
|
echoAndLog "${FUNCNAME}(): creating directory paths in $path_opengnsys_base"
|
|
mkdir -p "$path_opengnsys_base"/{bin,config,docs,public,src,etc/kea/backup,templates,var/{cache,log},vendor}
|
|
if [ $? -ne 0 ]; then
|
|
errorAndLog "${FUNCNAME}(): error while creating dirs. Do you have write permissions?"
|
|
return 1
|
|
fi
|
|
|
|
# Crear usuario ficticio.
|
|
if id -u "$OPENGNSYS_CLIENT_USER" &>/dev/null; then
|
|
echoAndLog "${FUNCNAME}(): user \"$OPENGNSYS_CLIENT_USER\" is already created"
|
|
else
|
|
echoAndLog "${FUNCNAME}(): creating OpenGnsys user"
|
|
useradd "$OPENGNSYS_CLIENT_USER" 2>/dev/null
|
|
if [ $? -ne 0 ]; then
|
|
errorAndLog "${FUNCNAME}(): error creating OpenGnsys user"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
# Mover el fichero de registro de instalación al directorio de logs.
|
|
echoAndLog "${FUNCNAME}(): moving installation log file"
|
|
mv "$LOG_FILE" "$path_opengnsys_base/var/log" && LOG_FILE="$OGLOGFILE"
|
|
chmod 777 "$LOG_FILE"
|
|
sudo chmod -R 777 "$path_opengnsys_base/etc"
|
|
|
|
# Mover el fichero de registro de instalación al directorio de logs.
|
|
echoAndLog "${FUNCNAME}(): moving installation log file"
|
|
touch "$path_opengnsys_base/var/log/dev.log"
|
|
chmod 777 "$path_opengnsys_base/var/log/dev.log"
|
|
|
|
echoAndLog "${FUNCNAME}(): directory paths created"
|
|
return 0
|
|
|
|
# Cambiar permisos de usuario
|
|
echoAndLog "Changing user permission"
|
|
chown -R "$OPENGNSYS_CLIENT_USER:$OPENGNSYS_CLIENT_USER" "$INSTALL_TARGET"
|
|
|
|
# Copiar .env
|
|
cp -a "$WORKDIR/ogdhcp/.env" "${path_opengnsys_base}/.env"
|
|
}
|
|
|
|
|
|
function create_ogdhcp_project {
|
|
# Crea el usuario ogdhcp si no existe
|
|
local path_opengnsys_base="$1"
|
|
if ! id -u ogdhcp &>/dev/null; then
|
|
echoAndLog "${FUNCNAME}(): creating ogdhcp user"
|
|
useradd ogdhcp 2>/dev/null
|
|
if [ $? -ne 0 ]; then
|
|
errorAndLog "${FUNCNAME}(): error creating ogdhcp user"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
# Crea el directorio path_opengnsys_base con el usuario ogdhcp
|
|
echoAndLog "${FUNCNAME}(): creating directory $path_opengnsys_base with ogdhcp user"
|
|
sudo mkdir -p "$path_opengnsys_base"
|
|
sudo chown ogdhcp:ogdhcp $path_opengnsys_base
|
|
if [ $? -ne 0 ]; then
|
|
errorAndLog "${FUNCNAME}(): error while creating directory $path_opengnsys_base"
|
|
return 1
|
|
fi
|
|
|
|
echoAndLog "Directory $path_opengnsys_base created with ogdhcp user"
|
|
}
|
|
|
|
function copyServerFiles() {
|
|
if [ $# -ne 1 ]; then
|
|
errorAndLog "${FUNCNAME}(): invalid number of parameters"
|
|
exit 1
|
|
fi
|
|
|
|
local path_opengnsys_base="$1"
|
|
|
|
# Lista de ficheros y directorios origen y de directorios destino.
|
|
local SOURCES=(
|
|
config
|
|
#public
|
|
src
|
|
etc
|
|
.env
|
|
composer.json
|
|
composer.lock
|
|
phpunit.xml.dist
|
|
symfony.lock
|
|
)
|
|
local TARGETS=(
|
|
config
|
|
#public
|
|
src
|
|
etc
|
|
.env
|
|
composer.json
|
|
composer.lock
|
|
phpunit.xml.dist
|
|
symfony.lock
|
|
)
|
|
|
|
if [ "${#SOURCES[@]}" != "${#TARGETS[@]}" ]; then
|
|
errorAndLog "${FUNCNAME}(): inconsistent number of array items"
|
|
exit 1
|
|
fi
|
|
|
|
# Copiar ficheros.
|
|
echoAndLog "${FUNCNAME}(): copying files to server directories"
|
|
|
|
pushd "$WORKDIR/ogdhcp" || return
|
|
local i
|
|
for (( i = 0; i < ${#SOURCES[@]}; i++ )); do
|
|
if [ -f "${SOURCES[$i]}" ]; then
|
|
echoAndLog "Copying ${SOURCES[$i]} to $path_opengnsys_base/${TARGETS[$i]}"
|
|
cp -a "${SOURCES[$i]}" "$path_opengnsys_base/${TARGETS[$i]}"
|
|
elif [ -d "${SOURCES[$i]}" ]; then
|
|
echoAndLog "Copying content of ${SOURCES[$i]} to $path_opengnsys_base/${TARGETS[$i]}"
|
|
cp -a "${SOURCES[$i]}"/* "$path_opengnsys_base/${TARGETS[$i]}"
|
|
else
|
|
warningAndLog "Unable to copy ${SOURCES[$i]} to $path_opengnsys_base/${TARGETS[$i]}"
|
|
fi
|
|
done
|
|
echoAndLog "Changing user permission"
|
|
chown -R "$OPENGNSYS_CLIENT_USER:$OPENGNSYS_CLIENT_USER" "$INSTALL_TARGET"
|
|
|
|
popd || return
|
|
}
|
|
|
|
|
|
function runComposer() {
|
|
echoAndLog "Running composer.phar to install dependencies..."
|
|
local path_opengnsys_base="$1"
|
|
pushd $path_opengnsys_base
|
|
pwd
|
|
# Ejecutar composer.phar
|
|
sudo -u "$OPENGNSYS_CLIENT_USER" composer --no-interaction install
|
|
|
|
# Comprobar si la ejecución fue exitosa
|
|
if [ $? -ne 0 ]; then
|
|
errorAndLog "Failed to run composer.phar"
|
|
popd
|
|
return 1
|
|
fi
|
|
|
|
|
|
echoAndLog "composer.phar ran successfully and dependencies were installed"
|
|
return 0
|
|
}
|
|
|
|
|
|
get_first_network_interface_with_traffic() {
|
|
while read -r line; do
|
|
if [[ "$line" == *:* ]]; then
|
|
interface=$(echo "$line" | cut -d ':' -f 1 | xargs)
|
|
if [[ "$interface" != "lo" ]]; then
|
|
received_bytes=$(echo "$line" | awk '{print $2}')
|
|
transmitted_bytes=$(echo "$line" | awk '{print $10}')
|
|
if (( received_bytes > 0 || transmitted_bytes > 0 )); then
|
|
DEFAULTDEV="$interface"
|
|
break
|
|
fi
|
|
fi
|
|
fi
|
|
done < /proc/net/dev
|
|
}
|
|
comment_auth_kea() {
|
|
KEA_CTRL_AGENT_CONF="/etc/kea/kea-ctrl-agent.conf"
|
|
|
|
# Verificar si el bloque de "authentication" ya está comentado
|
|
if grep -q '^[^#]*"authentication": {' "$KEA_CTRL_AGENT_CONF"; then
|
|
echo "Comentando el bloque de autenticación en $KEA_CTRL_AGENT_CONF..."
|
|
|
|
# Comentar solo el bloque de authentication desde la apertura hasta la línea con '},'
|
|
sed -i '/"authentication": {/,/^[[:space:]]*},/ {
|
|
s/^\([[:space:]]*\)\([^#]\)/\1#\2/
|
|
}' "$KEA_CTRL_AGENT_CONF"
|
|
|
|
echo "Bloque de autenticación comentado correctamente."
|
|
else
|
|
echo "El bloque de autenticación ya está comentado."
|
|
fi
|
|
|
|
# Verificar si el bloque fue comentado correctamente
|
|
if grep -q '^#\s*"authentication": {' "$KEA_CTRL_AGENT_CONF"; then
|
|
echo "Confirmación: Bloque de autenticación comentado correctamente."
|
|
|
|
# Reiniciar el servicio de Kea Control Agent para aplicar los cambios
|
|
echo "Reiniciando el agente de Kea Control Agent..."
|
|
sudo systemctl restart kea-ctrl-agent.service
|
|
|
|
if systemctl is-active --quiet kea-ctrl-agent.service; then
|
|
echo "El agente de Kea Control Agent se ha reiniciado correctamente."
|
|
else
|
|
echo "Error: No se pudo reiniciar el agente de Kea Control Agent."
|
|
fi
|
|
else
|
|
echo "Error: No se pudo comentar correctamente el bloque de autenticación."
|
|
fi
|
|
}
|
|
# Función para obtener la dirección IP de una interfaz
|
|
get_ip_address() {
|
|
local interface="$1"
|
|
ip -4 addr show "$interface" | grep -oP "(?<=inet\s)\d+(\.\d+){3}"
|
|
}
|
|
|
|
# Función para obtener la versión de PHP instalada
|
|
get_php_fpm_version() {
|
|
php -v | grep -oP "PHP \K\d+\.\d+"
|
|
}
|
|
add_write_permission_apparmor() {
|
|
APPARMOR_PROFILE="/etc/apparmor.d/usr.sbin.kea-dhcp4"
|
|
|
|
# Comprobar si las líneas existen
|
|
if grep -q "/etc/kea/ r," "$APPARMOR_PROFILE" && grep -q "/etc/kea/** r," "$APPARMOR_PROFILE"; then
|
|
echo "Modificando permisos en $APPARMOR_PROFILE..."
|
|
|
|
# Modificar las líneas /etc/kea/ r, y /etc/kea/** r, añadiendo w
|
|
sed -i 's#/etc/kea/ r,#/etc/kea/ rw,#g' "$APPARMOR_PROFILE"
|
|
sed -i 's#/etc/kea/\*\* r,#/etc/kea/** rw,#g' "$APPARMOR_PROFILE"
|
|
|
|
echo "Permisos de escritura añadidos correctamente a $APPARMOR_PROFILE."
|
|
else
|
|
echo "Las líneas no fueron encontradas o ya están modificadas."
|
|
fi
|
|
|
|
# Recargar el perfil de AppArmor para aplicar los cambios
|
|
echo "Recargando el perfil de AppArmor para kea-dhcp4..."
|
|
sudo apparmor_parser -r "$APPARMOR_PROFILE"
|
|
|
|
if [ $? -eq 0 ]; then
|
|
echo "El perfil de AppArmor se recargó correctamente."
|
|
else
|
|
echo "Error al recargar el perfil de AppArmor."
|
|
fi
|
|
}
|
|
|
|
|
|
# Función para configurar Nginx
|
|
setup_nginx() {
|
|
get_first_network_interface_with_traffic
|
|
|
|
if [[ -z "$DEFAULTDEV" ]]; then
|
|
echo "Error: No se encontró una interfaz de red activa."
|
|
exit 1
|
|
fi
|
|
|
|
ip_address_server=$(get_ip_address "$DEFAULTDEV")
|
|
php_version=$(get_php_fpm_version)
|
|
|
|
if [[ -z "$php_version" ]]; then
|
|
echo "Error: No se pudo obtener la versión de PHP."
|
|
exit 1
|
|
fi
|
|
|
|
# Leer y modificar la plantilla de configuración de nginx
|
|
if [[ ! -f "$NGINX_TEMPLATE" ]]; then
|
|
echo "Error: La plantilla de Nginx no se encontró."
|
|
exit 1
|
|
fi
|
|
|
|
nginx_content=$(<"$NGINX_TEMPLATE")
|
|
nginx_content="${nginx_content//__SERVERIP__/$ip_address_server}"
|
|
nginx_content="${nginx_content//__PHPVERSION__/$php_version}"
|
|
|
|
# Crear el archivo de configuración de Nginx
|
|
echo "$nginx_content" > "$NGINX_OUTPUT"
|
|
echo "Archivo de configuración de Nginx creado en $NGINX_OUTPUT."
|
|
|
|
# Crear el enlace simbólico
|
|
ln -sf "$NGINX_OUTPUT" /etc/nginx/sites-enabled/ogdhcp.conf
|
|
echo "Enlace simbólico creado en /etc/nginx/sites-enabled/ogdhcp.conf."
|
|
|
|
# Modificar nginx.conf para ejecutar como ogdhcp
|
|
sed -i 's/user www-data;/user ogdhcp;/g' "$NGINX_CONF_PATH"
|
|
echo "Nginx configurado para ejecutarse como ogdhcp."
|
|
|
|
# Reiniciar Nginx
|
|
systemctl restart nginx.service
|
|
echo "Servicio Nginx reiniciado."
|
|
}
|
|
|
|
# Función para modificar el archivo de configuración PHP-FPM
|
|
modify_php_fpm_config() {
|
|
php_version=$(get_php_fpm_version)
|
|
|
|
if [[ -z "$php_version" ]]; then
|
|
echo "Error: No se pudo obtener la versión de PHP."
|
|
exit 1
|
|
fi
|
|
|
|
php_fpm_conf_path="/etc/php/$php_version/fpm/pool.d/www.conf"
|
|
new_fpm_conf_path="/etc/php/$php_version/fpm/pool.d/ogdhcp.conf"
|
|
socket_path="/run/php/php$php_version-fpm-ogdhcp.sock"
|
|
|
|
# Copiar el archivo www.conf a ogdhcp.conf
|
|
cp "$php_fpm_conf_path" "$new_fpm_conf_path"
|
|
|
|
# Modificar el archivo ogdhcp.conf
|
|
sed -i 's/\[www\]/[ogdhcp]/g' "$new_fpm_conf_path"
|
|
sed -i 's/user = www-data/user = ogdhcp/g' "$new_fpm_conf_path"
|
|
sed -i 's/group = www-data/group = ogdhcp/g' "$new_fpm_conf_path"
|
|
sed -i "s|listen =.*|listen = $socket_path|g" "$new_fpm_conf_path"
|
|
sed -i 's/listen.owner = www-data/listen.owner = ogdhcp/g' "$new_fpm_conf_path"
|
|
sed -i 's/listen.group = www-data/listen.group = ogdhcp/g' "$new_fpm_conf_path"
|
|
|
|
# Reiniciar PHP-FPM
|
|
systemctl restart php"$php_version"-fpm.service
|
|
echo "PHP-FPM reiniciado."
|
|
|
|
# Verificar la creación del socket
|
|
if [[ -S "$socket_path" ]]; then
|
|
echo "Socket PHP-FPM $socket_path creado correctamente."
|
|
else
|
|
echo "Error: El socket PHP-FPM $socket_path no se ha creado."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
|
|
#####################################################################
|
|
####### Algunas funciones útiles de propósito general:
|
|
#####################################################################
|
|
|
|
# Obtiene la fecha y hora actual en el formato especificado
|
|
function getDateTime() {
|
|
date "+%Y%m%d-%H%M%S"
|
|
}
|
|
|
|
# Escribe un mensaje en el archivo de registro y lo muestra por pantalla
|
|
function echoAndLog() {
|
|
local DATETIME=$(getDateTime)
|
|
echo "$1"
|
|
echo "$DATETIME;$SSH_CLIENT;$1" >> "$LOG_FILE"
|
|
}
|
|
|
|
# Escribe un mensaje de error en el archivo de registro y lo muestra por pantalla
|
|
function errorAndLog() {
|
|
local DATETIME=$(getDateTime)
|
|
echo "ERROR: $1"
|
|
echo "$DATETIME;$SSH_CLIENT;ERROR: $1" >> "$LOG_FILE"
|
|
}
|
|
|
|
# Escribe un mensaje de advertencia en el archivo de registro y lo muestra por pantalla
|
|
function warningAndLog() {
|
|
local DATETIME=$(getDateTime)
|
|
echo "Warning: $1"
|
|
echo "$DATETIME;$SSH_CLIENT;Warning: $1" >> "$LOG_FILE"
|
|
}
|
|
|
|
##########################################################################
|
|
################################main######################################
|
|
|
|
# Sólo ejecutable por usuario root
|
|
if [ "$(whoami)" != 'root' ]; then
|
|
echo "ERROR: this program must run under root privileges!!"
|
|
exit 1
|
|
fi
|
|
|
|
globalSetup
|
|
|
|
echoAndLog "OpenGnsys installation begins at $(date)"
|
|
|
|
mkdir -p $WORKDIR
|
|
pushd $WORKDIR
|
|
|
|
checkDependencies
|
|
# Si es necesario, descarga el repositorio de código en directorio temporal
|
|
if [ $REMOTE -eq 1 ]; then
|
|
downloadCode $GIT_REPO
|
|
if [ $? -ne 0 ]; then
|
|
errorAndLog "Error while getting code from the repository"
|
|
exit 1
|
|
fi
|
|
else
|
|
ln -fs "$(dirname $PROGRAMDIR)" ogdhcp
|
|
fi
|
|
|
|
create_ogdhcp_project ${INSTALL_TARGET}
|
|
if [ $? -ne 0 ]; then
|
|
errorAndLog "Error while creating skeleton directory!"
|
|
exit 1
|
|
fi
|
|
|
|
# Arbol de directorios de OpenGnsys.
|
|
createDirs ${INSTALL_TARGET}
|
|
if [ $? -ne 0 ]; then
|
|
errorAndLog "Error while creating directory paths!"
|
|
exit 1
|
|
fi
|
|
|
|
|
|
# Copiar ficheros de servicios OpenGnsys Server.
|
|
copyServerFiles ${INSTALL_TARGET}
|
|
if [ $? -ne 0 ]; then
|
|
errorAndLog "Error while copying the server files!"
|
|
exit 1
|
|
fi
|
|
|
|
|
|
comment_auth_kea
|
|
if [ $? -ne 0 ]; then
|
|
errorAndLog "Error while commenting auth block!"
|
|
exit 1
|
|
fi
|
|
|
|
|
|
runComposer ${INSTALL_TARGET}
|
|
setup_nginx $INSTALL_TARGET
|
|
if [ $? -ne 0 ]; then
|
|
errorAndLog "Error configuring Nginx for OpenGnsys Admin"
|
|
exit 1
|
|
fi
|
|
modify_php_fpm_config
|
|
if [ $? -ne 0 ]; then
|
|
errorAndLog "Error configuring PHP-FPM for OpenGnsys Admin"
|
|
exit 1
|
|
fi
|
|
|
|
add_write_permission_apparmor
|
|
if [ $? -ne 0 ]; then
|
|
errorAndLog "Error adding write permission to AppArmor profile"
|
|
exit 1
|
|
fi
|
|
|
|
# install_kea
|
|
# install_php
|
|
# install_composer
|
|
# install_symfony
|
|
# install_swagger
|
|
# Ahora puedes clonar e instalar el componente ogDhcp
|
|
# git clone <URL del repositorio de ogDhcp>
|
|
# cd <directorio de ogDhcp>
|
|
# composer install
|