revert _quote to marco's version
[plcapi.git] / PLC / Methods / GetSlivers.py
1 import time
2
3 from PLC.Faults import *
4 from PLC.Method import Method
5 from PLC.Parameter import Parameter, Mixed
6 from PLC.Filter import Filter
7 from PLC.Auth import Auth
8 from PLC.Nodes import Node, Nodes
9 from PLC.Interfaces import Interface, Interfaces
10 from PLC.NodeGroups import NodeGroup, NodeGroups
11 from PLC.ConfFiles import ConfFile, ConfFiles
12 from PLC.Slices import Slice, Slices
13 from PLC.Persons import Person, Persons
14 from PLC.Sites import Sites
15 from PLC.Roles import Roles
16 from PLC.Keys import Key, Keys
17 from PLC.SliceTags import SliceTag, SliceTags
18 from PLC.InitScripts import InitScript, InitScripts
19 from PLC.Leases import Lease, Leases
20 from PLC.Timestamp import Duration
21 from PLC.Methods.GetSliceFamily import GetSliceFamily
22 from PLC.PersonTags import PersonTag,PersonTags
23
24 from PLC.Accessors.Accessors_standard import *
25
26 # XXX used to check if slice expiration time is sane
27 MAXINT =  2L**31-1
28
29 # slice_filter essentially contains the slice_ids for the relevant slices (on the node + system & delegated slices)
30 def get_slivers(api, caller, auth, slice_filter, node = None):
31     # Get slice information
32     slices = Slices(api, slice_filter, ['slice_id', 'name', 'instantiation', 'expires', 'person_ids', 'slice_tag_ids'])
33
34     # Build up list of users and slice attributes
35     person_ids = set()
36     slice_tag_ids = set()
37     for slice in slices:
38         person_ids.update(slice['person_ids'])
39         slice_tag_ids.update(slice['slice_tag_ids'])
40
41     # Get user information
42     all_persons = Persons(api, {'person_id':person_ids,'enabled':True}, ['person_id', 'enabled', 'key_ids']).dict()
43
44     # Build up list of keys
45     key_ids = set()
46     for person in all_persons.values():
47         key_ids.update(person['key_ids'])
48
49     # Get user account keys
50     all_keys = Keys(api, key_ids, ['key_id', 'key', 'key_type']).dict()
51
52     # Get slice attributes
53     all_slice_tags = SliceTags(api, slice_tag_ids).dict()
54
55     slivers = []
56     for slice in slices:
57         keys = []
58         for person_id in slice['person_ids']:
59             if person_id in all_persons:
60                 person = all_persons[person_id]
61                 if not person['enabled']:
62                     continue
63                 for key_id in person['key_ids']:
64                     if key_id in all_keys:
65                         key = all_keys[key_id]
66                         keys += [{'key_type': key['key_type'],
67                                   'key': key['key']}]
68
69         attributes = []
70
71         # All (per-node and global) attributes for this slice
72         slice_tags = []
73         for slice_tag_id in slice['slice_tag_ids']:
74             if slice_tag_id in all_slice_tags:
75                 slice_tags.append(all_slice_tags[slice_tag_id])
76
77         # Per-node sliver attributes take precedence over global
78         # slice attributes, so set them first.
79         # Then comes nodegroup slice attributes
80         # Followed by global slice attributes
81         sliver_attributes = []
82
83         if node is not None:
84             for sliver_attribute in [ a for a in slice_tags if a['node_id'] == node['node_id'] ]:
85                 sliver_attributes.append(sliver_attribute['tagname'])
86                 attributes.append({'tagname': sliver_attribute['tagname'],
87                                    'value': sliver_attribute['value']})
88
89             # set nodegroup slice attributes
90             for slice_tag in [ a for a in slice_tags if a['nodegroup_id'] in node['nodegroup_ids'] ]:
91                 # Do not set any nodegroup slice attributes for
92                 # which there is at least one sliver attribute
93                 # already set.
94                 if slice_tag not in slice_tags:
95                     attributes.append({'tagname': slice_tag['tagname'],
96                                    'value': slice_tag['value']})
97
98         for slice_tag in [ a for a in slice_tags if a['node_id'] is None ]:
99             # Do not set any global slice attributes for
100             # which there is at least one sliver attribute
101             # already set.
102             if slice_tag['tagname'] not in sliver_attributes:
103                 attributes.append({'tagname': slice_tag['tagname'],
104                                    'value': slice_tag['value']})
105
106         # XXX Sanity check; though technically this should be a system invariant
107         # checked with an assertion
108         if slice['expires'] > MAXINT:  slice['expires']= MAXINT
109
110         # expose the slice vref as computed by GetSliceFamily
111         family = GetSliceFamily (api,caller).call(auth, slice['slice_id'])
112
113         slivers.append({
114             'name': slice['name'],
115             'slice_id': slice['slice_id'],
116             'instantiation': slice['instantiation'],
117             'expires': slice['expires'],
118             'keys': keys,
119             'attributes': attributes,
120             'GetSliceFamily': family,
121             })
122
123     return slivers
124
125 ### The pickle module, used in conjunction with caching has a restriction that it does not
126 ### work on "connection objects." It doesn't matter if the connection object has
127 ### an 'str' or 'repr' method, there is a taint check that throws an exception if
128 ### the pickled class is found to derive from a connection.
129 ### (To be moved to Method.py)
130
131 def sanitize_for_pickle (obj):
132     if (isinstance(obj, dict)):
133         parent = dict(obj)
134         for k in parent.keys(): parent[k] = sanitize_for_pickle (parent[k])
135         return parent
136     elif (isinstance(obj, list)):
137         parent = list(obj)
138         parent = map(sanitize_for_pickle, parent)
139         return parent
140     else:
141         return obj
142
143 class GetSlivers(Method):
144     """
145     Returns a struct containing information about the specified node
146     (or calling node, if called by a node and node_id_or_hostname is
147     not specified), including the current set of slivers bound to the
148     node.
149
150     All of the information returned by this call can be gathered from
151     other calls, e.g. GetNodes, GetInterfaces, GetSlices, etc. This
152     function exists almost solely for the benefit of Node Manager.
153     """
154
155     roles = ['admin', 'node']
156
157     accepts = [
158         Auth(),
159         Mixed(Node.fields['node_id'],
160               Node.fields['hostname']),
161         ]
162
163     returns = {
164         'timestamp': Parameter(int, "Timestamp of this call, in seconds since UNIX epoch"),
165         'node_id': Node.fields['node_id'],
166         'hostname': Node.fields['hostname'],
167         'interfaces': [Interface.fields],
168         'groups': [NodeGroup.fields['groupname']],
169         'conf_files': [ConfFile.fields],
170         'initscripts': [InitScript.fields],
171         'accounts': [{
172             'name': Parameter(str, "unix style account name", max = 254),
173             'keys': [{
174                 'key_type': Key.fields['key_type'],
175                 'key': Key.fields['key']
176             }],
177             }],
178         'slivers': [{
179             'name': Slice.fields['name'],
180             'slice_id': Slice.fields['slice_id'],
181             'instantiation': Slice.fields['instantiation'],
182             'expires': Slice.fields['expires'],
183             'keys': [{
184                 'key_type': Key.fields['key_type'],
185                 'key': Key.fields['key']
186             }],
187             'attributes': [{
188                 'tagname': SliceTag.fields['tagname'],
189                 'value': SliceTag.fields['value']
190             }]
191         }],
192         # how to reach the xmpp server
193         'xmpp': {'server':Parameter(str,"hostname for the XMPP server"),
194                  'user':Parameter(str,"username for the XMPP server"),
195                  'password':Parameter(str,"username for the XMPP server"),
196                  },
197         # we consider three policies (reservation-policy)
198         # none : the traditional way to use a node
199         # lease_or_idle : 0 or 1 slice runs at a given time
200         # lease_or_shared : 1 slice is running during a lease, otherwise all the slices come back
201         'reservation_policy': Parameter(str,"one among none, lease_or_idle, lease_or_shared"),
202         'leases': [  { 'slice_id' : Lease.fields['slice_id'],
203                        't_from' : Lease.fields['t_from'],
204                        't_until' : Lease.fields['t_until'],
205                        }],
206     }
207
208     def call(self, auth, node_id_or_hostname = None):
209         return self.raw_call(auth, node_id_or_hostname)
210
211
212     def raw_call(self, auth, node_id_or_hostname):
213         timestamp = int(time.time())
214
215         # Get node
216         if node_id_or_hostname is None:
217             if isinstance(self.caller, Node):
218                 node = self.caller
219             else:
220                 raise PLCInvalidArgument, "'node_id_or_hostname' not specified"
221         else:
222             nodes = Nodes(self.api, [node_id_or_hostname])
223             if not nodes:
224                 raise PLCInvalidArgument, "No such node"
225             node = nodes[0]
226
227             if node['peer_id'] is not None:
228                 raise PLCInvalidArgument, "Not a local node"
229
230         # Get interface information
231         interfaces = Interfaces(self.api, node['interface_ids'])
232
233         # Get node group information
234         nodegroups = NodeGroups(self.api, node['nodegroup_ids']).dict('groupname')
235         groups = nodegroups.keys()
236
237         # Get all (enabled) configuration files
238         all_conf_files = ConfFiles(self.api, {'enabled': True}).dict()
239         conf_files = {}
240
241         # Global configuration files are the default. If multiple
242         # entries for the same global configuration file exist, it is
243         # undefined which one takes precedence.
244         for conf_file in all_conf_files.values():
245             if not conf_file['node_ids'] and not conf_file['nodegroup_ids']:
246                 conf_files[conf_file['dest']] = conf_file
247
248         # Node group configuration files take precedence over global
249         # ones. If a node belongs to multiple node groups for which
250         # the same configuration file is defined, it is undefined
251         # which one takes precedence.
252         for nodegroup in nodegroups.values():
253             for conf_file_id in nodegroup['conf_file_ids']:
254                 if conf_file_id in all_conf_files:
255                     conf_file = all_conf_files[conf_file_id]
256                     conf_files[conf_file['dest']] = conf_file
257
258         # Node configuration files take precedence over node group
259         # configuration files.
260         for conf_file_id in node['conf_file_ids']:
261             if conf_file_id in all_conf_files:
262                 conf_file = all_conf_files[conf_file_id]
263                 conf_files[conf_file['dest']] = conf_file
264
265         # Get all (enabled) initscripts
266         initscripts = InitScripts(self.api, {'enabled': True})
267
268         # Get system slices
269         system_slice_tags = SliceTags(self.api, {'tagname': 'system', 'value': '1'}).dict('slice_id')
270         system_slice_ids = system_slice_tags.keys()
271
272         # Get nm-controller slices
273         # xxx Thierry: should these really be exposed regardless of their mapping to nodes ?
274         controller_and_delegated_slices = Slices(self.api, {'instantiation': ['nm-controller', 'delegated']}, ['slice_id']).dict('slice_id')
275         controller_and_delegated_slice_ids = controller_and_delegated_slices.keys()
276         slice_ids = system_slice_ids + controller_and_delegated_slice_ids + node['slice_ids']
277
278         slivers = get_slivers(self.api, self.caller, auth, slice_ids, node)
279
280         # get the special accounts and keys needed for the node
281         # root
282         # site_admin
283         accounts = []
284         if False and 'site_id' not in node:
285             nodes = Nodes(self.api, node['node_id'])
286             node = nodes[0]
287
288         # used in conjunction with reduce to flatten lists, like in
289         # reduce ( reduce_flatten_list, [ [1] , [2,3] ], []) => [ 1,2,3 ]
290         def reduce_flatten_list (x,y): return x+y
291
292         # root users are users marked with the tag 'isrootonsite'. Hack for Mlab and other sites in which admins participate in diagnosing problems.
293         def get_site_root_user_keys(api,site_id_or_name):
294            site = Sites (api,site_id_or_name,['person_ids'])[0]
295            all_site_persons = site['person_ids']
296            all_site_person_tags = PersonTags(self.api,{'person_id':all_site_persons,'tagname':'isrootonsite'},['value','person_id'])
297            site_root_person_tags = filter(lambda r:r['value']=='true',all_site_person_tags)
298            site_root_person_ids = map(lambda r:r['person_id'],site_root_person_tags)
299            key_ids = reduce (reduce_flatten_list,
300                              [ p['key_ids'] for p in \
301                                    Persons(api,{ 'person_id':site_root_person_ids,
302                                                  'enabled':True, '|role_ids' : [20, 40] },
303                                            ['key_ids']) ],
304                              [])
305            return [ key['key'] for key in Keys (api, key_ids) if key['key_type']=='ssh']
306
307         # power users are pis and techs
308         def get_site_power_user_keys(api,site_id_or_name):
309             site = Sites (api,site_id_or_name,['person_ids'])[0]
310             key_ids = reduce (reduce_flatten_list,
311                               [ p['key_ids'] for p in \
312                                     Persons(api,{ 'person_id':site['person_ids'],
313                                                   'enabled':True, '|role_ids' : [20, 40] },
314                                             ['key_ids']) ],
315                               [])
316             return [ key['key'] for key in Keys (api, key_ids) if key['key_type']=='ssh']
317
318         # all admins regardless of their site
319         def get_all_admin_keys(api):
320             key_ids = reduce (reduce_flatten_list,
321                               [ p['key_ids'] for p in \
322                                     Persons(api, {'peer_id':None, 'enabled':True, '|role_ids':[10] },
323                                             ['key_ids']) ],
324                               [])
325             return [ key['key'] for key in Keys (api, key_ids) if key['key_type']=='ssh']
326
327         # 'site_admin' account setup
328         personsitekeys=get_site_power_user_keys(self.api,node['site_id'])
329         accounts.append({'name':'site_admin','keys':personsitekeys})
330
331         # 'root' account setup on nodes from all 'admin' users and ones marked with 'isrootonsite' for this site
332         siterootkeys=get_site_root_user_keys(self.api,node['site_id'])
333         personsitekeys=get_all_admin_keys(self.api)
334         personsitekeys.extend(siterootkeys)
335
336         accounts.append({'name':'root','keys':personsitekeys})
337
338         hrn = GetNodeHrn(self.api,self.caller).call(auth,node['node_id'])
339
340         # XMPP config for omf federation
341         try:
342             if not self.api.config.PLC_OMF_ENABLED:
343                 raise Exception,"OMF disabled"
344             xmpp={'server':self.api.config.PLC_OMF_XMPP_SERVER,
345                   'user':self.api.config.PLC_OMF_XMPP_USER,
346                   'password':self.api.config.PLC_OMF_XMPP_PASSWORD,
347                   }
348         except:
349             xmpp={'server':None,'user':None,'password':None}
350
351         node.update_last_contact()
352
353         # expose leases & reservation policy
354         # in a first implementation we only support none and lease_or_idle
355         lease_exposed_fields = [ 'slice_id', 't_from', 't_until', 'name', ]
356         leases=None
357         if node['node_type'] != 'reservable':
358             reservation_policy='none'
359         else:
360             reservation_policy='lease_or_idle'
361             # expose the leases for the next 24 hours
362             leases = [ dict ( [ (k,l[k]) for k in lease_exposed_fields ] )
363                        for l in Leases (self.api, {'node_id':node['node_id'],
364                                                    'clip': (timestamp, timestamp+24*Duration.HOUR),
365                                                    '-SORT': 't_from',
366                                                    }) ]
367         granularity=self.api.config.PLC_RESERVATION_GRANULARITY
368
369         raw_data = {
370             'timestamp': timestamp,
371             'node_id': node['node_id'],
372             'hostname': node['hostname'],
373             'interfaces': interfaces,
374             'groups': groups,
375             'conf_files': conf_files.values(),
376             'initscripts': initscripts,
377             'slivers': slivers,
378             'accounts': accounts,
379             'xmpp':xmpp,
380             'hrn':hrn,
381             'reservation_policy': reservation_policy,
382             'leases':leases,
383             'lease_granularity': granularity,
384         }
385
386         sanitized_data = sanitize_for_pickle (raw_data)
387         return sanitized_data
388