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
20 def valid_hostname(hostname):
21 # 1. Each part begins and ends with a letter or number.
22 # 2. Each part except the last can contain letters, numbers, or hyphens.
23 # 3. Each part is between 1 and 64 characters, including the trailing dot.
24 # 4. At least two parts.
25 # 5. Last part can only contain between 2 and 6 letters.
26 good_hostname = r'^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+' \
29 re.match(good_hostname, hostname, re.IGNORECASE)
33 Representation of a row in the nodes table. To use, optionally
34 instantiate with a dict of values. Update as you would a
35 dict. Commit to the database with sync().
39 primary_key = 'node_id'
40 join_tables = [ 'slice_node', 'peer_node', 'slice_tag',
41 'node_session', 'node_slice_whitelist',
42 'node_tag', 'conf_file_node', 'pcu_node', 'leases', ]
44 'node_id': Parameter(int, "Node identifier"),
45 'node_type': Parameter(str,"Node type",max=20),
46 'hostname': Parameter(str, "Fully qualified hostname", max = 255),
47 'site_id': Parameter(int, "Site at which this node is located"),
48 'boot_state': Parameter(str, "Boot state", max = 20),
49 'run_level': Parameter(str, "Run level", max = 20),
50 'model': Parameter(str, "Make and model of the actual machine", max = 255, nullok = True),
51 'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot", max = 128),
52 'version': Parameter(str, "Apparent Boot CD version", max = 64),
53 'ssh_rsa_key': Parameter(str, "Last known SSH host key", max = 1024),
54 'date_created': Parameter(int, "Date and time when node entry was created", ro = True),
55 'last_updated': Parameter(int, "Date and time when node entry was created", ro = True),
56 'last_contact': Parameter(int, "Date and time when node last contacted plc", ro = True),
57 'last_boot': Parameter(int, "Date and time when node last booted", ro = True),
58 'last_download': Parameter(int, "Date and time when node boot image was created", ro = True),
59 'last_pcu_reboot': Parameter(int, "Date and time when PCU reboot was attempted", ro = True),
60 'last_pcu_confirmation': Parameter(int, "Date and time when PCU reboot was confirmed", ro = True),
61 'verified': Parameter(bool, "Whether the node configuration is verified correct", ro=False),
62 'key': Parameter(str, "(Admin only) Node key", max = 256),
63 'session': Parameter(str, "(Admin only) Node session value", max = 256, ro = True),
64 'interface_ids': Parameter([int], "List of network interfaces that this node has"),
65 'conf_file_ids': Parameter([int], "List of configuration files specific to this node"),
66 # 'root_person_ids': Parameter([int], "(Admin only) List of people who have root access to this node"),
67 'slice_ids': Parameter([int], "List of slices on this node"),
68 'slice_ids_whitelist': Parameter([int], "List of slices allowed on this node"),
69 'pcu_ids': Parameter([int], "List of PCUs that control this node"),
70 'ports': Parameter([int], "List of PCU ports that this node is connected to"),
71 'peer_id': Parameter(int, "Peer to which this node belongs", nullok = True),
72 'peer_node_id': Parameter(int, "Foreign node identifier at peer", nullok = True),
73 'node_tag_ids' : Parameter ([int], "List of tags attached to this node"),
74 'nodegroup_ids': Parameter([int], "List of node groups that this node is in"),
77 'interfaces': [Mixed(Parameter(int, "Interface identifier"),
78 Filter(Interface.fields))],
79 'conf_files': [Parameter(int, "ConfFile identifier")],
80 'slices': [Mixed(Parameter(int, "Slice identifier"),
81 Parameter(str, "Slice name"))],
82 'slices_whitelist': [Mixed(Parameter(int, "Slice identifier"),
83 Parameter(str, "Slice name"))]
86 view_tags_name = "view_node_tags"
87 # tags are used by the Add/Get/Update methods to expose tags
88 # this is initialized here and updated by the accessors factory
91 def validate_hostname(self, hostname):
92 hostname = hostname.lower()
93 if not valid_hostname(hostname):
94 raise PLCInvalidArgument, "Invalid hostname"
96 conflicts = Nodes(self.api, [hostname])
97 for node in conflicts:
98 if 'node_id' not in self or self['node_id'] != node['node_id']:
99 raise PLCInvalidArgument, "Hostname already in use"
103 def validate_node_type(self, node_type):
104 node_types = [row['node_type'] for row in NodeTypes(self.api)]
105 if node_type not in node_types:
106 raise PLCInvalidArgument, "Invalid node type %r"%node_type
109 def validate_boot_state(self, boot_state):
110 boot_states = [row['boot_state'] for row in BootStates(self.api)]
111 if boot_state not in boot_states:
112 raise PLCInvalidArgument, "Invalid boot state %r"%boot_state
115 validate_date_created = Row.validate_timestamp
116 validate_last_updated = Row.validate_timestamp
117 validate_last_contact = Row.validate_timestamp
118 validate_last_boot = Row.validate_timestamp
119 validate_last_download = Row.validate_timestamp
120 validate_last_pcu_reboot = Row.validate_timestamp
121 validate_last_pcu_confirmation = Row.validate_timestamp
123 def update_timestamp(self, col_name, commit = True):
125 Update col_name field with current time
128 assert 'node_id' in self
129 assert self.table_name
131 self.api.db.do("UPDATE %s SET %s = CURRENT_TIMESTAMP " % (self.table_name, col_name) + \
132 " where node_id = %d" % (self['node_id']) )
135 def update_last_boot(self, commit = True):
136 self.update_timestamp('last_boot', commit)
137 def update_last_download(self, commit = True):
138 self.update_timestamp('last_download', commit)
139 def update_last_pcu_reboot(self, commit = True):
140 self.update_timestamp('last_pcu_reboot', commit)
141 def update_last_pcu_confirmation(self, commit = True):
142 self.update_timestamp('last_pcu_confirmation', commit)
144 def update_last_contact(self, commit = True):
145 self.update_timestamp('last_contact', commit)
146 def update_last_updated(self, commit = True):
147 self.update_timestamp('last_updated', commit)
149 def update_tags(self, tags):
150 from PLC.Shell import Shell
151 from PLC.NodeTags import NodeTags
152 from PLC.Methods.AddNodeTag import AddNodeTag
153 from PLC.Methods.UpdateNodeTag import UpdateNodeTag
155 for (tagname,value) in tags.iteritems():
156 # the tagtype instance is assumed to exist, just check that
157 if not TagTypes(self.api,{'tagname':tagname}):
158 raise PLCInvalidArgument,"No such TagType %s"%tagname
159 node_tags=NodeTags(self.api,{'tagname':tagname,'node_id':node['node_id']})
161 AddNodeTag(self.api).__call__(shell.auth,node['node_id'],tagname,value)
163 UpdateNodeTag(self.api).__call__(shell.auth,node_tags[0]['node_tag_id'],value)
165 def associate_interfaces(self, auth, field, value):
167 Delete interfaces not found in value list (using DeleteInterface)
168 Add interfaces found in value list (using AddInterface)
169 Updates interfaces found w/ interface_id in value list (using UpdateInterface)
172 assert 'interface_ids' in self
173 assert 'node_id' in self
174 assert isinstance(value, list)
176 (interface_ids, blank, interfaces) = self.separate_types(value)
178 if self['interface_ids'] != interface_ids:
179 from PLC.Methods.DeleteInterface import DeleteInterface
181 stale_interfaces = set(self['interface_ids']).difference(interface_ids)
183 for stale_interface in stale_interfaces:
184 DeleteInterface.__call__(DeleteInterface(self.api), auth, stale_interface['interface_id'])
186 def associate_conf_files(self, auth, field, value):
188 Add conf_files found in value list (AddConfFileToNode)
189 Delets conf_files not found in value list (DeleteConfFileFromNode)
192 assert 'conf_file_ids' in self
193 assert 'node_id' in self
194 assert isinstance(value, list)
196 conf_file_ids = self.separate_types(value)[0]
198 if self['conf_file_ids'] != conf_file_ids:
199 from PLC.Methods.AddConfFileToNode import AddConfFileToNode
200 from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
201 new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
202 stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
204 for new_conf_file in new_conf_files:
205 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
206 for stale_conf_file in stale_conf_files:
207 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
209 def associate_slices(self, auth, field, value):
211 Add slices found in value list to (AddSliceToNode)
212 Delete slices not found in value list (DeleteSliceFromNode)
215 from PLC.Slices import Slices
217 assert 'slice_ids' in self
218 assert 'node_id' in self
219 assert isinstance(value, list)
221 (slice_ids, slice_names) = self.separate_types(value)[0:2]
224 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
225 slice_ids += slices.keys()
227 if self['slice_ids'] != slice_ids:
228 from PLC.Methods.AddSliceToNodes import AddSliceToNodes
229 from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
230 new_slices = set(slice_ids).difference(self['slice_ids'])
231 stale_slices = set(self['slice_ids']).difference(slice_ids)
233 for new_slice in new_slices:
234 AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
235 for stale_slice in stale_slices:
236 DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])
238 def associate_slices_whitelist(self, auth, field, value):
240 Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
241 Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
244 from PLC.Slices import Slices
246 assert 'slice_ids_whitelist' in self
247 assert 'node_id' in self
248 assert isinstance(value, list)
250 (slice_ids, slice_names) = self.separate_types(value)[0:2]
253 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
254 slice_ids += slices.keys()
256 if self['slice_ids_whitelist'] != slice_ids:
257 from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
258 from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
259 new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
260 stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
262 for new_slice in new_slices:
263 AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
264 for stale_slice in stale_slices:
265 DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']])
268 def delete(self, commit = True):
270 Delete existing node.
273 assert 'node_id' in self
275 # we need to clean up InterfaceTags, so handling interfaces as part of join_tables does not work
276 # federated nodes don't have interfaces though so for smooth transition from 4.2 to 4.3
277 if 'peer_id' in self and self['peer_id']:
280 assert 'interface_ids' in self
281 for interface in Interfaces(self.api,self['interface_ids']):
284 # Clean up miscellaneous join tables
285 for table in self.join_tables:
286 self.api.db.do("DELETE FROM %s WHERE node_id = %d" % \
287 (table, self['node_id']))
290 self['deleted'] = True
295 Representation of row(s) from the nodes table in the
299 def __init__(self, api, node_filter = None, columns = None):
300 Table.__init__(self, api, Node, columns)
302 # the view that we're selecting upon: start with view_nodes
304 # as many left joins as requested tags
305 for tagname in self.tag_columns:
306 view= "%s left join %s using (%s)"%(view,Node.tagvalue_view_name(tagname),
309 sql = "SELECT %s FROM %s WHERE deleted IS False" % \
310 (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
312 if node_filter is not None:
313 if isinstance(node_filter, (list, tuple, set)):
314 # Separate the list into integers and strings
315 ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
316 strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
317 node_filter = Filter(Node.fields, {'node_id': ints, 'hostname': strs})
318 sql += " AND (%s) %s" % node_filter.sql(api, "OR")
319 elif isinstance(node_filter, dict):
320 allowed_fields=dict(Node.fields.items()+Node.tags.items())
321 node_filter = Filter(allowed_fields, node_filter)
322 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
323 elif isinstance (node_filter, StringTypes):
324 node_filter = Filter(Node.fields, {'hostname':node_filter})
325 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
326 elif isinstance (node_filter, (int, long)):
327 node_filter = Filter(Node.fields, {'node_id':node_filter})
328 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
330 raise PLCInvalidArgument, "Wrong node filter %r"%node_filter