d9141b66a090fbb3cb0e1678d021a45391411344
[nepi.git] / src / nepi / util / manifoldapi.py
1 #
2 #    NEPI, a framework to manage network experiments
3 #    Copyright (C) 2013 INRIA
4 #
5 #    This program is free software: you can redistribute it and/or modify
6 #    it under the terms of the GNU General Public License version 2 as
7 #    published by the Free Software Foundation;
8 #
9 #    This program is distributed in the hope that it will be useful,
10 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
11 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 #    GNU General Public License for more details.
13 #
14 #    You should have received a copy of the GNU General Public License
15 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 #
17 # Author: Lucia Guevgeozian Odizzio <lucia.guevgeozian_odizzio@inria.fr>
18
19 from __future__ import print_function
20
21
22 import xmlrpc.client
23 import hashlib
24 import threading
25
26
27 class MANIFOLDAPI(object):
28     """
29     API to query different data platforms as SFA, TopHat, OML Central Server,
30     using Manifold Framework, the backend of MySlice.
31     """
32     def __init__(self, username, password, hostname, urlpattern): 
33         
34         self.auth_pwd = dict(AuthMethod='password', Username=username, 
35             AuthString=password)
36         self._url = urlpattern % {'hostname':hostname}
37         self.lock = threading.Lock()
38         self.auth = self.get_session_key()
39
40     @property
41     def api(self):
42         return xmlrpc.client.Server(self._url, allow_none = True)
43
44     def get_session_key(self):
45         """
46         Retrieves the session key, in order to use the same session for 
47         queries.
48         """
49         query = {'timestamp' : 'now', 'object': 'local:session',      
50             'filters' : [], 'fields' : [], 'action' : 'create'}
51
52         session = self.api.forward(self.auth_pwd, query)
53
54         if not session['value']:
55             msg = "Can not authenticate in Manifold API"
56             raise RuntimeError(msg)
57
58         session_key = session['value'][0]['session']
59         return dict(AuthMethod='session', session=session_key)
60
61     def get_resource_info(self, filters=None, fields=None):
62         """
63         Create and execute the Manifold API Query to get the resources 
64         according fields and filters.
65         :param filters: resource's constraints for the experiment
66         :type filters: dict
67         :param fields: desire fields in the result of the query
68         :type fields: list
69         """
70         query = {'action' : 'get', 'object' : 'resource'}
71
72         if filters:
73             filters = self._map_attr_to_resource_filters(filters)
74
75             qfilters = list()
76             for filtername, filtervalue in filters.items():
77                 newfilter = [filtername, "==", filtervalue]
78                 qfilters.append(newfilter)
79             
80             query['filters'] = qfilters
81
82         if fields:
83             fields = self._check_valid_fields(fields)
84
85             if fields:
86                 query['fields'] = fields
87
88         return self.api.forward(self.auth, query)['value']
89         
90     def get_resource_urn(self, filters=None):
91         """
92         Retrieves the resources urn of the resources matching filters.
93         """
94         return self.get_resource_info(filters, 'urn')
95
96     def get_slice_resources(self, slicename):
97         """
98         Retrieves resources attached to user's slice.
99         return value: list of resources' urn
100         """
101         result = []
102         query = {'action' : 'get', 'object' : 'resource', 
103             'filters' : [['slice','==', slicename]],
104             'fields' : ['urn']}
105
106         with self.lock:
107             value = self.api.forward(self.auth, query)['value']
108
109         for resource in value:
110             result.append(resource['urn'])
111         
112         return result
113
114     def add_resource_to_slice(self, slicename, resource_urn):
115         """
116         Add resource to user's slice. The query needs to specify the new
117         resource plus the previous resources already in the slice.
118         """
119         resources = self.get_slice_resources(slicename)
120         resources.append(resource_urn) 
121
122         urn_list = list()
123         for r in resources:
124             urn_dict = dict()
125             urn_dict['urn'] = r
126             urn_list.append(urn_dict)
127             
128         query = {'action' : 'update', 'object' : 'slice', 
129             'filters' : [['slice_hrn','==', slicename]],
130             'params' : {'resource' : urn_list}}
131  
132         with self.lock:
133             self.api.forward(self.auth, query)
134
135         resources = self.get_slice_resources(slicename)
136         if resource_urn in resources:
137             return True
138         else:
139             msg = "Failed while trying to add %s to slice" % resource_urn
140             print(msg)
141             # check how to do warning
142             return False
143
144     def remove_resource_from_slice(self, slicename, resource_urn):
145         """
146         Remove resource from user's slice. The query needs to specify the list
147         of previous resources in the slice without the one to be remove.
148         """
149         resources = self.get_slice_resources(slicename)
150         resources.remove(resource_urn)
151
152         urn_list = list()
153         for r in resources:
154             urn_dict = dict()
155             urn_dict['urn'] = r
156             urn_list.append(urn_dict)
157
158         query = {'action' : 'update', 'object' : 'slice',
159             'filters' : [['slice_hrn','==', slicename]],
160             'params' : {'resource' : urn_list}}
161
162         with self.lock:
163             self.api.forward(self.auth, query)
164
165         resources = self.get_slice_resources(slicename)
166         if resource_urn not in resources:
167             return True
168         else:
169             msg = "Failed while trying to remove %s to slice" % resource_urn
170             # check how to do warning
171             return False
172
173     def _get_metadata(self):
174         """
175         This method is useful to retrive metadata from different platforms
176         in order to update fields and possible filters.
177         """
178         query = {'action' : 'get', 'object' : 'local:object', 
179             'filters' : [['table','=','resource']]}
180         
181         res = self.api.forward(self.auth, query)
182
183         valid_fields = list()
184         for i in res['value'][0]['column']:
185             valid_fields.append(i['name'])
186
187         return valid_fields
188         
189     def _map_attr_to_resource_filters(self, filters):
190         """
191         Depending on the object used for the Manifold query, the filters and 
192         fields can change its sintaxis. A resource field in a slice object
193         query adds 'resource.' to the field. Other changes don't follow any 
194         particular convention.
195         """
196         #TODO: find out useful filters
197         attr_to_filter = {
198             'hostname' : 'hostname',
199             'longitude' : 'longitude',
200             'latitude' : 'latitude',
201             'network' : 'network',
202             'component_id' : 'component_id'
203         }
204
205         mapped_filters = dict()
206         for filtername, filtervalue in filters.items():
207             if attr_to_filter[filtername]:
208                 new_filtername = attr_to_filter[filtername]
209                 mapped_filters[new_filtername] = filtervalue
210         
211         return mapped_filters
212          
213     def _check_valid_fields(self, fields):
214         """
215         The fields can be a predefine set, define in the Manifold metadata.
216         """
217         valid_fields = self._get_metadata()
218
219         if not isinstance(fields, list):
220             fields = [fields]
221
222         for field in fields:
223             if field not in valid_fields:
224                 fields.remove(field)
225                 #self.warning(" Invalid Manifold field or filter ")
226         
227         return fields
228
229
230 class MANIFOLDAPIFactory(object):
231     """
232     API Factory to manage a map of MANIFOLDAPI instances as key-value pairs, it
233     instanciate a single instance per key. The key represents the same SFA, 
234     MF (ManiFold) credentials.
235     """
236
237     _lock = threading.Lock()
238     _apis = dict()
239
240     @classmethod
241     def get_api(cls, username, password, 
242             #hostname = "manifold.pl.sophia.inria.fr",
243             hostname ="test.myslice.info",
244             urlpattern = "http://%(hostname)s:7080"):
245         """
246         :param username: Manifold user (also used for MySlice web login)
247         :type username: str
248         :param password: Manifold password (also used for MySlice web login)
249         :type password: str
250         :param hostname: Hostname of the Manifold API to query SFA, TopHat, etc
251         :type hostname: str
252         :param urlpattern: Url of the Manifold API to query SFA, TopHat, etc
253         :type urlpattern: str
254         """
255
256         if username and password:
257             key = cls.make_key(username, password)
258             with cls._lock:
259                 api = cls._apis.get(key)
260     
261                 if not api:
262                     api = MANIFOLDAPI(username, password, hostname, urlpattern)
263                     cls._apis[key] = api
264
265                 return api
266
267         return None
268
269     @classmethod
270     def make_key(cls, *args):
271         skey = "".join(map(str, args))
272         return hashlib.md5(skey).hexdigest()
273
274