2 # Functions for interacting with the nodes table in the database
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
7 from datetime import datetime
8 from types import StringTypes
11 from PLC.Faults import *
12 from PLC.Parameter import Parameter, Mixed
13 from PLC.Filter import Filter
14 from PLC.Debug import profile
15 from PLC.Storage.AlchemyObject import AlchemyObj
16 from PLC.NodeTypes import NodeTypes
17 from PLC.BootStates import BootStates
18 from PLC.Interfaces import Interface, Interfaces
19 from PLC.ConfFileNodes import ConfFileNode
20 from PLC.SliceNodes import SliceNode
21 from PLC.SliceNodeWhitelists import SliceNodeWhitelist
22 from PLC.PCUNodes import PCUNode
23 from PLC.PCUNodePorts import PCUNodePort
24 from PLC.NodeTags import NodeTag
25 from PLC.NodeNodeGroups import NodeNodeGroup
27 def valid_hostname(hostname):
28 # 1. Each part begins and ends with a letter or number.
29 # 2. Each part except the last can contain letters, numbers, or hyphens.
30 # 3. Each part is between 1 and 64 characters, including the trailing dot.
31 # 4. At least two parts.
32 # 5. Last part can only contain between 2 and 6 letters.
33 good_hostname = r'^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+' \
36 re.match(good_hostname, hostname, re.IGNORECASE)
38 class Node(AlchemyObj):
40 Representation of a row in the nodes table. To use, optionally
41 instantiate with a dict of values. Update as you would a
42 dict. Commit to the database with sync().
46 join_tables = [ 'slice_node', 'peer_node', 'slice_tag',
47 'node_session', 'node_slice_whitelist',
48 'node_tag', 'conf_file_node', 'pcu_node', 'leases', ]
50 'node_id': Parameter(int, "Node identifier", primary_key=True),
51 'node_type': Parameter(str,"Node type",max=20),
52 'hostname': Parameter(str, "Fully qualified hostname", max = 255),
53 'site_id': Parameter(int, "Site at which this node is located"),
54 'boot_state': Parameter(str, "Boot state", max = 20, nullok=True),
55 'run_level': Parameter(str, "Run level", max = 20, nullok=True),
56 'model': Parameter(str, "Make and model of the actual machine", max = 255, nullok = True),
57 'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot", max = 128, nullok=True),
58 'version': Parameter(str, "Apparent Boot CD version", max = 64, nullok=True),
59 'ssh_rsa_key': Parameter(str, "Last known SSH host key", max = 1024, nullok=True),
60 'date_created': Parameter(datetime, "Date and time when node entry was created", ro = True),
61 'last_updated': Parameter(datetime, "Date and time when node entry was created", ro = True),
62 'last_contact': Parameter(datetime, "Date and time when node last contacted plc", ro = True, nullok=True),
63 'last_boot': Parameter(datetime, "Date and time when node last booted", ro = True, nullok=True),
64 'last_download': Parameter(datetime, "Date and time when node boot image was created", ro = True, nullok=True),
65 'last_pcu_reboot': Parameter(datetime, "Date and time when PCU reboot was attempted", ro = True, nullok=True),
66 'last_pcu_confirmation': Parameter(datetime, "Date and time when PCU reboot was confirmed", ro = True, nullok=True),
67 'last_time_spent_online': Parameter(datetime, "Length of time the node was last online before shutdown/failure", ro = True, nullok=True),
68 'last_time_spent_offline': Parameter(datetime, "Length of time the node was last offline after failure and before reboot", ro = True, nullok=True),
69 'verified': Parameter(bool, "Whether the node configuration is verified correct", ro=False, nullok=True),
70 'key': Parameter(str, "(Admin only) Node key", max = 256, nullok=True),
71 'session': Parameter(str, "(Admin only) Node session value", max = 256, ro = True, nullok=True),
72 'interface_ids': Parameter([int], "List of network interfaces that this node has", joined=True),
73 'conf_file_ids': Parameter([int], "List of configuration files specific to this node", joined=True),
74 # 'root_person_ids': Parameter([int], "(Admin only) List of people who have root access to this node"),
75 'slice_ids': Parameter([int], "List of slices on this node", joined=True),
76 'slice_ids_whitelist': Parameter([int], "List of slices allowed on this node", joined=True),
77 'pcu_ids': Parameter([int], "List of PCUs that control this node", joined=True),
78 'ports': Parameter([int], "List of PCU ports that this node is connected to", joined=True),
79 'peer_id': Parameter(int, "Peer to which this node belongs", nullok = True),
80 'peer_node_id': Parameter(int, "Foreign node identifier at peer", nullok = True),
81 'node_tag_ids' : Parameter ([int], "List of tags attached to this node", joined=True),
82 'nodegroup_ids': Parameter([int], "List of node groups that this node is in", joined=True),
86 def validate_hostname(self, hostname):
87 hostname = hostname.lower()
88 if not valid_hostname(hostname):
89 raise PLCInvalidArgument, "Invalid hostname"
91 conflicts = Node().select(filter={'hostname': hostname})
92 for node in conflicts:
93 if 'node_id' not in self or self['node_id'] != node['node_id']:
94 raise PLCInvalidArgument, "Hostname already in use"
98 def validate_node_type(self, node_type):
99 # Make sure node type does not alredy exist
100 conflicts = NodeTypes(self.api, [node_type])
102 raise PLCInvalidArgument, "Invalid node_type"
105 def validate_boot_state(self, boot_state):
106 boot_states = [row['boot_state'] for row in BootStates(self.api)]
107 if boot_state not in boot_states:
108 raise PLCInvalidArgument, "Invalid boot state %r"%boot_state
111 validate_date_created = AlchemyObj.validate_timestamp
112 validate_last_updated = AlchemyObj.validate_timestamp
113 validate_last_contact = AlchemyObj.validate_timestamp
114 validate_last_boot = AlchemyObj.validate_timestamp
115 validate_last_download = AlchemyObj.validate_timestamp
116 validate_last_pcu_reboot = AlchemyObj.validate_timestamp
117 validate_last_pcu_confirmation = AlchemyObj.validate_timestamp
119 def check_whitelist(self, slice, caller):
120 if self['slice_ids_whitelist'] and \
121 slice['slice_id'] not in self['slice_ids_whitelist'] and \
122 not set(caller['site_ids']).intersection([self['site_id']]):
123 raise PLCInvalidArgument, "%s is not allowed on %s (not on the whitelist)" % \
124 (slice['name'], self['hostname'])
126 def update_readonly_int(self, col_name, commit = True):
128 assert 'node_id' in self
129 assert self.table_name
131 self.api.db.do("UPDATE %s SET %s = %s" % (self.table_name, col_name, self[col_name]) + \
132 " where node_id = %d" % (self['node_id']) )
135 def update_timestamp(self, col_name, commit = True):
137 Update col_name field with current time
139 assert 'node_id' in self
140 self[col_name] = datetime.now()
142 'node_id': self['node_id'],
143 col_name: datetime.now()
145 Node(self.api, fields).sync()
147 def update_last_boot(self, commit = True):
148 self.update_timestamp('last_boot', commit)
149 def update_last_download(self, commit = True):
150 self.update_timestamp('last_download', commit)
151 def update_last_pcu_reboot(self, commit = True):
152 self.update_timestamp('last_pcu_reboot', commit)
153 def update_last_pcu_confirmation(self, commit = True):
154 self.update_timestamp('last_pcu_confirmation', commit)
156 def update_last_contact(self, commit = True):
157 self.update_timestamp('last_contact', commit)
158 def update_last_updated(self, commit = True):
159 self.update_timestamp('last_updated', commit)
161 def update_tags(self, tags):
162 from PLC.Shell import Shell
163 from PLC.NodeTags import NodeTags
164 from PLC.Methods.AddNodeTag import AddNodeTag
165 from PLC.Methods.UpdateNodeTag import UpdateNodeTag
167 for (tagname,value) in tags.iteritems():
168 # the tagtype instance is assumed to exist, just check that
169 if not TagTypes(self.api,{'tagname':tagname}):
170 raise PLCInvalidArgument,"No such TagType %s"%tagname
171 node_tags=NodeTags(self.api,{'tagname':tagname,'node_id':node['node_id']})
173 AddNodeTag(self.api).__call__(shell.auth,node['node_id'],tagname,value)
175 UpdateNodeTag(self.api).__call__(shell.auth,node_tags[0]['node_tag_id'],value)
177 def associate_interfaces(self, auth, field, value):
179 Delete interfaces not found in value list (using DeleteInterface)
180 Add interfaces found in value list (using AddInterface)
181 Updates interfaces found w/ interface_id in value list (using UpdateInterface)
184 assert 'interface_ids' in self
185 assert 'node_id' in self
186 assert isinstance(value, list)
188 (interface_ids, blank, interfaces) = self.separate_types(value)
190 if self['interface_ids'] != interface_ids:
191 from PLC.Methods.DeleteInterface import DeleteInterface
193 stale_interfaces = set(self['interface_ids']).difference(interface_ids)
195 for stale_interface in stale_interfaces:
196 DeleteInterface.__call__(DeleteInterface(self.api), auth, stale_interface['interface_id'])
198 def associate_conf_files(self, auth, field, value):
200 Add conf_files found in value list (AddConfFileToNode)
201 Delets conf_files not found in value list (DeleteConfFileFromNode)
204 assert 'conf_file_ids' in self
205 assert 'node_id' in self
206 assert isinstance(value, list)
208 conf_file_ids = self.separate_types(value)[0]
210 if self['conf_file_ids'] != conf_file_ids:
211 from PLC.Methods.AddConfFileToNode import AddConfFileToNode
212 from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
213 new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
214 stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
216 for new_conf_file in new_conf_files:
217 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
218 for stale_conf_file in stale_conf_files:
219 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
221 def associate_slices(self, auth, field, value):
223 Add slices found in value list to (AddSliceToNode)
224 Delete slices not found in value list (DeleteSliceFromNode)
227 from PLC.Slices import Slices
229 assert 'slice_ids' in self
230 assert 'node_id' in self
231 assert isinstance(value, list)
233 (slice_ids, slice_names) = self.separate_types(value)[0:2]
236 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
237 slice_ids += slices.keys()
239 if self['slice_ids'] != slice_ids:
240 from PLC.Methods.AddSliceToNodes import AddSliceToNodes
241 from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
242 new_slices = set(slice_ids).difference(self['slice_ids'])
243 stale_slices = set(self['slice_ids']).difference(slice_ids)
245 for new_slice in new_slices:
246 AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
247 for stale_slice in stale_slices:
248 DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])
250 def associate_slices_whitelist(self, auth, field, value):
252 Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
253 Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
256 from PLC.Slices import Slices
258 assert 'slice_ids_whitelist' in self
259 assert 'node_id' in self
260 assert isinstance(value, list)
262 (slice_ids, slice_names) = self.separate_types(value)[0:2]
265 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
266 slice_ids += slices.keys()
268 if self['slice_ids_whitelist'] != slice_ids:
269 from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
270 from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
271 new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
272 stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
274 for new_slice in new_slices:
275 AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
276 for stale_slice in stale_slices:
277 DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']])
281 def sync(self, commit=True, validate=True):
282 AlchemyObj.sync(self, commit=commit, validate=validate)
284 self['last_updated'] = ts
285 if 'node_id' not in self:
286 self['date_created'] = ts
287 AlchemyObj.insert(self, dict(self))
289 AlchemyObj.update(self, {'node_id': self['node_id']}, dict(self))
291 def delete(self, commit = True):
293 Delete existing node.
296 assert 'node_id' in self
297 assert 'interface_ids' in self
298 Interface().delete(filter={'interface_id': self['interface_ids']})
299 AlchemyObj.delete(self, dict(self))
303 Representation of row(s) from the nodes table in the
307 def __init__(self, api, node_filter = None, columns = None):
310 # as many left joins as requested tags
312 nodes = Node().select()
313 elif isinstance(node_filter, (list, tuple, set)):
314 # Separate the list into integers and strings
315 ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
316 strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
318 if ints: node_filter['node_id'] = ints
319 if strs: node_filter['hostname'] = strs
320 nodes = Node().select(filter=node_filter)
321 elif isinstance(node_filter, dict):
322 nodes = Node().select(filter=node_filter)
323 elif isinstance (node_filter, StringTypes):
324 nodes = Node().select(filter={'hostname': strs})
325 elif isinstance (node_filter, (int, long)):
326 nodes = Node().select(filter={'node_id': ints})
328 raise PLCInvalidArgument, "Wrong node filter %r"%node_filter
331 node = Node(api, object=node, columns=columns)
332 if not columns or 'interface_ids' in columns:
333 interfaces = Interface().select(filter={'node_id': node['node_id']})
334 node['interface_ids'] = [rec.interface_id for rec in interfaces]
335 if not columns or 'conf_file_ids' in columns:
336 conf_files = ConfFileNode().select(filter={'node_id': node['node_id']})
337 node['conf_file_ids'] = [rec.conf_file_id for rec in conf_files]
338 if not columns or 'slice_ids' in columns:
339 slice_nodes = SliceNode().select(filter={'node_id': node['node_id']})
340 node['slice_ids'] = [rec.slice_id for rec in slice_nodes]
341 if not columns or 'slice_ids_whitelist' in columns:
342 slice_whitelist = SliceNodeWhitelist().select(filter={'node_id': node['node_id']})
343 node['slice_ids_whitelist'] = [rec.slice_id for rec in slice_whitelist]
344 if not columns or 'pcu_ids' in columns:
345 pcus = PCUNode().select(filter={'node_id': node['node_id']})
346 node['pcu_ids'] = [rec.pcu_id for rec in pcus]
347 if not columns or 'pcu_ports' in columns:
348 pcu_ports = PCUNodePort().select(filter={'node_id': node['node_id']})
349 node['pcu_ports'] = [rec.port for rec in pcu_ports]
350 if not columns or 'node_tag_ids' in columns:
351 node_tags = NodeTag().select(filter={'node_id': node['node_id']})
352 node['node_tag_ids'] = [rec.node_tag_id for rec in node_tags]
353 if not columns or 'nodegroup_ids' in columns:
354 nodegroups = NodeNodeGroup().select(filter={'node_id': node['node_id']})
355 node['nodegroup_ids'] = [rec.nodegroup_id for rec in nodegroups]
358 def refresh(self, api):
360 Import node records from nova.
362 from PLC.Sites import Sites
363 default_site = Sites(api, site_filter={'login_base': 'default'})[0]
364 # get current list of compute nodes
365 hypervisors = api.client_shell.nova.hypervisors.list()
366 compute_hosts = [h.hypervisor_hostname for h in hypervisors]
368 nodes = Node().select()
369 hostnames = [node.hostname for node in nodes]
371 added_nodes = set(compute_hosts).difference(hostnames)
372 for added_node in added_nodes:
373 node = Node(api, {'hostname': added_node,
374 'node_type': 'regular',
375 'site_id': default_site['site_id']})