- add functions add_node_network() and set_primary_node_network(). Node
[plcapi.git] / PLC / Nodes.py
1 #
2 # Functions for interacting with the nodes table in the database
3 #
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
6 #
7 # $Id: Nodes.py,v 1.2 2006/09/08 19:44:51 mlhuang Exp $
8 #
9
10 from types import StringTypes
11 import re
12
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
19
20 class Node(Row):
21     """
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().
25     """
26
27     fields = {
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),
39         }
40
41     # These fields are derived from join tables and are not actually
42     # in the nodes table.
43     join_fields = {
44         'nodenetwork_ids': Parameter([int], "List of network interfaces that this node has"),
45         }
46
47     # These fields are derived from join tables and are not returned
48     # by default unless specified.
49     extra_fields = {
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"),
53         # XXX Too inefficient
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"),
57         }
58
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()))
63
64     extra_fields.update(primary_nodenetwork_fields)
65
66     default_fields = dict(fields.items() + join_fields.items())
67     all_fields = dict(default_fields.items() + extra_fields.items())
68
69     def __init__(self, api, fields):
70         Row.__init__(self, fields)
71         self.api = api
72
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])?\.)+' \
80                         r'[a-z]{2,6}$'
81         if not hostname or \
82            not re.match(good_hostname, hostname, re.IGNORECASE):
83             raise PLCInvalidArgument, "Invalid hostname"
84
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"
89
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"
95
96         return hostname
97
98     def validate_boot_state(self, boot_state):
99         if boot_state not in BootStates(self.api):
100             raise PLCInvalidArgument, "Invalid boot state"
101
102         return boot_state
103
104     def add_node_network(self, nodenetwork, commit = True):
105         """
106         Add node network to this node.
107         """
108
109         assert 'node_id' in self
110         assert isinstance(nodenetwork, NodeNetwork)
111         assert 'nodenetwork_id' in nodenetwork
112
113         nodenetwork_id = nodenetwork['nodenetwork_id']
114         nodenetwork['node_id'] = self['node_id']
115
116         self.api.db.do("INSERT INTO node_nodenetworks (node_id, nodenetwork_id, is_primary)" \
117                        " VALUES(%(node_id)d, %(nodenetwork_id)d, False)",
118                        nodenetwork)
119
120         if commit:
121             self.api.db.commit()
122
123         if 'nodenetwork_ids' in self and nodenetwork_id not in self['nodenetwork_ids']:
124             self['nodenetwork_ids'].append(nodenetwork_id)
125
126     def set_primary_node_network(self, nodenetwork, commit = True):
127         """
128         Remove node network from this node.
129         """
130
131         assert 'node_id' in self
132         assert isinstance(nodenetwork, NodeNetwork)
133         assert 'nodenetwork_id' in nodenetwork
134
135         node_id = self['node_id']
136         nodenetwork_id = nodenetwork['nodenetwork_id']
137
138         self.api.db.do("UPDATE node_nodenetworks SET is_primary = False" \
139                        " WHERE node_id = %(node_id)d",
140                        locals())
141
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",
145                        locals())
146
147         if commit:
148             self.api.db.commit()
149
150         nodenetwork['is_primary'] = True
151
152     def flush(self, commit = True):
153         """
154         Flush changes back to the database.
155         """
156
157         self.validate()
158
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")
162             if not rows:
163                 raise PLCDBError, "Unable to fetch new node_id"
164             self['node_id'] = rows[0]['node_id']
165             insert = True
166         else:
167             insert = False
168
169         # Filter out fields that cannot be set or updated directly
170         fields = dict(filter(lambda (key, value): key in self.fields,
171                              self.items()))
172
173         # Parameterize for safety
174         keys = fields.keys()
175         values = [self.api.db.param(key, value) for (key, value) in fields.items()]
176
177         if insert:
178             # Insert new row in nodes table
179             sql = "INSERT INTO nodes (%s) VALUES (%s)" % \
180                   (", ".join(keys), ", ".join(values))
181         else:
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"
187
188         self.api.db.do(sql, fields)
189
190         if commit:
191             self.api.db.commit()
192
193     def delete(self, commit = True):
194         """
195         Delete existing node.
196         """
197
198         assert 'node_id' in self
199
200         # Delete all nodenetworks
201         nodenetworks = NodeNetworks(self.api, self['nodenetwork_ids'])
202         for nodenetwork in nodenetworks.values():
203             nodenetwork.delete(commit = False)
204
205         # Clean up miscellaneous join tables
206         for table in ['nodegroup_nodes', 'pod_hash', 'conf_assoc',
207                       'node_root_access', 'dslice03_slicenode',
208                       'pcu_ports']:
209             self.api.db.do("DELETE FROM %s" \
210                            " WHERE node_id = %d" % \
211                            (table, self['node_id']))
212
213         # Mark as deleted
214         self['deleted'] = True
215         self.flush(commit)
216
217 class Nodes(Table):
218     """
219     Representation of row(s) from the nodes table in the
220     database.
221     """
222
223     def __init__(self, api, node_id_or_hostname_list = None, extra_fields = []):
224         self.api = api
225
226         sql = "SELECT nodes.*, node_nodenetworks.nodenetwork_id"
227
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)
231
232         # N.B.: Joined IDs may be marked as deleted in their primary tables
233         join_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'),
240             }
241
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]
249
250         if extra_columns:
251             sql += ", " + ", ".join(extra_columns)
252
253         sql += " FROM nodes" \
254                " LEFT JOIN node_nodenetworks USING (node_id)"
255
256         if extra_tables:
257             sql += " LEFT JOIN " + " LEFT JOIN ".join(extra_tables)
258
259         sql += " WHERE deleted IS False"
260
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)
267             sql += " AND (False"
268             if node_ids:
269                 sql += " OR node_id IN (%s)" % ", ".join(map(str, node_ids))
270             if hostnames:
271                 sql += " OR hostname IN (%s)" % ", ".join(api.db.quote(hostnames)).lower()
272             sql += ")"
273
274         # So that if the node has a primary interface, it is listed
275         # first.
276         if 'nodenetwork_ids' in extra_fields:
277             sql += " ORDER BY node_nodenetworks.is_primary DESC"
278
279         rows = self.api.db.selectall(sql)
280         for row in rows:
281             if self.has_key(row['node_id']):
282                 node = self[row['node_id']]
283                 node.update(row)
284             else:
285                 self[row['node_id']] = Node(api, row)
286
287         # XXX Should instead have a site_node join table that is
288         # magically taken care of above.
289         if rows:
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()))
293
294             rows = self.api.db.selectall(sql, self)
295             for row in rows:
296                 assert self.has_key(row['node_id'])
297                 node = self[row['node_id']]
298                 node.update(row)
299
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
303             nodenetwork_ids = []
304             for node in self.values():
305                 nodenetwork_ids += node['nodenetwork_ids']
306
307             # Remove duplicates
308             nodenetwork_ids = set(nodenetwork_ids)
309
310             # Get all nodenetwork information
311             nodenetworks = NodeNetworks(self.api, nodenetwork_ids)
312
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]
319                         break