Fix version output when missing.
[plcapi.git] / PLC / Interfaces.py
1 #
2 # Functions for interacting with the interfaces 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 socket
13 import struct
14
15 from PLC.Faults import *
16 from PLC.Parameter import Parameter
17 from PLC.Filter import Filter
18 from PLC.Debug import profile
19 from PLC.Table import Row, Table
20 from PLC.NetworkTypes import NetworkType, NetworkTypes
21 from PLC.NetworkMethods import NetworkMethod, NetworkMethods
22 import PLC.Nodes
23
24 def valid_ip(ip):
25     try:
26         ip = socket.inet_ntoa(socket.inet_aton(ip))
27         return True
28     except socket.error:
29         return False
30
31 def in_same_network(address1, address2, netmask):
32     """
33     Returns True if two IPv4 addresses are in the same network. Faults
34     if an address is invalid.
35     """
36
37     address1 = struct.unpack('>L', socket.inet_aton(address1))[0]
38     address2 = struct.unpack('>L', socket.inet_aton(address2))[0]
39     netmask = struct.unpack('>L', socket.inet_aton(netmask))[0]
40
41     return (address1 & netmask) == (address2 & netmask)
42
43 class Interface(Row):
44     """
45     Representation of a row in the interfaces table. To use, optionally
46     instantiate with a dict of values. Update as you would a
47     dict. Commit to the database with sync().
48     """
49
50     table_name = 'interfaces'
51     primary_key = 'interface_id'
52     join_tables = ['interface_tag']
53     fields = {
54         'interface_id': Parameter(int, "Node interface identifier"),
55         'method': Parameter(str, "Addressing method (e.g., 'static' or 'dhcp')"),
56         'type': Parameter(str, "Address type (e.g., 'ipv4')"),
57         'ip': Parameter(str, "IP address", nullok = True),
58         'mac': Parameter(str, "MAC address", nullok = True),
59         'gateway': Parameter(str, "IP address of primary gateway", nullok = True),
60         'network': Parameter(str, "Subnet address", nullok = True),
61         'broadcast': Parameter(str, "Network broadcast address", nullok = True),
62         'netmask': Parameter(str, "Subnet mask", nullok = True),
63         'dns1': Parameter(str, "IP address of primary DNS server", nullok = True),
64         'dns2': Parameter(str, "IP address of secondary DNS server", nullok = True),
65         'bwlimit': Parameter(int, "Bandwidth limit", min = 0, nullok = True),
66         'hostname': Parameter(str, "(Optional) Hostname", nullok = True),
67         'node_id': Parameter(int, "Node associated with this interface"),
68         'is_primary': Parameter(bool, "Is the primary interface for this node"),
69         'interface_tag_ids' : Parameter([int], "List of interface settings"),
70         'last_updated': Parameter(int, "Date and time when node entry was created", ro = True),
71         }
72
73     view_tags_name = "view_interface_tags"
74     tags = {}
75
76     def validate_method(self, method):
77         network_methods = [row['method'] for row in NetworkMethods(self.api)]
78         if method not in network_methods:
79             raise PLCInvalidArgument, "Invalid addressing method %s"%method
80         return method
81
82     def validate_type(self, type):
83         network_types = [row['type'] for row in NetworkTypes(self.api)]
84         if type not in network_types:
85             raise PLCInvalidArgument, "Invalid address type %s"%type
86         return type
87
88     def validate_ip(self, ip):
89         if ip and not valid_ip(ip):
90             raise PLCInvalidArgument, "Invalid IP address %s"%ip
91         return ip
92
93     def validate_mac(self, mac):
94         if not mac:
95             return mac
96
97         try:
98             bytes = mac.split(":")
99             if len(bytes) < 6:
100                 raise Exception
101             for i, byte in enumerate(bytes):
102                 byte = int(byte, 16)
103                 if byte < 0 or byte > 255:
104                     raise Exception
105                 bytes[i] = "%02x" % byte
106             mac = ":".join(bytes)
107         except:
108             raise PLCInvalidArgument, "Invalid MAC address %s"%mac
109
110         return mac
111
112     validate_gateway = validate_ip
113     validate_network = validate_ip
114     validate_broadcast = validate_ip
115     validate_netmask = validate_ip
116     validate_dns1 = validate_ip
117     validate_dns2 = validate_ip
118
119     def validate_bwlimit(self, bwlimit):
120         if not bwlimit:
121             return bwlimit
122
123         if bwlimit < 500000:
124             raise PLCInvalidArgument, 'Minimum bw is 500 kbs'
125
126         return bwlimit
127
128     def validate_hostname(self, hostname):
129         # Optional
130         if not hostname:
131             return hostname
132
133         if not PLC.Nodes.valid_hostname(hostname):
134             raise PLCInvalidArgument, "Invalid hostname %s"%hostname
135
136         return hostname
137
138     def validate_node_id(self, node_id):
139         nodes = PLC.Nodes.Nodes(self.api, [node_id])
140         if not nodes:
141             raise PLCInvalidArgument, "No such node %d"%node_id
142
143         return node_id
144
145     def validate_is_primary(self, is_primary):
146         """
147         Set this interface to be the primary one.
148         """
149
150         if is_primary:
151             nodes = PLC.Nodes.Nodes(self.api, [self['node_id']])
152             if not nodes:
153                 raise PLCInvalidArgument, "No such node %d"%node_id
154             node = nodes[0]
155
156             if node['interface_ids']:
157                 conflicts = Interfaces(self.api, node['interface_ids'])
158                 for interface in conflicts:
159                     if ('interface_id' not in self or \
160                         self['interface_id'] != interface['interface_id']) and \
161                        interface['is_primary']:
162                         raise PLCInvalidArgument, "Can only set one primary interface per node"
163
164         return is_primary
165
166     def validate(self):
167         """
168         Flush changes back to the database.
169         """
170
171         # Basic validation
172         Row.validate(self)
173
174         assert 'method' in self
175         method = self['method']
176
177         if method == "proxy" or method == "tap":
178             if 'mac' in self and self['mac']:
179                 raise PLCInvalidArgument, "For %s method, mac should not be specified" % method
180             if 'ip' not in self or not self['ip']:
181                 raise PLCInvalidArgument, "For %s method, ip is required" % method
182             if method == "tap" and ('gateway' not in self or not self['gateway']):
183                 raise PLCInvalidArgument, "For tap method, gateway is required and should be " \
184                       "the IP address of the node that proxies for this address"
185             # Should check that the proxy address is reachable, but
186             # there's no way to tell if the only primary interface is
187             # DHCP!
188
189         elif method == "static":
190             if 'is_primary' in self and self['is_primary'] is True:
191                 for key in ['gateway', 'dns1']:
192                     if key not in self or not self[key]:
193                         raise PLCInvalidArgument, "For static method primary network, %s is required" % key
194                     globals()[key] = self[key]
195             for key in ['ip', 'network', 'broadcast', 'netmask']:
196                 if key not in self or not self[key]:
197                     raise PLCInvalidArgument, "For static method, %s is required" % key
198                 globals()[key] = self[key]
199             if not in_same_network(ip, network, netmask):
200                 raise PLCInvalidArgument, "IP address %s is inconsistent with network %s/%s" % \
201                       (ip, network, netmask)
202             if not in_same_network(broadcast, network, netmask):
203                 raise PLCInvalidArgument, "Broadcast address %s is inconsistent with network %s/%s" % \
204                       (broadcast, network, netmask)
205             if 'gateway' in globals() and not in_same_network(ip, gateway, netmask):
206                 raise PLCInvalidArgument, "Gateway %s is not reachable from %s/%s" % \
207                       (gateway, ip, netmask)
208
209         elif method == "ipmi":
210             if 'ip' not in self or not self['ip']:
211                 raise PLCInvalidArgument, "For ipmi method, ip is required"
212
213     validate_last_updated = Row.validate_timestamp
214
215     def update_timestamp(self, col_name, commit = True):
216         """
217         Update col_name field with current time
218         """
219
220         assert 'interface_id' in self
221         assert self.table_name
222
223         self.api.db.do("UPDATE %s SET %s = CURRENT_TIMESTAMP " % (self.table_name, col_name) + \
224                        " where interface_id = %d" % (self['interface_id']) )
225         self.sync(commit)
226
227     def update_last_updated(self, commit = True):
228         self.update_timestamp('last_updated', commit)
229
230
231 class Interfaces(Table):
232     """
233     Representation of row(s) from the interfaces table in the
234     database.
235     """
236
237     def __init__(self, api, interface_filter = None, columns = None):
238         Table.__init__(self, api, Interface, columns)
239
240         # the view that we're selecting upon: start with view_nodes
241         view = "view_interfaces"
242         # as many left joins as requested tags
243         for tagname in self.tag_columns:
244             view= "%s left join %s using (%s)"%(view,Interface.tagvalue_view_name(tagname),
245                                                 Interface.primary_key)
246
247         sql = "SELECT %s FROM %s WHERE True" % \
248             (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
249
250         if interface_filter is not None:
251             if isinstance(interface_filter, (list, tuple, set)):
252                 # Separate the list into integers and strings
253                 ints = filter(lambda x: isinstance(x, (int, long)), interface_filter)
254                 strs = filter(lambda x: isinstance(x, StringTypes), interface_filter)
255                 interface_filter = Filter(Interface.fields, {'interface_id': ints, 'ip': strs})
256                 sql += " AND (%s) %s" % interface_filter.sql(api, "OR")
257             elif isinstance(interface_filter, dict):
258                 allowed_fields=dict(Interface.fields.items()+Interface.tags.items())
259                 interface_filter = Filter(allowed_fields, interface_filter)
260                 sql += " AND (%s) %s" % interface_filter.sql(api)
261             elif isinstance(interface_filter, int):
262                 interface_filter = Filter(Interface.fields, {'interface_id': [interface_filter]})
263                 sql += " AND (%s) %s" % interface_filter.sql(api)
264             elif isinstance (interface_filter, StringTypes):
265                 interface_filter = Filter(Interface.fields, {'ip':[interface_filter]})
266                 sql += " AND (%s) %s" % interface_filter.sql(api, "AND")
267             else:
268                 raise PLCInvalidArgument, "Wrong interface filter %r"%interface_filter
269
270         self.selectall(sql)