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