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
7 # $Id: Nodes.py,v 1.2 2006/09/08 19:44:51 mlhuang Exp $
10 from types import StringTypes
13 from PLC.Faults import *
14 from PLC.Parameter import Parameter
15 from PLC.Debug import profile
16 from PLC.Table import Row, Table
17 from PLC.NodeNetworks import NodeNetwork, NodeNetworks
18 from PLC.BootStates import BootStates
22 Representation of a row in the nodes table. To use, optionally
23 instantiate with a dict of values. Update as you would a
24 dict. Commit to the database with flush().
28 'node_id': Parameter(int, "Node identifier"),
29 'hostname': Parameter(str, "Fully qualified hostname", max = 255),
30 'boot_state': Parameter(str, "Boot state", max = 20),
31 'model': Parameter(str, "Make and model of the actual machine", max = 255),
32 'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot", max = 128),
33 'version': Parameter(str, "Apparent Boot CD version", max = 64),
34 'ssh_rsa_key': Parameter(str, "Last known SSH host key", max = 1024),
35 'date_created': Parameter(str, "Date and time when node entry was created"),
36 'deleted': Parameter(bool, "Has been deleted"),
37 'key': Parameter(str, "(Admin only) Node key", max = 256),
38 'session': Parameter(str, "(Admin only) Node session value", max = 256),
41 # These fields are derived from join tables and are not actually
44 'nodenetwork_ids': Parameter([int], "List of network interfaces that this node has"),
47 # These fields are derived from join tables and are not returned
48 # by default unless specified.
50 'nodegroup_ids': Parameter([int], "List of node groups that this node is in"),
51 'conf_file_ids': Parameter([int], "List of configuration files specific to this node"),
52 'root_person_ids': Parameter([int], "(Admin only) List of people who have root access to this node"),
54 # 'slice_ids': Parameter([int], "List of slices on this node"),
55 'pcu_ids': Parameter([int], "List of PCUs that control this node"),
56 'site_id': Parameter([int], "Site at which this node is located"),
59 # Primary interface values
60 primary_nodenetwork_fields = dict(filter(lambda (key, value): \
61 key not in ['node_id', 'is_primary', 'hostname'],
62 NodeNetwork.fields.items()))
64 extra_fields.update(primary_nodenetwork_fields)
66 default_fields = dict(fields.items() + join_fields.items())
67 all_fields = dict(default_fields.items() + extra_fields.items())
69 def __init__(self, api, fields):
70 Row.__init__(self, fields)
73 def validate_hostname(self, hostname):
74 # 1. Each part begins and ends with a letter or number.
75 # 2. Each part except the last can contain letters, numbers, or hyphens.
76 # 3. Each part is between 1 and 64 characters, including the trailing dot.
77 # 4. At least two parts.
78 # 5. Last part can only contain between 2 and 6 letters.
79 good_hostname = r'^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+' \
82 not re.match(good_hostname, hostname, re.IGNORECASE):
83 raise PLCInvalidArgument, "Invalid hostname"
85 conflicts = Nodes(self.api, [hostname])
86 for node_id, node in conflicts.iteritems():
87 if not node['deleted'] and ('node_id' not in self or self['node_id'] != node_id):
88 raise PLCInvalidArgument, "Hostname already in use"
90 # Check for conflicts with a nodenetwork hostname
91 conflicts = NodeNetworks(self.api, [hostname])
92 for nodenetwork_id in conflicts:
93 if 'nodenetwork_ids' not in self or nodenetwork_id not in self['nodenetwork_ids']:
94 raise PLCInvalidArgument, "Hostname already in use"
98 def validate_boot_state(self, boot_state):
99 if boot_state not in BootStates(self.api):
100 raise PLCInvalidArgument, "Invalid boot state"
104 def add_node_network(self, nodenetwork, commit = True):
106 Add node network to this node.
109 assert 'node_id' in self
110 assert isinstance(nodenetwork, NodeNetwork)
111 assert 'nodenetwork_id' in nodenetwork
113 nodenetwork_id = nodenetwork['nodenetwork_id']
114 nodenetwork['node_id'] = self['node_id']
116 self.api.db.do("INSERT INTO node_nodenetworks (node_id, nodenetwork_id, is_primary)" \
117 " VALUES(%(node_id)d, %(nodenetwork_id)d, False)",
123 if 'nodenetwork_ids' in self and nodenetwork_id not in self['nodenetwork_ids']:
124 self['nodenetwork_ids'].append(nodenetwork_id)
126 def set_primary_node_network(self, nodenetwork, commit = True):
128 Remove node network from this node.
131 assert 'node_id' in self
132 assert isinstance(nodenetwork, NodeNetwork)
133 assert 'nodenetwork_id' in nodenetwork
135 node_id = self['node_id']
136 nodenetwork_id = nodenetwork['nodenetwork_id']
138 self.api.db.do("UPDATE node_nodenetworks SET is_primary = False" \
139 " WHERE node_id = %(node_id)d",
142 self.api.db.do("UPDATE node_nodenetworks SET is_primary = True" \
143 " WHERE node_id = %(node_id)d" \
144 " AND nodenetwork_id = %(nodenetwork_id)d",
150 nodenetwork['is_primary'] = True
152 def flush(self, commit = True):
154 Flush changes back to the database.
159 # Fetch a new node_id if necessary
160 if 'node_id' not in self:
161 rows = self.api.db.selectall("SELECT NEXTVAL('nodes_node_id_seq') AS node_id")
163 raise PLCDBError, "Unable to fetch new node_id"
164 self['node_id'] = rows[0]['node_id']
169 # Filter out fields that cannot be set or updated directly
170 fields = dict(filter(lambda (key, value): key in self.fields,
173 # Parameterize for safety
175 values = [self.api.db.param(key, value) for (key, value) in fields.items()]
178 # Insert new row in nodes table
179 sql = "INSERT INTO nodes (%s) VALUES (%s)" % \
180 (", ".join(keys), ", ".join(values))
182 # Update existing row in nodes table
183 columns = ["%s = %s" % (key, value) for (key, value) in zip(keys, values)]
184 sql = "UPDATE nodes SET " + \
185 ", ".join(columns) + \
186 " WHERE node_id = %(node_id)d"
188 self.api.db.do(sql, fields)
193 def delete(self, commit = True):
195 Delete existing node.
198 assert 'node_id' in self
200 # Delete all nodenetworks
201 nodenetworks = NodeNetworks(self.api, self['nodenetwork_ids'])
202 for nodenetwork in nodenetworks.values():
203 nodenetwork.delete(commit = False)
205 # Clean up miscellaneous join tables
206 for table in ['nodegroup_nodes', 'pod_hash', 'conf_assoc',
207 'node_root_access', 'dslice03_slicenode',
209 self.api.db.do("DELETE FROM %s" \
210 " WHERE node_id = %d" % \
211 (table, self['node_id']))
214 self['deleted'] = True
219 Representation of row(s) from the nodes table in the
223 def __init__(self, api, node_id_or_hostname_list = None, extra_fields = []):
226 sql = "SELECT nodes.*, node_nodenetworks.nodenetwork_id"
228 # For compatibility and convenience, support returning primary
229 # interface values directly in the Node structure.
230 extra_nodenetwork_fields = set(extra_fields).intersection(Node.primary_nodenetwork_fields)
232 # N.B.: Joined IDs may be marked as deleted in their primary tables
234 # extra_field: (extra_table, extra_column, join_using)
235 'nodegroup_ids': ('nodegroup_nodes', 'nodegroup_id', 'node_id'),
236 'conf_file_ids': ('conf_assoc', 'conf_file_id', 'node_id'),
237 'root_person_ids': ('node_root_access', 'person_id AS root_person_id', 'node_id'),
238 'slice_ids': ('dslice03_slicenode', 'slice_id', 'node_id'),
239 'pcu_ids': ('pcu_ports', 'pcu_id', 'node_id'),
242 extra_fields = filter(join_tables.has_key, extra_fields)
243 extra_tables = ["%s USING (%s)" % \
244 (join_tables[field][0], join_tables[field][2]) \
245 for field in extra_fields]
246 extra_columns = ["%s.%s" % \
247 (join_tables[field][0], join_tables[field][1]) \
248 for field in extra_fields]
251 sql += ", " + ", ".join(extra_columns)
253 sql += " FROM nodes" \
254 " LEFT JOIN node_nodenetworks USING (node_id)"
257 sql += " LEFT JOIN " + " LEFT JOIN ".join(extra_tables)
259 sql += " WHERE deleted IS False"
261 if node_id_or_hostname_list:
262 # Separate the list into integers and strings
263 node_ids = filter(lambda node_id: isinstance(node_id, (int, long)),
264 node_id_or_hostname_list)
265 hostnames = filter(lambda hostname: isinstance(hostname, StringTypes),
266 node_id_or_hostname_list)
269 sql += " OR node_id IN (%s)" % ", ".join(map(str, node_ids))
271 sql += " OR hostname IN (%s)" % ", ".join(api.db.quote(hostnames)).lower()
274 # So that if the node has a primary interface, it is listed
276 if 'nodenetwork_ids' in extra_fields:
277 sql += " ORDER BY node_nodenetworks.is_primary DESC"
279 rows = self.api.db.selectall(sql)
281 if self.has_key(row['node_id']):
282 node = self[row['node_id']]
285 self[row['node_id']] = Node(api, row)
287 # XXX Should instead have a site_node join table that is
288 # magically taken care of above.
290 sql = "SELECT node_id, sites.site_id FROM nodegroup_nodes" \
291 " INNER JOIN sites USING (nodegroup_id)" \
292 " WHERE node_id IN (%s)" % ", ".join(map(str, self.keys()))
294 rows = self.api.db.selectall(sql, self)
296 assert self.has_key(row['node_id'])
297 node = self[row['node_id']]
300 # Fill in optional primary interface fields for each node
301 if extra_nodenetwork_fields:
302 # More efficient to get all the nodenetworks at once
304 for node in self.values():
305 nodenetwork_ids += node['nodenetwork_ids']
308 nodenetwork_ids = set(nodenetwork_ids)
310 # Get all nodenetwork information
311 nodenetworks = NodeNetworks(self.api, nodenetwork_ids)
313 for node in self.values():
314 for nodenetwork_id in node['nodenetwork_ids']:
315 nodenetwork = nodenetworks[nodenetwork_id]
316 if nodenetwork['is_primary']:
317 for field in extra_nodenetwork_fields:
318 node[field] = nodenetwork[field]