Merge "Bug 6745 Remove thread renaming and unnecessary logging" into stable/boron
[openflowplugin.git] / test-scripts / crud / odl_flow_test.py
1 '''
2 Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
3
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
7
8 Created on May 11, 2014
9
10 @author: <a href="mailto:vdemcak@cisco.com">Vaclav Demcak</a>
11 '''
12 import requests
13 from xml.dom.minidom import Element
14
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
19
20
21 TABLE_ID_TAG_NAME = 'table_id'
22 FLOW_ID_TAG_NAME = 'id'
23
24 IGNORED_TAGS_FOR_OPERATIONAL_COMPARISON = ['id', 'flow-name', 'barrier', 'cookie_mask', 'installHw', 'flags', 'strict', 'byte-count', 'duration', 'packet-count', 'in-port']
25
26 FLOW_TAGS_FOR_UPDATE = ['action']
27
28 class OF_CRUD_Test_Flows( OF_CRUD_Test_Base ):
29
30
31     def setUp( self ):
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
42         # ---- operations ---
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' )
55
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' \
59                                         '%s' \
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
62
63
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()
72
73
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 )
88
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 )
102
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 )
113
114
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 )
132
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 )
143
144
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
152
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 )
161
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' )
171
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' \
176                                         '%s' \
177                                     '</original-flow>\n' \
178                                     '<updated-flow>\n' \
179                                         '%s' \
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 )
183
184         self.post_REST_XML_request( self.oper_url_upd, oper_update_stream )
185         # TODO : check no empty transaction_id from post add_service
186
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 )
195
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 )
206
207
208 # --------------- HELP METHODS ---------------
209
210
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' )
218         nodeDict = {}
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 )
223                 break
224         if exp_contain :
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 )
231         else :
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 )
237
238
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 ) :
250                 return False
251         return True
252
253
254
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' )
267                         else :
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' )