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