Python: утилита для создания нового Apache VirtualHost + пользователь + база MySQL

Автор: | 14/09/2014
 

PythonГотовясь с переезду со старого сервера (FreeBSD) на новый (CentOS) – пришлось задуматься о том, сколько сайтов надо создавать вручную: добавить пользователя, создать директории, файлы логов, отредактировать файл конфигурации Apache HTTP.

С целью упростить себе жизнь, а заодно – попрактиковаться в Python, задумался следующий скрипт.

Предполагается, что Apache настроен с поддержкой suEXEC + mod_fcgid (CentOS: установка Apache HTTP + suEXEC + mod_fcgid) + в качестве frontend установлен NGINX (CentOS: установка Apache HTTP + NGINX).

Скрипт использует модуль argparcer для получения данных. Есть возможность отключить добавление базы и пользователя MySQL или наоборот – отключить создание файлов и директорий, и создать только базу и пользователя MySQL (хотя в данном случае используется MariaDB, но роли не играет).

В общем, это завершённая и уже более адекватная версия скрипта из поста Python: скрипт создания нового виртуалхоста для нового пользователя.

На сервере используются:

# cat /etc/redhat-release
CentOS release 6.5 (Final)
# python -V
Python 2.6.6
# httpd -V
Server version: Apache/2.2.15 (Unix)
Server built: Jul 23 2014 14:17:29
...
Architecture: 64-bit
Server MPM: Prefork
# nginx -v
nginx version: nginx/1.6.1

Скрипт разделён на две части – одна с классами, и вторая, “основная” – непосредственно тело скрипта.

Сначала – файл классов:

#!/usr/bin/env python

import os
import sys
import crypt
import subprocess
import errno
import errno
import shutil
import pwd
import grp
import re
import MySQLdb
import warnings

class MainClass(object):

        def __init__(self, user, password, domain):
                self.user = user
                self.password = password
                self.domain = domain

        def isroot(self):
                if not os.geteuid() == 0:
                        raise SystemExit

        def answer(self, prompt, choice='Yes or no, please!'):
                while True:
                        result = raw_input(prompt)
                        if result in ('y', 'Y', 'yes', 'Yes'):
                                break
                        elif result in ('n', 'N', 'no', 'No'):
                                raise SystemExit
                        else:
                                print(choice)

class SystemOperations(MainClass):

        def __init__(self, *args):
                super(SystemOperations, self).__init__(*args)

        def useradd(self):
                # create salt
                salt = os.urandom(6).encode('base_64')
                # encrypt password
                encpass = crypt.crypt(self.password, salt)
                # adding user
                subprocess.call('useradd -p %s %s' % (encpass, self.user), shell=True)

        def mkdir(self, dir):
                self.dir = dir
                os.makedirs(self.dir)

        def cpcgi(self, src, dst):
                self.src = src
                self.dst = dst
                shutil.copy(self.src, self.dst)

        def chowdir(self, path):
                self.path = path
                uid = pwd.getpwnam(self.user).pw_uid
                gid = grp.getgrnam(self.user).gr_gid

                for root, dirs, files in os.walk(self.path):
                        for a in dirs:
                                os.chown(os.path.join(root, a), uid, gid)
                        for a in files:
                                os.chown(os.path.join(root, a),  uid, gid)
                os.chown(self.path, uid, gid)
                subprocess.call('ls -l %s | grep -v total' % self.path, shell=True)

                # create lof-files
        def tchlog(self, logfile):
                self.logfile = logfile
                open(logfile, 'a')

        def createconf(self, configin, configout, oflist):
                self.configin = configin
                self.configout = configout
                self.oflist = oflist
                tolist = (self.user, self.domain)

                shutil.copy(self.configin, self.configout)

                for of, to in zip(self.oflist, tolist):
                        with open(self.configout, 'r') as infile:
                                data = infile.readlines()

                        with open(self.configout, 'w') as infile:
                                for string in data:
                                        infile.write(re.sub(of, to, string))

class mysqlUserDb(MainClass):

        warnings.filterwarnings('error')

        def __init__(self, dbroot, dbhost, dbrootpw, *args):
                super(mysqlUserDb, self).__init__(*args)

                self.dbroot = dbroot
                self.dbhost = dbhost
                self.dbrootpw = dbrootpw

                self.db = MySQLdb.connect(self.dbhost, self.dbroot, self.dbrootpw)
                self.cursor = self.db.cursor()

        def createdb(self, dbuserdb):

                self.dbuserdb = dbuserdb

                self.cursor.execute('create database if not exists ' + self.dbuserdb)
                self.cursor.execute("show databases like '%s' " % self.dbuserdb) #+ ''' + self.dbuserdb + ''')
                self.dbs = self.cursor.fetchone()
                return self.dbs

        def grants(self, dbuserdb):

                self.cursor.execute("grant all on %s.* to '%s'@'localhost' identified by '%s'  " % (self.dbuserdb, self.user, self.password))
                self.cursor.execute("select user, db from mysql.db where db='%s'" % self.dbuserdb)
                self.grs = self.cursor.fetchall()
                return self.grs

        def __del__(self):
                self.cursor.close()
                self.db.close()

И файл самого скрипта:

#!/usr/bin/env python
import os
import argparse
import getpass
import sys
import pwd
import warnings
import create_vhost_Classes_new as fun

parser = argparse.ArgumentParser()
parser.add_argument('-u', '--user', help='Username, for OS user and MySQL user.')
parser.add_argument('-p', '--password', help='Password for OS and MySQL user.', dest='password', default=None)
parser.add_argument('-d', '--domain', help='Domainname, will be created virtualhost directories, files.')
parser.add_argument('-m', '--dbuserdb', help='MySQL database name for user.')
parser.add_argument('-r', '--dbrootpw', help='MySQL root password.', dest='dbrootpw', default=None)
parser.add_argument('-s', '--system', help='Add `-s` option to disable system operation (add user, create directories and files)', action='store_false')
parser.add_argument('-M', '--mysql', help='Add `-m` to disable MySQL operations (add user, add database).', action='store_false')

options = parser.parse_args()

# user's data
user = options.user
password = options.password
domain = options.domain

# MySQL data
dbhost = 'localhost'
dbroot = 'root'
dbrootpw = options.dbrootpw
dbuserdb = options.dbuserdb

# Apache and NGINX config directories
virt_hosts_dir = '/var/www/'
# Apache log dir
logs_dir = '/var/log/httpd/'
# used when creates new directories (mkdir)
dest = ('vhosts/', 'php-cgi/')
# used when creates log-files
logtypes = ('-access.log', '-error.log')
# used when creates Apache and NGINX configuration files
config_dirs = ('/etc/httpd/conf.d/', '/etc/nginx/conf.d/')

try:
        # user can skip enter password with option;
        # instead - will enter here, more secure
        if not options.password:
                password = getpass.getpass('Please, enter user password: ')

        # check input data
        main = fun.MainClass(user, password, domain)
        print('nUser = %s;nDomain = %s;nPassword = %s;nMySQL database = %s.n' % (main.user, main.password, main.domain, dbuserdb))
        main.answer('Is it correct? ')
        print('OK, proceeding.')
except KeyboardInterrupt:
        print('Exit.')
        sys.exit(0)
# if answer() `NO` - it raise SystemExit
except SystemExit:
        print('Exit. Please, restart with correct data.')
        sys.exit(0)

try:
        # check does scipt started under root
        print('nRunning user check...')
        main.isroot()
        print('You are root, OK.n')
except SystemExit:
        print('Must be started as root. Exit.n')
        sys.exit(0)

# if `-s` option was not set and options.system = True
if options.system:

        # creates object for main OS operations
        sysacts = fun.SystemOperations(user, password, domain)

        # user add
        print('Adding user %s...' % user)
        sysacts.useradd()

        # check if new user exist
        try:
                pwd.getpwnam(user)
                print('Done.')
        except KeyError as e:
                print('ERROR! %s.nExit.' %e)
                sys.exit(1)

        ## DIRECTORIES
        # first create site files directory
        # then php-cgi directory
        for i in dest:
                dir = virt_hosts_dir + i + user + '/' + domain + '/'
                print('nCreating new site files directory %s' % dir)
                # if no directory - run mkdir
                if not os.path.isdir(dir):
                        try:
                                sysacts.mkdir(dir)
                                print('Done.')
                        except OSError as e:
                                print('ERROR: %s' %e)
                                sys.exit(2)
                # otherwise - skip
                else:
                        print('Directory %s already present, skipping.' % dir)

        # copy php.cgi file to new directory
        # from file template
        src = virt_hosts_dir + 'php-cgi/php.cgi'
        # to newly created directory
        dst = virt_hosts_dir + 'php-cgi/' + user + '/' + domain
        print('nCopying %s to %s...' % (src, dst))
        if not os.path.isfile(dst + '/php.cgi'):
                try:
                        sysacts.cpcgi(src, dst)
                        print('Done.')
                except IOError as e:
                        print('ERROR! %s.nExit.' %e)
                        sys.exit(3)
        else:
                print('File %s already present, skipping.' % (dst + '/php.cgi'))

        # chmod files and directories for new user
        for i in dest:
                path = virt_hosts_dir + i + user + '/'
                print('nChanging owner of %s directory and files to %s:' % (path, user))
                try:
                        sysacts.chowdir(path)
                except OSError as e:
                        print('ERROR: %s' %e)
                        sys.exit(4)

        # create log-file
        # two types of logs (access and error)
        # thus - run in loop both
        for i in logtypes:
                logfile = logs_dir + domain + i
                if not os.path.isfile(logfile):
                        print('nCreating log-file %s' % logfile)
                        try:
                                sysacts.tchlog(logfile)
                        except OSError as e:
                                print('ERROR: %s' %e)
                                sys.exit(5)
                else:
                        print('nFile %s already present, skipping.' % logfile)

        ## create config file
        # data which will looking for
        # in template configs to change it
        oflist = ('USER', 'DOMAIN')
        # will change to this variables
        tolist = (user, domain)

        # for both Aoache and NGINX config dirs
        for path in config_dirs:
                # create var with name of template file
                configin = path + 'TEMPLATE'
                # creat var with name of new config file
                configout = path + domain + '.conf'
                print('nCreating new configuration file %s.' % configout)
                if not os.path.isfile(configout):
                        try:
                                sysacts.createconf(configin, configout, oflist)
                                print('Done.')
                        except IOError as e:
                                print('ERROR: %s' %e)
                                sys.exit(6)
                else:
                        print('File %s already present, skipping.' % configout)
else:
        print('Skipping system actions.')

# if `-M` option was not set and options.mysql = True
if options.mysql:

        print('nStarting MySQL actions.')

        if not options.dbrootpw:
                dbrootpw = getpass.getpass('nPlease, enter MySQL root password: ')


        # we must have database name
        if dbuserdb == None:
                print('nERROR! please, set `-m` option with User database name.n')
                sys.exit(7)

        warnings.filterwarnings('error')

        # Open and check MySQL connection
        try:
                dbconnect = fun.mysqlUserDb(dbroot, dbhost, dbrootpw, user, password, domain)
                print('Connection opened successfully.')
        except MySQLdb.Error as e:
                print('MySQL error: %s nExit.n' % e)
                sys.exit(8)

        # create database
        try:
                print('nCreating database %s.' % dbuserdb)
                dbconnect.createdb(dbuserdb)
                print 'Database created: %s' % dbconnect.dbs
        except Warning as error:
                print('MySQL warning: %s' % error)
        except MySQLdb.Error as e:
                print('MySQL error: %s nExit.n' % e)
                sys,exit(9)

        # add user and grant prielegies
        try:
                print('nAdding privilegies to user %s with password %s to database %s.' % (user, password, dbuserdb))
                dbconnect.grants(dbuserdb)
                print('Access granted: %r' % dbconnect.grs)
        except MySQLdb.Error as e:
                print('MySQL error: %s nExit.n' % e)
                sys,exit(10)

        # close connection
        print('nFinishing MySQL actions...')
        del dbconnect
        print 'Done.n'

else:
        print('Skipping MySQL actions.')

Пример работы:

# ./create_vhost_new.py -h
usage: create_vhost_new.py [-h] [-u USER] [-p PASSWORD] [-d DOMAIN]
                           [-m DBUSERDB] [-r DBROOTPW] [-s] [-M]

optional arguments:
  -h, --help            show this help message and exit
  -u USER, --user USER  Username, for OS user and MySQL user.
  -p PASSWORD, --password PASSWORD
                        Password for OS and MySQL user.
  -d DOMAIN, --domain DOMAIN
                        Domainname, will be created virtualhost directories,
                        files.
  -m DBUSERDB, --dbuserdb DBUSERDB
                        MySQL database name for user.
  -r DBROOTPW, --dbrootpw DBROOTPW
                        MySQL root password.
  -s, --system          Add `-s` option to disable system operation (add user,
                        create directories and files)
  -M, --mysql           Add `-m` to disable MySQL operations (add user, add
                        database).
# ./create_vhost_new.py -u testsite8 -p Password -d testsite8.com -m testsite8_db1

User = testsite8;
Domain = Password;
Password = testsite8.com;
MySQL database = testsite8_db1.

Is it correct? y
OK, proceeding.

Running user check...
You are root, OK.

Adding user testsite8...
Done.

Creating new site files directory /var/www/vhosts/testsite8/testsite8.com/
Done.

Creating new site files directory /var/www/php-cgi/testsite8/testsite8.com/
Done.

Copying /var/www/php-cgi/php.cgi to /var/www/php-cgi/testsite8/testsite8.com...
Done.

Changing owner of /var/www/vhosts/testsite8/ directory and files to testsite8:
drwxr-xr-x 2 testsite8 testsite8 4096 Sep 14 17:19 testsite8.com

Changing owner of /var/www/php-cgi/testsite8/ directory and files to testsite8:
drwxr-xr-x 2 testsite8 testsite8 4096 Sep 14 17:19 testsite8.com

Creating log-file /var/log/httpd/testsite8.com-access.log

Creating log-file /var/log/httpd/testsite8.com-error.log

Creating new configuration file /etc/httpd/conf.d/testsite8.com.conf.
Done.

Creating new configuration file /etc/nginx/conf.d/testsite8.com.conf.
Done.

Starting MySQL actions.

Please, enter MySQL root password:
Connection opened successfully.

Creating database testsite8_db1.
Database created: testsite8_db1

Adding privilegies to user testsite8 with password Password to database testsite8_db1.
Access granted: ('testsite8', 'testsite8_db1')

Finishing MySQL actions...
Done.

Проверяем MySQL (хотя – это видно и в выводе самого скрипта):

# mysql -u testsite8 -pPassword
Welcome to the MariaDB monitor.  Commands end with ; or g.
Your MariaDB connection id is 22311
Server version: 5.5.39-MariaDB MariaDB Server

Copyright (c) 2000, 2014, Oracle, Monty Program Ab and others.

Type 'help;' or 'h' for help. Type 'c' to clear the current input statement.

MariaDB [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| testsite8_db1      |
+--------------------+
2 rows in set (0.00 sec)

Проверяем работу PHP:

# printf "This is Testsite 8.n<?php echo phpinfo(); ?>" > /var/www/vhosts/testsite8/testsite8.com/index.php

Перезапускаем Apache HTTP и NGINX:

# service httpd restart
Stopping httpd: [ OK ]
Starting httpd: [ OK ]

# service nginx restart
Stopping nginx: [ OK ]
Starting nginx: [ OK ]

Добавляем запись в hosts:

# cat /etc/hosts | tail -n 1
77.***.***.20 testsite8.com

Проверяем:

# curl http://testsite8.com | head
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 57122    0 57122    0     0  1558k      0 --:--:-- --:--:-- --:--:-- 1690k
This is Testsite 8.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
<html><head>
<style type="text/css">
body {background-color: #ffffff; color: #000000;}
body, td, th, h1, h2 {font-family: sans-serif;}
pre {margin: 0px; font-family: monospace;}
a:link {color: #000099; text-decoration: none; background-color: #ffffff;}
a:hover {text-decoration: underline;}
table {border-collapse: collapse;}

Всё работает.