Add list and show pool commands to Pool Manager

This patch add the support to get the current status of the pools.
It can list the existing pools and their number of available
subports, as well as to show the ids of the ports associated to
a given pool.

Implements: blueprint kuryr-manager-cli-tool
Change-Id: I9332ffa259c4651b69c788985e5bbd1f98cb38cc
This commit is contained in:
Luis Tomas Bolivar 2017-09-15 13:36:18 +00:00
parent 54ef9dcd10
commit 3de89337f7
6 changed files with 393 additions and 0 deletions

View File

@ -245,6 +245,62 @@ Or from all the pools at once::
$ # returns nothing
List pools for nested environment
---------------------------------
There is a `list` command available to show information about the existing
pools, i.e., it prints out the pool keys (trunk_ip, project_id,
[security_groups]) and the amount of available ports in each one of them::
$ python contrib/pools-management/subports.py list -h
usage: subports.py list [-h] [-t TIMEOUT]
optional arguments:
-h, --help show this help message and exit
-t TIMEOUT, --timeout TIMEOUT
set timeout for operation. Default is 180 sec
As an example::
$ python contrib/pools-management/subports.py list
Content-length: 150
Pools:
["10.0.0.6", "9d2b45c4efaa478481c30340b49fd4d2", ["00efc78c-f11c-414a-bfcd-a82e16dc07d1", "fd6b13dc-7230-4cbe-9237-36b4614bc6b5"]] has 4 ports
Show pool for nested environment
--------------------------------
There is a `show` command available to print out information about a given
pool. It prints the ids of the ports associated to that pool:::
$ python contrib/pools-management/subports.py show -h
usage: subports.py show [-h] --trunk TRUNK_IP -p PROJECT_ID --sg SG [SG ...]
[-t TIMEOUT]
optional arguments:
-h, --help show this help message and exit
--trunk TRUNK_IP Trunk IP of the desired pool
-p PROJECT_ID, --project-id PROJECT_ID
project id of the pool
--sg SG [SG ...] Security group ids of the pool
-t TIMEOUT, --timeout TIMEOUT
set timeout for operation. Default is 180 sec
As an example::
$ python contrib/pools-management/subports.py show --trunk 10.0.0.6 -p 9d2b45c4efaa478481c30340b49fd4d2 --sg 00efc78c-f11c-414a-bfcd-a82e16dc07d1 fd6b13dc-7230-4cbe-9237-36b4614bc6b5
Content-length: 299
Pool (u'10.0.0.6', u'9d2b45c4efaa478481c30340b49fd4d2', (u'00efc78c-f11c-414a-bfcd-a82e16dc07d1', u'fd6b13dc-7230-4cbe-9237-36b4614bc6b5')) ports are:
4913fbde-5939-4aef-80c0-7fcca0348871
864c8237-6ab4-4713-bec8-3d8bb6aa2144
8138134b-44df-489c-a693-3defeb2adb58
f5e107c6-f998-4416-8f17-a055269f2829
Without the script
------------------
@ -256,3 +312,9 @@ REST API with curl::
# To free the pool
$ curl --unix-socket /run/kuryr/kuryr_manage.sock http://localhost/freePool -H "Content-Type: application/json" -X POST -d '{"trunks": ["10.0.4.6"]}'
# To list the existing pools
$ curl --unix-socket /run/kuryr/kuryr_manage.sock http://localhost/listPools -H "Content-Type: application/json" -X GET -d '{}'
# To show a specific pool
$ curl --unix-socket /run/kuryr/kuryr_manage.sock http://localhost/showPool -H "Content-Type: application/json" -X GET -d '{"pool_key": ["10.0.0.6", "9d2b45c4efaa478481c30340b49fd4d2", ["00efc78c-f11c-414a-bfcd-a82e16dc07d1", "fd6b13dc-7230-4cbe-9237-36b4614bc6b5"]]}'

View File

@ -62,6 +62,32 @@ def delete_subports(trunk_ips, timeout=180):
print(resp.read())
def list_pools(timeout=180):
method = 'GET'
body = jsonutils.dumps({})
headers = {'Context-Type': 'application/json', 'Connection': 'close'}
headers['Context-Length'] = len(body)
path = 'http://localhost{0}'.format(constants.VIF_POOL_LIST)
socket_path = constants.MANAGER_SOCKET_FILE
conn = UnixDomainHttpConnection(socket_path, timeout)
conn.request(method, path, body=body, headers=headers)
resp = conn.getresponse()
print(resp.read())
def show_pool(trunk_ip, project_id, sg, timeout=180):
method = 'GET'
body = jsonutils.dumps({"pool_key": [trunk_ip, project_id, sg]})
headers = {'Context-Type': 'application/json', 'Connection': 'close'}
headers['Context-Length'] = len(body)
path = 'http://localhost{0}'.format(constants.VIF_POOL_SHOW)
socket_path = constants.MANAGER_SOCKET_FILE
conn = UnixDomainHttpConnection(socket_path, timeout)
conn.request(method, path, body=body, headers=headers)
resp = conn.getresponse()
print(resp.read())
def _get_parser():
parser = argparse.ArgumentParser(
description='Tool to create/free subports from the subports pool')
@ -104,6 +130,42 @@ def _get_parser():
default=180,
type=int)
list_pools_parser = subparser.add_parser(
'list',
help='List available pools and the number of ports they have')
list_pools_parser.add_argument(
'-t', '--timeout',
help='set timeout for operation. Default is 180 sec',
dest='timeout',
default=180,
type=int)
show_pool_parser = subparser.add_parser(
'show',
help='Show the ports associated to a given pool')
show_pool_parser.add_argument(
'--trunk',
help='Trunk IP of the desired pool',
dest='trunk_ip',
required=True)
show_pool_parser.add_argument(
'-p', '--project-id',
help='project id of the pool',
dest='project_id',
required=True)
show_pool_parser.add_argument(
'--sg',
help='Security group ids of the pool',
dest='sg',
nargs='+',
required=True)
show_pool_parser.add_argument(
'-t', '--timeout',
help='set timeout for operation. Default is 180 sec',
dest='timeout',
default=180,
type=int)
return parser
@ -115,6 +177,10 @@ def main():
create_subports(args.num, args.subports, args.timeout)
elif args.command == 'free':
delete_subports(args.subports, args.timeout)
elif args.command == 'list':
list_pools(args.timeout)
elif args.command == 'show':
show_pool(args.trunk_ip, args.project_id, args.sg, args.timeout)
if __name__ == '__main__':

View File

@ -40,3 +40,5 @@ OCTAVIA_L3_MEMBER_MODE = "L3"
VIF_POOL_POPULATE = '/populatePool'
VIF_POOL_FREE = '/freePool'
VIF_POOL_LIST = '/listPools'
VIF_POOL_SHOW = '/showPool'

View File

@ -205,6 +205,12 @@ class BaseVIFPool(base.VIFPoolDriver):
annotations['versioned_object.data']['id'])
return in_use_ports
def list_pools(self):
return self._available_ports_pools
def show_pool(self, pool_key):
return self._available_ports_pools.get(pool_key)
class NeutronVIFPool(BaseVIFPool):
"""Manages VIFs for Bare Metal Kubernetes Pods."""

View File

@ -102,6 +102,50 @@ class RequestHandler(BaseHTTPRequestHandler):
self.end_headers()
self.wfile.write(response.encode())
def do_GET(self):
content_length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(content_length)
params = dict(jsonutils.loads(body))
if self.path.endswith(constants.VIF_POOL_LIST):
try:
pools_info = self._list_pools()
except Exception:
response = 'Error listing the pools.'
else:
response = 'Pools:\n{0}'.format(pools_info)
self.send_header('Content-Length', len(response))
self.end_headers()
self.wfile.write(response.encode())
elif self.path.endswith(constants.VIF_POOL_SHOW):
raw_key = params.get('pool_key', None)
if len(raw_key) != 3:
response = ('Invalid pool key. Proper format is:\n'
'[trunk_ip, project_id, [security_groups]]\n')
else:
pool_key = (raw_key[0], raw_key[1], tuple(sorted(raw_key[2])))
try:
pool_info = self._show_pool(pool_key)
except Exception:
response = 'Error showing pool: {0}.'.format(pool_key)
else:
response = 'Pool {0} ports are:\n{1}'.format(pool_key,
pool_info)
self.send_header('Content-Length', len(response))
self.end_headers()
self.wfile.write(response.encode())
else:
response = 'Method not allowed.'
self.send_header('Content-Length', len(response))
self.end_headers()
self.wfile.write(response.encode())
def _create_subports(self, num_ports, trunk_ips):
try:
drv_project = drivers.PodProjectDriver.get_instance()
@ -141,6 +185,44 @@ class RequestHandler(BaseHTTPRequestHandler):
LOG.error("Invalid driver type")
raise ex
def _list_pools(self):
try:
drv_vif = drivers.PodVIFDriver.get_instance()
drv_vif_pool = drivers.VIFPoolDriver.get_instance()
drv_vif_pool.set_vif_driver(drv_vif)
available_pools = drv_vif_pool.list_pools()
except TypeError as ex:
LOG.error("Invalid driver type")
raise ex
pools_info = ""
for pool_key, pool_items in available_pools.items():
pools_info += (jsonutils.dumps(pool_key) + " has "
+ str(len(pool_items)) + " ports\n")
if pools_info:
return pools_info
return "There are no pools"
def _show_pool(self, pool_key):
try:
drv_vif = drivers.PodVIFDriver.get_instance()
drv_vif_pool = drivers.VIFPoolDriver.get_instance()
drv_vif_pool.set_vif_driver(drv_vif)
pool = drv_vif_pool.show_pool(pool_key)
except TypeError as ex:
LOG.error("Invalid driver type")
raise ex
if pool:
pool_info = ""
for pool_id in pool:
pool_info += str(pool_id) + "\n"
return pool_info
else:
return "Empty pool"
class PoolManager(object):
"""Manages the ports pool enabling population and free actions.

View File

@ -74,6 +74,47 @@ class TestRequestHandler(test_base.TestCase):
m_end_headers.assert_called_once()
m_write.assert_called_once_with(expected_resp)
def _do_GET_helper(self, method, method_resp, path, headers, body,
expected_resp, trigger_exception, pool_key=None):
self._req_handler.headers = headers
self._req_handler.path = path
with mock.patch.object(self._req_handler.rfile, 'read') as m_read,\
mock.patch.object(self._req_handler,
'_list_pools') as m_list,\
mock.patch.object(self._req_handler,
'_show_pool') as m_show:
m_read.return_value = body
if trigger_exception:
m_list.side_effect = Exception
m_show.side_effect = Exception
else:
m_list.return_value = method_resp
m_show.return_value = method_resp
with mock.patch.object(self._req_handler,
'send_header') as m_send_header,\
mock.patch.object(self._req_handler,
'end_headers') as m_end_headers,\
mock.patch.object(self._req_handler.wfile,
'write') as m_write:
self._req_handler.do_GET()
if method == 'list':
m_list.assert_called_once()
if method == 'show':
if pool_key and len(pool_key) == 3:
m_show.assert_called_once_with(
(pool_key[0], pool_key[1],
tuple(sorted(pool_key[2]))))
else:
m_show.assert_not_called()
m_send_header.assert_called_once_with('Content-Length',
len(expected_resp))
m_end_headers.assert_called_once()
m_write.assert_called_once_with(expected_resp)
def test_do_POST_populate(self):
method = 'create'
path = "http://localhost/populatePool"
@ -182,3 +223,137 @@ class TestRequestHandler(test_base.TestCase):
self._do_POST_helper(method, path, headers, body, expected_resp,
trigger_exception, trunk_ips)
def test_do_GET_list(self):
method = 'list'
method_resp = ('["10.0.0.6", "9d2b45c4efaa478481c30340b49fd4d2", '
'["00efc78c-f11c-414a-bfcd-a82e16dc07d1", '
'"fd6b13dc-7230-4cbe-9237-36b4614bc6b5"]] '
'has 5 ports')
path = "http://localhost/listPools"
body = jsonutils.dumps({})
headers = {'Content-Type': 'application/json', 'Connection': 'close'}
headers['Content-Length'] = len(body)
trigger_exception = False
expected_resp = ('Pools:\n{0}'.format(method_resp)).encode()
self._do_GET_helper(method, method_resp, path, headers, body,
expected_resp, trigger_exception)
def test_do_GET_list_exception(self):
method = 'list'
method_resp = ('["10.0.0.6", "9d2b45c4efaa478481c30340b49fd4d2", '
'["00efc78c-f11c-414a-bfcd-a82e16dc07d1", '
'"fd6b13dc-7230-4cbe-9237-36b4614bc6b5"]] '
'has 5 ports')
path = "http://localhost/listPools"
body = jsonutils.dumps({})
headers = {'Content-Type': 'application/json', 'Connection': 'close'}
headers['Content-Length'] = len(body)
trigger_exception = True
expected_resp = ('Error listing the pools.').encode()
self._do_GET_helper(method, method_resp, path, headers, body,
expected_resp, trigger_exception)
def test_do_GET_list_empty(self):
method = 'list'
method_resp = 'There are no pools'
path = "http://localhost/listPools"
body = jsonutils.dumps({})
headers = {'Content-Type': 'application/json', 'Connection': 'close'}
headers['Content-Length'] = len(body)
trigger_exception = False
expected_resp = ('Pools:\n{0}'.format(method_resp)).encode()
self._do_GET_helper(method, method_resp, path, headers, body,
expected_resp, trigger_exception)
def test_do_GET_show(self):
method = 'show'
method_resp = "251f748d-2a0d-4143-bce8-2e616f7a6a4a"
path = "http://localhost/showPool"
pool_key = [u"10.0.0.6", u"9d2b45c4efaa478481c30340b49fd4d2",
[u"00efc78c-f11c-414a-bfcd-a82e16dc07d1",
u"fd6b13dc-7230-4cbe-9237-36b4614bc6b5"]]
body = jsonutils.dumps({"pool_key": pool_key})
headers = {'Content-Type': 'application/json', 'Connection': 'close'}
headers['Content-Length'] = len(body)
trigger_exception = False
formated_key = (pool_key[0], pool_key[1], tuple(sorted(pool_key[2])))
expected_resp = ('Pool {0} ports are:\n{1}'
.format(formated_key, method_resp)).encode()
self._do_GET_helper(method, method_resp, path, headers, body,
expected_resp, trigger_exception, pool_key)
def test_do_GET_show_exception(self):
method = 'show'
method_resp = "251f748d-2a0d-4143-bce8-2e616f7a6a4a"
path = "http://localhost/showPool"
pool_key = [u"10.0.0.6", u"9d2b45c4efaa478481c30340b49fd4d2",
[u"00efc78c-f11c-414a-bfcd-a82e16dc07d1",
u"fd6b13dc-7230-4cbe-9237-36b4614bc6b5"]]
body = jsonutils.dumps({"pool_key": pool_key})
headers = {'Content-Type': 'application/json', 'Connection': 'close'}
headers['Content-Length'] = len(body)
trigger_exception = True
formated_key = (pool_key[0], pool_key[1], tuple(sorted(pool_key[2])))
expected_resp = ('Error showing pool: {0}.'
.format(formated_key)).encode()
self._do_GET_helper(method, method_resp, path, headers, body,
expected_resp, trigger_exception, pool_key)
def test_do_GET_show_empty(self):
method = 'show'
method_resp = "Empty pool"
path = "http://localhost/showPool"
pool_key = [u"10.0.0.6", u"9d2b45c4efaa478481c30340b49fd4d2",
[u"00efc78c-f11c-414a-bfcd-a82e16dc07d1",
u"fd6b13dc-7230-4cbe-9237-36b4614bc6b5"]]
body = jsonutils.dumps({"pool_key": pool_key})
headers = {'Content-Type': 'application/json', 'Connection': 'close'}
headers['Content-Length'] = len(body)
trigger_exception = False
formated_key = (pool_key[0], pool_key[1], tuple(sorted(pool_key[2])))
expected_resp = ('Pool {0} ports are:\n{1}'
.format(formated_key, method_resp)).encode()
self._do_GET_helper(method, method_resp, path, headers, body,
expected_resp, trigger_exception, pool_key)
def test_do_GET_show_wrong_key(self):
method = 'show'
method_resp = ""
path = "http://localhost/showPool"
pool_key = [u"10.0.0.6", u"9d2b45c4efaa478481c30340b49fd4d2"]
body = jsonutils.dumps({"pool_key": pool_key})
headers = {'Content-Type': 'application/json', 'Connection': 'close'}
headers['Content-Length'] = len(body)
trigger_exception = False
expected_resp = ('Invalid pool key. Proper format is:\n[trunk_ip, '
'project_id, [security_groups]]\n').encode()
self._do_GET_helper(method, method_resp, path, headers, body,
expected_resp, trigger_exception, pool_key)
def test_do_GET_wrong_action(self):
method = 'fake'
method_resp = ""
path = "http://localhost/fakeMethod"
body = jsonutils.dumps({})
headers = {'Content-Type': 'application/json', 'Connection': 'close'}
headers['Content-Length'] = len(body)
trigger_exception = False
expected_resp = ('Method not allowed.').encode()
self._do_GET_helper(method, method_resp, path, headers, body,
expected_resp, trigger_exception)