diff --git a/.travis.yml b/.travis.yml index bfea7bb..ba48e45 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,3 +13,4 @@ script: - cd .. - cp samples/server-sqlite3.conf bootstrap/bootstrap.conf - ./launcher bootstrap + - ./launcher start diff --git a/launcher b/launcher index 7d10afc..dc380b2 100755 --- a/launcher +++ b/launcher @@ -5,10 +5,11 @@ set -o pipefail version=6.0.5 image=seafileorg/server:$version -topdir=$(cd $(dirname $0); pwd -P) -sharedir=$topdir/shared +dockerdir=$(cd $(dirname $0); pwd -P) +sharedir=$dockerdir/shared +installdir=/opt/seafile/seafile-server-$version -cd $topdir +cd $dockerdir dbg() { if [[ $debug == "true" ]]; then @@ -36,8 +37,9 @@ set_volumes() { mounts=( $sharedir:/shared - $topdir/bootstrap:/bootstrap:ro - $topdir/scripts:/scripts:ro + $dockerdir/bootstrap:/bootstrap:ro + $dockerdir/scripts:/scripts:ro + $dockerdir/scripts/tmp/check_init_admin.py:$installdir/check_init_admin.py:ro $bash_history:/root/.bash_history ) volumes="" @@ -54,7 +56,7 @@ bootstrap() { start() { set_volumes - docker run --rm -it $volumes $image # scripts/start.py + docker run --rm -it $volumes $image scripts/start.py } enter() { diff --git a/scripts/bootstrap.py b/scripts/bootstrap.py index 02aa9c7..b3e7a1d 100755 --- a/scripts/bootstrap.py +++ b/scripts/bootstrap.py @@ -7,26 +7,16 @@ setup-seafile.sh or setup-seafile-mysql.sh. It's supposed to run inside the container. """ -from ConfigParser import ConfigParser import os from os.path import abspath, basename, exists, dirname, join, isdir import shutil import sys -from utils import call, get_install_dir, get_script +from utils import call, get_conf, get_install_dir, get_script installdir = get_install_dir() topdir = dirname(installdir) -_config = None - -def get_conf(key): - global _config - if _config is None: - _config = ConfigParser() - _config.read("/bootstrap/bootstrap.conf") - return _config.get("server", key) - def main(): env = { 'SERVER_NAME': 'seafile', diff --git a/scripts/start.py b/scripts/start.py old mode 100644 new mode 100755 index e69de29..23bd732 --- a/scripts/start.py +++ b/scripts/start.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +#coding: UTF-8 + +""" +This script calls the appropriate seafile init scripts (e.g. +setup-seafile.sh or setup-seafile-mysql.sh. It's supposed to run inside the +container. +""" + +import json +import os +from os.path import abspath, basename, exists, dirname, join, isdir +import shutil +import sys + +from utils import call, get_conf, get_install_dir, get_script + +installdir = get_install_dir() +topdir = dirname(installdir) + +def main(): + admin_pw = { + 'email': get_conf('admin.email'), + 'password': get_conf('admin.password'), + } + password_file = join(topdir, 'conf', 'admin.txt') + with open(password_file, 'w') as fp: + json.dump(admin_pw, fp) + + try: + call('{} start'.format(get_script('seafile.sh')), check_call=True) + call('{} start'.format(get_script('seahub.sh')), check_call=True) + finally: + if exists(password_file): + os.unlink(password_file) + +if __name__ == '__main__': + main() diff --git a/scripts/tmp/check_init_admin.py b/scripts/tmp/check_init_admin.py new file mode 100644 index 0000000..2f5eed6 --- /dev/null +++ b/scripts/tmp/check_init_admin.py @@ -0,0 +1,382 @@ +#coding: UTF-8 + +'''This script would check if there is admin, and prompt the user to create a new one if non exist''' + +import json +import sys +import os +import time +import re +import shutil +import glob +import subprocess +import hashlib +import getpass +import uuid +import warnings + +from ConfigParser import ConfigParser + +try: + import readline # pylint: disable=W0611 +except ImportError: + pass + + +SERVER_MANUAL_HTTP = 'https://github.com/haiwen/seafile/wiki' + +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 + raw_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 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''' + try: + os.mkdir(path) + except OSError, 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, 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 = raw_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, 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 + +### END of Utils +#################### + +class RPC(object): + def __init__(self): + import ccnet + ccnet_dir = os.environ['CCNET_CONF_DIR'] + central_config_dir = os.environ['SEAFILE_CENTRAL_CONF_DIR'] + self.rpc_client = ccnet.CcnetThreadedRpcClient( + ccnet.ClientPool(ccnet_dir, central_config_dir=central_config_dir)) + + def get_db_email_users(self): + return self.rpc_client.get_emailusers('DB', 0, 1) + + def create_admin(self, email, user): + return self.rpc_client.add_emailuser(email, user, 1, 1) + +def need_create_admin(): + users = rpc.get_db_email_users() + return len(users) == 0 + +def create_admin(email, passwd): + if rpc.create_admin(email, passwd) < 0: + raise Exception('failed to create admin') + else: + print '\n\n' + print '----------------------------------------' + print 'Successfully created seafile admin' + print '----------------------------------------' + print '\n\n' + +def ask_admin_email(): + print + print '----------------------------------------' + print 'It\'s the first time you start the seafile server. 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?' + return Utils.ask_question(question, + key=key, + validate=validate) + +def ask_admin_password(): + 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?' + return Utils.ask_question(question, + key=key, + password=True, + validate=validate) + +rpc = RPC() + +def main(): + if not need_create_admin(): + return + + password_file = os.path.join(os.environ['SEAFILE_CENTRAL_CONF_DIR'], 'admin.txt') + if os.path.exists(password_file): + with open(password_file, 'r') as fp: + pwinfo = json.load(fp) + email = pwinfo['email'] + passwd = pwinfo['password'] + os.unlink(password_file) + else: + email = ask_admin_email() + passwd = ask_admin_password() + + create_admin(email, passwd) + +if __name__ == '__main__': + try: + main() + except KeyboardInterrupt: + print '\n\n\n' + print Utils.highlight('Aborted.') + print + sys.exit(1) + except Exception, e: + print + print Utils.highlight('Error happened during creating seafile admin.') + print diff --git a/scripts/utils/__init__.py b/scripts/utils/__init__.py index cc65fe2..9391ee3 100644 --- a/scripts/utils/__init__.py +++ b/scripts/utils/__init__.py @@ -1,17 +1,18 @@ # coding: UTF-8 from __future__ import print_function +from ConfigParser import ConfigParser +from contextlib import contextmanager import os +from os.path import abspath, basename, exists, dirname, join, isdir, expanduser import platform import sys -import termcolor import subprocess import logging import logging.config import click +import termcolor import colorlog -from os.path import abspath, basename, exists, dirname, join, isdir, expanduser -from contextlib import contextmanager logger = logging.getLogger('.utils') @@ -47,6 +48,7 @@ def call(*a, **kw): dry_run = kw.pop('dry_run', False) quiet = kw.pop('quiet', False) cwd = kw.get('cwd', os.getcwd()) + check_call = kw.pop('check_call', False) reduct_args = kw.pop('reduct_args', []) if not quiet: toprint = a[0] @@ -58,7 +60,10 @@ def call(*a, **kw): eprint('cwd: ', green(cwd)) kw.setdefault('shell', True) if not dry_run: - return subprocess.Popen(*a, **kw).wait() + if check_call: + return subprocess.check_call(*a, **kw) + else: + return subprocess.Popen(*a, **kw).wait() @contextmanager def cd(path): @@ -204,3 +209,13 @@ def get_install_dir(): def get_script(script): return join(get_install_dir(), script) + + +_config = None + +def get_conf(key): + global _config + if _config is None: + _config = ConfigParser() + _config.read("/bootstrap/bootstrap.conf") + return _config.get("server", key)