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
19 from PLC.IpAddresses import SimpleAddress
21 def valid_hostname(hostname):
22 # 1. Each part begins and ends with a letter or number.
23 # 2. Each part except the last can contain letters, numbers, or hyphens.
24 # 3. Each part is between 1 and 64 characters, including the trailing dot.
25 # 4. At least two parts.
26 # 5. Last part can only contain between 2 and 6 letters.
27 good_hostname = r'^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+' \
30 re.match(good_hostname, hostname, re.IGNORECASE)
34 Representation of a row in the nodes table. To use, optionally
35 instantiate with a dict of values. Update as you would a
36 dict. Commit to the database with sync().
40 primary_key = 'node_id'
41 join_tables = [ 'slice_node', 'peer_node', 'slice_tag',
42 'node_session', 'node_slice_whitelist',
43 'node_tag', 'conf_file_node', 'pcu_node', 'leases', 'routes']
45 'node_id': Parameter(int, "Node identifier"),
46 'node_type': Parameter(str,"Node type",max=20),
47 'hostname': Parameter(str, "Fully qualified hostname", max = 255),
48 'site_id': Parameter(int, "Site at which this node is located"),
49 'boot_state': Parameter(str, "Boot state", max = 20),
50 'run_level': Parameter(str, "Run level", max = 20),
51 'model': Parameter(str, "Make and model of the actual machine", max = 255, nullok = True),
52 'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot", max = 128),
53 'version': Parameter(str, "Apparent Boot CD version", max = 64),
54 'ssh_rsa_key': Parameter(str, "Last known SSH host key", max = 1024),
55 'date_created': Parameter(int, "Date and time when node entry was created", ro = True),
56 'last_updated': Parameter(int, "Date and time when node entry was created", ro = True),
57 'last_contact': Parameter(int, "Date and time when node last contacted plc", ro = True),
58 'last_boot': Parameter(int, "Date and time when node last booted", ro = True),
59 'last_download': Parameter(int, "Date and time when node boot image was created", ro = True),
60 'last_pcu_reboot': Parameter(int, "Date and time when PCU reboot was attempted", ro = True),
61 'last_pcu_confirmation': Parameter(int, "Date and time when PCU reboot was confirmed", ro = True),
62 'last_time_spent_online': Parameter(int, "Length of time the node was last online before shutdown/failure", ro = True),
63 'last_time_spent_offline': Parameter(int, "Length of time the node was last offline after failure and before reboot", ro = True),
64 'verified': Parameter(bool, "Whether the node configuration is verified correct", ro=False),
65 'dns': Parameter(str, "Comma-separated list of dns servers", max=255, nullok=True),
66 'ntp': Parameter(str, "Comma-separated list of ntp servers", max=255, nullok=True),
67 'key': Parameter(str, "(Admin only) Node key", max = 256),
68 'session': Parameter(str, "(Admin only) Node session value", max = 256, ro = True),
69 'interface_ids': Parameter([int], "List of network interfaces that this node has"),
70 'conf_file_ids': Parameter([int], "List of configuration files specific to this node"),
71 # 'root_person_ids': Parameter([int], "(Admin only) List of people who have root access to this node"),
72 'slice_ids': Parameter([int], "List of slices on this node"),
73 'slice_ids_whitelist': Parameter([int], "List of slices allowed on this node"),
74 'pcu_ids': Parameter([int], "List of PCUs that control this node"),
75 'ports': Parameter([int], "List of PCU ports that this node is connected to"),
76 'peer_id': Parameter(int, "Peer to which this node belongs", nullok = True),
77 'peer_node_id': Parameter(int, "Foreign node identifier at peer", nullok = True),
78 'node_tag_ids' : Parameter ([int], "List of tags attached to this node"),
79 'nodegroup_ids': Parameter([int], "List of node groups that this node is in"),
80 'route_ids': Parameter([int], "List of routes attached to this node"),
83 'interfaces': [Mixed(Parameter(int, "Interface identifier"),
84 Filter(Interface.fields))],
85 'conf_files': [Parameter(int, "ConfFile identifier")],
86 'slices': [Mixed(Parameter(int, "Slice identifier"),
87 Parameter(str, "Slice name"))],
88 'slices_whitelist': [Mixed(Parameter(int, "Slice identifier"),
89 Parameter(str, "Slice name"))]
92 view_tags_name = "view_node_tags"
93 # tags are used by the Add/Get/Update methods to expose tags
94 # this is initialized here and updated by the accessors factory
97 def validate_hostname(self, hostname):
98 hostname = hostname.lower()
99 if not valid_hostname(hostname):
100 raise PLCInvalidArgument, "Invalid hostname"
102 conflicts = Nodes(self.api, [hostname])
103 for node in conflicts:
104 if 'node_id' not in self or self['node_id'] != node['node_id']:
105 raise PLCInvalidArgument, "Hostname already in use"
109 def validate_node_type(self, node_type):
110 node_types = [row['node_type'] for row in NodeTypes(self.api)]
111 if node_type not in node_types:
112 raise PLCInvalidArgument, "Invalid node type %r"%node_type
115 def validate_boot_state(self, boot_state):
116 boot_states = [row['boot_state'] for row in BootStates(self.api)]
117 if boot_state not in boot_states:
118 raise PLCInvalidArgument, "Invalid boot state %r"%boot_state
121 validate_date_created = Row.validate_timestamp
122 validate_last_updated = Row.validate_timestamp
123 validate_last_contact = Row.validate_timestamp
124 validate_last_boot = Row.validate_timestamp
125 validate_last_download = Row.validate_timestamp
126 validate_last_pcu_reboot = Row.validate_timestamp
127 validate_last_pcu_confirmation = Row.validate_timestamp
129 def validate_dns(self, dns):
130 addrs = dns.split(",")
131 addrs = [x.strip() for x in addrs]
137 validate_ntp = validate_dns
139 def update_readonly_int(self, col_name, commit = True):
141 assert 'node_id' in self
142 assert self.table_name
144 self.api.db.do("UPDATE %s SET %s = %s" % (self.table_name, col_name, self[col_name]) + \
145 " where node_id = %d" % (self['node_id']) )
148 def update_timestamp(self, col_name, commit = True):
150 Update col_name field with current time
153 assert 'node_id' in self
154 assert self.table_name
156 self.api.db.do("UPDATE %s SET %s = CURRENT_TIMESTAMP " % (self.table_name, col_name) + \
157 " where node_id = %d" % (self['node_id']) )
160 def update_last_boot(self, commit = True):
161 self.update_timestamp('last_boot', commit)
162 def update_last_download(self, commit = True):
163 self.update_timestamp('last_download', commit)
164 def update_last_pcu_reboot(self, commit = True):
165 self.update_timestamp('last_pcu_reboot', commit)
166 def update_last_pcu_confirmation(self, commit = True):
167 self.update_timestamp('last_pcu_confirmation', commit)
169 def update_last_contact(self, commit = True):
170 self.update_timestamp('last_contact', commit)
171 def update_last_updated(self, commit = True):
172 self.update_timestamp('last_updated', commit)
174 def update_tags(self, tags):
175 from PLC.Shell import Shell
176 from PLC.NodeTags import NodeTags
177 from PLC.Methods.AddNodeTag import AddNodeTag
178 from PLC.Methods.UpdateNodeTag import UpdateNodeTag
180 for (tagname,value) in tags.iteritems():
181 # the tagtype instance is assumed to exist, just check that
182 if not TagTypes(self.api,{'tagname':tagname}):
183 raise PLCInvalidArgument,"No such TagType %s"%tagname
184 node_tags=NodeTags(self.api,{'tagname':tagname,'node_id':node['node_id']})
186 AddNodeTag(self.api).__call__(shell.auth,node['node_id'],tagname,value)
188 UpdateNodeTag(self.api).__call__(shell.auth,node_tags[0]['node_tag_id'],value)
190 def associate_interfaces(self, auth, field, value):
192 Delete interfaces not found in value list (using DeleteInterface)
193 Add interfaces found in value list (using AddInterface)
194 Updates interfaces found w/ interface_id in value list (using UpdateInterface)
197 assert 'interface_ids' in self
198 assert 'node_id' in self
199 assert isinstance(value, list)
201 (interface_ids, blank, interfaces) = self.separate_types(value)
203 if self['interface_ids'] != interface_ids:
204 from PLC.Methods.DeleteInterface import DeleteInterface
206 stale_interfaces = set(self['interface_ids']).difference(interface_ids)
208 for stale_interface in stale_interfaces:
209 DeleteInterface.__call__(DeleteInterface(self.api), auth, stale_interface['interface_id'])
211 def associate_conf_files(self, auth, field, value):
213 Add conf_files found in value list (AddConfFileToNode)
214 Delets conf_files not found in value list (DeleteConfFileFromNode)
217 assert 'conf_file_ids' in self
218 assert 'node_id' in self
219 assert isinstance(value, list)
221 conf_file_ids = self.separate_types(value)[0]
223 if self['conf_file_ids'] != conf_file_ids:
224 from PLC.Methods.AddConfFileToNode import AddConfFileToNode
225 from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
226 new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
227 stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
229 for new_conf_file in new_conf_files:
230 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
231 for stale_conf_file in stale_conf_files:
232 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
234 def associate_slices(self, auth, field, value):
236 Add slices found in value list to (AddSliceToNode)
237 Delete slices not found in value list (DeleteSliceFromNode)
240 from PLC.Slices import Slices
242 assert 'slice_ids' in self
243 assert 'node_id' in self
244 assert isinstance(value, list)
246 (slice_ids, slice_names) = self.separate_types(value)[0:2]
249 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
250 slice_ids += slices.keys()
252 if self['slice_ids'] != slice_ids:
253 from PLC.Methods.AddSliceToNodes import AddSliceToNodes
254 from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
255 new_slices = set(slice_ids).difference(self['slice_ids'])
256 stale_slices = set(self['slice_ids']).difference(slice_ids)
258 for new_slice in new_slices:
259 AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
260 for stale_slice in stale_slices:
261 DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])
263 def associate_slices_whitelist(self, auth, field, value):
265 Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
266 Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
269 from PLC.Slices import Slices
271 assert 'slice_ids_whitelist' in self
272 assert 'node_id' in self
273 assert isinstance(value, list)
275 (slice_ids, slice_names) = self.separate_types(value)[0:2]
278 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
279 slice_ids += slices.keys()
281 if self['slice_ids_whitelist'] != slice_ids:
282 from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
283 from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
284 new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
285 stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
287 for new_slice in new_slices:
288 AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
289 for stale_slice in stale_slices:
290 DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']])
293 def delete(self, commit = True):
295 Delete existing node.
298 assert 'node_id' in self
300 # we need to clean up InterfaceTags, so handling interfaces as part of join_tables does not work
301 # federated nodes don't have interfaces though so for smooth transition from 4.2 to 4.3
302 if 'peer_id' in self and self['peer_id']:
305 assert 'interface_ids' in self
306 for interface in Interfaces(self.api,self['interface_ids']):
309 # Clean up miscellaneous join tables
310 for table in self.join_tables:
311 self.api.db.do("DELETE FROM %s WHERE node_id = %d" % \
312 (table, self['node_id']))
315 self['deleted'] = True
320 Representation of row(s) from the nodes table in the
324 def __init__(self, api, node_filter = None, columns = None):
325 Table.__init__(self, api, Node, columns)
327 # the view that we're selecting upon: start with view_nodes
329 # as many left joins as requested tags
330 for tagname in self.tag_columns:
331 view= "%s left join %s using (%s)"%(view,Node.tagvalue_view_name(tagname),
334 sql = "SELECT %s FROM %s WHERE deleted IS False" % \
335 (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
337 if node_filter is not None:
338 if isinstance(node_filter, (list, tuple, set)):
339 # Separate the list into integers and strings
340 ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
341 strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
342 node_filter = Filter(Node.fields, {'node_id': ints, 'hostname': strs})
343 sql += " AND (%s) %s" % node_filter.sql(api, "OR")
344 elif isinstance(node_filter, dict):
345 allowed_fields=dict(Node.fields.items()+Node.tags.items())
346 node_filter = Filter(allowed_fields, node_filter)
347 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
348 elif isinstance (node_filter, StringTypes):
349 node_filter = Filter(Node.fields, {'hostname':node_filter})
350 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
351 elif isinstance (node_filter, (int, long)):
352 node_filter = Filter(Node.fields, {'node_id':node_filter})
353 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
355 raise PLCInvalidArgument, "Wrong node filter %r"%node_filter