#!/usr/bin/python import json import jsonschema import collections from pcp.pmda import PMDA, pmdaMetric, pmdaIndom, pmdaInstid import cpmapi as c_api from pcp.pmapi import pmUnits, pmContext as PCP class STAP_JSON_PMDA(PMDA): def __init__(self, domain=255): # Load the schema and data. self.load_json_schema() self.load_json_data() # Make sure the data fits the schema. jsonschema.validate(self.json_data, self.schema) # Parse the schema header, looking for a name. self.pmda_name = "" self._parse_schema_header() PMDA.__init__(self, self.pmda_name, domain) self.configfile = "%s/%s/%s.conf" % (PCP.pmGetConfig('PCP_PMDAS_DIR'), self.pmda_name, self.pmda_name) self.connect_pmcd() # Parse the data portion of the schema header, creating # metrics as needed. self._parse_schema_data() self.set_fetch(self._fetch) self.set_fetch_callback(self._fetch_callback) self.set_store_callback(self._store_callback) self.set_user(PCP.pmGetConfig('PCP_USER')) def load_jscon_schema(self): # Load schema f = open("/proc/systemtap/json/schema") self.schema = json.load(f, object_pairs_hook=collections.OrderedDict) f.close() def load_json_data(self): # Load data f = open("/proc/systemtap/json/data") self.json_data = json.load(f, object_pairs_hook=collections.OrderedDict) f.close() def _parse_schema_header(self): ''' Go through the schema, looking for information we can use to create the pcp representation of the schema. Note that we don't support every possible JSON schema, we're looking for certain items. Refer to the following link for details of JSON schemas: ''' # First process the schema "header" information. self.data_header = None for (key, value) in self.schema.items(): # 'type' (required): Just sanity check it. if key == "type": if not isinstance(value, unicode) or value != "object": raise TypeError # 'title' (optional): Type check it. elif key == "title": if not isinstance(value, unicode): raise TypeError # 'description' (optional): Type check it. elif key == "description": if not isinstance(value, unicode): raise TypeError # 'additionalProperties' (optional): Ignore it. elif key == "additionalProperties": # Do nothing. pass # 'properties' (required): Type check it and save for later. elif key == "properties": if not isinstance(value, dict): raise TypeError self.data_header = value # For everything else, raise an error. else: raise RuntimeError, "Unknown attribute '%s'" % key # Pick the right field for the PMDA name - prefer "title" over # "description". if self.schema.has_key("title"): self.pmda_name = self.schema["title"] elif self.schema.has_key("description"): self.pmda_name = self.schema["description"] else: raise RuntimeError, "No 'title' or 'description' field in schema header" def _parse_schema_data(self): # If we're here, we know the "header" was reasonable. Now process # "properties", which is the data "header". if not self.data_header: raise RuntimeError, "Schema has no 'properties' attribute" data_properties = None for (key, value) in self.data_header.items(): # 'generation' (required): Just sanity check it. if key == "generation": if not isinstance(value, dict): raise TypeError # 'data' (required): Type check it. elif key == "data": if not isinstance(value, dict) \ or not value.has_key("properties") \ or not isinstance(value["properties"], dict): raise TypeError data_properties = value["properties"] # For everything else, raise an error. else: raise RuntimeError, "Unknown attribute '%s'" % key # If we're here, we know the data "header" was reasonable. Now process # "properties.data.properties", which is the real data description. if not data_properties: raise RuntimeError, "Schema has no 'properties.data.properties' attribute" metric_idx = 0 for (name, attributes) in data_properties.items(): metric_info = {} metric_info["name"] = name metric_info["oneline"] = "" for (key, value) in attributes.items(): # 'type' (required): Sanity check it and save it. if key == "type": if not isinstance(value, unicode): raise TypeError if value == "string": metric_info['type'] = c_api.PM_TYPE_STRING elif value == "integer": metric_info['type'] = c_api.PM_TYPE_64 # 'description' (optional): Type check it and save it. elif key == "description": if not isinstance(value, unicode): raise TypeError metric_info['oneline'] = value # 'minLength' (optional): Ignore it (for now). elif key == "minLength": # Do nothing. pass # 'additionalProperties' (optional): Ignore it. elif key == "additionalProperties": # Do nothing. pass # 'minimum' (optional): Ignore it (for now). elif key == "minimum": # Do nothing for now. pass # 'default' (optional): Ignore it (for now). elif key == "default": # Do nothing for now. pass # For everything else, raise an error. else: raise RuntimeError, \ ("Schema for '%s' has an unknown attribute '%s'" % (name, key)) # Make sure we have everything we need. if not metric_info.has_key("type"): raise RuntimeError, ("Schema for '%s' has no 'type' attribute" % name) # Add the metric. metric_info["pmid"] = self.pmid(0, metric_idx) metric = pmdaMetric(metric_info["pmid"], metric_info["type"], c_api.PM_INDOM_NULL, c_api.PM_SEM_COUNTER, pmUnits(0, 0, 0, 0, 0, 0)) self.add_metric('stap_json.' + self.pmda_name + '.' + name, metric, metric_info["oneline"]) self.data_idx[metric_idx] = name metric_idx += 1 def _fetch(self): ''' Called once per "fetch" PDU, before callbacks ''' self.load_json_data() def _fetch_callback(self, cluster, item, inst): ''' Main fetch callback. Returns a list of value,status (single pair) for requested pmid/inst. ''' if cluser != 0 or inst != 0: return [c_api.PM_ERR_PMID, 0] try: # Note that this will have to get more complicated once we # have instances. This only handles single valued items. metric_name = self.data_idx[item] return [metric_name, self.json_data['data'][metric_name]] except: return [c_api.PM_ERR_PMID, 0] def _store_callback(self, cluster, item, inst, val): ''' Store callback, executed when a request to write to a metric happens. Returns a single value. ''' # Since we don't support storing values, always fail. return c_api.PM_ERR_PERMISSION if __name__ == '__main__': STAP_JSON_PMDA(255).run()