Merge "Add save command to ring-builder-analyzer"

This commit is contained in:
Jenkins 2015-07-28 00:17:50 +00:00 committed by Gerrit Code Review
commit 2b86890381
2 changed files with 96 additions and 46 deletions

View File

@ -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)

View File

@ -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))