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
10 from types import StringTypes
13 from PLC.Faults import *
14 from PLC.Parameter import Parameter, Mixed
15 from PLC.Filter import Filter
16 from PLC.Debug import profile
17 from PLC.Table import Row, Table
18 from PLC.NodeTypes import NodeTypes
19 from PLC.BootStates import BootStates
20 from PLC.Interfaces import Interface, Interfaces
22 def valid_hostname(hostname):
23 # 1. Each part begins and ends with a letter or number.
24 # 2. Each part except the last can contain letters, numbers, or hyphens.
25 # 3. Each part is between 1 and 64 characters, including the trailing dot.
26 # 4. At least two parts.
27 # 5. Last part can only contain between 2 and 6 letters.
28 good_hostname = r'^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+' \
31 re.match(good_hostname, hostname, re.IGNORECASE)
35 Representation of a row in the nodes table. To use, optionally
36 instantiate with a dict of values. Update as you would a
37 dict. Commit to the database with sync().
41 primary_key = 'node_id'
42 join_tables = [ 'slice_node', 'peer_node', 'slice_tag',
43 'node_session', 'node_slice_whitelist',
44 'node_tag', 'conf_file_node', 'pcu_node', ]
46 'node_id': Parameter(int, "Node identifier"),
47 'node_type': Parameter(str,"Node type",max=20),
48 'hostname': Parameter(str, "Fully qualified hostname", max = 255),
49 'site_id': Parameter(int, "Site at which this node is located"),
50 'boot_state': Parameter(str, "Boot state", max = 20),
51 'run_level': Parameter(str, "Run level", max = 20),
52 'model': Parameter(str, "Make and model of the actual machine", max = 255, nullok = True),
53 'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot", max = 128),
54 'version': Parameter(str, "Apparent Boot CD version", max = 64),
55 'ssh_rsa_key': Parameter(str, "Last known SSH host key", max = 1024),
56 'date_created': Parameter(int, "Date and time when node entry was created", ro = True),
57 'last_updated': Parameter(int, "Date and time when node entry was created", ro = True),
58 'last_contact': Parameter(int, "Date and time when node last contacted plc", ro = True),
59 'verified': Parameter(bool, "Whether the node configuration is verified correct", ro=False),
60 'key': Parameter(str, "(Admin only) Node key", max = 256),
61 'session': Parameter(str, "(Admin only) Node session value", max = 256, ro = True),
62 'interface_ids': Parameter([int], "List of network interfaces that this node has"),
63 'conf_file_ids': Parameter([int], "List of configuration files specific to this node"),
64 # 'root_person_ids': Parameter([int], "(Admin only) List of people who have root access to this node"),
65 'slice_ids': Parameter([int], "List of slices on this node"),
66 'slice_ids_whitelist': Parameter([int], "List of slices allowed on this node"),
67 'pcu_ids': Parameter([int], "List of PCUs that control this node"),
68 'ports': Parameter([int], "List of PCU ports that this node is connected to"),
69 'peer_id': Parameter(int, "Peer to which this node belongs", nullok = True),
70 'peer_node_id': Parameter(int, "Foreign node identifier at peer", nullok = True),
71 'node_tag_ids' : Parameter ([int], "List of tags attached to this node"),
72 'nodegroup_ids': Parameter([int], "List of node groups that this node is in"),
75 'interfaces': [Mixed(Parameter(int, "Interface identifier"),
76 Filter(Interface.fields))],
77 'conf_files': [Parameter(int, "ConfFile identifier")],
78 'slices': [Mixed(Parameter(int, "Slice identifier"),
79 Parameter(str, "Slice name"))],
80 'slices_whitelist': [Mixed(Parameter(int, "Slice identifier"),
81 Parameter(str, "Slice name"))]
84 view_tags_name = "view_node_tags"
85 # tags are used by the Add/Get/Update methods to expose tags
86 # this is initialized here and updated by the accessors factory
89 def validate_hostname(self, hostname):
90 if not valid_hostname(hostname):
91 raise PLCInvalidArgument, "Invalid hostname"
93 conflicts = Nodes(self.api, [hostname])
94 for node in conflicts:
95 if 'node_id' not in self or self['node_id'] != node['node_id']:
96 raise PLCInvalidArgument, "Hostname already in use"
100 def validate_node_type(self, node_type):
101 node_types = [row['node_type'] for row in NodeTypes(self.api)]
102 if node_type not in node_types:
103 raise PLCInvalidArgument, "Invalid node type %r"%node_type
106 def validate_boot_state(self, boot_state):
107 boot_states = [row['boot_state'] for row in BootStates(self.api)]
108 if boot_state not in boot_states:
109 raise PLCInvalidArgument, "Invalid boot state %r"%boot_state
112 validate_date_created = Row.validate_timestamp
113 validate_last_updated = Row.validate_timestamp
114 validate_last_contact = Row.validate_timestamp
116 def update_last_contact(self, commit = True):
118 Update last_contact field with current time
121 assert 'node_id' in self
122 assert self.table_name
124 self.api.db.do("UPDATE %s SET last_contact = CURRENT_TIMESTAMP " % (self.table_name) + \
125 " where node_id = %d" % ( self['node_id']) )
129 def update_last_updated(self, commit = True):
131 Update last_updated field with current time
134 assert 'node_id' in self
135 assert self.table_name
137 self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
138 " where node_id = %d" % (self['node_id']) )
141 def associate_interfaces(self, auth, field, value):
143 Delete interfaces not found in value list (using DeleteInterface)
144 Add interfaces found in value list (using AddInterface)
145 Updates interfaces found w/ interface_id in value list (using UpdateInterface)
148 assert 'interface_ids' in self
149 assert 'node_id' in self
150 assert isinstance(value, list)
152 (interface_ids, blank, interfaces) = self.separate_types(value)
154 if self['interface_ids'] != interface_ids:
155 from PLC.Methods.DeleteInterface import DeleteInterface
157 stale_interfaces = set(self['interface_ids']).difference(interface_ids)
159 for stale_interface in stale_interfaces:
160 DeleteInterface.__call__(DeleteInterface(self.api), auth, stale_interface['interface_id'])
162 def associate_conf_files(self, auth, field, value):
164 Add conf_files found in value list (AddConfFileToNode)
165 Delets conf_files not found in value list (DeleteConfFileFromNode)
168 assert 'conf_file_ids' in self
169 assert 'node_id' in self
170 assert isinstance(value, list)
172 conf_file_ids = self.separate_types(value)[0]
174 if self['conf_file_ids'] != conf_file_ids:
175 from PLC.Methods.AddConfFileToNode import AddConfFileToNode
176 from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
177 new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
178 stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
180 for new_conf_file in new_conf_files:
181 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
182 for stale_conf_file in stale_conf_files:
183 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
185 def associate_slices(self, auth, field, value):
187 Add slices found in value list to (AddSliceToNode)
188 Delete slices not found in value list (DeleteSliceFromNode)
191 from PLC.Slices import Slices
193 assert 'slice_ids' in self
194 assert 'node_id' in self
195 assert isinstance(value, list)
197 (slice_ids, slice_names) = self.separate_types(value)[0:2]
200 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
201 slice_ids += slices.keys()
203 if self['slice_ids'] != slice_ids:
204 from PLC.Methods.AddSliceToNodes import AddSliceToNodes
205 from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
206 new_slices = set(slice_ids).difference(self['slice_ids'])
207 stale_slices = set(self['slice_ids']).difference(slice_ids)
209 for new_slice in new_slices:
210 AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
211 for stale_slice in stale_slices:
212 DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])
214 def associate_slices_whitelist(self, auth, field, value):
216 Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
217 Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
220 from PLC.Slices import Slices
222 assert 'slice_ids_whitelist' in self
223 assert 'node_id' in self
224 assert isinstance(value, list)
226 (slice_ids, slice_names) = self.separate_types(value)[0:2]
229 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
230 slice_ids += slices.keys()
232 if self['slice_ids_whitelist'] != slice_ids:
233 from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
234 from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
235 new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
236 stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
238 for new_slice in new_slices:
239 AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
240 for stale_slice in stale_slices:
241 DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']])
244 def delete(self, commit = True):
246 Delete existing node.
249 assert 'node_id' in self
251 # we need to clean up InterfaceTags, so handling interfaces as part of join_tables does not work
252 # federated nodes don't have interfaces though so for smooth transition from 4.2 to 4.3
253 if 'peer_id' in self and self['peer_id']:
256 assert 'interface_ids' in self
257 for interface in Interfaces(self.api,self['interface_ids']):
260 # Clean up miscellaneous join tables
261 for table in self.join_tables:
262 self.api.db.do("DELETE FROM %s WHERE node_id = %d" % \
263 (table, self['node_id']))
266 self['deleted'] = True
272 Representation of row(s) from the nodes table in the
276 def __init__(self, api, node_filter = None, columns = None):
277 Table.__init__(self, api, Node, columns)
279 # the view that we're selecting upon: start with view_nodes
281 # as many left joins as requested tags
282 for tagname in self.tag_columns:
283 view= "%s left join %s using (%s)"%(view,Node.tagvalue_view_name(tagname),
286 sql = "SELECT %s FROM %s WHERE deleted IS False" % \
287 (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
289 if node_filter is not None:
290 if isinstance(node_filter, (list, tuple, set)):
291 # Separate the list into integers and strings
292 ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
293 strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
294 node_filter = Filter(Node.fields, {'node_id': ints, 'hostname': strs})
295 sql += " AND (%s) %s" % node_filter.sql(api, "OR")
296 elif isinstance(node_filter, dict):
297 allowed_fields=dict(Node.fields.items()+Node.tags.items())
298 node_filter = Filter(allowed_fields, node_filter)
299 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
300 elif isinstance (node_filter, StringTypes):
301 node_filter = Filter(Node.fields, {'hostname':[node_filter]})
302 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
303 elif isinstance (node_filter, int):
304 node_filter = Filter(Node.fields, {'node_id':[node_filter]})
305 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
307 raise PLCInvalidArgument, "Wrong node filter %r"%node_filter