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