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