Surveiller les performances d’un serveur Drupal avec le stack TICK d’influxDB et grafana

Il existe toute une palette d’outils de monitoring de serveurs, mais dans le cas présent, c’est du monitoring de site, ce qui est un peu moins commun. Il est possible de s’acquitter de cette tâche avec cacti, munin, nagios, zabbix (qui permet de configurer des scénarios de scan) mais j’ai choisis un outil dédié à cette tâche : le stack TICK (telegraf, influxdb, chronograf, kapacitor), quoi que dans la pratique, c’est surtout telegraf, influxdb et grafana que j’utilises.

Une autre possibilité aurait été le stack ELK (Elastick Search, Logstash, Kibana) qui font presque la même chose à une différence près : alors que ELK est orienté « log », c’est une base de donnée non structurée, c’est à dire analyse de fichier de log (desquels on peut remonter les informations de performance sans problème), TICK est une base de donnée temporelle, qui intègre directement les mécanismes de gestion des séries temporelles (notamment le stockage des données lissées sur une longue durée) ainsi que les outils de visualisation qui vont bien.

Installation de la partie serveur

InfluxDB

Le serveur influxdb

InfluxDB c’est la base de donnée qui stocke toutes les métriques récoltées, c’est un peu le centre névralgique du dispositif.

C’est la base de donnée centrale qui stocke les données temporelles. Voici comment l’installer.

INFLUXDB=1.2.2
URN="localhost"
RED='\033[0;31m'
NC='\033[0m' # No Color
wget --quiet https://dl.influxdata.com/influxdb/releases/influxdb_${INFLUXDB}_amd64.deb
sudo dpkg -i influxdb_${INFLUXDB}_amd64.deb
cp influxdb.conf /etc/influxdb/influxdb.conf
sudo service influxdb restart
echo -e "${RED}influxdb aviable at http://${URN}:8083${NC}"

Configuration

Voici un fichier de configuration. Dans le fichier par défaut de nombreuses options sont commentées. Cet exemple est minimaliste. Il s’agit de mettre en place un serveur qui supporte les fonctionnalités de base pour un serveur en prod, c’est à dire, enregistrer des données temps réel précises et garder une archive de ces données lissées (afin de ne pas surcharger la base trop rapidement).

### Welcome to the InfluxDB configuration file.

# we'll try to get the hostname automatically, but if it the os returns something
# that isn't resolvable by other servers in the cluster, use this option to
# manually set the hostname
# hostname = "localhost"

###
### [meta]
###
### Controls the parameters for the Raft consensus group that stores metadata
### about the InfluxDB cluster.
###

[meta]
  # Where the metadata/raft database is stored
  dir = "/var/lib/influxdb/meta"

###
### [data]
###
### Controls where the actual shard data for InfluxDB lives and how it is
### flushed from the WAL. "dir" may need to be changed to a suitable place
### for your system, but the WAL settings are an advanced configuration. The
### defaults should work for most systems.
###

[data]
  # The directory where the TSM storage engine stores TSM files.
  dir = "/var/lib/influxdb/data"

  # The directory where the TSM storage engine stores WAL files.
  wal-dir = "/var/lib/influxdb/wal"

###
### [retention]
###
### Controls the enforcement of retention policies for evicting old data.
###

[retention]
  # Determines whether retention policy enforcment enabled.
  enabled = true

  # The interval of time when retention policy enforcement checks run.
  check-interval = "24h"

###
### [admin]
###
### Controls the availability of the built-in, web-based admin interface. If HTTPS is
### enabled for the admin interface, HTTPS must also be enabled on the [http] service.
###
### NOTE: This interface is deprecated as of 1.1.0 and will be removed in a future release.

[admin]
  # Determines whether the admin service is enabled.
  enabled = true

  # The default bind address used by the admin service.
  bind-address = ":8083"

###
### [http]
###
### Controls how the HTTP endpoints are configured. These are the primary
### mechanism for getting data into and out of InfluxDB.
###

[http]
  # Determines whether HTTP endpoint is enabled.
  enabled = true

  # The bind address used by the HTTP service.
  bind-address = ":8086"


###
### [continuous_queries]
###
### Controls how continuous queries are run within InfluxDB.
###

[continuous_queries]
  # Determiens whether the continuous query service is enabled.
  enabled = true

  # Controls whether queries are logged when executed by the CQ service.
  log-enabled = true

  # interval for how often continuous queries will be checked if they need to run
  run-interval = "1h"

Création de la configuration de base: retention policy et continuous query.

Dans le cas présent, par défaut nous stockons les données « temps réels haute précision » pendant une journée et les données lissées sont archivées pour une durée indéfinie. Le downsampling (granularité d’une minute) se fait avec une requête « continue » (qui s’exécute régulièrement en tâche de fond) qui agit sur toutes les données de la base MONITOR. C’est l’une des fonctionnalités les plus intéressantes des TSDB !

    echo 'CREATE RETENTION POLICY retention_test ON MONITOR DURATION 1d REPLICATION 1 DEFAULT' | influx
    echo 'CREATE RETENTION POLICY retention_infinite ON MONITOR DURATION 0d REPLICATION 1' | influx
    echo 'CREATE CONTINUOUS QUERY "cq_downsampling_1Min" ON "MONITOR" BEGIN  SELECT mean(*) INTO "MONITOR"."retention_infinite".:MEASUREMENT FROM /.*/ GROUP BY time(1min),* END' | influx

A partir de la, on a accès à une interface minimaliste pour interroger la base (qui est censée disparaître à terme au profit de Chronograf, cf plus bas)

Installation de Grafana

Grafana permet de visualiser les données avec des graphiques et de construire des requêtes très simplement:

echo "deb https://packagecloud.io/grafana/stable/debian/ jessie main" | sudo tee /etc/apt/sources.list.d/grafana.list
curl --silent https://packagecloud.io/gpg.key | sudo apt-key add -
sudo apt-get -qq update && sudo apt-get install grafana
sudo systemctl enable grafana-server.service
sudo /bin/systemctl restart grafana-server
echo -e "${RED}grafana aviable at http://${URN}:3000${NC}"

Installation de Chronograf

Chronograf est un grafana « simplifié » livré avec influxdb. Il proposes un dashboard par défaut et permet de parcourir l’ensemble des données remontées facilement.

CHRONOGRAF=1.2.0~beta7
wget --quiet https://dl.influxdata.com/chronograf/releases/chronograf_${CHRONOGRAF}_amd64.deb
sudo dpkg -i chronograf_${CHRONOGRAF}_amd64.deb
sudo systemctl enable chronograf.service
sudo service chronograf restart
echo -e "${RED}chronograf aviable at http://${URN}:8888${NC}"

Installation et configuration de la partie client

Dans le cas présent, le serveur sert à stocker les données, le client, c’est la machine (ou la VM) qu’on surveille. Principalement, il s’agit d’une sonde (telegraf) minimaliste en terme d’utilisation de ressources (alors qu’influxdb est plus gourmand par définition).

Installation

TELEGRAF=1.2.1
wget https://dl.influxdata.com/telegraf/releases/telegraf_${TELEGRAF}_amd64.deb
sudo dpkg -i telegraf_${TELEGRAF}_amd64.deb
service telegraf restart

Configuration de base

# Telegraf configuration

[tags]

[agent]
  interval = "10s"
  round_interval = true
  flush_interval = "10s"
  flush_jitter = "0s"
  debug = false
  hostname = "Drupal"


###############################################################################
#                                  OUTPUTS                                    #
###############################################################################

[[outputs.influxdb]]
  urls = ["http://localhost:8086"] # required
  database = "MONITOR" # required
  precision = "s"

Configuration pour recupérer les performances système

###############################################################################
#                            INPUT PLUGINS                                    #
###############################################################################

# Read metrics about cpu usage
[[inputs.cpu]]
  ## Whether to report per-cpu stats or not
  percpu = true
  ## Whether to report total system cpu stats or not
  totalcpu = true
  ## If true, collect raw CPU time metrics.
  collect_cpu_time = false


# Read metrics about disk usage by mount point
[[inputs.disk]]
  ## By default, telegraf gather stats for all mountpoints.
  ## Setting mountpoints will restrict the stats to the specified mountpoints.
  # mount_points = ["/"]

  ## Ignore some mountpoints by filesystem type. For example (dev)tmpfs (usually
  ## present on /run, /var/run, /dev/shm or /dev).
  ignore_fs = ["tmpfs", "devtmpfs"]


# Read metrics about disk IO by device
[[inputs.diskio]]
  ## By default, telegraf will gather stats for all devices including
  ## disk partitions.
  ## Setting devices will restrict the stats to the specified devices.
  # devices = ["sda", "sdb"]
  ## Uncomment the following line if you need disk serial numbers.
  # skip_serial_number = false


# Get kernel statistics from /proc/stat
[[inputs.kernel]]
  # no configuration


# Read metrics about memory usage
[[inputs.mem]]
  # no configuration


# Get the number of processes and group them by status
[[inputs.processes]]
  # no configuration


# Read metrics about swap memory usage
[[inputs.swap]]
  # no configuration


# Read metrics about system load & uptime
[[inputs.system]]
  # no configuration

Configuration pour surveiller LAMP

###############################################################################
#                              SERVICE INPUTS                                 #
###############################################################################

[[inputs.apache]]
  urls = ["http://client.local/server-status?auto"]

[[inputs.logparser]]
  files = ["/var/log/apache2/access.log", "/var/log/apache2/error.log"]
  from_beginning = false
  name_override = "apache_log"
  [inputs.logparser.grok]
    patterns = ["%{COMBINED_LOG_FORMAT}"]
    #measurement = "apache_access_log"
    #custom_pattern_files = []
    #custom_patterns = '''
    #'''

[[inputs.logparser]]
  files = ["/var/log/mysql/error.log"]
  from_beginning = false
  name_override = "mysql_log"
  [inputs.logparser.grok]
    patterns = ["%{COMBINED_LOG_FORMAT}"]

[[inputs.logparser]]
  files = ["/var/log/syslog"]
  from_beginning = false
  name_override = "syslog"
  [inputs.logparser.grok]
    patterns = ["%{SYSLOG}"]
    custom_patterns = '''
      SYSLOG %{SYSLOGTIMESTAMP:syslog_timestamp} %{SYSLOGHOST:syslog_hostname} %{DATA:syslog_program}(?:\[%{POSINT:syslog_pid}\])?: %{GREEDYDATA:syslog_message}
    '''
[[inputs.ping]]
  urls = ["localhost"] # required
  count = 1
  ping_interval = 1.0
  timeout = 0.0

[[inputs.memcached]]
  servers = ["localhost:11211"]
  # unix_sockets = ["/var/run/memcached.sock"]

[[inputs.mysql]]
  servers = ["root:root@tcp(127.0.0.1:3306)/"]
  perf_events_statements_digest_text_limit  = 120
  perf_events_statements_limit              = 250
  perf_events_statements_time_limit         = 86400
  table_schema_databases                    = []
  gather_table_schema                       = true
  gather_process_list                       = true
  gather_info_schema_auto_inc               = false
  gather_slave_status                       = false
  gather_binary_logs                        = false
  gather_table_io_waits                     = false
  gather_table_lock_waits                   = false
  gather_index_io_waits                     = false
  gather_event_waits                        = false
  gather_file_events_stats                  = false
  gather_perf_events_statements             = false
  interval_slow                             = "30m"

[[inputs.net]]
  # interfaces = ["eth0"]

[[inputs.netstat]]

#[[inputs.procstat]]
#  pattern = "mysqld"
#  fielddrop = ["cpu_time_*"]
#[[inputs.procstat]]
#  pattern = "apache2"
#  fielddrop = ["cpu_time_*"]
#[[inputs.procstat]]
#  pattern = "memcached"
#  fielddrop = ["cpu_time_*"]
#[[inputs.procstat]]
#  pattern = "telegraf"
#  fielddrop = ["cpu_time_*"]

Congifuration pour surveiller Drupal

###############################################################################
#                            SERVICE INPUT PLUGINS                            #
###############################################################################

# # Statsd Server
[[inputs.statsd]]
  ## Address and port to host UDP listener on
  service_address = ":8125"
  ## Delete gauges every interval (default=false)
  delete_gauges = false
  ## Delete counters every interval (default=false)
  delete_counters = false
  ## Delete sets every interval (default=false)
  delete_sets = false
  ## Delete timings & histograms every interval (default=true)
  delete_timings = true
  ## Percentiles to calculate for timing & histogram stats
  percentiles = [90]

[[inputs.logparser]]
  files = ["/var/log/syslog"]
  from_beginning = false
  name_override = "drupal_log"
  [inputs.logparser.grok]
    patterns = ["%{DRUPAL_LOG}"]
    custom_patterns = '''
      DRUPAL_LOG %{SYSLOGTIMESTAMP:syslog_timestamp} %{SYSLOGHOST:syslog_hostname} %{DATA:syslog_program}(?:\[%{POSINT:syslog_pid}\])?: %{URI:drupal_base_url}\|%{NUMBER:drupal_log_unixtimestamp}\|%{DATA:drupal_log_type}\|%{IPORHOST:drupal_client_ip}\|%{URI:drupal_request_uri}\|%{URI:drupal_referer_uri}?\|%{NUMBER:drupal_userid}\|%{DATA:drupal_page_link}?\|%{GREEDYDATA:drupal_log_msg}
    '''

Il nous reste à configurer le module statsd de Drupal (qui envoies un rapport de performance à chaque page appelée)

 

Performance monitoring avec ELK / Grafana – test de base

Vous avez un site en production, avec le temps, il accumule de plus en plus de données, et le nombre de visiteurs augmente. Généralement les sysadmins ont tout un ensemble d’outils de surveillance et d’alertes mais peu de prophylactique (préventif).

Afin d’éviter que les performances ne s’effondrent ou ne s’érodent, il peu être intéressant sur certains projets d’anticiper en mettant en place des métriques. Généralement on va utiliser un outil de test de charge qui permet de déterminer combien d’utilisateurs simultanés un site peut supporter. Mais les tests de charges sont ponctuels et se font hors des serveurs de production.

Une autre manière de procéder (complémentaire) c’est de surveiller les performances du site (en plus de surveiller l’utilisation des ressources serveurs, bien évidement). Tout l’intérêt est de faire ça :

Dans cet article, nous allons voir comment tracer un graphe de performance sur un simple temps de réponse à la demande de chargement d’une page web. Nous verrons aussi comment aller plus loin dans les sondes à mettre en place pour surveiller plus finement les performances d’un site afin de repérer le plus tôt possible les goulots d’étranglements et de voir l’impact des différentes évolutions du site.

Installer le stack ELK

Sur une debian 8.

ELK est composé des outils suivants : Elasticsearch, logstash, et Kibana. Elasticsearch est un moteur de recherche basé sur Lucene, sans schéma, qu’il faut voir ici comme un outil d’indexation avant tout. Logstash lui est un « collecteur » de données, principalement depuis des logs (mais il permet d’aller chercher des indicateurs temps réels aussi). Kibana permet de requêter et visualiser ces données.

Attention beaucoup de tutos sur le net décrivent comment installer la version 1.3 ou 2.x. J’utilise la version 5 ici.

Préliminaire, installer java :

sudo apt-get install openjdk-8-jre

https://www.elastic.co/guide/en/elastic-stack/current/installing-elastic-stack.html

wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
sudo apt-get install apt-transport-https
echo "deb https://artifacts.elastic.co/packages/5.x/apt stable main" | sudo tee -a /etc/apt/sources.list.d/elastic-5.x.list
sudo apt-get update && sudo apt-get install elasticsearch
sudo /bin/systemctl daemon-reload
sudo /bin/systemctl enable elasticsearch.service
sudo systemctl start elasticsearch.service
#sudo systemctl stop elasticsearch.service
sudo apt-get install logstash
sudo apt-get install kibana

Nous avons elasticsearch sur le port 9200 (mais il ne réponds pas en HTTP) et Kibana sur le port 5601 (qu’on peut visiter comme un site web).

Vérification de l’installation :

curl -XGET 'localhost:9200/?pretty'

Astuce pour vider totalement Elastic (pratique en cas de fausse manip) :

curl -XDELETE localhost:9200/*

Configurer logstash

A partir de la, il faut configurer logstash pour remonter les données qui nous intéressent dans elasticsearch, à savoir, les temps de réponses de notre site. Je prends emh.fr comme exemple de site (ce n’est pas un vrai site).

etc/logstash/conf.d/perfmon.conf :

input { 
    # Web Application Response Time
    exec {
        type => "ws-ping"
        add_field => [ "service" , "emh" ]
        add_field => [ "received_at", "%{@timestamp}" ]

        command => "/usr/bin/time -f '%e' curl -sk -o /dev/null http://www.emh.fr 2>&1"
        interval => 60
    }
}
 
filter {  
    if [type] == "ws-ping" {
        grok {
            match => { "message" => "%{NUMBER:responsetime:float}" }
        }
    }
}
 
 
output {
    elasticsearch { 
       hosts => ["localhost"] 
       index => "logstash-perf-emh"
    }
}

Kibana

Installer Grafana

Kibana est certes très puissant. Pourquoi Grafana ? Kibana serait finalement suffisant mais souffre d’un gros défaut (en version gratuite) : c’est une page non protégée. Grafana, qui est très puissant et versatile dispose d’une page de login. En plus Grafana peut se brancher sur plusieurs sources, dispose de plugins très facile à installer.

http://docs.grafana.org/installation/debian/

echo "deb https://packagecloud.io/grafana/stable/debian/ jessie main" | sudo tee -a /etc/apt/sources.list.d/grafana.list
curl https://packagecloud.io/gpg.key | sudo apt-key add -
sudo apt-get update && sudo apt-get install grafana
systemctl daemon-reload
systemctl start grafana-server
systemctl status grafana-server
sudo systemctl enable grafana-server.service

Configurer Grafana

Par défaut on travaille sur le port 3000 : http://localhost:3000   admin/admin

Ajouter une datasource, ici j’ai mis « logstash-* » car je souhaite pouvoir grapher plusieurs index, mais j’aurais pu mettre « logstash-perf-emh » :

Pour créer une visualisation, il faut créer un dashboard et lui rajouter une ligne (row). Ensuite on rajoute une query sur une datasource et on défini des métriques à afficher.

D’une manière générale, la création de graphiques sous Grafana est plus intuitive que sous Kibana. La possibilité d’intervenir en aval sur les valeurs (Script : _value*1000) peut-être utile.

Tests de scalabilité avec Locust et Taurus

jMeter est lourd à mettre en œuvre et complexe. A la place j’ai trouvé des petits outils plus simples, mais très puissants et scriptables en ligne de commande.

Locust.io qui permet de faire des tests de performances et Taurus qui permet d’aller plus loin avec des tests de scalabilité.

Installation de Locust

apt-get install python-pip python-dev && pip install locustio

Création du fichier locustfile.py qui indique les scénarios à tester. Ici on teste la HP, puis le login, puis l’accès à la page /node/add/request qui est un formulaire important sur ce site :

from locust import HttpLocust, TaskSet

def login(l):
    l.client.post("/user", {"name":"admin", "pass":"admin", "form_id":"user_login"})

def index(l):
    l.client.get("/")

def create_request(l):
    l.client.get("/node/add/request")

class UserBehavior(TaskSet):
    tasks = {index:2, create_request:1}

    def on_start(self):
        login(self)

class WebsiteUser(HttpLocust):
    task_set = UserBehavior
    min_wait = 5000
    max_wait = 9000

Ensuite on lance le serveur :

locust --host=http://10.0.2.2:8088/emh/www/

Les résultats donnent quelque chose comme ceci :

Screenshot of Locust web UI

On peut grâce au lien « Edit » en dessous de status faire facilement varier le nombre de clients et le nombre de répétitions des tests pour simuler une montée en charge. Il suffit ensuite de surveiller l’apparition des #fails pour déterminer le seuil de scalabilité.

Installation de taurus

Taurus est un complément à Locust.io : il permet de planifier l’exécution d’une montée en charge de manière très précise de manière à surveiller exactement ou se situent les seuils qui font « exploser » votre site. En complément on pourra installer un système de monitoring du serveur comme le stack TICK par exemple qui permettra d’avoir des métriques précises sur le maillon faible du serveur.
Quelques pré-requis

pip install --upgrade pip && pip install --upgrade requests && apt-get install python-dev libxml2-dev libxslt1-dev zlib1g-dev && pip install bzt

Création d’un fichier de test: emh_test.yml

---
execution:
- executor: locust
  concurrency: 10
  ramp-up: 1m
  hold-for: 3m
  iterations: 1000
  scenario: example

scenarios:
  example:
    default-address: http://emh.box.local
    script: locustfile.py

reporting:
- final_stats
- console

Pour obtenir les résultats il faut lancer le serveur :

bzt emh_test.yml

Et maintenant, dernière petite amélioration. On va lancer plusieurs scénarios en différé en rajoutant dans le fichier de configuration :

- executor: locust
  concurrency: 10
  ramp-up: 1m
  hold-for: 2m
  iterations: 1000
  scenario: example
  delay: 1m
- executor: locust
  concurrency: 10
  ramp-up: 1m
  hold-for: 1m
  iterations: 1000
  scenario: example
  delay: 2m

behat & drupal 8 : exemple complet et simple

Addendum : behat sera entièrement installé dans drupal 8.3 (5 avril 2017)

Une fois drupal et le composer installés correctement, dans /var/www/d8 avec un compte admin/admin

Le tuto officiel n’est pas vraiment complet/utilisatble tel quel (le behat.yml par défaut n’est pas complet).

Pour installer, j’utilises le composer installé globalement :

composer require drupal/drupal-extension='~3.0'

Ensuite behat est rajouté automatiquement dans « vendor » ou il y avait déjà un répertoire behat, mais pas complet.

vendor/behat/behat/bin/behat --init

Maintenant on créer le behat.yml minimaliste pour pouvoir lancer les tests :

default:
  suites:
    default:
      contexts:
        - Drupal\DrupalExtension\Context\DrupalContext
        - Drupal\DrupalExtension\Context\MinkContext
  extensions:
    Behat\MinkExtension:
      goutte: ~
      base_url: http://localhost/d8
    Drupal\DrupalExtension:
      api_driver: "drupal"
      blackbox: ~
      drupal:
        drupal_root: "/var/www/d8"

Premier test basique dans features/navigate.feature

  Everything from the site.

  Scenario: Title
    Given I am on the homepage
    Then I should see "Drupal"

  Scenario: Log in
    Given I visit "/user"
    # fill the username and password input fields, and click submit
    When I fill in "Username" with "admin"
    And I fill in "Password" with "admin"
    And I press the "Log in" button
    Then I should get a "200" HTTP response
    And I should see text matching "Log out"

On lance le test :

vendor/behat/behat/bin/behat
 @d8 @api
 Feature: Navigation
 Everything from the site.
Scenario: Title # features/navigate.feature:5
 Given I am on the homepage # Drupal\DrupalExtension\Context\MinkContext::iAmOnHomepage()
 Then I should see "Drupal" # Drupal\DrupalExtension\Context\MinkContext::assertPageContainsText()
Scenario: Log in # features/navigate.feature:9
 Given I visit "/user" # Drupal\DrupalExtension\Context\MinkContext::assertAtPath()
 When I fill in "Username" with "admin" # Drupal\DrupalExtension\Context\MinkContext::fillField()
 And I fill in "Password" with "admin" # Drupal\DrupalExtension\Context\MinkContext::fillField()
 And I press the "Log in" button # Drupal\DrupalExtension\Context\MinkContext::pressButton()
 Then I should get a "200" HTTP response # Drupal\DrupalExtension\Context\MinkContext::assertHttpResponse()
 And I should see text matching "Log out" # Drupal\DrupalExtension\Context\MinkContext::assertPageMatchesText()
2 scenarios (2 passed)
 8 steps (8 passed)
 0m0.69s (26.92Mb)

 

Liste des librairies de Drupal8 (répertoire vendor)

  • asm89/stack-cors : Cross-origin resource sharing library and stack middleware.
  • behat : Behat is an open source Behavior Driven Development framework for PHP 5.3+.
  • composer : Dependency Manager for PHP.
  • doctrine : The Doctrine Project is the home to several PHP libraries primarily focused on database storage and object mapping.
  • easyrdf : A PHP library designed to make it easy to consume and produce RDF.
  • egulias/email-validator : EmailValidator – PHP Email validator library inspired in @dominicsayers isemail
  • fabpot/goutte : Goutte is a screen scraping and web crawling library for PHP.
  • guzzlehttp : Guzzle is a PHP HTTP client that makes it easy to send HTTP requests and trivial to integrate with web services.
  • ircmaxell/password-compat : A compatibility library for the proposed simplified password hashing algorithm
  • jcalderonzumba/gastonjs : PhantomJS API based server for webpage automation.
  • jcalderonzumba/mink-phantomjs-driver : PhantomJS driver for Mink framework. Mink is an open source browser controller/emulator for web applications, written in PHP 5.3.
  • masterminds/html5 : An HTML5 parser and serializer for PHP.
  • mikey179/vfsStream : vfsStream is a stream wrapper for a virtual file system that may be helpful in unit tests to mock the real file system.
  • paragonie/random_compat : PHP 5.x support for random_bytes() and random_int()
  • phpdocumentor : phpDocumentor enables you to generate documentation from your PHP source code.
  • phpspec : A php toolset to drive emergent design by specification.
  • phpunit : Welcome to PHPUnit! PHPUnit is a programmer-oriented testing framework for PHP. It is an instance of the xUnit architecture for unit testing frameworks.
  • psr : norm
  • pusher : PHP library for interacting with the Pusher HTTP REST API
  • sebastian/comparator : Provides the functionality to compare PHP values for equality.
  • sebastian/diff : Diff implementation
  • stack : Composing HttpKernelInterface middlewares since 2013!
  • symfony
    • asset
    • browser-kit : Simulates the behavior of a web browser.
    • cache
    • class-loader : The PSR-0 Class Loader: loads classes that follow the PSR-0 class naming standard
    • config
    • console : The Console component allows you to create command-line commands.
    • css-selector : Converts CSS selectors to XPath expressions.
    • debug : The Debug component provides tools to ease debugging PHP code.
    • dependency-injection : The DependencyInjection component allows you to standardize and centralize the way objects are constructed in your application.
    • dom-crawler : The DomCrawler will attempt to automatically fix your HTML to match the official specification.
    • event-dispatcher : The EventDispatcher component provides tools that allow your application components to communicate with each other by dispatching events and listening to them.
    • expression-language
    • file-system
    • finder
    • form
    • http-foundation : The HttpFoundation component defines an object-oriented layer for the HTTP specification.
    • http-kernel : The HttpKernel component provides a structured process for converting a Request into a Response by making use of the EventDispatcher component. It’s flexible enough to create a full-stack framework (Symfony), a micro-framework (Silex) or an advanced CMS system (Drupal).
    • polyfill : This project backports features found in the latest PHP versions and provides compatibility layers for some extensions and functions. It is intended to be used when portability across PHP versions and extensions is desired. polyfill-apcu / polyfill-iconv / polyfill-mbstring / polyfill-php54 / polyfill-php55
    • intl
    • ldap
    • option-resolver
    • phpunit
    • process : he Process component executes and monitor commands in sub-processes.
    • property-access
    • property-info
    • psr-http-message-bridge : The PSR-7 bridge converts HttpFoundation objects from and to objects implementing HTTP message interfaces defined by the PSR-7.
    • routing : Whenever you have a {placeholder} in your route path, that portion becomes a wildcard: it matches any value.
    • security
    • serializer : The Serializer component is meant to be used to turn objects into a specific format (XML, JSON, YAML, …) and the other way around.
    • stopwatch : The Stopwatch component provides a way to profile code.
    • templating
    • translation : Translations are handled by a translator service that uses the user’s locale to lookup and return translated messages
    • validator : So far, this is just an ordinary class that serves some purpose inside your application.
    • var-dumper
    • yaml : The Symfony Yaml component parses YAML strings to convert them to PHP arrays
  • symfony-cmf/routing : routingRouting component building on the Symfony2 Routing component.
  • twig : Twig – The flexible, fast, and secure template engine for PHP.
  • webmozart/assert : assert – Assertions to validate method input/output with nice error messages.
  • wikimedia/composer-merge-plugin : composermergepluginMerge one or more additional composer.json files at Composer runtime.
  • zendframework
    • zend-diactoros : PSR-7 HTTP Message implementation. Contribute to zenddiactoros development by creating an account on GitHub.
    • zend-escaper : zendescaper. Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs
    • zend-feed : Feed component from Zend Framework. Contribute to zendfeed development by creating an account on GitHub.
    • zend-stdlib : Zend\Stdlib is a set of components that implements general purpose utility class for different scopes like:
      • array utilities functions;
      • json serializable interfaces;
      • general messaging systems;
      • string wrappers;
      • etc.

 

http://symfony.com/doc/current/components/index.html

 

Import d’un site existant dans aegir depuis un dépôt git

Petit préambule pour bien comprendre le vocabulaire d’aegir.

Imaginons que vous ayez un site sur monurl.fr qui pointe sur /var/www/monsite (la plateforme) dans votre vhost avec un sous-répertoire /sites/monurl.fr (le site), le tout géré par votre dépôt git sous gitlab.

Pour faire simple aegir appelle un drupal sans base de donnée une plateforme et un drupal avec sa base de donné un site :

  • une plateforme au sens aegir, c’est le dépôt git d’un répetoire drupal avec sa branche courante (qu’on ne peut plus changer à postériori)
  • une site au sens aegir c’est : à l’intérieur d’une plateforme, un répertoire dans sites avec le settings.php (généré par aegir, il ne faut pas y toucher), une base de donnée, un alias, et un vhost

Etape 1 : créer la plateforme à partir du dépôt git

Une plateforme, c’est un répertoire PHP avec un drupal installé dedans, mais sans base de donnée associée (une plateforme peut contenir plusieurs sites).

http://aegir.monurl.fr/node/add/platform

Donner un nom « monsite » par exemple dans le BO aegir pour votre plateforme.

On relie la plateforme à un dépôt git avec sa branche sous la forme :

ssh://git@gitlab.monurl.fr:22443/mon-depot.git

Attention, il faut utiliser le protocole ssh car il y a une clé ssh définie sur gitlab et sur la VM pour pouvoir cloner sans mot de passe. Via https ça ne fonctionnera pas.

Choisir le mode git pull (Git pull task pour des pull manuels).

Aegir va cloner le dépôt du site dans le répertoire

/var/aegir/platforms/monsite

Il va automatiquement repérer les sites dans /var/aegir/platforms/monsite/sites/ et tenter de les importer automatiquement, mais ça ne va pas fonctionner. C’est normal, pas de panique 😉

Idéalement, il faudrait que répertoire n’existe pas dans le dépôt git.

Etape 2 : suppression du site importé

Il faut donc supprimer le répertoire du site aegir :

# rm -r /var/aegir/platforms/monsite/sites/monurl.fr/

et supprimer le nœud du site (automatiquement créé) dans aegir aussi :

http://aegir.monurl.fr/node/add/platform#overlay=admin/content : supprimer le noeud « monurl.fr »

Ensuite, il faut recréer le site « à vide » puis le « remplir » avec le vrai site.

Pourquoi cette étape de suppression avant la re-création ?

Parce que aegir lors de l’import est censé créer une base de donnée reconnue par aegir pour le nouveau site. Comme l’import plante, il ne crée pas la base de donnée.

Il faut supprimer et re-créer pour qu’il fasse les choses proprement et qu’on ai cette fameuse base de donnée par défaut gérée par aegir, dans laquelle on va pouvoir importer notre site.

Etape 3 : création du site

Cette fois on va rattacher le site à une plateforme, ce qui revient à créer une base de donnée, un répertoire dans /sites, et un settings.php qui relie les deux.

http://aegir.monurl.fr/node/add/site

dans le domain name mettre : monurl.fr

Laisser vide le deploy from git (Repository URL : vide).

Attention à bien choisir la plateforme récemment crée pour que le site soit créé au bon endroit.

A ce moment la aegir fait plusieurs choses :

  • il crée une base de donnée vide : monurl_0
  • il lance l’installation par défaut de drupal
  • il crée un alias drush : su – aegir -c « drush status » : /var/aegir/.drush/monurl.fr.alias.drushrc.php
  • il crée un vhost qui contient des paramètres utilisées dans le settings.php aussi : port 80 namevhost monurl.fr (/var/aegir/config/server_master/apache/vhost.d/monurl.fr:1)

NB : monurl.fr existe déjà dans votre ancien vhost. Si vous regardez votre configuration apache, vous verrez qu’il y en a 2, mais celui d’aegir est prioritaire.

Etape 4 : import des données

Il y a deux choses à faire, comme pour tout clonage de site : récupérer les fichiers dans files et récupérer la base de donnée initiale.

#cp -a /var/www/monsite/sites/monurl.fr/files /var/aegir/platforms/monsite/sites/monurl.fr/
# chown -R aegir:aegir /var/aegir/platforms/monsite/sites/monurl.fr/files

#mysqldump -u root -p'PASSWORD' monsite | su - aegir -c 'drush @monurl.fr sqlc'

Etape 5 (optionnel) : supprimer l’ancien site

Si on veut supprimer la base de donnée initiale :

#cd /var/www/monsite/sites/monurl.fr ; drush sql-drop -y

Si on ne veut supprimer que le site :

#rm -r /var/www/monsite/sites/monurl.fr

Si on n’a plus besoin de la plateforme :

#rm -r /var/www/monsite

Etape 6 : mise à jour du site

A partir de la, vous pouvez gérer votre site dans aegir, et donc, demander un backup d’un simple click, cloner votre site, et plein d’autres choses (en fonction des sous-modules que vous aurez activés).

Pour faire une mise à jour (si vous avez updaté votre dépôt git) voici les étapes à suivre.

Faire un git pull sur la plateforme, dans les tâches : http://aegir.monurl.fr/hosting/c/platform_monsite

Faire le « Revert features » et le « Run db updates » sur les tâches du site : http://aegir.monsite.fr/hosting/c/monurl.fr

On peut faire la même chose en ligne de commande, mais il faut être en user aegir pour que ça fonctionne.

Installation d’aegir pour gérer un parc de sites Drupal

aegir est un outil web qui permet de piloter un parc de sites Drupal. Ce qu’on fait en général via des commandes drush (drush updb, drush fra, etc…) on peut le faire via un site web directement. C’est un site Drupal, qui pilote la gestion d’autres sites Drupal. Bien sûr, on pourrait faire tout ça et même plus via des scripts ou quelques outils de supervision.

L’intérêt c’est de donner à l’utilisateur (le client) la possibilité de créer des sites à la volé par exemple, ou de lancer des opérations sur plusieurs sites à la fois, sans « mettre les mains dans le cambouis ».

Il s’agit d’une installation mono-serveur dans le cas présent, sur une machine virtuelle contenant déjà un ensemble de sites Drupal qu’on souhaite migrer vers aegir afin de pouvoir gérer les sites dessus (mise à jour, clonage, backup).

Installation d’aegir

Je me place dans un contexte debian/apache/mysql/php classique pour un drupal 7 (ou 8). Je supposes que vous avez déjà installé drush.

Avant toute chose, il faut s’assurer de 3 choses :

  • vous disposez du mot de passe root de MySQL
  • vous êtes sudoer
  • vous avez un nom de domaine (optionnel mais plus propre) aegir.mondomaine.com ou aegir-mondomaine.com de disponible.

Pour installer aegir, c’est simple :

echo "deb http://debian.aegirproject.org stable main" | sudo tee -a /etc/apt/sources.list.d/aegir-stable.list
curl http://debian.aegirproject.org/key.asc | sudo apt-key add -
sudo apt-get update
sudo apt-get install aegir3

Optionnellement vous pouvez faire un ‘sudo mysql_secure_installation’ au préalable pour plus de sécurité (pour ma part, je fais confiance au sysadmin qui a installé la VM).

A partir de la, l’installateur va vous demander un nom de domaine pour le futur site aegir et le mot de passe root mysql pour créer sa base donnée.

Vérification de l’installation

Une fois l’installation effectuée, vous obtenez ceci :

  • un user linux aegir
  • son répertoire home avec tout une install dedans : /var/aegir
  • un nouveau vhost
# apachectl -S
VirtualHost configuration:
wildcard NameVirtualHosts and _default_ servers:
*:80                   is a NameVirtualHost
         default server default (/etc/apache2/conf.d/aegir.conf:5)
         port 80 namevhost default (/etc/apache2/conf.d/aegir.conf:5)
         port 80 namevhost aegir.monsite.fr (/var/aegir/config/server_master/apache/vhost.d/aegir.monsite.fr:1)

Aegir va automatiquement créer des alias drush pour vos nouveaux sites. Mais, pour les obtenir, il faut être loggué en tant qu’aegir. Donc au lieu d’un drush status il faut faire :

su - aegir -c 'drush status'
 PHP configuration      :  /etc/php5/cli/php.ini                                                                                                                                                       
 PHP OS                 :  Linux
 Drush script           :  /usr/local/bin/drush
 Drush version          :  8.1.2
 Drush temp directory   :  /tmp
 Drush configuration    :  /var/aegir/.drush/drushrc.php
 Drush alias files      :  /var/aegir/.drush/monsite.fr.alias.drushrc.php /var/aegir/.drush/hm.alias.drushrc.php /var/aegir/.drush/server_master.alias.drushrc.php  
                           /var/aegir/.drush/hostmaster.alias.drushrc.php /var/aegir/.drush/platform_hostmaster.alias.drushrc.php /var/aegir/.drush/server_localhost.alias.drushrc.php                

So far so good.

Nous verrons dans un prochain article comment rajouter des sites existant au sein d’aegir.

Retex : sécurisation Drupal

Je me suis fait hacker sur l’un de mes sites Drupal en production, sur le serveur qui était mal protégé.

L’alerte

Tout commence par OVH qui bloque l’envoies d’email parce que le serveur est repéré comme spammeur :

Bonjour,

Notre protection Anti-Spam a détecté un envoi important de spam à partir d'une de vos IP: 
92.XX.XX.XXX

Afin d'assurer la sécurité de notre réseau le trafic sortant de votre serveur vers les
ports 25 a été suspendu.

Afin que vous puissiez effectuer les vérifications voici un échantillon des emails bloqués:

Destination IP: 217.69.139.150 - Message-ID: <span id="OBJ_PREFIX_DWT169_com_zimbra_email" class="Object"><span id="OBJ_PREFIX_DWT170_com_zimbra_email" class="Object">20160325090323.E07981XXXXX@vpsXXXXX.ovh.net</span></span> - Spam score: 9999
Destination IP: 66.102.1.27 - Message-ID: <span id="OBJ_PREFIX_DWT171_com_zimbra_email" class="Object"><span id="OBJ_PREFIX_DWT172_com_zimbra_email" class="Object">20160325090402.E91EE1XXXXX@vpsXXXXX.ovh.net</span></span> - Spam score: 9999
Destination IP: 194.186.47.93 - Message-ID: <span id="OBJ_PREFIX_DWT173_com_zimbra_email" class="Object"><span id="OBJ_PREFIX_DWT174_com_zimbra_email" class="Object">20160325090343.81C801XXXXX@vpsXXXXX.ovh.net</span></span> - Spam score: 9999
Destination IP: 94.100.180.150 - Message-ID: <span id="OBJ_PREFIX_DWT175_com_zimbra_email" class="Object"><span id="OBJ_PREFIX_DWT176_com_zimbra_email" class="Object">20160325090422.973121XXXXX@vpsXXXXX.ovh.net</span></span> - Spam score: 9999
Destination IP: 94.100.180.150 - Message-ID: <span id="OBJ_PREFIX_DWT177_com_zimbra_email" class="Object"><span id="OBJ_PREFIX_DWT178_com_zimbra_email" class="Object">20160325090441.E3F061XXXXX@vpsXXXXX.ovh.net</span></span> - Spam score: 9999

Merci de consulter attentivement ce guide:

    <span id="OBJ_PREFIX_DWT179_com_zimbra_url" class="Object"><span id="OBJ_PREFIX_DWT180_com_zimbra_url" class="Object"><a href="http://guide.ovh.net/AntiSpamBestPratice" target="_blank">http://guide.ovh.net/AntiSpamBestPratice</a></span></span>

Si vous avez identifié et résolu la cause du blocage, vous pouvez débloquer votre IP
depuis le manager, en vous rendant à cette adresse :

    <span id="OBJ_PREFIX_DWT181_com_zimbra_url" class="Object"><span id="OBJ_PREFIX_DWT182_com_zimbra_url" class="Object"><a href="https://www.ovh.com/manager/#/configuration/ip?action=antispam&amp;ip=92.XX.XX.XX&amp;ipSpamming=92.XX.XX.XX" target="_blank">https://www.ovh.com/manager/#/configuration/ip?action=antispam&amp;ip=92.XX.XX.XX&amp;ipSpamming=92.XX.XX.XX</a></span></span>

C’est une IP Russe qui envoies des emails (probablement du SPAM) vers les Chinois. Ce qui signifie que nous avons été hacké.

A partir de la, j’ai fait appel à un sysadmin pour découvrir la faille et protéger le site correctement.

L’audit

L’expert sécurité repère vite l’apparition d’un nouveau thème « fusion core » qui n’a rien à faire la. D’ailleurs git lui même nous le dit :

/var/www/prod/sites/all/themes/fusion/fusion_core/system_ml.php

Après que son robot ai repéré le site, et se soit loggué (l’inscription peut se faire de manière automatique), il passe par le authorise.php pour demander l’installation d’un « theme » qui contient des scripts tels que « shell.php » qui permet ensuite de prendre le contrôle de la machine.
Voici un extrait des logs d’apache :

/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT407_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT408_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:48:12 +0100] "GET / HTTP/1.1" 200 34864 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari
/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT409_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT410_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:48:12 +0100] "POST / HTTP/1.0" 302 680 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/
537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT411_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT412_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:48:13 +0100] "POST /?destination=node/323 HTTP/1.0" 200 193793 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chr
ome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT413_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT414_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:48:20 +0100] "GET /admin/modules HTTP/1.1" 200 68620 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2
454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT415_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT416_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:48:30 +0100] "POST /admin/modules HTTP/1.0" 200 740266 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0
.2454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT417_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT418_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:49:21 +0100] "GET /admin/appearance/install HTTP/1.1" 200 147306 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) C
hrome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT419_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT420_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:49:22 +0100] "POST /admin/appearance/install HTTP/1.0" 302 465 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chr
ome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT421_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT422_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:49:23 +0100] "POST /authorize.php?batch=1&amp;amp;op=start&amp;amp;id=1901 HTTP/1.0" 200 245 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, l
ike Gecko) Chrome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT423_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT424_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:49:24 +0100] "POST /authorize.php?batch=1&amp;amp;id=1901&amp;amp;op=do HTTP/1.0" 200 312 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like
  Gecko) Chrome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT425_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT426_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:49:25 +0100] "GET /authorize.php?batch=1&amp;amp;id=1901&amp;amp;op=finished HTTP/1.1" 302 297 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML,
  like Gecko) Chrome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT427_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT428_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:49:26 +0100] "GET /authorize.php HTTP/1.1" 200 3699 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.24
54.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT429_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT430_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:49:27 +0100] "GET /admin/modules HTTP/1.1" 200 68620 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2
454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT431_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT432_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:49:38 +0100] "POST /admin/modules HTTP/1.0" 200 739926 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0
.2454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT433_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT434_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:50:18 +0100] "GET /sites/all/themes/fusion/fusion_core/shell.php HTTP/1.1" 200 368 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KH
TML, like Gecko) Chrome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT435_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT437_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:20:05:06 +0100] "POST /sites/all/themes/fusion/fusion_core/shell.php HTTP/1.0" 200 23488 "&lt;span id="OBJ_PREFIX_DWT436_com_zimbra_url" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT438_com_zimbra_url" class="Object"&gt;&lt;a href="http://www.emindhub.com/sites/all/themes/fusion/fusi" target="_blank"&gt;http://www.emindhub.com/sites/all/themes/fusion/fusi&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;
on_core/shell.php" "Opera/9.80 (Windows NT 5.1; U; ru) Presto/2.9.168 Version/11.50"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT439_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT441_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:20:05:06 +0100] "POST /sites/all/themes/fusion/fusion_core/shell.php HTTP/1.0" 200 24083 "&lt;span id="OBJ_PREFIX_DWT440_com_zimbra_url" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT442_com_zimbra_url" class="Object"&gt;&lt;a href="http://www.emindhub.com/sites/all/themes/fusion/fusi" target="_blank"&gt;http://www.emindhub.com/sites/all/themes/fusion/fusi&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;
on_core/shell.php" "Opera/9.80 (Windows NT 5.1; U; ru) Presto/2.9.168 Version/11.50"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT443_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT444_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:20:05:06 +0100] "GET /sites/all/themes/fusion/fusion_core/shell.php HTTP/1.0" 200 24073 "-" "Opera/9.80 (Windows NT 5.1; U; ru) Presto/2.9.168
  Version/11.50"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT445_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT446_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:20:15:41 +0100] "POST /sites/all/themes/fusion/fusion_core/system_ml.php HTTP/1.1" 200 1126 "-" "Mozilla/5.0 (Windows NT 6.1; rv:38.0) Gecko/2
0100101 Firefox/38.0"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT447_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT449_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:20:15:41 +0100] "POST /sites/all/themes/fusion/fusion_core/system_ml.php HTTP/1.1" 200 1080 "&lt;span id="OBJ_PREFIX_DWT448_com_zimbra_url" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT450_com_zimbra_url" class="Object"&gt;&lt;a href="http://www.emindhub.com/sites/all/themes/fusion/f" target="_blank"&gt;http://www.emindhub.com/sites/all/themes/fusion/f&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;
usion_core/system_ml.php" "Mozilla/5.0 (Windows NT 6.1; rv:38.0) Gecko/20100101 Firefox/38.0"

Un nslookup nous indique que l’attaque est lancée depuis un serveur amazon (difficile donc d’aller plus loin) :

nslookup 52.29.249.200
Server:        212.27.40.240
Address:    212.27.40.240#53

Non-authoritative answer:
200.249.29.52.in-addr.arpa    name = ec2-52-29-249-200.eu-central-1.compute.amazonaws.com.

Authoritative answers can be found from:
29.52.in-addr.arpa    nameserver = pdns1.ultradns.net.
29.52.in-addr.arpa    nameserver = x1.amazonaws.com.
29.52.in-addr.arpa    nameserver = x3.amazonaws.org.
29.52.in-addr.arpa    nameserver = x4.amazonaws.org.
29.52.in-addr.arpa    nameserver = x2.amazonaws.com.

Veuillez noter le passage par :

/admin/appearance/install
/authorize.php?batch=1&amp;op=start&amp;id=1901
/sites/all/themes/fusion/fusion_core/shell.php

Du coup l’expert a testé le XSS par acquis de conscience :

First Name : guest<script>alert('attacked')</script>
Last Name : <h1>guest<h1>

En effet, il manquait des « check_plain » sur l’affichage des profils utilisateurs.

Mais ce fût loin d’être terminé :

  • répertoires de tests behat et de deploy dans le root drupal qui pouvaient donner accès à des informations importantes sur le site
  • un adminer.php à la racine du VPS, bien pratique pour développer, mais aurais du être à minima derrière un htpasswd
  • sites de dev non protégés derrière un htpasswd (et pour cause, ça empêche les tests behat de fonctionner)
  • une seule VM pour la prod et les devs : du temporaire qui dure.

Actions menées

  • Migration vers un serveur de prod séparé du serveur de dev
  • déplacement de la racine drupal dans ${GITROOT}/www
  • déplacement du répertoire bdd, deploy et autres répertoires non drupal à la racine
  • sécurisation de la conf apache (intégration du htaccess directement dedans + protection accrue des répertoires)
  • accès interdit aux scripts drupal dangereux : install.php, authorize.php
  • répertoire tmp public private
  • mise en place du ssl
  • passage du script « fix perm » sur la racine drupal afin de s’assurer qu’il n’y a pas de répertoires qui sont trop ouverts (ou inaccessibles)

Améliorer Behat pour Drupal avec 3 extensions : screenshot, code coverage, et watchdog

Behat est très puissant pour faire des tests de non regression. Comment le rendre encore plus puissant, avec 3 petites extensions très pratiques pour le debug :

  1. En affichant les warning rajoutés dans le watchdog automatiquement à la fin d’un test. Très pratique pour s’assurer qu’il n’y a pas d’erreur cachées pendant l’exécution des tests
  2. En rajoutant un test de couverture du code avec xdebug et phpcov pour voir si tout est bien testé
  3. En prenant un screenshot automatique de l’étape behat si elle plante, afin de pouvoir voir ou est le problème sans avoir passer par un « Then I break »

Rajouter un display du watchdog à la fin d’un scénario

dans le composer.json de votre répertoire behat (il faut avoir installé composer avant bien sûr)

{
  "require": {
    "drupal/drupal-extension": "~3.0",
    "jorgegc/behat-drupal-extension": "*"
}

dans le shell on fait la mise à jour :

composer update

On rajoute le contexte dans behat.yml

default:
  suites:
    default:
      contexts:
        - JGC\Behat\DrupalExtension\Context\WatchdogContext

Et voila, c’est tout. Il aller chercher un vendor/jorgegc/behat-drupal-extension/src/JGC/Behat/DrupalExtension/Context/WatchdogContext.php qui fait tout le job.

Il suffit de rajouter @watchdog à vos scénarios.

Rajouter un test de couverture de code

dans le composer.json

{
"require": {
"drupal/drupal-extension": "~3.0",
"phpunit/php-code-coverage": "^2.2",
"phpunit/phpcov": "*",
}

Ne pas oublier le « composer update » bien sûr. Et le contexte qui va bien avec features/bootstrap/CoverageContext.php :

use Behat\Behat\Hook\Scope\BeforeScenarioScope;
//require_once('bdd/vendor/phpunit/php-code-coverage/src/CodeCoverage/Filter.php');

/**
 * Created by PhpStorm.
 * User: elie
 * Date: 01/09/15
 * Time: 11:29
 */
class CoverageContext implements Context
{
    /**
     * @var PHP_CodeCoverage
     */
    private static $coverage;

    /** @BeforeSuite */
    public static function setup()
    {
        $filter = new PHP_CodeCoverage_Filter();
        $filter->addDirectoryToBlacklist(__DIR__ . "/../../vendor");
        $filter->addDirectoryToWhitelist(__DIR__ . "/../../src");
        self::$coverage = new PHP_CodeCoverage(null, $filter);
    }

    /** @AfterSuite */
    public static function tearDown()
    {
        $writer = new PHP_CodeCoverage_Report_HTML();
        $writer->process(self::$coverage, __DIR__ . "/../../tmp/coverage");
    }

    private function getCoverageKeyFromScope(BeforeScenarioScope $scope)
    {
        $name = $scope->getFeature()->getTitle() . '::' . $scope->getScenario()->getTitle();
        return $name;
    }

    /**
     * @BeforeScenario
     */
    public function startCoverage(BeforeScenarioScope $scope)
    {
        self::$coverage->start($this->getCoverageKeyFromScope($scope));
    }

    /** @AfterScenario */
    public function stopCoverage()
    {
        self::$coverage->stop();
    }
}

Il faut avoir installé xdebug au préalable. Évidement, en général vos tests behat ne concernent que votre code métier.  phpcov va sûrement vous dire que vous n’avez pas tout testé, et pour cause, vous ne couvrez pas l’ensemble du BO Drupal. Il faudrait donc encore rajouter tous les bons répertoires dans la partie  « addDirectoryToBlacklist ».

Installer xdebug:

sudo apt-get install php5-dev php-pear
sudo pecl install xdebug
find / -name 'xdebug.so' 2> /dev/null

/etc/php5/conf.d/20-xdebug.ini :

zend_extension=/usr/lib/php5/20100525/xdebug.so
xdebug.max_nesting_level=200

La procédure normale n’ayant pas fonctionné dans mon cas (Debian 7).

Screenshot automatique en cas d’erreur

Cette fois ci, c’est un contexte qu’on va rajouter.

Ce n’est pas tout à fait un screenshot qui est fait. Ce n’est pas un .png qui généré, mais une sauvegarde du HTML généré, qu’on peut visualiser dans un navigateur. Et en fait, c’est encore mieux puisque ça nous permet, en plus de voir la page affichée, de voir les éléments HTML.

C’est cette ligne qui fait tout le boulot, le reste ne concerne que l’écriture dans un fichier :

      $html_data = $this->getSession()->getDriver()->getContent();

features/bootstrap/ScreenshotContext.php

<?php

use Drupal\DrupalExtension\Context\RawDrupalContext,
    Drupal\DrupalExtension\Context\DrupalContext;
use Behat\MinkExtension\Context\MinkContext;
use Behat\Behat\Hook\Scope\BeforeScenarioScope,
    Behat\Behat\Hook\Scope\AfterStepScope;

/**
 * Defines application features from the specific context.
 */
class ScreenshotContext extends DrupalContext {

  private $screenshotPath;

  /**
   * Initializes context.
   *
   * Every scenario gets its own context instance.
   * You can also pass arbitrary arguments to the
   * context constructor through behat.yml.
   */
  public function __construct( $tempPath = '/../bdd/tmp', $screenshotPath = '/screenshots', $htmlpagePath = '/behat_page.html' ) {
    $this->tempPath = $tempPath;
    $this->screenshotPath = $screenshotPath;
    $this->htmlPagePath = $htmlpagePath;
  }


 /**
   * Take screen-shot when step fails.
   *
   * @AfterStep
   * @param AfterStepScope $scope
   */
  public function takeScreenshotAfterFailedStep(AfterStepScope $scope)
  {
    // come from : https://github.com/Behat/Behat/issues/649
    // and from : https://gist.github.com/fbrnc/4550079

    global $base_url;

    if (99 === $scope->getTestResult()->getResultCode()) {

      if (! is_dir( $base_url . $this->tempPath . $this->screenshotPath )) {
        mkdir( $base_url . $this->tempPath . $this->screenshotPath, 0777, true );
      }
      $step = $scope->getStep();
            $id = /*$step->getParent()->getTitle() . '.' .*/ $step->getType() . ' ' . $step->getText();
            $id = $scope->getFeature()->getTitle().' '.$step->getLine().'-'.  $step->getType() . ' ' . $step->getText();
            $filename = 'Fail.'.preg_replace('/[^a-zA-Z0-9-_\.]/','_', $id) . '.html';

      $html_data = $this->getSession()->getDriver()->getContent();
      file_put_contents( DRUPAL_ROOT. $this->tempPath . $this->screenshotPath . '/' . $filename, $html_data);
      echo 'Screenshot error at : ' . $base_url . $this->tempPath . $this->screenshotPath . '/' . $filename;
    }
  }
}

Et bien sûr, dans le fichier de conf behat.yml

default:
  suites:
    default:
      contexts:
        - ScreenshotContext

memo YAML

Respecte l’indentation (uniquement via les espaces)

  • avec [ pour les listes
  • : avec { pour les mappings (enregistrements / tableau associatifs)

Pour le multi-ligne c’est un peu compliqué (_ veut dire espace):

  • | et > démarre à la ligne suivante, garde les espaces à la fin
    • | considère les sauts de ligne comme des sauts de ligne et pas des espaces
  • _ et et «  sur la même ligne, élimine les espaces à la fin
    • «  échappe les \n et \ et « 
    • _ si la chaîne ne contient pas # ou :
Scalars
# scalar = value
a: 1
a: 1.234
b: 'abc'
b: "abc"
b: abc
c: false	# boolean type
d: 2015-04-05	# date type

# Enforcing strings
b: !str 2015-04-05
Sequences
# sequence
array:
- 132
- 2.434
- 'abc'

# sqeuence of sequences
my_array:
- [1, 2, 3]
- [4, 5, 6]
Hashes
# Nest hash
my_hash:
  subkey:
    subsubkey1: 5
    subsubkey2: 6
  another:
    somethingelse: 'Important!'

# Hash of hashes
my_hash: {nr1: 5, nr2: 6}
Newlines
# block notation (newlines become spaces)
content:
  Arbitrary free text
  over multiple lines stopping
  after indentation changes...

# literal style (newlines are preserved)
content: |
  Arbitrary free text
  over "multiple lines" stopping
  after indentation changes...

# + indicator (keep extra newlines after block)
content: |+
  Arbitrary free text with two newlines after


# - indicator (remove extra newlines after block)
content: |-
  Arbitrary free text without newlines after it


# folded style (plié newlines are preserved)
content: >
  Arbitrary free text
  over "multiple lines" stopping
  after indentation changes...

Multiple Documents
---
content: doc1
---
content: doc2
Reference Content
---
values:
- &ref Something to reuse
- *ref	# Reused content
Merging Keys
default_settings:
  install:
    dir: /usr/local
    owner: root
  config:
    enabled: false

# Derive settings for 'my_app' from default and change install::owner
my_app_settings:
  <<: *default_settings
  install:
    owner: my_user
Complex Mapping
---
? - key
:
  - value
# Note: key and value can be multiple, complex structures
Tags
%TAG !personne! tag:foo.org,2004:bar
---
- !personne
    nom:    Simpson
    prenom: Omer
explicit_string: !!str 0.5
python_complex_number: !!python/complex 1+2j

Mémo pour les sautes de ligne

> | _ «  >-
Trailing spaces Kept Kept Kept
Single newline => _ \n _ _ _ _
Double newline => \n \n\n \n \n \n \n
In-line newlines No No No \n No No
Appended* \n \n \n
Single quote  »
Double quote «  «  «  \ » «  « 
Backslash \ \ \ \\ \ \
 » # », « : «  Ok Ok No Ok Ok Ok
Can start on same line as key  No No Yes Yes Yes No
  • http://symfony.com/legacy/doc/reference/1_3/fr/02-yaml
  • http://www.yaml.org/refcard.html
  • http://lzone.de/cheat-sheet/YAML
  • https://fr.wikipedia.org/wiki/YAML
  • http://sweetohm.net/article/introduction-yaml.html
  • http://stackoverflow.com/questions/3790454/in-yaml-how-do-i-break-a-string-over-multiple-lines
  • http://learnxinyminutes.com/docs/yaml/