2 Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved.
4 This program and the accompanying materials are made available under the
5 terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 and is available at http://www.eclipse.org/legal/epl-v10.html
8 Created on May 11, 2014
10 @author: <a href="mailto:vdemcak@cisco.com">Vaclav Demcak</a>
13 from xml.dom.minidom import Element
15 from openvswitch.parser_tools import ParseTools
16 from tools.crud_test_with_param_superclass import OF_CRUD_Test_Base
17 from tools.xml_parser_tools import XMLtoDictParserTools
18 import xml.dom.minidom as md
21 TABLE_ID_TAG_NAME = 'table_id'
22 FLOW_ID_TAG_NAME = 'id'
24 IGNORED_TAGS_FOR_OPERATIONAL_COMPARISON = ['id', 'flow-name', 'barrier', 'cookie_mask', 'installHw', 'flags', 'strict', 'byte-count', 'duration', 'packet-count', 'in-port']
26 FLOW_TAGS_FOR_UPDATE = ['action']
28 class OF_CRUD_Test_Flows( OF_CRUD_Test_Base ):
32 super( OF_CRUD_Test_Flows, self ).setUp()
33 ids = ParseTools.get_values( self.xml_input_DOM, TABLE_ID_TAG_NAME, FLOW_ID_TAG_NAME )
34 data = ( self.host, self.port, ids[TABLE_ID_TAG_NAME], ids[FLOW_ID_TAG_NAME] )
35 self.conf_url = 'http://%s:%d/restconf/config/opendaylight-inventory:nodes' \
36 '/node/openflow:1/table/%s/flow/%s' % data
37 self.oper_url = 'http://%s:%d/restconf/operational/opendaylight-inventory:nodes' \
38 '/node/openflow:1/table/%s/flow/%s' % data
39 data = ( self.host, self.port, ids[TABLE_ID_TAG_NAME] )
40 self.conf_url_post = 'http://%s:%d/restconf/config/opendaylight-inventory:nodes' \
41 '/node/openflow:1/table/%s' % data
43 self.oper_url_get = 'http://%s:%d/restconf/operational/opendaylight-inventory:nodes' \
44 '/node/openflow:1/table/%s' % data
45 data = ( self.host, self.port )
46 self.oper_url_add = 'http://%s:%d/restconf/operations/sal-flow:add-flow' % data
47 self.oper_url_upd = 'http://%s:%d/restconf/operations/sal-flow:update-flow' % data
48 self.oper_url_del = 'http://%s:%d/restconf/operations/sal-flow:remove-flow' % data
49 # Modify input operations data
50 self.data_from_file_input = ''
51 for node in self.xml_input_DOM.documentElement.childNodes:
52 nodeKey = None if node.localName == None else ( node.localName ).encode( 'utf-8', 'ignore' )
53 if ( nodeKey is None or nodeKey != 'id' ) :
54 self.data_from_file_input += node.toxml( encoding = 'utf-8' )
56 # The xml body without data - data come from file (all flow's subtags)
57 self.oper_input_stream = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n' \
58 '<input xmlns="urn:opendaylight:flow:service">\n' \
60 '<node xmlns:inv="urn:opendaylight:inventory" xmlns:finv="urn:opendaylight:flow:inventory">/inv:nodes/inv:node[inv:id="openflow:1"]</node>\n' \
61 '</input>' % self.data_from_file_input
64 def tearDown( self ) :
65 # cleaning configuration DataStore and device without a response validation
66 self.log.info( self._paint_msg_cyan( 'Uncontrolled cleaning after flow test' ) )
67 requests.delete( self.conf_url, auth = self._get_auth(),
68 headers = self._get_xml_result_header() )
69 requests.post( self.oper_url_del, data = self.oper_input_stream, auth = self._get_auth(),
70 headers = self._get_xml_request_result_header() )
71 super( OF_CRUD_Test_Flows, self ).tearDown()
74 def test_conf_PUT( self ):
75 self.log.info( "--- Flow conf. PUT test ---" )
76 # -------------- CREATE -------------------
77 self.log.info( self._paint_msg_yellow( " CREATE Flow by PUT REST" ) )
78 # send request via RESTCONF
79 self.put_REST_XML_conf_request( self.conf_url, self.xml_input_stream )
80 # check request content against restconf's config datastore
81 response = self.get_REST_XML_response( self.conf_url )
82 xml_resp_stream = ( response.text ).encode( 'utf-8', 'ignore' )
83 xml_resp_DOM = md.parseString( xml_resp_stream )
84 self.assertDataDOM( self.xml_input_DOM, xml_resp_DOM )
85 # check request content against restconf's operational datastore
86 response = self.get_REST_XML_response( self.oper_url )
87 self.__validate_contain_flow( response, self.xml_input_DOM )
89 # -------------- UPDATE -------------------
90 self.log.info( self._paint_msg_yellow( " UPDATE Flow by PUT REST" ) )
91 xml_updated_stream = self.__update_flow_input();
92 self.put_REST_XML_conf_request( self.conf_url, xml_updated_stream )
93 # check request content against restconf's config datastore
94 response = self.get_REST_XML_response( self.conf_url )
95 xml_resp_stream = ( response.text ).encode( 'utf-8', 'ignore' )
96 xml_resp_DOM = md.parseString( xml_resp_stream )
97 xml_upd_DOM = md.parseString( xml_updated_stream )
98 self.assertDataDOM( xml_upd_DOM, xml_resp_DOM )
99 # check request content against restconf's operational datastore
100 response = self.get_REST_XML_response( self.oper_url )
101 self.__validate_contain_flow( response, xml_upd_DOM )
103 # -------------- DELETE -------------------
104 self.log.info( self._paint_msg_yellow( " DELETE Flow by DELETE REST" ) )
105 # Delte data from config DataStore
106 response = self.delete_REST_XML_response( self.conf_url )
107 # Data has been deleted, so we expect the 404 response code
108 response = self.get_REST_XML_deleted_response( self.conf_url )
109 # Flow data has specific format and it has to be compared in different way
110 response = self.get_REST_XML_response( super.oper_url )
111 # find a correct flow-statistic (expect -> flow-statistic not exist)
112 self.__validate_contain_flow( response, self.xml_input_DOM, False )
115 def test_conf_POST( self ):
116 self.log.info( "--- Flow conf. POST test ---" )
117 # -------------- CREATE -------------------
118 self.log.info( self._paint_msg_yellow( " CREATE Flow by POST REST" ) )
119 # send request via RESTCONF
120 self.post_REST_XML_request( self.conf_url_post, self.xml_input_stream )
121 # check request content against restconf's config datastore
122 response = self.get_REST_XML_response( self.conf_url )
123 xml_resp_stream = ( response.text ).encode( 'utf-8', 'ignore' )
124 xml_resp_DOM = md.parseString( xml_resp_stream )
125 self.assertDataDOM( self.xml_input_DOM, xml_resp_DOM )
126 # check request content against restconf's operational datastore
127 response = self.get_REST_XML_response( self.oper_url )
128 self.__validate_contain_flow( response, self.xml_input_DOM )
129 # test error for double create (POST could create data only)
130 self.log.info( self._paint_msg_yellow( " UPDATE Flow by POST REST" ) )
131 response = self.post_REST_XML_repeat_request( self.conf_url_post, self.xml_input_stream )
133 # -------------- DELETE -------------------
134 self.log.info( self._paint_msg_yellow( " DELETE Flow by DELETE REST" ) )
135 # Delte data from config DataStore
136 response = self.delete_REST_XML_response( self.conf_url )
137 # Data has been deleted, so we expect the 404 response code
138 response = self.get_REST_XML_deleted_response( self.conf_url )
139 # Flow data has specific format and it has to be compared in different way
140 response = self.get_REST_XML_response( self.oper_url )
141 # find a correct flow-statistic (expect -> flow-statistic not exist)
142 self.__validate_contain_flow( response, self.xml_input_DOM, False )
145 def test_operations_POST( self ):
146 self.log.info( "--- Flow operations sal-service test ---" )
147 # -------------- CREATE -------------------
148 self.log.info( self._paint_msg_yellow( " CREATE Flow by add-sal-service" ) )
149 # send request via RESTCONF
150 self.post_REST_XML_request( self.oper_url_add, self.oper_input_stream )
151 # TODO : check no empty transaction_id from post add_service
153 # check request content against restconf's config datastore
154 # operation service don't change anything in a Config. Data Store
155 # so we expect 404 response code (same as a check after delete
156 self.get_REST_XML_deleted_response( self.conf_url )
157 # check request content against restconf's operational datastore
158 # operational Data Store has to present new flow, but with generated key
159 response = self.get_REST_XML_response( self.oper_url_get )
160 self.__validate_contain_flow( response, self.xml_input_DOM )
162 # -------------- UPDATE -------------------
163 self.log.info( self._paint_msg_yellow( " UPDATE Flow by update-sal-service" ) )
164 xml_updated_stream = self.__update_flow_input();
165 xml_updated_DOM = md.parseString( xml_updated_stream )
166 data_from_updated_stream = ''
167 for node in xml_updated_DOM.documentElement.childNodes:
168 nodeKey = None if node.localName == None else ( node.localName ).encode( 'utf-8', 'ignore' )
169 if ( nodeKey is None or nodeKey != 'id' ) :
170 data_from_updated_stream += node.toxml( encoding = 'utf-8' )
172 # The xml body without data - data come from file (all flow's subtags)
173 oper_update_stream = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n' \
174 '<input xmlns="urn:opendaylight:flow:service">\n' \
175 '<original-flow>\n' \
177 '</original-flow>\n' \
180 '</updated-flow>\n' \
181 '<node xmlns:inv="urn:opendaylight:inventory">/inv:nodes/inv:node[inv:id="openflow:1"]</node>\n' \
182 '</input>' % ( self.data_from_file_input, data_from_updated_stream )
184 self.post_REST_XML_request( self.oper_url_upd, oper_update_stream )
185 # TODO : check no empty transaction_id from post add_service
187 # check request content against restconf's config datastore
188 # operation service don't change anything in a Config. Data Store
189 # so we expect 404 response code (same as a check after delete
190 self.get_REST_XML_deleted_response( self.conf_url )
191 # check request content against restconf's operational datastore
192 # operational Data Store has to present updated flow
193 response = self.get_REST_XML_response( self.oper_url_get )
194 self.__validate_contain_flow( response, xml_updated_DOM )
196 # -------------- DELETE -------------------
197 self.log.info( self._paint_msg_yellow( " DELETE Flow by remove-sal-service" ) )
198 # Delte data from config DataStore
199 response = self.post_REST_XML_request( self.oper_url_del, self.oper_input_stream )
200 # Data never been added, so we expect the 404 response code
201 response = self.get_REST_XML_deleted_response( self.conf_url )
202 # Flow operational data has a specific content
203 # and the comparable data has to be filtered before comparison
204 response = self.get_REST_XML_response( self.oper_url_get )
205 self.__validate_contain_flow( response, self.xml_input_DOM, False )
208 # --------------- HELP METHODS ---------------
211 def __validate_contain_flow( self, oper_resp, orig_DOM, exp_contain = True ):
212 xml_resp_stream = ( oper_resp.text ).encode( 'utf-8', 'ignore' )
213 xml_resp_DOM = md.parseString( xml_resp_stream )
214 nodeListOperFlows = xml_resp_DOM.getElementsByTagName( 'flow-statistics' )
215 origDict = XMLtoDictParserTools.parseDOM_ToDict( orig_DOM._get_documentElement(),
216 ignoreList = IGNORED_TAGS_FOR_OPERATIONAL_COMPARISON )
217 origDict['flow-statistics'] = origDict.pop( 'flow' )
219 for node in nodeListOperFlows :
220 if self.__is_wanted_flow( orig_DOM, node ) :
221 nodeDict = XMLtoDictParserTools.parseDOM_ToDict( node,
222 ignoreList = IGNORED_TAGS_FOR_OPERATIONAL_COMPARISON )
225 if nodeDict != origDict :
226 err_msg = '\n!!!!! Loaded operation statistics doesn\'t contain expected flow \n' \
227 ' expected: %s\n found: %s\n differences: %s\n' \
228 '' % ( origDict, nodeDict, XMLtoDictParserTools.getDifferenceDict( origDict, nodeDict ) )
229 self.log.error( self._paint_msg_red( err_msg ) )
230 raise AssertionError( err_msg )
232 if nodeDict == origDict :
233 err_msg = '\n !!! Loaded operation statistics contains expected flow, delete fail \n' \
234 ' found: %s\n' % ( nodeDict )
235 self.log.error( self._paint_msg_red( err_msg ) )
236 raise AssertionError( err_msg )
239 # ID is not a response part from device so we have to check the correct identification
240 def __is_wanted_flow( self, orig_flow_DOM, resp_flow_DOM ):
241 identif_list = ['priority', 'cookie', 'match']
242 for ident in identif_list :
243 orig_ident_node = orig_flow_DOM.getElementsByTagName( ident )[0]
244 resp_ident_node = resp_flow_DOM.getElementsByTagName( ident )[0]
245 orig_ident_dict = XMLtoDictParserTools.parseDOM_ToDict( orig_ident_node,
246 ignoreList = IGNORED_TAGS_FOR_OPERATIONAL_COMPARISON )
247 resp_ident_dict = XMLtoDictParserTools.parseDOM_ToDict( resp_ident_node,
248 ignoreList = IGNORED_TAGS_FOR_OPERATIONAL_COMPARISON )
249 if ( orig_ident_dict != resp_ident_dict ) :
255 def __update_flow_input( self ):
256 # action only for yet
257 xml_dom_input = md.parseString( self.xml_input_stream )
258 actionList = xml_dom_input.getElementsByTagName( 'action' )
259 if actionList is not None and len( actionList ) > 0 :
260 action = actionList[0]
261 for child in action.childNodes :
262 if child.nodeType == Element.ELEMENT_NODE:
263 nodeKey = ( child.localName ).encode( 'utf-8', 'ignore' )
264 if nodeKey != 'order' :
265 if nodeKey != 'drop-action' :
266 new_act = child.ownerDocument.createElement( 'drop-action' )
268 new_act = child.ownerDocument.createElement( 'dec-mpls-ttl' )
269 child.parentNode.replaceChild( new_act, child )
270 return xml_dom_input.toxml( encoding = 'utf-8' )