issue 7 - async. op with manager now returns RPC object
[ncclient] / ncclient / xml_.py
1 # Copyright 2009 Shikhar Bhushan
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #    http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15
16 """The :mod:`xml` module provides methods for creating XML documents, parsing
17 XML, and converting between different XML representations. It uses
18 :mod:`~xml.etree.ElementTree` internally.
19 """
20
21 from cStringIO import StringIO
22 from xml.etree import cElementTree as ET
23
24 from ncclient import NCClientError
25
26 class ContentError(NCClientError):
27     "Raised by methods of the :mod:`content` module in case of an error."
28     pass
29
30 ### Namespace-related
31
32 #: Base NETCONF namespace
33 BASE_NS_1_0 = 'urn:ietf:params:xml:ns:netconf:base:1.0'
34 #: ... and this is BASE_NS according to Cisco devices tested
35 CISCO_BS_1_0 = 'urn:ietf:params:netconf:base:1.0'
36 #: namespace for Tail-f data model
37 TAILF_AAA_1_1 = 'http://tail-f.com/ns/aaa/1.1'
38 #: namespace for Tail-f data model
39 TAILF_EXECD_1_1 = 'http://tail-f.com/ns/execd/1.1'
40 #: namespace for Cisco data model
41 CISCO_CPI_1_0 = 'http://www.cisco.com/cpi_10/schema'
42 #: namespace for Flowmon data model
43 FLOWMON_1_0 = 'http://www.liberouter.org/ns/netopeer/flowmon/1.0'
44
45 try:
46     register_namespace = ET.register_namespace
47 except AttributeError:
48     def register_namespace(prefix, uri):
49         from xml.etree import ElementTree
50         # cElementTree uses ElementTree's _namespace_map, so that's ok
51         ElementTree._namespace_map[uri] = prefix
52
53 prefix_map = {
54     BASE_NS_1_0: 'nc',
55     TAILF_AAA_1_1: 'aaa',
56     TAILF_EXECD_1_1: 'execd',
57     CISCO_CPI_1_0: 'cpi',
58     FLOWMON_1_0: 'fm',
59 }
60
61 for (ns, pre) in prefix_map.items():
62     register_namespace(pre, ns)
63
64 qualify = lambda tag, ns=BASE_NS_1_0: tag if ns is None else '{%s}%s' % (ns, tag)
65
66 multiqualify = lambda tag, nslist=(BASE_NS_1_0, CISCO_BS_1_0): [qualify(tag, ns) for ns in nslist]
67
68 unqualify = lambda tag: tag[tag.rfind('}')+1:]
69
70 ### XML representations
71
72 class DictTree:
73
74     @staticmethod
75     def Element(spec):
76         """DictTree -> Element
77
78         :type spec: :obj:`dict` or :obj:`string` or :class:`~xml.etree.ElementTree.Element`
79
80         :rtype: :class:`~xml.etree.ElementTree.Element`
81         """
82         if iselement(spec):
83             return spec
84         elif isinstance(spec, basestring):
85             return XML.Element(spec)
86         if not isinstance(spec, dict):
87             raise ContentError("Invalid tree spec")
88         if 'tag' in spec:
89             ele = ET.Element(spec.get('tag'), spec.get('attrib', {}))
90             ele.text = spec.get('text', '')
91             ele.tail = spec.get('tail', '')
92             subtree = spec.get('subtree', [])
93             # might not be properly specified as list but may be dict
94             if not isinstance(subtree, list):
95                 subtree = [subtree]
96             for subele in subtree:
97                 ele.append(DictTree.Element(subele))
98             return ele
99         elif 'comment' in spec:
100             return ET.Comment(spec.get('comment'))
101         else:
102             raise ContentError('Invalid tree spec')
103
104     @staticmethod
105     def XML(spec, encoding='UTF-8'):
106         """DictTree -> XML
107
108         :type spec: :obj:`dict` or :obj:`string` or :class:`~xml.etree.ElementTree.Element`
109
110         :arg encoding: chraracter encoding
111
112         :rtype: string
113         """
114         return Element.XML(DictTree.Element(spec), encoding)
115
116 class Element:
117
118     @staticmethod
119     def DictTree(ele):
120         """DictTree -> Element
121
122         :type spec: :class:`~xml.etree.ElementTree.Element`
123         :rtype: :obj:`dict`
124         """
125         return {
126             'tag': ele.tag,
127             'attributes': ele.attrib,
128             'text': ele.text,
129             'tail': ele.tail,
130             'subtree': [ Element.DictTree(child) for child in ele.getchildren() ]
131         }
132
133     @staticmethod
134     def XML(ele, encoding='UTF-8'):
135         """Element -> XML
136
137         :type spec: :class:`~xml.etree.ElementTree.Element`
138         :arg encoding: character encoding
139         :rtype: :obj:`string`
140         """
141         xml = ET.tostring(ele, encoding)
142         if xml.startswith('<?xml'):
143             return xml
144         else:
145             return '<?xml version="1.0" encoding="%s"?>%s' % (encoding, xml)
146
147 class XML:
148
149     @staticmethod
150     def DictTree(xml):
151         """XML -> DictTree
152
153         :type spec: :obj:`string`
154         :rtype: :obj:`dict`
155         """
156         return Element.DictTree(XML.Element(xml))
157
158     @staticmethod
159     def Element(xml):
160         """XML -> Element
161
162         :type xml: :obj:`string`
163         :rtype: :class:`~xml.etree.ElementTree.Element`
164         """
165         return ET.fromstring(xml)
166
167 dtree2ele = DictTree.Element
168 dtree2xml = DictTree.XML
169 ele2dtree = Element.DictTree
170 ele2xml = Element.XML
171 xml2dtree = XML.DictTree
172 xml2ele = XML.Element
173
174 ### Other utility functions
175
176 iselement = ET.iselement
177
178
179 NSLIST = [BASE_NS_1_0, CISCO_BS_1_0]
180
181 def find(ele, tag, nslist=[]):
182     """If *nslist* is empty, same as :meth:`xml.etree.ElementTree.Element.find`.
183     If it is not, *tag* is interpreted as an unqualified name and qualified
184     using each item in *nslist* (with a :const:`None` item in *nslit* meaning no
185     qualification is done). The first match is returned.
186
187     :arg nslist: optional list of namespaces
188     :type nslit: `string` `list`
189     """
190     if nslist:
191         for qname in multiqualify(tag):
192             found = ele.find(qname)
193             if found is not None:
194                 return found
195     else:
196         return ele.find(tag)
197
198 def parse_root(raw):
199     """Efficiently parses the root element of an XML document.
200
201     :arg raw: XML document
202     :type raw: string
203     :returns: a tuple of `(tag, attributes)`, where `tag` is the (qualified) name of the element and `attributes` is a dictionary of its attributes.
204     :rtype: `tuple`
205     """
206     fp = StringIO(raw[:1024]) # this is a guess but start element beyond 1024 bytes would be a bit absurd
207     for event, element in ET.iterparse(fp, events=('start',)):
208         return (element.tag, element.attrib)
209
210 def validated_element(rep, tags=None, attrs=None, text=None):
211     """Checks if the root element meets the supplied criteria. Returns a
212     :class:`~xml.etree.ElementTree.Element` instance if so, otherwise raises
213     :exc:`ContentError`.
214
215     :arg tags: tag name or a list of allowable tag names
216     :arg attrs: list of required attribute names, each item may be a list of allowable alternatives
217     :arg text: textual content to match
218     :type rep: :obj:`dict` or :obj:`string` or :class:`~xml.etree.ElementTree.Element`
219     """
220     ele = dtree2ele(rep)
221     err = False
222     if tags:
223         if isinstance(tags, basestring):
224             tags = [tags]
225         if ele.tag not in tags:
226             err = True
227     if attrs:
228         for req in attrs:
229             if isinstance(req, basestring): req = [req]
230             for alt in req:
231                 if alt in ele.attrib:
232                     break
233             else:
234                 err = True
235     if text and ele.text != text:
236         err = True
237     if err:
238         raise ContentError("Element [%s] does not meet requirements" % ele.tag)
239     return ele