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