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