diff --git a/f5_openstack_agent/lbaasv2/drivers/bigip/agent_manager.py b/f5_openstack_agent/lbaasv2/drivers/bigip/agent_manager.py index de02d5af5..e65cb813f 100644 --- a/f5_openstack_agent/lbaasv2/drivers/bigip/agent_manager.py +++ b/f5_openstack_agent/lbaasv2/drivers/bigip/agent_manager.py @@ -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.""" diff --git a/f5_openstack_agent/lbaasv2/drivers/bigip/icontrol_driver.py b/f5_openstack_agent/lbaasv2/drivers/bigip/icontrol_driver.py index 95b1de457..d79c14d20 100644 --- a/f5_openstack_agent/lbaasv2/drivers/bigip/icontrol_driver.py +++ b/f5_openstack_agent/lbaasv2/drivers/bigip/icontrol_driver.py @@ -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 @@ -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): diff --git a/f5_openstack_agent/lbaasv2/drivers/bigip/lbaas_builder.py b/f5_openstack_agent/lbaasv2/drivers/bigip/lbaas_builder.py index fdea2b7b7..890de6028 100644 --- a/f5_openstack_agent/lbaasv2/drivers/bigip/lbaas_builder.py +++ b/f5_openstack_agent/lbaasv2/drivers/bigip/lbaas_builder.py @@ -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 diff --git a/f5_openstack_agent/lbaasv2/drivers/bigip/lbaas_driver.py b/f5_openstack_agent/lbaasv2/drivers/bigip/lbaas_driver.py index ba1cd56f2..0c39a2755 100644 --- a/f5_openstack_agent/lbaasv2/drivers/bigip/lbaas_driver.py +++ b/f5_openstack_agent/lbaasv2/drivers/bigip/lbaas_driver.py @@ -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): diff --git a/f5_openstack_agent/lbaasv2/drivers/bigip/listener_service.py b/f5_openstack_agent/lbaasv2/drivers/bigip/listener_service.py index 975a83043..7a42cc98b 100644 --- a/f5_openstack_agent/lbaasv2/drivers/bigip/listener_service.py +++ b/f5_openstack_agent/lbaasv2/drivers/bigip/listener_service.py @@ -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 diff --git a/f5_openstack_agent/lbaasv2/drivers/bigip/plugin_rpc.py b/f5_openstack_agent/lbaasv2/drivers/bigip/plugin_rpc.py index 4c93dd159..54df8cfb7 100644 --- a/f5_openstack_agent/lbaasv2/drivers/bigip/plugin_rpc.py +++ b/f5_openstack_agent/lbaasv2/drivers/bigip/plugin_rpc.py @@ -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.""" diff --git a/f5_openstack_agent/lbaasv2/drivers/bigip/resource_helper.py b/f5_openstack_agent/lbaasv2/drivers/bigip/resource_helper.py index 708d16669..6353f2151 100644 --- a/f5_openstack_agent/lbaasv2/drivers/bigip/resource_helper.py +++ b/f5_openstack_agent/lbaasv2/drivers/bigip/resource_helper.py @@ -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