add enable_hmac slice tag accessor
[plcapi.git] / PLC / Methods / GetSlivers.py
1 # $Id$
2 # $URL$
3 import time
4
5 from PLC.Faults import *
6 from PLC.Method import Method
7 from PLC.Parameter import Parameter, Mixed
8 from PLC.Filter import Filter
9 from PLC.Auth import Auth
10 from PLC.Nodes import Node, Nodes
11 from PLC.Interfaces import Interface, Interfaces
12 from PLC.NodeGroups import NodeGroup, NodeGroups
13 from PLC.ConfFiles import ConfFile, ConfFiles
14 from PLC.Slices import Slice, Slices
15 from PLC.Persons import Person, Persons
16 from PLC.Sites import Sites
17 from PLC.Roles import Roles
18 from PLC.Keys import Key, Keys
19 from PLC.SliceTags import SliceTag, SliceTags
20 from PLC.InitScripts import InitScript, InitScripts
21 from PLC.Leases import Lease, Leases
22 from PLC.Timestamp import Duration
23 from PLC.Methods.GetSliceFamily import GetSliceFamily
24
25 from PLC.Accessors.Accessors_standard import *
26
27 # XXX used to check if slice expiration time is sane
28 MAXINT =  2L**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, 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 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 not in slice_tags:
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 ]:
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).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         timestamp = int(time.time())
211
212         # Get node
213         if node_id_or_hostname is None:
214             if isinstance(self.caller, Node):
215                 node = self.caller
216             else:
217                 raise PLCInvalidArgument, "'node_id_or_hostname' not specified"
218         else:
219             nodes = Nodes(self.api, [node_id_or_hostname])
220             if not nodes:
221                 raise PLCInvalidArgument, "No such node"
222             node = nodes[0]
223
224             if node['peer_id'] is not None:
225                 raise PLCInvalidArgument, "Not a local node"
226
227         # Get interface information
228         interfaces = Interfaces(self.api, node['interface_ids'])
229
230         # Get node group information
231         nodegroups = NodeGroups(self.api, node['nodegroup_ids']).dict('groupname')
232         groups = nodegroups.keys()
233
234         # Get all (enabled) configuration files
235         all_conf_files = ConfFiles(self.api, {'enabled': True}).dict()
236         conf_files = {}
237
238         # Global configuration files are the default. If multiple
239         # entries for the same global configuration file exist, it is
240         # undefined which one takes precedence.
241         for conf_file in all_conf_files.values():
242             if not conf_file['node_ids'] and not conf_file['nodegroup_ids']:
243                 conf_files[conf_file['dest']] = conf_file
244
245         # Node group configuration files take precedence over global
246         # ones. If a node belongs to multiple node groups for which
247         # the same configuration file is defined, it is undefined
248         # which one takes precedence.
249         for nodegroup in nodegroups.values():
250             for conf_file_id in nodegroup['conf_file_ids']:
251                 if conf_file_id in all_conf_files:
252                     conf_file = all_conf_files[conf_file_id]
253                     conf_files[conf_file['dest']] = conf_file
254
255         # Node configuration files take precedence over node group
256         # configuration files.
257         for conf_file_id in node['conf_file_ids']:
258             if conf_file_id in all_conf_files:
259                 conf_file = all_conf_files[conf_file_id]
260                 conf_files[conf_file['dest']] = conf_file
261
262         # Get all (enabled) initscripts
263         initscripts = InitScripts(self.api, {'enabled': True})
264
265         # Get system slices
266         system_slice_tags = SliceTags(self.api, {'tagname': 'system', 'value': '1'}).dict('slice_id')
267         system_slice_ids = system_slice_tags.keys()
268
269         # Get nm-controller slices
270         # xxx Thierry: should these really be exposed regardless of their mapping to nodes ?
271         controller_and_delegated_slices = Slices(self.api, {'instantiation': ['nm-controller', 'delegated']}, ['slice_id']).dict('slice_id')
272         controller_and_delegated_slice_ids = controller_and_delegated_slices.keys()
273         slice_ids = system_slice_ids + controller_and_delegated_slice_ids + node['slice_ids']
274
275         slivers = get_slivers(self.api, auth, slice_ids, node)
276
277         # get the special accounts and keys needed for the node
278         # root
279         # site_admin
280         accounts = []
281         if False and 'site_id' not in node:
282             nodes = Nodes(self.api, node['node_id'])
283             node = nodes[0]
284
285         # used in conjunction with reduce to flatten lists, like in
286         # reduce ( reduce_flatten_list, [ [1] , [2,3] ], []) => [ 1,2,3 ]
287         def reduce_flatten_list (x,y): return x+y
288
289         # power users are pis and techs
290         def get_site_power_user_keys(api,site_id_or_name):
291             site = Sites (api,site_id_or_name,['person_ids'])[0]
292             key_ids = reduce (reduce_flatten_list,
293                               [ p['key_ids'] for p in \
294                                     Persons(api,{ 'person_id':site['person_ids'],
295                                                   'enabled':True, '|role_ids' : [20, 40] },
296                                             ['key_ids']) ],
297                               [])
298             return [ key['key'] for key in Keys (api, key_ids) if key['key_type']=='ssh']
299
300         # all admins regardless of their site
301         def get_all_admin_keys(api):
302             key_ids = reduce (reduce_flatten_list,
303                               [ p['key_ids'] for p in \
304                                     Persons(api, {'peer_id':None, 'enabled':True, '|role_ids':[10] },
305                                             ['key_ids']) ],
306                               [])
307             return [ key['key'] for key in Keys (api, key_ids) if key['key_type']=='ssh']
308
309         # 'site_admin' account setup
310         personsitekeys=get_site_power_user_keys(self.api,node['site_id'])
311         accounts.append({'name':'site_admin','keys':personsitekeys})
312
313         # 'root' account setup on nodes from all 'admin' users
314         personsitekeys=get_all_admin_keys(self.api)
315         accounts.append({'name':'root','keys':personsitekeys})
316
317         hrn = GetNodeHrn(self.api).call(auth,node['node_id'])
318
319         # XMPP config for omf federation
320         try:
321             if not self.api.config.PLC_OMF_ENABLED:
322                 raise Exception,"OMF disabled"
323             xmpp={'server':self.api.config.PLC_OMF_XMPP_SERVER,
324                   'user':self.api.config.PLC_OMF_XMPP_USER,
325                   'password':self.api.config.PLC_OMF_XMPP_PASSWORD,
326                   }
327         except:
328             xmpp={'server':None,'user':None,'password':None}
329
330         node.update_last_contact()
331
332         # expose leases & reservation policy
333         # in a first implementation we only support none and lease_or_idle
334         lease_exposed_fields = [ 'slice_id', 't_from', 't_until', 'name', ]
335         leases=None
336         if node['node_type'] != 'reservable':
337             reservation_policy='none'
338         else:
339             reservation_policy='lease_or_idle'
340             # expose the leases for the next 24 hours
341             leases = [ dict ( [ (k,l[k]) for k in lease_exposed_fields ] )
342                        for l in Leases (self.api, {'node_id':node['node_id'],
343                                                    'clip': (timestamp, timestamp+24*Duration.HOUR),
344                                                    '-SORT': 't_from',
345                                                    }) ]
346         granularity=self.api.config.PLC_RESERVATION_GRANULARITY
347
348         return {
349             'timestamp': timestamp,
350             'node_id': node['node_id'],
351             'hostname': node['hostname'],
352             'interfaces': interfaces,
353             'groups': groups,
354             'conf_files': conf_files.values(),
355             'initscripts': initscripts,
356             'slivers': slivers,
357             'accounts': accounts,
358             'xmpp':xmpp,
359             'hrn':hrn,
360             'reservation_policy': reservation_policy,
361             'leases':leases,
362             'lease_granularity': granularity,
363             }