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