Statistics
| Branch: | Tag: | Revision:

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