Merge remote-tracking branch 'origin/pycurl' into planetlab-4_0-branch
[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: Nodes.py 5654 2007-11-06 03:43:55Z tmack $
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.NodeNetworks import NodeNetwork, NodeNetworks
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     # Thierry -- we use delete on nodenetworks so the related NodeNetworkSettings get deleted too
42     join_tables = ['nodegroup_node', 'conf_file_node', 'pcu_node', 'slice_node', 'slice_attribute', 'node_session', 'peer_node','node_slice_whitelist']
43     fields = {
44         'node_id': Parameter(int, "Node identifier"),
45         'hostname': Parameter(str, "Fully qualified hostname", max = 255),
46         'site_id': Parameter(int, "Site at which this node is located"),
47         'boot_state': Parameter(str, "Boot state", max = 20),
48         'model': Parameter(str, "Make and model of the actual machine", max = 255, nullok = True),
49         'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot", max = 128),
50         'version': Parameter(str, "Apparent Boot CD version", max = 64),
51         'ssh_rsa_key': Parameter(str, "Last known SSH host key", max = 1024),
52         'date_created': Parameter(int, "Date and time when node entry was created", ro = True),
53         'last_updated': Parameter(int, "Date and time when node entry was created", ro = True),
54         'last_contact': Parameter(int, "Date and time when node last contacted plc", ro = True), 
55         'key': Parameter(str, "(Admin only) Node key", max = 256),
56         'session': Parameter(str, "(Admin only) Node session value", max = 256, ro = True),
57         'nodenetwork_ids': Parameter([int], "List of network interfaces that this node has"),
58         'nodegroup_ids': Parameter([int], "List of node groups that this node is in"),
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         }
68     related_fields = {
69         'nodenetworks': [Mixed(Parameter(int, "NodeNetwork identifier"),
70                                Filter(NodeNetwork.fields))],
71         'nodegroups': [Mixed(Parameter(int, "NodeGroup identifier"),
72                              Parameter(str, "NodeGroup name"))],
73         'conf_files': [Parameter(int, "ConfFile identifier")],
74         'slices': [Mixed(Parameter(int, "Slice identifier"),
75                          Parameter(str, "Slice name"))],
76         'slices_whitelist': [Mixed(Parameter(int, "Slice identifier"),
77                                    Parameter(str, "Slice name"))]
78         }
79     # for Cache
80     class_key = 'hostname'
81     foreign_fields = ['boot_state','model','version']
82     # forget about these ones, they are read-only anyway
83     # handling them causes Cache to re-sync all over again 
84     # 'date_created','last_updated'
85     foreign_xrefs = [
86         # in this case, we dont need the 'table' but Cache will look it up, so...
87         {'field' : 'site_id' , 'class' : 'Site' , 'table' : 'unused-on-direct-refs' } ,
88         ]
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_boot_state(self, boot_state):
102         boot_states = [row['boot_state'] for row in BootStates(self.api)]
103         if boot_state not in boot_states:
104             raise PLCInvalidArgument, "Invalid boot state"
105
106         return boot_state
107
108     validate_date_created = Row.validate_timestamp
109     validate_last_updated = Row.validate_timestamp
110     validate_last_contact = Row.validate_timestamp
111
112     def update_last_contact(self, commit = True):
113         """
114         Update last_contact field with current time
115         """
116         
117         assert 'node_id' in self
118         assert self.table_name
119
120         self.api.db.do("UPDATE %s SET last_contact = CURRENT_TIMESTAMP " % (self.table_name) + \
121                        " where node_id = %d" % ( self['node_id']) )
122         self.sync(commit)
123
124
125     def update_last_updated(self, commit = True):
126         """
127         Update last_updated field with current time
128         """
129
130         assert 'node_id' in self
131         assert self.table_name
132
133         self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
134                        " where node_id = %d" % (self['node_id']) )
135         self.sync(commit)
136
137     def associate_nodenetworks(self, auth, field, value):
138         """
139         Delete nodenetworks not found in value list (using DeleteNodeNetwor)k   
140         Add nodenetworks found in value list (using AddNodeNetwork)
141         Updates nodenetworks found w/ nodenetwork_id in value list (using UpdateNodeNetwork) 
142         """
143
144         assert 'nodenetworkp_ids' in self
145         assert 'node_id' in self
146         assert isinstance(value, list)
147
148         (nodenetwork_ids, blank, nodenetworks) = self.separate_types(value)
149
150         if self['nodenetwork_ids'] != nodenetwork_ids:
151             from PLC.Methods.DeleteNodeNetwork import DeleteNodeNetwork
152
153             stale_nodenetworks = set(self['nodenetwork_ids']).difference(nodenetwork_ids)
154
155             for stale_nodenetwork in stale_nodenetworks:
156                 DeleteNodeNetwork.__call__(DeleteNodeNetwork(self.api), auth, stale_nodenetwork['nodenetwork_id'])
157
158     def associate_nodegroups(self, auth, field, value):
159         """
160         Add node to nodegroups found in value list (AddNodeToNodegroup)
161         Delete node from nodegroup not found in value list (DeleteNodeFromNodegroup)
162         """
163         
164         from PLC.NodeGroups import NodeGroups
165         
166         assert 'nodegroup_ids' in self
167         assert 'node_id' in self
168         assert isinstance(value, list)
169
170         (nodegroup_ids, nodegroup_names) = self.separate_types(value)[0:2]
171         
172         if nodegroup_names:
173             nodegroups = NodeGroups(self.api, nodegroup_names, ['nodegroup_id']).dict('nodegroup_id')
174             nodegroup_ids += nodegroups.keys()
175
176         if self['nodegroup_ids'] != nodegroup_ids:
177             from PLC.Methods.AddNodeToNodeGroup import AddNodeToNodeGroup
178             from PLC.Methods.DeleteNodeFromNodeGroup import DeleteNodeFromNodeGroup
179         
180             new_nodegroups = set(nodegroup_ids).difference(self['nodegroup_ids'])
181             stale_nodegroups = set(self['nodegroup_ids']).difference(nodegroup_ids)
182         
183             for new_nodegroup in new_nodegroups:
184                 AddNodeToNodeGroup.__call__(AddNodeToNodeGroup(self.api), auth, self['node_id'], new_nodegroup)
185             for stale_nodegroup in stale_nodegroups:
186                 DeleteNodeFromNodeGroup.__call__(DeleteNodeFromNodeGroup(self.api), auth, self['node_id'], stale_nodegroup)
187           
188
189  
190     def associate_conf_files(self, auth, field, value):
191         """
192         Add conf_files found in value list (AddConfFileToNode)
193         Delets conf_files not found in value list (DeleteConfFileFromNode)
194         """
195         
196         assert 'conf_file_ids' in self
197         assert 'node_id' in self
198         assert isinstance(value, list)
199         
200         conf_file_ids = self.separate_types(value)[0]
201         
202         if self['conf_file_ids'] != conf_file_ids:
203             from PLC.Methods.AddConfFileToNode import AddConfFileToNode
204             from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
205             new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
206             stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
207         
208             for new_conf_file in new_conf_files:
209                 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
210             for stale_conf_file in stale_conf_files:
211                 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
212
213  
214     def associate_slices(self, auth, field, value):
215         """
216         Add slices found in value list to (AddSliceToNode)
217         Delete slices not found in value list (DeleteSliceFromNode)
218         """
219         
220         from PLC.Slices import Slices
221         
222         assert 'slice_ids' 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'] != slice_ids:
233             from PLC.Methods.AddSliceToNodes import AddSliceToNodes
234             from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
235             new_slices = set(slice_ids).difference(self['slice_ids'])
236             stale_slices = set(self['slice_ids']).difference(slice_ids)
237         
238         for new_slice in new_slices:
239             AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
240         for stale_slice in stale_slices:
241             DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])                         
242
243     def associate_slices_whitelist(self, auth, field, value):
244         """
245         Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
246         Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
247         """
248
249         from PLC.Slices import Slices
250
251         assert 'slice_ids_whitelist' in self
252         assert 'node_id' in self
253         assert isinstance(value, list)
254
255         (slice_ids, slice_names) = self.separate_types(value)[0:2]
256
257         if slice_names:
258             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
259             slice_ids += slices.keys()
260
261         if self['slice_ids_whitelist'] != slice_ids:
262             from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
263             from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
264             new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
265             stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
266
267         for new_slice in new_slices:
268             AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
269         for stale_slice in stale_slices:
270             DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']]) 
271                 
272
273     def delete(self, commit = True):
274         """
275         Delete existing node.
276         """
277
278         assert 'node_id' in self
279         assert 'nodenetwork_ids' in self
280
281         # we need to clean up NodeNetworkSettings, so handling nodenetworks as part of join_tables does not work
282         for nodenetwork in NodeNetworks(self.api,self['nodenetwork_ids']):
283             nodenetwork.delete()
284
285         # Clean up miscellaneous join tables
286         for table in self.join_tables:
287             self.api.db.do("DELETE FROM %s WHERE node_id = %d" % \
288                            (table, self['node_id']))
289
290         # Mark as deleted
291         self['deleted'] = True
292         self.sync(commit)
293
294
295 class Nodes(Table):
296     """
297     Representation of row(s) from the nodes table in the
298     database.
299     """
300
301     def __init__(self, api, node_filter = None, columns = None):
302         Table.__init__(self, api, Node, columns)
303
304         sql = "SELECT %s FROM view_nodes WHERE deleted IS False" % \
305               ", ".join(self.columns)
306
307         if node_filter is not None:
308             if isinstance(node_filter, (list, tuple, set)):
309                 # Separate the list into integers and strings
310                 ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
311                 strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
312                 node_filter = Filter(Node.fields, {'node_id': ints, 'hostname': strs})
313                 sql += " AND (%s) %s" % node_filter.sql(api, "OR")
314             elif isinstance(node_filter, dict):
315                 node_filter = Filter(Node.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)