# Copyright 1999-2017. Parallels IP Holdings GmbH. All Rights Reserved. import shutil import os, cStringIO from xml.dom import minidom from xml.dom import Node from encoder import EncoderFile from sys import argv, exit from xml.sax import make_parser from xml.sax.xmlreader import AttributesImpl from xml.sax.handler import ContentHandler import pmm_config import xml_transform import pmmcli_session import pmm_dump_transformer import pmm_api_xml_protocols import pmm_dump_access_service class InfoXmlHandler(ContentHandler): def __init__(self, xmlTransformSpecificationWorker, xmlTransformPathProcessor): self.__xmlTransformSpecificationWorker = xmlTransformSpecificationWorker # The attribute self.__xml_transform_path_processor is an instance of xml_transform_path_processor, # unique to one xml transform processing session. # Tracks current context path, should be passed to XMLTransformWorker to process XPath matches self.__xmlTransformPathProcessor = xmlTransformPathProcessor self.__elementValue = u'' self.__elementProcessingStack = [] def startElement(self, name, attrs): self.__elementValue = u'' self.__xmlTransformSpecificationWorker.addPathElement(name, attrs, self.__xmlTransformPathProcessor) parent_processing = None if len(self.__elementProcessingStack) > 0: parent_processing = self.__elementProcessingStack[len(self.__elementProcessingStack)-1][0] started = None # parent 'copy', 'filter-out' and 'skip' should override processing_type processing_type = parent_processing if processing_type in ['filter-out', 'skip']: pass elif processing_type == 'copy': self.__xmlTransformSpecificationWorker.startElement(name, attrs, self.__xmlTransformPathProcessor) started = 1 else: matched, processing_type, attributes_processing_type = self.__xmlTransformSpecificationWorker.matchNode( name, attrs, self.__xmlTransformPathProcessor) if matched and processing_type != 'skip': if attributes_processing_type == 'skip': attrs = AttributesImpl({}) if processing_type == 'process-auxiliary': attrs = AttributesImpl({'deployer-action': 'auxiliary'}) elif processing_type == 'process-content': attrs = AttributesImpl({'deployer-action': 'deploy-content'}) self.__xmlTransformSpecificationWorker.startElement(name, attrs, self.__xmlTransformPathProcessor) started = 1 else: # act like processing_type be 'filter-out' or 'skip' pass self.__elementProcessingStack.append((processing_type, started)) def characters(self, ch): self.__elementValue = self.__elementValue + ch def endElement(self, name): self.__xmlTransformSpecificationWorker.removePathElement(name, self.__xmlTransformPathProcessor) started = self.__elementProcessingStack[len(self.__elementProcessingStack)-1][1] if started: self.__xmlTransformSpecificationWorker.endElement(name, self.__elementValue) self.__elementValue = u'' self.__elementProcessingStack.pop() class InfoXMLProcessor: def __init__(self, info_xml, infoXmlHandler): self.__info_xml = info_xml self.__infoXmlHandler = infoXmlHandler def process(self): parser = make_parser() parser.setContentHandler(self.__infoXmlHandler) parser.parse(open(self.__info_xml)) #class to work with (XX)XMLTransformSpecification classes class XMLTransformWorker: def __init__(self, xml_transform_specification, xml_writer): self.__xml_transform_specification = xml_transform_specification self.__xml_writer = xml_writer def get_xml_writer(self): return self.__xml_writer def get_xml_transform_specification(self): return self.__xml_transform_specification def addPathElement(self, name, attrs, xmlTransformPathProcessor): return xmlTransformPathProcessor.addPathElement(name, attrs) def removePathElement(self, name, xmlTransformPathProcessor): return xmlTransformPathProcessor.removePathElement(name) def matchNode(self, name, attrs, xmlTransformPathProcessor): return self.__xml_transform_specification.match(name, attrs, xmlTransformPathProcessor) def startElement(self, name, attrs, xmlTransformPathProcessor = None): self.__xml_writer.startElement(name, attrs) def endElement(self, name, value): self.__xml_writer.endElement(name, value) class XMLTransformPathProcessor: def __init__(self): self.__exclude_from_path = ['Envelope', 'Data', 'migration-dump'] # self.__path is a list of the tuples (element_name, attribute_dictionary) self.__path = [] def addPathElement(self, name, attrs): if not name in self.__exclude_from_path: path_element = (name, attrs) self.__path.append(path_element) def removePathElement(self, name): if not name in self.__exclude_from_path: self.__path.pop() def getAttrRequiredPath(self, required_attribute_name, get_parent_path): guided_path = '' if get_parent_path: if len(self.__path) <= 1: return '' path_array = self.__path[:-1] else: path_array = self.__path for path_element in path_array: guided_path = guided_path + "/" + path_element[0] try: guid_value = path_element[1][required_attribute_name] guided_path = guided_path + "[@" + required_attribute_name + "='" + guid_value + "']" except KeyError: pass return guided_path # macth string 'path' with current context path def pathMatched(self, path): path_elements = path.split('/') # next allows both the path forms: "/root/element" and "root/element" if path_elements[0] == '': del path_elements[0] path_index = -1 if (len(path_elements)+1) != len(self.__path): return False for element in path_elements: path_index = path_index + 1 has_attr = False element_name = '' attr_value = '' attr_name = '' if element[-1] == ']' and element.find('[') > 0: attr_string_index = element.index('[') element_name = element[:attr_string_index] attr_string = element[attr_string_index+1:-1].strip() if attr_string[0] == "@": aps = attr_string[-1] aps_start_index = attr_string.find(aps) equal_index = attr_string.find('=') if (aps in ['"',"'"]) and (aps_start_index != (len(attr_string)-1)) and (equal_index < aps_start_index) and (equal_index > 2): attr_value = attr_string[attr_string[:-1].rindex(aps)+1:-1] attr_name = attr_string[1:equal_index].strip() else: element_name = element if self.__path[path_index][0] != element_name: return False if attr_name != '': attrs = self.__path[path_index][1] try: attribute_value = attrs[attr_name] if attr_value != '': if attr_value != attribute_value: return False except KeyError: return False return True # class to work with (XX)XMLTransformSpecification classes # # Notes: # Current functional specification & pmm_api_xml_protocol.xsd schema do not support 'copy' processing type in 'dump-overview- transformation. # Anyway, the 'dump-overview' xml transform processing type is not a 'copy' type by nature. It 'wraps' each processed and passed node into ... elements. # So, as there is no appropriate processing for 'copy' instruction, let it will be a synonym for 'filter-out' processing type. # class DumpOverviewXMLTransformWorker(XMLTransformWorker): def __init__(self, xml_transform_specification, xml_writer): XMLTransformWorker.__init__(self,xml_transform_specification,xml_writer) # The dump root element 'migration-dump' not exists usually in XMLTransform specification. So we should force its matching to work the same way as for other nodes # Include 'Envelope', 'Data' elements to allow get dump overview on signed dump self.__pass_throw_element = ['Envelope', 'Data', 'migration-dump'] def matchNode(self, name, attrs, xmlTransformPathProcessor): if name in self.__pass_throw_element: return 1, 'process-recursive', 'copy' else: return XMLTransformWorker.matchNode(self, name, attrs, xmlTransformPathProcessor) def startElement(self, name, attrs, xmlTransformPathProcessor): if name in self.__pass_throw_element: if name == 'migration-dump': XMLTransformWorker.startElement(self, 'metainformation',{}) for at_name in attrs.keys(): XMLTransformWorker.startElement(self, 'record',{'name':at_name}) XMLTransformWorker.endElement(self, 'record',attrs[at_name]) XMLTransformWorker.endElement(self, 'metainformation','') XMLTransformWorker.startElement(self, 'objects-to-select',{}) else: XMLTransformWorker.startElement(self, 'object', {}) display_element_attrs = {'type':name,'name':''} if attrs.has_key('name'): display_element_attrs['name'] = attrs['name'] XMLTransformWorker.startElement(self, 'display', display_element_attrs) XMLTransformWorker.endElement(self, 'display', '') select_element_attrs = {'nodename':name,'path':xmlTransformPathProcessor.getAttrRequiredPath('guid', True)} XMLTransformWorker.startElement(self, 'select', select_element_attrs) XMLTransformWorker.startElement(self, 'attributes', {}) if attrs.has_key('owner-guid'): XMLTransformWorker.startElement(self, 'owner-guid', {}) XMLTransformWorker.endElement(self, 'owner-guid', attrs['owner-guid']) if attrs.has_key('guid'): XMLTransformWorker.startElement(self, 'guid', {}) XMLTransformWorker.endElement(self, 'guid', attrs['guid']) if attrs.has_key('id'): XMLTransformWorker.startElement(self, 'id', {}) XMLTransformWorker.endElement(self, 'id', attrs['id']) if attrs.has_key('name'): XMLTransformWorker.startElement(self, 'name', {}) XMLTransformWorker.endElement(self, 'name', attrs['name']) XMLTransformWorker.endElement(self, 'attributes', '') XMLTransformWorker.endElement(self, 'select', '') XMLTransformWorker.startElement(self, 'objects', {}) def endElement(self, name, value): if name == 'migration-dump': XMLTransformWorker.endElement(self, 'objects-to-select','') elif not name in self.__pass_throw_element: XMLTransformWorker.endElement(self, 'objects', '') XMLTransformWorker.endElement(self, 'object', '') #class to work with (XX)XMLTransformSpecification classes class RestoreSpecificationXMLTransformWorker(XMLTransformWorker): def __init__(self, xml_transform_specification, xml_writer): XMLTransformWorker.__init__(self,xml_transform_specification,xml_writer) # The dump root element 'migration-dump' not exists usually in XMLTransform specification. So we should force its matching to work the same way as for other nodes self.__pass_throw_element = ['migration-dump'] def matchNode(self, name, attrs, xmlTransformPathProcessor): if name in self.__pass_throw_element: return 1, 'process-recursive', 'copy' else: return XMLTransformWorker.get_xml_transform_specification(self).match(name, attrs, xmlTransformPathProcessor) def startElement(self, name, attrs, xmlTransformPathProcessor): XMLTransformWorker.startElement(self, name, attrs, xmlTransformPathProcessor) def endElement(self, name, value): XMLTransformWorker.endElement(self, name, value) #implements XMLTransformSpecification static schema for 'dump-overview' type class DumpOverviewXMLTransformSpecification: def __init__(self, dump_overview_xml_transform_specification_file = None): self.__dump_overview_xml_transform_specification_file = None self.__xmlt = None if not dump_overview_xml_transform_specification_file: dump_overview_xml_transform_specification_file = os.path.join(pmm_config.pmmcli_dir(),'dump_overview_xml_transform.xml') self.load(dump_overview_xml_transform_specification_file) def load(self,dump_overview_xml_transform_specification_file): self.__dump_overview_xml_transform_specification_file = dump_overview_xml_transform_specification_file self.__xmlt = xml_transform.Transformation.factory() needed_node = None for child_node in minidom.parse(os.path.join(pmm_config.pmmcli_dir(),dump_overview_xml_transform_specification_file)).childNodes: if child_node.nodeType == Node.ELEMENT_NODE: needed_node = child_node break if needed_node == None: raise Exception("File %s has wrong format" % os.path.join(pmm_config.pmmcli_dir(),dump_overview_xml_transform_specification_file)) self.__xmlt.build(needed_node) def match(self, name, attrs, xmlTransformPathProcessor): # Dump Overview XmlTransform specification does not work with node attributes if self.__xmlt: for item in self.__xmlt.get_node(): if item.get_name() == name: return 1, item.get_children_processing_type(), item.get_attributes_processing_type() return 0, 'filter-out', '' #implements XMLTransformSpecification static schema for 'dump-overview' type class RestoreSpecificationXMLTransformSpecification: def __init__(self, restore_specification_xml_transform_specification_file = None): self.__restore_specification_xml_transform_specification_file = None self.__xmlt = None if not restore_specification_xml_transform_specification_file: restore_specification_xml_transform_specification_file = os.path.join(pmm_config.pmmcli_dir(),'restore_specification_xml_transform.xml') self.load(restore_specification_xml_transform_specification_file) def load(self,restore_specification_xml_transform_specification_file): self.__restore_specification_xml_transform_specification_file = restore_specification_xml_transform_specification_file self.__xmlt = xml_transform.Transformation.factory() self.__xmlt.build(minidom.parse(os.path.join(pmm_config.pmmcli_dir(),restore_specification_xml_transform_specification_file)).childNodes[0] ) def match(self, name, attrs, xmlTransformPathProcessor): if self.__xmlt: for item in self.__xmlt.get_node(): if item.get_name() == name: attributes = item.get_attributes() attributes_passed = 1 if attributes: for attr in attributes.get_attribute(): attr_name = attr.get_name() attr_value = attr.get_value() try: attribute_value = attrs[attr_name] if attribute_value != attr_value: attributes_passed = 0 except KeyError: attributes_passed = 0 if attributes_passed == 0: break if attributes_passed: # now check if any corresponds to 'path' context_passed = 1 context = item.get_context() if context: context_passed = 0 for path_item in context.get_path(): if xmlTransformPathProcessor.pathMatched(path_item): context_passed = 1 break if attributes_passed and context_passed: return 1, item.get_children_processing_type(), item.get_attributes_processing_type() # matched node is not found or there are no nodes at all return 0, 'process-recursive', '' class DumpOverviewCreator: def __init__(self, session_id): self.__session_id = session_id self.__dump_overview = None def getDumpFormat(self,info_xml): session = pmmcli_session.PmmcliSession(self.__session_id) dump_format = pmm_dump_transformer.DumpTransformer().getDumpFormat(info_xml, session.get_session_path()) return dump_format def addDumpFormat(self,info_xml,dump_overview_object): dump_format_found = False metainformation_object = dump_overview_object.get_metainformation() if metainformation_object is not None: records = metainformation_object.get_record() if records is not None: dump_format_found = False for record_object in records: if record_object.get_name() == 'dump-format': dump_format_found = True break if not dump_format_found: dump_format = self.getDumpFormat(info_xml) record_object = pmm_api_xml_protocols.Record( name = 'dump-format') record_object.setValueOf_(dump_format) if metainformation_object is None: metainformation_object = pmm_api_xml_protocols.Metainformation() dump_overview_object.set_metainformation( metainformation_object ) metainformation_object.add_record(record_object) def create(self,info_xml,dump_overview_file): writer = pmm_dump_access_service.XMLContentSimpleWriter() xml_processor = InfoXMLProcessor( info_xml, InfoXmlHandler( DumpOverviewXMLTransformWorker(DumpOverviewXMLTransformSpecification(),writer),XMLTransformPathProcessor()) ) xml_processor.process() self.__dump_overview = pmm_api_xml_protocols.DumpOverview.factory() self.__dump_overview.build(minidom.parseString('' + writer.get_content().encode('utf-8') + '').childNodes[0] ) self.addDumpFormat(info_xml, self.__dump_overview) f = open(dump_overview_file, 'wt') resp_str = cStringIO.StringIO() resp_encoded = EncoderFile(resp_str, "utf-8") resp_encoded.write('\n') self.__dump_overview.export(resp_encoded, 0, name_ = 'dump-overview') f.write(resp_str.getvalue()) f.close() class RestoreSpecificationCreator: def __init__(self, session_id): self.__session_id = session_id # the only way to create is create from info.xml and objects list to restore def create(self, owner_type, owner_guid, info_xml, restore_specification_file, dump_overview_file, selected_objects_file = None): # if selected_objects_file not specified - create restore_specification as copy of info_xml if not selected_objects_file: if info_xml != restore_specification_file: shutil.copy2(info_xml,restore_specification_file) else: writer = pmm_dump_access_service.XMLSimpleWriter() xml_processor = InfoXMLProcessor( info_xml, InfoXmlHandler( RestoreSpecificationXMLTransformWorker(RestoreSpecificationXMLTransformSpecification(selected_objects_file),writer),XMLTransformPathProcessor() ) ) xml_processor.process() ps = open(restore_specification_file,'wt') ps.write(writer.get_content()) ps.close() DumpOverviewCreator(self.__session_id).create(restore_specification_file,dump_overview_file) # assign restore_specification_file modified by ConflictResolver def update(self,restore_specification_file, dump_overview_file): DumpOverviewCreator(self.__session_id).create(restore_specification_file,dump_overview_file) def usage(): print "pmm_dump_overview. Plesk Migration Manager." print "(c) Parallels" print "" print "Usage:" print "" print " pmm_dump_overview dump-overview [] []" print " or" print " pmm_dump_overview restore-specification [] [] []" print "" print "" print " if 'dump-overview' command is specified, the result file will be dump overview of info-xml file" print " if 'restore-specification'command is specified, the result file will be dump overview of info-xml file" print "" print " is an absolute path to input info.xml file" print " is an absolute path to input xml_transform xml file" print "" print "" def main(): print argv if len(argv) == 4 and argv[1] == 'dump-overview': info_xml_path = argv[2] dump_overview_path = argv[3] dump_overview_creator = DumpOverviewCreator('0') dump_overview_creator.create(info_xml_path,dump_overview_path) elif len(argv) == 5 and argv[1] == 'restore-specification': info_xml_path = argv[2] xml_transform_path = argv[3] restore_specification_path = argv[4] writer = pmm_dump_access_service.XMLSimpleWriter() rsxmlts = RestoreSpecificationXMLTransformSpecification(xml_transform_path) xml_processor = InfoXMLProcessor( info_xml_path, InfoXmlHandler( RestoreSpecificationXMLTransformWorker(rsxmlts,writer),XMLTransformPathProcessor() ) ) xml_processor.process() ps = open(restore_specification_path,'wt') ps.write(writer.get_content()) ps.close() else: usage() exit(1) if __name__ == '__main__': main()