From 6393571281ff749660a65f239664266baf3fc6ed Mon Sep 17 00:00:00 2001 From: Tony Mack Date: Tue, 26 Mar 2013 13:23:12 -0400 Subject: [PATCH] initial checkin --- plstackapi/planetstack/config.py | 240 +++++++++++++++++++++ plstackapi/util/__init__.py | 0 plstackapi/util/xml.py | 347 +++++++++++++++++++++++++++++++ 3 files changed, 587 insertions(+) create mode 100644 plstackapi/planetstack/config.py create mode 100644 plstackapi/util/__init__.py create mode 100644 plstackapi/util/xml.py diff --git a/plstackapi/planetstack/config.py b/plstackapi/planetstack/config.py new file mode 100644 index 0000000..c9ee5f2 --- /dev/null +++ b/plstackapi/planetstack/config.py @@ -0,0 +1,240 @@ +#!/usr/bin/python +import sys +import os +import time +import ConfigParser +import tempfile +import codecs +from StringIO import StringIO +from PLC.Xml import Xml + +default_config = \ +""" +""" + +def isbool(v): + return v.lower() in ("true", "false") + +def str2bool(v): + return v.lower() in ("true", "1") + +class Config: + + def __init__(self, config_file='/etc/planetlab/plcapi_config'): + self._files = [] + self.config_path = os.path.dirname(config_file) + self.config = ConfigParser.ConfigParser() + self.filename = config_file + if not os.path.isfile(self.filename): + self.create(self.filename) + self.load(self.filename) + + + def _header(self): + header = """ +DO NOT EDIT. This file was automatically generated at +%s from: + +%s +""" % (time.asctime(), os.linesep.join(self._files)) + + # Get rid of the surrounding newlines + return header.strip().split(os.linesep) + + def create(self, filename): + if not os.path.exists(os.path.dirname(filename)): + os.makedirs(os.path.dirname(filename)) + configfile = open(filename, 'w') + configfile.write(default_config) + configfile.close() + + + def load(self, filename): + if filename: + try: + self.config.read(filename) + except ConfigParser.MissingSectionHeaderError: + if filename.endswith('.xml'): + self.load_xml(filename) + else: + self.load_shell(filename) + self._files.append(filename) + self.set_attributes() + + def load_xml(self, filename): + xml = XML(filename) + categories = xml.xpath('//configuration/variables/category') + for category in categories: + section_name = category.get('id') + if not self.config.has_section(section_name): + self.config.add_section(section_name) + options = category.xpath('./variablelist/variable') + for option in options: + option_name = option.get('id') + value = option.xpath('./value')[0].text + if not value: + value = "" + self.config.set(section_name, option_name, value) + + def load_shell(self, filename): + f = open(filename, 'r') + for line in f: + try: + if line.startswith('#'): + continue + parts = line.strip().split("=") + if len(parts) < 2: + continue + option = parts[0] + value = parts[1].replace('"', '').replace("'","") + section, var = self.locate_varname(option, strict=False) + if section and var: + self.set(section, var, value) + except: + pass + f.close() + + def locate_varname(self, varname, strict=True): + varname = varname.lower() + sections = self.config.sections() + section_name = "" + var_name = "" + for section in sections: + if varname.startswith(section.lower()) and len(section) > len(section_name): + section_name = section.lower() + var_name = varname.replace(section_name, "")[1:] + if strict and not self.config.has_option(section_name, var_name): + raise ConfigParser.NoOptionError(var_name, section_name) + return (section_name, var_name) + + def set_attributes(self): + sections = self.config.sections() + for section in sections: + for item in self.config.items(section): + name = "%s_%s" % (section, item[0]) + value = item[1] + if isbool(value): + value = str2bool(value) + elif value.isdigit(): + value = int(value) + setattr(self, name, value) + setattr(self, name.upper(), value) + + + def verify(self, config1, config2, validate_method): + return True + + def validate_type(self, var_type, value): + return True + + @staticmethod + def is_xml(config_file): + try: + x = Xml(config_file) + return True + except: + return False + + @staticmethod + def is_ini(config_file): + try: + c = ConfigParser.ConfigParser() + c.read(config_file) + return True + except ConfigParser.MissingSectionHeaderError: + return False + + + def dump(self, sections = []): + sys.stdout.write(output_python()) + + def output_python(self, encoding = "utf-8"): + buf = codecs.lookup(encoding)[3](StringIO()) + buf.writelines(["# " + line + os.linesep for line in self._header()]) + + for section in self.sections(): + buf.write("[%s]%s" % (section, os.linesep)) + for (name,value) in self.items(section): + buf.write("%s=%s%s" % (name,value,os.linesep)) + buf.write(os.linesep) + return buf.getvalue() + + def output_shell(self, show_comments = True, encoding = "utf-8"): + """ + Return variables as a shell script. + """ + + buf = codecs.lookup(encoding)[3](StringIO()) + buf.writelines(["# " + line + os.linesep for line in self._header()]) + + for section in self.sections(): + for (name,value) in self.items(section): + # bash does not have the concept of NULL + if value: + option = "%s_%s" % (section.upper(), name.upper()) + if isbool(value): + value = str(str2bool(value)) + elif not value.isdigit(): + value = '"%s"' % value + buf.write(option + "=" + value + os.linesep) + return buf.getvalue() + + def output_php(self, encoding = "utf-8"): + """ + Return variables as a PHP script. + """ + + buf = codecs.lookup(encoding)[3](StringIO()) + buf.write("" + os.linesep) + + return buf.getvalue() + + def output_xml(self, encoding = "utf-8"): + pass + + def output_variables(self, encoding="utf-8"): + """ + Return list of all variable names. + """ + + buf = codecs.lookup(encoding)[3](StringIO()) + for section in self.sections(): + for (name,value) in self.items(section): + option = "%s_%s" % (section,name) + buf.write(option + os.linesep) + + return buf.getvalue() + pass + + def write(self, filename=None): + if not filename: + filename = self.filename + configfile = open(filename, 'w') + self.config.write(configfile) + + def save(self, filename=None): + self.write(filename) + + def __getattr__(self, attr): + return getattr(self.config, attr) + +if __name__ == '__main__': + filename = None + if len(sys.argv) > 1: + filename = sys.argv[1] + config = Config(filename) + else: + config = Config() + config.dump() diff --git a/plstackapi/util/__init__.py b/plstackapi/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plstackapi/util/xml.py b/plstackapi/util/xml.py new file mode 100644 index 0000000..7b6c72a --- /dev/null +++ b/plstackapi/util/xml.py @@ -0,0 +1,347 @@ +#!/usr/bin/python +from types import StringTypes +from lxml import etree +from StringIO import StringIO + +# helper functions to help build xpaths +class XpathFilter: + @staticmethod + + def filter_value(key, value): + xpath = "" + if isinstance(value, str): + if '*' in value: + value = value.replace('*', '') + xpath = 'contains(%s, "%s")' % (key, value) + else: + xpath = '%s="%s"' % (key, value) + return xpath + + @staticmethod + def xpath(filter={}): + xpath = "" + if filter: + filter_list = [] + for (key, value) in filter.items(): + if key == 'text': + key = 'text()' + else: + key = '@'+key + if isinstance(value, str): + filter_list.append(XpathFilter.filter_value(key, value)) + elif isinstance(value, list): + stmt = ' or '.join([XpathFilter.filter_value(key, str(val)) for val in value]) + filter_list.append(stmt) + if filter_list: + xpath = ' and '.join(filter_list) + xpath = '[' + xpath + ']' + return xpath + +# a wrapper class around lxml.etree._Element +# the reason why we need this one is because of the limitations +# we've found in xpath to address documents with multiple namespaces defined +# in a nutshell, we deal with xml documents that have +# a default namespace defined (xmlns="http://default.com/") and specific prefixes defined +# (xmlns:foo="http://foo.com") +# according to the documentation instead of writing +# element.xpath ( "//node/foo:subnode" ) +# we'd then need to write xpaths like +# element.xpath ( "//{http://default.com/}node/{http://foo.com}subnode" ) +# which is a real pain.. +# So just so we can keep some reasonable programming style we need to manage the +# namespace map that goes with the _Element (its internal .nsmap being unmutable) +class XmlElement: + def __init__(self, element, namespaces): + self.element = element + self.namespaces = namespaces + + # redefine as few methods as possible + def xpath(self, xpath, namespaces=None): + if not namespaces: + namespaces = self.namespaces + elems = self.element.xpath(xpath, namespaces=namespaces) + return [XmlElement(elem, namespaces) for elem in elems] + + def add_element(self, tagname, **kwds): + element = etree.SubElement(self.element, tagname, **kwds) + return XmlElement(element, self.namespaces) + + def append(self, elem): + if isinstance(elem, XmlElement): + self.element.append(elem.element) + else: + self.element.append(elem) + + def getparent(self): + return XmlElement(self.element.getparent(), self.namespaces) + + def get_instance(self, instance_class=None, fields=[]): + """ + Returns an instance (dict) of this xml element. The instance + holds a reference to this xml element. + """ + if not instance_class: + instance_class = Object + if not fields and hasattr(instance_class, 'fields'): + fields = instance_class.fields + + if not fields: + instance = instance_class(self.attrib, self) + else: + instance = instance_class({}, self) + for field in fields: + if field in self.attrib: + instance[field] = self.attrib[field] + return instance + + def add_instance(self, name, instance, fields=[]): + """ + Adds the specifed instance(s) as a child element of this xml + element. + """ + if not fields and hasattr(instance, 'keys'): + fields = instance.keys() + elem = self.add_element(name) + for field in fields: + if field in instance and instance[field]: + elem.set(field, unicode(instance[field])) + return elem + + def remove_elements(self, name): + """ + Removes all occurences of an element from the tree. Start at + specified root_node if specified, otherwise start at tree's root. + """ + + if not element_name.startswith('//'): + element_name = '//' + element_name + elements = self.element.xpath('%s ' % name, namespaces=self.namespaces) + for element in elements: + parent = element.getparent() + parent.remove(element) + + def delete(self): + parent = self.getparent() + parent.remove(self) + + def remove(self, element): + if isinstance(element, XmlElement): + self.element.remove(element.element) + else: + self.element.remove(element) + + def set_text(self, text): + self.element.text = text + + # Element does not have unset ?!? + def unset(self, key): + del self.element.attrib[key] + + def toxml(self): + return etree.tostring(self.element, encoding='UTF-8', pretty_print=True) + + def __str__(self): + return self.toxml() + + # are redirected on self.element + def __getattr__ (self, name): + if not hasattr(self.element, name): + raise AttributeError, name + return getattr(self.element, name) + +class Xml: + + def __init__(self, xml=None, namespaces=None): + self.root = None + self.namespaces = namespaces + self.default_namespace = None + self.schema = None + if isinstance(xml, basestring): + self.parse_xml(xml) + if isinstance(xml, XmlElement): + self.root = xml + self.namespaces = xml.namespaces + elif isinstance(xml, etree._ElementTree) or isinstance(xml, etree._Element): + self.parse_xml(etree.tostring(xml)) + + def parse_xml(self, xml): + """ + parse rspec into etree + """ + parser = etree.XMLParser(remove_blank_text=True) + try: + tree = etree.parse(xml, parser) + except IOError: + # 'rspec' file doesnt exist. 'rspec' is proably an xml string + try: + tree = etree.parse(StringIO(xml), parser) + except Exception, e: + raise Exception, str(e) + root = tree.getroot() + self.namespaces = dict(root.nsmap) + # set namespaces map + if 'default' not in self.namespaces and None in self.namespaces: + # If the 'None' exist, then it's pointing to the default namespace. This makes + # it hard for us to write xpath queries for the default naemspace because lxml + # wont understand a None prefix. We will just associate the default namespeace + # with a key named 'default'. + self.namespaces['default'] = self.namespaces.pop(None) + + else: + self.namespaces['default'] = 'default' + + self.root = XmlElement(root, self.namespaces) + # set schema + for key in self.root.attrib.keys(): + if key.endswith('schemaLocation'): + # schemaLocation should be at the end of the list. + # Use list comprehension to filter out empty strings + schema_parts = [x for x in self.root.attrib[key].split(' ') if x] + self.schema = schema_parts[1] + namespace, schema = schema_parts[0], schema_parts[1] + break + + def parse_dict(self, d, root_tag_name='xml', element = None): + if element is None: + if self.root is None: + self.parse_xml('<%s/>' % root_tag_name) + element = self.root.element + + if 'text' in d: + text = d.pop('text') + element.text = text + + # handle repeating fields + for (key, value) in d.items(): + if isinstance(value, list): + value = d.pop(key) + for val in value: + if isinstance(val, dict): + child_element = etree.SubElement(element, key) + self.parse_dict(val, key, child_element) + elif isinstance(val, basestring): + child_element = etree.SubElement(element, key).text = val + + elif isinstance(value, int): + d[key] = unicode(d[key]) + elif value is None: + d.pop(key) + + # element.attrib.update will explode if DateTimes are in the + # dcitionary. + d=d.copy() + # looks like iteritems won't stand side-effects + for k in d.keys(): + if not isinstance(d[k],StringTypes): + del d[k] + + element.attrib.update(d) + + def validate(self, schema): + """ + Validate against rng schema + """ + relaxng_doc = etree.parse(schema) + relaxng = etree.RelaxNG(relaxng_doc) + if not relaxng(self.root): + error = relaxng.error_log.last_error + message = "%s (line %s)" % (error.message, error.line) + raise Exception, message + return True + + def xpath(self, xpath, namespaces=None): + if not namespaces: + namespaces = self.namespaces + return self.root.xpath(xpath, namespaces=namespaces) + + def set(self, key, value): + return self.root.set(key, value) + + def remove_attribute(self, name, element=None): + if not element: + element = self.root + element.remove_attribute(name) + + def add_element(self, *args, **kwds): + """ + Wrapper around etree.SubElement(). Adds an element to + specified parent node. Adds element to root node is parent is + not specified. + """ + return self.root.add_element(*args, **kwds) + + def remove_elements(self, name, element = None): + """ + Removes all occurences of an element from the tree. Start at + specified root_node if specified, otherwise start at tree's root. + """ + if not element: + element = self.root + + element.remove_elements(name) + + def add_instance(self, *args, **kwds): + return self.root.add_instance(*args, **kwds) + + def get_instance(self, *args, **kwds): + return self.root.get_instnace(*args, **kwds) + + def get_element_attributes(self, elem=None, depth=0): + if elem == None: + elem = self.root + if not hasattr(elem, 'attrib'): + # this is probably not an element node with attribute. could be just and an + # attribute, return it + return elem + attrs = dict(elem.attrib) + attrs['text'] = str(elem.text).strip() + attrs['parent'] = elem.getparent() + if isinstance(depth, int) and depth > 0: + for child_elem in list(elem): + key = str(child_elem.tag) + if key not in attrs: + attrs[key] = [self.get_element_attributes(child_elem, depth-1)] + else: + attrs[key].append(self.get_element_attributes(child_elem, depth-1)) + else: + attrs['child_nodes'] = list(elem) + return attrs + + def append(self, elem): + return self.root.append(elem) + + def iterchildren(self): + return self.root.iterchildren() + + def merge(self, in_xml): + pass + + def __str__(self): + return self.toxml() + + def toxml(self): + return etree.tostring(self.root.element, encoding='UTF-8', pretty_print=True) + + # XXX smbaker, for record.load_from_string + def todict(self, elem=None): + if elem is None: + elem = self.root + d = {} + d.update(elem.attrib) + d['text'] = elem.text + for child in elem.iterchildren(): + if child.tag not in d: + d[child.tag] = [] + d[child.tag].append(self.todict(child)) + + if len(d)==1 and ("text" in d): + d = d["text"] + + return d + + def save(self, filename): + f = open(filename, 'w') + f.write(self.toxml()) + f.close() + + -- 2.47.0