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