Skip to content

Commit e6d1fd5

Browse files
author
Razvan Becheriu
committed
[#1790] add tool to parse CB parameters
1 parent a48e9ee commit e6d1fd5

File tree

3 files changed

+298
-16
lines changed

3 files changed

+298
-16
lines changed

src/lib/dhcpsrv/tests/srv_config_unittest.cc

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1113,9 +1113,13 @@ TEST_F(SrvConfigTest, mergeGlobals4) {
11131113
cfg_from.addConfiguredGlobal("ip-reservations-unique", Element::create(false));
11141114

11151115
// Add some configured globals:
1116-
cfg_to.addConfiguredGlobal("dhcp4o6-port", Element::create(999));
1117-
cfg_to.addConfiguredGlobal("server-tag", Element::create("use_this_server"));
1118-
cfg_to.addConfiguredGlobal("reservations-lookup-first", Element::create(true));
1116+
cfg_from.addConfiguredGlobal("dhcp4o6-port", Element::create(999));
1117+
cfg_from.addConfiguredGlobal("server-tag", Element::create("use_this_server"));
1118+
cfg_from.addConfiguredGlobal("reservations-lookup-first", Element::create(true));
1119+
ElementPtr mt = Element::createMap();
1120+
cfg_from.addConfiguredGlobal("multi-threading", mt);
1121+
mt->set("enable-multi-threading", Element::create(false));
1122+
mt->set("thread-pool-size", Element::create(256));
11191123

11201124
// Now let's merge.
11211125
ASSERT_NO_THROW(cfg_to.merge(cfg_from));
@@ -1140,16 +1144,23 @@ TEST_F(SrvConfigTest, mergeGlobals4) {
11401144
// ip-reservations-unique
11411145
EXPECT_FALSE(cfg_to.getCfgDbAccess()->getIPReservationsUnique());
11421146

1147+
// multi-threading
1148+
EXPECT_TRUE(cfg_to.getDHCPMultiThreading());
1149+
11431150
// Next we check the explicitly "configured" globals.
11441151
// The list should be all of the "to" + "from", with the
11451152
// latter overwriting the former.
11461153
std::string exp_globals =
11471154
"{ \n"
1148-
" \"decline-probation-period\": 300, \n"
1149-
" \"dhcp4o6-port\": 999, \n"
1150-
" \"ip-reservations-unique\": false, \n"
1151-
" \"server-tag\": \"use_this_server\", \n"
1152-
" \"reservations-lookup-first\": true"
1155+
" \"decline-probation-period\": 300, \n"
1156+
" \"dhcp4o6-port\": 999, \n"
1157+
" \"ip-reservations-unique\": false, \n"
1158+
" \"server-tag\": \"use_this_server\", \n"
1159+
" \"reservations-lookup-first\": true,"
1160+
" \"multi-threading\": { \"enable-multi-threading\": false, \n"
1161+
" \"packet-queue-size\": 64, \n"
1162+
" \"thread-pool-size\": 256 \n"
1163+
" } \n"
11531164
"} \n";
11541165

11551166
ConstElementPtr expected_globals;
@@ -1195,9 +1206,13 @@ TEST_F(SrvConfigTest, mergeGlobals6) {
11951206
cfg_from.addConfiguredGlobal("ip-reservations-unique", Element::create(false));
11961207

11971208
// Add some configured globals:
1198-
cfg_to.addConfiguredGlobal("dhcp4o6-port", Element::create(999));
1199-
cfg_to.addConfiguredGlobal("server-tag", Element::create("use_this_server"));
1200-
cfg_to.addConfiguredGlobal("reservations-lookup-first", Element::create(true));
1209+
cfg_from.addConfiguredGlobal("dhcp4o6-port", Element::create(999));
1210+
cfg_from.addConfiguredGlobal("server-tag", Element::create("use_this_server"));
1211+
cfg_from.addConfiguredGlobal("reservations-lookup-first", Element::create(true));
1212+
ElementPtr mt = Element::createMap();
1213+
cfg_from.addConfiguredGlobal("multi-threading", mt);
1214+
mt->set("enable-multi-threading", Element::create(false));
1215+
mt->set("thread-pool-size", Element::create(256));
12011216

12021217
// Now let's merge.
12031218
ASSERT_NO_THROW(cfg_to.merge(cfg_from));
@@ -1219,16 +1234,23 @@ TEST_F(SrvConfigTest, mergeGlobals6) {
12191234
// ip-reservations-unique
12201235
EXPECT_FALSE(cfg_to.getCfgDbAccess()->getIPReservationsUnique());
12211236

1237+
// multi-threading
1238+
EXPECT_TRUE(cfg_to.getDHCPMultiThreading());
1239+
12221240
// Next we check the explicitly "configured" globals.
12231241
// The list should be all of the "to" + "from", with the
12241242
// latter overwriting the former.
12251243
std::string exp_globals =
12261244
"{ \n"
1227-
" \"decline-probation-period\": 300, \n"
1228-
" \"dhcp4o6-port\": 999, \n"
1229-
" \"ip-reservations-unique\": false, \n"
1230-
" \"server-tag\": \"use_this_server\", \n"
1231-
" \"reservations-lookup-first\": true"
1245+
" \"decline-probation-period\": 300, \n"
1246+
" \"dhcp4o6-port\": 999, \n"
1247+
" \"ip-reservations-unique\": false, \n"
1248+
" \"server-tag\": \"use_this_server\", \n"
1249+
" \"reservations-lookup-first\": true, \n"
1250+
" \"multi-threading\": { \"enable-multi-threading\": false, \n"
1251+
" \"packet-queue-size\": 64, \n"
1252+
" \"thread-pool-size\": 256 \n"
1253+
" } \n"
12321254
"} \n";
12331255

12341256
ConstElementPtr expected_globals;

tools/kea-breeder/kb.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
from termcolor import colored, cprint
5+
from io import StringIO
6+
import json
7+
import os
8+
import re
9+
import sqlalchemy as db
10+
from sqlalchemy.sql import select
11+
import sys
12+
13+
def convert_to_db(entity_name, make_singular=True):
14+
sql_name = entity_name.replace('-', '_').lower()
15+
if not make_singular:
16+
return sql_name
17+
18+
if not re.search(r'ddns$', sql_name) and not re.search(r'times$', sql_name):
19+
replacements = [
20+
[r'classes', 'class'],
21+
[r'ies', 'y'],
22+
[r's$', '']
23+
]
24+
25+
for r in replacements:
26+
new_name = re.sub(r[0], r[1], sql_name)
27+
if new_name != sql_name:
28+
break
29+
sql_name = new_name
30+
31+
return sql_name
32+
33+
class State:
34+
def __init__(self, config, database, path = None, token_name = None):
35+
self.config = config
36+
self.database = database
37+
if path is not None:
38+
self._path = path
39+
else:
40+
self._path = []
41+
42+
if token_name is not None:
43+
self._path += [token_name]
44+
45+
def copy(self, token_name = None):
46+
return State(self.config, self.database, self._path.copy(), token_name)
47+
48+
def sql_prefix(self):
49+
return self._path[0].lower()
50+
51+
def sql_parent_name(self):
52+
return convert_to_db(self.get_parent_name())
53+
54+
def sql_current_name(self):
55+
return convert_to_db(self.get_current_name(), False)
56+
57+
def sql_current_global_name(self):
58+
return self.sql_parent_name() + '_' + self.sql_current_name()
59+
60+
def sql_global_table_name(self):
61+
return self.sql_parent_name() + '_global_parameter'
62+
63+
def sql_parent_table_name(self):
64+
return self.config.get_mapped_table_name('{0}_{1}'.format(self.sql_prefix(), self.sql_parent_name()))
65+
66+
def sql_table_name(self):
67+
return self.config.get_mapped_table_name('{0}_{1}'.format(self.sql_prefix(), convert_to_db(self.get_current_name(), True)))
68+
69+
def get_parent_name(self):
70+
return self._path[-2]
71+
72+
def get_current_name(self):
73+
if self._path:
74+
return self._path[-1]
75+
return None
76+
77+
def get_path(self):
78+
return self._path
79+
80+
def get_path_len(self):
81+
return len(self._path)
82+
83+
class ConfigFile:
84+
def __init__(self, filename):
85+
self.filename = filename
86+
87+
def load(self):
88+
if not os.path.exists(self.filename):
89+
print('The all keys file %s does not exist.' % self.filename)
90+
sys.exit(1)
91+
92+
with open(self.filename) as f:
93+
self.config = json.load(f)
94+
f.close()
95+
96+
def get_mapped_table_name(self, generated_name):
97+
mappings = self.config['sql_table_mappings']
98+
if not mappings or generated_name not in mappings.keys():
99+
return generated_name
100+
101+
return mappings[generated_name]['actual_name']
102+
103+
def is_ignored_parameter(self, name):
104+
ignored_parameters = self.config['ignored_parameters']
105+
return name in ignored_parameters
106+
107+
class KeaDatabase:
108+
def __init__(self):
109+
engine = db.create_engine('mysql+mysqldb://root@localhost/keatest')
110+
self.connection = engine.connect()
111+
112+
def has_table(self, table_name):
113+
sql = db.text(
114+
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = :table_name"
115+
)
116+
result = self.connection.execute(sql, {"table_name": table_name}).fetchone()
117+
return result[0] > 0
118+
119+
def has_column(self, table_name, column_name):
120+
sql = db.text(
121+
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = :table_name AND COLUMN_NAME = :column_name"
122+
)
123+
result = self.connection.execute(sql, {"table_name": table_name, "column_name": column_name}).fetchone()
124+
return result[0] > 0
125+
126+
def traverse(state, parents, json_object):
127+
if state.config.is_ignored_parameter(state.get_current_name()):
128+
return
129+
130+
new_parents = parents.copy()
131+
new_parents.append(json_object)
132+
133+
comment = ''
134+
135+
if state.get_path_len() == 1:
136+
# Top level configuration item, e.g. Dhcp4.
137+
comment = cprint(text='Top level configuration item', color='green')
138+
139+
elif state.get_path_len() == 2 and not isinstance(json_object, list) and not isinstance(json_object, dict):
140+
# Global primitive value, e.g. boolean. Kea has a dedicated table for them.
141+
comment = cprint(text='Set primitive value {0} in {1} table'.format(state.sql_current_name(), state.sql_global_table_name()), color='green')
142+
143+
else:
144+
# Handle primitives at lower levels.
145+
if not isinstance(json_object, dict) and not isinstance(json_object, list):
146+
if isinstance(parents[-1], dict) and isinstance(parents[-2], dict):
147+
if state.get_path_len() > 3:
148+
# If the primitive belongs to a hierarchy of two maps, the structure of
149+
# the lower level map must be flattened and the respective parameters
150+
# must be moved to the upper level map.
151+
comment = cprint(text='Create column {0} in the parent table'.format(state.sql_current_name()), color='red')
152+
else:
153+
# An exception is the parameter belonging to the top level maps, e.g.
154+
# Dhcp4/map/primitive. This primitive goes to the dhcp4_global_parameter
155+
# table.
156+
comment = cprint(text='Use global parameter {0}'.format(state.sql_current_global_name()), color='yellow')
157+
158+
elif isinstance(parents[-1], dict) and isinstance(parents[-2], list):
159+
# A list of maps deserves its own table. For example: subnet4 or
160+
# shared_networks, option_def etc.
161+
if state.database.has_column(state.sql_parent_table_name(), state.sql_current_name()):
162+
comment = cprint(text='Column {0} in {1} table exists'.format(state.sql_current_name(), state.sql_parent_table_name()), color='green')
163+
else:
164+
comment = cprint(text='Create column {0} in {1} table'.format(state.sql_current_name(), state.sql_parent_table_name()), color='red')
165+
166+
elif isinstance(json_object, list):
167+
if json_object and isinstance(json_object[0], dict):
168+
if state.database.has_table(state.sql_table_name()):
169+
comment = cprint(text='Table {0} exists'.format(state.sql_table_name()), color='green')
170+
else:
171+
comment = cprint(text='Create table {0}'.format(state.sql_table_name()), color='red')
172+
else:
173+
comment = cprint(text='Unable to determine children types because all-keys file contains no children for this object', color='red')
174+
175+
elif isinstance(json_object, dict):
176+
if len(parents) > 1 and isinstance(parents[-2], dict):
177+
if state.get_path_len() == 2:
178+
comment = cprint(text='Parameters belonging to this map should be in {0}'.format(state.sql_global_table_name()), color='yellow')
179+
180+
# Format printing the current object depending on its type.
181+
if isinstance(json_object, dict):
182+
if parents and not isinstance(parents[-1], list):
183+
# Only print the map information if the parent is not a list. Otherwise
184+
# it will be printed twice.
185+
print('{0}/dict: {1}'.format(state.get_path(), comment))
186+
187+
# Print each child of the map with recursion.
188+
for key in sorted(json_object.keys()):
189+
traverse(state.copy(key), parents + [json_object], json_object[key])
190+
191+
elif isinstance(json_object, list) and len(json_object):
192+
# If it is a list, print only the first element using recursion.
193+
# All elements of the list should have the same type, so it makes
194+
# no sense to print all of them.
195+
print('{0}/list: {1}'.format(state.get_path(), comment))
196+
traverse(state.copy(), parents + [json_object], json_object[0])
197+
198+
else:
199+
# It is neither a list nor a map, so it must be a primitive. Print it
200+
# along with a comment.
201+
print('{0}/{1}: {2}'.format(state.get_path(), type(json_object).__name__, comment))
202+
203+
def main():
204+
parser = argparse.ArgumentParser(description='Kea Developer Tools')
205+
parser.add_argument('--all-keys-file', metavar='all_keys_file', required=True,
206+
help='Kea all_keys.json file location.')
207+
parser.add_argument('--config-file', metavar='config_file', required=True,
208+
help='Configuration file location for this tool.')
209+
args = parser.parse_args()
210+
211+
config = ConfigFile(args.config_file)
212+
config.load()
213+
214+
if not os.path.exists(args.all_keys_file):
215+
print('The all keys file %s does not exist.' % args.all_keys_file)
216+
sys.exit(1)
217+
218+
sanitized_contents = ''
219+
f = open(args.all_keys_file)
220+
for line in f:
221+
sanitized_line = line.strip()
222+
if not sanitized_line:
223+
continue
224+
225+
if sanitized_line.find('//') != -1 or sanitized_line.find('#') != -1:
226+
continue
227+
228+
sanitized_line = sanitized_line.replace(': .', ': 0.')
229+
sanitized_contents = sanitized_contents + sanitized_line
230+
231+
f.close()
232+
233+
io = StringIO(sanitized_contents)
234+
parsed = json.load(io)
235+
236+
database = KeaDatabase()
237+
238+
traverse(State(config, database), [], parsed)
239+
240+
if __name__ == '__main__':
241+
main()

tools/kea-breeder/mappings.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"sql_table_mappings": {
3+
"dhcp4_option_data": {
4+
"actual_name": "dhcp4_options"
5+
},
6+
"dhcp4_subnet4": {
7+
"actual_name": "dhcp4_subnet"
8+
},
9+
"dhcp6_option_data": {
10+
"actual_name": "dhcp6_options"
11+
},
12+
"dhcp6_subnet6": {
13+
"actual_name": "dhcp6_subnet"
14+
}
15+
},
16+
"ignored_parameters": [
17+
"reservations"
18+
]
19+
}

0 commit comments

Comments
 (0)