2 // LBNodesViewController.m
5 // Created by Mike Mayo on 5/4/11.
6 // Copyright 2011 __MyCompanyName__. All rights reserved.
9 #import "LBNodesViewController.h"
10 #import "OpenStackAccount.h"
11 #import "AccountManager.h"
12 #import "LoadBalancer.h"
13 #import "LoadBalancerNode.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"
26 #define kCloudServers 1
28 @implementation LBNodesViewController
30 @synthesize account, loadBalancer, isNewLoadBalancer;
34 [loadBalancer release];
36 [cloudServerNodes release];
37 [nodesToDelete release];
41 #pragma mark - Utilities
43 - (void)deleteEmptyIPRows {
44 NSMutableArray *indexPaths = [[NSMutableArray alloc] init];
45 NSMutableArray *nodesToRemove = [[NSMutableArray alloc] init];
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];
56 for (LoadBalancerNode *node in nodesToRemove) {
57 [self.loadBalancer.nodes removeObject:node];
60 [self.tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationTop];
62 [nodesToRemove release];
65 #pragma mark - View lifecycle
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];
81 [ipNodes addObject:node];
83 //[copiedNode release];
85 previousNodes = [[NSArray alloc] initWithArray:nodes];
89 - (void)viewWillAppear:(BOOL)animated {
90 [super viewWillAppear:animated];
91 [self.tableView reloadData];
92 if (!isNewLoadBalancer) {
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];
106 for (LoadBalancerNode *node in cloudServerNodes) {
107 [finalNodes addObject:node];
109 if ([finalNodes count] > 0) {
110 self.loadBalancer.nodes = [[[NSMutableArray alloc] initWithArray:finalNodes] autorelease];
112 [finalNodes release];
113 self.navigationItem.rightBarButtonItem = nil;
116 self.loadBalancer.nodes = [NSMutableArray arrayWithArray:previousNodes];
121 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
122 return (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) || (toInterfaceOrientation == UIInterfaceOrientationPortrait);
125 #pragma mark - Table view data source
127 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
131 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
132 if (section == kNodes) {
133 return [ipNodes count] + 1;
135 return [cloudServerNodes count] + 1;
139 - (RSTextFieldCell *)tableView:(UITableView *)tableView ipCellForRowAtIndexPath:(NSIndexPath *)indexPath {
140 NSString *CellIdentifier = [NSString stringWithFormat:@"IPCell%i", indexPath.row];
142 RSTextFieldCell *cell = (RSTextFieldCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
144 cell = [[[RSTextFieldCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier] autorelease];
145 cell.textField.delegate = self;
147 // tag it so we'll know which node we're editing
148 cell.textField.tag = indexPath.row;
150 cell.textField.returnKeyType = UIReturnKeyDone;
151 cell.textField.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
152 [textFields addObject:cell.textField];
154 cell.imageView.image = [UIImage imageNamed:@"red-delete-button.png"];
157 if (indexPath.row < [ipNodes count]) {
158 LoadBalancerNode *node = [ipNodes objectAtIndex:indexPath.row];
159 cell.textField.text = node.address;
165 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
166 static NSString *CellIdentifier = @"Cell";
168 UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
170 cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
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"];
181 return [self tableView:tableView ipCellForRowAtIndexPath:indexPath];
183 } else if (indexPath.section == kCloudServers) {
184 if (indexPath.row == [cloudServerNodes count]) {
185 if ([cloudServerNodes count] == 0) {
186 cell.textLabel.text = @"Add Cloud Servers";
188 cell.textLabel.text = @"Add/Remove Cloud Servers";
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;
195 cell.accessoryType = UITableViewCellAccessoryNone;
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];
204 if ([server.image respondsToSelector:@selector(logoPrefix)]) {
205 cell.imageView.image = [UIImage imageNamed:[NSString stringWithFormat:@"%@-icon.png", [server.image logoPrefix]]];
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];
219 #pragma mark - Table view delegate
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];
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];
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];
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];
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];
257 [self presentModalViewControllerWithNavigation:vc];
263 #pragma mark - Text field delegate
265 - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
266 // if (isNewLoadBalancer) {
267 // [self addDoneButton];
272 - (BOOL)textFieldShouldReturn:(UITextField *)textField {
273 [textField resignFirstResponder];
275 if ([textField.text isEqualToString:@""]) {
276 [self deleteEmptyIPRows];
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];
288 #pragma mark - Button Handlers
290 //- (void)doneButtonPressed:(id)sender {
291 // for (UITextField *textField in textFields) {
292 // [textField resignFirstResponder];
294 // self.navigationItem.rightBarButtonItem = nil;
297 - (void)deleteNodeProgress {
299 NSLog(@"((%i / 1.0) / %i) = %f", currentAPICalls, totalAPICalls, ((currentAPICalls / 1.0) / totalAPICalls));
300 [spinner.progressView setProgress:((currentAPICalls / 1.0) / totalAPICalls) animated:YES];
302 if (currentAPICalls == totalAPICalls) {
303 [spinner removeFromSuperviewAndRelease];
307 - (void)deleteNode:(LoadBalancerNode *)node {
309 NSLog(@"trying to delete node %@", node.identifier);
311 NSString *endpoint = [self.account loadBalancerEndpointForRegion:self.loadBalancer.region];
313 APICallback *callback = [self.account.manager deleteLBNode:node loadBalancer:self.loadBalancer endpoint:endpoint];
315 __block void (^callBackBlock)(OpenStackRequest *request);
316 callBackBlock = ^(OpenStackRequest *request) {
319 [self deleteNodeProgress];
321 if (![request isSuccess]) {
322 [self alert:@"There was a problem deleting a node." request:request];
324 self.loadBalancer.status = @"PENDING_UPDATE";
328 if (deleteIndex < [nodesToDelete count]) {
330 self.loadBalancer.status = @"PENDING_UPDATE";
331 [self.loadBalancer pollUntilActive:self.account delegate:self completeSelector:@selector(deleteNode:) object:[nodesToDelete objectAtIndex:deleteIndex]];
333 // [self.loadBalancer pollUntilActive:self.account complete:^{
334 // deleteNodeBlock([nodesToDelete objectAtIndex:deleteIndex]);
340 [callback success:callBackBlock failure:callBackBlock];
343 - (void)deleteNodesWithProgress:(ASIBasicBlock)progressBlock {
345 NSString *endpoint = [self.account loadBalancerEndpointForRegion:self.loadBalancer.region];
348 __block void (^deleteNodeBlock)(LoadBalancerNode *node);
349 deleteNodeBlock = ^(LoadBalancerNode *node) {
351 NSLog(@"trying to delete node %@", node.identifier);
353 APICallback *callback = [self.account.manager deleteLBNode:node loadBalancer:self.loadBalancer endpoint:endpoint];
355 __block void (^callBackBlock)(OpenStackRequest *request);
356 callBackBlock = ^(OpenStackRequest *request) {
359 [self deleteNodeProgress];
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]];
365 // [self.loadBalancer pollUntilActive:self.account complete:^{
366 // deleteNodeBlock([nodesToDelete objectAtIndex:deleteIndex]);
370 if (![request isSuccess]) {
371 [self alert:@"There was a problem deleting a node." request:request];
375 [callback success:callBackBlock failure:callBackBlock];
379 LoadBalancerNode *node = [nodesToDelete objectAtIndex:deleteIndex];
380 deleteNodeBlock(node);
384 - (void)addNodes:(NSArray *)nodesToAdd andDeleteNodesWithProgress:(ASIBasicBlock)progressBlock failure:(APIResponseBlock)failureBlock {
386 NSString *endpoint = [self.account loadBalancerEndpointForRegion:self.loadBalancer.region];
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) {
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";
398 [self deleteNodeProgress];
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];
407 } failure:^(OpenStackRequest *request) {
408 failureBlock(request);
411 [self deleteNodesWithProgress:progressBlock];
416 - (void)saveButtonPressed:(id)sender {
418 if ([self.loadBalancer.nodes count] == 0) {
419 [self alert:nil message:@"You must have at least one node attached to this load balancer."];
422 NSInteger enabledCount = 0;
423 for (LoadBalancerNode *node in self.loadBalancer.nodes) {
424 if ([node.condition isEqualToString:@"ENABLED"]) {
428 if (enabledCount == 0) {
429 [self alert:nil message:@"You must have at least one enabled node attached to this load balancer."];
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];
441 NSLog(@"previous nodes: %@", previousNodes);
442 NSLog(@"lb nodes: %@", self.loadBalancer.nodes);
444 for (LoadBalancerNode *node in previousNodes) {
445 if (![self.loadBalancer.nodes containsObject:node]) {
446 [nodesToDelete addObject:node];
447 NSLog(@"going to delete node: %@", node);
451 for (LoadBalancerNode *node in self.loadBalancer.nodes) {
452 if (![previousNodes containsObject:node]) {
453 [nodesToAdd addObject:node];
454 NSLog(@"going to add node: %@", node);
459 totalAPICalls = [nodesToDelete count] + ([nodesToAdd count] > 0 ? 1 : 0);
461 if (totalAPICalls > 0) {
462 spinner = [[ActivityIndicatorView alloc] initWithFrame:[ActivityIndicatorView frameForText:@"Saving..." withProgress:YES] text:@"Saving..." withProgress:YES];
463 [spinner addToView:self.view];
467 // make the API calls
468 [self addNodes:nodesToAdd andDeleteNodesWithProgress:^{
470 // TODO: update progress view on spinner
471 if (currentAPICalls == totalAPICalls) {
472 [spinner removeFromSuperviewAndRelease];
474 } failure:^(OpenStackRequest *request) {
475 [self alert:@"There was a problem adding nodes." request:request];
476 [spinner removeFromSuperviewAndRelease];
479 [self alert:nil message:@"You did not select any nodes to add or remove."];
482 [nodesToAdd release];