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.1 2006/09/06 15:36:07 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 flush(self, commit = True):
106 Flush changes back to the database.
111 # Fetch a new node_id if necessary
112 if 'node_id' not in self:
113 rows = self.api.db.selectall("SELECT NEXTVAL('nodes_node_id_seq') AS node_id")
115 raise PLCDBError, "Unable to fetch new node_id"
116 self['node_id'] = rows[0]['node_id']
121 # Filter out fields that cannot be set or updated directly
122 fields = dict(filter(lambda (key, value): key in self.fields,
125 # Parameterize for safety
127 values = [self.api.db.param(key, value) for (key, value) in fields.items()]
130 # Insert new row in nodes table
131 sql = "INSERT INTO nodes (%s) VALUES (%s)" % \
132 (", ".join(keys), ", ".join(values))
134 # Update existing row in nodes table
135 columns = ["%s = %s" % (key, value) for (key, value) in zip(keys, values)]
136 sql = "UPDATE nodes SET " + \
137 ", ".join(columns) + \
138 " WHERE node_id = %(node_id)d"
140 self.api.db.do(sql, fields)
145 def delete(self, commit = True):
147 Delete existing node.
150 assert 'node_id' in self
152 # Delete all nodenetworks
153 nodenetworks = NodeNetworks(self.api, self['nodenetwork_ids'])
154 for nodenetwork in nodenetworks.values():
155 nodenetwork.delete(commit = False)
157 # Clean up miscellaneous join tables
158 for table in ['nodegroup_nodes', 'pod_hash', 'conf_assoc',
159 'node_root_access', 'dslice03_slicenode',
161 self.api.db.do("DELETE FROM %s" \
162 " WHERE node_id = %d" % \
163 (table, self['node_id']))
166 self['deleted'] = True
171 Representation of row(s) from the nodes table in the
175 def __init__(self, api, node_id_or_hostname_list = None, extra_fields = []):
178 sql = "SELECT nodes.*, node_nodenetworks.nodenetwork_id"
180 # For compatibility and convenience, support returning primary
181 # interface values directly in the Node structure.
182 extra_nodenetwork_fields = set(extra_fields).intersection(Node.primary_nodenetwork_fields)
184 # N.B.: Joined IDs may be marked as deleted in their primary tables
186 # extra_field: (extra_table, extra_column, join_using)
187 'nodegroup_ids': ('nodegroup_nodes', 'nodegroup_id', 'node_id'),
188 'conf_file_ids': ('conf_assoc', 'conf_file_id', 'node_id'),
189 'root_person_ids': ('node_root_access', 'person_id AS root_person_id', 'node_id'),
190 'slice_ids': ('dslice03_slicenode', 'slice_id', 'node_id'),
191 'pcu_ids': ('pcu_ports', 'pcu_id', 'node_id'),
194 extra_fields = filter(join_tables.has_key, extra_fields)
195 extra_tables = ["%s USING (%s)" % \
196 (join_tables[field][0], join_tables[field][2]) \
197 for field in extra_fields]
198 extra_columns = ["%s.%s" % \
199 (join_tables[field][0], join_tables[field][1]) \
200 for field in extra_fields]
203 sql += ", " + ", ".join(extra_columns)
205 sql += " FROM nodes" \
206 " LEFT JOIN node_nodenetworks USING (node_id)"
209 sql += " LEFT JOIN " + " LEFT JOIN ".join(extra_tables)
211 sql += " WHERE deleted IS False"
213 if node_id_or_hostname_list:
214 # Separate the list into integers and strings
215 node_ids = filter(lambda node_id: isinstance(node_id, (int, long)),
216 node_id_or_hostname_list)
217 hostnames = filter(lambda hostname: isinstance(hostname, StringTypes),
218 node_id_or_hostname_list)
221 sql += " OR node_id IN (%s)" % ", ".join(map(str, node_ids))
223 sql += " OR hostname IN (%s)" % ", ".join(api.db.quote(hostnames)).lower()
226 # So that if the node has a primary interface, it is listed
228 if 'nodenetwork_ids' in extra_fields:
229 sql += " ORDER BY node_nodenetworks.is_primary DESC"
231 rows = self.api.db.selectall(sql)
233 if self.has_key(row['node_id']):
234 node = self[row['node_id']]
237 self[row['node_id']] = Node(api, row)
239 # XXX Should instead have a site_node join table that is
240 # magically taken care of above.
242 sql = "SELECT node_id, sites.site_id FROM nodegroup_nodes" \
243 " INNER JOIN sites USING (nodegroup_id)" \
244 " WHERE node_id IN (%s)" % ", ".join(map(str, self.keys()))
246 rows = self.api.db.selectall(sql, self)
248 assert self.has_key(row['node_id'])
249 node = self[row['node_id']]
252 # Fill in optional primary interface fields for each node
253 if extra_nodenetwork_fields:
254 # More efficient to get all the nodenetworks at once
256 for node in self.values():
257 nodenetwork_ids += node['nodenetwork_ids']
260 nodenetwork_ids = set(nodenetwork_ids)
262 # Get all nodenetwork information
263 nodenetworks = NodeNetworks(self.api, nodenetwork_ids)
265 for node in self.values():
266 for nodenetwork_id in node['nodenetwork_ids']:
267 nodenetwork = nodenetworks[nodenetwork_id]
268 if nodenetwork['is_primary']:
269 for field in extra_nodenetwork_fields:
270 node[field] = nodenetwork[field]