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