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
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.Table import Row, Table
16 from PLC.NodeTypes import NodeTypes
17 from PLC.BootStates import BootStates
18 from PLC.Interfaces import Interface, Interfaces
20 def valid_hostname(hostname):
21 # 1. Each part begins and ends with a letter or number.
22 # 2. Each part except the last can contain letters, numbers, or hyphens.
23 # 3. Each part is between 1 and 64 characters, including the trailing dot.
24 # 4. At least two parts.
25 # 5. Last part can only contain between 2 and 6 letters.
26 good_hostname = r'^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+' \
29 re.match(good_hostname, hostname, re.IGNORECASE)
33 Representation of a row in the nodes table. To use, optionally
34 instantiate with a dict of values. Update as you would a
35 dict. Commit to the database with sync().
39 primary_key = 'node_id'
40 join_tables = [ 'slice_node', 'peer_node', 'slice_tag',
41 'node_session', 'node_slice_whitelist',
42 'node_tag', 'conf_file_node', 'pcu_node', 'leases', ]
44 'node_id': Parameter(int, "Node identifier"),
45 'node_type': Parameter(str,"Node type",max=20),
46 'hostname': Parameter(str, "Fully qualified hostname", max = 255),
47 'site_id': Parameter(int, "Site at which this node is located"),
48 'boot_state': Parameter(str, "Boot state", max = 20),
49 'run_level': Parameter(str, "Run level", max = 20),
50 'model': Parameter(str, "Make and model of the actual machine", max = 255, nullok = True),
51 'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot", max = 128),
52 'version': Parameter(str, "Apparent Boot CD version", max = 64),
53 'ssh_rsa_key': Parameter(str, "Last known SSH host key", max = 1024),
54 'date_created': Parameter(int, "Date and time when node entry was created", ro = True),
55 'last_updated': Parameter(int, "Date and time when node entry was created", ro = True),
56 'last_contact': Parameter(int, "Date and time when node last contacted plc", ro = True),
57 'last_boot': Parameter(int, "Date and time when node last booted", ro = True),
58 'last_download': Parameter(int, "Date and time when node boot image was created", ro = True),
59 'last_pcu_reboot': Parameter(int, "Date and time when PCU reboot was attempted", ro = True),
60 'last_pcu_confirmation': Parameter(int, "Date and time when PCU reboot was confirmed", ro = True),
61 'last_time_spent_online': Parameter(int, "Length of time the node was last online before shutdown/failure", ro = True),
62 'last_time_spent_offline': Parameter(int, "Length of time the node was last offline after failure and before reboot", ro = True),
63 'verified': Parameter(bool, "Whether the node configuration is verified correct", ro=False),
64 'key': Parameter(str, "(Admin only) Node key", max = 256),
65 'session': Parameter(str, "(Admin only) Node session value", max = 256, ro = True),
66 'interface_ids': Parameter([int], "List of network interfaces that this node has"),
67 'conf_file_ids': Parameter([int], "List of configuration files specific to this node"),
68 # 'root_person_ids': Parameter([int], "(Admin only) List of people who have root access to this node"),
69 'slice_ids': Parameter([int], "List of slices on this node"),
70 'slice_ids_whitelist': Parameter([int], "List of slices allowed on this node"),
71 'pcu_ids': Parameter([int], "List of PCUs that control this node"),
72 'ports': Parameter([int], "List of PCU ports that this node is connected to"),
73 'peer_id': Parameter(int, "Peer to which this node belongs", nullok = True),
74 'peer_node_id': Parameter(int, "Foreign node identifier at peer", nullok = True),
75 'node_tag_ids' : Parameter ([int], "List of tags attached to this node"),
76 'nodegroup_ids': Parameter([int], "List of node groups that this node is in"),
79 'interfaces': [Mixed(Parameter(int, "Interface identifier"),
80 Filter(Interface.fields))],
81 'conf_files': [Parameter(int, "ConfFile identifier")],
82 'slices': [Mixed(Parameter(int, "Slice identifier"),
83 Parameter(str, "Slice name"))],
84 'slices_whitelist': [Mixed(Parameter(int, "Slice identifier"),
85 Parameter(str, "Slice name"))]
88 view_tags_name = "view_node_tags"
89 # tags are used by the Add/Get/Update methods to expose tags
90 # this is initialized here and updated by the accessors factory
93 def validate_hostname(self, hostname):
94 hostname = hostname.lower()
95 if not valid_hostname(hostname):
96 raise PLCInvalidArgument, "Invalid hostname"
98 conflicts = Nodes(self.api, [hostname])
99 for node in conflicts:
100 if 'node_id' not in self or self['node_id'] != node['node_id']:
101 raise PLCInvalidArgument, "Hostname already in use"
105 def validate_node_type(self, node_type):
106 node_types = [row['node_type'] for row in NodeTypes(self.api)]
107 if node_type not in node_types:
108 raise PLCInvalidArgument, "Invalid node type %r"%node_type
111 def validate_boot_state(self, boot_state):
112 boot_states = [row['boot_state'] for row in BootStates(self.api)]
113 if boot_state not in boot_states:
114 raise PLCInvalidArgument, "Invalid boot state %r"%boot_state
117 validate_date_created = Row.validate_timestamp
118 validate_last_updated = Row.validate_timestamp
119 validate_last_contact = Row.validate_timestamp
120 validate_last_boot = Row.validate_timestamp
121 validate_last_download = Row.validate_timestamp
122 validate_last_pcu_reboot = Row.validate_timestamp
123 validate_last_pcu_confirmation = Row.validate_timestamp
125 def update_readonly_int(self, col_name, commit = True):
127 assert 'node_id' in self
128 assert self.table_name
130 self.api.db.do("UPDATE %s SET %s = %s" % (self.table_name, col_name, self[col_name]) + \
131 " where node_id = %d" % (self['node_id']) )
134 def update_timestamp(self, col_name, commit = True):
136 Update col_name field with current time
139 assert 'node_id' in self
140 assert self.table_name
142 self.api.db.do("UPDATE %s SET %s = CURRENT_TIMESTAMP " % (self.table_name, col_name) + \
143 " where node_id = %d" % (self['node_id']) )
146 def update_last_boot(self, commit = True):
147 self.update_timestamp('last_boot', commit)
148 def update_last_download(self, commit = True):
149 self.update_timestamp('last_download', commit)
150 def update_last_pcu_reboot(self, commit = True):
151 self.update_timestamp('last_pcu_reboot', commit)
152 def update_last_pcu_confirmation(self, commit = True):
153 self.update_timestamp('last_pcu_confirmation', commit)
155 def update_last_contact(self, commit = True):
156 self.update_timestamp('last_contact', commit)
157 def update_last_updated(self, commit = True):
158 self.update_timestamp('last_updated', commit)
160 def update_tags(self, tags):
161 from PLC.Shell import Shell
162 from PLC.NodeTags import NodeTags
163 from PLC.Methods.AddNodeTag import AddNodeTag
164 from PLC.Methods.UpdateNodeTag import UpdateNodeTag
166 for (tagname,value) in tags.iteritems():
167 # the tagtype instance is assumed to exist, just check that
168 if not TagTypes(self.api,{'tagname':tagname}):
169 raise PLCInvalidArgument,"No such TagType %s"%tagname
170 node_tags=NodeTags(self.api,{'tagname':tagname,'node_id':node['node_id']})
172 AddNodeTag(self.api).__call__(shell.auth,node['node_id'],tagname,value)
174 UpdateNodeTag(self.api).__call__(shell.auth,node_tags[0]['node_tag_id'],value)
176 def associate_interfaces(self, auth, field, value):
178 Delete interfaces not found in value list (using DeleteInterface)
179 Add interfaces found in value list (using AddInterface)
180 Updates interfaces found w/ interface_id in value list (using UpdateInterface)
183 assert 'interface_ids' in self
184 assert 'node_id' in self
185 assert isinstance(value, list)
187 (interface_ids, blank, interfaces) = self.separate_types(value)
189 if self['interface_ids'] != interface_ids:
190 from PLC.Methods.DeleteInterface import DeleteInterface
192 stale_interfaces = set(self['interface_ids']).difference(interface_ids)
194 for stale_interface in stale_interfaces:
195 DeleteInterface.__call__(DeleteInterface(self.api), auth, stale_interface['interface_id'])
197 def associate_conf_files(self, auth, field, value):
199 Add conf_files found in value list (AddConfFileToNode)
200 Delets conf_files not found in value list (DeleteConfFileFromNode)
203 assert 'conf_file_ids' in self
204 assert 'node_id' in self
205 assert isinstance(value, list)
207 conf_file_ids = self.separate_types(value)[0]
209 if self['conf_file_ids'] != conf_file_ids:
210 from PLC.Methods.AddConfFileToNode import AddConfFileToNode
211 from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
212 new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
213 stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
215 for new_conf_file in new_conf_files:
216 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
217 for stale_conf_file in stale_conf_files:
218 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
220 def associate_slices(self, auth, field, value):
222 Add slices found in value list to (AddSliceToNode)
223 Delete slices not found in value list (DeleteSliceFromNode)
226 from PLC.Slices import Slices
228 assert 'slice_ids' in self
229 assert 'node_id' in self
230 assert isinstance(value, list)
232 (slice_ids, slice_names) = self.separate_types(value)[0:2]
235 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
236 slice_ids += slices.keys()
238 if self['slice_ids'] != slice_ids:
239 from PLC.Methods.AddSliceToNodes import AddSliceToNodes
240 from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
241 new_slices = set(slice_ids).difference(self['slice_ids'])
242 stale_slices = set(self['slice_ids']).difference(slice_ids)
244 for new_slice in new_slices:
245 AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
246 for stale_slice in stale_slices:
247 DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])
249 def associate_slices_whitelist(self, auth, field, value):
251 Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
252 Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
255 from PLC.Slices import Slices
257 assert 'slice_ids_whitelist' in self
258 assert 'node_id' in self
259 assert isinstance(value, list)
261 (slice_ids, slice_names) = self.separate_types(value)[0:2]
264 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
265 slice_ids += slices.keys()
267 if self['slice_ids_whitelist'] != slice_ids:
268 from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
269 from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
270 new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
271 stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
273 for new_slice in new_slices:
274 AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
275 for stale_slice in stale_slices:
276 DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']])
279 def delete(self, commit = True):
281 Delete existing node.
284 assert 'node_id' in self
286 # we need to clean up InterfaceTags, so handling interfaces as part of join_tables does not work
287 # federated nodes don't have interfaces though so for smooth transition from 4.2 to 4.3
288 if 'peer_id' in self and self['peer_id']:
291 assert 'interface_ids' in self
292 for interface in Interfaces(self.api,self['interface_ids']):
295 # Clean up miscellaneous join tables
296 for table in self.join_tables:
297 self.api.db.do("DELETE FROM %s WHERE node_id = %d" % \
298 (table, self['node_id']))
301 self['deleted'] = True
306 Representation of row(s) from the nodes table in the
310 def __init__(self, api, node_filter = None, columns = None):
311 Table.__init__(self, api, Node, columns)
313 # the view that we're selecting upon: start with view_nodes
315 # as many left joins as requested tags
316 for tagname in self.tag_columns:
317 view= "%s left join %s using (%s)"%(view,Node.tagvalue_view_name(tagname),
320 sql = "SELECT %s FROM %s WHERE deleted IS False" % \
321 (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
323 if node_filter is not None:
324 if isinstance(node_filter, (list, tuple, set)):
325 # Separate the list into integers and strings
326 ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
327 strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
328 node_filter = Filter(Node.fields, {'node_id': ints, 'hostname': strs})
329 sql += " AND (%s) %s" % node_filter.sql(api, "OR")
330 elif isinstance(node_filter, dict):
331 allowed_fields=dict(Node.fields.items()+Node.tags.items())
332 node_filter = Filter(allowed_fields, node_filter)
333 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
334 elif isinstance (node_filter, StringTypes):
335 node_filter = Filter(Node.fields, {'hostname':node_filter})
336 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
337 elif isinstance (node_filter, (int, long)):
338 node_filter = Filter(Node.fields, {'node_id':node_filter})
339 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
341 raise PLCInvalidArgument, "Wrong node filter %r"%node_filter