#!/usr/bin/env python3 #coding: UTF-8 '''This script would guide the seafile admin to setup seafile with MySQL''' import argparse import sys import os import time import re import shutil import glob import subprocess import hashlib import getpass import uuid import warnings import socket from configparser import ConfigParser import pymysql try: import readline # pylint: disable=W0611 except ImportError: pass SERVER_MANUAL_HTTP = 'https://download.seafile.com/published/seafile-manual/home.md' class Utils(object): '''Groups all helper functions here''' @staticmethod def welcome(): '''Show welcome message''' welcome_msg = '''\ ----------------------------------------------------------------- This script will guide you to setup your seafile server using MySQL. Make sure you have read seafile server manual at %s Press ENTER to continue -----------------------------------------------------------------''' % SERVER_MANUAL_HTTP print(welcome_msg) input() @staticmethod def highlight(content): '''Add ANSI color to content to get it highlighted on terminal''' return '\x1b[33m%s\x1b[m' % content @staticmethod def info(msg): print(msg) @staticmethod def error(msg): '''Print error and exit''' print() print('Error: ' + msg) sys.exit(1) @staticmethod def run_argv(argv, cwd=None, env=None, suppress_stdout=False, suppress_stderr=False): '''Run a program and wait it to finish, and return its exit code. The standard output of this program is supressed. ''' with open(os.devnull, 'w') as devnull: if suppress_stdout: stdout = devnull else: stdout = sys.stdout if suppress_stderr: stderr = devnull else: stderr = sys.stderr proc = subprocess.Popen(argv, cwd=cwd, stdout=stdout, stderr=stderr, env=env) return proc.wait() @staticmethod def run(cmdline, cwd=None, env=None, suppress_stdout=False, suppress_stderr=False): '''Like run_argv but specify a command line string instead of argv''' with open(os.devnull, 'w') as devnull: if suppress_stdout: stdout = devnull else: stdout = sys.stdout if suppress_stderr: stderr = devnull else: stderr = sys.stderr proc = subprocess.Popen(cmdline, cwd=cwd, stdout=stdout, stderr=stderr, env=env, shell=True) return proc.wait() @staticmethod def get_command_output(args, *a, **kw): return subprocess.check_output(args, *a, **kw) @staticmethod def prepend_env_value(name, value, env=None, seperator=':'): '''prepend a new value to a list''' if env is None: env = os.environ try: current_value = env[name] except KeyError: current_value = '' new_value = value if current_value: new_value += seperator + current_value env[name] = new_value @staticmethod def must_mkdir(path): '''Create a directory, exit on failure''' if os.path.exists(path): return try: os.mkdir(path) except OSError as e: Utils.error('failed to create directory %s:%s' % (path, e)) @staticmethod def must_copy(src, dst): '''Copy src to dst, exit on failure''' try: shutil.copy(src, dst) except Exception as e: Utils.error('failed to copy %s to %s: %s' % (src, dst, e)) @staticmethod def find_in_path(prog): if 'win32' in sys.platform: sep = ';' else: sep = ':' dirs = os.environ['PATH'].split(sep) for d in dirs: d = d.strip() if d == '': continue path = os.path.join(d, prog) if os.path.exists(path): return path return None @staticmethod def get_python_executable(): '''Return the python executable. This should be the PYTHON environment variable which is set in setup-seafile-mysql.sh ''' return os.environ['PYTHON'] @staticmethod def read_config(fn): '''Return a case sensitive ConfigParser by reading the file "fn"''' cp = ConfigParser() cp.optionxform = str cp.read(fn) return cp @staticmethod def write_config(cp, fn): '''Return a case sensitive ConfigParser by reading the file "fn"''' with open(fn, 'w') as fp: cp.write(fp) @staticmethod def ask_question(desc, key=None, note=None, default=None, validate=None, yes_or_no=False, password=False): '''Ask a question, return the answer. @desc description, e.g. "What is the port of ccnet?" @key a name to represent the target of the question, e.g. "port for ccnet server" @note additional information for the question, e.g. "Must be a valid port number" @default the default value of the question. If the default value is not None, when the user enter nothing and press [ENTER], the default value would be returned @validate a function that takes the user input as the only parameter and validate it. It should return a validated value, or throws an "InvalidAnswer" exception if the input is not valid. @yes_or_no If true, the user must answer "yes" or "no", and a boolean value would be returned @password If true, the user input would not be echoed to the console ''' assert key or yes_or_no # Format description print() if note: desc += '\n' + note desc += '\n' if yes_or_no: desc += '[ yes or no ]' else: if default: desc += '[ default "%s" ]' % default else: desc += '[ %s ]' % key desc += ' ' while True: # prompt for user input if password: answer = getpass.getpass(desc).strip() else: answer = input(desc).strip() # No user input: use default if not answer: if default: answer = default else: continue # Have user input: validate answer if yes_or_no: if answer not in ['yes', 'no']: print(Utils.highlight('\nPlease answer yes or no\n')) continue else: return answer == 'yes' else: if validate: try: return validate(answer) except InvalidAnswer as e: print(Utils.highlight('\n%s\n' % e)) continue else: return answer @staticmethod def validate_port(port): try: port = int(port) except ValueError: raise InvalidAnswer('%s is not a valid port' % Utils.highlight(port)) if port <= 0 or port > 65535: raise InvalidAnswer('%s is not a valid port' % Utils.highlight(port)) return port class InvalidAnswer(Exception): def __init__(self, msg): Exception.__init__(self) self.msg = msg def __str__(self): return self.msg class InvalidParams(Exception): def __init__(self, msg): Exception.__init__(self) self.msg = msg def __str__(self): return self.msg ### END of Utils #################### class EnvManager(object): '''System environment and directory layout''' def __init__(self): self.install_path = os.path.dirname(os.path.abspath(__file__)) self.top_dir = os.path.dirname(self.install_path) self.bin_dir = os.path.join(self.install_path, 'seafile', 'bin') self.central_config_dir = os.path.join(self.top_dir, 'conf') self.central_pids_dir = os.path.join(self.top_dir, 'pids') self.central_logs_dir = os.path.join(self.top_dir, 'logs') Utils.must_mkdir(self.central_config_dir) def check_pre_condiction(self): def error_if_not_exists(path): if not os.path.exists(path): Utils.error('"%s" not found' % path) paths = [ os.path.join(self.install_path, 'seafile'), os.path.join(self.install_path, 'seahub'), os.path.join(self.install_path, 'runtime'), ] for path in paths: error_if_not_exists(path) if os.path.exists(ccnet_config.ccnet_dir): Utils.error('Ccnet config dir \"%s\" already exists.' % ccnet_config.ccnet_dir) def get_seahub_env(self): '''Prepare for seahub syncdb''' env = dict(os.environ) env['CCNET_CONF_DIR'] = ccnet_config.ccnet_dir env['SEAFILE_CONF_DIR'] = seafile_config.seafile_dir env['SEAFES_DIR'] = os.path.join(self.install_path, 'pro', 'python', 'seafes') self.setup_python_path(env) return env def setup_python_path(self, env): '''And PYTHONPATH and CCNET_CONF_DIR/SEAFILE_CONF_DIR to env, which is needed by seahub ''' install_path = self.install_path pro_pylibs_dir = os.path.join(install_path, 'pro', 'python') extra_python_path = [ pro_pylibs_dir, os.path.join(install_path, 'seahub', 'thirdpart'), os.path.join(install_path, 'seahub-extra'), os.path.join(install_path, 'seahub-extra', 'thirdparts'), os.path.join(install_path, 'seafile/lib/python3.6/site-packages'), os.path.join(install_path, 'seafile/lib64/python3.6/site-packages'), ] for path in extra_python_path: Utils.prepend_env_value('PYTHONPATH', path, env=env) def get_binary_env(self): '''Set LD_LIBRARY_PATH for seafile server executables''' env = dict(os.environ) lib_dir = os.path.join(self.install_path, 'seafile', 'lib') lib64_dir = os.path.join(self.install_path, 'seafile', 'lib64') Utils.prepend_env_value('LD_LIBRARY_PATH', lib_dir, env=env) Utils.prepend_env_value('LD_LIBRARY_PATH', lib64_dir, env=env) return env class AbstractConfigurator(object): '''Abstract Base class for ccnet/seafile/seahub/db configurator''' def __init__(self): pass def ask_questions(self): raise NotImplementedError def generate(self): raise NotImplementedError class AbstractDBConfigurator(AbstractConfigurator): '''Abstract class for database related configuration''' def __init__(self): AbstractConfigurator.__init__(self) self.mysql_host = 'localhost' self.mysql_port = 3306 self.unix_socket = None self.use_existing_db = False self.seafile_mysql_user = '' self.seafile_mysql_password = '' self.seafile_mysql_userhost = '127.0.0.1' self.root_password = '' self.root_conn = '' self.ccnet_db_name = '' self.seafile_db_name = '' self.seahub_db_name = '' self.seahub_admin_email = '' self.seahub_admin_password = '' @staticmethod def ask_use_existing_db(): def validate(choice): if choice not in ['1', '2']: raise InvalidAnswer('Please choose 1 or 2') return choice == '2' question = '''\ ------------------------------------------------------- Please choose a way to initialize seafile databases: ------------------------------------------------------- ''' note = '''\ [1] Create new ccnet/seafile/seahub databases [2] Use existing ccnet/seafile/seahub databases ''' return Utils.ask_question(question, key='1 or 2', note=note, validate=validate) def validate_mysql_host(self, host): if not re.match(r'^[a-zA-Z0-9_\-\.]+$', host): raise InvalidAnswer('%s is not a valid host' % Utils.highlight(host)) if host == 'localhost': host = '127.0.0.1' return host def ask_mysql_host(self): question = 'What is the host of mysql server?' key = 'mysql server host' default = 'localhost' self.mysql_host = Utils.ask_question(question, key=key, default=default, validate=self.validate_mysql_host) def validate_mysql_user_host(self, host): MYSQL_HOST_RE = re.compile(r'^(%|[^.].+\..+[^.])$') if not MYSQL_HOST_RE.match(host): raise InvalidAnswer('invalid mysql user host: {}'.format(host)) return host def ask_mysql_user_host(self): self.seafile_mysql_userhost = Utils.ask_question( 'From which hosts could the mysql account be used?', key='mysql user host', default='%', validate=self.validate_mysql_user_host ) def ask_mysql_port(self): question = 'What is the port of mysql server?' key = 'mysql server port' default = '3306' port = Utils.ask_question(question, key=key, default=default, validate=Utils.validate_port) # self.check_mysql_server(host, port) self.mysql_port = port def ask_mysql_host_port(self): self.ask_mysql_host() if self.mysql_host != '127.0.0.1': self.ask_mysql_user_host() self.ask_mysql_port() def check_mysql_server(self, host, port): print('\nverifying mysql server running ... ', end=' ') try: dummy = pymysql.connect(host=host, port=port) except Exception: print() raise InvalidAnswer('Failed to connect to mysql server at "%s:%s"' \ % (host, port)) print('done') def check_mysql_user(self, user, password, host=None, unix_socket=None): print('\nverifying password of user %s ... ' % user, end=' ') kwargs = dict(port=self.mysql_port, user=user, passwd=password) if unix_socket: kwargs['unix_socket'] = unix_socket else: kwargs['host'] = host or self.mysql_host try: conn = pymysql.connect(**kwargs) except Exception as e: if isinstance(e, pymysql.err.OperationalError): raise InvalidAnswer('Failed to connect to mysql server using user "%s" and password "***": %s' \ % (user, e.args[1])) else: raise InvalidAnswer('Failed to connect to mysql server using user "%s" and password "***": %s' \ % (user, e)) print('done') return conn def create_seahub_admin(self): try: conn = pymysql.connect(host=self.mysql_host, port=self.mysql_port, user=self.seafile_mysql_user, passwd=self.seafile_mysql_password, db=self.ccnet_db_name) except Exception as e: if isinstance(e, pymysql.err.OperationalError): Utils.error('Failed to connect to mysql database %s: %s' % (self.ccnet_db_name, e.args[1])) else: Utils.error('Failed to connect to mysql database %s: %s' % (self.ccnet_db_name, e)) cursor = conn.cursor() sql = '''\ CREATE TABLE IF NOT EXISTS EmailUser (id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, email VARCHAR(255), passwd CHAR(64), is_staff BOOL NOT NULL, is_active BOOL NOT NULL, ctime BIGINT, UNIQUE INDEX (email)) ENGINE=INNODB''' try: cursor.execute(sql) except Exception as e: if isinstance(e, pymysql.err.OperationalError): Utils.error('Failed to create ccnet user table: %s' % e.args[1]) else: Utils.error('Failed to create ccnet user table: %s' % e) sql = '''REPLACE INTO EmailUser(email, passwd, is_staff, is_active, ctime) VALUES ('%s', '%s', 1, 1, 0)''' \ % (seahub_config.admin_email, seahub_config.hashed_admin_password()) try: cursor.execute(sql) except Exception as e: if isinstance(e, pymysql.err.OperationalError): Utils.error('Failed to create admin user: %s' % e.args[1]) else: Utils.error('Failed to create admin user: %s' % e) conn.commit() def ask_questions(self): '''Ask questions and do database operations''' raise NotImplementedError class NewDBConfigurator(AbstractDBConfigurator): '''Handles the case of creating new mysql databases for ccnet/seafile/seahub''' def __init__(self): AbstractDBConfigurator.__init__(self) def ask_questions(self): self.ask_mysql_host_port() self.ask_root_password() self.ask_seafile_mysql_user_password() self.ask_db_names() def generate(self): #if not self.mysql_user_exists(self.seafile_mysql_user): # self.create_user() #self.create_databases() pass def validate_root_passwd(self, password): try: self.root_conn = self.check_mysql_user('root', password) except InvalidAnswer: # For MariaDB on Ubuntu 16.04, the msyql root user can only be # accessed from localhost with unix socket. So we retry with # localhost when failing with 127.0.0.1. if self.mysql_host == '127.0.0.1': self.root_conn = self.check_mysql_user('root', password, unix_socket=self.unix_socket) else: raise return password def ask_root_password(self): question = 'What is the password of the mysql root user?' key = 'root password' self.root_password = Utils.ask_question(question, key=key, validate=self.validate_root_passwd, password=True) def mysql_user_exists(self, user): cursor = self.root_conn.cursor() sql = '''SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '%s' and host = '%s')''' % \ (user, self.seafile_mysql_userhost) try: cursor.execute(sql) return cursor.fetchall()[0][0] except Exception as e: if isinstance(e, pymysql.err.OperationalError): Utils.error('Failed to check mysql user %s@%s: %s' % \ (user, self.seafile_mysql_userhost, e.args[1])) else: Utils.error('Failed to check mysql user %s@%s: %s' % \ (user, self.seafile_mysql_userhost, e)) finally: cursor.close() def ask_seafile_mysql_user_password(self): def validate(user): if user == 'root': raise InvalidAnswer( 'Using mysql "root" user is not allowed for security reasons. Please specify a different database user.' ) else: question = 'Enter the password for mysql user "%s":' % Utils.highlight(user) key = 'password for %s' % user password = Utils.ask_question(question, key=key, password=True) # If the user already exists, check the password here #if self.mysql_user_exists(user): # self.check_mysql_user(user, password) self.seafile_mysql_password = password return user question = 'Enter the name for mysql user of seafile. It would be created if not exists.' key = 'mysql user for seafile' default = 'seafile' self.seafile_mysql_user = Utils.ask_question(question, key=key, default=default, validate=validate) def ask_db_name(self, program, default): question = 'Enter the database name for %s:' % program key = '%s database' % program return Utils.ask_question(question, key=key, default=default, validate=self.validate_db_name) def ask_db_names(self): self.ccnet_db_name = self.ask_db_name('ccnet-server', 'ccnet-db') self.seafile_db_name = self.ask_db_name('seafile-server', 'seafile-db') self.seahub_db_name = self.ask_db_name('seahub', 'seahub-db') def validate_db_name(self, db_name): return db_name def create_user(self): cursor = self.root_conn.cursor() sql = '''CREATE USER '{}'@'{}' IDENTIFIED BY '{}' '''.format( self.seafile_mysql_user, self.seafile_mysql_userhost, self.seafile_mysql_password ) try: cursor.execute(sql) except Exception as e: if isinstance(e, pymysql.err.OperationalError): Utils.error('Failed to create mysql user {}@{}: {}'.format(self.seafile_mysql_user, self.seafile_mysql_userhost, e.args[1])) else: Utils.error('Failed to create mysql user {}@{}: {}'.format(self.seafile_mysql_user, self.seafile_mysql_userhost, e)) finally: cursor.close() def create_db(self, db_name): cursor = self.root_conn.cursor() sql = '''CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET UTF8''' \ % db_name try: cursor.execute(sql) except Exception as e: if isinstance(e, pymysql.err.OperationalError): Utils.error('Failed to create database %s: %s' % (db_name, e.args[1])) else: Utils.error('Failed to create database %s: %s' % (db_name, e)) finally: cursor.close() def grant_db_permission(self, db_name): cursor = self.root_conn.cursor() sql = '''GRANT ALL PRIVILEGES ON `{}`.* to `{}`@`{}` '''.format( db_name, self.seafile_mysql_user, self.seafile_mysql_userhost ) try: cursor.execute(sql) except Exception as e: if isinstance(e, pymysql.err.OperationalError): Utils.error('Failed to grant permission of database %s: %s' % (db_name, e.args[1])) else: Utils.error('Failed to grant permission of database %s: %s' % (db_name, e)) finally: cursor.close() def create_databases(self): self.create_db(self.ccnet_db_name) self.create_db(self.seafile_db_name) self.create_db(self.seahub_db_name) if self.seafile_mysql_user != 'root': self.grant_db_permission(self.ccnet_db_name) self.grant_db_permission(self.seafile_db_name) self.grant_db_permission(self.seahub_db_name) class ExistingDBConfigurator(AbstractDBConfigurator): '''Handles the case of use existing mysql databases for ccnet/seafile/seahub''' def __init__(self): AbstractDBConfigurator.__init__(self) self.use_existing_db = True def ask_questions(self): self.ask_mysql_host_port() self.ask_existing_mysql_user_password() self.ccnet_db_name = self.ask_db_name('ccnet') self.seafile_db_name = self.ask_db_name('seafile') self.seahub_db_name = self.ask_db_name('seahub') def generate(self): pass def ask_existing_mysql_user_password(self): def validate(user): if user == 'root': raise InvalidAnswer( 'Using root is not allowed for security reasons. Please specify a different database user.' ) question = 'What is the password for mysql user "%s"?' % Utils.highlight(user) key = 'password for %s' % user password = Utils.ask_question(question, key=key, password=True) #self.check_mysql_user(user, password) self.seafile_mysql_password = password return user question = 'Which mysql user to use for seafile?' key = 'mysql user for seafile' self.seafile_mysql_user = Utils.ask_question(question, key=key, validate=validate) def validate_db_name(self, db_name): self.check_user_db_access(db_name) return db_name def ask_db_name(self, program): question = 'Enter the existing database name for %s:' % program key = '%s database' % program return Utils.ask_question(question, key=key, validate=self.validate_db_name) def check_user_db_access(self, db_name): user = self.seafile_mysql_user password = self.seafile_mysql_password print('\nverifying user "%s" access to database %s ... ' % (user, db_name), end=' ') try: conn = pymysql.connect(host=self.mysql_host, port=self.mysql_port, user=user, passwd=password, db=db_name) cursor = conn.cursor() cursor.execute('show tables') cursor.close() except Exception as e: if isinstance(e, pymysql.err.OperationalError): raise InvalidAnswer('Failed to access database %s using user "%s" and password "***": %s' \ % (db_name, user, e.args[1])) else: raise InvalidAnswer('Failed to access database %s using user "%s" and password "***": %s' \ % (db_name, user, e)) print('done') return conn class CcnetConfigurator(AbstractConfigurator): SERVER_NAME_REGEX = r'^[a-zA-Z0-9_\-]{3,15}$' SERVER_IP_OR_DOMAIN_REGEX = r'^[^.].+\..+[^.]$' def __init__(self): '''Initialize default values of ccnet configuration''' AbstractConfigurator.__init__(self) self.ccnet_dir = os.path.join(env_mgr.top_dir, 'ccnet') self.port = 10001 self.server_name = None self.ip_or_domain = None self.ccnet_conf = os.path.join(env_mgr.central_config_dir, 'ccnet.conf') def ask_questions(self): if not self.server_name: self.ask_server_name() if not self.ip_or_domain: self.ask_server_ip_or_domain() # self.ask_port() def generate(self): print('Generating ccnet configuration ...\n') ccnet_init = os.path.join(env_mgr.bin_dir, 'ccnet-init') argv = [ ccnet_init, '-F', env_mgr.central_config_dir, '--config-dir', self.ccnet_dir, '--host', self.ip_or_domain, ] if Utils.run_argv(argv, env=env_mgr.get_binary_env()) != 0: Utils.error('Failed to generate ccnet configuration') time.sleep(1) self.generate_db_conf() def generate_db_conf(self): config = Utils.read_config(self.ccnet_conf) # [Database] # ENGINE= # HOST= # USER= # PASSWD= # DB= db_section = 'Database' if not config.has_section(db_section): config.add_section(db_section) config.set(db_section, 'ENGINE', 'mysql') config.set(db_section, 'HOST', db_config.mysql_host) config.set(db_section, 'PORT', str(db_config.mysql_port)) config.set(db_section, 'USER', db_config.seafile_mysql_user) config.set(db_section, 'PASSWD', db_config.seafile_mysql_password) config.set(db_section, 'DB', db_config.ccnet_db_name) config.set(db_section, 'CONNECTION_CHARSET', 'utf8') Utils.write_config(config, self.ccnet_conf) def validate_server_name(self, name): if not re.match(self.SERVER_NAME_REGEX, name): raise InvalidAnswer('%s is not a valid name' % Utils.highlight(name)) return name def ask_server_name(self): question = 'What is the name of the server? It will be displayed on the client.' key = 'server name' note = '3 - 15 letters or digits' self.server_name = Utils.ask_question(question, key=key, note=note, validate=self.validate_server_name) def validate_server_ip(self, ip_or_domain): if not re.match(self.SERVER_IP_OR_DOMAIN_REGEX, ip_or_domain): raise InvalidAnswer('%s is not a valid ip or domain' % ip_or_domain) return ip_or_domain def ask_server_ip_or_domain(self): question = 'What is the ip or domain of the server?' key = 'This server\'s ip or domain' note = 'For example: www.mycompany.com, 192.168.1.101' self.ip_or_domain = Utils.ask_question(question, key=key, note=note, validate=self.validate_server_ip) def ask_port(self): def validate(port): return Utils.validate_port(port) question = 'Which port do you want to use for the ccnet server?' key = 'ccnet server port' default = 10001 self.port = Utils.ask_question(question, key=key, default=default, validate=validate) class SeafileConfigurator(AbstractConfigurator): def __init__(self): AbstractConfigurator.__init__(self) self.seafile_dir = os.path.join(env_mgr.top_dir, 'seafile-data') self.port = 12001 self.fileserver_port = None self.seafile_conf = os.path.join(env_mgr.central_config_dir, 'seafile.conf') def ask_questions(self): # if not self.seafile_dir: # self.ask_seafile_dir() # self.ask_port() if not self.fileserver_port: self.ask_fileserver_port() def generate(self): print('Generating seafile configuration ...\n') seafserv_init = os.path.join(env_mgr.bin_dir, 'seaf-server-init') argv = [ seafserv_init, '-F', env_mgr.central_config_dir, '--seafile-dir', self.seafile_dir, '--fileserver-port', str(self.fileserver_port), ] if Utils.run_argv(argv, env=env_mgr.get_binary_env()) != 0: Utils.error('Failed to generate ccnet configuration') time.sleep(1) self.generate_db_conf() ## use default seafile-data path: seafile_data_dir=${TOPDIR}/seafile-data # self.write_seafile_ini() print('done') def generate_db_conf(self): config = Utils.read_config(self.seafile_conf) # [database] # type= # host= # user= # password= # db_name= # unix_socket= db_section = 'database' if not config.has_section(db_section): config.add_section(db_section) config.set(db_section, 'type', 'mysql') config.set(db_section, 'host', db_config.mysql_host) config.set(db_section, 'port', str(db_config.mysql_port)) config.set(db_section, 'user', db_config.seafile_mysql_user) config.set(db_section, 'password', db_config.seafile_mysql_password) config.set(db_section, 'db_name', db_config.seafile_db_name) config.set(db_section, 'connection_charset', 'utf8') Utils.write_config(config, self.seafile_conf) def validate_seafile_dir(self, path): if os.path.exists(path): raise InvalidAnswer('%s already exists' % Utils.highlight(path)) return path def ask_seafile_dir(self): question = 'Where do you want to put your seafile data?' key = 'seafile-data' note = 'Please use a volume with enough free space' default = os.path.join(env_mgr.top_dir, 'seafile-data') self.seafile_dir = Utils.ask_question(question, key=key, note=note, default=default, validate=self.validate_seafile_dir) def ask_port(self): def validate(port): port = Utils.validate_port(port) if port == ccnet_config.port: raise InvalidAnswer('%s is used by ccnet server, choose another one' \ % Utils.highlight(port)) return port question = 'Which port do you want to use for the seafile server?' key = 'seafile server port' default = 12001 self.port = Utils.ask_question(question, key=key, default=default, validate=validate) def ask_fileserver_port(self): question = 'Which port do you want to use for the seafile fileserver?' key = 'seafile fileserver port' default = 8082 self.fileserver_port = Utils.ask_question(question, key=key, default=default, validate=Utils.validate_port) def write_seafile_ini(self): seafile_ini = os.path.join(ccnet_config.ccnet_dir, 'seafile.ini') with open(seafile_ini, 'w') as fp: fp.write(self.seafile_dir) class SeahubConfigurator(AbstractConfigurator): def __init__(self): AbstractConfigurator.__init__(self) self.admin_email = '' self.admin_password = '' self.seahub_settings_py = os.path.join(env_mgr.central_config_dir, 'seahub_settings.py') def hashed_admin_password(self): return hashlib.sha1(self.admin_password).hexdigest() # pylint: disable=E1101 def ask_questions(self): pass def generate(self): '''Generating seahub_settings.py''' print('Generating seahub configuration ...\n') time.sleep(1) with open(self.seahub_settings_py, 'w') as fp: self.write_utf8_comment(fp) fp.write('\n') self.write_secret_key(fp) fp.write('\n') self.write_database_config(fp) def write_utf8_comment(self, fp): fp.write('# -*- coding: utf-8 -*-') def write_secret_key(self, fp): script = os.path.join(env_mgr.install_path, 'seahub/tools/secret_key_generator.py') cmd = [ Utils.get_python_executable(), script, ] key = Utils.get_command_output(cmd).strip() fp.write('SECRET_KEY = "%s"' % key) def write_database_config(self, fp): template = '''\ \nDATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': '%(name)s', 'USER': '%(username)s', 'PASSWORD': '%(password)s', 'HOST': '%(host)s', 'PORT': '%(port)s' } } ''' text = template % dict(name=db_config.seahub_db_name, username=db_config.seafile_mysql_user, password=db_config.seafile_mysql_password, host=db_config.mysql_host, port=db_config.mysql_port) fp.write(text) def ask_admin_email(self): print() print('----------------------------------------') print('Now let\'s create the admin account') print('----------------------------------------') def validate(email): # whitespace is not allowed if re.match(r'[\s]', email): raise InvalidAnswer('%s is not a valid email address' % Utils.highlight(email)) # must be a valid email address if not re.match(r'^.+@.*\..+$', email): raise InvalidAnswer('%s is not a valid email address' % Utils.highlight(email)) return email key = 'admin email' question = 'What is the ' + Utils.highlight('email') + ' for the admin account?' self.admin_email = Utils.ask_question(question, key=key, validate=validate) def ask_admin_password(self): def validate(password): key = 'admin password again' question = 'Enter the ' + Utils.highlight('password again:') password_again = Utils.ask_question(question, key=key, password=True) if password_again != password: raise InvalidAnswer('password mismatch') return password key = 'admin password' question = 'What is the ' + Utils.highlight('password') + ' for the admin account?' self.admin_password = Utils.ask_question(question, key=key, password=True, validate=validate) def prepare_avatar_dir(self): # media_dir=${INSTALLPATH}/seahub/media # orig_avatar_dir=${INSTALLPATH}/seahub/media/avatars # dest_avatar_dir=${TOPDIR}/seahub-data/avatars # if [[ ! -d ${dest_avatar_dir} ]]; then # mkdir -p "${TOPDIR}/seahub-data" # mv "${orig_avatar_dir}" "${dest_avatar_dir}" # ln -s ../../../seahub-data/avatars ${media_dir} # fi try: media_dir = os.path.join(env_mgr.install_path, 'seahub', 'media') orig_avatar_dir = os.path.join(media_dir, 'avatars') seahub_data_dir = os.path.join(env_mgr.top_dir, 'seahub-data') dest_avatar_dir = os.path.join(seahub_data_dir, 'avatars') if os.path.exists(dest_avatar_dir): return if not os.path.exists(seahub_data_dir): os.mkdir(seahub_data_dir) shutil.move(orig_avatar_dir, dest_avatar_dir) os.symlink('../../../seahub-data/avatars', orig_avatar_dir) except Exception as e: Utils.error('Failed to prepare seahub avatars dir: %s' % e) class SeafDavConfigurator(AbstractConfigurator): def __init__(self): AbstractConfigurator.__init__(self) self.seafdav_conf = None def ask_questions(self): pass def generate(self): self.seafdav_conf = os.path.join(env_mgr.central_config_dir, 'seafdav.conf') text = ''' [WEBDAV] enabled = false port = 8080 fastcgi = false share_name = / ''' with open(self.seafdav_conf, 'w') as fp: fp.write(text) class ProfessionalConfigurator(AbstractConfigurator): '''Seafile Pro related configuration''' def __init__(self): AbstractConfigurator.__init__(self) self.pro_py = os.path.join(env_mgr.install_path, 'pro', 'pro.py') self.pro_data_dir = os.path.join(env_mgr.top_dir, 'pro-data') def ask_questions(self): pass def generate(self): argv = [ Utils.get_python_executable(), self.pro_py, 'setup', '--mysql', '--mysql_host=%s' % db_config.mysql_host, '--mysql_port=%s' % db_config.mysql_port, '--mysql_user=%s' % db_config.seafile_mysql_user, '--mysql_password=%s' % db_config.seafile_mysql_password, '--mysql_db=%s' % db_config.seahub_db_name, ] if Utils.run_argv(argv, env=env_mgr.get_seahub_env()) != 0: Utils.error('Failed to generate seafile pro configuration') class GunicornConfigurator(AbstractConfigurator): def __init__(self): AbstractConfigurator.__init__(self) self.gunicorn_conf = None def ask_questions(self): pass def generate(self): self.gunicorn_conf = os.path.join(env_mgr.central_config_dir, 'gunicorn.conf.py') template = ''' import os daemon = True workers = 5 # default localhost:8000 bind = "127.0.0.1:8000" # Pid pids_dir = '%(pids_dir)s' pidfile = os.path.join(pids_dir, 'seahub.pid') # for file upload, we need a longer timeout value (default is only 30s, too short) timeout = 1200 limit_request_line = 8190 ''' text = template % dict(pids_dir=env_mgr.central_pids_dir, logs_dir=env_mgr.central_logs_dir) with open(self.gunicorn_conf, 'w') as fp: fp.write(text) class UserManualHandler(object): def __init__(self): self.src_docs_dir = os.path.join(env_mgr.install_path, 'seafile', 'docs') self.library_template_dir = None def copy_user_manuals(self): self.library_template_dir = os.path.join(seafile_config.seafile_dir, 'library-template') Utils.must_mkdir(self.library_template_dir) pattern = os.path.join(self.src_docs_dir, '*.doc') for doc in glob.glob(pattern): Utils.must_copy(doc, self.library_template_dir) def report_config(): print() print('---------------------------------') print('This is your configuration') print('---------------------------------') print() template = '''\ server name: %(server_name)s server ip/domain: %(ip_or_domain)s seafile data dir: %(seafile_dir)s fileserver port: %(fileserver_port)s database: %(use_existing_db)s ccnet database: %(ccnet_db_name)s seafile database: %(seafile_db_name)s seahub database: %(seahub_db_name)s database user: %(db_user)s ''' config = { 'server_name' : ccnet_config.server_name, 'ip_or_domain' : ccnet_config.ip_or_domain, 'seafile_dir' : seafile_config.seafile_dir, 'fileserver_port' : seafile_config.fileserver_port, 'admin_email' : seahub_config.admin_email, 'use_existing_db': 'use existing' if db_config.use_existing_db else 'create new', 'ccnet_db_name': db_config.ccnet_db_name, 'seafile_db_name': db_config.seafile_db_name, 'seahub_db_name': db_config.seahub_db_name, 'db_user': db_config.seafile_mysql_user } print(template % config) if need_pause: print() print('---------------------------------') print('Press ENTER to continue, or Ctrl-C to abort') print('---------------------------------') input() def create_seafile_server_symlink(): print('\ncreating seafile-server-latest symbolic link ... ', end=' ') seafile_server_symlink = os.path.join(env_mgr.top_dir, 'seafile-server-latest') try: os.symlink(os.path.basename(env_mgr.install_path), seafile_server_symlink) except Exception as e: print('\n') Utils.error('Failed to create symbolic link %s: %s' % (seafile_server_symlink, e)) else: print('done\n\n') def set_file_perm(): filemode = 0o600 dirmode = 0o700 files = [ seahub_config.seahub_settings_py, ] dirs = [ env_mgr.central_config_dir, ccnet_config.ccnet_dir, seafile_config.seafile_dir, seahub_config.seahub_settings_py, ] for fpath in files: os.chmod(fpath, filemode) for dpath in dirs: os.chmod(dpath, dirmode) env_mgr = EnvManager() ccnet_config = CcnetConfigurator() seafile_config = SeafileConfigurator() seafdav_config = SeafDavConfigurator() gunicorn_config = GunicornConfigurator() seahub_config = SeahubConfigurator() user_manuals_handler = UserManualHandler() pro_config = ProfessionalConfigurator() # Would be created after AbstractDBConfigurator.ask_use_existing_db() db_config = None need_pause = True def get_param_val(arg, env, default=None): return arg or os.environ.get(env, default) def check_params(args): server_name = 'seafile' ccnet_config.server_name = ccnet_config.validate_server_name(server_name) server_ip = get_param_val(args.server_ip, 'SERVER_IP', '127.0.0.1') ccnet_config.ip_or_domain = ccnet_config.validate_server_ip(server_ip) fileserver_port = get_param_val(args.fileserver_port, 'FILESERVER_PORT', '8082') seafile_config.fileserver_port = Utils.validate_port(fileserver_port) seafile_dir = get_param_val(args.seafile_dir, 'SEAFILE_DIR', os.path.join(env_mgr.top_dir, 'seafile-data')) seafile_config.seafile_dir = seafile_config.validate_seafile_dir(seafile_dir) global db_config use_existing_db = get_param_val(args.use_existing_db, 'USE_EXISTING_DB', '0') # pylint: disable=redefined-variable-type if use_existing_db == '0': db_config = NewDBConfigurator() elif use_existing_db == '1': db_config = ExistingDBConfigurator() else: raise InvalidParams('Invalid use existing db parameter, the value can only be 0 or 1') mysql_host = get_param_val(args.mysql_host, 'MYSQL_HOST', '127.0.0.1') if not mysql_host: raise InvalidParams('Incomplete mysql configuration parameters, ' \ 'missing mysql host parameter') db_config.mysql_host = db_config.validate_mysql_host(mysql_host) mysql_port = get_param_val(args.mysql_port, 'MYSQL_PORT', '3306') db_config.mysql_port = Utils.validate_port(mysql_port) mysql_user = get_param_val(args.mysql_user, 'MYSQL_USER') if not mysql_user: raise InvalidParams('Incomplete mysql configuration parameters, ' \ 'missing mysql user name parameter') mysql_user_passwd = get_param_val(args.mysql_user_passwd, 'MYSQL_USER_PASSWD') if not mysql_user_passwd: raise InvalidParams('Incomplete mysql configuration parameters, ' \ 'missing mysql user password parameter') ccnet_db = get_param_val(args.ccnet_db, 'CCNET_DB', 'ccnet_db') if not ccnet_db: raise InvalidParams('Incomplete mysql configuration parameters, ' \ 'missing ccnet db name parameter') seafile_db = get_param_val(args.seafile_db, 'SEAFILE_DB', 'seafile_db') if not seafile_db: raise InvalidParams('Incomplete mysql configuration parameters, ' \ 'missing seafile db name parameter') seahub_db = get_param_val(args.seahub_db, 'SEAHUB_DB', 'seahub_db') if not seahub_db: raise InvalidParams('Incomplete mysql configuration parameters, ' \ 'missing seahub db name parameter') mysql_user_host = get_param_val(args.mysql_user_host, 'MYSQL_USER_HOST') mysql_root_passwd = get_param_val(args.mysql_root_passwd, 'MYSQL_ROOT_PASSWD') if db_config.use_existing_db: db_config.check_mysql_user(mysql_user, mysql_user_passwd) db_config.seafile_mysql_user = mysql_user db_config.seafile_mysql_password = mysql_user_passwd db_config.ccnet_db_name = db_config.validate_db_name(ccnet_db) db_config.seafile_db_name = db_config.validate_db_name(seafile_db) db_config.seahub_db_name = db_config.validate_db_name(seahub_db) else: if db_config.mysql_host != '127.0.0.1' and not mysql_user_host: raise InvalidParams('mysql user host parameter is missing in creating new db mode') if not mysql_user_host: db_config.seafile_mysql_userhost = '127.0.0.1' else: db_config.seafile_mysql_userhost = db_config.validate_mysql_user_host(mysql_user_host) if not mysql_root_passwd and "MYSQL_ROOT_PASSWD" not in os.environ: raise InvalidParams('mysql root password parameter is missing in creating new db mode') db_config.root_password = mysql_root_passwd if mysql_user == 'root': db_config.seafile_mysql_user = 'root' db_config.seafile_mysql_password = db_config.root_password else: #if db_config.mysql_user_exists(mysql_user): # db_config.check_mysql_user(mysql_user, mysql_user_passwd) db_config.seafile_mysql_user = mysql_user db_config.seafile_mysql_password = mysql_user_passwd db_config.ccnet_db_name = ccnet_db db_config.seafile_db_name = seafile_db db_config.seahub_db_name = seahub_db global need_pause need_pause = False def main(): if len(sys.argv) > 2 and sys.argv[1] == 'auto': sys.argv.remove('auto') parser = argparse.ArgumentParser() parser.add_argument('-n', '--server-name', help='server name') parser.add_argument('-i', '--server-ip', help='server ip or domain') parser.add_argument('-p', '--fileserver-port', help='fileserver port') parser.add_argument('-d', '--seafile-dir', help='seafile dir to store seafile data') parser.add_argument('-e', '--use-existing-db', help='use mysql existing dbs or create new dbs, ' '0: create new dbs 1: use existing dbs') parser.add_argument('-o', '--mysql-host', help='mysql host') parser.add_argument('-t', '--mysql-port', help='mysql port') parser.add_argument('-u', '--mysql-user', help='mysql user name') parser.add_argument('-w', '--mysql-user-passwd', help='mysql user password') parser.add_argument('-q', '--mysql-user-host', help='mysql user host') parser.add_argument('-r', '--mysql-root-passwd', help='mysql root password') parser.add_argument('-c', '--ccnet-db', help='ccnet db name') parser.add_argument('-s', '--seafile-db', help='seafile db name') parser.add_argument('-b', '--seahub-db', help='seahub db name') args = parser.parse_args() try: check_params(args) except (InvalidAnswer, InvalidParams) as e: print(Utils.highlight('\n%s\n' % e)) sys.exit(-1) global db_config if need_pause: Utils.welcome() warnings.filterwarnings('ignore', category=pymysql.Warning) env_mgr.check_pre_condiction() # Part 1: collect configuration ccnet_config.ask_questions() seafile_config.ask_questions() seahub_config.ask_questions() pro_config.ask_questions() # pylint: disable=redefined-variable-type if not db_config: if AbstractDBConfigurator.ask_use_existing_db(): db_config = ExistingDBConfigurator() else: db_config = NewDBConfigurator() db_config.ask_questions() report_config() # Part 2: generate configuration db_config.generate() ccnet_config.generate() seafile_config.generate() seafdav_config.generate() gunicorn_config.generate() seahub_config.generate() pro_config.generate() seahub_config.prepare_avatar_dir() # db_config.create_seahub_admin() user_manuals_handler.copy_user_manuals() #create_seafile_server_symlink() set_file_perm() report_success() def report_success(): message = '''\ ----------------------------------------------------------------- Your seafile server configuration has been finished successfully. ----------------------------------------------------------------- run seafile server: ./seafile.sh { start | stop | restart } run seahub server: ./seahub.sh { start | stop | restart } ----------------------------------------------------------------- If you are behind a firewall, remember to allow input/output of these tcp ports: ----------------------------------------------------------------- port of seafile fileserver: %(fileserver_port)s port of seahub: 8000 When problems occur, Refer to %(server_manual_http)s for information. ''' print(message % dict(fileserver_port=seafile_config.fileserver_port, server_manual_http=SERVER_MANUAL_HTTP)) if __name__ == '__main__': try: main() except KeyboardInterrupt: print() print(Utils.highlight('The setup process is aborted')) print()