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
11 from types import StringTypes
14 from PLC.Faults import *
15 from PLC.Parameter import Parameter, Mixed
16 from PLC.Filter import Filter
17 from PLC.Debug import profile
18 from PLC.Table import Row, Table
19 from PLC.NodeTypes import NodeTypes
20 from PLC.BootStates import BootStates
21 from PLC.Interfaces import Interface, Interfaces
23 def valid_hostname(hostname):
24 # 1. Each part begins and ends with a letter or number.
25 # 2. Each part except the last can contain letters, numbers, or hyphens.
26 # 3. Each part is between 1 and 64 characters, including the trailing dot.
27 # 4. At least two parts.
28 # 5. Last part can only contain between 2 and 6 letters.
29 good_hostname = r'^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+' \
32 re.match(good_hostname, hostname, re.IGNORECASE)
36 Representation of a row in the nodes table. To use, optionally
37 instantiate with a dict of values. Update as you would a
38 dict. Commit to the database with sync().
42 primary_key = 'node_id'
43 join_tables = [ 'slice_node', 'peer_node', 'slice_tag',
44 'node_session', 'node_slice_whitelist',
45 'node_tag', 'conf_file_node', 'pcu_node', 'leases', ]
47 'node_id': Parameter(int, "Node identifier"),
48 'node_type': Parameter(str,"Node type",max=20),
49 'hostname': Parameter(str, "Fully qualified hostname", max = 255),
50 'site_id': Parameter(int, "Site at which this node is located"),
51 'boot_state': Parameter(str, "Boot state", max = 20),
52 'run_level': Parameter(str, "Run level", max = 20),
53 'model': Parameter(str, "Make and model of the actual machine", max = 255, nullok = True),
54 'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot", max = 128),
55 'version': Parameter(str, "Apparent Boot CD version", max = 64),
56 'ssh_rsa_key': Parameter(str, "Last known SSH host key", max = 1024),
57 'date_created': Parameter(int, "Date and time when node entry was created", ro = True),
58 'last_updated': Parameter(int, "Date and time when node entry was created", ro = True),
59 'last_contact': Parameter(int, "Date and time when node last contacted plc", ro = True),
60 'last_boot': Parameter(int, "Date and time when node last booted", ro = True),
61 'last_download': Parameter(int, "Date and time when node boot image was created", ro = True),
62 'last_pcu_reboot': Parameter(int, "Date and time when PCU reboot was attempted", ro = True),
63 'last_pcu_confirmation': Parameter(int, "Date and time when PCU reboot was confirmed", ro = True),
64 'verified': Parameter(bool, "Whether the node configuration is verified correct", ro=False),
65 'key': Parameter(str, "(Admin only) Node key", max = 256),
66 'session': Parameter(str, "(Admin only) Node session value", max = 256, ro = True),
67 'interface_ids': Parameter([int], "List of network interfaces that this node has"),
68 'conf_file_ids': Parameter([int], "List of configuration files specific to this node"),
69 # 'root_person_ids': Parameter([int], "(Admin only) List of people who have root access to this node"),
70 'slice_ids': Parameter([int], "List of slices on this node"),
71 'slice_ids_whitelist': Parameter([int], "List of slices allowed on this node"),
72 'pcu_ids': Parameter([int], "List of PCUs that control this node"),
73 'ports': Parameter([int], "List of PCU ports that this node is connected to"),
74 'peer_id': Parameter(int, "Peer to which this node belongs", nullok = True),
75 'peer_node_id': Parameter(int, "Foreign node identifier at peer", nullok = True),
76 'node_tag_ids' : Parameter ([int], "List of tags attached to this node"),
77 'nodegroup_ids': Parameter([int], "List of node groups that this node is in"),
80 'interfaces': [Mixed(Parameter(int, "Interface identifier"),
81 Filter(Interface.fields))],
82 'conf_files': [Parameter(int, "ConfFile identifier")],
83 'slices': [Mixed(Parameter(int, "Slice identifier"),
84 Parameter(str, "Slice name"))],
85 'slices_whitelist': [Mixed(Parameter(int, "Slice identifier"),
86 Parameter(str, "Slice name"))]
89 view_tags_name = "view_node_tags"
90 # tags are used by the Add/Get/Update methods to expose tags
91 # this is initialized here and updated by the accessors factory
94 def validate_hostname(self, hostname):
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_timestamp(self, col_name, commit = True):
127 Update col_name field with current time
130 assert 'node_id' in self
131 assert self.table_name
133 self.api.db.do("UPDATE %s SET %s = CURRENT_TIMESTAMP " % (self.table_name, col_name) + \
134 " where node_id = %d" % (self['node_id']) )
137 def update_last_boot(self, commit = True):
138 self.update_timestamp('last_boot', commit)
139 def update_last_download(self, commit = True):
140 self.update_timestamp('last_download', commit)
141 def update_last_pcu_reboot(self, commit = True):
142 self.update_timestamp('last_pcu_reboot', commit)
143 def update_last_pcu_confirmation(self, commit = True):
144 self.update_timestamp('last_pcu_confirmation', commit)
146 def update_last_contact(self, commit = True):
147 self.update_timestamp('last_contact', commit)
148 def update_last_updated(self, commit = True):
149 self.update_timestamp('last_updated', commit)
151 def update_tags(self, tags):
152 from PLC.Shell import Shell
153 from PLC.NodeTags import NodeTags
154 from PLC.Methods.AddNodeTag import AddNodeTag
155 from PLC.Methods.UpdateNodeTag import UpdateNodeTag
157 for (tagname,value) in tags.iteritems():
158 # the tagtype instance is assumed to exist, just check that
159 if not TagTypes(self.api,{'tagname':tagname}):
160 raise PLCInvalidArgument,"No such TagType %s"%tagname
161 node_tags=NodeTags(self.api,{'tagname':tagname,'node_id':node['node_id']})
163 AddNodeTag(self.api).__call__(shell.auth,node['node_id'],tagname,value)
165 UpdateNodeTag(self.api).__call__(shell.auth,node_tags[0]['node_tag_id'],value)
167 def associate_interfaces(self, auth, field, value):
169 Delete interfaces not found in value list (using DeleteInterface)
170 Add interfaces found in value list (using AddInterface)
171 Updates interfaces found w/ interface_id in value list (using UpdateInterface)
174 assert 'interface_ids' in self
175 assert 'node_id' in self
176 assert isinstance(value, list)
178 (interface_ids, blank, interfaces) = self.separate_types(value)
180 if self['interface_ids'] != interface_ids:
181 from PLC.Methods.DeleteInterface import DeleteInterface
183 stale_interfaces = set(self['interface_ids']).difference(interface_ids)
185 for stale_interface in stale_interfaces:
186 DeleteInterface.__call__(DeleteInterface(self.api), auth, stale_interface['interface_id'])
188 def associate_conf_files(self, auth, field, value):
190 Add conf_files found in value list (AddConfFileToNode)
191 Delets conf_files not found in value list (DeleteConfFileFromNode)
194 assert 'conf_file_ids' in self
195 assert 'node_id' in self
196 assert isinstance(value, list)
198 conf_file_ids = self.separate_types(value)[0]
200 if self['conf_file_ids'] != conf_file_ids:
201 from PLC.Methods.AddConfFileToNode import AddConfFileToNode
202 from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
203 new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
204 stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
206 for new_conf_file in new_conf_files:
207 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
208 for stale_conf_file in stale_conf_files:
209 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
211 def associate_slices(self, auth, field, value):
213 Add slices found in value list to (AddSliceToNode)
214 Delete slices not found in value list (DeleteSliceFromNode)
217 from PLC.Slices import Slices
219 assert 'slice_ids' in self
220 assert 'node_id' in self
221 assert isinstance(value, list)
223 (slice_ids, slice_names) = self.separate_types(value)[0:2]
226 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
227 slice_ids += slices.keys()
229 if self['slice_ids'] != slice_ids:
230 from PLC.Methods.AddSliceToNodes import AddSliceToNodes
231 from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
232 new_slices = set(slice_ids).difference(self['slice_ids'])
233 stale_slices = set(self['slice_ids']).difference(slice_ids)
235 for new_slice in new_slices:
236 AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
237 for stale_slice in stale_slices:
238 DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])
240 def associate_slices_whitelist(self, auth, field, value):
242 Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
243 Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
246 from PLC.Slices import Slices
248 assert 'slice_ids_whitelist' in self
249 assert 'node_id' in self
250 assert isinstance(value, list)
252 (slice_ids, slice_names) = self.separate_types(value)[0:2]
255 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
256 slice_ids += slices.keys()
258 if self['slice_ids_whitelist'] != slice_ids:
259 from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
260 from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
261 new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
262 stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
264 for new_slice in new_slices:
265 AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
266 for stale_slice in stale_slices:
267 DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']])
270 def delete(self, commit = True):
272 Delete existing node.
275 assert 'node_id' in self
277 # we need to clean up InterfaceTags, so handling interfaces as part of join_tables does not work
278 # federated nodes don't have interfaces though so for smooth transition from 4.2 to 4.3
279 if 'peer_id' in self and self['peer_id']:
282 assert 'interface_ids' in self
283 for interface in Interfaces(self.api,self['interface_ids']):
286 # Clean up miscellaneous join tables
287 for table in self.join_tables:
288 self.api.db.do("DELETE FROM %s WHERE node_id = %d" % \
289 (table, self['node_id']))
292 self['deleted'] = True
298 Representation of row(s) from the nodes table in the
302 def __init__(self, api, node_filter = None, columns = None):
303 Table.__init__(self, api, Node, columns)
305 # the view that we're selecting upon: start with view_nodes
307 # as many left joins as requested tags
308 for tagname in self.tag_columns:
309 view= "%s left join %s using (%s)"%(view,Node.tagvalue_view_name(tagname),
312 sql = "SELECT %s FROM %s WHERE deleted IS False" % \
313 (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
315 if node_filter is not None:
316 if isinstance(node_filter, (list, tuple, set)):
317 # Separate the list into integers and strings
318 ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
319 strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
320 node_filter = Filter(Node.fields, {'node_id': ints, 'hostname': strs})
321 sql += " AND (%s) %s" % node_filter.sql(api, "OR")
322 elif isinstance(node_filter, dict):
323 allowed_fields=dict(Node.fields.items()+Node.tags.items())
324 node_filter = Filter(allowed_fields, node_filter)
325 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
326 elif isinstance (node_filter, StringTypes):
327 node_filter = Filter(Node.fields, {'hostname':[node_filter]})
328 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
329 elif isinstance (node_filter, int):
330 node_filter = Filter(Node.fields, {'node_id':[node_filter]})
331 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
333 raise PLCInvalidArgument, "Wrong node filter %r"%node_filter