summaryrefslogtreecommitdiffstats
path: root/utils
diff options
context:
space:
mode:
Diffstat (limited to 'utils')
-rwxr-xr-xutils/fetch_os_creds.sh70
-rwxr-xr-xutils/test/reporting/functest/reporting-status.py1
-rwxr-xr-xutils/test/reporting/functest/reporting-tempest.py19
-rw-r--r--utils/test/reporting/utils/reporting_utils.py27
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/app.js16
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/components/results/resultsController.js4
-rw-r--r--utils/test/testapi/3rd_party/static/testapi-ui/config.json2
-rw-r--r--utils/test/testapi/etc/config.ini10
-rw-r--r--utils/test/testapi/htmlize/htmlize.py4
-rw-r--r--utils/test/testapi/opnfv_testapi/common/config.py5
-rw-r--r--utils/test/testapi/opnfv_testapi/resources/handlers.py41
-rw-r--r--utils/test/testapi/opnfv_testapi/resources/result_handlers.py21
-rw-r--r--utils/test/testapi/opnfv_testapi/tests/unit/fake_pymongo.py59
13 files changed, 200 insertions, 79 deletions
diff --git a/utils/fetch_os_creds.sh b/utils/fetch_os_creds.sh
index 458bbda3b..993c0b948 100755
--- a/utils/fetch_os_creds.sh
+++ b/utils/fetch_os_creds.sh
@@ -12,8 +12,9 @@ set -o nounset
set -o pipefail
usage() {
- echo "usage: $0 [-v] -d <destination> -i <installer_type> -a <installer_ip>" >&2
+ echo "usage: $0 [-v] -d <destination> -i <installer_type> -a <installer_ip> [-s <ssh_key>]" >&2
echo "[-v] Virtualized deployment" >&2
+ echo "[-s <ssh_key>] Path to ssh key. For MCP deployments only" >&2
}
info () {
@@ -53,11 +54,12 @@ swap_to_public() {
: ${DEPLOY_TYPE:=''}
#Get options
-while getopts ":d:i:a:h:v" optchar; do
+while getopts ":d:i:a:h:s:v" optchar; do
case "${optchar}" in
d) dest_path=${OPTARG} ;;
i) installer_type=${OPTARG} ;;
a) installer_ip=${OPTARG} ;;
+ s) ssh_key=${OPTARG} ;;
v) DEPLOY_TYPE="virt" ;;
*) echo "Non-option argument: '-${OPTARG}'" >&2
usage
@@ -70,6 +72,9 @@ done
dest_path=${dest_path:-$HOME/opnfv-openrc.sh}
installer_type=${installer_type:-$INSTALLER_TYPE}
installer_ip=${installer_ip:-$INSTALLER_IP}
+if [ "${installer_type}" == "fuel" ] && [ "${BRANCH}" == "master" ]; then
+ installer_ip=${SALT_MASTER_IP}
+fi
if [ -z $dest_path ] || [ -z $installer_type ] || [ -z $installer_ip ]; then
usage
@@ -89,40 +94,45 @@ ssh_options="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
# Start fetching the files
if [ "$installer_type" == "fuel" ]; then
- #ip_fuel="10.20.0.2"
verify_connectivity $installer_ip
+ if [ "${BRANCH}" == "master" ]; then
+ ssh_key=${ssh_key:-$SSH_KEY}
+ if [ -z $ssh_key ] || [ ! -f $ssh_key ]; then
+ error "Please provide path to existing ssh key for mcp deployment."
+ exit 2
+ fi
+ ssh_options+=" -i ${ssh_key}"
- env=$(sshpass -p r00tme ssh 2>/dev/null $ssh_options root@${installer_ip} \
- 'fuel env'|grep operational|head -1|awk '{print $1}') &> /dev/null
- if [ -z $env ]; then
- error "No operational environment detected in Fuel"
- fi
- env_id="${FUEL_ENV:-$env}"
-
- # Check if controller is alive (online='True')
- controller_ip=$(sshpass -p r00tme ssh 2>/dev/null $ssh_options root@${installer_ip} \
- "fuel node --env ${env_id} | grep controller | grep 'True\| 1' | awk -F\| '{print \$5}' | head -1" | \
- sed 's/ //g') &> /dev/null
+ # retrieving controller vip
+ controller_ip=$(ssh 2>/dev/null ${ssh_options} ubuntu@${installer_ip} \
+ "sudo salt --out txt 'ctl01*' pillar.get _param:openstack_control_address | awk '{print \$2}'" | \
+ sed 's/ //g') &> /dev/null
- if [ -z $controller_ip ]; then
- error "The controller $controller_ip is not up. Please check that the POD is correctly deployed."
- fi
+ info "Fetching rc file from controller $controller_ip..."
+ ssh ${ssh_options} ubuntu@${controller_ip} "sudo cat /root/keystonercv3" > $dest_path
+ else
+ #ip_fuel="10.20.0.2"
+ env=$(sshpass -p r00tme ssh 2>/dev/null ${ssh_options} root@${installer_ip} \
+ 'fuel env'|grep operational|head -1|awk '{print $1}') &> /dev/null
+ if [ -z $env ]; then
+ error "No operational environment detected in Fuel"
+ fi
+ env_id="${FUEL_ENV:-$env}"
- info "Fetching rc file from controller $controller_ip..."
- sshpass -p r00tme ssh 2>/dev/null $ssh_options root@${installer_ip} \
- "scp $ssh_options ${controller_ip}:/root/openrc ." &> /dev/null
- sshpass -p r00tme scp 2>/dev/null $ssh_options root@${installer_ip}:~/openrc $dest_path &> /dev/null
+ # Check if controller is alive (online='True')
+ controller_ip=$(sshpass -p r00tme ssh 2>/dev/null ${ssh_options} root@${installer_ip} \
+ "fuel node --env ${env_id} | grep controller | grep 'True\| 1' | awk -F\| '{print \$5}' | head -1" | \
+ sed 's/ //g') &> /dev/null
- #This file contains the mgmt keystone API, we need the public one for our rc file
- admin_ip=$(cat $dest_path | grep "OS_AUTH_URL" | sed 's/^.*\=//' | sed "s/^\([\"']\)\(.*\)\1\$/\2/g" | sed s'/\/$//')
- public_ip=$(sshpass -p r00tme ssh $ssh_options root@${installer_ip} \
- "ssh ${controller_ip} 'source openrc; openstack endpoint list'" \
- | grep keystone | grep public | sed 's/ /\n/g' | grep ^http | head -1) &> /dev/null
- #| grep http | head -1 | cut -d '|' -f 4 | sed 's/v1\/.*/v1\//' | sed 's/ //g') &> /dev/null
- #NOTE: this is super ugly sed 's/v1\/.*/v1\//'OS_AUTH_URL
- # but sometimes the output of endpoint-list is like this: http://172.30.9.70:8004/v1/%(tenant_id)s
- # Fuel virtual need a fix
+ if [ -z $controller_ip ]; then
+ error "The controller $controller_ip is not up. Please check that the POD is correctly deployed."
+ fi
+ info "Fetching rc file from controller $controller_ip..."
+ sshpass -p r00tme ssh 2>/dev/null ${ssh_options} root@${installer_ip} \
+ "scp ${ssh_options} ${controller_ip}:/root/openrc ." &> /dev/null
+ sshpass -p r00tme scp 2>/dev/null ${ssh_options} root@${installer_ip}:~/openrc $dest_path &> /dev/null
+ fi
#convert to v3 URL
auth_url=$(cat $dest_path|grep AUTH_URL)
if [[ -z `echo $auth_url |grep v3` ]]; then
diff --git a/utils/test/reporting/functest/reporting-status.py b/utils/test/reporting/functest/reporting-status.py
index e700e047f..77ab7840f 100755
--- a/utils/test/reporting/functest/reporting-status.py
+++ b/utils/test/reporting/functest/reporting-status.py
@@ -107,7 +107,6 @@ for version in versions:
scenario_results = rp_utils.getScenarios(healthcheck,
installer,
version)
-
# get nb of supported architecture (x86, aarch64)
architectures = rp_utils.getArchitectures(scenario_results)
logger.info("Supported architectures: {}".format(architectures))
diff --git a/utils/test/reporting/functest/reporting-tempest.py b/utils/test/reporting/functest/reporting-tempest.py
index 6e6585a32..0304298b4 100755
--- a/utils/test/reporting/functest/reporting-tempest.py
+++ b/utils/test/reporting/functest/reporting-tempest.py
@@ -1,4 +1,15 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2017 Orange and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+# SPDX-license-identifier: Apache-2.0
+
from urllib2 import Request, urlopen, URLError
+from datetime import datetime
import json
import jinja2
import os
@@ -97,7 +108,13 @@ for version in rp_utils.get_config('general.versions'):
crit_rate = True
# Expect that the suite duration is inferior to 30m
- if result['details']['duration'] < criteria_duration:
+ stop_date = datetime.strptime(result['stop_date'],
+ '%Y-%m-%d %H:%M:%S')
+ start_date = datetime.strptime(result['start_date'],
+ '%Y-%m-%d %H:%M:%S')
+
+ delta = stop_date - start_date
+ if (delta.total_seconds() < criteria_duration):
crit_time = True
result['criteria'] = {'tests': crit_tests,
diff --git a/utils/test/reporting/utils/reporting_utils.py b/utils/test/reporting/utils/reporting_utils.py
index 599a93818..0a178ba1f 100644
--- a/utils/test/reporting/utils/reporting_utils.py
+++ b/utils/test/reporting/utils/reporting_utils.py
@@ -117,19 +117,29 @@ def getScenarios(case, installer, version):
url = ("http://" + url_base + "?case=" + case +
"&period=" + str(period) + "&installer=" + installer +
"&version=" + version)
- request = Request(url)
try:
+ request = Request(url)
response = urlopen(request)
k = response.read()
results = json.loads(k)
test_results = results['results']
- except URLError as e:
- print('Got an error code:', e)
+
+ page = results['pagination']['total_pages']
+ if page > 1:
+ test_results = []
+ for i in range(1, page + 1):
+ url_page = url + "&page=" + str(i)
+ request = Request(url_page)
+ response = urlopen(request)
+ k = response.read()
+ results = json.loads(k)
+ test_results += results['results']
+ except URLError as err:
+ print('Got an error code:', err)
if test_results is not None:
test_results.reverse()
-
scenario_results = {}
for r in test_results:
@@ -157,7 +167,6 @@ def getScenarioStats(scenario_results):
return scenario_stats
-# TODO convergence with above function getScenarios
def getScenarioStatus(installer, version):
period = get_config('general.period')
url_base = get_config('testapi.url')
@@ -213,8 +222,8 @@ def getQtipResults(version, installer):
k = response.read()
response.close()
results = json.loads(k)['results']
- except URLError as e:
- print('Got an error code:', e)
+ except URLError as err:
+ print('Got an error code:', err)
result_dict = {}
if results:
@@ -427,9 +436,9 @@ def export_csv(scenario_file_name, installer, version):
"/functest/scenario_history_" +
installer + ".csv")
scenario_installer_file = open(scenario_installer_file_name, "a")
- with open(scenario_file_name, "r") as f:
+ with open(scenario_file_name, "r") as scenario_file:
scenario_installer_file.write("date,scenario,installer,detail,score\n")
- for line in f:
+ for line in scenario_file:
if installer in line:
scenario_installer_file.write(line)
scenario_installer_file.close
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/app.js b/utils/test/testapi/3rd_party/static/testapi-ui/app.js
index 4a2f23af9..8c701c36c 100644
--- a/utils/test/testapi/3rd_party/static/testapi-ui/app.js
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/app.js
@@ -37,30 +37,30 @@
$stateProvider.
state('home', {
url: '/',
- templateUrl: '/testapi-ui/components/home/home.html'
+ templateUrl: 'testapi-ui/components/home/home.html'
}).
state('about', {
url: '/about',
- templateUrl: '/testapi-ui/components/about/about.html'
+ templateUrl: 'testapi-ui/components/about/about.html'
}).
state('guidelines', {
url: '/guidelines',
- templateUrl: '/testapi-ui/components/guidelines/guidelines.html',
+ templateUrl: 'testapi-ui/components/guidelines/guidelines.html',
controller: 'GuidelinesController as ctrl'
}).
state('communityResults', {
url: '/community_results',
- templateUrl: '/testapi-ui/components/results/results.html',
+ templateUrl: 'testapi-ui/components/results/results.html',
controller: 'ResultsController as ctrl'
}).
state('userResults', {
- url: '/user_results',
+ url: 'user_results',
templateUrl: '/testapi-ui/components/results/results.html',
controller: 'ResultsController as ctrl'
}).
state('resultsDetail', {
url: '/results/:testID',
- templateUrl: '/testapi-ui/components/results-report' +
+ templateUrl: 'testapi-ui/components/results-report' +
'/resultsReport.html',
controller: 'ResultsReportController as ctrl'
}).
@@ -71,12 +71,12 @@
}).
state('authFailure', {
url: '/auth_failure',
- templateUrl: '/testapi-ui/components/home/home.html',
+ templateUrl: 'testapi-ui/components/home/home.html',
controller: 'AuthFailureController as ctrl'
}).
state('logout', {
url: '/logout',
- templateUrl: '/testapi-ui/components/logout/logout.html',
+ templateUrl: 'testapi-ui/components/logout/logout.html',
controller: 'LogoutController as ctrl'
}).
state('userVendors', {
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/components/results/resultsController.js b/utils/test/testapi/3rd_party/static/testapi-ui/components/results/resultsController.js
index 93a549a7f..9e3540da5 100644
--- a/utils/test/testapi/3rd_party/static/testapi-ui/components/results/resultsController.js
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/components/results/resultsController.js
@@ -123,8 +123,8 @@
ctrl.resultsRequest =
$http.get(content_url).success(function (data) {
ctrl.data = data;
- ctrl.totalItems = 20 // ctrl.data.pagination.total_pages * ctrl.itemsPerPage;
- ctrl.currentPage = 1 // ctrl.data.pagination.current_page;
+ ctrl.totalItems = ctrl.data.pagination.total_pages * ctrl.itemsPerPage;
+ ctrl.currentPage = ctrl.data.pagination.current_page;
}).error(function (error) {
ctrl.data = null;
ctrl.totalItems = 0;
diff --git a/utils/test/testapi/3rd_party/static/testapi-ui/config.json b/utils/test/testapi/3rd_party/static/testapi-ui/config.json
index 5d48c7b12..9fdd85fbb 100644
--- a/utils/test/testapi/3rd_party/static/testapi-ui/config.json
+++ b/utils/test/testapi/3rd_party/static/testapi-ui/config.json
@@ -1 +1 @@
-{"testapiApiUrl": "http://localhost:8000/api/v1"}
+{"testapiApiUrl": "http://testresults.opnfv.org/test/api/v1"}
diff --git a/utils/test/testapi/etc/config.ini b/utils/test/testapi/etc/config.ini
index 692e48897..dad59d2d0 100644
--- a/utils/test/testapi/etc/config.ini
+++ b/utils/test/testapi/etc/config.ini
@@ -8,8 +8,12 @@ dbname = test_results_collection
[api]
# Listening port
-url = http://localhost:8000/api/v1
+url = http://testresults.opnfv.org/test/api/v1
port = 8000
+
+# Number of results for one page (integer value)
+#results_per_page = 20
+
# With debug_on set to true, error traces will be shown in HTTP responses
debug = True
authenticate = False
@@ -18,7 +22,7 @@ authenticate = False
base_url = http://localhost:8000
[ui]
-url = http://localhost:8000
+url = http://testresults.opnfv.org/test
[osid]
@@ -41,7 +45,7 @@ openid_ns = http://specs.openid.net/auth/2.0
# Return endpoint in Refstack's API. Value indicating the endpoint
# where the user should be returned to after signing in. Openstack Id
# Idp only supports HTTPS address types. (string value)
-openid_return_to = /api/v1/auth/signin_return
+openid_return_to = v1/auth/signin_return
# Claimed identifier. This value must be set to
# "http://specs.openid.net/auth/2.0/identifier_select". or to user
diff --git a/utils/test/testapi/htmlize/htmlize.py b/utils/test/testapi/htmlize/htmlize.py
index b8c4fb43f..4576d9bb0 100644
--- a/utils/test/testapi/htmlize/htmlize.py
+++ b/utils/test/testapi/htmlize/htmlize.py
@@ -40,13 +40,13 @@ if __name__ == '__main__':
type=str,
required=False,
default=('http://testresults.opnfv.org'
- '/test/swagger/spec.json'),
+ '/test/swagger/resources.json'),
help='Resource Listing Spec File')
parser.add_argument('-au', '--api-declaration-url',
type=str,
required=False,
default=('http://testresults.opnfv.org'
- '/test/swagger/spec'),
+ '/test/swagger/APIs'),
help='API Declaration Spec File')
parser.add_argument('-o', '--output-directory',
required=True,
diff --git a/utils/test/testapi/opnfv_testapi/common/config.py b/utils/test/testapi/opnfv_testapi/common/config.py
index 46765ffd1..f73c0abf2 100644
--- a/utils/test/testapi/opnfv_testapi/common/config.py
+++ b/utils/test/testapi/opnfv_testapi/common/config.py
@@ -17,6 +17,7 @@ class Config(object):
def __init__(self):
self.file = self.CONFIG if self.CONFIG else self._default_config()
self._parse()
+ self._parse_per_page()
self.static_path = os.path.join(
os.path.dirname(os.path.normpath(__file__)),
os.pardir,
@@ -37,6 +38,10 @@ class Config(object):
[setattr(self, '{}_{}'.format(section, k), self._parse_value(v))
for k, v in config.items(section)]
+ def _parse_per_page(self):
+ if not hasattr(self, 'api_results_per_page'):
+ self.api_results_per_page = 20
+
@staticmethod
def _parse_value(value):
try:
diff --git a/utils/test/testapi/opnfv_testapi/resources/handlers.py b/utils/test/testapi/opnfv_testapi/resources/handlers.py
index 2fc31ca45..0234c8a73 100644
--- a/utils/test/testapi/opnfv_testapi/resources/handlers.py
+++ b/utils/test/testapi/opnfv_testapi/resources/handlers.py
@@ -104,17 +104,50 @@ class GenericApiHandler(web.RequestHandler):
if query is None:
query = {}
data = []
+ sort = kwargs.get('sort')
+ page = kwargs.get('page', 0)
+ last = kwargs.get('last', 0)
+ per_page = kwargs.get('per_page', 0)
+
cursor = self._eval_db(self.table, 'find', query)
- if 'sort' in kwargs:
- cursor = cursor.sort(kwargs.get('sort'))
- if 'last' in kwargs:
- cursor = cursor.limit(kwargs.get('last'))
+ records_count = yield cursor.count()
+ records_nr = records_count
+ if (records_count > last) and (last > 0):
+ records_nr = last
+
+ pipelines = list()
+ if query:
+ pipelines.append({'$match': query})
+ if sort:
+ pipelines.append({'$sort': sort})
+
+ if page > 0:
+ total_pages, remainder = divmod(records_nr, per_page)
+ if remainder > 0:
+ total_pages += 1
+ pipelines.append({'$skip': (page - 1) * per_page})
+ pipelines.append({'$limit': per_page})
+ else:
+ pipelines.append({'$limit': records_nr})
+
+ cursor = self._eval_db(self.table,
+ 'aggregate',
+ pipelines,
+ allowDiskUse=True)
+
while (yield cursor.fetch_next):
data.append(self.format_data(cursor.next_object()))
if res_op is None:
res = {self.table: data}
else:
res = res_op(data, *args)
+ if page:
+ res.update({
+ 'pagination': {
+ 'current_page': page,
+ 'total_pages': total_pages
+ }
+ })
self.finish_request(res)
@web.asynchronous
diff --git a/utils/test/testapi/opnfv_testapi/resources/result_handlers.py b/utils/test/testapi/opnfv_testapi/resources/result_handlers.py
index 824a89e58..1773216c0 100644
--- a/utils/test/testapi/opnfv_testapi/resources/result_handlers.py
+++ b/utils/test/testapi/opnfv_testapi/resources/result_handlers.py
@@ -11,12 +11,15 @@ from datetime import timedelta
from bson import objectid
+from opnfv_testapi.common import config
from opnfv_testapi.common import message
from opnfv_testapi.common import raises
from opnfv_testapi.resources import handlers
from opnfv_testapi.resources import result_models
from opnfv_testapi.tornado_swagger import swagger
+CONF = config.Config()
+
class GenericResultHandler(handlers.GenericApiHandler):
def __init__(self, application, request, **kwargs):
@@ -135,22 +138,28 @@ class ResultsCLHandler(GenericResultHandler):
@type last: L{string}
@in last: query
@required last: False
+ @param page: which page to list
+ @type page: L{int}
+ @in page: query
+ @required page: False
@param trust_indicator: must be float
@type trust_indicator: L{float}
@in trust_indicator: query
@required trust_indicator: False
"""
+ limitations = {'sort': {'start_date': -1}}
last = self.get_query_argument('last', 0)
if last is not None:
last = self.get_int('last', last)
+ limitations.update({'last': last})
- page = self.get_query_argument('page', 0)
- if page:
- last = 20
+ page = self.get_query_argument('page', None)
+ if page is not None:
+ page = self.get_int('page', page)
+ limitations.update({'page': page,
+ 'per_page': CONF.api_results_per_page})
- self._list(query=self.set_query(),
- sort=[('start_date', -1)],
- last=last)
+ self._list(query=self.set_query(), **limitations)
@swagger.operation(nickname="createTestResult")
def post(self):
diff --git a/utils/test/testapi/opnfv_testapi/tests/unit/fake_pymongo.py b/utils/test/testapi/opnfv_testapi/tests/unit/fake_pymongo.py
index ef74a0857..adaf6f7c3 100644
--- a/utils/test/testapi/opnfv_testapi/tests/unit/fake_pymongo.py
+++ b/utils/test/testapi/opnfv_testapi/tests/unit/fake_pymongo.py
@@ -20,38 +20,52 @@ def thread_execute(method, *args, **kwargs):
class MemCursor(object):
def __init__(self, collection):
self.collection = collection
- self.count = len(self.collection)
+ self.length = len(self.collection)
self.sorted = []
def _is_next_exist(self):
- return self.count != 0
+ return self.length != 0
@property
def fetch_next(self):
return thread_execute(self._is_next_exist)
def next_object(self):
- self.count -= 1
+ self.length -= 1
return self.collection.pop()
def sort(self, key_or_list):
- key = key_or_list[0][0]
- if key_or_list[0][1] == -1:
- reverse = True
- else:
- reverse = False
+ for k, v in key_or_list.iteritems():
+ if v == -1:
+ reverse = True
+ else:
+ reverse = False
- if key_or_list is not None:
self.collection = sorted(self.collection,
- key=itemgetter(key), reverse=reverse)
+ key=itemgetter(k), reverse=reverse)
return self
def limit(self, limit):
if limit != 0 and limit < len(self.collection):
- self.collection = self.collection[0:limit]
- self.count = limit
+ self.collection = self.collection[0: limit]
+ self.length = limit
+ return self
+
+ def skip(self, skip):
+ if skip < self.length and (skip > 0):
+ self.collection = self.collection[self.length - skip: -1]
+ self.length -= skip
+ elif skip >= self.length:
+ self.collection = []
+ self.length = 0
return self
+ def _count(self):
+ return self.length
+
+ def count(self):
+ return thread_execute(self._count)
+
class MemDb(object):
@@ -187,6 +201,27 @@ class MemDb(object):
def find(self, *args):
return MemCursor(self._find(*args))
+ def _aggregate(self, *args, **kwargs):
+ res = self.contents
+ print args
+ for arg in args[0]:
+ for k, v in arg.iteritems():
+ if k == '$match':
+ res = self._find(v)
+ cursor = MemCursor(res)
+ for arg in args[0]:
+ for k, v in arg.iteritems():
+ if k == '$sort':
+ cursor = cursor.sort(v)
+ elif k == '$skip':
+ cursor = cursor.skip(v)
+ elif k == '$limit':
+ cursor = cursor.limit(v)
+ return cursor
+
+ def aggregate(self, *args, **kwargs):
+ return self._aggregate(*args, **kwargs)
+
def _update(self, spec, document, check_keys=True):
updated = False