Skip to content

Commit

Permalink
Merge pull request #404 from jlongstaf/feature.lb-stats
Browse files Browse the repository at this point in the history
Implement lbaas-loadbalancer-stats command
  • Loading branch information
richbrowne authored Dec 16, 2016
2 parents 7a88c25 + 8c177bb commit c3beff9
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 102 deletions.
11 changes: 11 additions & 0 deletions f5_openstack_agent/lbaasv2/drivers/bigip/agent_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,17 @@ def delete_loadbalancer(self, context, loadbalancer, service):
except Exception as exc:
LOG.error("Exception: %s" % exc.message)

@log_helpers.log_method_call
def update_loadbalancer_stats(self, context, loadbalancer, service):
"""Handle RPC cast from plugin to get stats."""
try:
self.lbdriver.get_stats(service)
self.cache.put(service, self.agent_host)
except q_exception.NeutronException as exc:
LOG.error("q_exception.NeutronException: %s" % exc.msg)
except Exception as exc:
LOG.error("Exception: %s" % exc.message)

@log_helpers.log_method_call
def create_listener(self, context, listener, service):
"""Handle RPC cast from plugin to create_listener."""
Expand Down
130 changes: 29 additions & 101 deletions f5_openstack_agent/lbaasv2/drivers/bigip/icontrol_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,7 @@
SystemHelper
from f5_openstack_agent.lbaasv2.drivers.bigip.tenants import \
BigipTenantManager
from f5_openstack_agent.lbaasv2.drivers.bigip.utils import OBJ_PREFIX
from f5_openstack_agent.lbaasv2.drivers.bigip.utils import serialized
from f5_openstack_agent.lbaasv2.drivers.bigip.utils import strip_domain_address
from f5_openstack_agent.lbaasv2.drivers.bigip.virtual_address import \
VirtualAddress

Expand Down Expand Up @@ -851,105 +849,35 @@ def delete_health_monitor(self, health_monitor, service):

@is_connected
def get_stats(self, service):
"""Get service stats"""
# use pool stats because the pool_id is the
# the service definition...
stats = {}
stats[lb_const.STATS_IN_BYTES] = 0
stats[lb_const.STATS_OUT_BYTES] = 0
stats[lb_const.STATS_ACTIVE_CONNECTIONS] = 0
stats[lb_const.STATS_TOTAL_CONNECTIONS] = 0
# add a members stats return dictionary
members = {}
for hostbigip in self.get_all_bigips():
# It appears that stats are collected for pools in a pending delete
# state which means that if those messages are queued (or delayed)
# it can result in the process of a stats request after the pool
# and tenant are long gone. Check if the tenant exists.
if not service['pool'] or not hostbigip.system.folder_exists(
OBJ_PREFIX + service['pool']['tenant_id']):
return None
pool = service['pool']
pool_stats = hostbigip.pool.get_statistics(
name=pool['id'],
folder=pool['tenant_id'],
config_mode=self.conf.icontrol_config_mode)
if 'STATISTIC_SERVER_SIDE_BYTES_IN' in pool_stats:
stats[lb_const.STATS_IN_BYTES] += \
pool_stats['STATISTIC_SERVER_SIDE_BYTES_IN']
stats[lb_const.STATS_OUT_BYTES] += \
pool_stats['STATISTIC_SERVER_SIDE_BYTES_OUT']
stats[lb_const.STATS_ACTIVE_CONNECTIONS] += \
pool_stats['STATISTIC_SERVER_SIDE_CURRENT_CONNECTIONS']
stats[lb_const.STATS_TOTAL_CONNECTIONS] += \
pool_stats['STATISTIC_SERVER_SIDE_TOTAL_CONNECTIONS']
# are there members to update status
if 'members' in service:
# only query BIG-IP® pool members if they
# not in a state indicating provisioning or error
# provisioning the pool member
some_members_require_status_update = False
update_if_status = [plugin_const.ACTIVE,
plugin_const.DOWN,
plugin_const.INACTIVE]
if plugin_const.ACTIVE not in update_if_status:
update_if_status.append(plugin_const.ACTIVE)

for member in service['members']:
if member['status'] in update_if_status:
some_members_require_status_update = True
# are we have members who are in a
# state to update there status
if some_members_require_status_update:
# query pool members on each BIG-IP
monitor_states = \
hostbigip.pool.get_members_monitor_status(
name=pool['id'],
folder=pool['tenant_id'],
config_mode=self.conf.icontrol_config_mode
)
for member in service['members']:
if member['status'] in update_if_status:
# create the entry for this
# member in the return status
# dictionary set to ACTIVE
if not member['id'] in members:
members[member['id']] = \
{'status': plugin_const.INACTIVE}
# check if it down or up by monitor
# and update the status
for state in monitor_states:
# matched the pool member
# by address and port number
if member['address'] == \
strip_domain_address(
state['addr']) and \
int(member['protocol_port']) == \
int(state['port']):
# if the monitor says member is up
if state['state'] == \
'MONITOR_STATUS_UP' or \
state['state'] == \
'MONITOR_STATUS_UNCHECKED':
# set ACTIVE as long as the
# status was not set to 'DOWN'
# on another BIG-IP
if members[
member['id']]['status'] != \
'DOWN':
if member['admin_state_up']:
members[member['id']][
'status'] = \
plugin_const.ACTIVE
else:
members[member['id']][
'status'] = \
plugin_const.INACTIVE
else:
members[member['id']]['status'] = \
plugin_const.DOWN
stats['members'] = members
return stats
lb_stats = {}
stats = ['clientside.bitsIn',
'clientside.bitsOut',
'clientside.curConns',
'clientside.totConns']
loadbalancer = service['loadbalancer']

try:
# sum virtual server stats for all BIG-IPs
vs_stats = self.lbaas_builder.get_listener_stats(service, stats)

# convert to bytes
lb_stats[lb_const.STATS_IN_BYTES] = \
vs_stats['clientside.bitsIn']/8
lb_stats[lb_const.STATS_OUT_BYTES] = \
vs_stats['clientside.bitsOut']/8
lb_stats[lb_const.STATS_ACTIVE_CONNECTIONS] = \
vs_stats['clientside.curConns']
lb_stats[lb_const.STATS_TOTAL_CONNECTIONS] = \
vs_stats['clientside.totConns']

# update Neutron
self.plugin_rpc.update_loadbalancer_stats(
loadbalancer['id'], lb_stats)
except Exception as e:
LOG.error("Error getting loadbalancer stats: %s", e.message)

finally:
return lb_stats

@serialized('remove_orphans')
def remove_orphans(self, all_loadbalancers):
Expand Down
34 changes: 34 additions & 0 deletions f5_openstack_agent/lbaasv2/drivers/bigip/lbaas_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,3 +412,37 @@ def listener_exists(self, bigip, service):
return False

return True

def get_listener_stats(self, service, stats):
"""Get statistics for a loadbalancer service.
Sums values for stats defined in stats dictionary for all listeners
defined in service object. For example, if loadbalancer has two
listeners and stats defines a stat 'clientside.bitsIn' as a key, the
sum of all pools' clientside.bitsIn will be returned in stats.
Provisioning status is ignored -- PENDING_DELETE objects are
included.
:param service: defines loadbalancer and set of pools.
:param stats: a dictionary that defines which stats to get.
Should be initialized by caller with 0 values.
:return: stats are appended to input stats dict (i.e., contains
the sum of given stats for all BIG-IPs).
"""

listeners = service["listeners"]
loadbalancer = service["loadbalancer"]
bigips = self.driver.get_config_bigips()

collected_stats = {}
for stat in stats:
collected_stats[stat] = 0

for listener in listeners:
svc = {"loadbalancer": loadbalancer, "listener": listener}
vs_stats = self.listener_builder.get_stats(svc, bigips, stats)
for stat in stats:
collected_stats[stat] += vs_stats[stat]

return collected_stats
2 changes: 1 addition & 1 deletion f5_openstack_agent/lbaasv2/drivers/bigip/lbaas_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def backup_configuration(self):
raise NotImplementedError()

def get_stats(self, service):
"""Get Stats for a Pool Service """
"""Get Stats for a loadbalancer Service """
raise NotImplementedError()

def exists(self, service):
Expand Down
35 changes: 35 additions & 0 deletions f5_openstack_agent/lbaasv2/drivers/bigip/listener_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,3 +423,38 @@ def _remove_cookie_persist_rule(self, vip, bigip):
obj = r.load(name=rule_name, partition=vip["partition"])
obj.delete()
LOG.debug("Deleted rule %s" % rule_name)

def get_stats(self, service, bigips, stat_keys):
"""Return stat values for a single virtual.
Stats to collect are defined as an array of strings in input stats.
Values are summed across one or more BIG-IPs defined in input bigips.
:param service: Has listener name/partition
:param bigips: One or more BIG-IPs to get listener stats from.
:param stat_keys: Array of strings that define which stats to collect.
:return: A dict with key/value pairs for each stat defined in
input stats.
"""
collected_stats = {}
for stat_key in stat_keys:
collected_stats[stat_key] = 0

virtual = self.service_adapter.get_virtual(service)
part = virtual["partition"]
for bigip in bigips:
try:
vs_stats = self.vs_helper.get_stats(
bigip,
name=virtual["name"],
partition=part,
stat_keys=stat_keys)
for stat_key in stat_keys:
if stat_key in vs_stats:
collected_stats[stat_key] += vs_stats[stat_key]

except Exception as e:
# log error but continue on
LOG.error("Error getting virtual server stats: %s", e.message)

return collected_stats
13 changes: 13 additions & 0 deletions f5_openstack_agent/lbaasv2/drivers/bigip/plugin_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,19 @@ def update_loadbalancer_status(self,
topic=self.topic
)

@log_helpers.log_method_call
def update_loadbalancer_stats(self,
lb_id,
stats):
"""Update the database with loadbalancer stats."""
return self._cast(
self.context,
self._make_msg('update_loadbalancer_stats',
loadbalancer_id=lb_id,
stats=stats),
topic=self.topic
)

@log_helpers.log_method_call
def loadbalancer_destroyed(self, loadbalancer_id):
"""Delete the loadbalancer from the database."""
Expand Down
40 changes: 40 additions & 0 deletions f5_openstack_agent/lbaasv2/drivers/bigip/resource_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,43 @@ def _collection(self, bigip):
"resource %s", self.resource_type)
raise KeyError("No collection available for %s" %
(self.resource_type))

def get_stats(self, bigip, name=None, partition=None, stat_keys=[]):
"""Returns dictionary of stats.
Use by calling with an array of stats to get from resource. Return
value will be a dict with key/value pairs. The stat key will only
be included in the return dict if the resource includes that stat.
:param bigip: BIG-IP to get stats from.
:param name: name of resource object.
:param partition: partition where to get resource.
:param stat_keys: Array of strings that define stats to collect.
:return: dictionary with key/value pairs where key is string
defined in input array, if present in resource stats, and value
as the value of resource stats 'value' key.
"""
collected_stats = {}

# get resource, then its stats
if self.exists(bigip, name=name, partition=partition):
resource = self.load(bigip, name=name, partition=partition)
resource_stats = resource.stats.load()
stat_entries = resource_stats.entries

# Difference between 11.6 and 12.1. Stats in 12.1 are embedded
# in nestedStats. In 11.6, they are directly accessible in entries.
if stat_keys[0] not in stat_entries:
# find nestedStats
for key in stat_entries.keys():
value = stat_entries.get(key, None)
if 'nestedStats' in value:
stat_entries = value['nestedStats']['entries']

# add stats defined in input stats array
for stat_key in stat_keys:
if stat_key in stat_entries:
collected_stats[stat_key] = \
stat_entries[stat_key]['value']

return collected_stats

0 comments on commit c3beff9

Please sign in to comment.