both hostnames and mail addresses should composed by lowercase letters
[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         hostname = hostname.lower()
92         if not valid_hostname(hostname):
93             raise PLCInvalidArgument, "Invalid hostname"
94
95         conflicts = Nodes(self.api, [hostname])
96         for node in conflicts:
97             if 'node_id' not in self or self['node_id'] != node['node_id']:
98                 raise PLCInvalidArgument, "Hostname already in use"
99
100         return hostname
101
102     def validate_node_type(self, node_type):
103         node_types = [row['node_type'] for row in NodeTypes(self.api)]
104         if node_type not in node_types:
105             raise PLCInvalidArgument, "Invalid node type %r"%node_type
106         return node_type
107
108     def validate_boot_state(self, boot_state):
109         boot_states = [row['boot_state'] for row in BootStates(self.api)]
110         if boot_state not in boot_states:
111             raise PLCInvalidArgument, "Invalid boot state %r"%boot_state
112         return boot_state
113
114     validate_date_created = Row.validate_timestamp
115     validate_last_updated = Row.validate_timestamp
116     validate_last_contact = Row.validate_timestamp
117
118     def update_last_contact(self, commit = True):
119         """
120         Update last_contact field with current time
121         """
122         
123         assert 'node_id' in self
124         assert self.table_name
125
126         self.api.db.do("UPDATE %s SET last_contact = CURRENT_TIMESTAMP " % (self.table_name) + \
127                        " where node_id = %d" % ( self['node_id']) )
128         self.sync(commit)
129
130
131     def update_last_updated(self, commit = True):
132         """
133         Update last_updated field with current time
134         """
135
136         assert 'node_id' in self
137         assert self.table_name
138
139         self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
140                        " where node_id = %d" % (self['node_id']) )
141         self.sync(commit)
142
143     def associate_interfaces(self, auth, field, value):
144         """
145         Delete interfaces not found in value list (using DeleteInterface)       
146         Add interfaces found in value list (using AddInterface)
147         Updates interfaces found w/ interface_id in value list (using UpdateInterface) 
148         """
149
150         assert 'interface_ids' in self
151         assert 'node_id' in self
152         assert isinstance(value, list)
153
154         (interface_ids, blank, interfaces) = self.separate_types(value)
155
156         if self['interface_ids'] != interface_ids:
157             from PLC.Methods.DeleteInterface import DeleteInterface
158
159             stale_interfaces = set(self['interface_ids']).difference(interface_ids)
160
161             for stale_interface in stale_interfaces:
162                 DeleteInterface.__call__(DeleteInterface(self.api), auth, stale_interface['interface_id'])
163
164     def associate_conf_files(self, auth, field, value):
165         """
166         Add conf_files found in value list (AddConfFileToNode)
167         Delets conf_files not found in value list (DeleteConfFileFromNode)
168         """
169         
170         assert 'conf_file_ids' in self
171         assert 'node_id' in self
172         assert isinstance(value, list)
173         
174         conf_file_ids = self.separate_types(value)[0]
175         
176         if self['conf_file_ids'] != conf_file_ids:
177             from PLC.Methods.AddConfFileToNode import AddConfFileToNode
178             from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
179             new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
180             stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
181         
182             for new_conf_file in new_conf_files:
183                 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
184             for stale_conf_file in stale_conf_files:
185                 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
186
187     def associate_slices(self, auth, field, value):
188         """
189         Add slices found in value list to (AddSliceToNode)
190         Delete slices not found in value list (DeleteSliceFromNode)
191         """
192         
193         from PLC.Slices import Slices
194         
195         assert 'slice_ids' in self
196         assert 'node_id' in self
197         assert isinstance(value, list)
198         
199         (slice_ids, slice_names) = self.separate_types(value)[0:2]
200
201         if slice_names:
202             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
203             slice_ids += slices.keys()
204
205         if self['slice_ids'] != slice_ids:
206             from PLC.Methods.AddSliceToNodes import AddSliceToNodes
207             from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
208             new_slices = set(slice_ids).difference(self['slice_ids'])
209             stale_slices = set(self['slice_ids']).difference(slice_ids)
210         
211         for new_slice in new_slices:
212             AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
213         for stale_slice in stale_slices:
214             DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])                         
215
216     def associate_slices_whitelist(self, auth, field, value):
217         """
218         Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
219         Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
220         """
221
222         from PLC.Slices import Slices
223
224         assert 'slice_ids_whitelist' in self
225         assert 'node_id' in self
226         assert isinstance(value, list)
227
228         (slice_ids, slice_names) = self.separate_types(value)[0:2]
229
230         if slice_names:
231             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
232             slice_ids += slices.keys()
233
234         if self['slice_ids_whitelist'] != slice_ids:
235             from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
236             from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
237             new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
238             stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
239
240         for new_slice in new_slices:
241             AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
242         for stale_slice in stale_slices:
243             DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']]) 
244                 
245
246     def delete(self, commit = True):
247         """
248         Delete existing node.
249         """
250
251         assert 'node_id' in self
252
253         # we need to clean up InterfaceTags, so handling interfaces as part of join_tables does not work
254         # federated nodes don't have interfaces though so for smooth transition from 4.2 to 4.3
255         if 'peer_id' in self and self['peer_id']:
256             pass
257         else:
258             assert 'interface_ids' in self
259             for interface in Interfaces(self.api,self['interface_ids']):
260                 interface.delete()
261
262         # Clean up miscellaneous join tables
263         for table in self.join_tables:
264             self.api.db.do("DELETE FROM %s WHERE node_id = %d" % \
265                            (table, self['node_id']))
266
267         # Mark as deleted
268         self['deleted'] = True
269         self.sync(commit)
270
271
272 class Nodes(Table):
273     """
274     Representation of row(s) from the nodes table in the
275     database.
276     """
277
278     def __init__(self, api, node_filter = None, columns = None):
279         Table.__init__(self, api, Node, columns)
280
281         # the view that we're selecting upon: start with view_nodes
282         view = "view_nodes"
283         # as many left joins as requested tags
284         for tagname in self.tag_columns:
285             view= "%s left join %s using (%s)"%(view,Node.tagvalue_view_name(tagname),
286                                                 Node.primary_key)
287             
288         sql = "SELECT %s FROM %s WHERE deleted IS False" % \
289               (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
290
291         if node_filter is not None:
292             if isinstance(node_filter, (list, tuple, set)):
293                 # Separate the list into integers and strings
294                 ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
295                 strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
296                 node_filter = Filter(Node.fields, {'node_id': ints, 'hostname': strs})
297                 sql += " AND (%s) %s" % node_filter.sql(api, "OR")
298             elif isinstance(node_filter, dict):
299                 allowed_fields=dict(Node.fields.items()+Node.tags.items())
300                 node_filter = Filter(allowed_fields, node_filter)
301                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
302             elif isinstance (node_filter, StringTypes):
303                 node_filter = Filter(Node.fields, {'hostname':[node_filter]})
304                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
305             elif isinstance (node_filter, int):
306                 node_filter = Filter(Node.fields, {'node_id':[node_filter]})
307                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
308             else:
309                 raise PLCInvalidArgument, "Wrong node filter %r"%node_filter
310
311         self.selectall(sql)