nodes have a node_type (NodeType object and api calls still missing)
[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_tag', 
42                     'node_session', 'node_slice_whitelist', 
43                     'node_tag', 'conf_file_node', 'pcu_node', ]
44     fields = {
45         'node_id': Parameter(int, "Node identifier"),
46         'node_type': Parameter(str,"Node type",max=20),
47         'hostname': Parameter(str, "Fully qualified hostname", max = 255),
48         'site_id': Parameter(int, "Site at which this node is located"),
49         'boot_state': Parameter(str, "Boot state", max = 20),
50         'model': Parameter(str, "Make and model of the actual machine", max = 255, nullok = True),
51         'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot", max = 128),
52         'version': Parameter(str, "Apparent Boot CD version", max = 64),
53         'ssh_rsa_key': Parameter(str, "Last known SSH host key", max = 1024),
54         'date_created': Parameter(int, "Date and time when node entry was created", ro = True),
55         'last_updated': Parameter(int, "Date and time when node entry was created", ro = True),
56         'last_contact': Parameter(int, "Date and time when node last contacted plc", ro = True), 
57         'key': Parameter(str, "(Admin only) Node key", max = 256),
58         'session': Parameter(str, "(Admin only) Node session value", max = 256, ro = True),
59         'interface_ids': Parameter([int], "List of network interfaces that this node has"),
60         'conf_file_ids': Parameter([int], "List of configuration files specific to this node"),
61         # 'root_person_ids': Parameter([int], "(Admin only) List of people who have root access to this node"),
62         'slice_ids': Parameter([int], "List of slices on this node"),
63         'slice_ids_whitelist': Parameter([int], "List of slices allowed on this node"),
64         'pcu_ids': Parameter([int], "List of PCUs that control this node"),
65         'ports': Parameter([int], "List of PCU ports that this node is connected to"),
66         'peer_id': Parameter(int, "Peer to which this node belongs", nullok = True),
67         'peer_node_id': Parameter(int, "Foreign node identifier at peer", nullok = True),
68         'tag_ids' : Parameter ([int], "List of tags attached to this node"),
69         'nodegroup_ids': Parameter([int], "List of node groups that this node is in"),
70         }
71     related_fields = {
72         'interfaces': [Mixed(Parameter(int, "Interface identifier"),
73                              Filter(Interface.fields))],
74         'nodegroups': [Mixed(Parameter(int, "NodeGroup identifier"),
75                              Parameter(str, "NodeGroup name"))],
76         'conf_files': [Parameter(int, "ConfFile identifier")],
77         'slices': [Mixed(Parameter(int, "Slice identifier"),
78                          Parameter(str, "Slice name"))],
79         'slices_whitelist': [Mixed(Parameter(int, "Slice identifier"),
80                                    Parameter(str, "Slice name"))]
81         }
82     view_name = "view_nodes"
83     view_tags_name = "view_node_tags"
84     tags = {
85         # regular
86         'arch': Parameter(str, "node/config", ro=True),
87         'deployment': Parameter(str, "node/operation"),
88         # dummynet
89         }
90
91     def validate_hostname(self, hostname):
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_boot_state(self, boot_state):
103         boot_states = [row['boot_state'] for row in BootStates(self.api)]
104         if boot_state not in boot_states:
105             raise PLCInvalidArgument, "Invalid boot state"
106
107         return boot_state
108
109     validate_date_created = Row.validate_timestamp
110     validate_last_updated = Row.validate_timestamp
111     validate_last_contact = Row.validate_timestamp
112
113     def update_last_contact(self, commit = True):
114         """
115         Update last_contact field with current time
116         """
117         
118         assert 'node_id' in self
119         assert self.table_name
120
121         self.api.db.do("UPDATE %s SET last_contact = CURRENT_TIMESTAMP " % (self.table_name) + \
122                        " where node_id = %d" % ( self['node_id']) )
123         self.sync(commit)
124
125
126     def update_last_updated(self, commit = True):
127         """
128         Update last_updated field with current time
129         """
130
131         assert 'node_id' in self
132         assert self.table_name
133
134         self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
135                        " where node_id = %d" % (self['node_id']) )
136         self.sync(commit)
137
138     def associate_interfaces(self, auth, field, value):
139         """
140         Delete interfaces not found in value list (using DeleteInterface)       
141         Add interfaces found in value list (using AddInterface)
142         Updates interfaces found w/ interface_id in value list (using UpdateInterface) 
143         """
144
145         assert 'interface_ids' in self
146         assert 'node_id' in self
147         assert isinstance(value, list)
148
149         (interface_ids, blank, interfaces) = self.separate_types(value)
150
151         if self['interface_ids'] != interface_ids:
152             from PLC.Methods.DeleteInterface import DeleteInterface
153
154             stale_interfaces = set(self['interface_ids']).difference(interface_ids)
155
156             for stale_interface in stale_interfaces:
157                 DeleteInterface.__call__(DeleteInterface(self.api), auth, stale_interface['interface_id'])
158
159     def associate_conf_files(self, auth, field, value):
160         """
161         Add conf_files found in value list (AddConfFileToNode)
162         Delets conf_files not found in value list (DeleteConfFileFromNode)
163         """
164         
165         assert 'conf_file_ids' in self
166         assert 'node_id' in self
167         assert isinstance(value, list)
168         
169         conf_file_ids = self.separate_types(value)[0]
170         
171         if self['conf_file_ids'] != conf_file_ids:
172             from PLC.Methods.AddConfFileToNode import AddConfFileToNode
173             from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
174             new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
175             stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
176         
177             for new_conf_file in new_conf_files:
178                 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
179             for stale_conf_file in stale_conf_files:
180                 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
181
182     def associate_slices(self, auth, field, value):
183         """
184         Add slices found in value list to (AddSliceToNode)
185         Delete slices not found in value list (DeleteSliceFromNode)
186         """
187         
188         from PLC.Slices import Slices
189         
190         assert 'slice_ids' in self
191         assert 'node_id' in self
192         assert isinstance(value, list)
193         
194         (slice_ids, slice_names) = self.separate_types(value)[0:2]
195
196         if slice_names:
197             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
198             slice_ids += slices.keys()
199
200         if self['slice_ids'] != slice_ids:
201             from PLC.Methods.AddSliceToNodes import AddSliceToNodes
202             from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
203             new_slices = set(slice_ids).difference(self['slice_ids'])
204             stale_slices = set(self['slice_ids']).difference(slice_ids)
205         
206         for new_slice in new_slices:
207             AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
208         for stale_slice in stale_slices:
209             DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])                         
210
211     def associate_slices_whitelist(self, auth, field, value):
212         """
213         Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
214         Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
215         """
216
217         from PLC.Slices import Slices
218
219         assert 'slice_ids_whitelist' in self
220         assert 'node_id' in self
221         assert isinstance(value, list)
222
223         (slice_ids, slice_names) = self.separate_types(value)[0:2]
224
225         if slice_names:
226             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
227             slice_ids += slices.keys()
228
229         if self['slice_ids_whitelist'] != slice_ids:
230             from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
231             from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
232             new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
233             stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
234
235         for new_slice in new_slices:
236             AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
237         for stale_slice in stale_slices:
238             DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']]) 
239                 
240
241     def delete(self, commit = True):
242         """
243         Delete existing node.
244         """
245
246         assert 'node_id' in self
247         assert 'interface_ids' in self
248
249         # we need to clean up InterfaceTags, so handling interfaces as part of join_tables does not work
250         for interface in Interfaces(self.api,self['interface_ids']):
251             interface.delete()
252
253         # Clean up miscellaneous join tables
254         for table in self.join_tables:
255             self.api.db.do("DELETE FROM %s WHERE node_id = %d" % \
256                            (table, self['node_id']))
257
258         # Mark as deleted
259         self['deleted'] = True
260         self.sync(commit)
261
262
263 class Nodes(Table):
264     """
265     Representation of row(s) from the nodes table in the
266     database.
267     """
268
269     def __init__(self, api, node_filter = None, columns = None):
270         Table.__init__(self, api, Node, columns)
271
272         # the view that we're selecting upon: start with view_nodes
273         view = "view_nodes"
274         # as many left joins as requested tags
275         for tagname in self.tag_columns:
276             view= "%s left join %s using (%s)"%(view,Node.tagvalue_view_name(tagname),Node.primary_key)
277             
278         sql = "SELECT %s FROM %s WHERE deleted IS False" % \
279               (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
280
281         if node_filter is not None:
282             if isinstance(node_filter, (list, tuple, set)):
283                 # Separate the list into integers and strings
284                 ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
285                 strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
286                 node_filter = Filter(Node.fields, {'node_id': ints, 'hostname': strs})
287                 sql += " AND (%s) %s" % node_filter.sql(api, "OR")
288             elif isinstance(node_filter, dict):
289                 node_filter = Filter(Node.fields, node_filter)
290                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
291             elif isinstance (node_filter, StringTypes):
292                 node_filter = Filter(Node.fields, {'hostname':[node_filter]})
293                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
294             elif isinstance (node_filter, int):
295                 node_filter = Filter(Node.fields, {'node_id':[node_filter]})
296                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
297             else:
298                 raise PLCInvalidArgument, "Wrong node filter %r"%node_filter
299
300         self.selectall(sql)