Environments operations handled in the fuel2
Requirement on package six added. Detailed error description propagated for 404 errors. Parse of comma separated client params implemented. Delete and patch operation added to http client. Change-Id: Id30358f9358047a0a6f6fd23341ab270edfa5874
This commit is contained in:
parent
d43e782421
commit
ca3a669c2d
|
@ -10,3 +10,4 @@ alembic
|
|||
cliff
|
||||
requests
|
||||
keystonemiddleware>=4.0.0,!=4.1.0,!=4.5.0
|
||||
six>=1.9.0
|
10
setup.cfg
10
setup.cfg
|
@ -52,10 +52,20 @@ tuning_box.cli =
|
|||
get = tuning_box.cli.resources:Get
|
||||
set = tuning_box.cli.resources:Set
|
||||
override = tuning_box.cli.resources:Override
|
||||
env_create = tuning_box.cli.environments:CreateEnvironment
|
||||
env_list = tuning_box.cli.environments:ListEnvironments
|
||||
env_show = tuning_box.cli.environments:ShowEnvironment
|
||||
env_delete = tuning_box.cli.environments:DeleteEnvironment
|
||||
env_update = tuning_box.cli.environments:UpdateEnvironment
|
||||
fuelclient =
|
||||
config_get = tuning_box.fuelclient:Get
|
||||
config_set = tuning_box.fuelclient:Set
|
||||
config_override = tuning_box.fuelclient:Override
|
||||
config_env_create = tuning_box.fuelclient:CreateEnvironment
|
||||
config_env_list = tuning_box.fuelclient:ListEnvironments
|
||||
config_env_show = tuning_box.fuelclient:ShowEnvironment
|
||||
config_env_delete = tuning_box.fuelclient:DeleteEnvironment
|
||||
config_env_update = tuning_box.fuelclient:UpdateEnvironment
|
||||
console_scripts =
|
||||
tuningbox_db_upgrade = tuning_box.migration:upgrade
|
||||
tuningbox_db_downgrade = tuning_box.migration:downgrade
|
||||
|
|
|
@ -14,6 +14,7 @@ import json
|
|||
import yaml
|
||||
|
||||
from cliff import command
|
||||
import six
|
||||
|
||||
|
||||
def level_converter(value):
|
||||
|
@ -53,9 +54,18 @@ def format_output(output, format_):
|
|||
|
||||
|
||||
class BaseCommand(command.Command):
|
||||
|
||||
def get_client(self):
|
||||
return self.app.client
|
||||
|
||||
def _parse_comma_separated(self, parsed_args, param_name, cast_to):
|
||||
param = getattr(parsed_args, param_name)
|
||||
if param is None or param == '[]':
|
||||
return []
|
||||
result = six.moves.map(six.text_type.strip,
|
||||
six.text_type(param).split(','))
|
||||
return list(six.moves.map(cast_to, result))
|
||||
|
||||
|
||||
class FormattedCommand(BaseCommand):
|
||||
format_choices = ('json', 'yaml', 'plain')
|
||||
|
@ -63,7 +73,7 @@ class FormattedCommand(BaseCommand):
|
|||
def get_parser(self, *args, **kwargs):
|
||||
parser = super(FormattedCommand, self).get_parser(*args, **kwargs)
|
||||
parser.add_argument(
|
||||
'--format',
|
||||
'-f', '--format',
|
||||
choices=self.format_choices,
|
||||
default='json',
|
||||
help="Desired format for return value",
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
# 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.
|
||||
|
||||
import six
|
||||
|
||||
from tuning_box.cli import base
|
||||
from tuning_box.cli.base import BaseCommand
|
||||
|
||||
|
||||
class ListEnvironments(base.FormattedCommand, BaseCommand):
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
return self.get_client().get('/environments')
|
||||
|
||||
|
||||
class CreateEnvironment(base.FormattedCommand, BaseCommand):
|
||||
|
||||
def get_parser(self, *args, **kwargs):
|
||||
parser = super(CreateEnvironment, self).get_parser(
|
||||
*args, **kwargs)
|
||||
parser.add_argument(
|
||||
'-c', '--components',
|
||||
type=str,
|
||||
help="Comma separated components IDs",
|
||||
)
|
||||
parser.add_argument(
|
||||
'-l', '--levels',
|
||||
type=str,
|
||||
help="Comma separated levels names",
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
levels = self._parse_comma_separated(
|
||||
parsed_args, 'levels', six.text_type)
|
||||
components = self._parse_comma_separated(
|
||||
parsed_args, 'components', int)
|
||||
res = self.get_client().post(
|
||||
'/environments',
|
||||
{'hierarchy_levels': levels, 'components': components}
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
class EnvironmentCommand(base.FormattedCommand, BaseCommand):
|
||||
|
||||
def get_parser(self, *args, **kwargs):
|
||||
parser = super(EnvironmentCommand, self).get_parser(*args, **kwargs)
|
||||
parser.add_argument('id', type=int, help='Id of the environment')
|
||||
return parser
|
||||
|
||||
|
||||
class ShowEnvironment(EnvironmentCommand):
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
return self.get_client().get('/environments/{0}'.format(
|
||||
parsed_args.id))
|
||||
|
||||
|
||||
class DeleteEnvironment(EnvironmentCommand):
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
return self.get_client().delete('/environments/{0}'.format(
|
||||
parsed_args.id))
|
||||
|
||||
|
||||
class UpdateEnvironment(EnvironmentCommand):
|
||||
|
||||
def get_parser(self, *args, **kwargs):
|
||||
parser = super(UpdateEnvironment, self).get_parser(
|
||||
*args, **kwargs)
|
||||
parser.add_argument(
|
||||
'-c', '--components',
|
||||
dest='components',
|
||||
type=str,
|
||||
help="Comma separated components IDs. "
|
||||
"Set parameter to [] if you want to pass empty list",
|
||||
)
|
||||
parser.add_argument(
|
||||
'-l', '--levels',
|
||||
type=str,
|
||||
dest='levels',
|
||||
help="Comma separated levels names "
|
||||
"Set parameter to [] if you want to pass empty list",
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
data = {}
|
||||
if parsed_args.levels is not None:
|
||||
data['hierarchy_levels'] = self._parse_comma_separated(
|
||||
parsed_args, 'levels', six.text_type)
|
||||
if parsed_args.components is not None:
|
||||
data['components'] = self._parse_comma_separated(
|
||||
parsed_args, 'components', int)
|
||||
|
||||
return self.get_client().patch(
|
||||
'/environments/{0}'.format(parsed_args.id),
|
||||
data
|
||||
)
|
|
@ -32,7 +32,6 @@ class HTTPClient(object):
|
|||
def request(self, method, url, **kwargs):
|
||||
full_url = self.base_url + url
|
||||
resp = self.session.request(method, full_url, **kwargs)
|
||||
resp.raise_for_status()
|
||||
if resp.headers.get('Content-Type') == 'application/json' and \
|
||||
resp.content:
|
||||
return resp.json()
|
||||
|
@ -44,3 +43,12 @@ class HTTPClient(object):
|
|||
|
||||
def put(self, url, body):
|
||||
return self.request('PUT', url, json=body)
|
||||
|
||||
def post(self, url, body):
|
||||
return self.request('POST', url, json=body)
|
||||
|
||||
def patch(self, url, body):
|
||||
return self.request('PATCH', url, json=body)
|
||||
|
||||
def delete(self, url):
|
||||
return self.request('DELETE', url)
|
||||
|
|
|
@ -14,7 +14,6 @@ import functools
|
|||
import json
|
||||
import re
|
||||
|
||||
import flask
|
||||
import flask_sqlalchemy
|
||||
import sqlalchemy
|
||||
import sqlalchemy.event
|
||||
|
@ -56,7 +55,9 @@ class BaseQuery(flask_sqlalchemy.BaseQuery):
|
|||
else:
|
||||
result = self.filter_by(name=id_or_name).one_or_none()
|
||||
if fail_on_none and result is None:
|
||||
flask.abort(404)
|
||||
raise errors.TuningboxNotFound(
|
||||
"Object not found by name or id {0}".format(id_or_name)
|
||||
)
|
||||
return result
|
||||
|
||||
# one_or_none is not present in sqlalchemy < 1.0.9
|
||||
|
|
|
@ -17,7 +17,8 @@ from fuelclient import client as fc_client
|
|||
|
||||
from tuning_box import cli
|
||||
from tuning_box.cli import base as cli_base
|
||||
import tuning_box.cli.resources
|
||||
from tuning_box.cli import environments
|
||||
from tuning_box.cli import resources
|
||||
from tuning_box import client as tb_client
|
||||
|
||||
|
||||
|
@ -42,15 +43,35 @@ class FuelBaseCommand(cli_base.BaseCommand):
|
|||
return FuelHTTPClient()
|
||||
|
||||
|
||||
class Get(FuelBaseCommand, tuning_box.cli.resources.Get):
|
||||
class Get(FuelBaseCommand, resources.Get):
|
||||
pass
|
||||
|
||||
|
||||
class Set(FuelBaseCommand, tuning_box.cli.resources.Set):
|
||||
class Set(FuelBaseCommand, resources.Set):
|
||||
pass
|
||||
|
||||
|
||||
class Override(FuelBaseCommand, tuning_box.cli.resources.Override):
|
||||
class Override(FuelBaseCommand, resources.Override):
|
||||
pass
|
||||
|
||||
|
||||
class CreateEnvironment(FuelBaseCommand, environments.CreateEnvironment):
|
||||
pass
|
||||
|
||||
|
||||
class ListEnvironments(FuelBaseCommand, environments.ListEnvironments):
|
||||
pass
|
||||
|
||||
|
||||
class ShowEnvironment(FuelBaseCommand, environments.ShowEnvironment):
|
||||
pass
|
||||
|
||||
|
||||
class DeleteEnvironment(FuelBaseCommand, environments.DeleteEnvironment):
|
||||
pass
|
||||
|
||||
|
||||
class UpdateEnvironment(FuelBaseCommand, environments.UpdateEnvironment):
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ class EnvironmentsCollection(flask_restful.Resource):
|
|||
method_decorators = [flask_restful.marshal_with(environment_fields)]
|
||||
|
||||
def get(self):
|
||||
envs = db.Environment.query.all()
|
||||
envs = db.Environment.query.order_by(db.Environment.id).all()
|
||||
result = []
|
||||
for env in envs:
|
||||
hierarchy_levels = db.EnvironmentHierarchyLevel.\
|
||||
|
@ -132,8 +132,8 @@ class Environment(flask_restful.Resource):
|
|||
def put(self, environment_id):
|
||||
return self.patch(environment_id)
|
||||
|
||||
def patch(self, env_id):
|
||||
self._perform_update(env_id)
|
||||
def patch(self, environment_id):
|
||||
self._perform_update(environment_id)
|
||||
return None, 204
|
||||
|
||||
@db.with_transaction
|
||||
|
|
|
@ -0,0 +1,186 @@
|
|||
# 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.
|
||||
|
||||
import testscenarios
|
||||
|
||||
from tuning_box.tests.cli import _BaseCLITest
|
||||
|
||||
|
||||
class TestCreateEnvironment(testscenarios.WithScenarios, _BaseCLITest):
|
||||
scenarios = [
|
||||
(s[0], dict(zip(('mock_url', 'args', 'expected_result'), s[1])))
|
||||
for s in [
|
||||
('json', ('/environments',
|
||||
'env create --levels lvl1 --components 1 --format json',
|
||||
'{}')),
|
||||
('yaml', ('/environments',
|
||||
'env create --levels lvl1,lvl2 --components 1 '
|
||||
'--format yaml',
|
||||
'{}\n')),
|
||||
('plain', ('/environments',
|
||||
'env create --levels lvl1,lvl2 --components 1,2,3 '
|
||||
'--format plain',
|
||||
'{}')),
|
||||
('plain', ('/environments',
|
||||
'env create '
|
||||
'--format plain',
|
||||
'{}')),
|
||||
('json', ('/environments',
|
||||
'env create -l lvl1 -c 1 -f json',
|
||||
'{}')),
|
||||
('yaml', ('/environments',
|
||||
'env create -l lvl1,lvl2 -c 1 -f yaml',
|
||||
'{}\n')),
|
||||
('plain', ('/environments',
|
||||
'env create -l lvl1,lvl2 -c 1,2,3 -f plain',
|
||||
'{}')),
|
||||
('plain', ('/environments',
|
||||
'env create -f plain',
|
||||
'{}'))
|
||||
]
|
||||
]
|
||||
|
||||
mock_url = None
|
||||
args = None
|
||||
expected_result = None
|
||||
|
||||
def test_post(self):
|
||||
self.req_mock.post(
|
||||
self.BASE_URL + self.mock_url,
|
||||
headers={'Content-Type': 'application/json'},
|
||||
json={},
|
||||
)
|
||||
self.cli.run(self.args.split())
|
||||
self.assertEqual(self.expected_result, self.cli.stdout.getvalue())
|
||||
|
||||
|
||||
class TestListEnvironments(testscenarios.WithScenarios, _BaseCLITest):
|
||||
|
||||
scenarios = [
|
||||
(s[0], dict(zip(('mock_url', 'args', 'expected_result'), s[1])))
|
||||
for s in [
|
||||
('json', ('/environments', 'env list', '{}')),
|
||||
('yaml', ('/environments', 'env list --format yaml', '{}\n')),
|
||||
('plain', ('/environments', 'env list --format plain', '{}'))
|
||||
]
|
||||
]
|
||||
mock_url = None
|
||||
args = None
|
||||
expected_result = None
|
||||
|
||||
def test_get(self):
|
||||
self.req_mock.get(
|
||||
self.BASE_URL + self.mock_url,
|
||||
headers={'Content-Type': 'application/json'},
|
||||
json={},
|
||||
)
|
||||
self.cli.run(self.args.split())
|
||||
self.assertEqual(self.expected_result, self.cli.stdout.getvalue())
|
||||
|
||||
|
||||
class TestShowEnvironment(testscenarios.WithScenarios, _BaseCLITest):
|
||||
|
||||
scenarios = [
|
||||
(s[0], dict(zip(('mock_url', 'args', 'expected_result'), s[1])))
|
||||
for s in [
|
||||
('json', ('/environments/9', 'env show 9 --format json', '{}')),
|
||||
('yaml', ('/environments/9', 'env show 9 --format yaml', '{}\n')),
|
||||
('plain', ('/environments/9', 'env show 9 --format plain', '{}'))
|
||||
]
|
||||
]
|
||||
mock_url = None
|
||||
args = None
|
||||
expected_result = None
|
||||
|
||||
def test_get(self):
|
||||
self.req_mock.get(
|
||||
self.BASE_URL + self.mock_url,
|
||||
headers={'Content-Type': 'application/json'},
|
||||
json={},
|
||||
)
|
||||
self.cli.run(self.args.split())
|
||||
self.assertEqual(self.expected_result, self.cli.stdout.getvalue())
|
||||
|
||||
|
||||
class TestDeleteEnvironment(testscenarios.WithScenarios, _BaseCLITest):
|
||||
|
||||
scenarios = [
|
||||
(s[0], dict(zip(('mock_url', 'args', 'expected_result'), s[1])))
|
||||
for s in [
|
||||
('json', ('/environments/9', 'env delete 9 --format json',
|
||||
'{}')),
|
||||
('yaml', ('/environments/9', 'env delete 9 --format yaml',
|
||||
'{}\n')),
|
||||
('plain', ('/environments/9', 'env delete 9 --format plain',
|
||||
'{}'))
|
||||
]
|
||||
]
|
||||
mock_url = None
|
||||
args = None
|
||||
expected_result = None
|
||||
|
||||
def test_delete(self):
|
||||
self.req_mock.delete(
|
||||
self.BASE_URL + self.mock_url,
|
||||
headers={'Content-Type': 'application/json'},
|
||||
json={}
|
||||
)
|
||||
self.cli.run(self.args.split())
|
||||
self.assertEqual(self.expected_result, self.cli.stdout.getvalue())
|
||||
|
||||
|
||||
class TestUpdateEnvironment(testscenarios.WithScenarios, _BaseCLITest):
|
||||
|
||||
scenarios = [
|
||||
(s[0], dict(zip(('mock_url', 'args', 'expected_result'), s[1])))
|
||||
for s in [
|
||||
('json', ('/environments/9',
|
||||
'env update 9 -f json',
|
||||
'{}')),
|
||||
('json', ('/environments/9',
|
||||
'env update 9 -l lvl1 -f json',
|
||||
'{}')),
|
||||
('yaml', ('/environments/9',
|
||||
'env update 9 -l lvl1,lvl2 -f yaml',
|
||||
'{}\n')),
|
||||
('plain', ('/environments/9',
|
||||
'env update 9 -l lvl1,lvl2 -c 1 -f plain',
|
||||
'{}')),
|
||||
('json', ('/environments/9',
|
||||
'env update 9 -l lvl1,lvl2 -c 1,2 -f json',
|
||||
'{}')),
|
||||
('json', ('/environments/9',
|
||||
'env update 9 -l [] -c 1,2 -f json',
|
||||
'{}')),
|
||||
('json', ('/environments/9',
|
||||
'env update 9 -l [] -c [] -f json',
|
||||
'{}')),
|
||||
('json', ('/environments/9',
|
||||
'env update 9 --levels [] --components [] --format json',
|
||||
'{}')),
|
||||
('json', ('/environments/9',
|
||||
'env update 9 --levels a,b --components 1,2',
|
||||
'{}')),
|
||||
]
|
||||
]
|
||||
mock_url = None
|
||||
args = None
|
||||
expected_result = None
|
||||
|
||||
def test_update(self):
|
||||
self.req_mock.patch(
|
||||
self.BASE_URL + self.mock_url,
|
||||
headers={'Content-Type': 'application/json'},
|
||||
json={}
|
||||
)
|
||||
self.cli.run(self.args.split())
|
||||
self.assertEqual(self.expected_result, self.cli.stdout.getvalue())
|
|
@ -14,6 +14,7 @@ import os
|
|||
|
||||
# oslo_db internals refuse to work properly if this is not set
|
||||
# actual file name in that URL doesn't matter, it'll be generated by oslo.db
|
||||
|
||||
os.environ.setdefault("OS_TEST_DBAPI_ADMIN_CONNECTION", "sqlite:///testdb")
|
||||
|
||||
from alembic import command as alembic_command
|
||||
|
@ -23,9 +24,9 @@ from oslo_db.sqlalchemy import test_base
|
|||
from oslo_db.sqlalchemy import test_migrations
|
||||
import sqlalchemy as sa
|
||||
import testscenarios
|
||||
from werkzeug import exceptions
|
||||
|
||||
from tuning_box import db
|
||||
from tuning_box import errors
|
||||
from tuning_box import migration
|
||||
from tuning_box.tests import base
|
||||
|
||||
|
@ -79,14 +80,14 @@ class TestGetByIdOrName(_DBTestCase):
|
|||
|
||||
def test_by_id_fail(self):
|
||||
self.assertRaises(
|
||||
exceptions.NotFound,
|
||||
errors.TuningboxNotFound,
|
||||
db.Component.query.get_by_id_or_name,
|
||||
self.component.id + 1,
|
||||
)
|
||||
|
||||
def test_by_name_fail(self):
|
||||
self.assertRaises(
|
||||
exceptions.NotFound,
|
||||
errors.TuningboxNotFound,
|
||||
db.Component.query.get_by_id_or_name,
|
||||
self.component.name + "_",
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue