From 57228ba03b024d08bafef4571270e65bfccb1ef8 Mon Sep 17 00:00:00 2001 From: root <517046497@qq.com> Date: Tue, 3 Apr 2018 02:58:14 +0000 Subject: [PATCH] pro --- MAINT.md | 51 ++- README.pro.md | 147 +++++++ ci/ci.sh | 40 +- ci/install_deps.sh | 3 + ci/publish-base.sh | 19 + ci/publish-image.sh | 1 + ci/publish-pro-base.sh | 24 ++ ci/publish-pro-image.sh | 34 ++ ci/upload.py | 64 +++ ci/validate_file.py | 48 +++ image/Makefile | 38 +- image/pro_base/Dockerfile | 12 + image/pro_seafile/Dockerfile | 18 + image/pro_seafile/create_data_links.sh | 60 +++ image/pro_seafile/scripts/bootstrap.py | 156 +++++++ image/pro_seafile/scripts/gc.sh | 30 ++ image/pro_seafile/scripts/ssl.sh | 46 +++ image/pro_seafile/scripts/start.py | 85 ++++ image/pro_seafile/scripts/upgrade.py | 82 ++++ image/pro_seafile/scripts/utils/__init__.py | 287 +++++++++++++ .../templates/letsencrypt.cron.template | 3 + .../templates/seafile.nginx.conf.template | 81 ++++ image/seafile/Dockerfile | 2 +- image/seafile/scripts/tmp/check_init_admin.py | 382 ------------------ image/seafile/templates/Dockerfile.template | 18 - 25 files changed, 1300 insertions(+), 431 deletions(-) create mode 100644 README.pro.md create mode 100755 ci/install_deps.sh create mode 100755 ci/publish-base.sh create mode 100755 ci/publish-pro-base.sh create mode 100755 ci/publish-pro-image.sh create mode 100755 ci/upload.py create mode 100755 ci/validate_file.py create mode 100644 image/pro_base/Dockerfile create mode 100644 image/pro_seafile/Dockerfile create mode 100755 image/pro_seafile/create_data_links.sh create mode 100755 image/pro_seafile/scripts/bootstrap.py create mode 100755 image/pro_seafile/scripts/gc.sh create mode 100755 image/pro_seafile/scripts/ssl.sh create mode 100755 image/pro_seafile/scripts/start.py create mode 100755 image/pro_seafile/scripts/upgrade.py create mode 100644 image/pro_seafile/scripts/utils/__init__.py create mode 100644 image/pro_seafile/templates/letsencrypt.cron.template create mode 100644 image/pro_seafile/templates/seafile.nginx.conf.template delete mode 100644 image/seafile/scripts/tmp/check_init_admin.py delete mode 100644 image/seafile/templates/Dockerfile.template diff --git a/MAINT.md b/MAINT.md index 362e52c..b1d46f2 100644 --- a/MAINT.md +++ b/MAINT.md @@ -2,26 +2,41 @@ Imagine the previous version is 6.0.5 and we have released 6.0.7. Here are the steps to do the upgrade. -* Switch to a branch "unstable" +* Switch to a branch "master" ```sh -git branch -f unstable origin/master -git checkout unstable +git branch -f master origin/master +git checkout master ``` * Update the version number in all the files/scripts from "6.0.5" to "6.0.7" and push it to github, then wait for travis ci (https://travis-ci.org/haiwen/seafile-docker/builds) to pass ```sh -git push origin unstable:unstable -``` -* Create a tag "v6.0.7" and push it to github. Wait for travis ci to finish: this time it would push the image seafileltd/seafile:6.0.7 to docker hub since it's triggered by a tag. -```sh -git tag v6.0.7 -git push origin v6.0.7 -``` -* Ensure the new image is available in https://hub.docker.com/r/seafileltd/seafile/tags/ -* Now update the master branch. -``` -git push origin unstable:master -``` -* Delete the unstable branch -```sh -git push origin :unstable +git push origin master ``` + +* Normal + + * Create a tag "seafile-base" and push it to github. Wait for travis ci to finish: this time it would push the image seafileltd/base:16.04 to docker hub since it's triggered by a tag. + ```sh + git tag seafile-base + git push origin seafile-base + ``` + + * Create a tag "v6.0.7" and push it to github. Wait for travis ci to finish: this time it would push the image seafileltd/seafile:6.0.7 to docker hub since it's triggered by a tag. + ```sh + git tag v6.0.7 + git push origin v6.0.7 + ``` + * Ensure the new image is available in https://hub.docker.com/r/seafileltd/seafile/tags/ + +* Pro + + * Create a tag "seafile-pro-base" and push it to github. Wait for travis ci to finish: this time it would push the image ${registry}/seafileltd/pro-base:16.04 to docker Registry since it's triggered by a tag. + ```sh + git tag seafile-pro-base + git push origin seafile-pro-base + ``` + + * Create a tag "v6.0.7-pro" and push it to github. Wait for travis ci to finish: this time it would push the image ${registry}/seafileltd/pro-seafile:6.0.7 to docker Registry since it's triggered by a tag. + ```sh + git tag v6.0.7-pro + git push origin v6.0.7 + ``` diff --git a/README.pro.md b/README.pro.md new file mode 100644 index 0000000..86b99c0 --- /dev/null +++ b/README.pro.md @@ -0,0 +1,147 @@ +[![Build Status](https://secure.travis-ci.org/haiwen/seafile-docker.png?branch=master)](http://travis-ci.org/haiwen/seafile-docker) + +### About + +- [Docker](https://docker.com/) is an open source project to pack, ship and run any Linux application in a lighter weight, faster container than a traditional virtual machine. + +- Docker makes it much easier to deploy [a Seafile server](https://github.com/haiwen/seafile) on your servers and keep it updated. + +- The base image configures Seafile with the Seafile team's recommended optimal defaults. + +If you are not familiar with docker commands, please refer to [docker documentation](https://docs.docker.com/engine/reference/commandline/cli/). + +### Getting Started + +To login the seafile private registry: + +```sh +docker login {pro-host} +``` + +You can see the private registry information on the [customer center](https://customer.seafile.com/downloads/) + +To run the seafile server container: + +```sh +docker run -d --name seafile \ + -e SEAFILE_SERVER_HOSTNAME=seafile.example.com \ + -v /opt/seafile-data:/shared \ + -p 80:80 \ + {pro-host}/seafileltd/pro-seafile:latest +``` + +Wait for a few minutes for the first time initialization, then visit `http://seafile.example.com` to open Seafile Web UI. + +This command will mount folder `/opt/seafile-data` at the local server to the docker instance. You can find logs and other data under this folder. + +### More configuration Options + +#### Custom Admin Username and Password + +The default admin account is `me@example.com` and the password is `asecret`. You can use a different password by setting the container's environment variables: +e.g. + +```sh +docker run -d --name seafile \ + -e SEAFILE_SERVER_HOSTNAME=seafile.example.com \ + -e SEAFILE_ADMIN_EMAIL=me@example.com \ + -e SEAFILE_ADMIN_PASSWORD=a_very_secret_password \ + -v /opt/seafile-data:/shared \ + -p 80:80 \ + {pro-host}/seafileltd/pro-seafile:latest +``` + +If you forget the admin password, you can add a new admin account and then go to the sysadmin panel to reset user password. + +#### Let's encrypt SSL certificate + +If you set `SEAFILE_SERVER_LETSENCRYPT` to `true`, the container would request a letsencrypt-signed SSL certificate for you automatically. + +e.g. + +``` +docker run -d --name seafile \ + -e SEAFILE_SERVER_LETSENCRYPT=true \ + -e SEAFILE_SERVER_HOSTNAME=seafile.example.com \ + -e SEAFILE_ADMIN_EMAIL=me@example.com \ + -e SEAFILE_ADMIN_PASSWORD=a_very_secret_password \ + -v /opt/seafile-data:/shared \ + -p 80:80 \ + -p 443:443 \ + {pro-host}/seafileltd/pro-seafile:latest +``` + +If you want to use your own SSL certificate: +- create a folder `/opt/seafile-data/ssl`, and put your certificate and private key under the ssl directory. +- Assume your site name is `seafile.example.com`, then your certificate must have the name `seafile.example.com.crt`, and the private key must have the name `seafile.example.com.key`. + +#### Modify Seafile Server Configurations + +The config files are under `shared/seafile/conf`. You can modify the configurations according to [Seafile manual](https://manual.seafile.com/) + +After modification, you need to restart the container: + +``` +docker restart seafile +``` + +#### Find logs + +The seafile logs are under `/shared/logs/seafile` in the docker, or `/opt/seafile-data/logs/seafile` in the server that run the docker. + +The system logs are under `/shared/logs/var-log`, or `/opt/seafile-data/logs/var-log` in the server that run the docker. + +#### Add a new Admin + +Ensure the container is running, then enter this command: + +``` +docker exec -it seafile /opt/seafile/seafile-server-latest/reset-admin.sh +``` + +Enter the username and password according to the prompts. You now have a new admin account. + +### Directory Structure + +#### `/shared` + +Placeholder spot for shared volumes. You may elect to store certain persistent information outside of a container, in our case we keep various logfiles and upload directory outside. This allows you to rebuild containers easily without losing important information. + +- /shared/db: This is the data directory for mysql server +- /shared/seafile: This is the directory for seafile server configuration and data. +- /shared/logs: This is the directory for logs. + - /shared/logs/var-log: This is the directory that would be mounted as `/var/log` inside the container. For example, you can find the nginx logs in `shared/logs/var-log/nginx/`. + - /shared/logs/seafile: This is the directory that would contain the log files of seafile server processes. For example, you can find seaf-server logs in `shared/logs/seafile/seafile.log`. +- /shared/ssl: This is directory for certificate, which does not exist by default. +- /shared/bootstrap.conf: This file does not exist by default. You can create it by your self, and write the configuration of files similar to the `samples` folder. + + +### Upgrading Seafile Server + +TO upgrade to latest version of seafile server: + +```sh +docker pull {pro-host}/seafileltd/pro-seafile:latest +docker rm -f seafile +docker run -d --name seafile \ + -e SEAFILE_SERVER_LETSENCRYPT=true \ + -e SEAFILE_SERVER_HOSTNAME=seafile.example.com \ + -e SEAFILE_ADMIN_EMAIL=me@example.com \ + -e SEAFILE_ADMIN_PASSWORD=a_very_secret_password \ + -v /opt/seafile-data:/shared \ + -p 80:80 \ + -p 443:443 \ + {pro-host}/seafileltd/pro-seafile:latest +``` + +If you are one of the early users who use the `launcher` script, you should refer to [upgrade from old format](https://github.com/haiwen/seafile-docker/blob/master/upgrade_from_old_format.md) document. + +### Troubleshooting + +You can run docker commands like "docker logs" or "docker exec" to find errors. + +```sh +docker logs -f seafile +# or +docker exec -it seafile bash +``` diff --git a/ci/ci.sh b/ci/ci.sh index df3bd0e..aac33fd 100755 --- a/ci/ci.sh +++ b/ci/ci.sh @@ -1,25 +1,49 @@ #!/bin/bash -version=6.2.5 +version=6.2.10 set -e -x - +./ci/install_deps.sh ( cd image # pip install docker-squash # make base squash-base server make base - make server + make pro-base + make pro-server ) mkdir -p /opt/seafile-data -docker run -d --name seafile -v /opt/seafile-data:/shared -p 80:80 -p 443:443 seafileltd/seafile:$version -docker stop seafile -docker start seafile -docker restart seafile +docker run -d --name seafile -e SEAFILE_SERVER_HOSTNAME=127.0.0.1 -v /opt/seafile-data:/shared -p 80:80 -p 443:443 seafileltd/pro-seafile:$version -if [[ $TRAVIS_TAG != "" ]]; then + +cat > doc.md < ${ssl_account_key} +fi + +if [[ ! -e ${ssl_key} ]]; then + openssl genrsa 4096 > ${ssl_key} +fi + +if [[ ! -e ${ssl_csr} ]]; then + openssl req -new -sha256 -key ${ssl_key} -subj "/CN=$domain" > $ssl_csr +fi + +python $letsencrypt_script --account-key ${ssl_account_key} --csr $ssl_csr --acme-dir /var/www/challenges/ > ./signed.crt +curl -sSL -o intermediate.pem https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem +cat signed.crt intermediate.pem > ${ssl_crt} + +nginx -s reload + +echo "Nginx reloaded." diff --git a/image/pro_seafile/scripts/start.py b/image/pro_seafile/scripts/start.py new file mode 100755 index 0000000..2ce5d5c --- /dev/null +++ b/image/pro_seafile/scripts/start.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +#coding: UTF-8 + +""" +Starts the seafile/seahub server and watches the controller process. It is +the entrypoint command of the docker container. +""" + +import json +import os +from os.path import abspath, basename, exists, dirname, join, isdir +import shutil +import sys +import time + +from utils import ( + call, get_conf, get_install_dir, get_script, get_command_output, + render_template, wait_for_mysql +) +from upgrade import check_upgrade +from bootstrap import init_seafile_server, is_https, init_letsencrypt, generate_local_nginx_conf + + +shared_seafiledir = '/shared/seafile' +ssl_dir = '/shared/ssl' +generated_dir = '/bootstrap/generated' +installdir = get_install_dir() +topdir = dirname(installdir) + +def watch_controller(): + maxretry = 4 + retry = 0 + while retry < maxretry: + controller_pid = get_command_output('ps aux | grep seafile-controller | grep -v grep || true').strip() + garbage_collector_pid = get_command_output('ps aux | grep /scripts/gc.sh | grep -v grep || true').strip() + if not controller_pid and not garbage_collector_pid: + retry += 1 + else: + retry = 0 + time.sleep(5) + print 'seafile controller exited unexpectedly.' + sys.exit(1) + +def main(): + if not exists(shared_seafiledir): + os.mkdir(shared_seafiledir) + if not exists(generated_dir): + os.makedirs(generated_dir) + + if is_https(): + init_letsencrypt() + generate_local_nginx_conf() + call('nginx -s reload') + + wait_for_mysql() + init_seafile_server() + + check_upgrade() + os.chdir(installdir) + + admin_pw = { + 'email': get_conf('SEAFILE_ADMIN_EMAIL', 'me@example.com'), + 'password': get_conf('SEAFILE_ADMIN_PASSWORD', 'asecret'), + } + 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'))) + call('{} start'.format(get_script('seahub.sh'))) + finally: + if exists(password_file): + os.unlink(password_file) + + print 'seafile server is running now.' + try: + watch_controller() + except KeyboardInterrupt: + print 'Stopping seafile server.' + sys.exit(0) + +if __name__ == '__main__': + main() diff --git a/image/pro_seafile/scripts/upgrade.py b/image/pro_seafile/scripts/upgrade.py new file mode 100755 index 0000000..65f401a --- /dev/null +++ b/image/pro_seafile/scripts/upgrade.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +#coding: UTF-8 + +""" +This script is used to run proper upgrade scripts automatically. +""" + +import json +import re +import glob +import os +from os.path import abspath, basename, exists, dirname, join, isdir +import shutil +import sys +import time + +from utils import ( + call, get_install_dir, get_script, get_command_output, replace_file_pattern, + read_version_stamp, wait_for_mysql, update_version_stamp, loginfo +) + +installdir = get_install_dir() +topdir = dirname(installdir) + +def collect_upgrade_scripts(from_version, to_version): + """ + Give the current installed version, calculate which upgrade scripts we need + to run to upgrade it to the latest verison. + + For example, given current version 5.0.1 and target version 6.1.0, and these + upgrade scripts: + + upgrade_4.4_5.0.sh + upgrade_5.0_5.1.sh + upgrade_5.1_6.0.sh + upgrade_6.0_6.1.sh + + We need to run upgrade_5.0_5.1.sh, upgrade_5.1_6.0.sh, and upgrade_6.0_6.1.sh. + """ + from_major_ver = '.'.join(from_version.split('.')[:2]) + to_major_ver = '.'.join(to_version.split('.')[:2]) + + scripts = [] + for fn in glob.glob(join(installdir, 'upgrade', 'upgrade_*_*.sh')): + va, vb = parse_upgrade_script_version(fn) + if va >= from_major_ver and vb <= to_major_ver: + scripts.append(fn) + return scripts + +def parse_upgrade_script_version(script): + script = basename(script) + m = re.match(r'upgrade_([0-9+.]+)_([0-9+.]+).sh', basename(script)) + return m.groups() + +def check_upgrade(): + last_version = read_version_stamp() + current_version = os.environ['SEAFILE_VERSION'] + if last_version == current_version: + return + + scripts_to_run = collect_upgrade_scripts(from_version=last_version, to_version=current_version) + for script in scripts_to_run: + loginfo('Running scripts {}'.format(script)) + # Here we use a trick: use a version stamp like 6.1.0 to prevent running + # all upgrade scripts before 6.1 again (because 6.1 < 6.1.0 in python) + new_version = parse_upgrade_script_version(script)[1] + '.0' + + replace_file_pattern(script, 'read dummy', '') + call(script) + + update_version_stamp(new_version) + + update_version_stamp(current_version) + +def main(): + wait_for_mysql() + + os.chdir(installdir) + check_upgrade() + +if __name__ == '__main__': + main() diff --git a/image/pro_seafile/scripts/utils/__init__.py b/image/pro_seafile/scripts/utils/__init__.py new file mode 100644 index 0000000..0996747 --- /dev/null +++ b/image/pro_seafile/scripts/utils/__init__.py @@ -0,0 +1,287 @@ +# coding: UTF-8 + +from __future__ import print_function +from ConfigParser import ConfigParser +from contextlib import contextmanager +import os +import datetime +from os.path import abspath, basename, exists, dirname, join, isdir, expanduser +import platform +import sys +import subprocess +import time +import logging +import logging.config +import click +import termcolor +import colorlog + +logger = logging.getLogger('.utils') + +DEBUG_ENABLED = os.environ.get('SEAFILE_DOCKER_VERBOSE', '').lower() in ('true', '1', 'yes') + +def eprint(*a, **kw): + kw['file'] = sys.stderr + print(*a, **kw) + +def identity(msg, *a, **kw): + return msg + +colored = identity if not os.isatty(sys.stdin.fileno()) else termcolor.colored +red = lambda s: colored(s, 'red') +green = lambda s: colored(s, 'green') + +def underlined(msg): + return '\x1b[4m{}\x1b[0m'.format(msg) + +def sudo(*a, **kw): + call('sudo ' + a[0], *a[1:], **kw) + +def _find_flag(args, *opts, **kw): + is_flag = kw.get('is_flag', False) + if is_flag: + return any([opt in args for opt in opts]) + else: + for opt in opts: + try: + return args[args.index(opt) + 1] + except ValueError: + pass + +def call(*a, **kw): + dry_run = kw.pop('dry_run', False) + quiet = kw.pop('quiet', DEBUG_ENABLED) + cwd = kw.get('cwd', os.getcwd()) + check_call = kw.pop('check_call', True) + reduct_args = kw.pop('reduct_args', []) + if not quiet: + toprint = a[0] + args = [x.strip('"') for x in a[0].split() if '=' not in x] + for arg in reduct_args: + value = _find_flag(args, arg) + toprint = toprint.replace(value, '{}**reducted**'.format(value[:3])) + logdbg('calling: ' + green(toprint)) + logdbg('cwd: ' + green(cwd)) + kw.setdefault('shell', True) + if not dry_run: + if check_call: + return subprocess.check_call(*a, **kw) + else: + return subprocess.Popen(*a, **kw).wait() + +@contextmanager +def cd(path): + path = expanduser(path) + olddir = os.getcwd() + os.chdir(path) + try: + yield + finally: + os.chdir(olddir) + +def must_makedir(p): + p = expanduser(p) + if not exists(p): + logger.info('created folder %s', p) + os.makedirs(p) + else: + logger.debug('folder %s already exists', p) + +def setup_colorlog(): + logging.config.dictConfig({ + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'standard': { + 'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s' + }, + 'colored': { + '()': 'colorlog.ColoredFormatter', + 'format': "%(log_color)s[%(asctime)s]%(reset)s %(blue)s%(message)s", + 'datefmt': '%m/%d/%Y %H:%M:%S', + }, + }, + 'handlers': { + 'default': { + 'level': 'INFO', + 'formatter': 'colored', + 'class': 'logging.StreamHandler', + }, + }, + 'loggers': { + '': { + 'handlers': ['default'], + 'level': 'INFO', + 'propagate': True + }, + 'django.request': { + 'handlers': ['default'], + 'level': 'WARN', + 'propagate': False + }, + } + }) + + logging.getLogger('requests.packages.urllib3.connectionpool').setLevel( + logging.WARNING) + + +def setup_logging(level=logging.INFO): + kw = { + 'format': '[%(asctime)s][%(module)s]: %(message)s', + 'datefmt': '%m/%d/%Y %H:%M:%S', + 'level': level, + 'stream': sys.stdout + } + + logging.basicConfig(**kw) + logging.getLogger('requests.packages.urllib3.connectionpool').setLevel( + logging.WARNING) + +def get_process_cmd(pid, env=False): + env = 'e' if env else '' + try: + return subprocess.check_output('ps {} -o command {}'.format(env, pid), + shell=True).strip().splitlines()[1] + # except Exception, e: + # print(e) + except: + return None + +def get_match_pids(pattern): + pgrep_output = subprocess.check_output( + 'pgrep -f "{}" || true'.format(pattern), + shell=True).strip() + return [int(pid) for pid in pgrep_output.splitlines()] + +def ask_for_confirm(msg): + confirm = click.prompt(msg, default='Y') + return confirm.lower() in ('y', 'yes') + +def confirm_command_to_run(cmd): + if ask_for_confirm('Run the command: {} ?'.format(green(cmd))): + call(cmd) + else: + sys.exit(1) + +def git_current_commit(): + return get_command_output('git rev-parse --short HEAD').strip() + +def get_command_output(cmd): + shell = not isinstance(cmd, list) + return subprocess.check_output(cmd, shell=shell) + +def ask_yes_or_no(msg, prompt='', default=None): + print('\n' + msg + '\n') + while True: + answer = raw_input(prompt + ' [yes/no] ').lower() + if not answer: + continue + + if answer not in ('yes', 'no', 'y', 'n'): + continue + + if answer in ('yes', 'y'): + return True + else: + return False + +def git_branch_exists(branch): + return call('git rev-parse --short --verify {}'.format(branch)) == 0 + +def to_unicode(s): + if isinstance(s, str): + return s.decode('utf-8') + else: + return s + +def to_utf8(s): + if isinstance(s, unicode): + return s.encode('utf-8') + else: + return s + +def git_commit_time(refspec): + return int(get_command_output('git log -1 --format="%ct" {}'.format( + refspec)).strip()) + +def get_seafile_version(): + return os.environ['SEAFILE_VERSION'] + +def get_install_dir(): + return join('/opt/seafile/seafile-pro-server-{}'.format(get_seafile_version())) + +def get_script(script): + return join(get_install_dir(), script) + + +_config = None + +def get_conf(key, default=None): + key = key.upper() + return os.environ.get(key, default) + +def _add_default_context(context): + default_context = { + 'current_timestr': datetime.datetime.now().strftime('%m/%d/%Y %H:%M:%S'), + } + for k in default_context: + context.setdefault(k, default_context[k]) + +def render_template(template, target, context): + from jinja2 import Environment, FileSystemLoader + env = Environment(loader=FileSystemLoader(dirname(template))) + _add_default_context(context) + content = env.get_template(basename(template)).render(**context) + with open(target, 'w') as fp: + fp.write(content) + +def logdbg(msg): + if DEBUG_ENABLED: + msg = '[debug] ' + msg + loginfo(msg) + +def loginfo(msg): + msg = '[{}] {}'.format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), green(msg)) + eprint(msg) + +def cert_has_valid_days(cert, days): + assert exists(cert) + + secs = 86400 * int(days) + retcode = call('openssl x509 -checkend {} -noout -in {}'.format(secs, cert), check_call=False) + return retcode == 0 + +def get_version_stamp_file(): + return '/shared/seafile/seafile-data/current_version' + +def read_version_stamp(fn=get_version_stamp_file()): + assert exists(fn), 'version stamp file {} does not exist!'.format(fn) + with open(fn, 'r') as fp: + return fp.read().strip() + +def update_version_stamp(version, fn=get_version_stamp_file()): + with open(fn, 'w') as fp: + fp.write(version + '\n') + +def wait_for_mysql(): + while not exists('/var/run/mysqld/mysqld.sock'): + logdbg('waiting for mysql server to be ready') + time.sleep(2) + logdbg('mysql server is ready') + +def wait_for_nginx(): + while True: + logdbg('waiting for nginx server to be ready') + output = get_command_output('netstat -nltp') + if ':80 ' in output: + logdbg(output) + logdbg('nginx is ready') + return + time.sleep(2) + +def replace_file_pattern(fn, pattern, replacement): + with open(fn, 'r') as fp: + content = fp.read() + with open(fn, 'w') as fp: + fp.write(content.replace(pattern, replacement)) diff --git a/image/pro_seafile/templates/letsencrypt.cron.template b/image/pro_seafile/templates/letsencrypt.cron.template new file mode 100644 index 0000000..cd877b6 --- /dev/null +++ b/image/pro_seafile/templates/letsencrypt.cron.template @@ -0,0 +1,3 @@ +PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +# min hour dayofmonth month dayofweek command +0 0 1 * * root /scripts/ssl.sh {{ ssl_dir }} {{ domain }} diff --git a/image/pro_seafile/templates/seafile.nginx.conf.template b/image/pro_seafile/templates/seafile.nginx.conf.template new file mode 100644 index 0000000..fe02cb0 --- /dev/null +++ b/image/pro_seafile/templates/seafile.nginx.conf.template @@ -0,0 +1,81 @@ +# -*- mode: nginx -*- +# Auto generated at {{ current_timestr }} +{% if https -%} +server { + listen 80; + server_name _ default_server; + rewrite ^ https://{{ domain }}$request_uri? permanent; +} +{% endif -%} + +server { +{% if https -%} + listen 443; + ssl on; + ssl_certificate /shared/ssl/{{ domain }}.crt; + ssl_certificate_key /shared/ssl/{{ domain }}.key; + + ssl_ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS; + + # TODO: More SSL security hardening: ssl_session_tickets & ssl_dhparam + # ssl_session_tickets on; + # ssl_session_ticket_key /etc/nginx/sessionticket.key; + # ssl_session_cache shared:SSL:10m; + # ssl_session_timeout 10m; +{% else -%} + listen 80; +{% endif -%} + + server_name {{ domain }}; + + client_max_body_size 10m; + + location / { + proxy_pass http://127.0.0.1:8000/; + proxy_read_timeout 310s; + proxy_set_header Host $host; + proxy_set_header Forwarded "for=$remote_addr;proto=$scheme"; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Connection ""; + proxy_http_version 1.1; + } + + location /seafhttp { + rewrite ^/seafhttp(.*)$ $1 break; + proxy_pass http://127.0.0.1:8082; + client_max_body_size 0; + proxy_connect_timeout 36000s; + proxy_read_timeout 36000s; + } + + location /seafdav { + client_max_body_size 0; + fastcgi_pass 127.0.0.1:8080; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_script_name; + + fastcgi_param SERVER_PROTOCOL $server_protocol; + fastcgi_param QUERY_STRING $query_string; + fastcgi_param REQUEST_METHOD $request_method; + fastcgi_param CONTENT_TYPE $content_type; + fastcgi_param CONTENT_LENGTH $content_length; + fastcgi_param SERVER_ADDR $server_addr; + fastcgi_param SERVER_PORT $server_port; + fastcgi_param SERVER_NAME $server_name; + + access_log /var/log/nginx/seafdav.access.log; + error_log /var/log/nginx/seafdav.error.log; + } + + location /media { + root /opt/seafile/seafile-server-latest/seahub; + } + + # For letsencrypt + location /.well-known/acme-challenge/ { + alias /var/www/challenges/; + try_files $uri =404; + } +} diff --git a/image/seafile/Dockerfile b/image/seafile/Dockerfile index 56c53ac..838df99 100644 --- a/image/seafile/Dockerfile +++ b/image/seafile/Dockerfile @@ -3,7 +3,7 @@ WORKDIR /opt/seafile RUN mkdir -p /etc/my_init.d -ENV SEAFILE_VERSION=6.2.5 +ENV SEAFILE_VERSION=6.2.5 SEAFILE_SERVER=seafile-pro-server RUN mkdir -p /opt/seafile/ && \ curl -sSL -o - https://download.seadrive.org/seafile-server_${SEAFILE_VERSION}_x86-64.tar.gz \ diff --git a/image/seafile/scripts/tmp/check_init_admin.py b/image/seafile/scripts/tmp/check_init_admin.py deleted file mode 100644 index 2f5eed6..0000000 --- a/image/seafile/scripts/tmp/check_init_admin.py +++ /dev/null @@ -1,382 +0,0 @@ -#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/image/seafile/templates/Dockerfile.template b/image/seafile/templates/Dockerfile.template deleted file mode 100644 index 741b54e..0000000 --- a/image/seafile/templates/Dockerfile.template +++ /dev/null @@ -1,18 +0,0 @@ -# -*- mode: dockerfile -*- -# This is a jinja2 template to generate the real Dockerfile to build the local image -FROM seafileltd/seafile:{{ seafile_version }} - -CMD ["/sbin/my_init", "--", "/scripts/start.py"] - -# shared/ are ignored in .dockerignore -ADD . /app - -RUN cp -rp /app/scripts /scripts && \ - cp -rp /app/bootstrap /bootstrap && \ -{%- if seafile_version <= '6.0.6' %} - cp /app/scripts/tmp/check_init_admin.py /opt/seafile/seafile-server-{{ seafile_version }}/check_init_admin.py && \ -{%- endif %} -{%- if https %} - cp /app/bootstrap/generated/letsencrypt.cron /etc/cron.d/letsencrypt.cron && \ -{%- endif %} - cp /app/bootstrap/generated/seafile.nginx.conf /etc/nginx/sites-enabled/seafile.nginx.conf