added create_network(), delete_network(), create_subnet(), delete_subnet(), process_t...
[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 from datetime import datetime
8 from types import StringTypes
9 import re
10
11 from PLC.Faults import *
12 from PLC.Parameter import Parameter, Mixed
13 from PLC.Filter import Filter
14 from PLC.Debug import profile
15 from PLC.Storage.AlchemyObject import AlchemyObj
16 from PLC.NodeTypes import NodeTypes
17 from PLC.BootStates import BootStates
18 from PLC.Interfaces import Interface, Interfaces
19 from PLC.ConfFileNodes import ConfFileNode
20 from PLC.SliceNodes import SliceNode
21 from PLC.SliceNodeWhitelists import SliceNodeWhitelist
22 from PLC.PCUNodes import PCUNode
23 from PLC.PCUNodePorts import PCUNodePort
24 from PLC.NodeTags import NodeTag
25 from PLC.NodeNodeGroups import NodeNodeGroup 
26
27 def valid_hostname(hostname):
28     # 1. Each part begins and ends with a letter or number.
29     # 2. Each part except the last can contain letters, numbers, or hyphens.
30     # 3. Each part is between 1 and 64 characters, including the trailing dot.
31     # 4. At least two parts.
32     # 5. Last part can only contain between 2 and 6 letters.
33     good_hostname = r'^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+' \
34                     r'[a-z]{2,6}$'
35     return hostname and \
36            re.match(good_hostname, hostname, re.IGNORECASE)
37
38 class Node(AlchemyObj):
39     """
40     Representation of a row in the nodes table. To use, optionally
41     instantiate with a dict of values. Update as you would a
42     dict. Commit to the database with sync().
43     """
44
45     tablename = 'nodes'
46     join_tables = [ 'slice_node', 'peer_node', 'slice_tag',
47                     'node_session', 'node_slice_whitelist',
48                     'node_tag', 'conf_file_node', 'pcu_node', 'leases', ]
49     fields = {
50         'node_id': Parameter(int, "Node identifier", primary_key=True),
51         'node_type': Parameter(str,"Node type",max=20),
52         'hostname': Parameter(str, "Fully qualified hostname", max = 255),
53         'site_id': Parameter(int, "Site at which this node is located"),
54         'boot_state': Parameter(str, "Boot state", max = 20, nullok=True),
55         'run_level': Parameter(str, "Run level", max = 20, nullok=True),
56         'model': Parameter(str, "Make and model of the actual machine", max = 255, nullok = True),
57         'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot", max = 128, nullok=True),
58         'version': Parameter(str, "Apparent Boot CD version", max = 64, nullok=True),
59         'ssh_rsa_key': Parameter(str, "Last known SSH host key", max = 1024, nullok=True),
60         'date_created': Parameter(datetime, "Date and time when node entry was created", ro = True),
61         'last_updated': Parameter(datetime, "Date and time when node entry was created", ro = True),
62         'last_contact': Parameter(datetime, "Date and time when node last contacted plc", ro = True, nullok=True),
63         'last_boot': Parameter(datetime, "Date and time when node last booted", ro = True, nullok=True),
64         'last_download': Parameter(datetime, "Date and time when node boot image was created", ro = True, nullok=True),
65         'last_pcu_reboot': Parameter(datetime, "Date and time when PCU reboot was attempted", ro = True, nullok=True),
66         'last_pcu_confirmation': Parameter(datetime, "Date and time when PCU reboot was confirmed", ro = True, nullok=True),
67         'last_time_spent_online': Parameter(datetime, "Length of time the node was last online before shutdown/failure", ro = True, nullok=True),
68         'last_time_spent_offline': Parameter(datetime, "Length of time the node was last offline after failure and before reboot", ro = True, nullok=True),
69         'verified': Parameter(bool, "Whether the node configuration is verified correct", ro=False, nullok=True),
70         'key': Parameter(str, "(Admin only) Node key", max = 256, nullok=True),
71         'session': Parameter(str, "(Admin only) Node session value", max = 256, ro = True, nullok=True),
72         'interface_ids': Parameter([int], "List of network interfaces that this node has", joined=True),
73         'conf_file_ids': Parameter([int], "List of configuration files specific to this node", joined=True),
74         # 'root_person_ids': Parameter([int], "(Admin only) List of people who have root access to this node"),
75         'slice_ids': Parameter([int], "List of slices on this node", joined=True),
76         'slice_ids_whitelist': Parameter([int], "List of slices allowed on this node", joined=True),
77         'pcu_ids': Parameter([int], "List of PCUs that control this node", joined=True),
78         'ports': Parameter([int], "List of PCU ports that this node is connected to", joined=True),
79         'peer_id': Parameter(int, "Peer to which this node belongs", nullok = True),
80         'peer_node_id': Parameter(int, "Foreign node identifier at peer", nullok = True),
81         'node_tag_ids' : Parameter ([int], "List of tags attached to this node", joined=True),
82         'nodegroup_ids': Parameter([int], "List of node groups that this node is in", joined=True),
83         }
84     tags = { }
85
86     def validate_hostname(self, hostname):
87         hostname = hostname.lower()
88         if not valid_hostname(hostname):
89             raise PLCInvalidArgument, "Invalid hostname"
90
91         conflicts = Node().select(filter={'hostname': hostname})
92         for node in conflicts:
93             if 'node_id' not in self or self['node_id'] != node['node_id']:
94                 raise PLCInvalidArgument, "Hostname already in use"
95
96         return hostname
97
98     def validate_node_type(self, node_type):
99         # Make sure node type does not alredy exist
100         conflicts = NodeTypes(self.api, [node_type])
101         if not conflicts:
102             raise PLCInvalidArgument, "Invalid node_type"
103         return node_type
104
105     def validate_boot_state(self, boot_state):
106         boot_states = [row['boot_state'] for row in BootStates(self.api)]
107         if boot_state not in boot_states:
108             raise PLCInvalidArgument, "Invalid boot state %r"%boot_state
109         return boot_state
110
111     validate_date_created = AlchemyObj.validate_timestamp
112     validate_last_updated = AlchemyObj.validate_timestamp
113     validate_last_contact = AlchemyObj.validate_timestamp
114     validate_last_boot = AlchemyObj.validate_timestamp
115     validate_last_download = AlchemyObj.validate_timestamp
116     validate_last_pcu_reboot = AlchemyObj.validate_timestamp
117     validate_last_pcu_confirmation = AlchemyObj.validate_timestamp
118
119     def check_whitelist(self, slice, caller):
120         if self['slice_ids_whitelist'] and \
121            slice['slice_id'] not in self['slice_ids_whitelist'] and \
122            not set(caller['site_ids']).intersection([self['site_id']]):
123             raise PLCInvalidArgument, "%s is not allowed on %s (not on the whitelist)" % \
124               (slice['name'], self['hostname'])
125
126     def update_readonly_int(self, col_name, commit = True):
127
128         assert 'node_id' in self
129         assert self.table_name
130
131         self.api.db.do("UPDATE %s SET %s = %s" % (self.table_name, col_name, self[col_name]) + \
132                         " where node_id = %d" % (self['node_id']) )
133         self.sync(commit)
134
135     def update_timestamp(self, col_name, commit = True):
136         """
137         Update col_name field with current time
138         """
139         assert 'node_id' in self
140         self[col_name] = datetime.now()
141         fields = {
142             'node_id': self['node_id'],         
143             col_name: datetime.now()
144         } 
145         Node(self.api, fields).sync()
146         
147     def update_last_boot(self, commit = True):
148         self.update_timestamp('last_boot', commit)
149     def update_last_download(self, commit = True):
150         self.update_timestamp('last_download', commit)
151     def update_last_pcu_reboot(self, commit = True):
152         self.update_timestamp('last_pcu_reboot', commit)
153     def update_last_pcu_confirmation(self, commit = True):
154         self.update_timestamp('last_pcu_confirmation', commit)
155
156     def update_last_contact(self, commit = True):
157         self.update_timestamp('last_contact', commit)
158     def update_last_updated(self, commit = True):
159         self.update_timestamp('last_updated', commit)
160
161     def update_tags(self, tags):
162         from PLC.Shell import Shell
163         from PLC.NodeTags import NodeTags
164         from PLC.Methods.AddNodeTag import AddNodeTag
165         from PLC.Methods.UpdateNodeTag import UpdateNodeTag
166         shell = Shell()
167         for (tagname,value) in tags.iteritems():
168             # the tagtype instance is assumed to exist, just check that
169             if not TagTypes(self.api,{'tagname':tagname}):
170                 raise PLCInvalidArgument,"No such TagType %s"%tagname
171             node_tags=NodeTags(self.api,{'tagname':tagname,'node_id':node['node_id']})
172             if not node_tags:
173                 AddNodeTag(self.api).__call__(shell.auth,node['node_id'],tagname,value)
174             else:
175                 UpdateNodeTag(self.api).__call__(shell.auth,node_tags[0]['node_tag_id'],value)
176
177     def associate_interfaces(self, auth, field, value):
178         """
179         Delete interfaces not found in value list (using DeleteInterface)
180         Add interfaces found in value list (using AddInterface)
181         Updates interfaces found w/ interface_id in value list (using UpdateInterface)
182         """
183
184         assert 'interface_ids' in self
185         assert 'node_id' in self
186         assert isinstance(value, list)
187
188         (interface_ids, blank, interfaces) = self.separate_types(value)
189
190         if self['interface_ids'] != interface_ids:
191             from PLC.Methods.DeleteInterface import DeleteInterface
192
193             stale_interfaces = set(self['interface_ids']).difference(interface_ids)
194
195             for stale_interface in stale_interfaces:
196                 DeleteInterface.__call__(DeleteInterface(self.api), auth, stale_interface['interface_id'])
197
198     def associate_conf_files(self, auth, field, value):
199         """
200         Add conf_files found in value list (AddConfFileToNode)
201         Delets conf_files not found in value list (DeleteConfFileFromNode)
202         """
203
204         assert 'conf_file_ids' in self
205         assert 'node_id' in self
206         assert isinstance(value, list)
207
208         conf_file_ids = self.separate_types(value)[0]
209
210         if self['conf_file_ids'] != conf_file_ids:
211             from PLC.Methods.AddConfFileToNode import AddConfFileToNode
212             from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
213             new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
214             stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
215
216             for new_conf_file in new_conf_files:
217                 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
218             for stale_conf_file in stale_conf_files:
219                 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
220
221     def associate_slices(self, auth, field, value):
222         """
223         Add slices found in value list to (AddSliceToNode)
224         Delete slices not found in value list (DeleteSliceFromNode)
225         """
226
227         from PLC.Slices import Slices
228
229         assert 'slice_ids' in self
230         assert 'node_id' in self
231         assert isinstance(value, list)
232
233         (slice_ids, slice_names) = self.separate_types(value)[0:2]
234
235         if slice_names:
236             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
237             slice_ids += slices.keys()
238
239         if self['slice_ids'] != slice_ids:
240             from PLC.Methods.AddSliceToNodes import AddSliceToNodes
241             from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
242             new_slices = set(slice_ids).difference(self['slice_ids'])
243             stale_slices = set(self['slice_ids']).difference(slice_ids)
244
245         for new_slice in new_slices:
246             AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
247         for stale_slice in stale_slices:
248             DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])
249
250     def associate_slices_whitelist(self, auth, field, value):
251         """
252         Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
253         Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
254         """
255
256         from PLC.Slices import Slices
257
258         assert 'slice_ids_whitelist' in self
259         assert 'node_id' in self
260         assert isinstance(value, list)
261
262         (slice_ids, slice_names) = self.separate_types(value)[0:2]
263
264         if slice_names:
265             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
266             slice_ids += slices.keys()
267
268         if self['slice_ids_whitelist'] != slice_ids:
269             from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
270             from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
271             new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
272             stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
273
274         for new_slice in new_slices:
275             AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
276         for stale_slice in stale_slices:
277             DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']])
278
279
280
281     def sync(self, commit=True, validate=True):
282         AlchemyObj.sync(self, commit=commit, validate=validate)
283         ts = datetime.now() 
284         self['last_updated'] = ts 
285         if 'node_id' not in self:
286             self['date_created'] = ts
287             AlchemyObj.insert(self, dict(self))
288         else:
289             AlchemyObj.update(self, {'node_id': self['node_id']}, dict(self))
290
291     def delete(self, commit = True):
292         """
293         Delete existing node.
294         """
295
296         assert 'node_id' in self
297         assert 'interface_ids' in self
298         Interface().delete(filter={'interface_id': self['interface_ids']})
299         AlchemyObj.delete(self, dict(self))
300
301 class Nodes(list):
302     """
303     Representation of row(s) from the nodes table in the
304     database.
305     """
306
307     def __init__(self, api, node_filter = None, columns = None):
308         self.api = api 
309         self.refresh(api)
310         # as many left joins as requested tags
311         if not node_filter:
312             nodes = Node().select()
313         elif isinstance(node_filter, (list, tuple, set)):
314             # Separate the list into integers and strings
315             ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
316             strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
317             node_filter = {}
318             if ints: node_filter['node_id'] = ints 
319             if strs: node_filter['hostname'] = strs    
320             nodes = Node().select(filter=node_filter)
321         elif isinstance(node_filter, dict):
322             nodes = Node().select(filter=node_filter)
323         elif isinstance (node_filter, StringTypes):
324             nodes = Node().select(filter={'hostname': strs})
325         elif isinstance (node_filter, (int, long)):
326             nodes = Node().select(filter={'node_id': ints})
327         else:
328             raise PLCInvalidArgument, "Wrong node filter %r"%node_filter
329
330         for node in nodes:
331             node = Node(api, object=node, columns=columns)
332             if not columns or 'interface_ids' in columns:
333                 interfaces = Interface().select(filter={'node_id': node['node_id']})
334                 node['interface_ids'] = [rec.interface_id for rec in interfaces]
335             if not columns or 'conf_file_ids' in columns:
336                 conf_files = ConfFileNode().select(filter={'node_id': node['node_id']})
337                 node['conf_file_ids'] = [rec.conf_file_id for rec in conf_files]
338             if not columns or 'slice_ids' in columns:
339                 slice_nodes = SliceNode().select(filter={'node_id': node['node_id']})
340                 node['slice_ids'] = [rec.slice_id for rec in slice_nodes]            
341             if not columns or 'slice_ids_whitelist' in columns:
342                 slice_whitelist = SliceNodeWhitelist().select(filter={'node_id': node['node_id']})
343                 node['slice_ids_whitelist'] = [rec.slice_id for rec in slice_whitelist]
344             if not columns or 'pcu_ids' in columns:
345                 pcus = PCUNode().select(filter={'node_id': node['node_id']})
346                 node['pcu_ids'] = [rec.pcu_id for rec in pcus]
347             if not columns or 'pcu_ports' in columns:
348                 pcu_ports = PCUNodePort().select(filter={'node_id': node['node_id']})
349                 node['pcu_ports'] = [rec.port for rec in pcu_ports]             
350             if not columns or 'node_tag_ids' in columns:
351                 node_tags = NodeTag().select(filter={'node_id': node['node_id']})
352                 node['node_tag_ids'] = [rec.node_tag_id for rec in node_tags]
353             if not columns or 'nodegroup_ids' in columns:
354                 nodegroups = NodeNodeGroup().select(filter={'node_id': node['node_id']})
355                 node['nodegroup_ids'] = [rec.nodegroup_id for rec in nodegroups] 
356             self.append(node)
357
358     def refresh(self, api):
359         """
360         Import node records from nova.
361         """
362         from PLC.Sites import Sites
363         default_site = Sites(api, site_filter={'login_base': 'default'})[0]
364         # get current list of compute nodes
365         hypervisors = api.client_shell.nova.hypervisors.list()
366         compute_hosts = [h.hypervisor_hostname for h in hypervisors]
367  
368         nodes = Node().select()
369         hostnames = [node.hostname for node in nodes]
370         
371         added_nodes = set(compute_hosts).difference(hostnames)
372         for added_node in added_nodes:
373             node = Node(api, {'hostname': added_node,
374                               'node_type': 'regular', 
375                               'site_id': default_site['site_id']})
376             node.sync()
377                    
378         
379