Resource definition operations handled in the fuel2

Common part of implementation for operations extracted to cli.base.

Change-Id: Iaad6e1e4655e5e60b7a97af4d6b07528b6579e65
This commit is contained in:
Alexander Kislitsky 2016-08-24 18:52:26 +03:00
parent 76867c6aa4
commit a175c31291
7 changed files with 413 additions and 27 deletions

View File

@ -62,6 +62,11 @@ tuning_box.cli =
comp_show = tuning_box.cli.components:ShowComponent
comp_delete = tuning_box.cli.components:DeleteComponent
comp_update = tuning_box.cli.components:UpdateComponent
def_create = tuning_box.cli.resource_definitions:CreateResourceDefinition
def_list = tuning_box.cli.resource_definitions:ListResourceDefinitions
def_show = tuning_box.cli.resource_definitions:ShowResourceDefinition
def_delete = tuning_box.cli.resource_definitions:DeleteResourceDefinition
def_update = tuning_box.cli.resource_definitions:UpdateResourceDefinition
fuelclient =
config_get = tuning_box.fuelclient:Get
config_set = tuning_box.fuelclient:Set
@ -76,6 +81,11 @@ fuelclient =
config_comp_show = tuning_box.fuelclient:ShowComponent
config_comp_delete = tuning_box.fuelclient:DeleteComponent
config_comp_update = tuning_box.fuelclient:UpdateComponent
config_def_create = tuning_box.fuelclient:CreateResourceDefinition
config_def_list = tuning_box.fuelclient:ListResourceDefinitions
config_def_show = tuning_box.fuelclient:ShowResourceDefinition
config_def_delete = tuning_box.fuelclient:DeleteResourceDefinition
config_def_update = tuning_box.fuelclient:UpdateResourceDefinition
console_scripts =
tuningbox_db_upgrade = tuning_box.migration:upgrade
tuningbox_db_downgrade = tuning_box.migration:downgrade

View File

@ -15,6 +15,10 @@ import json
import yaml
from cliff import command
from cliff import lister
from cliff import show
from fuelclient.cli import error as fc_error
from fuelclient.common import data_utils
import six
@ -67,6 +71,18 @@ class BaseCommand(command.Command):
six.text_type(param).split(','))
return list(six.moves.map(cast_to, result))
def read_json(self):
return json.load(self.app.stdin)
def read_yaml(self):
docs_gen = yaml.safe_load_all(self.app.stdin)
doc = next(docs_gen)
guard = object()
if next(docs_gen, guard) is not guard:
self.app.stderr.write("Warning: will use only first "
"document from YAML stream")
return doc
class BaseOneCommand(BaseCommand):
@ -106,6 +122,28 @@ class BaseDeleteCommand(BaseOneCommand):
return self.get_deletion_message(parsed_args)
class BaseListCommand(BaseCommand, lister.Lister):
def take_action(self, parsed_args):
result = self.get_client().get(self.base_url)
try:
data = data_utils.get_display_data_multi(self.columns, result)
return self.columns, data
except fc_error.BadDataException:
return zip(*result.items())
class BaseShowCommand(BaseOneCommand, show.ShowOne):
def take_action(self, parsed_args):
result = self.get_client().get(self.get_url(parsed_args))
try:
data = data_utils.get_display_data_single(self.columns, result)
return self.columns, data
except fc_error.BadDataException:
return zip(*result.items())
class FormattedCommand(BaseCommand):
format_choices = ('json', 'yaml', 'plain')

View File

@ -10,10 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from cliff import lister
from cliff import show
from fuelclient.cli import error as fc_error
from fuelclient.common import data_utils
from tuning_box.cli import base
@ -24,15 +21,16 @@ class ComponentsCommand(base.BaseCommand):
columns = ('id', 'name', 'resource_definitions')
class ListComponents(ComponentsCommand, lister.Lister):
class ListComponents(ComponentsCommand, base.BaseListCommand):
pass
def take_action(self, parsed_args):
result = self.get_client().get(self.base_url)
try:
data = data_utils.get_display_data_multi(self.columns, result)
return self.columns, data
except fc_error.BadDataException:
return zip(*result.items())
class ShowComponent(ComponentsCommand, base.BaseShowCommand):
pass
class DeleteComponent(ComponentsCommand, base.BaseDeleteCommand):
pass
class CreateComponent(ComponentsCommand, show.ShowOne):
@ -49,26 +47,11 @@ class CreateComponent(ComponentsCommand, show.ShowOne):
def take_action(self, parsed_args):
result = self.get_client().post(
'/components', {'name': parsed_args.name,
self.base_url, {'name': parsed_args.name,
'resource_definitions': []})
return zip(*result.items())
class ShowComponent(ComponentsCommand, base.BaseOneCommand, show.ShowOne):
def take_action(self, parsed_args):
result = self.get_client().get(self.get_url(parsed_args))
try:
data = data_utils.get_display_data_single(self.columns, result)
return self.columns, data
except fc_error.BadDataException:
return zip(*result.items())
class DeleteComponent(ComponentsCommand, base.BaseDeleteCommand):
pass
class UpdateComponent(ComponentsCommand, base.BaseOneCommand):
def get_parser(self, *args, **kwargs):

23
tuning_box/cli/errors.py Normal file
View File

@ -0,0 +1,23 @@
# 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.
class TuningBoxCliError(Exception):
pass
class IncompatibleParams(TuningBoxCliError):
pass
class UnsupportedDataType(TuningBoxCliError):
pass

View File

@ -0,0 +1,147 @@
# 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 json
import yaml
from cliff import show
from tuning_box.cli import base
from tuning_box.cli import errors
class ResourceDefinitionsCommand(base.BaseCommand):
entity_name = 'resource_definition'
base_url = '/resource_definitions'
columns = ('id', 'name', 'component_id', 'content')
class ListResourceDefinitions(ResourceDefinitionsCommand,
base.BaseListCommand):
pass
class ShowResourceDefinition(ResourceDefinitionsCommand,
base.BaseShowCommand):
pass
class DeleteResourceDefinition(ResourceDefinitionsCommand,
base.BaseDeleteCommand):
pass
class ModifyResourceDefinitionCommand(ResourceDefinitionsCommand):
def get_parser(self, *args, **kwargs):
parser = super(ModifyResourceDefinitionCommand, self).get_parser(
*args, **kwargs)
parser.add_argument(
'-n', '--name',
dest='name',
type=str,
help="Resource definition name"
)
parser.add_argument(
'-i', '--component-id',
dest='component_id',
type=int,
help="Component Id"
)
parser.add_argument(
'-p', '--content',
dest='content',
type=str,
help="Content to be set"
)
parser.add_argument(
'-t', '--type',
choices=('json', 'yaml'),
help="Content type"
)
parser.add_argument(
'-d', '--data-format',
dest='data_format',
choices=('json', 'yaml'),
help="Format of data passed to stdin to be set to content"
)
return parser
def verify_arguments(self, parsed_args):
if parsed_args.content is not None:
if parsed_args.data_format is not None:
raise errors.IncompatibleParams(
"You shouldn't specify --data-format if you pass "
"content in command line, specify --type instead."
)
elif parsed_args.type is None:
raise errors.IncompatibleParams(
"You should specify --type if you pass "
"content in command line."
)
elif parsed_args.data_format is None:
raise errors.IncompatibleParams(
"You should specify --data-format for stdin data if you "
"don't pass content in command line."
)
elif parsed_args.type is not None:
raise errors.IncompatibleParams(
"--type and --data-format parameters can't "
"be used together."
)
def get_content(self, parsed_args):
type_ = parsed_args.type
if type_ == 'json':
return json.loads(parsed_args.content)
elif type_ == 'yaml':
return yaml.safe_load(parsed_args.content)
elif type_ is None:
data_format = parsed_args.data_format
if data_format == 'json':
return self.read_json()
elif data_format == 'yaml':
return self.read_yaml()
raise errors.UnsupportedDataType(
"Unsupported format: {0}".format(data_format)
)
raise errors.UnsupportedDataType("Unsupported type: {0}".format(type_))
class CreateResourceDefinition(ModifyResourceDefinitionCommand, show.ShowOne):
def take_action(self, parsed_args):
self.verify_arguments(parsed_args)
data = {
'name': parsed_args.name,
'component_id': parsed_args.component_id,
'content': self.get_content(parsed_args)
}
result = self.get_client().post(self.base_url, data)
return zip(*result.items())
class UpdateResourceDefinition(ModifyResourceDefinitionCommand,
base.BaseOneCommand):
def take_action(self, parsed_args):
data = {}
if parsed_args.name is not None:
data['name'] = parsed_args.name
if parsed_args.component_id is not None:
data['component_id'] = parsed_args.component_id
if (parsed_args.content is not None
or parsed_args.data_format is not None):
data['content'] = self.get_content(parsed_args)
self.get_client().patch(self.get_url(parsed_args), data)
return self.get_update_message(parsed_args)

View File

@ -19,6 +19,7 @@ from tuning_box import cli
from tuning_box.cli import base as cli_base
from tuning_box.cli import components
from tuning_box.cli import environments
from tuning_box.cli import resource_definitions
from tuning_box.cli import resources
from tuning_box import client as tb_client
@ -96,6 +97,41 @@ class UpdateComponent(FuelBaseCommand, components.UpdateComponent):
pass
class CreateResourceDefinition(
FuelBaseCommand,
resource_definitions.CreateResourceDefinition
):
pass
class ListResourceDefinitions(
FuelBaseCommand,
resource_definitions.ListResourceDefinitions
):
pass
class ShowResourceDefinition(
FuelBaseCommand,
resource_definitions.ShowResourceDefinition
):
pass
class DeleteResourceDefinition(
FuelBaseCommand,
resource_definitions.DeleteResourceDefinition
):
pass
class UpdateResourceDefinition(
FuelBaseCommand,
resource_definitions.UpdateResourceDefinition
):
pass
class Config(command.Command):
def get_parser(self, *args, **kwargs):
parser = super(Config, self).get_parser(*args, **kwargs)

View File

@ -0,0 +1,149 @@
# 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
import yaml
from tuning_box.tests.cli import _BaseCLITest
class TestCreateResourceDefinition(testscenarios.WithScenarios, _BaseCLITest):
scenarios = [
(s[0],
dict(zip(('args', 'expected_body', 'stdin'), s[1])))
for s in [
('json', ('def create -n n -i 1 -d json -f yaml',
'content: {}\ncomponent_id: 1\nid: 1\nname: n\n',
'{"a": 3}')),
('yaml', ('def create -n n -i 1 -d yaml -f yaml',
'content: {}\ncomponent_id: 1\nid: 1\nname: n\n',
'a: b\n')),
]
]
args = None
expected_body = None
stdin = None
def test_post(self):
url = self.BASE_URL + '/resource_definitions'
self.req_mock.post(
url,
headers={'Content-Type': 'application/json'},
json={'id': 1, 'component_id': 1, 'name': 'n', 'content': {}}
)
if self.stdin:
self.cli.stdin.write(self.stdin)
self.cli.stdin.seek(0)
self.cli.run(self.args.split())
self.assertEqual(
yaml.safe_load(self.expected_body),
yaml.safe_load(self.cli.stdout.getvalue())
)
class TestListResourceDefinitions(testscenarios.WithScenarios, _BaseCLITest):
scenarios = [
(s[0], dict(zip(('mock_url', 'args', 'expected_result'), s[1])))
for s in [
('json', ('/resource_definitions', 'def list -f json', '[]')),
('yaml', ('/resource_definitions', 'def list --format yaml',
'[]\n')),
]
]
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 TestShowComponent(testscenarios.WithScenarios, _BaseCLITest):
scenarios = [
(s[0], dict(zip(('mock_url', 'args', 'expected_result'), s[1])))
for s in [
('yaml', ('/resource_definitions/9', 'def show 9 -f yaml',
'id: 1\nname: n\ncomponent_id: 2\ncontent: {}\n')),
]
]
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={'id': 1, 'name': 'n', 'component_id': 2, 'content': {}},
)
self.cli.run(self.args.split())
self.assertEqual(self.expected_result, self.cli.stdout.getvalue())
class TestDeleteComponent(testscenarios.WithScenarios, _BaseCLITest):
scenarios = [
(s[0], dict(zip(('mock_url', 'args', 'expected_result'), s[1])))
for s in [
('', ('/resource_definitions/9', 'def delete 9', '')),
]
]
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'}
)
self.cli.run(self.args.split())
self.assertEqual(self.expected_result, self.cli.stdout.getvalue())
class TestUpdateResourceDefinition(testscenarios.WithScenarios, _BaseCLITest):
scenarios = [
(s[0], dict(zip(('args', 'expected_result', 'stdin'), s[1])))
for s in [
('no_data', ('def update 9', '')),
('name', ('def update 9 -n comp_name', '', False)),
('component_id', ('def update 9 -i 1', '', False)),
('content', ('def update 9 -p "a" -t yaml', '', False)),
('stdin_content', ('def update 9 -d yaml', '', 'a: b')),
('stdin_content', ('def update 9 -d yaml', '', 'a: b')),
]
]
args = None
expected_result = None
stdin = None
def test_update(self):
self.req_mock.patch(
self.BASE_URL + '/resource_definitions/9',
headers={'Content-Type': 'application/json'},
json={}
)
if self.stdin:
self.cli.stdin.write(self.stdin)
self.cli.stdin.seek(0)
self.cli.run(self.args.split())
self.assertEqual(self.expected_result, self.cli.stdout.getvalue())