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
24 from PLC.Accessors.Accessors_standard import *
25 from functools import reduce
27 # XXX used to check if slice expiration time is sane
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'])
35 # Build up list of users and slice attributes
39 person_ids.update(slice['person_ids'])
40 slice_tag_ids.update(slice['slice_tag_ids'])
42 # Get user information
43 all_persons = Persons(api, {'person_id':person_ids,'enabled':True}, ['person_id', 'enabled', 'key_ids']).dict()
45 # Build up list of keys
47 for person in list(all_persons.values()):
48 key_ids.update(person['key_ids'])
50 # Get user account keys
51 all_keys = Keys(api, key_ids, ['key_id', 'key', 'key_type']).dict()
53 # Get slice attributes
54 all_slice_tags = SliceTags(api, slice_tag_ids).dict()
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']:
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'],
72 # All (per-node and global) attributes for this slice
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])
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 = []
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']})
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
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']})
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
104 if slice_tag['tagname'] not in sliver_attributes:
105 attributes.append({'tagname': slice_tag['tagname'],
106 'value': slice_tag['value']})
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
112 # expose the slice vref as computed by GetSliceFamily
113 family = GetSliceFamily (api,caller).call(auth, slice['slice_id'])
116 'name': slice['name'],
117 'slice_id': slice['slice_id'],
118 'instantiation': slice['instantiation'],
119 'expires': slice['expires'],
121 'attributes': attributes,
122 'GetSliceFamily': family,
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)
133 def sanitize_for_pickle (obj):
134 if (isinstance(obj, dict)):
136 for k in list(parent.keys()): parent[k] = sanitize_for_pickle (parent[k])
138 elif (isinstance(obj, list)):
140 parent = list(map(sanitize_for_pickle, parent))
145 class GetSlivers(Method):
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
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.
157 roles = ['admin', 'node']
161 Mixed(Node.fields['node_id'],
162 Node.fields['hostname']),
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],
174 'name': Parameter(str, "unix style account name", max = 254),
176 'key_type': Key.fields['key_type'],
177 'key': Key.fields['key']
181 'name': Slice.fields['name'],
182 'slice_id': Slice.fields['slice_id'],
183 'instantiation': Slice.fields['instantiation'],
184 'expires': Slice.fields['expires'],
186 'key_type': Key.fields['key_type'],
187 'key': Key.fields['key']
190 'tagname': SliceTag.fields['tagname'],
191 'value': SliceTag.fields['value']
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"),
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'],
210 def call(self, auth, node_id_or_hostname = None):
211 return self.raw_call(auth, node_id_or_hostname)
214 def raw_call(self, auth, node_id_or_hostname):
215 timestamp = int(time.time())
218 if node_id_or_hostname is None:
219 if isinstance(self.caller, Node):
222 raise PLCInvalidArgument("'node_id_or_hostname' not specified")
224 nodes = Nodes(self.api, [node_id_or_hostname])
226 raise PLCInvalidArgument("No such node")
229 if node['peer_id'] is not None:
230 raise PLCInvalidArgument("Not a local node")
232 # Get interface information
233 interfaces = Interfaces(self.api, node['interface_ids'])
235 # Get node group information
236 nodegroups = NodeGroups(self.api, node['nodegroup_ids']).dict('groupname')
237 groups = list(nodegroups.keys())
239 # Get all (enabled) configuration files
240 all_conf_files = ConfFiles(self.api, {'enabled': True}).dict()
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
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
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
267 # Get all (enabled) initscripts
268 initscripts = InitScripts(self.api, {'enabled': True})
271 system_slice_tags = SliceTags(self.api, {'tagname': 'system', 'value': '1'}).dict('slice_id')
272 system_slice_ids = list(system_slice_tags.keys())
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']
280 slivers = get_slivers(self.api, self.caller, auth, slice_ids, node)
282 # get the special accounts and keys needed for the node
286 if False and 'site_id' not in node:
287 nodes = Nodes(self.api, node['node_id'])
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
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] },
307 return [ key['key'] for key in Keys (api, key_ids) if key['key_type']=='ssh']
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] },
318 return [ key['key'] for key in Keys (api, key_ids) if key['key_type']=='ssh']
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] },
327 return [ key['key'] for key in Keys (api, key_ids) if key['key_type']=='ssh']
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})
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)
338 accounts.append({'name':'root','keys':personsitekeys})
340 hrn = GetNodeHrn(self.api,self.caller).call(auth,node['node_id'])
342 # XMPP config for omf federation
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}
350 node.update_last_contact()
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', ]
356 if node['node_type'] != 'reservable':
357 reservation_policy='none'
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),
366 granularity=self.api.config.PLC_RESERVATION_GRANULARITY
369 'timestamp': timestamp,
370 'node_id': node['node_id'],
371 'hostname': node['hostname'],
372 'interfaces': interfaces,
374 'conf_files': list(conf_files.values()),
375 'initscripts': initscripts,
377 'accounts': accounts,
380 'reservation_policy': reservation_policy,
382 'lease_granularity': granularity,
385 sanitized_data = sanitize_for_pickle (raw_data)
386 return sanitized_data