First draft for leases
[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', 'leases', ]
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 update_tags(self, tags):
143         from PLC.Shell import Shell
144         from PLC.NodeTags import NodeTags
145         from PLC.Methods.AddNodeTag import AddNodeTag
146         from PLC.Methods.UpdateNodeTag import UpdateNodeTag
147         shell = Shell()
148         for (tagname,value) in tags.iteritems():
149             # the tagtype instance is assumed to exist, just check that
150             if not TagTypes(self.api,{'tagname':tagname}):
151                 raise PLCInvalidArgument,"No such TagType %s"%tagname
152             node_tags=NodeTags(self.api,{'tagname':tagname,'node_id':node['node_id']})
153             if not node_tags:
154                 AddNodeTag(self.api).__call__(shell.auth,node['node_id'],tagname,value)
155             else:
156                 UpdateNodeTag(self.api).__call__(shell.auth,node_tags[0]['node_tag_id'],value) 
157     
158     def associate_interfaces(self, auth, field, value):
159         """
160         Delete interfaces not found in value list (using DeleteInterface)       
161         Add interfaces found in value list (using AddInterface)
162         Updates interfaces found w/ interface_id in value list (using UpdateInterface) 
163         """
164
165         assert 'interface_ids' in self
166         assert 'node_id' in self
167         assert isinstance(value, list)
168
169         (interface_ids, blank, interfaces) = self.separate_types(value)
170
171         if self['interface_ids'] != interface_ids:
172             from PLC.Methods.DeleteInterface import DeleteInterface
173
174             stale_interfaces = set(self['interface_ids']).difference(interface_ids)
175
176             for stale_interface in stale_interfaces:
177                 DeleteInterface.__call__(DeleteInterface(self.api), auth, stale_interface['interface_id'])
178
179     def associate_conf_files(self, auth, field, value):
180         """
181         Add conf_files found in value list (AddConfFileToNode)
182         Delets conf_files not found in value list (DeleteConfFileFromNode)
183         """
184         
185         assert 'conf_file_ids' in self
186         assert 'node_id' in self
187         assert isinstance(value, list)
188         
189         conf_file_ids = self.separate_types(value)[0]
190         
191         if self['conf_file_ids'] != conf_file_ids:
192             from PLC.Methods.AddConfFileToNode import AddConfFileToNode
193             from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
194             new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
195             stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
196         
197             for new_conf_file in new_conf_files:
198                 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
199             for stale_conf_file in stale_conf_files:
200                 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
201
202     def associate_slices(self, auth, field, value):
203         """
204         Add slices found in value list to (AddSliceToNode)
205         Delete slices not found in value list (DeleteSliceFromNode)
206         """
207         
208         from PLC.Slices import Slices
209         
210         assert 'slice_ids' in self
211         assert 'node_id' in self
212         assert isinstance(value, list)
213         
214         (slice_ids, slice_names) = self.separate_types(value)[0:2]
215
216         if slice_names:
217             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
218             slice_ids += slices.keys()
219
220         if self['slice_ids'] != slice_ids:
221             from PLC.Methods.AddSliceToNodes import AddSliceToNodes
222             from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
223             new_slices = set(slice_ids).difference(self['slice_ids'])
224             stale_slices = set(self['slice_ids']).difference(slice_ids)
225         
226         for new_slice in new_slices:
227             AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
228         for stale_slice in stale_slices:
229             DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])                         
230
231     def associate_slices_whitelist(self, auth, field, value):
232         """
233         Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
234         Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
235         """
236
237         from PLC.Slices import Slices
238
239         assert 'slice_ids_whitelist' in self
240         assert 'node_id' in self
241         assert isinstance(value, list)
242
243         (slice_ids, slice_names) = self.separate_types(value)[0:2]
244
245         if slice_names:
246             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
247             slice_ids += slices.keys()
248
249         if self['slice_ids_whitelist'] != slice_ids:
250             from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
251             from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
252             new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
253             stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
254
255         for new_slice in new_slices:
256             AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
257         for stale_slice in stale_slices:
258             DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']]) 
259                 
260
261     def delete(self, commit = True):
262         """
263         Delete existing node.
264         """
265
266         assert 'node_id' in self
267
268         # we need to clean up InterfaceTags, so handling interfaces as part of join_tables does not work
269         # federated nodes don't have interfaces though so for smooth transition from 4.2 to 4.3
270         if 'peer_id' in self and self['peer_id']:
271             pass
272         else:
273             assert 'interface_ids' in self
274             for interface in Interfaces(self.api,self['interface_ids']):
275                 interface.delete()
276
277         # Clean up miscellaneous join tables
278         for table in self.join_tables:
279             self.api.db.do("DELETE FROM %s WHERE node_id = %d" % \
280                            (table, self['node_id']))
281
282         # Mark as deleted
283         self['deleted'] = True
284         self.sync(commit)
285
286
287 class Nodes(Table):
288     """
289     Representation of row(s) from the nodes table in the
290     database.
291     """
292
293     def __init__(self, api, node_filter = None, columns = None):
294         Table.__init__(self, api, Node, columns)
295
296         # the view that we're selecting upon: start with view_nodes
297         view = "view_nodes"
298         # as many left joins as requested tags
299         for tagname in self.tag_columns:
300             view= "%s left join %s using (%s)"%(view,Node.tagvalue_view_name(tagname),
301                                                 Node.primary_key)
302             
303         sql = "SELECT %s FROM %s WHERE deleted IS False" % \
304               (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
305
306         if node_filter is not None:
307             if isinstance(node_filter, (list, tuple, set)):
308                 # Separate the list into integers and strings
309                 ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
310                 strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
311                 node_filter = Filter(Node.fields, {'node_id': ints, 'hostname': strs})
312                 sql += " AND (%s) %s" % node_filter.sql(api, "OR")
313             elif isinstance(node_filter, dict):
314                 allowed_fields=dict(Node.fields.items()+Node.tags.items())
315                 node_filter = Filter(allowed_fields, node_filter)
316                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
317             elif isinstance (node_filter, StringTypes):
318                 node_filter = Filter(Node.fields, {'hostname':[node_filter]})
319                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
320             elif isinstance (node_filter, int):
321                 node_filter = Filter(Node.fields, {'node_id':[node_filter]})
322                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
323             else:
324                 raise PLCInvalidArgument, "Wrong node filter %r"%node_filter
325
326         self.selectall(sql)