lindev.fr

Aller au contenu | Aller au menu | Aller à la recherche

18 mai 2015

Amazon Ec2 IO et Raid

J'ai eu quelques soucis récemment avec une instance Amazon qui héberge une base de données Mysql très sollicitée par période.
En effet les données de la base étaient sur la même partition que le système ( déjà c'est mauvais ), celle-ci étant fortement sollicitée en IO, la charge système se met à grimper, les temps d'attente d’accès au disque étant de plus en plus grand, jusqu'au crash complet .

La solution simple et rapide est de déplacer les données vers une partition dédiée, avec des performances en IO plus importantes (tant qu'à faire). C'est dans ce contexte que je vais présenter, dans ce billet, la mise en place d'une partition en Raid0 pour augmenter significativement les IO/s et surtout éviter de surcharger le système.

Ce que l'on va voir

  • Création de système de fichiers XFS
  • Création d'un Raid logiciel
  • Monitoring avec atop
  • Ajout d'un disque au Raid
  • Extension d'une partition XFS

Etat des lieux

Je vais utiliser une instance micro ( gratuite ) sous Debian Jessie en HVM ( virtualisation totale ). je vais y attacher 4 disques.

Capture_d_e_cran_2015-05-17_a__10.28.54.png

  • /dev/xvda pour le système
  • /dev/sdb pour tester les perfs. sur un seul disque
  • /dev/sdc premier disque du Raid
  • /dev/sdb deuxième disque du Raid

Installation des outils

Le système étant vierge nous allons installer les paquets nécessaires pour les manipulations ci-dessous.

sudo apt-get install xfsprogs atop mdadm

Premier test : Disque seul

Pour pouvoir comparer, nous allons commencer avec un test sur un disque "seul" possédant une partition XFS

Voici comment sont perçus les disques à ce moment

Capture_d_e_cran_2015-05-17_a__10.35.40.png

Commençons par créer la partition :

sudo cfdisk /dev/xvdb

Une fois la partition créée, voici comment sont perçus les disques

Capture_d_e_cran_2015-05-17_a__10.37.39.png

Nous allons maintenant créer le système de fichiers sur cette nouvelle partition

mkfs.xfs /dev/xvdb1

Enfin il ne nous reste plus qu'à monter cette partition sur un répertoire que nous allons également créer maintenant

mkdir /home/admin/diskLocal
mount -t xfs /dev/xvdb1 /home/admin/diskLocal/

Dirigeons nous dans ce répertoire et effectuons le premier test basique, grâce à l'outil dd

cd /home/admin/diskLocal
dd if=/dev/zero of=bench.dat bs=1M count=1024 oflag=direct

Pendant le test on voit bien grâce à la commande atop, l'utilisation du disque qui est à son maximum

Capture_d_e_cran_2015-05-17_a__10.45.21.png

Et voici le résultat

Capture_d_e_cran_2015-05-17_a__10.45.39.png

Deuxième test : 2 Disques en Raid0

Commençons par construire notre raid0 "software" ( via la commande mdadm )

mdadm --create --verbose /dev/md0 --level=stripe --raid-devices=2 /dev/xvdc /dev/xvdd

Ce qui nous donne :

Capture_d_e_cran_2015-05-17_a__10.48.43.png

Un petit check :

mdadm -D /dev/md0

Maintenant, créons notre système de fichiers sur md0

mkfs.xfs /dev/md0

Enfin il ne nous reste plus qu'à monter cette partition sur un répertoire que nous allons créer: diskRaid

mkdir /home/admin/diskRaid
mount -t xfs /dev/md0 /home/admin/diskRaid/

Dirigeons nous dans ce répertoire et effectuons le second test sur ce disque Raid0

cd /home/admin/diskRaid
dd if=/dev/zero of=bench.dat bs=1M count=1024 oflag=direct

Regardons ce qui se passe pendant le test

Capture_d_e_cran_2015-05-17_a__10.52.27.png

Les deux disques sont en pleine charge, et quelques secondes plus tard ...

Capture_d_e_cran_2015-05-17_a__10.52.37.png

On passe donc de 30 secondes à 18 sec. pour effectuer ce test et un débit d’écriture de 35.5MB/s à 51MB/s

Conclusion

Il est plus intéressant de mettre en place 2 disques en Raid0 qu'avoir un gros disque pour améliorer les IO/s.

Ajoutons un disque au Raid0

Si nous ajoutons un disque à notre Raid0, nous passerons ainsi à 3 disques

Voici le disque à ajouter

  • /dev/xvde

Capture_d_e_cran_2015-05-17_a__14.21.34.png

Ajoutons le à notre raid md0

sudo mdadm --grow /dev/md0 --raid-devices=3 --level=0 --add /dev/xvde
sudo xfs_growfs /dev/md0

Regardons si tout est ok

lsblk

Capture_d_e_cran_2015-05-17_a__14.59.08.png

Vérifions que la partition fasse bien 15G

df -h

Capture_d_e_cran_2015-05-17_a__15.00.30.png

Bien ça me semble parfait, lançons le troisième test

cd /home/admin/diskRaid
dd if=/dev/zero of=bench.dat bs=1M count=1024 oflag=direct

Capture_d_e_cran_2015-05-17_a__15.02.34.png

Résultat :

Capture_d_e_cran_2015-05-17_a__15.03.51.png

Les performances restent les mêmes, certainement limitées par la virtualisation ou simplement physiquement, cependant la charge étant répartie sur 4 disques, l'occupation ne dépasse pas les 65%, ce qui permet d'avoir d'autres processus en parallèles.

L'ajout de disques reste donc très intéressante pour pouvoir encaisser une forte charge d'IO/s .

Bon tests ,

Ch.

16 mai 2015

Envoi de mail à la création de compte via le backend de prestashop

logo.png Un petit truc tout bête bien pratique, à tel point que je ne comprends pas pourquoi ce n'est pas en place dans les fonctionnalités de base de prestashop.

Le besoin

Prestashop 1.6

Lorsqu'un employé crée un compte utilisateur depuis le backoffice de prestashop, aucun mail n'est envoyé au client final ( le mail de bienvenue contenant ses identifiants pour se connecter ), de plus l'employé doit entrer lui-même un mot de passe, ce qui n'est pas des plus pratique, surtout lorsque l'employé est en manque d'imagination, on peut se retrouver alors dans une problématique de sécurité non négligeable.

La solution

Nous allons simplement permettre à l'employé de laisser le champs "passwd" vide, il sera alors automatiquement généré de façon aléatoire.
Enfin, nous allons ajouter un bouton "On/Off" dans le formulaire de création de compte qui va permettre à l'employé de décider si oui ou non les identifiants seront envoyés au client.

Le code

Bien commençons par créer la classe de surcharge

override/controllers/admin/AdminCustomersController.php

Nous allons donc surcharger les méthodes suivantes :

  1. processAdd()
  2. renderForm()

Et créer une fonction

  1. sendConfirmationMail()

Voici donc le code complet de notre fichier AdminCustomersController.php

<?php
/*
*  05-2015
*
*  @author Christophe De Saint Leger 
*  @Description Surcharge Formulaire création compte depuis le BackEnd
*/
class AdminCustomersController extends AdminCustomersControllerCore
{


        public function processAdd()
        {
            if (Tools::getValue('submitFormAjax'))
                $this->redirect_after = false;
            // Check that the new email is not already in use
            $customer_email = strval(Tools::getValue('email'));
            $customer = new Customer();
            if (Validate::isEmail($customer_email))
                $customer->getByEmail($customer_email);
            if ($customer->id)
            {
                $this->errors[] = Tools::displayError('An account already exists for this email address:').' '.$customer_email;
                $this->display = 'edit';
                return $customer;
            }
            elseif (trim(Tools::getValue('passwd')) == '')
            {
                $_POST['passwd'] = Tools::passwdGen();
            }
            if ($customer = parent::processAdd())
            {
                $this->context->smarty->assign('new_customer', $customer);
                if( Tools::getValue('sendWelcomeEmail') ){
                    $this->sendConfirmationMail($customer);
                }
                return $customer;
            }
            return false;
        }




        public function renderForm()
        {

            if (!($obj = $this->loadObject(true)))
                return;
            
            $genders = Gender::getGenders();
            $list_genders = array();
            foreach ($genders as $key => $gender)
            {
                $list_genders[$key]['id'] = 'gender_'.$gender->id;
                $list_genders[$key]['value'] = $gender->id;
                $list_genders[$key]['label'] = $gender->name;
            }

            $years = Tools::dateYears();
            $months = Tools::dateMonths();
            $days = Tools::dateDays();

            $groups = Group::getGroups($this->default_form_language, true);
            $this->fields_form = array(
                'legend' => array(
                    'title' => $this->l('Customer'),
                    'icon' => 'icon-user'
                ),
                'input' => array(
                    array(
                        'type' => 'radio',
                        'label' => $this->l('Social title'),
                        'name' => 'id_gender',
                        'required' => false,
                        'class' => 't',
                        'values' => $list_genders
                    ),
                    array(
                        'type' => 'text',
                        'label' => $this->l('First name'),
                        'name' => 'firstname',
                        'required' => true,
                        'col' => '4',
                        'hint' => $this->l('Invalid characters:').' 0-9!&lt;&gt;,;?=+()@#"°{}_$%:'
                    ),
                    array(
                        'type' => 'text',
                        'label' => $this->l('Last name'),
                        'name' => 'lastname',
                        'required' => true,
                        'col' => '4',
                        'hint' => $this->l('Invalid characters:').' 0-9!&lt;&gt;,;?=+()@#"°{}_$%:'
                    ),
                    array(
                        'type' => 'text',
                        'prefix' => '<i class="icon-envelope-o"></i>',
                        'label' => $this->l('Email address'),
                        'name' => 'email',
                        'col' => '4',
                        'required' => true,
                        'autocomplete' => false
                    ),
                    array(
                        'type' => 'password',
                        'label' => $this->l('Password'),
                        'name' => 'passwd',
                        'required' => ($obj->id ? false : true),
                        'col' => '4',
                        'hint' => ($obj->id ? $this->l('Leave this field blank if there\'s no change.') :
                            sprintf($this->l('Password should be at least %s characters long. or void for automatic generation'), Validate::PASSWORD_LENGTH))
                    ), 
                    array(
                        'type' => 'switch',
                        'label' => $this->l('Send Welcome Email'),
                        'name' => 'sendWelcomeEmail',
                        'required' => false,
                        'class' => 't',
                        'is_bool' => true,
                        'values' => array(
                            array(
                                'id' => 'sendWelcomeEmail_on',
                                'value' => 1,
                                'label' => $this->l('Enabled')
                            ),
                            array(
                                'id' => 'sendWelcomeEmail_off',
                                'value' => 0,
                                'label' => $this->l('Disabled')
                            )
                        ),
                        'hint' => $this->l('Send the credentials to the client')
                    ),
                    array(
                        'type' => 'birthday',
                        'label' => $this->l('Birthday'),
                        'name' => 'birthday',
                        'options' => array(
                            'days' => $days,
                            'months' => $months,
                            'years' => $years
                        )
                    ),
                    array(
                        'type' => 'switch',
                        'label' => $this->l('Enabled'),
                        'name' => 'active',
                        'required' => false,
                        'class' => 't',
                        'is_bool' => true,
                        'values' => array(
                            array(
                                'id' => 'active_on',
                                'value' => 1,
                                'label' => $this->l('Enabled')
                            ),
                            array(
                                'id' => 'active_off',
                                'value' => 0,
                                'label' => $this->l('Disabled')
                            )
                        ),
                        'hint' => $this->l('Enable or disable customer login.')
                    ),
                    array(
                        'type' => 'switch',
                        'label' => $this->l('Newsletter'),
                        'name' => 'newsletter',
                        'required' => false,
                        'class' => 't',
                        'is_bool' => true,
                        'values' => array(
                            array(
                                'id' => 'newsletter_on',
                                'value' => 1,
                                'label' => $this->l('Enabled')
                            ),
                            array(
                                'id' => 'newsletter_off',
                                'value' => 0,
                                'label' => $this->l('Disabled')
                            )
                        ),
                        'disabled' =>  (bool)!Configuration::get('PS_CUSTOMER_NWSL'),
                        'hint' => $this->l('This customer will receive your newsletter via email.')
                    ),
                    array(
                        'type' => 'switch',
                        'label' => $this->l('Opt-in'),
                        'name' => 'optin',
                        'required' => false,
                        'class' => 't',
                        'is_bool' => true,
                        'values' => array(
                            array(
                                'id' => 'optin_on',
                                'value' => 1,
                                'label' => $this->l('Enabled')
                            ),
                            array(
                                'id' => 'optin_off',
                                'value' => 0,
                                'label' => $this->l('Disabled')
                            )
                        ),
                        'disabled' =>  (bool)!Configuration::get('PS_CUSTOMER_OPTIN'),
                        'hint' => $this->l('This customer will receive your ads via email.')
                    ),
                )
            );
            
            // if we add a customer via fancybox (ajax), it's a customer and he doesn't need to be added to the visitor and guest groups
            if (Tools::isSubmit('addcustomer') && Tools::isSubmit('submitFormAjax'))
            {
                $visitor_group = Configuration::get('PS_UNIDENTIFIED_GROUP');
                $guest_group = Configuration::get('PS_GUEST_GROUP');
                foreach ($groups as $key => $g)
                    if (in_array($g['id_group'], array($visitor_group, $guest_group)))
                        unset($groups[$key]);
            }

            $this->fields_form['input'] = array_merge(
                $this->fields_form['input'],
                array(
                    array(
                        'type' => 'group',
                        'label' => $this->l('Group access'),
                        'name' => 'groupBox',
                        'values' => $groups,
                        'required' => true,
                        'col' => '6',
                        'hint' => $this->l('Select all the groups that you would like to apply to this customer.')
                    ),
                    array(
                        'type' => 'select',
                        'label' => $this->l('Default customer group'),
                        'name' => 'id_default_group',
                        'options' => array(
                            'query' => $groups,
                            'id' => 'id_group',
                            'name' => 'name'
                        ),
                        'col' => '4',
                        'hint' => array(
                            $this->l('This group will be the user\'s default group.'),
                            $this->l('Only the discount for the selected group will be applied to this customer.')
                        )
                    )
                )
            );

            // if customer is a guest customer, password hasn't to be there
            if ($obj->id && ($obj->is_guest && $obj->id_default_group == Configuration::get('PS_GUEST_GROUP')))
            {
                foreach ($this->fields_form['input'] as $k => $field)
                    if ($field['type'] == 'password')
                        array_splice($this->fields_form['input'], $k, 1);
            }

            if (Configuration::get('PS_B2B_ENABLE'))
            {
                $risks = Risk::getRisks();

                $list_risks = array();
                foreach ($risks as $key => $risk)
                {
                    $list_risks[$key]['id_risk'] = (int)$risk->id;
                    $list_risks[$key]['name'] = $risk->name;
                }

                $this->fields_form['input'][] = array(
                    'type' => 'text',
                    'label' => $this->l('Company'),
                    'name' => 'company'
                );
                $this->fields_form['input'][] = array(
                    'type' => 'text',
                    'label' => $this->l('SIRET'),
                    'name' => 'siret'
                );
                $this->fields_form['input'][] = array(
                    'type' => 'text',
                    'label' => $this->l('APE'),
                    'name' => 'ape'
                );
                $this->fields_form['input'][] = array(
                    'type' => 'text',
                    'label' => $this->l('Website'),
                    'name' => 'website'
                );
                $this->fields_form['input'][] = array(
                    'type' => 'text',
                    'label' => $this->l('Allowed outstanding amount'),
                    'name' => 'outstanding_allow_amount',
                    'hint' => $this->l('Valid characters:').' 0-9',
                    'suffix' => $this->context->currency->sign
                );
                $this->fields_form['input'][] = array(
                    'type' => 'text',
                    'label' => $this->l('Maximum number of payment days'),
                    'name' => 'max_payment_days',
                    'hint' => $this->l('Valid characters:').' 0-9'
                );
                $this->fields_form['input'][] = array(
                    'type' => 'select',
                    'label' => $this->l('Risk rating'),
                    'name' => 'id_risk',
                    'required' => false,
                    'class' => 't',
                    'options' => array(
                        'query' => $list_risks,
                        'id' => 'id_risk',
                        'name' => 'name'
                    ),
                );
            }

            $this->fields_form['submit'] = array(
                'title' => $this->l('Save'),
            );

            $birthday = explode('-', $this->getFieldValue($obj, 'birthday'));

            $this->fields_value = array(
                'years' => $this->getFieldValue($obj, 'birthday') ? $birthday[0] : 0,
                'months' => $this->getFieldValue($obj, 'birthday') ? $birthday[1] : 0,
                'days' => $this->getFieldValue($obj, 'birthday') ? $birthday[2] : 0,
            );

            // Added values of object Group
            if (!Validate::isUnsignedId($obj->id))
                $customer_groups = array();
            else
                $customer_groups = $obj->getGroups();
            $customer_groups_ids = array();
            if (is_array($customer_groups))
                foreach ($customer_groups as $customer_group)
                    $customer_groups_ids[] = $customer_group;

            // if empty $carrier_groups_ids : object creation : we set the default groups
            if (empty($customer_groups_ids))
            {
                $preselected = array(Configuration::get('PS_UNIDENTIFIED_GROUP'), Configuration::get('PS_GUEST_GROUP'), Configuration::get('PS_CUSTOMER_GROUP'));
                $customer_groups_ids = array_merge($customer_groups_ids, $preselected);
            }

            foreach ($groups as $group)
                $this->fields_value['groupBox_'.$group['id_group']] =
                    Tools::getValue('groupBox_'.$group['id_group'], in_array($group['id_group'], $customer_groups_ids));

            return AdminController::renderForm();
        }



        /**
         * sendConfirmationMail
         * @param Customer $customer
         * @return bool
         */
        protected function sendConfirmationMail(Customer $customer)
        {
            if (!Configuration::get('PS_CUSTOMER_CREATION_EMAIL'))
                return true;

            return Mail::Send(
                $this->context->language->id,
                'account',
                Mail::l('Welcome!'),
                array(
                    '{firstname}' => $customer->firstname,
                    '{lastname}' => $customer->lastname,
                    '{email}' => $customer->email,
                    '{passwd}' => Tools::getValue('passwd')),
                $customer->email,
                $customer->firstname.' '.$customer->lastname
            );
        }



}

Attention à ne pas oublier

Pour toute nouvelle surcharge, il vous faut supprimer le fichier de cache suivant

cache/class_index.php

Résultat

Et voilà le résultat au niveau du formulaire

Capture_d_e_cran_2015-05-16_a__14.27.06.png

Bonne journée,

Ch.

13 mai 2015

Time zones nommées mysql

Un nouveau billet pense bête ...

A l'installation de Mysql selon la version ou la façon de l'installer, les Time Zone nommées ne sont pas toujours ajoutées dans la base de données mysql. Voilà la petite ligne de commande à exécuter (sous linux) pour y remédier ( les ajouter )

mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u <utilisateur admin> mysql

Et voilà .. vous pouvez utiliser les requête

SET time_zone = timezonename;

26 avr. 2015

Jessie, la bonne nouvelle du jour

2015.04.25 8 Jessie

Ça y est, après de longs mois de développement des équipes Debian, voilà la nouvelle monture disponible "Debian Jessie".

Merci aux développeurs pour ce travail formidable .

22 mar. 2015

Un blog, une histoire

Voilà maintenant plus de 8 ans que ce blog existe ... déjà 8 ans !!
Je pense jamais avoir expliquer l'origine de cette volonté de tenir un tel site, ce sera chose faite à la fin de ce billet .

L'origine

Me voilà jeune diplômé sur le marché du travail, sans expérience j'arrive alors dans une société industrielle dédiée à l'impression numérique et sérigraphique grand format Creavi.
Rapidement, je mets en pratique tout ce que je connais dans mon domaine qu'est l'informatique ( plus précisément le développement et l'administration système linux ).
Grand "guru" de l'open source, "ce qui ma valu quelques pseudonymes au passage" je mets en place de plus en plus de solutions libres au sein de la société dans laquelle je me trouve .
Comme tout bon informaticien, je me tiens à jour, et tente de suivre les évolutions qui changent à un rythme effréné.
C'est alors que je me donne le temps pour partager mes découvertes, mes tests, mes galères, Lindev.fr est né !
C'est ce même genre de site qui m'a permis d'évoluer dans le domaine, je vois donc ça comme un juste retour des choses .
Évidemment, toujours sans pub, le but n'est certainement pas de tenter une quelconque rentabilité de ce blog, mais bien un partage pur et simple.
Le temps passe, les articles se succèdent à une fréquence plus ou moins rapide, mais reste toujours actif, d'ailleurs vous êtes de plus en plus nombreux à lire mes pages, ce qui au passage me motive encore un peu plus pour continuer à prendre du temps pour en écrire de nouveaux .

2015 le changement

Ce blog étant lié à mon activité professionnelle, il évoluera avec elle.
En 2015 je décide de changer de job !
Et oui, une envie de nouvelles aventures, une remise en question total, une nouvelle prise de risques, mais aussi des nouveaux chemins à parcourir/découvrir .
Me voilà maintenant dans une nouvelle structure totalement différente de la précédente: Speechi, spécialiste dans les solutions interactives pour l'enseignement que ce soit logiciel ou matériel ( vidéoprojecteur interactif, visualiseurs, tableau blanc interactif, ou encore et plus impressionnant, les écrans intéractifs )
Autant de nouvelles technologies au service de l'apprentissage avec lesquels je vais désormais travailler, et qui dit nouveau boulot, dit nouveaux besoins et indirectement nouvelles technos donc pour vous ... nouveaux articles .

Voilà pour cette petite explication sur l'origine de ce blog et son avenir, je trouve que ce changement est suffisamment important pour vous en faire part, c'est désormais chose faite .

A bientôt, Ch.

22 fév. 2015

Apache2 php5-fpm VirtualHost

Cela fait maintenant quelques mois que je n'ai pas édité de nouveau billet sur ce site, je vais donc couper cette période creuse en vous proposant un petit tutoriel sur la mise en place de php5-fpm avec apache2 et tout ça configurable au besoin dans des virtualhost .

Nous avons déjà vu comment faire avec Nginx ( qui est franchement plus clair qu'apache ) mais parfois on a pas le choix du serveur web sur lequel on travail .. alors voyons comment faire ça proprement avec apache2.

Etat des lieux

Le tutoriel se base sur la version stable de Débian du moment, Debian 7 "Wheezy" .
Nous allons également activer les dépôts contrib et non-free pour installer ce qui va suivre .

Pour cela commencez par éditer le fichier /etc/apt/sources.list

sudo vim /etc/apt/sources.list

Et ajoutez à la fin de chaque ligne contrib non-free
Ce qui donne quelque chose comme çà

deb http://cloudfront.debian.net/debian wheezy main contrib non-free
deb-src http://cloudfront.debian.net/debian wheezy main contrib non-free
deb http://security.debian.org/ wheezy/updates main contrib non-free
deb-src http://security.debian.org/ wheezy/updates main contrib non-free
deb http://cloudfront.debian.net/debian wheezy-updates main contrib non-free
deb-src http://cloudfront.debian.net/debian wheezy-updates main contrib non-free

PS: ne faite pas attention à l'url de mes dépôts ci-dessus "cloudfront.debian.net" , j'utilise une instance (VM) Amazon pour réaliser mes tests .

Installation des paquets nécessaires

Nous allons installer les paquets suivants :

sudo apt-get install php5-fpm libapache2-mod-fastcgi php5-cgi

Puis il nous faut activer le module fastcgi et actions via la commande a2enmod

sudo a2enmod actions fastcgi

Configuration du module fastcgi

Il nous faut lier le module fastcgi aux processus php5-fpm, pour celà nous allons éditer le fichier de configuration du module fastcgi

vim /etc/apache2/mods-available/fastcgi.conf

Pour arriver à ce résultat

<IfModule mod_fastcgi.c>
 AddType application/x-httpd-fastphp5 .php
 Action application/x-httpd-fastphp5 /php5-fcgi
 Alias /php5-fcgi /usr/bin/php5-fcgi
 FastCgiExternalServer /usr/bin/php5-fcgi -socket /var/run/php5-fpm.sock -pass-header Authorization
</IfModule>

NB : il vous faut vérifier le chemin du socket php5-fpm dans votre cas, normalement, /var/run/php5-fpm.sock et la valeur par défaut pour l’environnement Débian 7, pour vérifier exécuter cette commande

cat /etc/php5/fpm/pool.d/www.conf | grep "listen = "

Le test

Il ne vous reste plus qu'à tester cette nouvelle configuration, commençons par tester l'ensemble des paramètres d'apache via la commande très pratique

sudo apache2ctl configtest

Puis il ne reste plus qu'à relancer Apache si tout est ok

sudo service apache2 restart

Enfin, nous allons mettre un simple fichier php dans le DocumentRoot par défaut d'apache2

sudo echo "<?php echo phpinfo();" > /var/www/lindev.php && chown www-data:www-data /var/www/lindev.php

Il ne vous reste plus qu'à entrer l'url http://localhost/lindev.php pour voir le résultat .

Conf spécifique par virtualhost

Jusque là, les scripts php sont exécutés par l'utilisateur système www-data dans la plupart des cas, cela va être adéquat, mais si pour une raison quelconque vous devez utiliser un autre utilisateur pour exécuter un script/site en particulier, dans le virtualhost correspondant, il vous faudra surcharger la configuration du module fastcgi, ce qui donnera par exemple

<VirtualHost *:80>
    ServerAdmin webmaster@lindev.fr
    ServerName lindev.fr
    ServerAlias www.lindev.fr
    DocumentRoot /var/www/lindev/
    ErrorLog /var/www/lindev/error.log
    CustomLog /var/www/lindev/access.log combined

    <IfModule mod_fastcgi.c>
        AddType application/x-httpd-fastphp5 .php
        Action application/x-httpd-fastphp5 /php5-fcgi
        Alias /php5-fcgi /usr/lib/cgi-bin/php5-fcgi_lindev
        FastCgiExternalServer /usr/lib/cgi-bin/php5-fcgi_lindev -socket /var/run/php5-fpm_lindev.sock -pass-header Authorization
    </IfModule>

</VirtualHost>

Ici , j'ai donc donné un autre socket à utiliser, que j'ai nommé php5-fpm_lindev.sock. Ce socket devra bien évidement être configuré coté php-fpm, en créant un nouveau pool ( dans un prochain article certainement ), en attendant, pour tester copiez juste la conf du pool par défaut, et changer le paramètre listen pour spécifier le socket à créer pour ce pool.
C'est également dans ce fichier de pool que vous pourrez spécifier l'utilisateur système à utiliser pour interpréter les fichiers php.

cp /etc/php5/fpm/pool.d/www.conf /etc/php5/fpm/pool.d/lindev.conf

Changer les paramètres listen, user et group , puis relancer php-fpm

sudo service php5-fpm reload

L'article touche maintenant à sa fin, vous savez maintenant installer apache, php5-fpm et configurer un pool spécifique de php pour un virtualhost.

Ch.

15 sept. 2014

Réparer son virtualenv aprés un update Systéme/Libs

Après une belle maj de votre système, vous avez, au moment de vous remettre au travail, un sympathique message du genre

ImportError: No module named datetime

detatime !! Cette lib fait pourtant partie du standard de python ! Le fait est que lorsque l'on met à jour python, les liens utilisés à la création du venv sont hs !

Ici c'est la lib datetime qui a été remonté, mais ça peu être n'importe laquelle ..

Solution

Il ne m'a pas fallu longtemps pour résoudre ce problème tout bête, ( beaucoup de cas similaires sur le net ).

Réinitialiser le virtualenv

Par exemple avec mon venv nommé foo

virtualenv /home/cdsl/.virtualenvs/foo
New python executable in /home/foo/.virtualenvs/creasoft3/bin/python
Installing setuptools............done.
Installing pip...............done.

Et voilà ..

27 août 2014

Django déploiement avec Nginx Gunicorn et Supervisord

Je vois souvent passer des questions sur le forums ou autres fils de discussion concernant la mise en production d'un projet Django.
Ici je vais présenter une façon de faire, utilisant Nginx Gunicorn et Supervisord .

Prérequis

Sur une Débian toute fraiche,

Nginx

sudo apt-get install nginx

Virtualenv

Car nous travaillons toujours avec un environnement virtuel, dans lequel nous allons y installer les dépendances de notre projet.
Ce qui permet de ne pas avoir de conflit de version avec d'autres éventuels projets déjà en prod.

pip install virtualenv virtualenvwrapper

Note : Si vous n'avez pas l'outil pip de disponible, installez-le comme ceci

sudo apt-get install python-setuptools
sudo easy_install pip

Puis dans votre fichier ~/.bashrc , ajoutez les lignes suivantes, pour l'autocompletion

export VIRTUALENVWRAPPER_VIRTUALENV_ARGS='--no-site-package'
export WORKON_HOME=$HOME/.virtualenvs
source /usr/local/bin/virtualenvwrapper.sh

Gunicorn

Pourquoi pas utiliser uWSGI ? humm ... car je préfère Gunicorn qui me donne toute satisfaction sur mes projets en prod. donc ... pourquoi pas !

sudo apt-get install libevent-dev
pip install gunicorn

Supervisor

Installation simple :

sudo pip install supervisor

Récupération du fichier de conf par défaut

echo_supervisord_conf > /etc/supervisord.conf

Dans le fichier de conf, afin de pouvoir manager votre projet via l'interface web, il faut dé-commenter et paramétrer les lignes suivantes

[inet_http_server]         ; inet (TCP) server disabled by default
port=*:9001        ; (ip_address:port specifier, *:port for all iface)
username=user              ; (default is no username (open server))
password=123               ; (default is no password (open server))

Note: Changer le username et password évidemment

L'interface web sera disponible à l'adresse : http://ipduserveur:9001

Pour le lancement automatique au démarrage du système, vous pouvez utiliser ces scripts d'init:

  1. debian init supervisor
  2. debian init supervisor

Installer votre projet

Notre environnement est prêt, nous allons commencer par installer le projet Django dans l’environnement virtuel.

Création du virtualenv

mkvirtualenv monprojet

Si vous utilisez git, cloner votre projet, sinon, copier le à l'endroit que vous souhaitez, ( pour l'exemple ce sera /var/www/monprojet )

Installation des dépendances du projet

si vous utilisez également les environnements virtuels pour développer ( ce que je conseille ) vous pouvez alors enregistrer la liste des libs python installées dans cette environnement, afin de pouvoir également les installer ( avec la même version ) sur un autre serveur.

Pour avoir la liste des libs :

pip freeze > requirements.txt

puis sur votre serveur, pour installer les libs depuis un export freeze :

pip install -r requirements.txt

Configuration de Django

Il vous faudra peut-être toucher un peu à votre fichier settings.py pour le passer du mode debug au mode production.
Personnellement j'utilise une autre subtilité qui me permet de ne pas avoir à toucher au fichier settings.py ( j'expliquerai celà dans un autre billet ) .

N'oubliez pas de vérifier le paramètre STATIC_ROOT

STATIC_ROOT = '/var/www/static_monprojet/'

C'est le répertoire ou seront copiés les fichiers statiques du projet, pour ensuite être servis par Nginx

Ce répertoire DOIT EXISTER

Une fois le répertoire créé, nous allons y "placer/lier" les fichiers statiques du projet

python manage.py collectstatic --link

Perso je préfère y mettre des liens symboliques .. ( les gouts et les couleurs ... )

Liaison du projet avec Gunicorn

Nous allons créer un fichier dans notre projet qui sera utilisé pour le lancement du/des process Gunicorn ( vous devrez adapter les valeurs dans ce script )

vim /var/www/monprojet/monprojet/gunicorn.sh

Et voici le contenu

#!/bin/bash
  set -e
  NUM_WORKERS=2
  USER=www-data
  GROUP=www-data
  ADDRESS=127.0.0.1:5002
  cd /var/www/monprojet
  source /home/monuser/.virtualenvs/monprojet/bin/activate
  exec gunicorn monprojet.wsgi:application -w $NUM_WORKERS --bind=$ADDRESS \
    --user=$USER --group=$GROUP --log-level=debug

Puis on le rend exécutable

chmod 777 /var/www/monprojet/monprojet/gunicorn.sh

Configuration Supervisord

Notre projet est prêt, afin de lancer Gunicorn automatiquement, nous allons utiliser supervisord, qui en plus de s'occuper de démarrer le projet automatiquement au démarrage du système, va aussi le relancer en cas de crash, gérer les logs et vous donner la main pour arrêter ré-demarrer les process, via l'interface web ou en mode console.

supervisord.png

Ajoutez à la fin du fichier de configuration /etc/supervisord.conf les lignes suivantes ( paramètres à adapter selon votre cas )

[program:guni_monprojet]
directory=/var/www/monprojet/monprojet
user = www-data
autostart=true
autorestart=true
stdout_logfile=/var/log/monprojet.log
redirect_stderr=true
stopsignal=QUIT
command = /var/www/monprojet/monprojet/monprojet.sh

Ne reste plus qu'à redémarrer supervisor pour prendre en compte la nouvelle config.

Pour le lancer manuellement

sudo supervisord -c /etc/supervisord.conf

Vhost Nginx

Dernier point, la création du vhost de Nginx, pour diriger les requêtes vers Gunicorn qui écoute sur le port 5002 ( il est aussi possible d'utiliser un fichier socket à la place )

vim /etc/nginx/sites-enabled/monprojet

Et voilà le contenu ( fonctionnel, mais vous pouvez l'adapter )

upstream us_monprojet {
        server 127.0.0.1:5002;
}


server {
        listen 80;

        root /var/www/monprojet;

        gzip             on;
        gzip_min_length  1000;
        gzip_proxied     expired no-cache no-store private auth;
        gzip_types       text/plain application/xml text/css text/javascript application/x-javascript application/x-shockwave-flash video/x-flv;
        gzip_disable     "MSIE [1-6]\.";


        server_name monprojet.com;
        charset utf-8;

        client_max_body_size 75M;


        location ~ /\.ht {
            deny  all;
        }

        location /favicon.ico {
                alias /var/www/monprojet/monprojet/static/favicon.ico;

                if (-f $request_filename) {
                        access_log off;
                        expires max;
                }

        }


        location /media {
                alias /var/www/monprojet/monprojet/media;

                if (-f $request_filename) {
                        access_log off;
                        expires max;
                }

        }

        location /static {
                alias /var/www/static_monprojet/;

                if (-f $request_filename) {
                        access_log off;
                        expires max;
                }

        }
location / {

                if (-f $request_filename) {
                        access_log off;
                        expires max;
                }       

                #gunicornParams
                if (!-f $request_filename) {
                        proxy_pass         http://us_monprojet;
                        break;
                }       
                proxy_redirect     off;
                proxy_set_header   Host             $host;
                proxy_set_header   X-Real-IP        $remote_addr;
                proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;

        }       
}       

Ne reste plus qu'à activer le vhost, et relancer nginx

ln -s /etc/nginx/sites-available/monprojet /etc/nginx/sites-enabled/
service nginx reload

01 août 2014

Proxy Socks sous android NON Rooté

En cette période de vacances, nous sommes nombreux à nous connecter depuis nos terminaux ( smartphone, tablette .. ) pour surfer, parfois travailler , dépanner .
Il est vrai que ces appareils sont formidables sur ce point ( le coté nomade ). Mais le revers de la médaille, c'est la sécurité !
Lorsque vous vous connectez à une borne wifi ( gratuite ou non ), qui vous assure que votre navigation n'est pas épiée ?
Voilà pour le coté sécurité, mais il peut être aussi utile de se connecter à son réseau local depuis l'extérieur.
Nous allons pour cela nous connecter via un serveur proxy de type socks.
Maintenant peu importe ce qui motive l'utilisation d'une telle connexion, voyons comment faire pour nous y connecter depuis un client Android non chrooté

Les apps à installer

  1. juiceSSH
  2. Firefox

JuiceSSH est un client SSH qui fonctionne vraiment bien, gratuit pour les fonctionnalités de base, mais payant pour faire une redirection de port. Vous allez donc devoir l'acheter, mais l'investissement en vaut la peine.
D'autres ont utilisé ConnectBOT qui lui est à 100% gratuit, mais de mon coté il plantait régulièrement je n'ai donc pas insisté.

Configuration de juiceSSH

Nous allons commencer par créer une connexion SSH de base, le tout en image :

2014_08_01_12.06.59.png

2014_08_01_12.08.03.png

  • Nickname : nom de la connexion
  • Type : SSH
  • Adresse : Adresse du serveur SSH
  • Identity : Login pour se connecter au serveur SSH
  • Port : port à utiliser pour se connecter au serveur SSH

Configuration de la redirection de port

Allez dans l'onglet PORT FORWARD, et nous allons utiliser la connexion SSH précédemment crée, comme ceci.

2014_08_01_12.08.40.png

Il est ensuite possible de mettre une icône sur votre page principale, pour se connecter plus rapidement au serveur proxy SOCKS

2014_08_01_12.08.57.png

Configuration de Firefox

Pour utiliser le serveur proxy ( après s'y être connecté ), dans firefox, ouvrez un nouvel onglet et entrez dans la barre d'adresse :

about:config


Voici les 5 paramètres à configurer comme ceci

2014_08_01_12.05.51.png 2014_08_01_12.06.11.png

Tests

Voilà maintenant vous pouvez tester le bon fonctionnement en entrant l'url suivante : http://www.whatismyip.com/, qui devrait vous afficher l'ip du serveur SSH à partir duquel vos requêtes sont envoyés .

Bonnes vacances .

16 juil. 2014

Django, générer du xlsx

Les exports ... on est tous amené dans un projet à devoir se farcir des exports de données pour de l'analyse comptable, statistiques ou de l'analyse de production.

Bien souvent je ne cherchai pas trop loin pour sortir les données ( il faut dire que ce n'est pas ce qu'il y a de plus sexy comme travail ), j'optais pour un vulgaire fichier CSV!
Bien pratique, et rapide à sortir mais il faut avouer que ce n'est pas ce qu'il y a de plus présentable aux clients finaux, et il faut vendre le fait que la personne qui va exploiter ce genre d'export, va devoir commencer par structurer le fichier csv pour le rendre exploitable . ( et je ne parle même pas des encodages ! )

Bref ... ça fait porc !

xlsxwriter

Je me suis donc tourné vers une lib très bien documentée et qui fonctionne à merveille . xlsxwriter
Qui plus est très rapide à mettre en place et à prendre en main .

voyez vous même avec l'exemple N°1 de la documentation

import xlsxwriter

# Create a workbook and add a worksheet.
workbook = xlsxwriter.Workbook('Expenses01.xlsx')
worksheet = workbook.add_worksheet()

# Some data we want to write to the worksheet.
expenses = (
    ['Rent', 1000],
    ['Gas',   100],
    ['Food',  300],
    ['Gym',    50],
)

# Start from the first cell. Rows and columns are zero indexed.
row = 0
col = 0

# Iterate over the data and write it out row by row.
for item, cost in (expenses):
    worksheet.write(row, col,     item)
    worksheet.write(row, col + 1, cost)
    row += 1

# Write a total using a formula.
worksheet.write(row, 0, 'Total')
worksheet.write(row, 1, '=SUM(B1:B4)')

workbook.close()

Voilà pour la lib xlsxwriter, je ne vais pas vous faire un tutoriel de la doc qui est très bien faite .
Par contre, regardons comment l'intégrer à une vue Django .

Django

L'objectif, est de générer le fichier xlsx ci-dessus, depuis une vue Django, mais sans enregistrer le fichier sur le disque du serveur pour ensuite le servir, non, nous allons le générer en mémoire et l'envoyer avec les entêtes qui vont bien pour avoir une belle boite de dialogue comme ceci

xlsx.png

StringIO et cStringIO

Afin d'éviter de générer un fichier sur le disque à chaque exportation, nous allons travailler en mémoire exclusivement.
Pour y parvenir, nous allons utiliser la lib StringIO ou cStringIO si elle est dispo.
StringIO permet de travailler sur un object qui réagit comme un fichier text ( read, write, ... ) sauf qu'on est en mémoire et non sur le disque .

Voilà à quoi va ressembler la vue Django :

try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO
buffer=StringIO()

    
import xlsxwriter

def export(self):
    """ Export Démo  """

    # Create a workbook and add a worksheet.
    workbook = xlsxwriter.Workbook(buffer, {'constant_memory': True})
    worksheet = workbook.add_worksheet()

    # Some data we want to write to the worksheet.
    expenses = (
        ['Rent', 1000],
        ['Gas',   100],
        ['Food',  300],
        ['Gym',    50],
    )

    # Start from the first cell. Rows and columns are zero indexed.
    row = 0
    col = 0

    # Iterate over the data and write it out row by row.
    for item, cost in (expenses):
        worksheet.write(row, col,     item)
        worksheet.write(row, col + 1, cost)
        row += 1

    # Write a total using a formula.
    worksheet.write(row, 0, 'Total')
    worksheet.write(row, 1, '=SUM(B1:B4)')

    workbook.close()


    #Reponse envoyée au navigateur
    response = HttpResponse(buffer.getvalue(), mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
    response['Content-Disposition'] = "attachment; filename=test_export.xlsx"

    return response

Petite liste des types mime .. toujours pratique .

Et voilà le travail .. simple et efficace .
Maintenant ne vous reste plus qu'à mettre en forme vos exports afin qu'ils soient immédiatement exploitables par les principaux intéressés.

C'est pas grand chose, mais ça fait du bien de partager ;)
Ch.

30 juin 2014

Jquery.tableSorter, persistance du tri

Jquery TableSorter

Est un plugin jquery très efficace qui vous permet d'avoir les fonctions de tri sur un tableau en quelques lignes !
Sa mise en place est extrêmement simple, il reconnait pas mal de formats ( monétaire, date .. ) et pour couronner le tout, il est livré avec quelques de widgets intéressants pour aller encore plus loin .

Dans un récent développement, j'ai utilisé ce plugin pour pouvoir trier un tableau, mais j'avais besoin d'une persistance de ce tri dans le temps. J'ai été étonné qu'il n'y ai pas de widget en natif pour ce cas, mais comme ce plugin est bien fichu, il suffit d'étendre ses fonctionnalités avec un nouveau widget !
Je vous partage donc le code du widget qui me permet de sauvegarder le tri dans un cookie ( Dépend du plugin Jquery-cookie )

PS: si vous avez plusieurs tableaux, il y aura un enregistrement par tableau .

widget-sortPersist.js

;(function($){
"use strict";
var ts = $.tablesorter;

  ts.addWidget({
    // give the widget an id
    id: "sortPersist",
    // format is called when the on init and when a
    // sorting has finished
    format: function(table, thisWidget, c) {
 
      // Cookie info
      var cookieName = 'MY_SORT_COOKIE';
      var cookie = $.cookie(cookieName);
      var options = {path: '/'};
 
      var data = {};
      var sortList = table.config.sortList;
      var tableId = $(table).attr('id');
      var cookieExists = (typeof(cookie) != "undefined"
          && cookie != null);
 
      // If the existing sortList isn't empty, set it into the cookie
      // and get out
      if (sortList.length > 0) {
        if (cookieExists) {
          data = $.parseJSON(cookie);
        }
        data[tableId] = sortList;
        $.cookie(cookieName, JSON.stringify(data), options);
      }
 
      // Otherwise...
      else {
        if (cookieExists) {
 
          // Get the cookie data
          var data = $.parseJSON($.cookie(cookieName));
 
          // If it exists
          if (typeof(data[tableId]) != "undefined"
              && data[tableId] != null) {
 
            // Get the list
            sortList = data[tableId];
 
            // And finally, if the list is NOT empty, trigger
            // the sort with the new list
            if (sortList.length > 0) {
              $(table).trigger("sorton", [sortList]);
            }
          }
        }
      }
    }
  });
  

})(jQuery);

Pour l'utiliser, il vous suffit :

  1. d'inclure le widget dans votre page html
  2. d'activer le widget dans l'appel de tablesorter
$("#montableau").tablesorter({widgets: ['sortPersist']});

Attention

Attention ce code possède une dépendance : Jquery-cookie

Ch.

27 mar. 2014

OS X 10.9.2, smb, et les autres ...

Petite mise en situation, j'ai un parc doté de nombreuses machines Windows, Linux et Mac de versions différentes, et un des partages réseau est placé sur une machine Windows 7 bien intégrée au domaine ...

Jusque là tout baigne, mais voilà qu'un étrange phénomène fait son apparition...

En effet dés qu'un utilisateur sur Mac OS X 10.9.x dépose un fichier sur ce fameux partage ( sous Windows je le rappelle ) tous les autres utilisateurs connectés également à ce partage se retrouvent dehors, et dans l’incapacité de s'y reconnecter !

C'est quoi ce bor*** truc

C'est un peu ce que je me suis dit au départ ... après avoir testé le partage depuis d'autres postes, vérifié les règles FW du windows, l'anti-virus etc etc .. bref sur le moment je sèche ..

Soudain une explication apparaît

J'ai vu quelque part, une note indiquant que la version d'OS X 10.9.x utilisait la version 2 du protocole samba par défaut, ce qui ne pose aucun problème à Windows, il "switch" le protocole du partage smb2 et point barre !!
Oui mais le problème est justement là !

Il "switch" dans un seul sens !!! Donc mes autres postes en une version antérieure ou ne possédant simplement pas le protocole samba dans sa version 2 se retrouvent dehors, à poil !

Les solutions

  1. Changer la version du protocole samba par défaut au niveau du client ( Mac 10.9.x )
  2. Utiliser le protocole cifs en lieu et place de smb , toujours au niveau client

Changer la version sur le mac

Une manip simple, mais qui a l'inconvénient de forcer l'utilisation du protocole dans son ancienne version pour l’ensemble des partages.

Ouvrez un terminal et entrez cette commande :

echo "[default]" >> ~/Library/Preferences/nsmb.conf; echo "smb_neg=smb1_only" >> ~/Library/Preferences/nsmb.conf

Pour revenir à l'état d'origine .. il suffit de supprimer le fichier de config

rm ~/Library/Preferences/nsmb.conf

Utilisez cifs

cifs est en fait l'autre nom qu'avait donné Microsoft pour le protocole smb dans sa version 1, en y intégrant quelques améliorations ..
bref c'est presque un alias de smb v1 !

Le fait donc de se connecter en spécifiant cifs://<le partage> va forcer l'utilisation du protocole samba dans sa version antérieure !

cifs://user:password@x.x.x.x/sharePath

C'est cette dernière solution que j'ai retenu pour son coté pratique et sans modification du comportement du client par défaut.

Conclusion

Je n'aime toujours pas Windows ! ;)

Ch.

07 mar. 2014

Wheezy & fsck

Le saviez-vous

Mon poste de travail, sous débian Wheezy redémarre régulièrement comme tout poste de travail et pourtant ... jamais il ne ma fait de check disque fsck au démarrage !

Et bien figurez-vous que c'est voulu !

En effet d’après ce que j'ai trouvé sur le web à ce sujet, les développeurs de debian estiment que dans le monde professionnel lorsque qu'une machine ( surtout un serveur ) lance son fsck au démarrage, la plupart du temps, c'est au mauvais moment, et provoque plus de frustrations des admin. sys. qu'autre chose .

Il est vrai que le fait de se baser uniquement sur un facteur temps, ou un nombre de montages des volumes pour exécuter cette commande rend sa réelle utilité très aléatoire.
Ils estiment donc que les Admin. Sys. sons assez "grands" pour planifier au moment opportun le fsck des disques .

Le réactiver par défaut à la création de partition

Lors de la création d'une nouvelle partition, pour réactiver par défaut le fsck sur le nombre de montages et l'intervalle de temps, il vous faut changer le paramètre suivant ( en le passant à 1 ):

enable_periodic_fsck = 0

Dans le fichier : /etc/mke2fs.conf

Le réactiver sur un partition existante

Pour réactiver le fsck tout les 50 montages :

sudo tune2fs -c 50 /dev/sdx

Pour réactiver le fsck si le dernier date de plus de 6 mois :

sudo tune2fs -i 6m /dev/sdx

Désactiver le fsck automatique

Si au contraire vous souhaiter tout désactiver,

tune2fs -c -1 -i 0 /dev/sde1

Forcer un fsck

C'est bien beau de tout désactiver, mais il peut être intéressant de lancer un check des disques de temps en temps quand même !
Pour lancer fsck au démarrage, 2 méthodes

Avec la commande shutdown

shutdown -r -F now

-r Redémarrer la machine après l'arrêt du système.
-F Forcer l'utilisation de fsck lors du redémarrage.

Avec le fichier forcefsck

Le simple fait de créer un fichier nommé forcefsck à la racine du système, va l'obliger à lancer un check fsck au démarrage . Ce fichier sera alors supprimé automatiquement .

sudo touch /forcefsck

Ch.

13 fév. 2014

iSCSI - target et utilisation sous Proxmox

Pour rappel, iSCSI est un protocole de stockage en réseau basé sur le protocole IP . Bref cela permet de présenter au(x) clients un disque SCSI via le réseau . Le système client le voit comme un disque SCSI local et peut donc le monter et l'utiliser de la même manière.

ça sert à quoi ?

Un cas d'utilisation ( qui sera présenter ici ), vous avez un environnement de serveurs virtualisés, répartis sur deux nœuds ( serveurs hôtes ).
Sans baie de stockage ou autre système d’espace disque partagé , il est impossible de mettre en place une "haute dispo" (HA), les disques des VM se trouvant sur l'un ou l'autre des nœuds, si l'un des deux tombe, les disques des VM ne sont plus accessibles, aucune migration n'est alors possible = coupure de service

Mettre en place un disque iSCSI sur un troisième serveur est donc un moyen de combler ce manque à moindre cout ! ( pour ce qui est des performances, cela va grandement dépendre du réseau local en place, l'idéal étant d'avoir des liens directs entre le serveur iSCSI et les nœuds qui doivent y accéder, ainsi que la gestion du stockage de ce troisième nœud: disque(s) SAS SATA IDE .. en Raid 0,1,5 ... etc ... ).

Le serveur iSCSI

Pour les tests, mon serveur ( serveur "Cible" ou encore "Traget" dans le jargon iSCSI ) est une simple machine sous debian Wheezy avec un disque de 1To, identifié par /dev/sdf.
commençons par installer les outils nécessaire pour configurer la cible

apt-get install iscsitarget-dkms

iSCSI

Le fichier de configuration se trouve à l'endroit suivant /etc/iet/ietd.conf .
Commençons par en faire une copie .

cp /etc/iet/ietd.conf /etc/iet/ietd.conf.orig

Nous pouvons donc maintenant modifier sereinement le fichier d'origine pour déclarer le disque iSCSI

vim /etc/iet/ietd.conf

Nous pouvons présenter plusieurs types de "volumes", un périphérique de type block ( ce sera notre cas ici ), un volume LVM , un volume RAID.

Nous allons donc dans notre fichier ietd.conf définir notre cible (target).
Ajoutons donc les lignes suivantes à la fin du fichier de configuration

Target iqn.2014-02.lindev.fr:storage.lun0
        Lun 0 Path=/dev/sdf,Type=fileio
        Alias Lun0

ATTENTION : le volume paramétré (ici /dev/sdf) ne doit pas être monté !

Les paramètres utilisés :

  • Target Nom unique de la cible iSCSI normalisé iqn.yyyy-mm.<reversed domain name>:identifier
  • Lun X "X étant le numéro de Lun, la numérotation DOIT commencer à 0"
  • Alias "Facultatif ... ai-je vraiment besoin d'expliquer ce qu'est un Alias ? "


Il ne reste plus qu'à redémarrer le service

service iscsitarget restart

Le client

Avant de nous attaquer au cas d'utilisation proxmox, voyons comment utiliser le volume iSCSI sur un client standard .. ( une autre machine linux )

Commençons par installer les utilitaires nécessaires

apt-get install open-iscsi

Nous allons maintenant scanner le serveur ( ici 10.0.0.213 ) pour savoir quel(s) volume(s) sont disponibles

iscsiadm -m discovery -t st -p 10.0.0.213

Ce qui nous donne

10.0.0.213:3260,1 iqn.2014-02.lindev.fr:storage.lun0

Pour nous connecter au volume iSCSI nous allons utiliser la commande suivante

iscsiadm -m node --targetname "iqn.2014-02.lindev.fr:storage.lun0" --portal "10.0.0.213:3260" --login

Ce qui donne

Logging in to [iface: default, target: iqn.2014-02.lindev.fr:storage.lun0, portal: 10.0.0.213,3260] (multiple)
Login to [iface: default, target: iqn.2014-02.lindev.fr:storage.lun0, portal: 10.0.0.213,3260] successful.

Pour voir les sessions actives

iscsiadm -m session

resultat :

tcp: [1] 10.0.0.213:3260,1 iqn.2014-02.lindev.fr:storage.lun0

Au moment de la connexion du volume iSCSI, un nouveau "device" a normalement été créé automatiquement , il suffit de regarder dans les log le nom de ce "device"

tail -n 50 /var/syslog

dans mon cas :

Feb 14 09:56:17 debian7-cdsl kernel: [89543.405273] scsi8 : iSCSI Initiator over TCP/IP
Feb 14 09:56:17 debian7-cdsl kernel: [89543.660414] scsi 8:0:0:0: Direct-Access     IET      VIRTUAL-DISK     0    PQ: 0 ANSI: 4
Feb 14 09:56:17 debian7-cdsl kernel: [89543.660739] sd 8:0:0:0: Attached scsi generic sg7 type 0
Feb 14 09:56:17 debian7-cdsl kernel: [89543.660994] sd 8:0:0:0: [sdg] 1953525168 512-byte logical blocks: (1.00 TB/931 GiB)
Feb 14 09:56:17 debian7-cdsl kernel: [89543.661098] sd 8:0:0:0: [sdg] Write Protect is off
Feb 14 09:56:17 debian7-cdsl kernel: [89543.661102] sd 8:0:0:0: [sdg] Mode Sense: 77 00 00 08
Feb 14 09:56:17 debian7-cdsl kernel: [89543.661252] sd 8:0:0:0: [sdg] Write cache: disabled, read cache: enabled, doesn't support DPO or FUA
Feb 14 09:56:17 debian7-cdsl kernel: [89543.677076]  sdg: unknown partition table
Feb 14 09:56:17 debian7-cdsl kernel: [89543.677970] sd 8:0:0:0: [sdg] Attached SCSI disk
Feb 14 09:56:17 debian7-cdsl iscsid: Connection1:0 to [target: iqn.2014-02.lindev.fr:storage.lun0, portal: 10.0.0.213,3260] through [iface: default] is operational now

Le volume iSCSI est donc attaché à /dev/sdg qui est donc utilisable comme n'importe quel disque local !

Monter automatiquement

Pour monter le volume automatiquement au démarrage du système, voici la ligne que je devrais ajouter au fstab

/dev/Lun0/	/mnt/diskLun0	auto	_netdev	0	0

Proxmox

Le but de ce disque ( dans cet article ), n'est pas de l'utiliser sur un poste , mais sur un cluster Proxmox .

A partir de maintenant tout se passe sur l'interface d'administration proxmox !
Je parts du principe ou le cluster proxmox est en place et fonctionnel .

Déclarer le disque iSCSI

Commençons par déclarer au cluster proxmox, le disque iSCSI ... suivez le guide ...

Selection_201.png
Selection_202.png
Selection_203.png
Voilà , à ce moment là, proxmox possède un nouveau disque "local" qui en fait est un disque iSCSI ( on est au même stade que la manip "client" ci-dessus )
Pour le rendre exploitable pour y mettre les disques des VM , il faut exploiter ce disque en y créant un volume LVM dessus .
Encore une fois .. suivez le guide ;)

Selection_204.png
Selection_205.png

Démo

Après avoir créé ou migré vos disques de VM sur ce nouveau volume, la migration à chaud est possible .. voici une démo pour l'occasion .

17 janv. 2014

Recrutement Développeur web Python/PHP

CDI Développeur PHP/Python Nord - Comines

comines2.png

Lire la suite...

Django, gérer les FormSets dynamiquement

Les formsets sous Django sont très pratiques, notamment pour gérer les formulaires à "champs multiples".

qu'est-ce que j'entends par "champs multiples" ... ceci :

champs_multiples.png

Dans ce formulaire, j'ai donc deux champs uniques

  1. Largeur
  2. Hauteur

Puis vient enfin mon formset qui est composé de trois champs

  1. Désignation
  2. Quantité
  3. Liste : R | RV

J'ai donc au final un formulaire dont je ne connais pas à l'avance la quantité de données à traiter puisque le formset permet d'ajouter dynamiquement ses champs. Et c'est précisément ce que nous allons voir dans ce billet .

  • Comment créer un formset
  • Comment le rendre dynamique pour l'utilisateur ( ajout / suppression )
  • Comment valider les données en une seule ligne ( Merci Django :) )

Créer un Formset

Nous allons reproduire le formulaire présenté ci-dessus , nous aurons donc un formulaire classique , puis un formset de trois champs .

Formulaire classique

class ConfForm(forms.Form):
        """
        Formulaire de configuration du module
        """
        format_x = forms.IntegerField( label="Largeur", required=True, min_value=0, widget=forms.TextInput(attrs={'placeholder':'mm','class':'form-control input-sm'}) )
        format_y = forms.IntegerField( label="Hauteur", required=True, min_value=0, widget=forms.TextInput(attrs={'placeholder':'mm','class':'form-control input-sm'}) )

On ne peut pas plus .. classique pour les détails je vous laisse voir la doc de Django

Le formset

class SorteForm(forms.Form):

    #Champs multiples
    designation = forms.CharField( label="Désignation", required=True, widget=forms.TextInput(attrs={'placeholder':'','class':'form-control input-sm'}) )
    quantite = forms.IntegerField( label="Quantité", required=True, min_value=0, widget=forms.TextInput(attrs={'placeholder':'','class':'form-control input-sm'}) )
    rectoverso = forms.ChoiceField(label='',choices = [(False,'Recto Seul'),(True,'Recto Verso')],required=True,widget=forms.Select( attrs={'class':'form-control input-sm'}))

Vous allez me dire ... "c'est une déclaration de formulaire classique" .. et bien oui rien ne différencie au niveau déclaration un formulaire qui sera utilisé de façon classique, d'un formulaire qui sera utilisé au sein d'un formset .

Tout ceci pour en venir au fait qu'un formset, n'est ni plus, ni moins qu'un mécanisme proposé par Django pour gérer l'affichage, la validation des données et quelques autres petites choses comme les limitations min/max en un minimum d'efforts pour le développeur ...

Ça tombe bien je suis fainéant pour ce qui est de la gestion de formulaires ( pas vous ? ).

Maintenant que nous avons nos deux déclarations de formulaires, voyons comment les implémenter dans une vue.

Dans la vue

Note : Je ne vais pas expliquer ici la mise en forme CSS du formulaire, ce n'est pas l'objectif du billet, sachez juste que j'utilise BootstrapV3 dans la présentation du début.

Commençons avec une vue standard ... vide

def show_form(self, request):
    """ Affichage du formulaire + formset """

    # Formulaire standard à deux champs
    form = ConfForm(auto_id=True)

    #Création du formset avec une seule itération : extra=1
    sorteForm = formset_factory(SorteForm,extra=1)
        
    # Récupération du formulaire géré par le mécanisme formset
    formset = sorteForm()

    # Affichage du template 
    return render_to_response('my_form.html', {'form':form, 'sorteForm':formset}, context_instance=RequestContext(request) )

Le template

D'habitude, pour afficher un formulaire sans mise en forme particulière , la balise form se suffit à elle même.
Pour le formset , c'est la même chose, si ce n'est que nous allons devoir mettre cette balise dans une itération, car rappelez-vous, un formset peut contenir plusieurs formulaires.

Ce qui va donc nous donner : ( sans mise en forme )

<!-- formulaire standard -->
{{ form }}

<!-- Champs utilisés par le mécanisme formset de Django -->
{{ sorteForm.management_form }}

<!-- itération des formulaires du formset -->
{% for form in sorteForm %}
    
    {{ form }}

{% endfor %}

La balise management_form est utilisée par le mécanisme formset pour gérer les éventuels ajouts dynamiques, modifications, etc ... bref .. fonctionner correctement .
Voici son contenu

<!-- nombre de formulaire (s) -->
<input id="id_form-TOTAL_FORMS" type="hidden" value="1" name="form-TOTAL_FORMS">
<!-- nombre de formulaires à l'origine --> 
<input id="id_form-INITIAL_FORMS" type="hidden" value="1" name="form-INITIAL_FORMS">
<!-- nombre max de formulaires gérés si ajout dynamique -->
<input id="id_form-MAX_NUM_FORMS" type="hidden" value="1000" name="form-MAX_NUM_FORMS">

La validation et l'enregistrement

Oui oui .. je sais .. pour le moment il n'y a rien de dynamique, pas d'ajout / suppression ... patience, commençons par boucler la boucle avec la gestion des données POST, nous verrons ensuite pour dynamiser le formulaire .

En effet, l'étape de validation et traitement, ne varie pas selon le nombre de formulaire(s) dans le formset, c'est là toute la force des formset avec Django .

Voici la vue de validation , qui reçoit les données POST

    def save_form(self):
        """  Check & Enregistre les données   """

        # Check si la méthode est bien en POST
        if request.method == 'POST':

            # Instancie et "bound" le formulaire
            form = ConfForm( request.POST, auto_id=True )

            # Création du formset
            sorteForm = formset_factory( SorteForm )
            # "Bound" le formset
            formset = sorteForm( request.POST )

            #Verifie si les données du formulaire simple sont valides
            if form.is_valid() :
                # Vérifie le(s) formulaire(s) du formset 
                if formset.is_valid():

                    #Affiche les données du formulaire simple
                    print form.cleaned_data

                    # Itération du formset pour afficher les données 
                    for fs in formset:
                        print fs.cleaned_data
                        
                else:
                    print = str( formset.errors )
            else:
                print = str( form.errors )

Forcément ici le code ne sert à rien, si ce n'est à expliquer comment valider formulaire + formset pour un traitement des données .. ( enregistrement en base, actions, mails etc ... ou comme ici un simple affichage ;) )

Dynamisme

Nous voilà avec un super formulaire + formset, qui s'affichent correctement, avec les données validées ( peu importe le nombre de formulaire(s) dans le formset ), mais maintenant nous allons voir comment ajouter / supprimer dynamiquement un formulaire dans le formset .

A partir de maintenant tout se passe dans le template , le principe est simple, nous ajoutons un boutton "+" pour ajouter un formulaire au formset et, pour chaque formulaire du formset un boutton "-" qui permettra de retirer le retirer .

Pour ajouter un formulaire au formset, nous allons faire appel à du javascript, qui va cloner un élément du DOM, ré-indexer les formulaires ( du formset ) puis mettre à jour la valeur du champ de management form-TOTAL_FORMS.

Pourquoi ré-indexer

Si l'on regarde le code généré par le formset, les champs sont indexés pour ne pas mélanger les données entres formulaires, vérifier la cohérence des données etc ..

<!-- Form 1 -->
<input id="id_form-0-designation" class="form-control input-sm" type="text" value="V2" placeholder="" name="form-0-designation">
<input id="id_form-0-quantite" class="form-control input-sm" type="text" value="1000" placeholder="" name="form-0-quantite">
....
<!-- Form n -->
<input id="id_form-n-designation" class="form-control input-sm" type="text" value="V2" placeholder="" name="form-n-designation">
<input id="id_form-n-quantite" class="form-control input-sm" type="text" value="1000" placeholder="" name="form-n-quantite">
...

Formulaire vide

Quel formulaire copier dans le DOM .. et puis .. je ne souhaite pas copier le contenu des champs ...ça tombe bien , encore une fois Django nous livre tout sur un plateau d'argent, grâce à l'attribut empty_form.

Nous allons donc dans un div hors de la balise form inclure le code suivant {{ sorteForm.empty_form }} ce qui va donc nous donner


<div id="my_form">
    <form action="#" name="form_test">
    <!-- Le formulaire standard -->
    {{ form }}

    <button class="btn btn-success btn-sm" id="bt_add_sorte" type="button">+</button>

    {{ sorteForm.management_form }}
        <!-- Les formulaires du formset -->
        <div id="formsetZone">
            {% for form in sorteForm %}
                <div class="nsorte">
                {{ form }}
                </div>
            {% endfor %}
        </div>
    </form>
</div>

<!-- Element à copier pour un ajout au formset -->
<div style="display:none;">
    <div id="eform" class="nsorte" >
        {{ sorteForm.empty_form }}
        <div class='btn btn-warning btn-sm bt_rm_sorte'>
                <i class='glyphicon glyphicon-trash'></i>
        </div>
    </div>
</div>

Voilà à quoi ressemble ce formulaire vide

<label for="id_form-__prefix__-designation">Désignation&nbsp;:</label>
<input class="form-control input-sm" id="id_form-__prefix__-designation" name="form-__prefix__-designation" placeholder="" type="text">
<label for="id_form-__prefix__-quantite">Quantité&nbsp;:</label>
<input class="form-control input-sm" id="id_form-__prefix__-quantite" name="form-__prefix__-quantite" placeholder="" type="text">
<select class="form-control input-sm" id="id_form-__prefix__-rectoverso" name="form-__prefix__-rectoverso">
<option value="False">Recto Seul</option>
<option value="True">Recto Verso</option>
</select>

L'objectif pour l'ajout est donc :

  1. Binder l’événement onclick du bouton avec l'id bt_add_sorte
  2. Cloner le contenu du div avec l'id eform
  3. Ajouter le données clonées à la fin du div id formsetZone
  4. Ré-indexer les formulaires du formset pour remplacer __prefix__ par le bon numéro de formulaire
  5. Incrémenter la valeur du champ de management avec l'id id_form-TOTAL_FORMS

Pour la suppression :

  1. Binder les boutons dont la classe est bt_rm_sorte
  2. Enlever du DOM le formulaire à retirer ( conteneur parent avec la classe nsorte )
  3. Re-indexer les formulaires du Formset
  4. Décrémenter a valeur du champ de management avec l'id id_form-TOTAL_FORMS

vive le javascript

Tout ça en javascript, grâce à JQuery

$(document).ready(function(){

    /**************************************************************************
    *
    *                                      Gesion ADD REMOVE Formset 
    *
    ***************************************************************************/

    index_form = function( fset, index ){

        $(fset).find(':input').each(function() {
            var name = $(this).attr('name').replace( new RegExp('(\_\_prefix\_\_|\\d)') , index );
            var id = 'id_' + name;
            $(this).attr({'name': name, 'id': id});
        });

        $(fset).find('label').each(function() {
            var newFor = $(this).attr('for').replace( new RegExp('(\_\_prefix\_\_|\\d)') , index );
            var id = 'label_' + newFor;
            $(this).attr({'id':id, 'for':newFor});
        });

    }

    reindex_formset = function( formset_zone ){

        var formset = $(formset_zone).find( '.nsorte' );
        for( var cpt=0;cpt<formset.length;cpt++ ){
            index_form( formset[cpt], cpt );
        };

        $("#id_form-TOTAL_FORMS").val( parseInt( cpt ) );

    };



    /**************************************************************************
    *
    *                               Gesion Des evenements formulaire
    *
    ***************************************************************************/


    set_event = function(){
            //Bind le(s) bt delete sorte
            $(".bt_rm_sorte").on('click',function(){
                $(this).parents(".nsorte").remove();
                reindex_formset( "#formsetZone" );
            });
    };

    $("#bt_add_sorte").on('click',function(){

        //Copy eform
        $( "#eform" ).clone(true).appendTo( $("#formsetZone") );

        reindex_formset( "#formsetZone" );

    });

    set_event();


});

Conclusion

Nous avons fait le tour pour gérer les formset dynamiquement, au diable les galères pour gérer ce genre de cas , merci Django .

Ch.

07 nov. 2013

Imprimer en ligne de commande

Nous allons voir dans ce billet comment installer une imprimante sur une machine distante, ne possédant aucune interface graphique, puis comment imprimer un document en ligne de commande .
Sans entrer dans les détails du système d'impression linux, nous allons survoler les principaux outils tels que CUPS, lpr, lpq, lpstat ...

Installation des paquets

COmmençons par installer les paquets nous fournissant les outils necessaires

apt-get install cups cups-bsd

Interface web de CUPS

Cups permet de configurer ( ajouter/modifier/supprimer ) les imprimantes via une interface web ... ( on ne va pas s'en priver ! )

Seulement, avant de pouvoir s'y connecter ( à distance ) nous allons devoir autoriser les connections extérieurs .. ( tout du moins le temps de la configuration )

vim /etc/cups/cupsd.conf

Voici les modifications à apporter

Changer

Listen localhost:631

par

Listen *:631

Ajouter la ligne Allow From All dans les blocs suivant

# Restrict access to the server...
<Location />
  Order allow,deny
  Allow From All
</Location>

# Restrict access to the admin pages...
<Location /admin>
  Order allow,deny
  Allow From All
</Location>

Puis redémarrer cups

service cups restart

Il ne vous reste plus qu'à vous rendre sur l'interface web :

http://ipduserveur:631

Selection_103.png

Ajouter une imprimante

Pour ajouter une imprimante, il suffit de se laisser guider par l'interface web de CUPS, ayant une imprimante réseau compatible postscript, je vais utiliser les pilotes génériques.
Mais vous pouvez trés bien donner le fichier PPD correspondant à votre imprimante .

Allons dans l'onglet Administration > Add Printer

Un mot de passe vous est demandé, utiliser le mot de passe root, ou créez un compte spécifique grâce à la commande lppasswd

root@vm2:~# lppasswd -a cdsl
Entrez le nouveau mot de passe :
Confirmez le nouveau mot de passe :

Cups va alors lister toutes les imprimantes réseau qu'il a trouvé, ou vous proposer d'entrer l'adresse vous même

Selection_104.png

Personnellement, je vais entrer moi même l'ip de mon imprimante ( adresse ldp )

Selection_105.png

Le pilote choisis sera le suivant ( pour mon imprimante laser couleur Konica Bizhub C253 ):

Generic PCL 5c Printer Foomatic/hpijs-pcl5c

Nous en avons fini avec l'interface web ...

En ligne de commande

Vérifions la présence de notre imprimante fraichement installée

root@vm2:~# lpstat -tvH
scheduler is running
no system default destination
device for Konica_Etage: lpd://10.0.0.4/print
Konica_Etage accepting requests since jeu. 07 nov. 2013 16:25:04 CET
printer Konica_Etage is idle.  enabled since jeu. 07 nov. 2013 16:25:04 CET

Le nom Konica_Etage sera le nom à utiliser dans les commandes et scripts pour imprimer via celle-ci.

Par exemple, pour vérifier sa file d'impression

root@vm2:~# lpq -PKonica_Etage
Konica_Etage is ready
no entries

Il ne nous reste plus qu'à imprimer ..
Pour imprimer un document, nous allons utiliser la commande lpr comme ceci

lpr -h -PKonica_Etage -#1 -o media=A4 -o scaling=100 monfichieraimprimer.pdf
  • -h pour désactiver l'impression de la bannière
  • -PKonica_Etage Désigne l'imprimante à utiliser
  • -#1 Permet de spécifier le nombre de copie(s) à sortir ( ici 1 copie )
  • -o media=A4 Spécifie le format de papier à utiliser au niveau de l'imprimante
  • -o scaling=100 Demande l'utilisation de 100% de la zone d'impression en respectant le ratio

Et en fin de ligne, le fichier à imprimer !

Conclusion

Je pense que l'objectif de cet article est rempli, vous avez la possibilité d'imprimer des documents ( pdf, ps, images .. ) depuis votre serveur en ligne de commande .

N'hésitez pas à me faire des retours .

Ch.

31 oct. 2013

Déployer votre app Python automatiquement avec fabric .

Petite mise en place

Lorsque vous développez en Python, contrairement à PHP, ce dernier compile le code à la première "interprétation", puis génère des fichiers "bytecode", avec l’extension .pyc.
Une fois cette génération faite, le fichier source .py du même nom sera ignoré, et ce jusqu'au redémarrage du process.
Le redémarrage du process, vérifiera les modifications apportées aux sources ( .py ) afin de les recompiler et exécuter le code mis à jour.

Tout ça pour en venir au fait, que lorsque vous développez en python ( par exemple une app. web avec Django ), lorsque vous envoyez vos maj sur le serveur de production, vos modifications n'auront aucun effet tant que vous n'avez pas redémarré les process ( uwsgi, ou autre .. ) .

Ce qui peut vous surprendre au début, si vous venez du monde PHP ou les modifications sont prises en compte instantanément.
Bien que vous pouvez avoir quelque chose de similaire si vous utilisez PHP avec le cache APC ... mais ce n'est pas le sujet ici.

Étapes manuelles

Donc si je prends mon projet Django, voici les étapes manuelles à appliquer :

Sur mon poste de développement :

git add xx ; s'il y a des fichiers sources à ajouter au dépôt
git commit -a -m "Commentaire de mon commit"
git push

Sur mon serveur de prod :

cd /mon/depot
git pull 
# vim settings.py pour des update de version de cache etc .. 
workon monenvironnementvirtuel
python manage.py collectstatic
supervisorctl MonApp reload

Étapes avec fabric

Sur mon poste de développement :
fab backup
Sur le serveur
fab deploy

Bon ... voyons maintenant comment mettre en place fabric .

Installation et fabfile.py

Commençons par installer la librairie ( sur le serveur et le poste de dev. )

workon monenvironnement
pip install fabric

Puis nous allons créer notre fichier qui va se charger d'automatiser les différentes étapes manuelles: fabfile.py

#! -*- coding:utf-8 -*-
from fabric.api import local,prefix
from fabric.colors import green

import re

PROJECT_NAME = "MonApp" # Nom de l'app
SUPERVISOR_APP_NAME = "MonApp" # Nom de l'app supervisor à redémarrer
VIRTUALENV_NAME = "MonApp"  #Nom de l'environnement virtuel à utiliser

def backup():
    #Sauvegarde les librairies utilisées par le projet ( pour les installer si besoin sur le serveur .. sans réfléchir )
    local('pip freeze > requirements.txt')

    #Ajoute les éventuels fichiers non "trackés" 
    local('git add .')

    git_status = local( 'git status', capture=True )
    print git_status

    regx = re.compile(r"(nothing to commit)")
    if not regx.search( git_status ):

        print(green("Il y a des choses à commiter !"))

        print("Entrez votre commentaire de commit : ")
        comment = raw_input()

        #Renseignement du commentaire du commit
        local('git commit -m "%s"' % comment)
        local('git push')
    else:
        print(green("Rien à commiter !"))


def deploy():
    """
    Déploie l'app sur le "serveur" courant 
    """

    #Récupère la dernière version du dépôt
    local('git pull')

    #Permet de changer la version des fichiers statiques à servir 
    print(green("Version du cache à appliquer []:"))
    vcache = raw_input()
    xs = vcache.strip()
    if xs[0:1] in '+-': xs = xs[1:]
    if xs.isdigit():
        init_cache(int(xs))


    # Active ou désactive le mode DEBUG
    print(green("Désactiver le mode DEBUG ( y/n ) ? [y]"))
    disable_debug = raw_input()
    print disable_debug
    if disable_debug == 'y' or disable_debug=='':
        print(green("Passage du mode DEBUG à FALSE"))
        switch_debug(False)
    else:
        print(green("Passage du mode DEBUG à TRUE"))
        switch_debug(True)

    #Récupère les éventuels nouveaux fichiers statiques 
    local(django_manage( 'collectstatic --link', VIRTUALENV_NAME ),capture=False,shell='/bin/bash')

    #Redémarre le processus supervisord
    local('supervisorctl restart '+SUPERVISOR_APP_NAME)



def init_cache(change_to):

    local( 'cp '+ PROJECT_NAME +'/settings.py '+ PROJECT_NAME +'/settings.bak' )
    sed = "sed 's/^VERSION_JS_CSS = [0-9]*$/VERSION_JS_CSS = %s/' "+ PROJECT_NAME+"/settings.bak > "+PROJECT_NAME+"/settings.py"
    local(sed % (change_to))

    local('rm '+PROJECT_NAME+'/settings.bak')

def switch_debug(change_to):

    local( 'cp '+ PROJECT_NAME +'/settings.py '+ PROJECT_NAME +'/settings.bak' )
    sed = "sed 's/^DEBUG = [a-zA-Z]*$/DEBUG = %s/' "+ PROJECT_NAME+"/settings.bak > "+PROJECT_NAME+"/settings.py"
    local(sed % (change_to))

    local('rm '+PROJECT_NAME+'/settings.bak')

Et voilà en deux commandes, vous avez mis à jour votre application sur le serveur de production, chargé les éventuels fichier statiques dans le répertoire prévu à cet effet, modifié le fichier settings.py pour y indiquer s'il faut invalider les fichiers statiques ( css, js .. ) afin de forcer les navigateurs à charger les nouvelles versions, activé ou non le mode DEBUG de Django, et pour finir relancé les process uwsgi grâce à supervisor ( commande supervisorctl ).

Il est possible de tout faire depuis son poste de développement, Fabric peu se connecter sur les serveurs distants pour déployer l'application, ce qui peu s'avérer très utile lorsque vous avez plusieurs frontaux à mettre à jour .

Backup

Fabric_backup.png

Deploy

Fabric_deploy.png

Source:

Fabric : http://docs.fabfile.org/en/1.8/

02 oct. 2013

Mezzanine épisode 2

mezzanine.png Nous avons vu dans un précédent billet, comment installer le CMS mezzanine et créer un nouveau projet.
Après s'être familiarisé avec son utilisation, voyons comment customiser notre projet.

Organisation des Thèmes

Un thème mezzanine = une app ( app: répertoire avec un fichier nommé init.py dedans )

Créons notre thème montheme

mkdir montheme && touch montheme/__init__.py

Puis nous allons le déclarer dans le fichier settings.py , au début de le liste INSTALLED_APPS

INSTALLED_APPS = (
    "montheme", # thème à utiliser
    "django.contrib.admin",
    ...

Bien, maintenant nous allons créer la structure de notre thème, à savoir, créer les répertoires de fichiers qtatiques ("static/") et celui des templates ("templates/")

mkdir -p montheme/static/css && mkdir montheme/static/img && mkdir montheme/static/js
mkdir -p montheme/templates/pages/menus && mkdir montheme/templates/includes

Nous voilà avec un superbe template ...... qui ne fait rien !

Il ne nous reste donc plus qu'à créer nos feuilles de styles et fichiers Html pour surcharger le thème par défaut .

Récupérer le thème par défaut

Pour avoir une base fonctionnel, nous allons récupérer les templates et fichiers statiques du thème mezzanine par défaut.

Toujours à la racine de notre projet,

python manage.py collectstatic
python manage.py collecttemplates

Vous vous retrouvez avec deux nouveaux répertoires templates et static qui contiennent donc respectivement les templates et fichiers statiques nécessaires au fonctionnement de base de Mezzanine.

/!\ Attention : si vous laissez tel quel les répertoires, votre template ne sera jamais pris en compte car les répertoires templates et static à la racine du projet sont prioritaires . ( Scannés en premier par Django )
Renommons donc ces répertoires afin de les rendre inactifs

mv templates templates_orig
mv static static_orig

Création de notre thème

Avant d'entrer dans le vif du sujet, il faut bien comprendre comment Mezzanine cherche ses templates.

Par défaut, Mezzanine cherche le template, qui a le même nom que la page demandée. Par exemple, dans l'interface admin, je crée une page dont le titre est "Authors" et le "slug" ( non .. pas la limace, mais une url propre ) : "About/authors", mezzanine va chercher une des correspondances suivantes :

  1. pages/about/authors.html
  2. pages/about/authors/about.html
  3. pages/about/about.html
  4. pages/about.html
  5. pages/page.html


page.html étant la page générique qui sera appelée en dernier recours, c'est cette page qui est disponible "entres-autres" dans les templates du thème par défaut.
Voilà notre point de départ, nous allons donc commencer par copier cette page dans notre thème, qui sera aussi notre page par défaut.

cp templates_orig/pages/page.html montheme/templates/pages/

Si on édite le fichier page.html, nous pouvons voir qu'il s'agit juste d'une structure d'affichage qui s’intègre dans le template base.html .
Copions le également.

cp templates/base.html montheme/templates/

Feuilles de style et javascript

Afin de ne pas perdre les styles et fonctions javascript de base, nous allons créer les nôtres ( au besoin évidemment )

touch montheme/static/css/montheme.css
touch montheme/static/js/montheme.js

Puis modifiez les blocs suivants pour ajouter montheme.css et montheme.js

block compress css
<link rel="stylesheet" href="{% static "css/montheme.css" %}">
block compress js
<script src="{% static "js/montheme.js" %}"></script>

Conclusion

Nous voilà avec un thème qui pour le moment ne fait que reprendre celui par défaut, mais il est en place ! Il ne reste plus qu'à personnaliser les fichiers page.html et base.html dans un premier temps à votre gout .
Si vous souhaitez allez plus loin sur l'intégration des différents éléments dans votre thème, il suffit de suivre le même principe .

  1. Copier les pages concernées du thème original dans votre thème
  2. Les adapter à votre gout ! c'est tout .

Par exemple, si je souhaite modifier le menu en entête, je vais chercher le fichier dropdown.html qui se trouve dans le répertoire templates/pages/menus/ , je le copie dans mon thème montheme/templates/pages/menus/ et je peux travailler dessus en toute liberté .

N'hésitez pas à activer django_debug_toolbar ( dans votre fichier settings.py ) pour vous aider.

Ch.

17 sept. 2013

Serveur de fichiers AFP sous wheezy

netatalk.gif Je partages de gros volumes sur le réseau, avec majoritairement des Clients Apple .
Jusqu'à maintenant, samba répondait pleinement à mon besoin, mais depuis la version Lion d'OS X, je rencontre des lenteurs plus ou moins prononcées pendant la navigation dans les volumes partagés.
Seuls les anciens postes et autres systèmes d'exploitation se comportent normalement.

N'ayant pas trouvé la cause pour le moment, j'ai décider de tester le protocole AFP, protocole de prédilection Apple ..

Sous linux, AFP == Netatalk, qui est intégré au noyau, afin de nous faciliter la tâche pour interconnecter des réseaux Apple .

Prérequis

Sous linux ( wheezy ) au moment ou j'écris ce tuto

sudo apt-get install netatalk avahi-daemon

Configuration

Afin de diffuser le service Netatalk sur le réseau, il nous faut mettre en place Avahi

netatalk

Pour avoir une idée clair des fichiers de configuration de netatalk, utiliser la commande

afpd -V

Ce qui donne

afpd 2.2.2 - Apple Filing Protocol (AFP) daemon of Netatalk
 
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation; either version 2 of the License, or (at your option) any later
version. Please see the file COPYING for further information and details.
 
afpd has been compiled with support for these features:
 
          AFP versions:	1.1 2.0 2.1 2.2 3.0 3.1 3.2 3.3 
DDP(AppleTalk) Support:	Yes
         CNID backends:	dbd last tdb 
           SLP support:	No
      Zeroconf support:	Yes
  TCP wrappers support:	Yes
         Quota support:	Yes
   Admin group support:	Yes
    Valid shell checks:	Yes
      cracklib support:	Yes
        Dropbox kludge:	No
  Force volume uid/gid:	No
            EA support:	ad | sys
           ACL support:	Yes
          LDAP support:	Yes
 
             afpd.conf:	/etc/netatalk/afpd.conf
   AppleVolumes.system:	/etc/netatalk/AppleVolumes.system
  AppleVolumes.default:	/etc/netatalk/AppleVolumes.default
    afp_signature.conf:	/etc/netatalk/afp_signature.conf
      afp_voluuid.conf:	/etc/netatalk/afp_voluuid.conf
         afp_ldap.conf:	/etc/netatalk/afp_ldap.conf
       UAM search path:	/usr/lib/netatalk/
  Server messages path:	/etc/netatalk/msg/
              lockfile:	/var/run/afpd.pid

Dans mon cas, je partage un répertoire commun à tous mes utilisateurs, donc pas de login ni de mot de passe.

vim /etc/netatalk/afpd.conf

pour éviter la demande de mot de passe à la connexion, nous allons dans le fichier afpd.conf modifier la ligne suivante

- -tcp -noddp -uamlist uams_dhx.so,uams_dhx2.so -nosavepassword

par celle-ci

- -tcp -noddp -uamlist uams_guest.so,uams_dhx.so,uams_dhx2.so -nosavepassword

Afin d'activer le compte invité.

Déclarer un partage

Il ne reste plus à déclarer un partage, exemple /var/data/commun . Évidemment ce répertoire doit exister .

Nous allons déclarer ce partage dans le fichier AppleVolumes.default

vim /etc/netatalk/AppleVolumes.default

On y déclare le répertoire à partager, en ajoutant à la fin

/var/data/commun "commun" cnidscheme:dbd options:usedots,upriv,tm

Le fichier contient toutes les explications sur les options disponibles .

Avahi

Nous allons demander à Avahi de déclarer le service afpd de notre serveur sur le réseau . Nous allons donc simplement déclarer un nouveau service à avahi

vim /etc/avahi/services/afpd.service

Voici le contenu ( qui n'est pas à modifier )

<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
  <name replace-wildcards="yes">%h</name>
  <service>
    <type>_afpovertcp._tcp</type>
    <port>548</port>
  </service>
  <service>
    <type>_device-info._tcp</type>
    <port>0</port>
    <txt-record>model=Xserve</txt-record>
  </service>
</service-group>

Lancement des services

Nous reste plus à lancer les services netatalk et avahi fraichement configurés .

sudo /etc/init.d/netatalk restart
sudo /etc/init.d/avahi-daemon restart

Sur les postes clients, dans l'exploration réseau, vous devriez voir apparaître le partage .

Ch.

- page 1 de 26