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 hostname = hostname.lower()
96 if not valid_hostname(hostname):
97 raise PLCInvalidArgument, "Invalid hostname"
99 conflicts = Nodes(self.api, [hostname])
100 for node in conflicts:
101 if 'node_id' not in self or self['node_id'] != node['node_id']:
102 raise PLCInvalidArgument, "Hostname already in use"
106 def validate_node_type(self, node_type):
107 node_types = [row['node_type'] for row in NodeTypes(self.api)]
108 if node_type not in node_types:
109 raise PLCInvalidArgument, "Invalid node type %r"%node_type
112 def validate_boot_state(self, boot_state):
113 boot_states = [row['boot_state'] for row in BootStates(self.api)]
114 if boot_state not in boot_states:
115 raise PLCInvalidArgument, "Invalid boot state %r"%boot_state
118 validate_date_created = Row.validate_timestamp
119 validate_last_updated = Row.validate_timestamp
120 validate_last_contact = Row.validate_timestamp
121 validate_last_boot = Row.validate_timestamp
122 validate_last_download = Row.validate_timestamp
123 validate_last_pcu_reboot = Row.validate_timestamp
124 validate_last_pcu_confirmation = Row.validate_timestamp
126 def update_timestamp(self, col_name, commit = True):
128 Update col_name field with current time
131 assert 'node_id' in self
132 assert self.table_name
134 self.api.db.do("UPDATE %s SET %s = CURRENT_TIMESTAMP " % (self.table_name, col_name) + \
135 " where node_id = %d" % (self['node_id']) )
138 def update_last_boot(self, commit = True):
139 self.update_timestamp('last_boot', commit)
140 def update_last_download(self, commit = True):
141 self.update_timestamp('last_download', commit)
142 def update_last_pcu_reboot(self, commit = True):
143 self.update_timestamp('last_pcu_reboot', commit)
144 def update_last_pcu_confirmation(self, commit = True):
145 self.update_timestamp('last_pcu_confirmation', commit)
147 def update_last_contact(self, commit = True):
148 self.update_timestamp('last_contact', commit)
149 def update_last_updated(self, commit = True):
150 self.update_timestamp('last_updated', commit)
152 def update_tags(self, tags):
153 from PLC.Shell import Shell
154 from PLC.NodeTags import NodeTags
155 from PLC.Methods.AddNodeTag import AddNodeTag
156 from PLC.Methods.UpdateNodeTag import UpdateNodeTag
158 for (tagname,value) in tags.iteritems():
159 # the tagtype instance is assumed to exist, just check that
160 if not TagTypes(self.api,{'tagname':tagname}):
161 raise PLCInvalidArgument,"No such TagType %s"%tagname
162 node_tags=NodeTags(self.api,{'tagname':tagname,'node_id':node['node_id']})
164 AddNodeTag(self.api).__call__(shell.auth,node['node_id'],tagname,value)
166 UpdateNodeTag(self.api).__call__(shell.auth,node_tags[0]['node_tag_id'],value)
168 def associate_interfaces(self, auth, field, value):
170 Delete interfaces not found in value list (using DeleteInterface)
171 Add interfaces found in value list (using AddInterface)
172 Updates interfaces found w/ interface_id in value list (using UpdateInterface)
175 assert 'interface_ids' in self
176 assert 'node_id' in self
177 assert isinstance(value, list)
179 (interface_ids, blank, interfaces) = self.separate_types(value)
181 if self['interface_ids'] != interface_ids:
182 from PLC.Methods.DeleteInterface import DeleteInterface
184 stale_interfaces = set(self['interface_ids']).difference(interface_ids)
186 for stale_interface in stale_interfaces:
187 DeleteInterface.__call__(DeleteInterface(self.api), auth, stale_interface['interface_id'])
189 def associate_conf_files(self, auth, field, value):
191 Add conf_files found in value list (AddConfFileToNode)
192 Delets conf_files not found in value list (DeleteConfFileFromNode)
195 assert 'conf_file_ids' in self
196 assert 'node_id' in self
197 assert isinstance(value, list)
199 conf_file_ids = self.separate_types(value)[0]
201 if self['conf_file_ids'] != conf_file_ids:
202 from PLC.Methods.AddConfFileToNode import AddConfFileToNode
203 from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
204 new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
205 stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
207 for new_conf_file in new_conf_files:
208 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
209 for stale_conf_file in stale_conf_files:
210 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
212 def associate_slices(self, auth, field, value):
214 Add slices found in value list to (AddSliceToNode)
215 Delete slices not found in value list (DeleteSliceFromNode)
218 from PLC.Slices import Slices
220 assert 'slice_ids' in self
221 assert 'node_id' in self
222 assert isinstance(value, list)
224 (slice_ids, slice_names) = self.separate_types(value)[0:2]
227 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
228 slice_ids += slices.keys()
230 if self['slice_ids'] != slice_ids:
231 from PLC.Methods.AddSliceToNodes import AddSliceToNodes
232 from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
233 new_slices = set(slice_ids).difference(self['slice_ids'])
234 stale_slices = set(self['slice_ids']).difference(slice_ids)
236 for new_slice in new_slices:
237 AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
238 for stale_slice in stale_slices:
239 DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])
241 def associate_slices_whitelist(self, auth, field, value):
243 Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
244 Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
247 from PLC.Slices import Slices
249 assert 'slice_ids_whitelist' in self
250 assert 'node_id' in self
251 assert isinstance(value, list)
253 (slice_ids, slice_names) = self.separate_types(value)[0:2]
256 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
257 slice_ids += slices.keys()
259 if self['slice_ids_whitelist'] != slice_ids:
260 from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
261 from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
262 new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
263 stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
265 for new_slice in new_slices:
266 AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
267 for stale_slice in stale_slices:
268 DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']])
271 def delete(self, commit = True):
273 Delete existing node.
276 assert 'node_id' in self
278 # we need to clean up InterfaceTags, so handling interfaces as part of join_tables does not work
279 # federated nodes don't have interfaces though so for smooth transition from 4.2 to 4.3
280 if 'peer_id' in self and self['peer_id']:
283 assert 'interface_ids' in self
284 for interface in Interfaces(self.api,self['interface_ids']):
287 # Clean up miscellaneous join tables
288 for table in self.join_tables:
289 self.api.db.do("DELETE FROM %s WHERE node_id = %d" % \
290 (table, self['node_id']))
293 self['deleted'] = True
299 Representation of row(s) from the nodes table in the
303 def __init__(self, api, node_filter = None, columns = None):
304 Table.__init__(self, api, Node, columns)
306 # the view that we're selecting upon: start with view_nodes
308 # as many left joins as requested tags
309 for tagname in self.tag_columns:
310 view= "%s left join %s using (%s)"%(view,Node.tagvalue_view_name(tagname),
313 sql = "SELECT %s FROM %s WHERE deleted IS False" % \
314 (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
316 if node_filter is not None:
317 if isinstance(node_filter, (list, tuple, set)):
318 # Separate the list into integers and strings
319 ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
320 strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
321 node_filter = Filter(Node.fields, {'node_id': ints, 'hostname': strs})
322 sql += " AND (%s) %s" % node_filter.sql(api, "OR")
323 elif isinstance(node_filter, dict):
324 allowed_fields=dict(Node.fields.items()+Node.tags.items())
325 node_filter = Filter(allowed_fields, node_filter)
326 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
327 elif isinstance (node_filter, StringTypes):
328 node_filter = Filter(Node.fields, {'hostname':node_filter})
329 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
330 elif isinstance (node_filter, (int, long)):
331 node_filter = Filter(Node.fields, {'node_id':node_filter})
332 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
334 raise PLCInvalidArgument, "Wrong node filter %r"%node_filter