Merge "Add save command to ring-builder-analyzer"
This commit is contained in:
commit
2b86890381
@ -96,26 +96,30 @@ ARG_PARSER.add_argument(
|
|||||||
help="Path to the scenario file")
|
help="Path to the scenario file")
|
||||||
|
|
||||||
|
|
||||||
|
class ParseCommandError(ValueError):
|
||||||
|
|
||||||
|
def __init__(self, name, round_index, command_index, msg):
|
||||||
|
msg = "Invalid %s (round %s, command %s): %s" % (
|
||||||
|
name, round_index, command_index, msg)
|
||||||
|
super(ParseCommandError, self).__init__(msg)
|
||||||
|
|
||||||
|
|
||||||
def _parse_weight(round_index, command_index, weight_str):
|
def _parse_weight(round_index, command_index, weight_str):
|
||||||
try:
|
try:
|
||||||
weight = float(weight_str)
|
weight = float(weight_str)
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
raise ValueError(
|
raise ParseCommandError('weight', round_index, command_index, err)
|
||||||
"Invalid weight %r (round %d, command %d): %s"
|
|
||||||
% (weight_str, round_index, command_index, err))
|
|
||||||
if weight < 0:
|
if weight < 0:
|
||||||
raise ValueError(
|
raise ParseCommandError('weight', round_index, command_index,
|
||||||
"Negative weight (round %d, command %d)"
|
'cannot be negative')
|
||||||
% (round_index, command_index))
|
|
||||||
return weight
|
return weight
|
||||||
|
|
||||||
|
|
||||||
def _parse_add_command(round_index, command_index, command):
|
def _parse_add_command(round_index, command_index, command):
|
||||||
if len(command) != 3:
|
if len(command) != 3:
|
||||||
raise ValueError(
|
raise ParseCommandError(
|
||||||
"Invalid add command (round %d, command %d): expected array of "
|
'add command', round_index, command_index,
|
||||||
"length 3, but got %d"
|
'expected array of length 3, but got %r' % command)
|
||||||
% (round_index, command_index, len(command)))
|
|
||||||
|
|
||||||
dev_str = command[1]
|
dev_str = command[1]
|
||||||
weight_str = command[2]
|
weight_str = command[2]
|
||||||
@ -123,43 +127,47 @@ def _parse_add_command(round_index, command_index, command):
|
|||||||
try:
|
try:
|
||||||
dev = parse_add_value(dev_str)
|
dev = parse_add_value(dev_str)
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
raise ValueError(
|
raise ParseCommandError('device specifier', round_index,
|
||||||
"Invalid device specifier '%s' in add (round %d, command %d): %s"
|
command_index, err)
|
||||||
% (dev_str, round_index, command_index, err))
|
|
||||||
|
|
||||||
dev['weight'] = _parse_weight(round_index, command_index, weight_str)
|
dev['weight'] = _parse_weight(round_index, command_index, weight_str)
|
||||||
|
|
||||||
if dev['region'] is None:
|
if dev['region'] is None:
|
||||||
dev['region'] = 1
|
dev['region'] = 1
|
||||||
|
|
||||||
|
default_key_map = {
|
||||||
|
'replication_ip': 'ip',
|
||||||
|
'replication_port': 'port',
|
||||||
|
}
|
||||||
|
for empty_key, default_key in default_key_map.items():
|
||||||
|
if dev[empty_key] is None:
|
||||||
|
dev[empty_key] = dev[default_key]
|
||||||
|
|
||||||
return ['add', dev]
|
return ['add', dev]
|
||||||
|
|
||||||
|
|
||||||
def _parse_remove_command(round_index, command_index, command):
|
def _parse_remove_command(round_index, command_index, command):
|
||||||
if len(command) != 2:
|
if len(command) != 2:
|
||||||
raise ValueError(
|
raise ParseCommandError('remove commnd', round_index, command_index,
|
||||||
"Invalid remove command (round %d, command %d): expected array of "
|
"expected array of length 2, but got %r" %
|
||||||
"length 2, but got %d"
|
(command,))
|
||||||
% (round_index, command_index, len(command)))
|
|
||||||
|
|
||||||
dev_str = command[1]
|
dev_str = command[1]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dev_id = int(dev_str)
|
dev_id = int(dev_str)
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
raise ValueError(
|
raise ParseCommandError('device ID in remove',
|
||||||
"Invalid device ID '%s' in remove (round %d, command %d): %s"
|
round_index, command_index, err)
|
||||||
% (dev_str, round_index, command_index, err))
|
|
||||||
|
|
||||||
return ['remove', dev_id]
|
return ['remove', dev_id]
|
||||||
|
|
||||||
|
|
||||||
def _parse_set_weight_command(round_index, command_index, command):
|
def _parse_set_weight_command(round_index, command_index, command):
|
||||||
if len(command) != 3:
|
if len(command) != 3:
|
||||||
raise ValueError(
|
raise ParseCommandError('remove command', round_index, command_index,
|
||||||
"Invalid remove command (round %d, command %d): expected array of "
|
"expected array of length 3, but got %r" %
|
||||||
"length 3, but got %d"
|
(command,))
|
||||||
% (round_index, command_index, len(command)))
|
|
||||||
|
|
||||||
dev_str = command[1]
|
dev_str = command[1]
|
||||||
weight_str = command[2]
|
weight_str = command[2]
|
||||||
@ -167,14 +175,21 @@ def _parse_set_weight_command(round_index, command_index, command):
|
|||||||
try:
|
try:
|
||||||
dev_id = int(dev_str)
|
dev_id = int(dev_str)
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
raise ValueError(
|
raise ParseCommandError('device ID in set_weight',
|
||||||
"Invalid device ID '%s' in set_weight (round %d, command %d): %s"
|
round_index, command_index, err)
|
||||||
% (dev_str, round_index, command_index, err))
|
|
||||||
|
|
||||||
weight = _parse_weight(round_index, command_index, weight_str)
|
weight = _parse_weight(round_index, command_index, weight_str)
|
||||||
return ['set_weight', dev_id, weight]
|
return ['set_weight', dev_id, weight]
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_save_command(round_index, command_index, command):
|
||||||
|
if len(command) != 2:
|
||||||
|
raise ParseCommandError(
|
||||||
|
command, round_index, command_index,
|
||||||
|
"expected array of length 2 but got %r" % (command,))
|
||||||
|
return ['save', command[1]]
|
||||||
|
|
||||||
|
|
||||||
def parse_scenario(scenario_data):
|
def parse_scenario(scenario_data):
|
||||||
"""
|
"""
|
||||||
Takes a serialized scenario and turns it into a data structure suitable
|
Takes a serialized scenario and turns it into a data structure suitable
|
||||||
@ -236,9 +251,12 @@ def parse_scenario(scenario_data):
|
|||||||
if not isinstance(raw_scenario['rounds'], list):
|
if not isinstance(raw_scenario['rounds'], list):
|
||||||
raise ValueError("rounds must be an array")
|
raise ValueError("rounds must be an array")
|
||||||
|
|
||||||
parser_for_command = {'add': _parse_add_command,
|
parser_for_command = {
|
||||||
|
'add': _parse_add_command,
|
||||||
'remove': _parse_remove_command,
|
'remove': _parse_remove_command,
|
||||||
'set_weight': _parse_set_weight_command}
|
'set_weight': _parse_set_weight_command,
|
||||||
|
'save': _parse_save_command,
|
||||||
|
}
|
||||||
|
|
||||||
parsed_scenario['rounds'] = []
|
parsed_scenario['rounds'] = []
|
||||||
for round_index, raw_round in enumerate(raw_scenario['rounds']):
|
for round_index, raw_round in enumerate(raw_scenario['rounds']):
|
||||||
@ -268,18 +286,24 @@ def run_scenario(scenario):
|
|||||||
|
|
||||||
rb = builder.RingBuilder(scenario['part_power'], scenario['replicas'], 1)
|
rb = builder.RingBuilder(scenario['part_power'], scenario['replicas'], 1)
|
||||||
rb.set_overload(scenario['overload'])
|
rb.set_overload(scenario['overload'])
|
||||||
|
|
||||||
|
command_map = {
|
||||||
|
'add': rb.add_dev,
|
||||||
|
'remove': rb.remove_dev,
|
||||||
|
'set_weight': rb.set_dev_weight,
|
||||||
|
'save': rb.save,
|
||||||
|
}
|
||||||
|
|
||||||
for round_index, commands in enumerate(scenario['rounds']):
|
for round_index, commands in enumerate(scenario['rounds']):
|
||||||
print "Round %d" % (round_index + 1)
|
print "Round %d" % (round_index + 1)
|
||||||
|
|
||||||
for command in commands:
|
for command in commands:
|
||||||
if command[0] == 'add':
|
key = command.pop(0)
|
||||||
rb.add_dev(command[1])
|
try:
|
||||||
elif command[0] == 'remove':
|
command_f = command_map[key]
|
||||||
rb.remove_dev(command[1])
|
except KeyError:
|
||||||
elif command[0] == 'set_weight':
|
raise ValueError("unknown command %r" % key)
|
||||||
rb.set_dev_weight(command[1], command[2])
|
command_f(*command)
|
||||||
else:
|
|
||||||
raise ValueError("unknown command %r" % (command[0],))
|
|
||||||
|
|
||||||
rebalance_number = 1
|
rebalance_number = 1
|
||||||
parts_moved, old_balance = rb.rebalance(seed=seed)
|
parts_moved, old_balance = rb.rebalance(seed=seed)
|
||||||
|
@ -14,22 +14,27 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import os
|
||||||
import json
|
import json
|
||||||
import mock
|
import mock
|
||||||
import unittest
|
import unittest
|
||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
|
from test.unit import with_tempdir
|
||||||
|
|
||||||
from swift.cli.ring_builder_analyzer import parse_scenario, run_scenario
|
from swift.cli.ring_builder_analyzer import parse_scenario, run_scenario
|
||||||
|
|
||||||
|
|
||||||
class TestRunScenario(unittest.TestCase):
|
class TestRunScenario(unittest.TestCase):
|
||||||
def test_it_runs(self):
|
@with_tempdir
|
||||||
|
def test_it_runs(self, tempdir):
|
||||||
|
builder_path = os.path.join(tempdir, 'test.builder')
|
||||||
scenario = {
|
scenario = {
|
||||||
'replicas': 3, 'part_power': 8, 'random_seed': 123, 'overload': 0,
|
'replicas': 3, 'part_power': 8, 'random_seed': 123, 'overload': 0,
|
||||||
'rounds': [[['add', 'r1z2-3.4.5.6:7/sda8', 100],
|
'rounds': [[['add', 'r1z2-3.4.5.6:7/sda8', 100],
|
||||||
['add', 'z2-3.4.5.6:7/sda9', 200]],
|
['add', 'z2-3.4.5.6:7/sda9', 200]],
|
||||||
[['set_weight', 0, 150]],
|
[['set_weight', 0, 150]],
|
||||||
[['remove', 1]]]}
|
[['remove', 1]],
|
||||||
|
[['save', builder_path]]]}
|
||||||
parsed = parse_scenario(json.dumps(scenario))
|
parsed = parse_scenario(json.dumps(scenario))
|
||||||
|
|
||||||
fake_stdout = StringIO()
|
fake_stdout = StringIO()
|
||||||
@ -40,6 +45,7 @@ class TestRunScenario(unittest.TestCase):
|
|||||||
# this doesn't crash and produces output that resembles something
|
# this doesn't crash and produces output that resembles something
|
||||||
# useful is good enough.
|
# useful is good enough.
|
||||||
self.assertTrue('Rebalance' in fake_stdout.getvalue())
|
self.assertTrue('Rebalance' in fake_stdout.getvalue())
|
||||||
|
self.assertTrue(os.path.exists(builder_path))
|
||||||
|
|
||||||
|
|
||||||
class TestParseScenario(unittest.TestCase):
|
class TestParseScenario(unittest.TestCase):
|
||||||
@ -62,8 +68,8 @@ class TestParseScenario(unittest.TestCase):
|
|||||||
'meta': '',
|
'meta': '',
|
||||||
'port': 7,
|
'port': 7,
|
||||||
'region': 1,
|
'region': 1,
|
||||||
'replication_ip': None,
|
'replication_ip': '3.4.5.6',
|
||||||
'replication_port': None,
|
'replication_port': 7,
|
||||||
'weight': 100.0,
|
'weight': 100.0,
|
||||||
'zone': 2}],
|
'zone': 2}],
|
||||||
['add', {'device': u'sda9',
|
['add', {'device': u'sda9',
|
||||||
@ -71,8 +77,8 @@ class TestParseScenario(unittest.TestCase):
|
|||||||
'meta': '',
|
'meta': '',
|
||||||
'port': 7,
|
'port': 7,
|
||||||
'region': 1,
|
'region': 1,
|
||||||
'replication_ip': None,
|
'replication_ip': '3.4.5.6',
|
||||||
'replication_port': None,
|
'replication_port': 7,
|
||||||
'weight': 200.0,
|
'weight': 200.0,
|
||||||
'zone': 2}]],
|
'zone': 2}]],
|
||||||
[['set_weight', 0, 150.0]],
|
[['set_weight', 0, 150.0]],
|
||||||
@ -180,7 +186,14 @@ class TestParseScenario(unittest.TestCase):
|
|||||||
|
|
||||||
# can't parse
|
# can't parse
|
||||||
busted = dict(base, rounds=[[['add', 'not a good value', 100]]])
|
busted = dict(base, rounds=[[['add', 'not a good value', 100]]])
|
||||||
self.assertRaises(ValueError, parse_scenario, json.dumps(busted))
|
# N.B. the ValueError's coming out of ring.utils.parse_add_value
|
||||||
|
# are already pretty good
|
||||||
|
expected = "Invalid device specifier (round 0, command 0): " \
|
||||||
|
"Invalid add value: not a good value"
|
||||||
|
try:
|
||||||
|
parse_scenario(json.dumps(busted))
|
||||||
|
except ValueError as err:
|
||||||
|
self.assertEqual(str(err), expected)
|
||||||
|
|
||||||
# negative weight
|
# negative weight
|
||||||
busted = dict(base, rounds=[[['add', 'r1z2-1.2.3.4:6000/d7', -1]]])
|
busted = dict(base, rounds=[[['add', 'r1z2-1.2.3.4:6000/d7', -1]]])
|
||||||
@ -216,7 +229,12 @@ class TestParseScenario(unittest.TestCase):
|
|||||||
|
|
||||||
# bad dev id
|
# bad dev id
|
||||||
busted = dict(base, rounds=[[['set_weight', 'not an int', 90]]])
|
busted = dict(base, rounds=[[['set_weight', 'not an int', 90]]])
|
||||||
self.assertRaises(ValueError, parse_scenario, json.dumps(busted))
|
expected = "Invalid device ID in set_weight (round 0, command 0): " \
|
||||||
|
"invalid literal for int() with base 10: 'not an int'"
|
||||||
|
try:
|
||||||
|
parse_scenario(json.dumps(busted))
|
||||||
|
except ValueError as e:
|
||||||
|
self.assertEqual(str(e), expected)
|
||||||
|
|
||||||
# negative weight
|
# negative weight
|
||||||
busted = dict(base, rounds=[[['set_weight', 1, -1]]])
|
busted = dict(base, rounds=[[['set_weight', 1, -1]]])
|
||||||
@ -225,3 +243,11 @@ class TestParseScenario(unittest.TestCase):
|
|||||||
# bogus weight
|
# bogus weight
|
||||||
busted = dict(base, rounds=[[['set_weight', 1, 'bogus']]])
|
busted = dict(base, rounds=[[['set_weight', 1, 'bogus']]])
|
||||||
self.assertRaises(ValueError, parse_scenario, json.dumps(busted))
|
self.assertRaises(ValueError, parse_scenario, json.dumps(busted))
|
||||||
|
|
||||||
|
def test_bad_save(self):
|
||||||
|
base = {
|
||||||
|
'replicas': 3, 'part_power': 8, 'random_seed': 123, 'overload': 0}
|
||||||
|
|
||||||
|
# no builder name
|
||||||
|
busted = dict(base, rounds=[[['save']]])
|
||||||
|
self.assertRaises(ValueError, parse_scenario, json.dumps(busted))
|
||||||
|
Loading…
Reference in New Issue
Block a user