Commits (4)
from .deploy import *
import drush
import behat
import docker
from .environments import e
from fabric.api import lcd, cd, task, roles, env, local, run, runs_once, execute
import helpers as h
@task
def init():
"""
Complete local installation process, used generally when building the docker image for install and configure Drupal.
"""
execute(docker.image_create)
execute(docker.container_start)
execute(drush.make, 'install')
execute(drush.site_install)
execute(drush.aliases)
execute(behat.init)
@task
def test():
"""
Setup Behat and run the complete tests suite. Default output formatters: pretty and JUnit.
The JUnit report file is specified in the Behat configuration file. Default: tests/behat/out/behat.junit.xml.
:param tag Specific Behat tests tags to run.
"""
execute(behat.init)
execute(behat.run)
@task
def install():
"""
Run the full installation process.
"""
execute(drush.make, 'install')
execute(drush.site_install)
execute(behat.init)
@task
def update():
"""
Update the full codebase and run the availabe database updates.
"""
execute(drush.make, 'update')
execute(drush.updatedb)
execute(behat.init)
@task
def release():
"""
Generate all artefacts related to a release process or a deployment process.
"""
execute(drush.archive_dump)
from __future__ import unicode_literals
from fabric.api import lcd, cd, task, roles, env, local, run, runs_once, execute
from fabric.contrib.console import confirm
from fabric.colors import red, green
import helpers as h
@task
@roles('docker')
def init(rewrite=True):
"""
Check installation and configure Behat.
:param role Default 'role' where to run the task
:param rewrite If the behat.yml file should be rewrited or not.
"""
role = 'docker'
workspace = env.docker_workspace
host = env.site_hostname
site_root = env.docker_site_root
if not h.fab_exists(role, '{}/tests/behat/behat.yml'.format(workspace)) or rewrite:
with h.fab_cd(role, '{}/tests/behat'.format(workspace)):
h.fab_run(role, 'cp example.behat.yml behat.yml')
h.fab_run(role, 'sed -i "s@%DRUPAL_ROOT@{}@g" behat.yml'.format(site_root))
h.fab_run(role, 'sed -i "s@%URL@http://{}@g" behat.yml'.format(host))
h.fab_run(role, 'echo "127.0.0.1 {}" >> /etc/hosts'.format(host))
print green('Behat is now properly configured. The configuration file is {}/tests/behat/behat.yml'.format(workspace))
else:
print green('{}/tests/behat/behat.yml is already created.'.format(workspace))
@task()
@roles('docker')
def install():
"""
Install behat
"""
role = 'docker'
workspace = env.docker_workspace
if not h.fab_exists(role, '/usr/local/bin/behat'):
with h.fab_cd(role, '{}/tests/behat'.format(workspace)):
h.fab_run(role, 'curl -s https://getcomposer.org/installer | php')
h.fab_run(role, 'php composer.phar install')
h.fab_run(role, 'ln -s bin/behat /usr/local/bin/behat')
print green('Behat has been properly installed with Composer in /usr/local/bin')
else:
print(green('Behat is already installed, no need for a new installation'))
@task
@roles('docker')
def run():
"""
Execute the complete Behat tests suite.
:param role Default 'role' where to run the task
"""
role = 'docker'
workspace = env.docker_workspace
h.fab_run(role, 'mkdir -p {}/logs/behat'.format(workspace))
# In the container behat is installed globaly, so check before install it inside the tests directory
if not h.fab_exists(role, '/usr/local/bin/behat') or not h.fab_exists(role, '../tests/behat/bin/behat'):
install()
# If the configuration file behat.yml doesn't exist, call behat_init before run the test.
if not h.fab_exists(role, '{}/tests/behat/behat.yml'.format(workspace)):
init()
with h.fab_cd(role, '{}/tests/behat'.format(workspace)):
h.fab_run(role, 'behat --format junit --format pretty --tags "~@wip&&~@disabled&&~@test" --colors')
# To run behat with only one test for example, comment previous line
# and uncomment next one
# fab_run(role, 'behat --format pretty --tags "~@wip&&~@disabled&&@yourTest" --colors')
......@@ -25,8 +25,7 @@ LOCAL_WORKSPACE = path.join(path.dirname(__file__), path.pardir)
LOCAL_DRUPAL_ROOT = '{}/src/drupal'.format(LOCAL_WORKSPACE)
# Variables to use inside the docker container
DOCKER_WORKSPACE = "/opt/sfl_boilerplate"
DOCKER_DRUPAL_ROOT = "/var/www/drupal"
DOCKER_WORKSPACE = "/opt/sfl"
DOCKER_PORT_TO_BIND = 8001
# Database variables
......@@ -46,6 +45,8 @@ SITE_HOSTNAME = 'local.boilerplate.sfl'
# Projects variables
PROJECT_NAME = 'sfl_boilerplate'
# Installation profile
PROFILE = {'sflinux':'git@gitlab.savoirfairelinux.com:drupal/sflinux.git'}
PROFILE_MAKE_FILE = 'build/build-sflinux.make'
......
from __future__ import unicode_literals
from fabric.api import lcd, cd, task, roles, env, local, run, runs_once, execute
from fabric.contrib.console import confirm
from fabric.colors import red, green
import helpers as h
###########################################################
# Helper functions to manage docker images and containers #
###########################################################
def docker_ps(running_only=False):
args = '' if running_only else '-a'
result = local('docker ps %s' % args, capture=True)
lines = result.stdout.splitlines()
# container name is supposed to be the last column
assert lines[0].strip().endswith('NAMES')
return [line.strip().split(' ')[-1] for line in lines[1:]]
def docker_tryrun(imgname, containername=None, opts='', mounts=None, cmd='', restart=True):
# mounts is a list of (from, to, canwrite) path tuples. ``from`` is relative to the project root.
# Returns True if the container was effectively ran (false if it was restarted or aborted)
if not mounts:
mounts = []
if containername and containername in docker_ps(running_only=True):
print green("%s already running" % containername)
return False
if containername and containername in docker_ps(running_only=False):
if restart:
print green("%s already exists and is stopped. Restarting!" % containername)
local('docker restart %s' % containername)
return True
else:
print red("There's a dangling container %s! That's not supposed to happen. Aborting" % containername)
print "Run 'docker rm %s' to remove that container" % containername
return False
for from_path, to_path, canwrite in mounts:
abspath = from_path
opt = ' -v %s:%s' % (abspath, to_path)
if not canwrite:
opt += ':ro'
opts += opt
if containername:
containername_opt = '--name %s' % containername
else:
containername_opt = ''
local('docker run %s %s %s %s' % (opts, containername_opt, imgname, cmd))
return True
def docker_ensureruns(containername):
# Makes sure that containername runs. If it doesn't, try restarting it. If the container
# doesn't exist, spew an error.
if containername not in docker_ps(running_only=True):
if containername in docker_ps(running_only=False):
local('docker restart %s' % containername)
return True
else:
return False
else:
return True
def docker_ensure_data_container(containername, volume_paths=None, base_image='busybox'):
# Make sure that we have our data containers running. Data containers are *never* removed.
# Their only purpose is to hold volume data.
# Returns whether a container was created by this call
if containername not in docker_ps(running_only=False):
if volume_paths:
volume_args = ' '.join('-v %s' % volpath for volpath in volume_paths)
else:
volume_args = ''
local('docker create %s --name %s %s' % (volume_args, containername, base_image))
return True
return False
def docker_isrunning(containername):
# Check if the containername is running.
if containername not in docker_ps(running_only=True):
return False
else:
return True
def docker_images():
result = local('docker images', capture=True)
lines = result.stdout.splitlines()
# image name is supposed to be the first column
assert lines[0].strip().startswith('REPOSITORY')
return [line.strip().split(' ')[0] for line in lines]
@task
@roles('local')
def connect(role='local'):
"""
Connect to a docker container using "docker -it exec <name> bash".
This is a better way to connect to the container than using ssh'
:param role Default 'role' where to run the task
"""
with h.fab_cd(role, env.workspace):
if docker_isrunning('{}_container'.format(env.project_name)):
h.fab_run(role, 'docker exec -it {}_container bash'.format(env.project_name))
else:
print(red('Docker container {}_container is not running, it should be running to be able to connect.'))
@task
@roles('local')
def image_create(role='local'):
"""
Create docker images
:param role Default 'role' where to run the task
"""
with h.fab_cd(role, env.workspace):
if '{}/drupal'.format(env.project_name) in docker_images():
print(red('Docker image {}/drupal was found, you has already build this image'.format(env.project_name)))
else:
h.fab_run(role, 'docker build -t {}/drupal .'.format(env.project_name))
print(green('Docker image {}/drupal was build successful'.format(env.project_name)))
@task
@roles('local')
def container_start(role='local'):
"""
Run docker containers
:param role Default 'role' where to run the task
"""
with h.fab_cd(role, env.workspace):
if '{}/drupal'.format(env.project_name) in docker_images():
if docker_tryrun('{}/drupal'.format(env.project_name),
'{}_container'.format(env.project_name),
'-d -p {}:80'.format(env.bind_port),
mounts=[(env.workspace, env.docker_workspace, True)]):
# If container was successful build, get the IP address and show it to the user.
global CONTAINER_IP
CONTAINER_IP = h.fab_run(role, 'docker inspect -f "{{{{.NetworkSettings.IPAddress}}}}" '
'{}_container'.format(env.project_name), capture=True)
h.fab_update_hosts(CONTAINER_IP, env.site_hostname)
print(green('Docker container {}_container was build successful. '
'To visit the Website open a web browser in http://{} or '
'http://localhost:{}.'.format(env.project_name, env.site_hostname, env.bind_port)))
else:
print(red('Docker image {}/drupal not found and is a requirement to run the {}_container.'
'Please, run first "fab create" in order to build the {}/drupal '
'image'.format(env.project_name, env.project_name, env.project_name)))
@task
@roles('local')
def container_stop(role='local'):
"""
Stop docker containers
:param role Default 'role' where to run the task
"""
with h.fab_cd(role, env.workspace):
if '{}_container'.format(env.project_name) in docker_ps():
h.fab_remove_from_hosts(env.site_hostname)
h.fab_run(role, 'docker stop {}_container'.format(env.project_name))
print(green('Docker container {}_container was successful stopped'.format(env.project_name)))
else:
print(red('Docker container {}_container was not running or paused'.format(env.project_name)))
@task
@roles('local')
def container_remove(role='local'):
"""
Stop docker containers
:param role Default 'role' where to run the task
"""
with h.fab_cd(role, env.workspace):
if '{}_container'.format(env.project_name) in docker_ps():
h.fab_remove_from_hosts(env.site_hostname)
h.fab_run(role, 'docker rm -f {}_container'.format(env.project_name))
print(green('Docker container {}_container was successful removed'.format(env.project_name)))
else:
print(red('Docker container {}_container was already removed'.format(env.project_name)))
@task()
@roles('local')
def image_remove(role='local'):
"""
Remove docker container and images
:param role Default 'role' where to run the task
"""
with h.fab_cd(role, env.workspace):
if docker_isrunning('{}_container'.format(env.project_name)):
print(red('Docker container {}_container is running, '
'you should stopped it after remove the image {}/drupal'.format(env.project_name, env.project_name)))
if '{}/drupal'.format(env.project_name) in docker_images():
h.fab_run(role, 'docker rmi -f {}/drupal'.format(env.project_name))
# Remove dangling docker images to free space.
if '<none>' in docker_images():
h.fab_run(role, 'docker images --filter="dangling=true" -q | xargs docker rmi -f')
print(green('Docker image {}/drupal was successful removed'.format(env.project_name)))
else:
print(red('Docker image {}/drupal was not found'.format(env.project_name)))
# coding: utf-8
#
# Copyright (C) 2016 Savoir-faire Linux Inc. (<www.savoirfairelinux.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import unicode_literals
from fabric.api import lcd, cd, task, roles, env, local, run, runs_once, execute
from fabric.contrib.console import confirm
from fabric.colors import red, green
# Import datetime
from datetime import datetime
import helpers as h
@task(alias='make')
@roles('local')
def make(action='install'):
"""
Build the platform by running the Makefile specified in the local_vars.py configuration file.
"""
# Update profile codebase
h._update_profile()
drush_opts = "--prepare-install " if action != 'update' else ''
if (env.interactive_mode and confirm(red('Say [Y] to {} the site at {} with the French translation, if you say [n] '
'the site will be installed in English only'.format(action, env.site_root)))
) or not env.interactive_mode:
drush_opts += "--translations=fr "
env.locale = True
drush_opts += "--contrib-destination=profiles/{} ".format(env.site_profile)
if env.interactive_mode:
drush_opts += " --working-copy --no-gitinfofile"
if not h.fab_exists('local', env.site_root):
h.fab_run('local', "mkdir {}".format(env.site_root))
with h.fab_cd('local', env.site_root):
h.fab_run('local', 'drush make {} {} -y'.format(drush_opts, env.makefile))
@task
@roles('local')
def aliases():
"""
Copy conf/aliases.drushrc.php in the site environment.
"""
role = 'local'
aliases = env.site_drush_aliases
workspace = env.workspace
if not h.fab_exists(role, aliases):
h.fab_run(role, 'mkdir {}'.format(aliases))
with h.fab_cd(role, aliases):
# Create aliases
if h.fab_exists(role, '{}/aliases.drushrc.php'.format(aliases)):
h.fab_run(role, 'rm aliases.drushrc.php')
h.fab_run(role, 'ln -s {}/conf/aliases.drushrc.php .'.format(workspace))
print green('Drush aliases have been copied to {} directory.'.format(aliases))
@task
@roles('docker')
def updatedb():
"""
Run the available database updates. Similar to drush updatedb.
"""
role = 'docker'
with h.fab_cd(role, env.docker_site_root):
h.fab_run(role, 'drush updatedb -y')
@task
@roles('docker')
def site_install():
"""
Run the site installation procedure.
"""
role = 'docker'
site_root = env.docker_site_root
apache = env.apache_user
profile = env.site_profile
db_user = env.site_db_user
db_pass = env.site_db_pass
db_host = env.site_db_host
db_name = env.site_db_name
site_name = env.site_name
site_admin_name = env.site_admin_user
site_admin_pass = env.site_admin_pass
site_subdir = env.site_subdir
# Create first the database if necessary
h._init_db('docker')
with h.fab_cd(role, site_root):
locale = '--locale="fr"' if env.locale else ''
h.fab_run(role, 'sudo -u {} drush site-install {} {} --db-url=mysql://{}:{}@{}/{} --site-name={} '
'--account-name={} --account-pass={} --sites-subdir={} -y'.format(apache, profile, locale,
db_user, db_pass,
db_host, db_name, site_name,
site_admin_name,
site_admin_pass,
site_subdir))
print green('Site installed successfully!')
h.hook_execute('post_install', role)
@task
@roles('docker')
def archive_dump(role='docker'):
"""
Archive the platform for release or deployment.
:param role Default 'role' where to run the task
"""
role = 'docker'
with h.fab_cd(role, env.docker_site_root):
platform = '{}-{}.tar.gz'.format(env.project_name, datetime.now().strftime('%Y%m%d_%H%M%S'))
print(green('Cleaning previous archives'))
h.fab_run(role, 'rm -f {}/*.tar.gz'.format(env.builddir))
print(green('Archiving the platform'))
h.fab_run(
role,
'drush archive-dump --destination={}/{} --tar-options="--exclude=.git"'.format(env.builddir, platform)
)
from importlib import import_module
from fabric.utils import abort
def e(name):
"""
Set your environment before running other commands
"""
try:
import_module('.settings.%s' % name, 'fabfile')
except ImportError:
abort('Environment settings not found for "%s", you must create a file at `settings/%s.py` and add your environment settings to it to use it' % (name, name))
This diff is collapsed.
# coding: utf-8
#
# Copyright (C) 2016 Savoir-faire Linux Inc. (<www.savoirfairelinux.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import unicode_literals
from getpass import getuser
from fabric.api import lcd, cd, task, roles, env, local, run, runs_once, execute
from fabric.colors import red, green
from fabric.contrib.console import confirm
from fabric.contrib.files import exists
# Import socket to find the localhost IP address
import socket
# Import datetime
from datetime import datetime
# Import default variables
from default_vars import *
# Import local variables' overrides, if they exist
if path.exists(path.join(path.dirname(__file__), 'local_vars.py')):
from local_vars import *
#####################################################################
# Function to manage differents users, hosts, roles, and variables #
#####################################################################
# Get info of the current user and host
user_name = getuser()
host_name = local("hostname", capture=True)
# Set the env dict with the roles and the hosts
env.roledefs['local'] = ["{}@{}".format(user_name, host_name)]
env.roledefs['docker'] = ["root@{}".format(SITE_HOSTNAME)]
# Flag to use for install the site with or without translations
LOCALE = False
# The CONTAINER_IP will be set at the creation of the container, see @task docker_run_container
CONTAINER_IP = None
env.builddir = path.join(env.workspace, 'build')
env.makefile = '{}/{}/{}'.format(env.builddir, env.site_profile, env.site_profile_makefile)
env.site_drush_aliases = path.join(env.site_root, 'sites/all/drush')
def set_env(role):
"""
Helper function to set the correct values of the global variables in function of the role
:param role: the role to use for define the host
:return:
"""
global INTERACTIVE_MODE
INTERACTIVE_MODE = False if hasattr(env, 'mode') and env.mode == 'release' else True
global WORKSPACE
WORKSPACE = {
'local': LOCAL_WORKSPACE,
'docker': DOCKER_WORKSPACE
}[role]
global DRUPAL_ROOT
DRUPAL_ROOT = {
'local': LOCAL_DRUPAL_ROOT,
'docker': '{}/src/drupal'.format(DOCKER_WORKSPACE)
}[role]
global BUILDDIR
BUILDDIR = path.join(WORKSPACE, 'build')
global MAKEFILE
MAKEFILE = '{}/{}/{}'.format(BUILDDIR, PROFILE.keys()[0], PROFILE_MAKE_FILE)
global DRUSH_ALIASES
DRUSH_ALIASES = path.join(DRUPAL_ROOT, 'sites/all/drush')
global DOCKER_IFACE_IP
DOCKER_IFACE_IP = None
if CONTAINER_IP:
DOCKER_IFACE_IP = [(s.connect((CONTAINER_IP, 80)), s.getsockname()[0], s.close())
for s in [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)]][0][1]
def fab_run(role="local", cmd="", capture=False):
"""
Helper function to run the task locally or remotely
:param role: the role to use for define the host
:param cmd: the command to execute
:param capture: if it should return or not the output of the command
:return: the function to execute the command locally or remotely
"""
if role == "local":
return local(cmd, capture)
else:
return run(cmd)
def fab_cd(role, directory):
"""
Helper function to manage the context locally or remotely
:param role: the role to use for define the host
:param directory: the directory of context
:return: the function to manage the context locally or remotely
"""
if role == "local":
return lcd(directory)
else:
return cd(directory)
def fab_exists(role, directory):
"""
Herlper function to check if a directory exist locally or remotely
:param role: the role to use for define the host.
:param directory: the directory to check
:return: the function for check the existence of the directory locally or remotely
"""
if role == "local":
return path.exists(directory)
else:
return exists(directory)
def fab_add_to_hosts(ip, site_hostname):
"""
Helper function to add the ip and hostname to /etc/hosts
:param ip:
:param site_hostname:
:return:
"""
if confirm(green('Do you want add to the /etc/hosts the line "{} {}"? '
'If you say yes you will be able to visit the site using a more frienldy url '
'"http://{}".'.format(ip, site_hostname, site_hostname))):
# Add if not find the comment "# Docker auto-added host" to the file /etc/hosts
local('grep "# Docker auto-added host" /etc/hosts > /dev/null || '
'sudo sed -i "$ a # Docker auto-added host" /etc/hosts')
# Add the ip address and hostname after the comment "# Docker auto-added host"
local('sudo sed -i "/# Docker auto-added host/a {} {}" /etc/hosts'.format(ip, site_hostname))
def fab_remove_from_hosts(site_hostname):
"""
Helper function to remove the ip and the hostname to /etc/hosts
:param site_hostname:
:return:
"""
print(green('Enter your password to remove the {} from your /etc/hosts file'.format(site_hostname)))
local('sudo sed -i "/{}/d" /etc/hosts'.format(site_hostname))
def fab_update_hosts(ip, site_hostname):
"""
Helper function to update the file /etc/hosts
:param ip:
:param site_hostname:
:return:
"""
fab_remove_from_hosts(site_hostname)
fab_add_to_hosts(ip, site_hostname)
def hook_execute(hook, role='docker'):
"""
Execute a list of drush commands after the installation or update process
:param role Default 'role' where to run the task
:param cmds Drush commands to run, default to POST_INSTALL, it could be POST_UPDATE too.
"""
cmds = env.hook_post_install
for cmd in cmds:
with fab_cd(role, env.docker_site_root):
fab_run(role, cmd)
def _update_profile(role='local'):
"""
Update or clone the installation profile specified in the configuration file.
The build file included will be used to build the application.
"""
if fab_exists(role, '{}/{}'.format(env.builddir, env.site_profile)):
with fab_cd(role, path.join(env.builddir, env.site_profile)):
fab_run(role, 'git checkout . && git pull')
print green('{} installation profile updated in {}/{}'.format(env.site_profile, env.builddir, env.site_profile))
else:
with fab_cd(role, env.builddir):
fab_run(role, 'git clone {} {}'.format(env.site_profile_repo, env.site_profile))
print green('{} installation profile cloned in {}/{}'.format(env.site_profile, env.builddir, env.site_profile))
@roles('docker')
def _init_db(role='docker'):
"""
Create a database and a user that can access it.
"""
set_env(role)
fab_run(role, 'mysql -uroot -e "CREATE DATABASE IF NOT EXISTS {}; GRANT ALL PRIVILEGES ON {}.* TO '
'\'{}\'@\'localhost\' IDENTIFIED BY \'{}\'; GRANT ALL PRIVILEGES ON {}.* TO \'{}\'@\'{}\' '
'IDENTIFIED BY \'{}\'; FLUSH PRIVILEGES;"'.format(env.site_db_name, env.site_db_name, env.site_db_user, env.site_db_pass,
env.site_db_name, env.site_db_user, DOCKER_IFACE_IP, env.site_db_user))