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