c1ea7dbcbd4ea12a6963b1d253b77c43d93c3f7b
[affinity.git] / scripts / analytics.py
1 #!/usr/local/bin/python
2
3 import httplib2
4 import json
5 import sys
6 import time
7
8 # 1. Start the controller
9 # 2. On the local machine (e.g., your laptop), start this script.
10 #    > python analytics.py
11 # 3. On the mininet VM, run:
12 #    > sudo mn --controller=remote,ip=192.168.56.1 --topo tree,2
13 #    > h1 ping h3
14 # 4. Give commands to analytics.py.  For instance:
15 #    > host bytes 10.0.0.1 10.0.0.3
16 #   (There is a usage prompt that prints at the beginning of analytics.py)
17 # 5. Type 'quit' to exit analytics.py
18
19
20 '''
21 Class for keeping track of host stats or affinity link stats, depending.
22 '''
23 class Stats:
24
25     # TODO: Each stat should probably be a thread, and handle its
26     # own output and refreshing for the EWMA
27
28     def __init__(self, stat_type, **kwargs):
29         self.stat_type = stat_type
30         if stat_type == "host":
31             self.src = kwargs['src']
32             self.dst = kwargs['dst']
33             self.url_prefix = "http://localhost:8080/affinity/nb/v2/analytics/default/hoststats/" 
34         elif stat_type == "affinityLink":
35             self.al = kwargs['al']
36             self.url_prefix = "http://localhost:8080/affinity/nb/v2/analytics/default/affinitylinkstats/"
37         else:
38             print "incorrect stat type", stat_type
39
40         self.stats = {}
41         self.rate_ewma = None
42
43         self.http = httplib2.Http(".cache")
44         self.http.add_credentials('admin', 'admin')
45         self.refresh()
46
47     # Refresh statistics
48     def refresh(self):
49         if (self.stat_type == "host"):
50             resp, content = self.http.request(self.url_prefix + self.src + "/" + self.dst, "GET")
51         elif (self.stat_type == "affinityLink"):
52             resp, content = self.http.request(self.url_prefix + self.al, "GET")
53         self.stats = json.loads(content)
54         self.handle_rate_ewma()
55
56     # EWMA calculation for bit rate
57     def handle_rate_ewma(self):
58         alpha = .25
59         anomaly_threshold = 2.0
60         new_bitrate = self.get_bit_rate()
61
62         if self.rate_ewma == None:
63             self.rate_ewma = new_bitrate
64         else:
65             new_rate_ewma = alpha * new_bitrate + (1 - alpha) * self.rate_ewma
66             if (self.rate_ewma > 0 and new_rate_ewma > anomaly_threshold * self.rate_ewma):
67                 if (self.stat_type == "host"):
68                     print "!! Anomaly detected between %s and %s" % (self.src, self.dst)
69                 elif (self.stat_type == "affinityLink"):
70                     print "!! Anomaly detected on AffinityLink %s" % (self.al)
71                 print "!! Rate rose from %1.1f Mbit/s to %1.1f Mbit/s" % ((self.rate_ewma/10**6), (new_rate_ewma/10**6))
72             self.rate_ewma = new_rate_ewma
73
74     # Bytes
75     def get_bytes(self):
76         try:
77             bytes = long(self.stats["byteCount"])
78         except Exception as e:
79             bytes = 0
80         return bytes
81
82     # Bit Rate
83     def get_bit_rate(self):
84         try:
85             bitrate = float(self.stats["bitRate"])
86         except Exception as e:
87             bitrate = 0.0
88         return bitrate
89
90
91 class AffinityControl:
92
93     def __init__(self):
94         self.http = httplib2.Http(".cache")
95         self.http.add_credentials("admin", "admin")
96         self.url_prefix = "http://localhost:8080/affinity/nb/v2/affinity/default/"
97         self.groups = []
98         self.links = []        
99
100     def add_affinity_group(self, group_name, ips):
101         resp, content = self.http.request(self.url_prefix + "create/group/%s" % group_name, "PUT")
102         if (resp.status != 201):
103             print "AffinityGroup %s could not be created" % group_name
104             return
105         for ip in ips:
106             resp, content = self.http.request(self.url_prefix + "group/%s/add/ip/%s" % (group_name, ip), "PUT")
107             if (resp.status != 201):
108                 print "IP %s could not be added to AffinityGroup %s" % (ip, group_name)
109                 return
110         self.groups.append(group_name)
111         print "AffinityGroup %s added successfully. IPs are %s" % (group_name, ips)
112
113
114     def add_affinity_link(self, link_name, src_group, dst_group):
115         resp, content = self.http.request(self.url_prefix + "create/link/%s/from/%s/to/%s" % (link_name, src_group, dst_group), "PUT")
116         if (resp.status != 201):
117             print "AffinityLink %s could not be added between %s and %s" % (link_name, src_group, dst_group)
118             return
119         self.links.append(link_name)
120         print "AffinityLink %s added between %s and %s" % (link_name, src_group, dst_group)
121
122
123 '''
124 Class for controlling subnets.  Right now, just adds subnets and
125 checks whether they exist, because that's all we need.
126 '''
127 class SubnetControl:
128
129     def __init__(self):
130         self.http = httplib2.Http(".cache")
131         self.http.add_credentials("admin", "admin")
132         self.url_prefix = "http://localhost:8080/controller/nb/v2/subnetservice/default/"
133
134     # Checks whether subnet exists.  Checks against the actual subnet
135     # string (e.g., "10.0.0.255/1"), not the subnet name.  Will not
136     # catch things like overlapping subnets.
137     def exists(self, subnet):
138         resp, content = self.http.request(self.url_prefix + "subnets", "GET")
139         if (resp.status != 200):
140             print "Fatal error - can't check for subnet existence"
141             sys.exit(-1)
142         data = json.loads(content)
143
144         for key in data["subnetConfig"]:
145             if (key["subnet"] == subnet):
146                 return True
147         return False
148
149     # Add a subnet if it doesn't already exist.
150     def add_subnet(self, subnet_name, subnet):
151         if (self.exists(subnet)):
152             print "subnet", subnet, "already exists"
153             return
154         subnet_config = dict(name=subnet_name, subnet=subnet)
155         json_data = json.dumps(subnet_config)
156         resp, content = self.http.request(self.url_prefix + "subnet/" + subnet_name, "POST", json_data, {'Content-Type': 'application/json'})
157         if (resp.status == 201):
158             print "subnet", subnet, "added"
159         else:
160             print "subnet", subnet, "could not be added"
161
162
163 def run_interactive_mode():
164
165     print "Usage: [host | link] [bytes | rate] [src dst | link-name]"
166
167     # Demo mode
168     while True:
169         request = raw_input("> ")
170         try:
171             request = request.split()
172             request_type = request[0]
173
174             if (request_type == "quit"):
175                 sys.exit()
176
177             action = request[1]
178
179             if (request_type == "host"):
180                 src, dst = request[2:4]
181                 if (action == "bytes"):
182                     host_stat = Stats("host", src=src, dst=dst)
183                     print("%d bytes between %s and %s" % (host_stat.get_bytes(), src, dst))
184                 elif (action == "rate"):
185                     host_stat = Stats("host", src=src, dst=dst)
186                     print("%f bit/s between %s and %s" % (host_stat.get_bit_rate(), src, dst))
187                 else:
188                     raise Exception
189
190             # TODO: Change this to use AffinityLinkStats
191             elif (request_type == "link"):
192                 link = request[2]
193                 h = httplib2.Http(".cache")
194                 h.add_credentials("admin", "admin")
195                 resp, content = h.request("http://localhost:8080/affinity/nb/v2/analytics/default/affinitylinkstats/" + link, "GET")
196                 al_stats = json.loads(content)
197
198                 if (action == "bytes"):
199                     print("%d bytes on %s" % (long(al_stats["byteCount"]), link))
200                 elif (action == "rate"):
201                     print("%f bit/s on %s" % (float(al_stats["bitRate"]), link))
202                 else:
203                     raise Exception
204
205             else:
206                 raise Exception
207         except Exception as e:
208             print "Error"
209
210
211 def get_all_hosts():
212
213     h = httplib2.Http(".cache")
214     h.add_credentials("admin", "admin")
215
216     resp, content = h.request("http://localhost:8080/controller/nb/v2/hosttracker/default/hosts/active", "GET")
217     host_content = json.loads(content)
218
219     # Even if there are no active hosts, host_content['hostConfig']
220     # still exists (and is empty)
221     active_hosts = []
222     for host_data in host_content['hostConfig']:
223         active_hosts.append(host_data['networkAddress'])
224     return active_hosts
225
226
227 def run_passive_mode(affinity_links):
228     # TODO: Get affinity_links automatically
229     affinity_link_stats = {}
230
231     # Go through all affinity link stats
232     while True:
233         for al in affinity_links:
234             if al not in affinity_link_stats:
235                 affinity_link_stats[al] = Stats("affinityLink", al=al)
236             stat = affinity_link_stats[al]
237             stat.refresh()
238             print "%d bytes (%1.1f Mbit/s) on %s" % (stat.get_bytes(), (stat.get_bit_rate() / (10**6)), al)
239         time.sleep(2)
240
241 def main():
242
243     # Default subnet is required for the host tracker to work.
244     subnet_control = SubnetControl()
245     subnet_control.add_subnet("defaultSubnet", "10.0.0.254/8")
246
247     # Set up an affinity link
248     affinity_control = AffinityControl()
249     affinity_control.add_affinity_group("testAG1", ["10.0.0.1", "10.0.0.2"])
250     affinity_control.add_affinity_group("testAG2", ["10.0.0.3", "10.0.0.4"])
251     affinity_control.add_affinity_link("testAL", "testAG1", "testAG2")
252     raw_input("[Press enter to continue] ")
253
254     interactive_mode = False
255
256     if interactive_mode:
257         run_interactive_mode()
258     else:
259         run_passive_mode(["testAL"])
260
261 if __name__ == "__main__":
262     main()