summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authordrebs <drebs@leap.se>2016-11-10 23:50:35 -0200
committerdrebs <drebs@leap.se>2016-11-10 23:50:35 -0200
commit564f55802455d08c9a38e892bb4b25ad6fbcb87d (patch)
treeb214482c46ecd09d531a3bc7bf254bf47d367fb5 /scripts
parentc1950b41e0995b0213227bd0ce2c633f312037dc (diff)
parent0fd7e9f018b02161a844c11332ffced56b256010 (diff)
Merge tag '0.9.0'
Tag version 0.9.0
Diffstat (limited to 'scripts')
-rw-r--r--scripts/ddocs/update_design_docs.py170
-rw-r--r--scripts/docker/Dockerfile53
-rw-r--r--scripts/docker/Makefile35
-rw-r--r--scripts/docker/README.md15
-rw-r--r--scripts/docker/TODO4
-rw-r--r--scripts/docker/couchdb/Dockerfile3
-rw-r--r--scripts/docker/couchdb/Makefile4
-rw-r--r--scripts/docker/couchdb/README.rst12
-rw-r--r--scripts/docker/couchdb/local.ini2
-rwxr-xr-xscripts/docker/files/bin/run-perf.sh22
-rwxr-xr-xscripts/docker/files/bin/run-tox.sh17
-rwxr-xr-xscripts/docker/files/bin/setup-test-env.py16
-rw-r--r--scripts/migration/0.9.0/.gitignore1
-rw-r--r--scripts/migration/0.9.0/README.md73
-rw-r--r--scripts/migration/0.9.0/log/.empty0
-rwxr-xr-xscripts/migration/0.9.0/migrate.py117
-rw-r--r--scripts/migration/0.9.0/migrate_couch_schema/__init__.py192
-rw-r--r--scripts/migration/0.9.0/requirements.pip3
-rw-r--r--scripts/migration/0.9.0/setup.py8
-rw-r--r--scripts/migration/0.9.0/tests/conftest.py54
-rw-r--r--scripts/migration/0.9.0/tests/test_migrate.py67
-rw-r--r--scripts/migration/0.9.0/tox.ini13
-rw-r--r--scripts/packaging/compile_design_docs.py112
-rw-r--r--scripts/profiling/mail/couchdb_server.py5
24 files changed, 662 insertions, 336 deletions
diff --git a/scripts/ddocs/update_design_docs.py b/scripts/ddocs/update_design_docs.py
deleted file mode 100644
index 281482b8..00000000
--- a/scripts/ddocs/update_design_docs.py
+++ /dev/null
@@ -1,170 +0,0 @@
-#!/usr/bin/python
-
-# This script updates Soledad's design documents in the session database and
-# all user databases with contents from the installed leap.soledad.common
-# package.
-
-import json
-import logging
-import argparse
-import re
-import threading
-import binascii
-
-from urlparse import urlparse
-from getpass import getpass
-from ConfigParser import ConfigParser
-
-from couchdb.client import Server
-from couchdb.http import Resource
-from couchdb.http import Session
-from couchdb.http import ResourceNotFound
-
-from leap.soledad.common import ddocs
-
-
-MAX_THREADS = 20
-DESIGN_DOCS = {
- '_design/docs': json.loads(binascii.a2b_base64(ddocs.docs)),
- '_design/syncs': json.loads(binascii.a2b_base64(ddocs.syncs)),
- '_design/transactions': json.loads(
- binascii.a2b_base64(ddocs.transactions)),
-}
-
-
-# create a logger
-logger = logging.getLogger(__name__)
-LOG_FORMAT = '%(asctime)s %(message)s'
-logging.basicConfig(format=LOG_FORMAT, level=logging.INFO)
-
-
-def _parse_args():
- parser = argparse.ArgumentParser()
- parser.add_argument('-u', dest='uuid', default=None, type=str,
- help='the UUID of the user')
- parser.add_argument('-t', dest='threads', default=MAX_THREADS, type=int,
- help='the number of parallel threads')
- return parser.parse_args()
-
-
-def _get_url():
- # get couch url
- cp = ConfigParser()
- cp.read('/etc/soledad/soledad-server.conf')
- url = urlparse(cp.get('soledad-server', 'couch_url'))
- # get admin password
- netloc = re.sub('^.*@', '', url.netloc)
- url = url._replace(netloc=netloc)
- password = getpass("Admin password for %s: " % url.geturl())
- return url._replace(netloc='admin:%s@%s' % (password, netloc))
-
-
-def _get_server(url):
- resource = Resource(
- url.geturl(), Session(retry_delays=[1, 2, 4, 8], timeout=10))
- return Server(url=resource)
-
-
-def _confirm(url):
- hidden_url = re.sub(
- 'http://(.*):.*@',
- 'http://\\1:xxxxx@',
- url.geturl())
-
- print """
- ==========
- ATTENTION!
- ==========
-
- This script will modify Soledad's shared and user databases in:
-
- %s
-
- This script does not make a backup of the couch db data, so make sure you
- have a copy or you may loose data.
- """ % hidden_url
- confirm = raw_input("Proceed (type uppercase YES)? ")
-
- if confirm != "YES":
- exit(1)
-
-
-#
-# Thread
-#
-
-class DBWorkerThread(threading.Thread):
-
- def __init__(self, server, dbname, db_idx, db_len, release_fun):
- threading.Thread.__init__(self)
- self._dbname = dbname
- self._cdb = server[self._dbname]
- self._db_idx = db_idx
- self._db_len = db_len
- self._release_fun = release_fun
-
- def run(self):
-
- logger.info(
- "(%d/%d) Updating db %s."
- % (self._db_idx, self._db_len, self._dbname))
-
- for doc_id in DESIGN_DOCS:
- try:
- doc = self._cdb[doc_id]
- except ResourceNotFound:
- doc = {'_id': doc_id}
- for key in ['lists', 'views', 'updates']:
- if key in DESIGN_DOCS[doc_id]:
- doc[key] = DESIGN_DOCS[doc_id][key]
- self._cdb.save(doc)
-
- # release the semaphore
- self._release_fun()
-
-
-def _launch_update_design_docs_thread(
- server, dbname, db_idx, db_len, semaphore_pool):
- semaphore_pool.acquire() # wait for an available working slot
- thread = DBWorkerThread(
- server, dbname, db_idx, db_len, semaphore_pool.release)
- thread.daemon = True
- thread.start()
- return thread
-
-
-def _update_design_docs(args, server):
-
- # find the actual databases to be updated
- dbs = []
- if args.uuid:
- dbs.append('user-%s' % args.uuid)
- else:
- for dbname in server:
- if dbname.startswith('user-') or dbname == 'shared':
- dbs.append(dbname)
- else:
- logger.info("Skipping db %s." % dbname)
-
- db_idx = 0
- db_len = len(dbs)
- semaphore_pool = threading.BoundedSemaphore(value=args.threads)
- threads = []
-
- # launch the update
- for db in dbs:
- db_idx += 1
- threads.append(
- _launch_update_design_docs_thread(
- server, db, db_idx, db_len, semaphore_pool))
-
- # wait for all threads to finish
- map(lambda thread: thread.join(), threads)
-
-
-if __name__ == "__main__":
- args = _parse_args()
- url = _get_url()
- _confirm(url)
- server = _get_server(url)
- _update_design_docs(args, server)
diff --git a/scripts/docker/Dockerfile b/scripts/docker/Dockerfile
index 915508ea..21764d84 100644
--- a/scripts/docker/Dockerfile
+++ b/scripts/docker/Dockerfile
@@ -1,51 +1,32 @@
# start with a fresh debian image
-FROM debian
-
-# expose soledad server port in case we want to run a server container
-EXPOSE 2424
-
-# install dependencies from debian repos
-COPY files/apt/leap.list /etc/apt/sources.list.d/
-
-RUN apt-get update
-RUN apt-get -y --force-yes install leap-archive-keyring
+# we use backports because of libsqlcipher-dev
+FROM debian:jessie-backports
RUN apt-get update
RUN apt-get -y install git
-RUN apt-get -y install vim
-RUN apt-get -y install python-ipdb
-# install python deps
+# needed to build python twisted module
RUN apt-get -y install libpython2.7-dev
-RUN apt-get -y install libffi-dev
+# needed to build python cryptography module
RUN apt-get -y install libssl-dev
-RUN apt-get -y install libzmq3-dev
-RUN apt-get -y install python-pip
-RUN apt-get -y install couchdb
-RUN apt-get -y install python-srp
-RUN apt-get -y install python-scrypt
-RUN apt-get -y install leap-keymanager
-RUN apt-get -y install python-tz
+RUN apt-get -y install libffi-dev
+# needed to build pysqlcipher
+RUN apt-get -y install libsqlcipher-dev
+# needed to support keymanager
+RUN apt-get -y install libsqlite3-dev
+# install pip and tox
+RUN apt-get -y install python-pip
RUN pip install -U pip
-RUN pip install psutil
-
-# install soledad-perf deps
-RUN pip install klein
-RUN apt-get -y install curl
-RUN apt-get -y install httperf
+RUN pip install tox
# clone repositories
-ENV BASEURL "https://github.com/leapcode"
-ENV VARDIR "/var/local"
-ENV REPOS "soledad leap_pycommon soledad-perf"
-RUN for repo in ${REPOS}; do git clone ${BASEURL}/${repo}.git /var/local/${repo}; done
+RUN mkdir -p /builds/leap
+RUN git clone -b develop https://0xacab.org/leap/soledad.git /builds/leap/soledad
-# copy over files to help setup the environment and run soledad
-RUN mkdir -p /usr/local/soledad
-
-COPY files/build/install-deps-from-repos.sh /usr/local/soledad/
-RUN /usr/local/soledad/install-deps-from-repos.sh
+# use tox to install everything needed to run tests
+RUN cd /builds/leap/soledad/testing && tox -v -r --notest
+RUN mkdir -p /usr/local/soledad
COPY files/bin/ /usr/local/soledad/
diff --git a/scripts/docker/Makefile b/scripts/docker/Makefile
index 4fa2e264..7050526a 100644
--- a/scripts/docker/Makefile
+++ b/scripts/docker/Makefile
@@ -16,7 +16,7 @@
# Some configurations you might override when calling this makefile #
#####################################################################
-IMAGE_NAME ?= leap/soledad:1.0
+IMAGE_NAME ?= leapcode/soledad:latest
SOLEDAD_REMOTE ?= https://0xacab.org/leap/soledad.git
SOLEDAD_BRANCH ?= develop
SOLEDAD_PRELOAD_NUM ?= 100
@@ -27,11 +27,14 @@ MEMORY ?= 512m
# Docker image generation (main make target) #
##############################################
-all: image
+all: soledad-image couchdb-image
-image:
+soledad-image:
docker build -t $(IMAGE_NAME) .
+couchdb-image:
+ (cd couchdb/ && make)
+
##################################################
# Run a Soledad Server inside a docker container #
##################################################
@@ -69,23 +72,37 @@ run-client-bootstrap:
/usr/local/soledad/run-client-bootstrap.sh
#################################################
-# Run all trial tests inside a docker container #
+# Run all tests inside a docker container #
#################################################
-run-trial:
+run-tox:
+ name=$$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 5 | head -n 1); \
+ docker run -d --name $${name} leap/couchdb; \
docker run -t -i \
--memory="$(MEMORY)" \
--env="SOLEDAD_REMOTE=$(SOLEDAD_REMOTE)" \
--env="SOLEDAD_BRANCH=$(SOLEDAD_BRANCH)" \
+ --env="COUCH_URL=http://$${name}:5984" \
+ --link $${name} \
$(IMAGE_NAME) \
- /usr/local/soledad/run-trial.sh
+ /usr/local/soledad/run-tox.sh
############################################
# Performance tests and graphic generation #
############################################
-run-perf-test:
- helper/run-test.sh perf
+run-perf:
+ name=$$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 5 | head -n 1); \
+ docker run -d --name $${name} leap/couchdb; \
+ docker run -t -i \
+ --memory="$(MEMORY)" \
+ --env="SOLEDAD_REMOTE=$(SOLEDAD_REMOTE)" \
+ --env="SOLEDAD_BRANCH=$(SOLEDAD_BRANCH)" \
+ --env="SOLEDAD_PRELOAD_NUM=$(SOLEDAD_PRELOAD_NUM)" \
+ --env="COUCH_URL=http://$${name}:5984" \
+ --link $${name} \
+ $(IMAGE_NAME) \
+ /usr/local/soledad/run-perf.sh
run-client-perf:
@if [ -z "$(CONTAINER_ID_FILE)" ]; then \
@@ -123,7 +140,7 @@ cp-perf-result:
# Other helper targets #
########################
-run-shell: image
+run-shell: soledad-image
docker run -t -i \
--memory="$(MEMORY)" \
$(IMAGE_NAME) \
diff --git a/scripts/docker/README.md b/scripts/docker/README.md
index c4d7ac94..97b39f87 100644
--- a/scripts/docker/README.md
+++ b/scripts/docker/README.md
@@ -11,7 +11,20 @@ Check the `Dockerfile` for the steps for creating the docker image.
Check the `Makefile` for the rules for running containers.
-Check the `helper/` directory for scripts that help running tests.
+
+Installation
+------------
+
+1. Install docker for your system: https://docs.docker.com/
+2. Build images by running `make`
+3. Execute `make run-tox` and `make run-perf` to run tox tests and perf tests,
+ respectivelly.
+4. You may want to pass some variables to the `make` command to control
+ parameters of execution, for example:
+
+ make run-perf SOLEDAD_PRELOAD_NUM=500
+
+ See more variables below.
Environment variables for docker containers
diff --git a/scripts/docker/TODO b/scripts/docker/TODO
index 5185d754..90597637 100644
--- a/scripts/docker/TODO
+++ b/scripts/docker/TODO
@@ -1 +1,5 @@
- limit resources of containers (mem and cpu)
+- allow running couchdb on another container
+- use a config file to get defaults for running tests
+- use the /builds directory as base of git repo
+- save the test state to a directory to make it reproducible
diff --git a/scripts/docker/couchdb/Dockerfile b/scripts/docker/couchdb/Dockerfile
new file mode 100644
index 00000000..03448da5
--- /dev/null
+++ b/scripts/docker/couchdb/Dockerfile
@@ -0,0 +1,3 @@
+FROM couchdb:latest
+
+COPY local.ini /usr/local/etc/couchdb/
diff --git a/scripts/docker/couchdb/Makefile b/scripts/docker/couchdb/Makefile
new file mode 100644
index 00000000..cf3ac966
--- /dev/null
+++ b/scripts/docker/couchdb/Makefile
@@ -0,0 +1,4 @@
+IMAGE_NAME ?= leap/couchdb
+
+image:
+ docker build -t $(IMAGE_NAME) .
diff --git a/scripts/docker/couchdb/README.rst b/scripts/docker/couchdb/README.rst
new file mode 100644
index 00000000..31a791a8
--- /dev/null
+++ b/scripts/docker/couchdb/README.rst
@@ -0,0 +1,12 @@
+Couchdb Docker image
+====================
+
+This directory contains rules to build a custom couchdb docker image to be
+provided as backend to soledad server.
+
+Type `make` to build the image.
+
+Differences between this image and the official one:
+
+ - add the "nodelay" socket option on the httpd section of the config file
+ (see: https://leap.se/code/issues/8264).
diff --git a/scripts/docker/couchdb/local.ini b/scripts/docker/couchdb/local.ini
new file mode 100644
index 00000000..3650e0ed
--- /dev/null
+++ b/scripts/docker/couchdb/local.ini
@@ -0,0 +1,2 @@
+[httpd]
+socket_options = [{recbuf, 262144}, {sndbuf, 262144}, {nodelay, true}]
diff --git a/scripts/docker/files/bin/run-perf.sh b/scripts/docker/files/bin/run-perf.sh
new file mode 100755
index 00000000..72060230
--- /dev/null
+++ b/scripts/docker/files/bin/run-perf.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+REPO=/builds/leap/soledad/testing
+COUCH_URL="${COUCH_URL:-http://127.0.0.1:5984}"
+SOLEDAD_PRELOAD_NUM="${SOLEDAD_PRELOAD_NUM:-100}"
+
+if [ ! -z "${SOLEDAD_REMOTE}" ]; then
+ git -C ${REPO} remote set-url origin ${SOLEDAD_REMOTE}
+ git -C ${REPO} fetch origin
+fi
+
+if [ ! -z "${SOLEDAD_BRANCH}" ]; then
+ git -C ${REPO} checkout ${SOLEDAD_BRANCH}
+fi
+
+cd ${REPO}
+
+tox perf -- \
+ --durations 0 \
+ --couch-url ${COUCH_URL} \
+ --twisted \
+ --num-docs ${SOLEDAD_PRELOAD_NUM}
diff --git a/scripts/docker/files/bin/run-tox.sh b/scripts/docker/files/bin/run-tox.sh
new file mode 100755
index 00000000..74fde182
--- /dev/null
+++ b/scripts/docker/files/bin/run-tox.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+REPO=/builds/leap/soledad/testing
+COUCH_URL="${COUCH_URL:-http://127.0.0.1:5984}"
+
+if [ ! -z "${SOLEDAD_REMOTE}" ]; then
+ git -C ${REPO} remote set-url origin ${SOLEDAD_REMOTE}
+ git -C ${REPO} fetch origin
+fi
+
+if [ ! -z "${SOLEDAD_BRANCH}" ]; then
+ git -C ${REPO} checkout ${SOLEDAD_BRANCH}
+fi
+
+cd ${REPO}
+
+tox -- --couch-url ${COUCH_URL}
diff --git a/scripts/docker/files/bin/setup-test-env.py b/scripts/docker/files/bin/setup-test-env.py
index 0f3ea6f4..4868fd56 100755
--- a/scripts/docker/files/bin/setup-test-env.py
+++ b/scripts/docker/files/bin/setup-test-env.py
@@ -194,12 +194,12 @@ def user_db_create(args):
url = 'http://localhost:%d/user-%s' % (args.port, args.uuid)
try:
CouchDatabase.open_database(
- url=url, create=False, replica_uid=None, ensure_ddocs=True)
+ url=url, create=False, replica_uid=None)
print '[*] error: database "user-%s" already exists' % args.uuid
exit(1)
except DatabaseDoesNotExist:
CouchDatabase.open_database(
- url=url, create=True, replica_uid=None, ensure_ddocs=True)
+ url=url, create=True, replica_uid=None)
print '[+] database created: user-%s' % args.uuid
@@ -372,7 +372,10 @@ CERT_CONFIG_FILE = os.path.join(
def cert_create(args):
private_key = os.path.join(args.basedir, args.private_key)
cert_key = os.path.join(args.basedir, args.cert_key)
- os.mkdir(args.basedir)
+ try:
+ os.mkdir(args.basedir)
+ except OSError:
+ pass
call([
'openssl',
'req',
@@ -389,8 +392,11 @@ def cert_create(args):
def cert_delete(args):
private_key = os.path.join(args.basedir, args.private_key)
cert_key = os.path.join(args.basedir, args.cert_key)
- os.unlink(private_key)
- os.unlink(cert_key)
+ try:
+ os.unlink(private_key)
+ os.unlink(cert_key)
+ except OSError:
+ pass
#
diff --git a/scripts/migration/0.9.0/.gitignore b/scripts/migration/0.9.0/.gitignore
new file mode 100644
index 00000000..6115c109
--- /dev/null
+++ b/scripts/migration/0.9.0/.gitignore
@@ -0,0 +1 @@
+log/*
diff --git a/scripts/migration/0.9.0/README.md b/scripts/migration/0.9.0/README.md
new file mode 100644
index 00000000..919a5235
--- /dev/null
+++ b/scripts/migration/0.9.0/README.md
@@ -0,0 +1,73 @@
+CouchDB schema migration to Soledad 0.8.2
+=========================================
+
+Migrate couch database schema from <= 0.8.1 version to 0.8.2 version.
+
+
+ATTENTION!
+----------
+
+ - This script does not backup your data for you. Make sure you have a backup
+ copy of your databases before running this script!
+
+ - Make sure you turn off any service that might be writing to the couch
+ database before running this script.
+
+
+Usage
+-----
+
+To see what the script would do, run:
+
+ ./migrate.py
+
+To actually run the migration, add the --do-migrate command line option:
+
+ ./migrate.py --do-migrate
+
+See command line options:
+
+ ./migrate.py --help
+
+
+Log
+---
+
+If you don't pass a --log-file command line option, a log will be written to
+the `log/` folder.
+
+
+Differences between old and new couch schema
+--------------------------------------------
+
+The differences between old and new schemas are:
+
+ - Transaction metadata was previously stored inside each document, and we
+ used design doc view/list functions to retrieve that information. Now,
+ transaction metadata is stored in documents with special ids
+ (gen-0000000001 to gen-9999999999).
+
+ - Database replica config metadata was stored in a document called
+ "u1db_config", and now we store it in the "_local/config" document.
+
+ - Sync metadata was previously stored in documents with id
+ "u1db_sync_<source-replica-id>", and now are stored in
+ "_local/sync_<source-replica-id>".
+
+ - The new schema doesn't make use of any design documents.
+
+
+What does this script do
+------------------------
+
+- List all databases starting with "user-".
+- For each one, do:
+ - Check if it contains the old "u1db_config" document.
+ - If it doesn't, skip this db.
+ - Get the transaction log using the usual design doc view/list functions.
+ - Write a new "gen-X" document for each line on the transaction log.
+ - Get the "u1db_config" document, create a new one in "_local/config",
+ Delete the old one.
+ - List all "u1db_sync_X" documents, create new ones in "_local/sync_X",
+ delete the old ones.
+ - Delete unused design documents.
diff --git a/scripts/migration/0.9.0/log/.empty b/scripts/migration/0.9.0/log/.empty
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/scripts/migration/0.9.0/log/.empty
diff --git a/scripts/migration/0.9.0/migrate.py b/scripts/migration/0.9.0/migrate.py
new file mode 100755
index 00000000..6ad5bc2d
--- /dev/null
+++ b/scripts/migration/0.9.0/migrate.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python
+# migrate.py
+
+"""
+Migrate CouchDB schema to Soledad 0.8.2 schema.
+
+******************************************************************************
+ ATTENTION!
+
+ - This script does not backup your data for you. Make sure you have a backup
+ copy of your databases before running this script!
+
+ - Make sure you turn off any service that might be writing to the couch
+ database before running this script.
+
+******************************************************************************
+
+Run this script with the --help option to see command line options.
+
+See the README.md file for more information.
+"""
+
+import datetime
+import logging
+import netrc
+import os
+
+from argparse import ArgumentParser
+
+from leap.soledad.server import load_configuration
+
+from migrate_couch_schema import migrate
+
+
+TARGET_VERSION = '0.8.2'
+DEFAULT_COUCH_URL = 'http://127.0.0.1:5984'
+CONF = load_configuration('/etc/soledad/soledad-server.conf')
+NETRC_PATH = CONF['soledad-server']['admin_netrc']
+
+
+#
+# command line args and execution
+#
+
+def _configure_logger(log_file, level=logging.INFO):
+ if not log_file:
+ fname, _ = os.path.basename(__file__).split('.')
+ timestr = datetime.datetime.now().strftime('%Y-%m-%d_%H:%M:%S')
+ filename = 'soledad_%s_%s_%s.log' \
+ % (TARGET_VERSION, fname, timestr)
+ dirname = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), 'log')
+ log_file = os.path.join(dirname, filename)
+ logging.basicConfig(
+ filename=log_file,
+ filemode='a',
+ format='%(asctime)s,%(msecs)d %(levelname)s %(message)s',
+ datefmt='%H:%M:%S',
+ level=level)
+
+
+def _default_couch_url():
+ if not os.path.exists(NETRC_PATH):
+ return DEFAULT_COUCH_URL
+ parsed_netrc = netrc.netrc(NETRC_PATH)
+ host, (login, _, password) = parsed_netrc.hosts.items()[0]
+ url = ('http://%(login)s:%(password)s@%(host)s:5984' % {
+ 'login': login,
+ 'password': password,
+ 'host': host})
+ return url
+
+
+def _parse_args():
+ parser = ArgumentParser()
+ parser.add_argument(
+ '--couch_url',
+ help='the url for the couch database',
+ default=_default_couch_url())
+ parser.add_argument(
+ '--do-migrate',
+ help='actually perform the migration (otherwise '
+ 'just print what would be done)',
+ action='store_true')
+ parser.add_argument(
+ '--log-file',
+ help='the log file to use')
+ parser.add_argument(
+ '--pdb', action='store_true',
+ help='escape to pdb shell in case of exception')
+ parser.add_argument(
+ '--verbose', action='store_true',
+ help='output detailed information about the migration '
+ '(i.e. include debug messages)')
+ return parser.parse_args()
+
+
+def _enable_pdb():
+ import sys
+ from IPython.core import ultratb
+ sys.excepthook = ultratb.FormattedTB(
+ mode='Verbose', color_scheme='Linux', call_pdb=1)
+
+
+if __name__ == '__main__':
+ args = _parse_args()
+ if args.pdb:
+ _enable_pdb()
+ _configure_logger(
+ args.log_file,
+ level=logging.DEBUG if args.verbose else logging.INFO)
+ logger = logging.getLogger(__name__)
+ try:
+ migrate(args, TARGET_VERSION)
+ except:
+ logger.exception('Fatal error on migrate script!')
+ raise
diff --git a/scripts/migration/0.9.0/migrate_couch_schema/__init__.py b/scripts/migration/0.9.0/migrate_couch_schema/__init__.py
new file mode 100644
index 00000000..f0b456e4
--- /dev/null
+++ b/scripts/migration/0.9.0/migrate_couch_schema/__init__.py
@@ -0,0 +1,192 @@
+# __init__.py
+"""
+Support functions for migration script.
+"""
+
+import logging
+
+from couchdb import Server
+from couchdb import ResourceNotFound
+from couchdb import ResourceConflict
+
+from leap.soledad.common.couch import GENERATION_KEY
+from leap.soledad.common.couch import TRANSACTION_ID_KEY
+from leap.soledad.common.couch import REPLICA_UID_KEY
+from leap.soledad.common.couch import DOC_ID_KEY
+from leap.soledad.common.couch import SCHEMA_VERSION_KEY
+from leap.soledad.common.couch import CONFIG_DOC_ID
+from leap.soledad.common.couch import SYNC_DOC_ID_PREFIX
+from leap.soledad.common.couch import SCHEMA_VERSION
+
+
+logger = logging.getLogger(__name__)
+
+
+#
+# support functions
+#
+
+def _get_couch_server(couch_url):
+ return Server(couch_url)
+
+
+def _is_migrateable(db):
+ config_doc = db.get('u1db_config')
+ return bool(config_doc)
+
+
+def _get_transaction_log(db):
+ ddoc_path = ['_design', 'transactions', '_view', 'log']
+ resource = db.resource(*ddoc_path)
+ try:
+ _, _, data = resource.get_json()
+ except ResourceNotFound:
+ logger.warning(
+ '[%s] missing transactions design document, '
+ 'can\'t get transaction log.' % db.name)
+ return []
+ rows = data['rows']
+ transaction_log = []
+ gen = 1
+ for row in rows:
+ transaction_log.append((gen, row['id'], row['value']))
+ gen += 1
+ return transaction_log
+
+
+def _get_user_dbs(server):
+ user_dbs = filter(lambda dbname: dbname.startswith('user-'), server)
+ return user_dbs
+
+
+#
+# migration main functions
+#
+
+def migrate(args, target_version):
+ server = _get_couch_server(args.couch_url)
+ logger.info('starting couch schema migration to %s' % target_version)
+ if not args.do_migrate:
+ logger.warning('dry-run: no changes will be made to databases')
+ user_dbs = _get_user_dbs(server)
+ for dbname in user_dbs:
+ db = server[dbname]
+ if not _is_migrateable(db):
+ logger.warning("[%s] skipping not migrateable user db" % dbname)
+ continue
+ logger.info("[%s] starting migration of user db" % dbname)
+ try:
+ _migrate_user_db(db, args.do_migrate)
+ logger.info("[%s] finished migration of user db" % dbname)
+ except:
+ logger.exception('[%s] error migrating user db' % dbname)
+ logger.error('continuing with next database.')
+ logger.info('finished couch schema migration to %s' % target_version)
+
+
+def _migrate_user_db(db, do_migrate):
+ _migrate_transaction_log(db, do_migrate)
+ _migrate_sync_docs(db, do_migrate)
+ _delete_design_docs(db, do_migrate)
+ _migrate_config_doc(db, do_migrate)
+
+
+def _migrate_transaction_log(db, do_migrate):
+ transaction_log = _get_transaction_log(db)
+ for gen, doc_id, trans_id in transaction_log:
+ gen_doc_id = 'gen-%s' % str(gen).zfill(10)
+ doc = {
+ '_id': gen_doc_id,
+ GENERATION_KEY: gen,
+ DOC_ID_KEY: doc_id,
+ TRANSACTION_ID_KEY: trans_id,
+ }
+ logger.debug('[%s] creating gen doc: %s' % (db.name, gen_doc_id))
+ if do_migrate:
+ try:
+ db.save(doc)
+ except ResourceConflict:
+ # this gen document already exists. if documents are the same,
+ # continue with migration.
+ existing_doc = db.get(gen_doc_id)
+ for key in [GENERATION_KEY, DOC_ID_KEY, TRANSACTION_ID_KEY]:
+ if existing_doc[key] != doc[key]:
+ raise
+
+
+def _migrate_config_doc(db, do_migrate):
+ old_doc = db['u1db_config']
+ new_doc = {
+ '_id': CONFIG_DOC_ID,
+ REPLICA_UID_KEY: old_doc[REPLICA_UID_KEY],
+ SCHEMA_VERSION_KEY: SCHEMA_VERSION,
+ }
+ logger.info("[%s] moving config doc: %s -> %s"
+ % (db.name, old_doc['_id'], new_doc['_id']))
+ if do_migrate:
+ # the config doc must not exist, otherwise we would have skipped this
+ # database.
+ db.save(new_doc)
+ db.delete(old_doc)
+
+
+def _migrate_sync_docs(db, do_migrate):
+ logger.info('[%s] moving sync docs' % db.name)
+ view = db.view(
+ '_all_docs',
+ startkey='u1db_sync',
+ endkey='u1db_synd',
+ include_docs='true')
+ for row in view.rows:
+ old_doc = row['doc']
+ old_id = old_doc['_id']
+
+ # older schemas used different documents with ids starting with
+ # "u1db_sync" to store sync-related data:
+ #
+ # - u1db_sync_log: was used to store the whole sync log.
+ # - u1db_sync_state: was used to store the sync state.
+ #
+ # if any of these documents exist in the current db, they are leftover
+ # from previous migrations, and should just be removed.
+ if old_id in ['u1db_sync_log', 'u1db_sync_state']:
+ logger.info('[%s] removing leftover document: %s'
+ % (db.name, old_id))
+ if do_migrate:
+ db.delete(old_doc)
+ continue
+
+ replica_uid = old_id.replace('u1db_sync_', '')
+ new_id = "%s%s" % (SYNC_DOC_ID_PREFIX, replica_uid)
+ new_doc = {
+ '_id': new_id,
+ GENERATION_KEY: old_doc['generation'],
+ TRANSACTION_ID_KEY: old_doc['transaction_id'],
+ REPLICA_UID_KEY: replica_uid,
+ }
+ logger.debug("[%s] moving sync doc: %s -> %s"
+ % (db.name, old_id, new_id))
+ if do_migrate:
+ try:
+ db.save(new_doc)
+ except ResourceConflict:
+ # this sync document already exists. if documents are the same,
+ # continue with migration.
+ existing_doc = db.get(new_id)
+ for key in [GENERATION_KEY, TRANSACTION_ID_KEY,
+ REPLICA_UID_KEY]:
+ if existing_doc[key] != new_doc[key]:
+ raise
+ db.delete(old_doc)
+
+
+def _delete_design_docs(db, do_migrate):
+ for ddoc in ['docs', 'syncs', 'transactions']:
+ doc_id = '_design/%s' % ddoc
+ doc = db.get(doc_id)
+ if doc:
+ logger.info("[%s] deleting design doc: %s" % (db.name, doc_id))
+ if do_migrate:
+ db.delete(doc)
+ else:
+ logger.warning("[%s] design doc not found: %s" % (db.name, doc_id))
diff --git a/scripts/migration/0.9.0/requirements.pip b/scripts/migration/0.9.0/requirements.pip
new file mode 100644
index 00000000..ea22a1a4
--- /dev/null
+++ b/scripts/migration/0.9.0/requirements.pip
@@ -0,0 +1,3 @@
+couchdb
+leap.soledad.common==0.9.0
+leap.soledad.server==0.9.0
diff --git a/scripts/migration/0.9.0/setup.py b/scripts/migration/0.9.0/setup.py
new file mode 100644
index 00000000..0467e932
--- /dev/null
+++ b/scripts/migration/0.9.0/setup.py
@@ -0,0 +1,8 @@
+from setuptools import setup
+from setuptools import find_packages
+
+
+setup(
+ name='migrate_couch_schema',
+ packages=find_packages('.'),
+)
diff --git a/scripts/migration/0.9.0/tests/conftest.py b/scripts/migration/0.9.0/tests/conftest.py
new file mode 100644
index 00000000..61f6c7ee
--- /dev/null
+++ b/scripts/migration/0.9.0/tests/conftest.py
@@ -0,0 +1,54 @@
+# conftest.py
+
+"""
+Provide a couch database with content stored in old schema.
+"""
+
+import couchdb
+import pytest
+import uuid
+
+
+COUCH_URL = 'http://127.0.0.1:5984'
+
+transaction_map = """
+function(doc) {
+ if (doc.u1db_transactions)
+ doc.u1db_transactions.forEach(function(t) {
+ emit(t[0], // use timestamp as key so the results are ordered
+ t[1]); // value is the transaction_id
+ });
+}
+"""
+
+initial_docs = [
+ {'_id': 'u1db_config', 'replica_uid': 'an-uid'},
+ {'_id': 'u1db_sync_A', 'generation': 0, 'replica_uid': 'A',
+ 'transaction_id': ''},
+ {'_id': 'u1db_sync_B', 'generation': 2, 'replica_uid': 'B',
+ 'transaction_id': 'X'},
+ {'_id': 'doc1', 'u1db_transactions': [(1, 'trans-1'), (3, 'trans-3')]},
+ {'_id': 'doc2', 'u1db_transactions': [(2, 'trans-2'), (4, 'trans-4')]},
+ {'_id': '_design/docs'},
+ {'_id': '_design/syncs'},
+ {'_id': '_design/transactions',
+ 'views': {'log': {'map': transaction_map}}},
+ # add some data from previous interrupted migration
+ {'_id': '_local/sync_A', 'gen': 0, 'trans_id': '', 'replica_uid': 'A'},
+ {'_id': 'gen-0000000002',
+ 'gen': 2, 'trans_id': 'trans-2', 'doc_id': 'doc2'},
+ # the following should be removed if found in the dbs
+ {'_id': 'u1db_sync_log'},
+ {'_id': 'u1db_sync_state'},
+]
+
+
+@pytest.fixture(scope='function')
+def db(request):
+ server = couchdb.Server(COUCH_URL)
+ dbname = "user-" + uuid.uuid4().hex
+ db = server.create(dbname)
+ for doc in initial_docs:
+ db.save(doc)
+ request.addfinalizer(lambda: server.delete(dbname))
+ return db
diff --git a/scripts/migration/0.9.0/tests/test_migrate.py b/scripts/migration/0.9.0/tests/test_migrate.py
new file mode 100644
index 00000000..10c8b906
--- /dev/null
+++ b/scripts/migration/0.9.0/tests/test_migrate.py
@@ -0,0 +1,67 @@
+# test_migrate.py
+
+"""
+Ensure that the migration script works!
+"""
+
+from migrate_couch_schema import _migrate_user_db
+
+from leap.soledad.common.couch import GENERATION_KEY
+from leap.soledad.common.couch import TRANSACTION_ID_KEY
+from leap.soledad.common.couch import REPLICA_UID_KEY
+from leap.soledad.common.couch import DOC_ID_KEY
+from leap.soledad.common.couch import SCHEMA_VERSION_KEY
+from leap.soledad.common.couch import CONFIG_DOC_ID
+from leap.soledad.common.couch import SYNC_DOC_ID_PREFIX
+from leap.soledad.common.couch import SCHEMA_VERSION
+
+
+def test__migrate_user_db(db):
+ _migrate_user_db(db, True)
+
+ # we should find exactly 6 documents: 2 normal documents and 4 generation
+ # documents
+ view = db.view('_all_docs')
+ assert len(view.rows) == 6
+
+ # ensure that the ids of the documents we found on the database are correct
+ doc_ids = map(lambda doc: doc.id, view.rows)
+ assert 'doc1' in doc_ids
+ assert 'doc2' in doc_ids
+ assert 'gen-0000000001' in doc_ids
+ assert 'gen-0000000002' in doc_ids
+ assert 'gen-0000000003' in doc_ids
+ assert 'gen-0000000004' in doc_ids
+
+ # assert config doc contents
+ config_doc = db.get(CONFIG_DOC_ID)
+ assert config_doc[REPLICA_UID_KEY] == 'an-uid'
+ assert config_doc[SCHEMA_VERSION_KEY] == SCHEMA_VERSION
+
+ # assert sync docs contents
+ sync_doc_A = db.get('%s%s' % (SYNC_DOC_ID_PREFIX, 'A'))
+ assert sync_doc_A[GENERATION_KEY] == 0
+ assert sync_doc_A[REPLICA_UID_KEY] == 'A'
+ assert sync_doc_A[TRANSACTION_ID_KEY] == ''
+ sync_doc_B = db.get('%s%s' % (SYNC_DOC_ID_PREFIX, 'B'))
+ assert sync_doc_B[GENERATION_KEY] == 2
+ assert sync_doc_B[REPLICA_UID_KEY] == 'B'
+ assert sync_doc_B[TRANSACTION_ID_KEY] == 'X'
+
+ # assert gen docs contents
+ gen_1 = db.get('gen-0000000001')
+ assert gen_1[DOC_ID_KEY] == 'doc1'
+ assert gen_1[GENERATION_KEY] == 1
+ assert gen_1[TRANSACTION_ID_KEY] == 'trans-1'
+ gen_2 = db.get('gen-0000000002')
+ assert gen_2[DOC_ID_KEY] == 'doc2'
+ assert gen_2[GENERATION_KEY] == 2
+ assert gen_2[TRANSACTION_ID_KEY] == 'trans-2'
+ gen_3 = db.get('gen-0000000003')
+ assert gen_3[DOC_ID_KEY] == 'doc1'
+ assert gen_3[GENERATION_KEY] == 3
+ assert gen_3[TRANSACTION_ID_KEY] == 'trans-3'
+ gen_4 = db.get('gen-0000000004')
+ assert gen_4[DOC_ID_KEY] == 'doc2'
+ assert gen_4[GENERATION_KEY] == 4
+ assert gen_4[TRANSACTION_ID_KEY] == 'trans-4'
diff --git a/scripts/migration/0.9.0/tox.ini b/scripts/migration/0.9.0/tox.ini
new file mode 100644
index 00000000..2bb6be4c
--- /dev/null
+++ b/scripts/migration/0.9.0/tox.ini
@@ -0,0 +1,13 @@
+[tox]
+envlist = py27
+
+[testenv]
+commands = py.test {posargs}
+changedir = tests
+deps =
+ pytest
+ couchdb
+ pdbpp
+ -e../../../common
+setenv =
+ TERM=xterm
diff --git a/scripts/packaging/compile_design_docs.py b/scripts/packaging/compile_design_docs.py
deleted file mode 100644
index b2b5729a..00000000
--- a/scripts/packaging/compile_design_docs.py
+++ /dev/null
@@ -1,112 +0,0 @@
-#!/usr/bin/python
-
-
-# This script builds files for the design documents represented in the
-# ../common/src/soledad/common/ddocs directory structure (relative to the
-# current location of the script) into a target directory.
-
-
-import argparse
-from os import listdir
-from os.path import realpath, dirname, isdir, join, isfile, basename
-import json
-
-DDOCS_REL_PATH = ('..', 'common', 'src', 'leap', 'soledad', 'common', 'ddocs')
-
-
-def build_ddocs():
- """
- Build design documents.
-
- For ease of development, couch backend design documents are stored as
- `.js` files in subdirectories of
- `../common/src/leap/soledad/common/ddocs`. This function scans that
- directory for javascript files, and builds the design documents structure.
-
- This funciton uses the following conventions to generate design documents:
-
- - Design documents are represented by directories in the form
- `<prefix>/<ddoc>`, there prefix is the `src/leap/soledad/common/ddocs`
- directory.
- - Design document directories might contain `views`, `lists` and
- `updates` subdirectories.
- - Views subdirectories must contain a `map.js` file and may contain a
- `reduce.js` file.
- - List and updates subdirectories may contain any number of javascript
- files (i.e. ending in `.js`) whose names will be mapped to the
- corresponding list or update function name.
- """
- ddocs = {}
-
- # design docs are represented by subdirectories of `DDOCS_REL_PATH`
- cur_pwd = dirname(realpath(__file__))
- ddocs_path = join(cur_pwd, *DDOCS_REL_PATH)
- for ddoc in [f for f in listdir(ddocs_path)
- if isdir(join(ddocs_path, f))]:
-
- ddocs[ddoc] = {'_id': '_design/%s' % ddoc}
-
- for t in ['views', 'lists', 'updates']:
- tdir = join(ddocs_path, ddoc, t)
- if isdir(tdir):
-
- ddocs[ddoc][t] = {}
-
- if t == 'views': # handle views (with map/reduce functions)
- for view in [f for f in listdir(tdir)
- if isdir(join(tdir, f))]:
- # look for map.js and reduce.js
- mapfile = join(tdir, view, 'map.js')
- reducefile = join(tdir, view, 'reduce.js')
- mapfun = None
- reducefun = None
- try:
- with open(mapfile) as f:
- mapfun = f.read()
- except IOError:
- pass
- try:
- with open(reducefile) as f:
- reducefun = f.read()
- except IOError:
- pass
- ddocs[ddoc]['views'][view] = {}
-
- if mapfun is not None:
- ddocs[ddoc]['views'][view]['map'] = mapfun
- if reducefun is not None:
- ddocs[ddoc]['views'][view]['reduce'] = reducefun
-
- else: # handle lists, updates, etc
- for fun in [f for f in listdir(tdir)
- if isfile(join(tdir, f))]:
- funfile = join(tdir, fun)
- funname = basename(funfile).replace('.js', '')
- try:
- with open(funfile) as f:
- ddocs[ddoc][t][funname] = f.read()
- except IOError:
- pass
- return ddocs
-
-
-if __name__ == '__main__':
- parser = argparse.ArgumentParser()
- parser.add_argument(
- 'target', type=str,
- help='the target dir where to store design documents')
- args = parser.parse_args()
-
- # check if given target is a directory
- if not isdir(args.target):
- print 'Error: %s is not a directory.' % args.target
- exit(1)
-
- # write desifgn docs files
- ddocs = build_ddocs()
- for ddoc in ddocs:
- ddoc_filename = "%s.json" % ddoc
- with open(join(args.target, ddoc_filename), 'w') as f:
- f.write("%s" % json.dumps(ddocs[ddoc], indent=3))
- print "Wrote _design/%s content in %s" \
- % (ddoc, join(args.target, ddoc_filename,))
diff --git a/scripts/profiling/mail/couchdb_server.py b/scripts/profiling/mail/couchdb_server.py
index 2cf0a3fd..452f8ec2 100644
--- a/scripts/profiling/mail/couchdb_server.py
+++ b/scripts/profiling/mail/couchdb_server.py
@@ -18,8 +18,7 @@ def start_couchdb_wrapper():
def get_u1db_database(dbname, port):
return CouchDatabase.open_database(
'http://127.0.0.1:%d/%s' % (port, dbname),
- True,
- ensure_ddocs=True)
+ True)
def create_tokens_database(port, uuid, token_value):
@@ -38,5 +37,5 @@ def get_couchdb_wrapper_and_u1db(uuid, token_value):
couchdb_u1db = get_u1db_database('user-%s' % uuid, couchdb_wrapper.port)
get_u1db_database('shared', couchdb_wrapper.port)
create_tokens_database(couchdb_wrapper.port, uuid, token_value)
-
+
return couchdb_wrapper, couchdb_u1db