Refactoring API and Archiver module

* Set uo manifest.in and tox.ini
* Add test module

Change-Id: I265401658922c75b50db4d1232af17215ee1b6fc
This commit is contained in:
efedorova 2013-10-17 19:18:08 +04:00 committed by Ekaterina Fedorova
parent e4b9fe765e
commit de388f5e64
26 changed files with 394 additions and 264 deletions

4
.gitreview Normal file
View File

@ -0,0 +1,4 @@
[gerrit]
host=review.openstack.org
port=29418
project=stackforge/murano-repository.git

View File

@ -1,3 +1,41 @@
commit d259b6c33a4379c12dd673cb8cabc769545739d0
Author: efedorova <efedorova@mirantis.com>
Date: Thu Oct 17 19:18:08 2013 +0400
Refactoring API and Archiver module
* Set uo manifest.in and tox.ini
* Add test module
Change-Id: I265401658922c75b50db4d1232af17215ee1b6fc
commit e4b9fe765e912a13ff02a394b329ecd117a0edb7
Author: efedorova <efedorova@mirantis.com>
Date: Tue Oct 15 19:03:09 2013 +0400
Fix buf with manifests listing
commit b24d260524d16dce96a682fd32629951e2ac17ee
Author: efedorova <efedorova@mirantis.com>
Date: Tue Oct 15 18:47:27 2013 +0400
Update api
commit ecb0c93b3077755192929fa8ddf045ef43d43112
Author: efedorova <efedorova@mirantis.com>
Date: Mon Oct 14 16:54:00 2013 +0400
Fix typos
commit c46bfbde86728d689677a2d7d484b2db04893186
Author: efedorova <efedorova@mirantis.com>
Date: Mon Oct 14 16:34:33 2013 +0400
Remove service to muranorepository
Add venv support
Update requirements
commit 28cbd261871038cf1fc4fa67d36c26ae9e9a4747
Author: efedorova <efedorova@mirantis.com>
Date: Mon Oct 14 13:27:09 2013 +0400

View File

@ -1,10 +1,13 @@
include AUTHORS
include README.rst
include ChangeLog
include LICENSE
recursive-include Services *
include ChangeLog
include README.rst
include MANIFEST.in
include AUTHORS
include LICENSE
include ChangeLog
include babel.cfg
include tox.ini
include muranorepository/tests/test.conf
exclude .gitignore
exclude .gitreview

View File

@ -1,6 +1,6 @@
[DEFAULT]
# Address to bind the server to
host = 172.18.10.111
host = localhost
#Port the bind the server to
port = 5000

View File

@ -10,4 +10,4 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# under the License.

View File

@ -10,4 +10,4 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# under the License.

View File

@ -1,19 +1,20 @@
# Copyright (c) 2013 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# Licensed under the Apache License, Version 2.0 (the 'License'); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
from flask import Blueprint, make_response, send_file
from flask import Blueprint, send_file
from flask import jsonify, request, abort
from werkzeug import secure_filename
@ -26,116 +27,132 @@ CONF = cfg.CONF
v1_api = Blueprint('v1', __name__)
@v1_api.route('/client/ui')
def get_ui_data():
def get_archive(client):
parser = ManifestParser(CONF.manifests)
manifests = parser.parse()
archive_name = Archiver().create(manifests, "ui")
if client == 'conductor':
return Archiver().create(manifests,
'heat',
'agent',
'scripts')
else:
return Archiver().create(manifests, client)
return send_file(archive_name)
def get_locations(data_type, result_path):
locations = []
if data_type == MANIFEST:
for item in os.listdir(result_path):
if '-manifest' in item and \
os.path.isfile(os.path.join(result_path, item)):
locations.append(item)
else:
for path, subdirs, files in os.walk(result_path):
for name in files:
if path != result_path:
base, diff = path.rsplit(result_path, 2)
# split base path and remove slash
name = os.path.join(diff[1:], name)
locations.append(name)
return jsonify({data_type: locations})
def save_file(request, result_path):
file_to_upload = request.files.get('file')
if file_to_upload:
filename = secure_filename(file_to_upload.filename)
file_to_upload.save(os.path.join(result_path, filename))
return jsonify(result='success')
else:
abort(400)
def compose_path(data_type, path=None):
if path:
return os.path.join(CONF.manifests, getattr(CONF, data_type), path)
else:
return os.path.join(CONF.manifests, getattr(CONF, data_type))
def check_data_type(data_type):
if data_type not in DATA_TYPES:
abort(404)
@v1_api.route('/client/ui')
def get_ui_data():
return send_file(get_archive('ui'))
@v1_api.route('/client/conductor')
def get_conductor_data():
parser = ManifestParser(CONF.manifests)
manifests = parser.parse()
archive_name = Archiver().create(manifests,
"heat",
"agent",
"scripts")
return send_file(archive_name)
return send_file(get_archive('conductor'))
@v1_api.route('/admin/<data_type>', methods=['GET', 'POST'])
@v1_api.route('/admin/<data_type>')
def get_data_type_locations(data_type):
####### validation ########
if data_type not in DATA_TYPES:
abort(404)
result_path = os.path.join(CONF.manifests, getattr(CONF, data_type))
####### end validation ########
if request.method == 'GET':
locations = []
if data_type == MANIFEST:
for item in os.listdir(result_path):
if '-manifest' in item:
locations.append(item)
else:
for path, subdirs, files in os.walk(result_path):
for name in files:
locations.append(name)
result = {data_type: locations}
return jsonify(result)
if request.method == 'POST':
try:
file_to_upload = request.files.get('files')
if file_to_upload:
filename = secure_filename(file_to_upload.filename)
file_to_upload.save(os.path.join(result_path, filename))
return jsonify(result="success")
except:
abort(403)
check_data_type(data_type)
result_path = compose_path(data_type)
return get_locations(data_type, result_path)
@v1_api.route('/admin/<data_type>/<path:path>', methods=['GET', 'POST'])
def get_data_type_locations_by_path_or_get_file(data_type, path):
if data_type not in DATA_TYPES:
abort(404)
result_path = os.path.join(os.path.join(CONF.manifests,
getattr(CONF, data_type),
path))
@v1_api.route('/admin/<data_type>', methods=['POST'])
def upload_file(data_type):
check_data_type(data_type)
result_path = compose_path(data_type)
try:
return save_file(request, result_path)
except:
abort(403)
@v1_api.route('/admin/<data_type>/<path:path>')
def get_locations_in_nested_path_or_get_file(data_type, path):
check_data_type(data_type)
result_path = compose_path(data_type, path)
if os.path.isfile(result_path):
return send_file(result_path)
else:
return get_locations(data_type, result_path)
@v1_api.route('/admin/<data_type>/<path:path>', methods=['POST'])
def upload_file_in_nested_path(data_type, path):
check_data_type(data_type)
result_path = compose_path(data_type, path)
return save_file(request, result_path)
@v1_api.route('/admin/<data_type>/<path:path>', methods=['PUT'])
def create_dirs(data_type, path):
check_data_type(data_type)
result_path = compose_path(data_type, path)
resp = jsonify(result='success')
if os.path.exists(result_path):
return resp
if data_type == MANIFEST:
abort(403)
try:
os.makedirs(result_path)
except Exception:
abort(403)
return resp
@v1_api.route('/admin/<data_type>/<path:path>', methods=['DELETE'])
def delete_dirictory_or_file(data_type, path):
check_data_type(data_type)
result_path = compose_path(data_type, path)
if not os.path.exists(result_path):
abort(404)
if request.method == 'GET':
locations = []
if os.path.isfile(result_path):
return send_file(result_path)
else:
for file in os.listdir(result_path):
locations.append(file)
result = {data_type: locations}
return jsonify(result)
if request.method == 'POST':
file_to_upload = request.files.get('files')
if file_to_upload:
filename = secure_filename(file_to_upload.filename)
file_to_upload.save(os.path.join(result_path, filename))
return jsonify(result="success")
else:
abort(403)
@v1_api.route('/admin/<data_type>/<path:path>', methods=['PUT', 'DELETE'])
def create_dirs(data_type, path):
if data_type not in DATA_TYPES:
abort(404)
result_path = os.path.join(CONF.manifests, getattr(CONF, data_type), path)
if request.method == 'PUT':
resp = make_response()
if os.path.exists(result_path):
return resp
if data_type == MANIFEST:
abort(403)
if os.path.isfile(result_path):
try:
os.makedirs(result_path)
except Exception as e:
abort(403)
return resp
if request.method == 'DELETE':
if not os.path.exists(result_path):
os.remove(result_path)
except Exception:
abort(404)
if os.path.isfile(result_path):
try:
os.remove(result_path)
except Exception as e:
abort(404)
else:
try:
os.rmdir(result_path)
except Exception as e:
abort(403)
resp = make_response()
return resp
else:
try:
os.rmdir(result_path)
except Exception:
abort(403)
return jsonify(result='success')

View File

@ -10,4 +10,4 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# under the License.

View File

@ -28,8 +28,8 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(__file__),
os.pardir,
os.pardir))
if os.path.exists(os.path.join(possible_topdir,
'muranorepository',
'__init__.py')):
'muranorepository',
'__init__.py')):
sys.path.insert(0, possible_topdir)
from muranorepository import config
@ -43,7 +43,7 @@ LOG = log.getLogger(__name__)
def main():
dev_conf = os.path.join(possible_topdir,
'etc',
'muranorepository.conf')
'murano-repository.conf')
config_files = None
if os.path.exists(dev_conf):
config_files = [dev_conf]

View File

@ -20,4 +20,4 @@ HEAT = 'heat'
AGENT = 'agent'
SCRIPTS = 'scripts'
DATA_TYPES = [UI, WORKFLOW, HEAT, AGENT, SCRIPTS, MANIFEST]
DATA_TYPES = [UI, WORKFLOW, HEAT, AGENT, SCRIPTS, MANIFEST]

View File

@ -16,4 +16,4 @@
class Manifest(object):
def __init__(self, initial_data):
for key in initial_data:
setattr(self, key, initial_data[key])
setattr(self, key, initial_data[key])

View File

@ -104,8 +104,7 @@ def _listen(host, start_port, end_port, listen_func):
try:
return listen_func((host, try_port))
except socket.error as exc:
if (exc.errno != errno.EADDRINUSE or
try_port >= end_port):
if (exc.errno != errno.EADDRINUSE or try_port >= end_port):
raise
try_port += 1

View File

@ -425,7 +425,8 @@ def _setup_logging_from_conf():
if CONF.publish_errors:
handler = importutils.import_object(
"muranorepository.openstack.common.log_handler.PublishErrorsHandler",
"muranorepository.openstack.common.log_handler."
"PublishErrorsHandler",
logging.ERROR)
log_root.addHandler(handler)

View File

@ -29,7 +29,8 @@ class BaseTestCase(testtools.TestCase):
super(BaseTestCase, self).setUp()
self._set_timeout()
self._fake_output()
self.useFixture(fixtures.FakeLogger('muranorepository.openstack.common'))
self.useFixture(
fixtures.FakeLogger('muranorepository.openstack.common'))
self.useFixture(fixtures.NestedTempfile())
def _set_timeout(self):

View File

View File

@ -0,0 +1,42 @@
# Copyright (c) 2013 Mirantis Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
MANIFEST_FILE = ''
'version: 0.1'
'service_display_name: Test'
'description: >-'
' <strong> This goes a description'
'full_service_name: test_service'
'author: Mirantis Inc.'
'service_version: 1.0'
'enabled: True'
'ui:'
' - test1.yaml'
'workflows:'
' - test1.xml'
'heat:'
' - Windows.template'
'agents:'
' - test1.template'
'scripts:'
' - test1.sh'

View File

@ -0,0 +1,28 @@
[DEFAULT]
# Address to bind the server to
host = localhost
#Port the bind the server to
port = 5000
# Provide information about data types
# absolute path to manifest location(root directory)
#manifests = /home/fervent/Projects/my_repo/muranorepository/tests/var
manifests = /bin/server
# Parameter name corresponds to section in manifest file
# Parameter value corresponds to relative path to data type
ui = ui
workflows = workflows
heat = heat
agent = agent
scripts = scripts
# Configure archive structure
# data_type = desired folder
[output]
ui = service_forms
workflows = workflows
heat = templates/cf
agent = templates/agent
scripts = templates/agent/script

View File

@ -0,0 +1,87 @@
import sys
import os
from flask.ext.testing import TestCase as FlaskTestCase
import shutil
from StringIO import StringIO
import mockfs
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(__file__),
os.pardir,
os.pardir,
os.pardir))
if os.path.exists(os.path.join(possible_topdir,
'muranorepository',
'__init__.py')):
sys.path.insert(0, possible_topdir)
from muranorepository.consts import MANIFEST
from muranorepository.tests.fixtures.consts import MANIFEST_FILE
from muranorepository import config
from muranorepository.main import make_app
class TestAdminAPI(FlaskTestCase):
url = "/v1/admin/{0}"
url_with_path = "/v1/admin/{0}/{1}"
def create_app(self):
test_app = make_app()
test_app.config['TESTING'] = True
return test_app
def setUp(self):
config_files = [os.path.join(possible_topdir,
'muranorepository',
'tests',
'test.conf')]
config.parse_configs(None, config_files)
self.mfs = mockfs.replace_builtins()
self.mfs.add_entries(
{
'/bin/server': {
'test-manifest.yaml': MANIFEST_FILE,
'ui': {'test1.yaml': ''},
'heat':
{'Windows.template': '',
'folder_to_delete': {}
}
}
})
def tearDown(self):
mockfs.restore_builtins()
def test_list_manifests(self):
response = self.client.get(self.url.format(MANIFEST))
expected_result = {MANIFEST: ['test-manifest.yaml']}
self.assert200(response)
self.assertEquals(response.json, expected_result)
def test_list_ui(self):
response = self.client.get(self.url.format('ui'))
expected_result = {'ui': ['test1.yaml']}
self.assert200(response)
self.assertEquals(response.json, expected_result)
def test_create_ui_subfolder(self):
response = self.client.put(self.url_with_path.format('ui', 'test'))
expected_result = {'result': 'success'}
self.assert200(response)
self.assertEquals(response.json, expected_result)
shutil.rmtree('bin/server/ui/test')
def test_delete_heat_subfolder(self):
url = self.url_with_path.format('heat',
'folder_to_delete')
response = self.client.delete(url)
self.assert200(response)
expected_result = {'result': 'success'}
self.assertEquals(response.json, expected_result)
def test_upload_ui_file(self):
upload_data = {'file': (StringIO('content'), 'test.yaml')}
response = self.client.post(self.url.format('ui'),
data=upload_data)
'test.yaml' in os.listdir('bin/server/ui')
self.assert200(response)
expected_result = {'result': 'success'}
self.assertEquals(response.json, expected_result)

View File

@ -10,4 +10,4 @@
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# under the License.

View File

@ -18,16 +18,44 @@ import shutil
import logging as log
from oslo.config import cfg
from muranorepository.consts import DATA_TYPES
OUTPUT_CONF = cfg.CONF.output
CONF = cfg.CONF
class Archiver(object):
def _copy_data(self, file_lists, src, dst):
if not os.path.exists(dst):
os.makedirs(dst)
for path in file_lists:
source = os.path.join(src, path)
destination = os.path.join(dst, path)
base_dir = os.path.dirname(destination)
if (base_dir != dst) and (not os.path.exists(base_dir)):
os.makedirs(os.path.dirname(destination))
try:
shutil.copyfile(source, destination)
except IOError:
log.error("Unable to copy file "
"{0}".format(file))
def _compose_archive(self, path):
target_archive = "service_metadata.tar"
with tarfile.open(target_archive, "w") as tar:
for item in os.listdir(path):
tar.add(os.path.join(path, item), item)
try:
shutil.rmtree(path, ignore_errors=True)
except Exception as e:
log.error("Unable to delete temp directory: {0}".format(e))
return os.path.abspath(target_archive)
def create(self, manifests, *types):
"""
manifests -- list of Manifest objects
*types - desired data types to be added to archive
return: absolute path to created archive
"""
temp_dir = tempfile.mkdtemp()
for data_type in types:
@ -38,44 +66,18 @@ class Archiver(object):
for manifest in manifests:
if not manifest.enabled and not manifest.valid:
continue
if hasattr(manifest, data_type):
file_list = getattr(manifest, data_type)
dst_directory = os.path.join(temp_dir,
getattr(OUTPUT_CONF,
getattr(CONF.output,
data_type))
scr_directory = os.path.join(CONF.manifests,
getattr(CONF, data_type))
if not os.path.exists(dst_directory):
os.makedirs(dst_directory)
for path in getattr(manifest, data_type):
source = os.path.join(scr_directory, path)
destination = os.path.join(dst_directory, path)
base_dir = os.path.dirname(destination)
if (base_dir != dst_directory) \
and (not os.path.exists(base_dir)):
os.makedirs(os.path.dirname(destination))
try:
shutil.copyfile(source, destination)
except IOError:
log.error("Unable to copy file "
"{0}".format(file))
self._copy_data(file_list, scr_directory, dst_directory)
else:
log.info(
"Manifest for {0} service has no file definitions for "
"{1}".format(manifest.service_display_name, data_type))
target_archive = "service_metadata.tar"
with tarfile.open(target_archive, "w") as tar:
for item in os.listdir(temp_dir):
tar.add(os.path.join(temp_dir, item), item)
try:
shutil.rmtree(temp_dir, ignore_errors=True)
except Exception as e:
log.error("Unable to delete temp directory: {0}".format(e))
return os.path.abspath(target_archive)
return self._compose_archive(temp_dir)

View File

@ -32,7 +32,8 @@ author-email = openstack-dev@lists.openstack.org
home-page = https://launchpad.net/murano
[global]
setup-hooks = pbr.hooks.setup_hook
setup-hooks =
pbr.hooks.setup_hook
[files]
packages =
@ -54,8 +55,9 @@ tag_date = 0
tag_svn_revision = 0
[compile_catalog]
directory = murano-repository/locale
domain = murano-repository
directory = muranorepository/locale
domain = muranorepository
[extract_messages]
keywords = _ gettext ngettext l_ lazy_gettext

View File

@ -2,16 +2,11 @@
pep8==1.4.5
pyflakes>=0.7.2,<0.7.4
flake8==2.0
hacking>=0.5.6,<0.8
coverage>=3.6
flask-testing
docutils==0.9.1
fixtures>=0.3.14
mock>=1.0
nose
openstack.nose_plugin>=0.7
mockfs
oslo.sphinx
pylint==0.25.2
sphinx>=1.1.2
sphinxcontrib-httpdomain
unittest2

View File

@ -1,69 +0,0 @@
#!/usr/bin/env bash
print_hint() {
echo "Try \`${0##*/} --help' for more information." >&2
}
PARSED_OPTIONS=$(getopt -n "${0##*/}" -o hb:p:o: \
--long help,base-dir:,package-name:,output-dir: -- "$@")
if [ $? != 0 ] ; then print_hint ; exit 1 ; fi
eval set -- "$PARSED_OPTIONS"
while true; do
case "$1" in
-h|--help)
echo "${0##*/} [options]"
echo ""
echo "options:"
echo "-h, --help show brief help"
echo "-b, --base-dir=DIR Project base directory (required)"
echo "-p, --package-name=NAME Project package name"
echo "-o, --output-dir=DIR File output directory"
exit 0
;;
-b|--base-dir)
shift
BASEDIR=`echo $1 | sed -e 's/\/*$//g'`
shift
;;
-p|--package-name)
shift
PACKAGENAME=`echo $1`
shift
;;
-o|--output-dir)
shift
OUTPUTDIR=`echo $1 | sed -e 's/\/*$//g'`
shift
;;
--)
break
;;
esac
done
if [ -z $BASEDIR ] || ! [ -d $BASEDIR ]
then
echo "${0##*/}: missing project base directory" >&2 ; print_hint ; exit 1
fi
PACKAGENAME=${PACKAGENAME:-${BASEDIR##*/}}
OUTPUTDIR=${OUTPUTDIR:-$BASEDIR/etc}
if ! [ -d $OUTPUTDIR ]
then
echo "${0##*/}: cannot access \`$OUTPUTDIR': No such file or directory" >&2
exit 1
fi
BASEDIRESC=`echo $BASEDIR | sed -e 's/\//\\\\\//g'`
FILES=$(find $BASEDIR/$PACKAGENAME -type f -name "*.py" ! -path "*/tests/*" \
-exec grep -l "Opt(" {} + | sed -e "s/^$BASEDIRESC\///g" | sort -u)
export EVENTLET_NO_GREENDNS=yes
MODULEPATH=murano-repository.openstack.common.config.generator
OUTPUTFILE=$OUTPUTDIR/$PACKAGENAME.conf.sample
python -m $MODULEPATH $FILES > $OUTPUTFILE

46
tox.ini
View File

@ -10,49 +10,29 @@ setenv = VIRTUAL_ENV={envdir}
NOSE_OPENSTACK_SHOW_ELAPSED=1
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands = nosetests
[testenv:pep8]
deps = pep8==1.3.3
commands = pep8 --repeat --show-source murano-repository setup.py
[testenv:venv]
commands = {posargs}
[testenv:cover]
commands = nosetests --cover-erase --cover-package=murano-repository --with-xcoverage
[tox:jenkins]
downloadcache = ~/cache/pip
[testenv:jenkins26]
basepython = python2.6
setenv = NOSE_WITH_XUNIT=1
deps = file://{toxinidir}/.cache.bundle
[testenv:jenkins27]
basepython = python2.7
setenv = NOSE_WITH_XUNIT=1
deps = file://{toxinidir}/.cache.bundle
[testenv:jenkinscover]
deps = file://{toxinidir}/.cache.bundle
setenv = NOSE_WITH_XUNIT=1
commands = nosetests --cover-erase --cover-package=muranoapi --with-xcoverage
[testenv:jenkinsvenv]
deps = file://{toxinidir}/.cache.bundle
setenv = NOSE_WITH_XUNIT=1
commands = {posargs}
commands = pep8 --repeat --show-source muranorepository setup.py
[testenv:pyflakes]
deps = flake8
commands = flake8
[testenv:venv]
commands = {posargs}
[testenv:cover]
commands = nosetests --cover-erase --cover-package=muranorepository --with-xcoverage
[tox:jenkins]
downloadcache = ~/cache/pip
[flake8]
# H301 one import per line
# H302 import only modules
ignore = H301,H302
# H201 no 'except:' at least use 'except Exception:'
ignore = H301,H302,F403,H201
show-source = true
builtins = _
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,tools
exclude=.build,.venv,.git,.tox,dist,doc,*/openstack,*lib/python*,*egg,tools