root / Classes / LBNodesViewController.m @ 4b4b833e
History | View | Annotate | Download (17.8 kB)
1 |
// |
---|---|
2 |
// LBNodesViewController.m |
3 |
// OpenStack |
4 |
// |
5 |
// Created by Mike Mayo on 5/4/11. |
6 |
// Copyright 2011 __MyCompanyName__. All rights reserved. |
7 |
// |
8 |
|
9 |
#import "LBNodesViewController.h" |
10 |
#import "OpenStackAccount.h" |
11 |
#import "AccountManager.h" |
12 |
#import "LoadBalancer.h" |
13 |
#import "LoadBalancerNode.h" |
14 |
#import "Server.h" |
15 |
#import "Flavor.h" |
16 |
#import "Image.h" |
17 |
#import "RSTextFieldCell.h" |
18 |
#import "UIViewController+Conveniences.h" |
19 |
#import "LBServersViewController.h" |
20 |
#import "LoadBalancerProtocol.h" |
21 |
#import "ActivityIndicatorView.h" |
22 |
#import "APICallback.h" |
23 |
#import "AnimatedProgressView.h" |
24 |
|
25 |
#define kNodes 0 |
26 |
#define kCloudServers 1 |
27 |
|
28 |
@implementation LBNodesViewController |
29 |
|
30 |
@synthesize account, loadBalancer, isNewLoadBalancer; |
31 |
|
32 |
- (void)dealloc { |
33 |
[account release]; |
34 |
[loadBalancer release]; |
35 |
[ipNodes release]; |
36 |
[cloudServerNodes release]; |
37 |
[nodesToDelete release]; |
38 |
[super dealloc]; |
39 |
} |
40 |
|
41 |
#pragma mark - Utilities |
42 |
|
43 |
- (void)deleteEmptyIPRows { |
44 |
NSMutableArray *indexPaths = [[NSMutableArray alloc] init]; |
45 |
NSMutableArray *nodesToRemove = [[NSMutableArray alloc] init]; |
46 |
|
47 |
for (int i = 0; i < [ipNodes count]; i++) { |
48 |
LoadBalancerNode *node = [self.loadBalancer.nodes objectAtIndex:i]; |
49 |
if (!node.address || [node.address isEqualToString:@""]) { |
50 |
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:kNodes]; |
51 |
[indexPaths addObject:indexPath]; |
52 |
[nodesToRemove addObject:node]; |
53 |
} |
54 |
} |
55 |
|
56 |
for (LoadBalancerNode *node in nodesToRemove) { |
57 |
[self.loadBalancer.nodes removeObject:node]; |
58 |
} |
59 |
|
60 |
[self.tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationTop]; |
61 |
[indexPaths release]; |
62 |
[nodesToRemove release]; |
63 |
} |
64 |
|
65 |
#pragma mark - View lifecycle |
66 |
|
67 |
- (void)viewDidLoad { |
68 |
[super viewDidLoad]; |
69 |
saved = NO; |
70 |
self.navigationItem.title = @"Nodes"; |
71 |
textFields = [[NSMutableArray alloc] init]; |
72 |
ipNodes = [[NSMutableArray alloc] init]; |
73 |
cloudServerNodes = [[NSMutableArray alloc] init]; |
74 |
NSMutableArray *nodes = [[NSMutableArray alloc] initWithCapacity:[self.loadBalancer.nodes count]]; |
75 |
for (LoadBalancerNode *node in self.loadBalancer.nodes) { |
76 |
LoadBalancerNode *copiedNode = node; //[node copy]; |
77 |
[nodes addObject:copiedNode]; |
78 |
if (copiedNode.server) { |
79 |
[cloudServerNodes addObject:node]; |
80 |
} else { |
81 |
[ipNodes addObject:node]; |
82 |
} |
83 |
//[copiedNode release]; |
84 |
} |
85 |
previousNodes = [[NSArray alloc] initWithArray:nodes]; |
86 |
[nodes release]; |
87 |
} |
88 |
|
89 |
- (void)viewWillAppear:(BOOL)animated { |
90 |
[super viewWillAppear:animated]; |
91 |
[self.tableView reloadData]; |
92 |
if (!isNewLoadBalancer) { |
93 |
[self addSaveButton]; |
94 |
} |
95 |
} |
96 |
|
97 |
- (void)viewWillDisappear:(BOOL)animated { |
98 |
[super viewWillDisappear:animated]; |
99 |
if (isNewLoadBalancer) { |
100 |
NSMutableArray *finalNodes = [[NSMutableArray alloc] init]; |
101 |
for (LoadBalancerNode *node in ipNodes) { |
102 |
if (node.address && ![node.address isEqualToString:@""]) { |
103 |
[finalNodes addObject:node]; |
104 |
} |
105 |
} |
106 |
for (LoadBalancerNode *node in cloudServerNodes) { |
107 |
[finalNodes addObject:node]; |
108 |
} |
109 |
if ([finalNodes count] > 0) { |
110 |
self.loadBalancer.nodes = [[[NSMutableArray alloc] initWithArray:finalNodes] autorelease]; |
111 |
} |
112 |
[finalNodes release]; |
113 |
self.navigationItem.rightBarButtonItem = nil; |
114 |
} else { |
115 |
if (!saved) { |
116 |
self.loadBalancer.nodes = [NSMutableArray arrayWithArray:previousNodes]; |
117 |
} |
118 |
} |
119 |
} |
120 |
|
121 |
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation { |
122 |
return (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) || (toInterfaceOrientation == UIInterfaceOrientationPortrait); |
123 |
} |
124 |
|
125 |
#pragma mark - Table view data source |
126 |
|
127 |
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { |
128 |
return 2; |
129 |
} |
130 |
|
131 |
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { |
132 |
if (section == kNodes) { |
133 |
return [ipNodes count] + 1; |
134 |
} else { |
135 |
return [cloudServerNodes count] + 1; |
136 |
} |
137 |
} |
138 |
|
139 |
- (RSTextFieldCell *)tableView:(UITableView *)tableView ipCellForRowAtIndexPath:(NSIndexPath *)indexPath { |
140 |
NSString *CellIdentifier = [NSString stringWithFormat:@"IPCell%i", indexPath.row]; |
141 |
|
142 |
RSTextFieldCell *cell = (RSTextFieldCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier]; |
143 |
if (cell == nil) { |
144 |
cell = [[[RSTextFieldCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier] autorelease]; |
145 |
cell.textField.delegate = self; |
146 |
|
147 |
// tag it so we'll know which node we're editing |
148 |
cell.textField.tag = indexPath.row; |
149 |
|
150 |
cell.textField.returnKeyType = UIReturnKeyDone; |
151 |
cell.textField.keyboardType = UIKeyboardTypeNumbersAndPunctuation; |
152 |
[textFields addObject:cell.textField]; |
153 |
|
154 |
cell.imageView.image = [UIImage imageNamed:@"red-delete-button.png"]; |
155 |
} |
156 |
|
157 |
if (indexPath.row < [ipNodes count]) { |
158 |
LoadBalancerNode *node = [ipNodes objectAtIndex:indexPath.row]; |
159 |
cell.textField.text = node.address; |
160 |
} |
161 |
|
162 |
return cell; |
163 |
} |
164 |
|
165 |
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { |
166 |
static NSString *CellIdentifier = @"Cell"; |
167 |
|
168 |
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; |
169 |
if (cell == nil) { |
170 |
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease]; |
171 |
} |
172 |
|
173 |
// Configure the cell... |
174 |
cell.accessoryType = UITableViewCellAccessoryNone; |
175 |
if (indexPath.section == kNodes) { |
176 |
if (indexPath.row == [ipNodes count]) { |
177 |
cell.textLabel.text = @"Add IP Address"; |
178 |
cell.detailTextLabel.text = @""; |
179 |
cell.imageView.image = [UIImage imageNamed:@"green-add-button.png"]; |
180 |
} else { |
181 |
return [self tableView:tableView ipCellForRowAtIndexPath:indexPath]; |
182 |
} |
183 |
} else if (indexPath.section == kCloudServers) { |
184 |
if (indexPath.row == [cloudServerNodes count]) { |
185 |
if ([cloudServerNodes count] == 0) { |
186 |
cell.textLabel.text = @"Add Cloud Servers"; |
187 |
} else { |
188 |
cell.textLabel.text = @"Add/Remove Cloud Servers"; |
189 |
} |
190 |
cell.detailTextLabel.text = @""; |
191 |
cell.imageView.image = [UIImage imageNamed:@"green-add-button.png"]; |
192 |
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { |
193 |
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; |
194 |
} else { |
195 |
cell.accessoryType = UITableViewCellAccessoryNone; |
196 |
} |
197 |
} else { |
198 |
Server *server = [[cloudServerNodes objectAtIndex:indexPath.row] server]; |
199 |
cell.textLabel.text = server.name; |
200 |
cell.detailTextLabel.text = server.flavor.name; |
201 |
if ([server.image respondsToSelector:@selector(logoPrefix)] && [[server.image logoPrefix] isEqualToString:kCustomImage]) { |
202 |
cell.imageView.image = [UIImage imageNamed:kCloudServersIcon]; |
203 |
} else { |
204 |
if ([server.image respondsToSelector:@selector(logoPrefix)]) { |
205 |
cell.imageView.image = [UIImage imageNamed:[NSString stringWithFormat:@"%@-icon.png", [server.image logoPrefix]]]; |
206 |
} |
207 |
} |
208 |
} |
209 |
} |
210 |
|
211 |
return cell; |
212 |
} |
213 |
|
214 |
// Override to support conditional editing of the table view. |
215 |
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { |
216 |
return indexPath.section == kNodes && indexPath.row == [ipNodes count]; |
217 |
} |
218 |
|
219 |
#pragma mark - Table view delegate |
220 |
|
221 |
- (void)focusOnLastTextField { |
222 |
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[textFields count] - 1 inSection:kNodes]; |
223 |
[[textFields lastObject] becomeFirstResponder]; |
224 |
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES]; |
225 |
} |
226 |
|
227 |
- (void)addIPRow { |
228 |
[self deleteEmptyIPRows]; |
229 |
LoadBalancerNode *node = [[[LoadBalancerNode alloc] init] autorelease]; |
230 |
node.condition = @"ENABLED"; |
231 |
node.port = [NSString stringWithFormat:@"%i", self.loadBalancer.protocol.port]; |
232 |
[ipNodes addObject:node]; |
233 |
[self.loadBalancer.nodes addObject:node]; |
234 |
NSArray *indexPath = [NSArray arrayWithObject:[NSIndexPath indexPathForRow:[ipNodes count] - 1 inSection:kNodes]]; |
235 |
[self.tableView insertRowsAtIndexPaths:indexPath withRowAnimation:UITableViewRowAnimationBottom]; |
236 |
[NSTimer scheduledTimerWithTimeInterval:0.35 target:self selector:@selector(focusOnLastTextField) userInfo:nil repeats:NO]; |
237 |
} |
238 |
|
239 |
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { |
240 |
if (indexPath.section == kNodes) { |
241 |
if (indexPath.row == [ipNodes count]) { |
242 |
[self.tableView deselectRowAtIndexPath:indexPath animated:YES]; |
243 |
[self addIPRow]; |
244 |
} else { |
245 |
[self.tableView deselectRowAtIndexPath:indexPath animated:YES]; |
246 |
NSArray *indexPaths = [NSArray arrayWithObject:indexPath]; |
247 |
LoadBalancerNode *node = [ipNodes objectAtIndex:indexPath.row]; |
248 |
[ipNodes removeObject:node]; |
249 |
[self.loadBalancer.nodes removeObject:node]; |
250 |
[self.tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationTop]; |
251 |
} |
252 |
} else if (indexPath.section == kCloudServers) { |
253 |
LBServersViewController *vc = [[LBServersViewController alloc] initWithAccount:self.account loadBalancer:self.loadBalancer serverNodes:cloudServerNodes]; |
254 |
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { |
255 |
[self.navigationController pushViewController:vc animated:YES]; |
256 |
} else { |
257 |
[self presentModalViewControllerWithNavigation:vc]; |
258 |
} |
259 |
[vc release]; |
260 |
} |
261 |
} |
262 |
|
263 |
#pragma mark - Text field delegate |
264 |
|
265 |
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField { |
266 |
// if (isNewLoadBalancer) { |
267 |
// [self addDoneButton]; |
268 |
// } |
269 |
return YES; |
270 |
} |
271 |
|
272 |
- (BOOL)textFieldShouldReturn:(UITextField *)textField { |
273 |
[textField resignFirstResponder]; |
274 |
|
275 |
if ([textField.text isEqualToString:@""]) { |
276 |
[self deleteEmptyIPRows]; |
277 |
} |
278 |
|
279 |
return NO; |
280 |
} |
281 |
|
282 |
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { |
283 |
LoadBalancerNode *node = [ipNodes objectAtIndex:textField.tag]; |
284 |
node.address = [textField.text stringByReplacingCharactersInRange:range withString:string]; |
285 |
return YES; |
286 |
} |
287 |
|
288 |
#pragma mark - Button Handlers |
289 |
|
290 |
//- (void)doneButtonPressed:(id)sender { |
291 |
// for (UITextField *textField in textFields) { |
292 |
// [textField resignFirstResponder]; |
293 |
// } |
294 |
// self.navigationItem.rightBarButtonItem = nil; |
295 |
//} |
296 |
|
297 |
- (void)deleteNodeProgress { |
298 |
currentAPICalls++; |
299 |
NSLog(@"((%i / 1.0) / %i) = %f", currentAPICalls, totalAPICalls, ((currentAPICalls / 1.0) / totalAPICalls)); |
300 |
[spinner.progressView setProgress:((currentAPICalls / 1.0) / totalAPICalls) animated:YES]; |
301 |
|
302 |
if (currentAPICalls == totalAPICalls) { |
303 |
[spinner removeFromSuperviewAndRelease]; |
304 |
} |
305 |
} |
306 |
|
307 |
- (void)deleteNode:(LoadBalancerNode *)node { |
308 |
|
309 |
NSLog(@"trying to delete node %@", node.identifier); |
310 |
|
311 |
NSString *endpoint = [self.account loadBalancerEndpointForRegion:self.loadBalancer.region]; |
312 |
|
313 |
APICallback *callback = [self.account.manager deleteLBNode:node loadBalancer:self.loadBalancer endpoint:endpoint]; |
314 |
|
315 |
__block void (^callBackBlock)(OpenStackRequest *request); |
316 |
callBackBlock = ^(OpenStackRequest *request) { |
317 |
|
318 |
deleteIndex++; |
319 |
[self deleteNodeProgress]; |
320 |
|
321 |
if (![request isSuccess]) { |
322 |
[self alert:@"There was a problem deleting a node." request:request]; |
323 |
} else { |
324 |
self.loadBalancer.status = @"PENDING_UPDATE"; |
325 |
} |
326 |
|
327 |
|
328 |
if (deleteIndex < [nodesToDelete count]) { |
329 |
|
330 |
self.loadBalancer.status = @"PENDING_UPDATE"; |
331 |
[self.loadBalancer pollUntilActive:self.account delegate:self completeSelector:@selector(deleteNode:) object:[nodesToDelete objectAtIndex:deleteIndex]]; |
332 |
|
333 |
// [self.loadBalancer pollUntilActive:self.account complete:^{ |
334 |
// deleteNodeBlock([nodesToDelete objectAtIndex:deleteIndex]); |
335 |
// }]; |
336 |
}; |
337 |
|
338 |
}; |
339 |
|
340 |
[callback success:callBackBlock failure:callBackBlock]; |
341 |
} |
342 |
|
343 |
- (void)deleteNodesWithProgress:(ASIBasicBlock)progressBlock { |
344 |
|
345 |
NSString *endpoint = [self.account loadBalancerEndpointForRegion:self.loadBalancer.region]; |
346 |
deleteIndex = 0; |
347 |
|
348 |
__block void (^deleteNodeBlock)(LoadBalancerNode *node); |
349 |
deleteNodeBlock = ^(LoadBalancerNode *node) { |
350 |
|
351 |
NSLog(@"trying to delete node %@", node.identifier); |
352 |
|
353 |
APICallback *callback = [self.account.manager deleteLBNode:node loadBalancer:self.loadBalancer endpoint:endpoint]; |
354 |
|
355 |
__block void (^callBackBlock)(OpenStackRequest *request); |
356 |
callBackBlock = ^(OpenStackRequest *request) { |
357 |
|
358 |
deleteIndex++; |
359 |
[self deleteNodeProgress]; |
360 |
|
361 |
if (deleteIndex < [nodesToDelete count]) { |
362 |
self.loadBalancer.status = @"PENDING_UPDATE"; |
363 |
[self.loadBalancer pollUntilActive:self.account delegate:self completeSelector:@selector(deleteNode:) object:[nodesToDelete objectAtIndex:deleteIndex]]; |
364 |
|
365 |
// [self.loadBalancer pollUntilActive:self.account complete:^{ |
366 |
// deleteNodeBlock([nodesToDelete objectAtIndex:deleteIndex]); |
367 |
// }]; |
368 |
}; |
369 |
|
370 |
if (![request isSuccess]) { |
371 |
[self alert:@"There was a problem deleting a node." request:request]; |
372 |
} |
373 |
}; |
374 |
|
375 |
[callback success:callBackBlock failure:callBackBlock]; |
376 |
|
377 |
}; |
378 |
|
379 |
LoadBalancerNode *node = [nodesToDelete objectAtIndex:deleteIndex]; |
380 |
deleteNodeBlock(node); |
381 |
|
382 |
} |
383 |
|
384 |
- (void)addNodes:(NSArray *)nodesToAdd andDeleteNodesWithProgress:(ASIBasicBlock)progressBlock failure:(APIResponseBlock)failureBlock { |
385 |
|
386 |
NSString *endpoint = [self.account loadBalancerEndpointForRegion:self.loadBalancer.region]; |
387 |
|
388 |
if ([nodesToAdd count] > 0) { |
389 |
// we want to add before doing any deletes to avoid attempting an invalid delete |
390 |
APICallback *callback = [self.account.manager addLBNodes:nodesToAdd loadBalancer:self.loadBalancer endpoint:endpoint]; |
391 |
[callback success:^(OpenStackRequest *request) { |
392 |
|
393 |
// if it's a successful add, the status will be PENDING_UPDATE. cheaper |
394 |
// to just set it than hit the API again since we're already going to hit it |
395 |
// n times for the deletes |
396 |
self.loadBalancer.status = @"PENDING_UPDATE"; |
397 |
|
398 |
[self deleteNodeProgress]; |
399 |
|
400 |
if ([nodesToDelete count] > 0) { |
401 |
// before you delete, you need to poll the LB until it hits active status |
402 |
[self.loadBalancer pollUntilActive:self.account complete:^{ |
403 |
[self deleteNodesWithProgress:progressBlock]; |
404 |
}]; |
405 |
} |
406 |
|
407 |
} failure:^(OpenStackRequest *request) { |
408 |
failureBlock(request); |
409 |
}]; |
410 |
} else { |
411 |
[self deleteNodesWithProgress:progressBlock]; |
412 |
} |
413 |
|
414 |
} |
415 |
|
416 |
- (void)saveButtonPressed:(id)sender { |
417 |
|
418 |
if ([self.loadBalancer.nodes count] == 0) { |
419 |
[self alert:nil message:@"You must have at least one node attached to this load balancer."]; |
420 |
return; |
421 |
} else { |
422 |
NSInteger enabledCount = 0; |
423 |
for (LoadBalancerNode *node in self.loadBalancer.nodes) { |
424 |
if ([node.condition isEqualToString:@"ENABLED"]) { |
425 |
enabledCount++; |
426 |
} |
427 |
} |
428 |
if (enabledCount == 0) { |
429 |
[self alert:nil message:@"You must have at least one enabled node attached to this load balancer."]; |
430 |
return; |
431 |
} |
432 |
} |
433 |
|
434 |
|
435 |
|
436 |
// we need to compare the previousNodoes list to the current nodes list so we |
437 |
// can know which nodes to add and which ones to delete |
438 |
NSMutableArray *nodesToAdd = [[NSMutableArray alloc] init]; |
439 |
nodesToDelete = [[NSMutableArray alloc] init]; |
440 |
|
441 |
NSLog(@"previous nodes: %@", previousNodes); |
442 |
NSLog(@"lb nodes: %@", self.loadBalancer.nodes); |
443 |
|
444 |
for (LoadBalancerNode *node in previousNodes) { |
445 |
if (![self.loadBalancer.nodes containsObject:node]) { |
446 |
[nodesToDelete addObject:node]; |
447 |
NSLog(@"going to delete node: %@", node); |
448 |
} |
449 |
} |
450 |
|
451 |
for (LoadBalancerNode *node in self.loadBalancer.nodes) { |
452 |
if (![previousNodes containsObject:node]) { |
453 |
[nodesToAdd addObject:node]; |
454 |
NSLog(@"going to add node: %@", node); |
455 |
} |
456 |
} |
457 |
|
458 |
currentAPICalls = 0; |
459 |
totalAPICalls = [nodesToDelete count] + ([nodesToAdd count] > 0 ? 1 : 0); |
460 |
|
461 |
if (totalAPICalls > 0) { |
462 |
spinner = [[ActivityIndicatorView alloc] initWithFrame:[ActivityIndicatorView frameForText:@"Saving..." withProgress:YES] text:@"Saving..." withProgress:YES]; |
463 |
[spinner addToView:self.view]; |
464 |
|
465 |
saved = YES; |
466 |
|
467 |
// make the API calls |
468 |
[self addNodes:nodesToAdd andDeleteNodesWithProgress:^{ |
469 |
currentAPICalls++; |
470 |
// TODO: update progress view on spinner |
471 |
if (currentAPICalls == totalAPICalls) { |
472 |
[spinner removeFromSuperviewAndRelease]; |
473 |
} |
474 |
} failure:^(OpenStackRequest *request) { |
475 |
[self alert:@"There was a problem adding nodes." request:request]; |
476 |
[spinner removeFromSuperviewAndRelease]; |
477 |
}]; |
478 |
} else { |
479 |
[self alert:nil message:@"You did not select any nodes to add or remove."]; |
480 |
} |
481 |
|
482 |
[nodesToAdd release]; |
483 |
} |
484 |
|
485 |
@end |