%define name sfa
%define version 1.1
-%define taglevel 0
+%define taglevel 1
%define release %{taglevel}%{?pldistro:.%{pldistro}}%{?date:.%{date}}
%global python_sitearch %( python -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)" )
[ "$1" -ge "1" ] && service sfa-cm restart || :
%changelog
+* Fri Oct 28 2011 Thierry Parmentelat <thierry.parmentelat@sophia.inria.fr> - sfa-1.1-1
+- first support for protogeni rspecs is working
+- vini no longer needs a specific manager
+- refactoring underway towards more flexible/generic architecture
+
* Thu Sep 15 2011 Tony Mack <tmack@cs.princeton.edu> - sfa-1.0-36
- Unicode-friendliness for user names with accents/special chars.
- Fix bug that could cause create the client to fail when calling CreateSliver for a slice that has the same hrn as a user.
parser.add_option("-d", "--delegate", dest="delegate", default=None,
action="store_true",
help="Include a credential delegated to the user's root"+\
- "authority in set of credentials for this call")
-
- # registy filter option
+ "authority in set of credentials for this call")
+
+ # registy filter option
if command in ("list", "show", "remove"):
parser.add_option("-t", "--type", dest="type", type="choice",
help="type filter ([all]|user|slice|authority|node|aggregate)",
if args:
hrn = args[0]
gid = self._get_gid(hrn)
- self.logger.debug("Sfi.get_gid-> %s",gid.save_to_string(save_parents=True))
+ self.logger.debug("Sfi.get_gid-> %s" % gid.save_to_string(save_parents=True))
return gid
def _get_gid(self, hrn=None, type=None):
slice_urn = hrn_to_urn(slice_hrn, 'slice')
user_cred = self.get_user_cred()
slice_cred = self.get_slice_cred(slice_hrn).save_to_string(save_parents=True)
- # delegate the cred to the callers root authority
- delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority)+'.slicemanager')
- #delegated_cred = self.delegate_cred(slice_cred, get_authority(slice_hrn))
- #creds.append(delegated_cred)
+
+ if hasattr(opts, 'aggregate') and opts.aggregate:
+ delegated_cred = None
+ else:
+ # delegate the cred to the callers root authority
+ delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority)+'.slicemanager')
+ #delegated_cred = self.delegate_cred(slice_cred, get_authority(slice_hrn))
+ #creds.append(delegated_cred)
+
rspec_file = self.get_rspec_file(args[1])
rspec = open(rspec_file).read()
creds = [slice_cred]
else:
users = sfa_users_arg(user_records, slice_record)
- creds = [slice_cred, delegated_cred]
+ creds = [slice_cred]
+ if delegated_cred:
+ creds.append(delegated_cred)
call_args = [slice_urn, creds, rspec, users]
if self.server_supports_call_id_arg(server):
call_args.append(unique_call_id())
-
+
result = server.CreateSliver(*call_args)
if opts.file is None:
print result
#! /usr/bin/env python
import sys
-from sfa.util.rspecHelper import RSpec, Commands
from sfa.client.sfi_commands import Commands
from sfa.rspecs.rspec import RSpec
--- /dev/null
+#! /usr/bin/env python
+
+import sys
+from sfa.client.sfi_commands import Commands
+from sfa.rspecs.rspec import RSpec
+from sfa.rspecs.version_manager import VersionManager
+
+command = Commands(usage="%prog [options] node1 node2...",
+ description="Add links to the RSpec. " +
+ "This command reads in an RSpec and outputs a modified " +
+ "RSpec. Use this to add links to your slivers")
+command.add_linkfile_option()
+command.prep()
+
+if not command.opts.linkfile:
+ print "Missing link list -- exiting"
+ command.parser.print_help()
+ sys.exit(1)
+
+if command.opts.infile:
+ infile=file(command.opts.infile)
+else:
+ infile=sys.stdin
+if command.opts.outfile:
+ outfile=file(command.opts.outfile,"w")
+else:
+ outfile=sys.stdout
+ad_rspec = RSpec(infile)
+links = file(command.opts.linkfile).read().split('\n')
+link_tuples = map(lambda x: tuple(x.split()), links)
+
+version_manager = VersionManager()
+try:
+ type = ad_rspec.version.type
+ version_num = ad_rspec.version.version
+ request_version = version_manager._get_version(type, version_num, 'request')
+ request_rspec = RSpec(version=request_version)
+ request_rspec.version.merge(ad_rspec)
+ request_rspec.version.add_link_requests(link_tuples)
+except:
+ print >> sys.stderr, "FAILED: %s" % links
+ raise
+ sys.exit(1)
+print >>outfile, request_rspec.toxml()
+sys.exit(0)
--- /dev/null
+#! /usr/bin/env python
+
+import sys
+from sfa.client.sfi_commands import Commands
+from sfa.rspecs.rspec import RSpec
+from sfa.util.xrn import Xrn
+
+command = Commands(usage="%prog [options]",
+ description="List all links in the RSpec. " +
+ "Use this to display the list of available links. " )
+command.prep()
+
+if command.opts.infile:
+ rspec = RSpec(command.opts.infile)
+ links = rspec.version.get_links()
+ if command.opts.outfile:
+ sys.stdout = open(command.opts.outfile, 'w')
+
+ for link in links:
+ ifname1 = Xrn(link['interface1']['component_id']).get_leaf()
+ ifname2 = Xrn(link['interface2']['component_id']).get_leaf()
+ print "%s %s" % (ifname1, ifname2)
+
+
+
+
requested_slivers = [str(host) for host in rspec.version.get_nodes_with_slivers()]
slices.verify_slice_nodes(slice, requested_slivers, peer)
+ aggregate.prepare_nodes({'hostname': requested_slivers})
+ aggregate.prepare_interfaces({'node_id': aggregate.nodes.keys()})
slices.verify_slice_links(slice, rspec.version.get_link_requests(), aggregate)
# hanlde MyPLC peer association.
def get_ticket(api, xrn, creds, rspec, users):
-#unused
-# reg_objects = __get_registry_objects(xrn, creds, users)
-
(slice_hrn, _) = urn_to_hrn(xrn)
-#unused
-# slices = Slices(api)
-# peer = slices.get_peer(slice_hrn)
-# sfa_peer = slices.get_sfa_peer(slice_hrn)
+ slices = Slices(api)
+ peer = slices.get_peer(slice_hrn)
+ sfa_peer = slices.get_sfa_peer(slice_hrn)
# get the slice record
- registry = api.registries[api.hrn]
credential = api.getCredential()
+ interface = api.registries[api.hrn]
+ registry = api.get_server(interface, credential)
records = registry.Resolve(xrn, credential)
- # similar to CreateSliver, we must verify that the required records exist
- # at this aggregate before we can issue a ticket
-#Error (E1121, get_ticket): Too many positional arguments for function call
-#unused anyway
-# site_id, remote_site_id = slices.verify_site(registry, credential, slice_hrn,
-# peer, sfa_peer, reg_objects)
-#Error (E1121, get_ticket): Too many positional arguments for function call
-#unused anyway
-# slice = slices.verify_slice(registry, credential, slice_hrn, site_id,
-# remote_site_id, peer, sfa_peer, reg_objects)
-
# make sure we get a local slice record
record = None
for tmp_record in records:
record = SliceRecord(dict=tmp_record)
if not record:
raise RecordNotFound(slice_hrn)
+
+ # similar to CreateSliver, we must verify that the required records exist
+ # at this aggregate before we can issue a ticket
+ # parse rspec
+ rspec = RSpec(rspec_string)
+ requested_attributes = rspec.version.get_slice_attributes()
+ # ensure site record exists
+ site = slices.verify_site(hrn, slice_record, peer, sfa_peer)
+ # ensure slice record exists
+ slice = slices.verify_slice(hrn, slice_record, peer, sfa_peer)
+ # ensure person records exists
+ persons = slices.verify_persons(hrn, slice, users, peer, sfa_peer)
+ # ensure slice attributes exists
+ slices.verify_slice_attributes(slice, requested_attributes)
+
# get sliver info
- slivers = Slices(api).get_slivers(slice_hrn)
+ slivers = slices.get_slivers(slice_hrn)
+
if not slivers:
raise SliverDoesNotExist(slice_hrn)
xrns = xrn_dict[registry_hrn]
if registry_hrn != api.hrn:
credential = api.getCredential()
- peer_records = registries[registry_hrn].Resolve(xrns, credential)
+ interface = api.registries[registry_hrn]
+ server = api.get_server(interface, credential)
+ peer_records = server.Resolve(xrns, credential)
records.extend([SfaRecord(dict=record).as_dict() for record in peer_records])
# try resolving the remaining unfound records at the local registry
#if there was no match then this record belongs to an unknow registry
if not registry_hrn:
raise MissingAuthority(xrn)
-
# if the best match (longest matching hrn) is not the local registry,
# forward the request
records = []
if registry_hrn != api.hrn:
credential = api.getCredential()
- record_list = registries[registry_hrn].List(xrn, credential)
+ interface = api.registries[registry_hrn]
+ server = api.get_server(interface, credential)
+ record_list = server.List(xrn, credential)
records = [SfaRecord(dict=record).as_dict() for record in record_list]
# if we still have not found the record yet, try the local registry
from sfa.trust.credential import Credential
from sfa.util.sfalogging import logger
-from sfa.util.rspecHelper import merge_rspecs
from sfa.util.xrn import Xrn, urn_to_hrn
from sfa.util.threadmanager import ThreadManager
from sfa.util.version import version_core
results = threads.get_results()
# gather information from each ticket
- rspecs = []
+ rspec = None
initscripts = []
slivers = []
object_gid = None
attrs = agg_ticket.get_attributes()
if not object_gid:
object_gid = agg_ticket.get_gid_object()
- rspecs.append(agg_ticket.get_rspec())
+ if not rspec:
+ rspec = RSpec(agg_ticket.get_rspec())
+ else:
+ rspec.version.merge(agg_ticket.get_rspec())
initscripts.extend(attrs.get('initscripts', []))
slivers.extend(attrs.get('slivers', []))
# merge info
attributes = {'initscripts': initscripts,
'slivers': slivers}
- merged_rspec = merge_rspecs(rspecs)
-
+
# create a new ticket
ticket = SfaTicket(subject = slice_hrn)
ticket.set_gid_caller(api.auth.client_gid)
ticket.set_pubkey(object_gid.get_pubkey())
#new_ticket.set_parent(api.auth.hierarchy.get_auth_ticket(auth_hrn))
ticket.set_attributes(attributes)
- ticket.set_rspec(merged_rspec)
+ ticket.set_rspec(rspec.toxml())
ticket.encode()
ticket.sign()
return ticket.save_to_string(save_parents=True)
self.api = api
self.user_options = user_options
- def prepare_sites(self, force=False):
+ def prepare_sites(self, filter={}, force=False):
if not self.sites or force:
- for site in self.api.plshell.GetSites(self.api.plauth):
+ for site in self.api.plshell.GetSites(self.api.plauth, filter):
self.sites[site['site_id']] = site
- def prepare_nodes(self, force=False):
+ def prepare_nodes(self, filter={}, force=False):
if not self.nodes or force:
- for node in self.api.plshell.GetNodes(self.api.plauth, {'peer_id': None}):
+ filter.update({'peer_id': None})
+ nodes = self.api.plshell.GetNodes(self.api.plauth, filter)
+ site_ids = []
+ interface_ids = []
+ tag_ids = []
+ for node in nodes:
+ site_ids.append(node['site_id'])
+ interface_ids.extend(node['interface_ids'])
+ tag_ids.extend(node['node_tag_ids'])
+ self.prepare_sites({'site_id': site_ids})
+ self.prepare_interfaces({'interface_id': interface_ids})
+ self.prepare_node_tags({'node_tag_id': tag_ids})
+ for node in nodes:
# add site/interface info to nodes.
# assumes that sites, interfaces and tags have already been prepared.
site = self.sites[node['site_id']]
node['tags'] = tags
self.nodes[node['node_id']] = node
- def prepare_interfaces(self, force=False):
+ def prepare_interfaces(self, filter={}, force=False):
if not self.interfaces or force:
- for interface in self.api.plshell.GetInterfaces(self.api.plauth):
+ for interface in self.api.plshell.GetInterfaces(self.api.plauth, filter):
self.interfaces[interface['interface_id']] = interface
- def prepare_links(self, force=False):
+ def prepare_links(self, filter={}, force=False):
if not self.links or force:
if not self.api.config.SFA_AGGREGATE_TYPE.lower() == 'vini':
return
# set interfaces
# just get first interface of the first node
if1_xrn = PlXrn(auth=self.api.hrn, interface='node%s:eth0' % (node1['node_id']))
+ if1_ipv4 = self.interfaces[node1['interface_ids'][0]]['ip']
if2_xrn = PlXrn(auth=self.api.hrn, interface='node%s:eth0' % (node2['node_id']))
+ if2_ipv4 = self.interfaces[node2['interface_ids'][0]]['ip']
- if1 = Interface({'component_id': if1_xrn.urn} )
- if2 = Interface({'component_id': if2_xrn.urn} )
+ if1 = Interface({'component_id': if1_xrn.urn, 'ipv4': if1_ipv4} )
+ if2 = Interface({'component_id': if2_xrn.urn, 'ipv4': if2_ipv4} )
# set link
link = Link({'capacity': '1000000', 'latency': '0', 'packet_loss': '0', 'type': 'ipv4'})
self.links[link['component_name']] = link
- def prepare_node_tags(self, force=False):
+ def prepare_node_tags(self, filter={}, force=False):
if not self.node_tags or force:
- for node_tag in self.api.plshell.GetNodeTags(self.api.plauth):
+ for node_tag in self.api.plshell.GetNodeTags(self.api.plauth, filter):
self.node_tags[node_tag['node_tag_id']] = node_tag
- def prepare_pl_initscripts(self, force=False):
+ def prepare_pl_initscripts(self, filter={}, force=False):
if not self.pl_initscripts or force:
- for initscript in self.api.plshell.GetInitScripts(self.api.plauth, {'enabled': True}):
+ filter.update({'enabled': True})
+ for initscript in self.api.plshell.GetInitScripts(self.api.plauth, filter):
self.pl_initscripts[initscript['initscript_id']] = initscript
- def prepare(self, force=False):
- if not self.prepared or force:
- self.prepare_sites(force)
- self.prepare_interfaces(force)
- self.prepare_node_tags(force)
- self.prepare_nodes(force)
- self.prepare_links(force)
- self.prepare_pl_initscripts()
- self.prepared = True
+ def prepare(self, slice = None, force=False):
+ if not self.prepared or force or slice:
+ if not slice:
+ self.prepare_sites(force=force)
+ self.prepare_interfaces(force=force)
+ self.prepare_node_tags(force=force)
+ self.prepare_nodes(force=force)
+ self.prepare_links(force=force)
+ self.prepare_pl_initscripts(force=force)
+ else:
+ self.prepare_sites({'site_id': slice['site_id']})
+ self.prepare_interfaces({'node_id': slice['node_ids']})
+ self.prepare_node_tags({'node_id': slice['node_ids']})
+ self.prepare_nodes({'node_id': slice['node_ids']})
+ self.prepare_links({'slice_id': slice['slice_id']})
+ self.prepare_pl_initscripts()
+ self.prepared = True
def get_rspec(self, slice_xrn=None, version = None):
- self.prepare()
version_manager = VersionManager()
version = version_manager.get_version(version)
if not slice_xrn:
slice_name = hrn_to_pl_slicename(slice_hrn)
slices = self.api.plshell.GetSlices(self.api.plauth, slice_name)
if slices:
- slice = slices[0]
-
+ slice = slices[0]
+ self.prepare(slice=slice)
+ else:
+ self.prepare()
+
# filter out nodes with a whitelist:
valid_nodes = []
for node in self.nodes.values():
from sfa.util.policy import Policy
from sfa.rspecs.rspec import RSpec
from sfa.plc.vlink import VLink
+from sfa.util.xrn import Xrn
MAXINT = 2L**31-1
def verify_slice_links(self, slice, links, aggregate):
# nodes is undefined here
- if not links or not aggregate.nodes:
+ if not links:
return
+
for link in links:
- topo_rspec = VLink.get_topo_rspec(link)
+ # get the ip address of the first node in the link
+ ifname1 = Xrn(link['interface1']['component_id']).get_leaf()
+ (node, device) = ifname1.split(':')
+ node_id = int(node.replace('node', ''))
+ node = aggregate.nodes[node_id]
+ if1 = aggregate.interfaces[node['interface_ids'][0]]
+ ipaddr = if1['ip']
+ topo_rspec = VLink.get_topo_rspec(link, ipaddr)
+ self.api.plshell.AddSliceTag(self.api.plauth, slice['name'], 'topo_rspec', str([topo_rspec]), node_id)
+
def handle_peer(self, site, slice, persons, peer):
-
+import re
from sfa.util.xrn import Xrn
# Taken from bwlimit.py
#
@staticmethod
def get_virt_ip(if1, if2):
- link_id = get_link_id(if1, if2)
- iface_id = get_iface_id(if1, if2)
+ link_id = VLink.get_link_id(if1, if2)
+ iface_id = VLink.get_iface_id(if1, if2)
first = link_id >> 6
second = ((link_id & 0x3f)<<2) + iface_id
- return "192.168.%d.%s" % (frist, second)
+ return "192.168.%d.%s" % (first, second)
@staticmethod
def get_virt_net(link):
- link_id = self.get_link_id(link)
+ link_id = VLink.get_link_id(link['interface1'], link['interface2'])
first = link_id >> 6
second = (link_id & 0x3f)<<2
return "192.168.%d.%d/30" % (first, second)
@staticmethod
def get_interface_id(interface):
- if_name = Xrn(interface=interface['component_id']).get_leaf()
+ if_name = Xrn(interface['component_id']).get_leaf()
node, dev = if_name.split(":")
- node_id = int(node.replace("pc", ""))
+ node_id = int(node.replace("node", ""))
return node_id
@staticmethod
- def get_topo_rspec(link):
+ def get_topo_rspec(link, ipaddr):
link['interface1']['id'] = VLink.get_interface_id(link['interface1'])
link['interface2']['id'] = VLink.get_interface_id(link['interface2'])
my_ip = VLink.get_virt_ip(link['interface1'], link['interface2'])
remote_ip = VLink.get_virt_ip(link['interface2'], link['interface1'])
net = VLink.get_virt_net(link)
bw = format_tc_rate(long(link['capacity']))
- ipaddr = remote.get_primary_iface().ipv4
- return (link['interface2']['id'], ipaddr, bw, my_ip, remote_ip, net)
+ return (link['interface2']['id'], ipaddr, bw, my_ip, remote_ip, net)
+
+ @staticmethod
+ def topo_rspec_to_link(topo_rspec):
+ pass
@staticmethod
def add_links(xml, links):
- root = xml.root
for link in links:
- link_elem = etree.SubElement(root, 'link')
+ link_elem = etree.SubElement(xml, 'link')
for attrib in ['component_name', 'component_id', 'client_id']:
if attrib in link and link[attrib] is not None:
link_elem.set(attrib, link[attrib])
latency=link['latency'], packet_loss=link['packet_loss'])
if 'type' in link and link['type']:
type_elem = etree.SubElement(link_elem, 'link_type', name=link['type'])
-
@staticmethod
def get_links(xml):
links = []
available_links = PGv2Link.get_links(xml)
recently_added = []
for link in available_links:
- auth = Xrn(link['component_id']).get_authority_hrn()
if_name1 = Xrn(link['interface1']['component_id']).get_leaf()
if_name2 = Xrn(link['interface2']['component_id']).get_leaf()
-
+
requested_link = None
l_tup_1 = (if_name1, if_name2)
- l_tup_2 = (if_name2, if_name1)
+ l_tup_2 = (if_name2, if_name1)
if link_tuples.issuperset([(if_name1, if_name2)]):
requested_link = (if_name1, if_name2)
elif link_tuples.issuperset([(if_name2, if_name2)]):
requested_link = (if_name2, if_name1)
-
if requested_link:
# add client id to link ane interface elements
link.element.set('client_id', link['component_name'])
return PGv2Link.get_link_requests(self.xml)
def add_links(self, links):
- PGv2Link.add_links(self.xml, links)
+ PGv2Link.add_links(self.xml.root, links)
def add_link_requests(self, link_tuples, append=False):
- PGv2Link.add_link_requests(self.xml, link_tuples, append)
+ PGv2Link.add_link_requests(self.xml.root, link_tuples, append)
def attributes_list(self, elem):
opts = []
+from copy import deepcopy
from lxml import etree
from sfa.util.xrn import hrn_to_urn, urn_to_hrn
from sfa.util.plxrn import PlXrn
if 'bwlimit' in interface and interface['bwlimit']:
bwlimit = etree.SubElement(node_tag, 'bw_limit', units='kbps').text = str(interface['bwlimit']/1000)
comp_id = PlXrn(auth=network, interface='node%s:eth%s' % (node['node_id'], i)).get_urn()
- interface_tag = etree.SubElement(node_tag, 'interface', component_id=comp_id)
+ ipaddr = interface['ip']
+ interface_tag = etree.SubElement(node_tag, 'interface', component_id=comp_id, ipv4=ipaddr)
i+=1
if 'bw_unallocated' in node:
bw_unallocated = etree.SubElement(node_tag, 'bw_unallocated', units='kbps').text = str(node['bw_unallocated']/1000)
pass
def add_links(self, links):
- PGv2Link.add_links(self.xml, links)
+ networks = self.get_network_elements()
+ if len(networks) > 0:
+ xml = networks[0]
+ else:
+ xml = self.xml
+ PGv2Link.add_links(xml, links)
+
+ def add_link_requests(self, links):
+ PGv2Link.add_link_requests(self.xml, links)
def add_slivers(self, slivers, network=None, sliver_urn=None, no_dupes=False, append=False):
# add slice name to network tag
#from sfa.util.faults import *
-from sfa.util.storage import XmlStorage
import sfa.util.xmlrpcprotocol as xmlrpcprotocol
+from sfa.util.xml import XML
# GeniLight client support is optional
try:
def __init__(self, conf_file):
dict.__init__(self, {})
# load config file
- self.interface_info = XmlStorage(conf_file, self.default_dict)
- self.interface_info.load()
- records = self.interface_info.values()[0]
- if not isinstance(records, list):
- records = [records]
-
- required_fields = self.default_fields.keys()
- for record in records:
- if not record or not set(required_fields).issubset(record.keys()):
- continue
- # port is appended onto the domain, before the path. Should look like:
- # http://domain:port/path
- hrn, address, port = record['hrn'], record['addr'], record['port']
- # sometime this is called at a very early stage with no config loaded
- # avoid to remember this instance in such a case
- if not address or not port:
- continue
- interface = Interface(hrn, address, port)
- self[hrn] = interface
+ required_fields = set(self.default_fields.keys())
+ self.interface_info = XML(conf_file).todict()
+ for value in self.interface_info.values():
+ if isinstance(value, list):
+ for record in value:
+ if isinstance(record, dict) and \
+ required_fields.issubset(record.keys()):
+ hrn, address, port = record['hrn'], record['addr'], record['port']
+ # sometime this is called at a very early stage with no config loaded
+ # avoid to remember this instance in such a case
+ if not address or not port:
+ continue
+ interface = Interface(hrn, address, port)
+ self[hrn] = interface
def get_server(self, hrn, key_file, cert_file, timeout=30):
return self[hrn].get_server(key_file, cert_file, timeout)
def get(self, key):
data = self.cache.get(key)
- if not data or data.is_expired():
- return None
- return data.get_data()
+ if not data:
+ data = None
+ elif data.is_expired():
+ self.pop(key)
+ data = None
+ else:
+ data = data.get_data()
+ return data
+
+ def pop(self, key):
+ if key in self.cache:
+ self.cache.pop(key)
def dump(self):
result = {}
"""
Load the record from a dictionary
"""
+
self.set_name(dict['hrn'])
gidstr = dict.get("gid", None)
if gidstr:
representation of the record.
"""
#dict = xmlrpclib.loads(str)[0][0]
-
+
record = XML(str)
self.load_from_dict(record.todict())
elif isinstance(value, int):
d[key] = unicode(d[key])
elif value is None:
- d.pop(key)
-
+ d.pop(key)
+
+ # element.attrib.update will explode if DateTimes are in the
+ # dcitionary.
+ d=d.copy()
+ for k in d.keys():
+ if (type(d[k]) != str) and (type(d[k]) != unicode):
+ del d[k]
+
element.attrib.update(d)
def validate(self, schema):
def toxml(self):
return etree.tostring(self.root, 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
if child.tag not in d:
d[child.tag] = []
d[child.tag].append(self.todict(child))
- return d
+
+ if len(d)==1 and ("text" in d):
+ d = d["text"]
+
+ return d
def save(self, filename):
f = open(filename, 'w')