1 """Library hiding details of restconf authentication.
3 Suitable for performance tests against different
4 restconf authentication methods.
5 Type of authentication is currently determined by "scope" value.
7 This library does not rely on RobotFramework libraries,
8 so it is suitable for running from wide range of VMs.
9 Requirements: Basic Python installation,
10 which should include "json" and "requests" Python modules.
12 *_Using_Session keywords take the same kwargs as requests.Session.request,
13 but instead of method and URL, they take "session" created by
14 Init_Session keyword and URI (without "/restconf/").
16 Due to performance of TCP on some systems, two session strategies are available.
17 reuse=True reuses the same requests.Session, which possibly means
18 the library tries to re-use the same source TCP port.
19 This conserves resources (TCP ports available), but it may lead
20 to a significant performance hit (as in 10 times slower).
21 reuse=False closes every requests.Session object, presumably
22 causing the new session to take another TCP port.
23 This has good performance, but may perhaps lead to port starvation in some cases.
25 TODO: Put "RESTCONF" to more places,
26 as URIs not starting with /restconf/ are not supported yet.
29 # Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved.
31 # This program and the accompanying materials are made available under the
32 # terms of the Eclipse Public License v1.0 which accompanies this distribution,
33 # and is available at http://www.eclipse.org/legal/epl-v10.html
39 __author__ = "Vratko Polak"
40 __copyright__ = "Copyright(c) 2015, Cisco Systems, Inc."
41 __license__ = "Eclipse Public License v1.0"
42 __email__ = "vrpolak@cisco.com"
46 # Karaf Keyword definitions.
48 def Init_Session(ip, username, password, scope, reuse=True, port="8181"):
49 """Robot keyword, return opaque session object, which handles authentication automatically."""
52 return _TokenReusingSession(ip, username, password, scope, port=port)
54 return _TokenClosingSession(ip, username, password, scope, port=port)
57 return _BasicReusingSession(ip, username, password, port=port)
59 return _BasicClosingSession(ip, username, password, port=port)
62 def Get_Using_Session(session, uri, **kwargs):
63 """Robot keyword, perform GET operation using given opaque session object."""
64 return session.robust_method("GET", uri, **kwargs)
67 def Post_Using_Session(session, uri, **kwargs):
68 """Robot keyword, perform POST operation using given opaque session object."""
69 return session.robust_method("POST", uri, **kwargs)
72 def Put_Using_Session(session, uri, **kwargs):
73 """Robot keyword, perform PUT operation using given opaque session object."""
74 return session.robust_method("PUT", uri, **kwargs)
77 def Delete_Using_Session(session, uri, **kwargs):
78 """Robot keyword, perform DELETE operation using given opaque session object."""
79 return session.robust_method("DELETE", uri, **kwargs)
85 class _BasicReusingSession(object):
86 """Handling of restconf requests using persistent session and basic http authentication."""
88 def __init__(self, ip, username="", password="", port="8181"):
89 """Initialize session using hardcoded text data, remember credentials."""
90 self.rest_prefix = "http://" + ip + ":" + port + "/restconf/"
91 self.session = requests.Session()
93 self.session.auth = (username, password) # May work with non-string values
95 self.session.auth = None # Supports "no authentication mode" as in odl-restconf-noauth
97 def robust_method(self, method, uri, **kwargs):
98 """Try method once using session credentials. Return last response."""
99 return self.session.request(method, self.rest_prefix + uri, **kwargs)
100 # TODO: Do we want to fix URI at __init__ just to avoid string concat?
103 class _BasicClosingSession(object):
104 """Handling of restconf requests using one-time sessions and basic http authentication."""
106 def __init__(self, ip, username="", password="", port="8181"):
107 """Prepare session initialization data using hardcoded text, remember credentials."""
108 self.rest_prefix = "http://" + ip + ":" + port + "/restconf/"
110 self.auth = (username, password) # May work with non-string values
112 self.auth = None # Supports "no authentication mode" as in odl-restconf-noauth
115 def robust_method(self, method, uri, **kwargs):
116 """Create new session, send method using remembered credentials. Return response"""
118 # The session object may still keep binding a TCP port here.
119 # As caller has finished processing previous response,
120 # we can explicitly close the old session to free resources
121 # before creating new session.
123 self.session = requests.Session()
124 self.session.auth = self.auth
125 return self.session.request(method, self.rest_prefix + uri, **kwargs)
128 class _TokenReusingSession(object):
129 """Handling of restconf requests using token-based authentication, one session per token."""
131 def __init__(self, ip, username, password, scope, port="8181"):
132 """Initialize session using hardcoded text data."""
133 self.auth_url = "http://" + ip + ":" + port + "/oauth2/token"
134 self.rest_prefix = "http://" + ip + ":" + port + "/restconf/"
135 self.auth_data = "grant_type=password&username=" + username
136 self.auth_data += "&password=" + password + "&scope=" + scope
137 self.auth_header = {"Content-Type": "application/x-www-form-urlencoded"}
142 def refresh_token(self):
143 """Reset session, invoke call to get token, parse it and remember."""
146 self.session = requests.Session()
147 resp = self.session.post(self.auth_url, data=self.auth_data, headers=self.auth_header)
148 resp_obj = json.loads(resp.text)
150 token = resp_obj["access_token"]
152 raise RuntimeError("Parse failed: " + resp.text)
154 # TODO: Use logging so that callers could see token refreshes.
155 # We keep self.session to use for the following restconf requests.
157 def oneshot_method(self, method, uri, **kwargs):
158 """Return response of request of given method to given uri (without restconf/)."""
159 # Token needs to be merged into headers.
160 authed_headers = kwargs.get("headers", {})
161 authed_headers["Authorization"] = "Bearer " + self.token
162 authed_kwargs = dict(kwargs) # shallow copy
163 authed_kwargs["headers"] = authed_headers
164 return self.session.request(method, self.rest_prefix + uri, **authed_kwargs)
166 def robust_method(self, method, uri, **kwargs):
167 """Try method once; upon 401, refresh token and retry once. Return last response."""
168 resp = self.oneshot_method(method, uri, **kwargs)
169 if resp.status_code != 401:
172 return self.oneshot_method(method, uri, **kwargs)
175 class _TokenClosingSession(object):
176 """Handling of restconf requests using token-based authentication, one session per request."""
178 def __init__(self, ip, username, password, scope, port="8181"):
179 """Prepare session initialization data using hardcoded text."""
180 self.auth_url = "http://" + ip + ":" + port + "/oauth2/token"
181 self.rest_prefix = "http://" + ip + ":" + port + "/restconf/"
182 self.auth_data = "grant_type=password&username=" + username
183 self.auth_data += "&password=" + password + "&scope=" + scope
184 self.auth_header = {"Content-Type": "application/x-www-form-urlencoded"}
189 def refresh_token(self):
190 """Reset session, invoke call to get token, parse it and remember."""
193 self.session = requests.Session()
194 resp = self.session.post(self.auth_url, data=self.auth_data, headers=self.auth_header)
195 resp_obj = json.loads(resp.text)
197 token = resp_obj["access_token"]
199 raise RuntimeError("Parse failed: " + resp.text)
201 # TODO: Use logging so that callers could see token refreshes.
202 # We keep self.session to use for the following restconf requests.
204 def oneshot_method(self, method, uri, **kwargs):
205 """Reset session, return response of request of given method to given uri (without restconf/)."""
206 # This assumes self.session was already initialized.
208 self.session = requests.Session()
209 # Token needs to be merged into headers.
210 authed_headers = kwargs.get("headers", {})
211 authed_headers["Authorization"] = "Bearer " + self.token
212 authed_kwargs = dict(kwargs) # shallow copy
213 authed_kwargs["headers"] = authed_headers
214 return self.session.request(method, self.rest_prefix + uri, **authed_kwargs)
216 def robust_method(self, method, uri, **kwargs):
217 """Try method once; upon 401, refresh token and retry once. Return last response."""
218 resp = self.oneshot_method(method, uri, **kwargs)
219 if resp.status_code != 401:
222 return self.oneshot_method(method, uri, **kwargs)