a8ee408cc8162a6148b9ccd051be97658170cf32
[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 class GetSlivers(Method):
127     """
128     Returns a struct containing information about the specified node
129     (or calling node, if called by a node and node_id_or_hostname is
130     not specified), including the current set of slivers bound to the
131     node.
132
133     All of the information returned by this call can be gathered from
134     other calls, e.g. GetNodes, GetInterfaces, GetSlices, etc. This
135     function exists almost solely for the benefit of Node Manager.
136     """
137
138     roles = ['admin', 'node']
139
140     accepts = [
141         Auth(),
142         Mixed(Node.fields['node_id'],
143               Node.fields['hostname']),
144         ]
145
146     returns = {
147         'timestamp': Parameter(int, "Timestamp of this call, in seconds since UNIX epoch"),
148         'node_id': Node.fields['node_id'],
149         'hostname': Node.fields['hostname'],
150         'interfaces': [Interface.fields],
151         'groups': [NodeGroup.fields['groupname']],
152         'conf_files': [ConfFile.fields],
153         'initscripts': [InitScript.fields],
154         'accounts': [{
155             'name': Parameter(str, "unix style account name", max = 254),
156             'keys': [{
157                 'key_type': Key.fields['key_type'],
158                 'key': Key.fields['key']
159             }],
160             }],
161         'slivers': [{
162             'name': Slice.fields['name'],
163             'slice_id': Slice.fields['slice_id'],
164             'instantiation': Slice.fields['instantiation'],
165             'expires': Slice.fields['expires'],
166             'keys': [{
167                 'key_type': Key.fields['key_type'],
168                 'key': Key.fields['key']
169             }],
170             'attributes': [{
171                 'tagname': SliceTag.fields['tagname'],
172                 'value': SliceTag.fields['value']
173             }]
174         }],
175         # how to reach the xmpp server
176         'xmpp': {'server':Parameter(str,"hostname for the XMPP server"),
177                  'user':Parameter(str,"username for the XMPP server"),
178                  'password':Parameter(str,"username for the XMPP server"),
179                  },
180         # we consider three policies (reservation-policy)
181         # none : the traditional way to use a node
182         # lease_or_idle : 0 or 1 slice runs at a given time
183         # lease_or_shared : 1 slice is running during a lease, otherwise all the slices come back
184         'reservation_policy': Parameter(str,"one among none, lease_or_idle, lease_or_shared"),
185         'leases': [  { 'slice_id' : Lease.fields['slice_id'],
186                        't_from' : Lease.fields['t_from'],
187                        't_until' : Lease.fields['t_until'],
188                        }],
189     }
190
191     def call(self, auth, node_id_or_hostname = None):
192         timestamp = int(time.time())
193
194         # Get node
195         if node_id_or_hostname is None:
196             if isinstance(self.caller, Node):
197                 node = self.caller
198             else:
199                 raise PLCInvalidArgument, "'node_id_or_hostname' not specified"
200         else:
201             nodes = Nodes(self.api, [node_id_or_hostname])
202             if not nodes:
203                 raise PLCInvalidArgument, "No such node"
204             node = nodes[0]
205
206             if node['peer_id'] is not None:
207                 raise PLCInvalidArgument, "Not a local node"
208
209         # Get interface information
210         interfaces = Interfaces(self.api, node['interface_ids'])
211
212         # Get node group information
213         nodegroups = NodeGroups(self.api, node['nodegroup_ids']).dict('groupname')
214         groups = nodegroups.keys()
215
216         # Get all (enabled) configuration files
217         all_conf_files = ConfFiles(self.api, {'enabled': True}).dict()
218         conf_files = {}
219
220         # Global configuration files are the default. If multiple
221         # entries for the same global configuration file exist, it is
222         # undefined which one takes precedence.
223         for conf_file in all_conf_files.values():
224             if not conf_file['node_ids'] and not conf_file['nodegroup_ids']:
225                 conf_files[conf_file['dest']] = conf_file
226
227         # Node group configuration files take precedence over global
228         # ones. If a node belongs to multiple node groups for which
229         # the same configuration file is defined, it is undefined
230         # which one takes precedence.
231         for nodegroup in nodegroups.values():
232             for conf_file_id in nodegroup['conf_file_ids']:
233                 if conf_file_id in all_conf_files:
234                     conf_file = all_conf_files[conf_file_id]
235                     conf_files[conf_file['dest']] = conf_file
236
237         # Node configuration files take precedence over node group
238         # configuration files.
239         for conf_file_id in node['conf_file_ids']:
240             if conf_file_id in all_conf_files:
241                 conf_file = all_conf_files[conf_file_id]
242                 conf_files[conf_file['dest']] = conf_file
243
244         # Get all (enabled) initscripts
245         initscripts = InitScripts(self.api, {'enabled': True})
246
247         # Get system slices
248         system_slice_tags = SliceTags(self.api, {'tagname': 'system', 'value': '1'}).dict('slice_id')
249         system_slice_ids = system_slice_tags.keys()
250
251         # Get nm-controller slices
252         # xxx Thierry: should these really be exposed regardless of their mapping to nodes ?
253         controller_and_delegated_slices = Slices(self.api, {'instantiation': ['nm-controller', 'delegated']}, ['slice_id']).dict('slice_id')
254         controller_and_delegated_slice_ids = controller_and_delegated_slices.keys()
255         slice_ids = system_slice_ids + controller_and_delegated_slice_ids + node['slice_ids']
256
257         slivers = get_slivers(self.api, auth, slice_ids, node)
258
259         # get the special accounts and keys needed for the node
260         # root
261         # site_admin
262         accounts = []
263         if False and 'site_id' not in node:
264             nodes = Nodes(self.api, node['node_id'])
265             node = nodes[0]
266
267         # used in conjunction with reduce to flatten lists, like in
268         # reduce ( reduce_flatten_list, [ [1] , [2,3] ], []) => [ 1,2,3 ]
269         def reduce_flatten_list (x,y): return x+y
270
271         # power users are pis and techs
272         def get_site_power_user_keys(api,site_id_or_name):
273             site = Sites (api,site_id_or_name,['person_ids'])[0]
274             key_ids = reduce (reduce_flatten_list,
275                               [ p['key_ids'] for p in \
276                                     Persons(api,{ 'person_id':site['person_ids'],
277                                                   'enabled':True, '|role_ids' : [20, 40] },
278                                             ['key_ids']) ],
279                               [])
280             return [ key['key'] for key in Keys (api, key_ids) if key['key_type']=='ssh']
281
282         # all admins regardless of their site
283         def get_all_admin_keys(api):
284             key_ids = reduce (reduce_flatten_list,
285                               [ p['key_ids'] for p in \
286                                     Persons(api, {'peer_id':None, 'enabled':True, '|role_ids':[10] },
287                                             ['key_ids']) ],
288                               [])
289             return [ key['key'] for key in Keys (api, key_ids) if key['key_type']=='ssh']
290
291         # 'site_admin' account setup
292         personsitekeys=get_site_power_user_keys(self.api,node['site_id'])
293         accounts.append({'name':'site_admin','keys':personsitekeys})
294
295         # 'root' account setup on nodes from all 'admin' users
296         personsitekeys=get_all_admin_keys(self.api)
297         accounts.append({'name':'root','keys':personsitekeys})
298
299         hrn = GetNodeHrn(self.api).call(auth,node['node_id'])
300
301         # XMPP config for omf federation
302         try:
303             if not self.api.config.PLC_OMF_ENABLED:
304                 raise Exception,"OMF disabled"
305             xmpp={'server':self.api.config.PLC_OMF_XMPP_SERVER,
306                   'user':self.api.config.PLC_OMF_XMPP_USER,
307                   'password':self.api.config.PLC_OMF_XMPP_PASSWORD,
308                   }
309         except:
310             xmpp={'server':None,'user':None,'password':None}
311
312         node.update_last_contact()
313
314         # expose leases & reservation policy
315         # in a first implementation we only support none and lease_or_idle
316         lease_exposed_fields = [ 'slice_id', 't_from', 't_until', 'name', ]
317         leases=None
318         if node['node_type'] != 'reservable':
319             reservation_policy='none'
320         else:
321             reservation_policy='lease_or_idle'
322             # expose the leases for the next 24 hours
323             leases = [ dict ( [ (k,l[k]) for k in lease_exposed_fields ] )
324                        for l in Leases (self.api, {'node_id':node['node_id'],
325                                                    'clip': (timestamp, timestamp+24*Duration.HOUR),
326                                                    '-SORT': 't_from',
327                                                    }) ]
328         granularity=self.api.config.PLC_RESERVATION_GRANULARITY
329
330         return {
331             'timestamp': timestamp,
332             'node_id': node['node_id'],
333             'hostname': node['hostname'],
334             'interfaces': interfaces,
335             'groups': groups,
336             'conf_files': conf_files.values(),
337             'initscripts': initscripts,
338             'slivers': slivers,
339             'accounts': accounts,
340             'xmpp':xmpp,
341             'hrn':hrn,
342             'reservation_policy': reservation_policy,
343             'leases':leases,
344             'lease_granularity': granularity,
345             }