-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaws-refresh-ssh-config.py
More file actions
130 lines (110 loc) · 6.04 KB
/
Copy pathaws-refresh-ssh-config.py
File metadata and controls
130 lines (110 loc) · 6.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import argparse
import boto3
import json
import time
IN_FILE = "initial-servers.json"
OUT_FILE = "temp-servers.json"
# Network ID for main network, gamma, and beta
NETWORK_ID="1"
NETWORK_ID_FILTER = {'Name': 'tag:NetworkId', 'Values': [NETWORK_ID]}
RUNNING_FILTER={'Name': 'instance-state-name', 'Values': ['running']}
ELASTIC_IP_GROUPS = ['bootnode-a', 'bootnode-b', 'observer-a', 'observer-b']
def parse_args():
parser = argparse.ArgumentParser(description='Create config for ssh-to via AWS')
parser.add_argument('--ssh-user', dest='ssh_user', default='ubuntu', action='store', help='Username that will be used to ssh instances')
parser.add_argument('--refresh-group', dest='refresh_group', required=True, action='store', help='Group from the input that is being replaced')
parser.add_argument('--no-elastic-ips', dest='no_elastic_ips', default=False, action='store_true', help='Disable the expectation for elastic IPs on bootnodes and observers')
return parser.parse_args()
def check_for_replacement(old_exim_node, expect_elastic_ips, replacements_so_far):
name_filter = {'Name': 'tag:Name', 'Values': [old_exim_node.name]}
conn = boto3.resource('ec2', region_name=old_exim_node.region)
# We distinguish 3 cases based on the contents of 'instances'
# 1. The old instance is still up and the new one is not up yet
# 2. The old instance is down but the new one is not up yet
# 3. The new instance is up
instances = conn.instances.filter(Filters=[NETWORK_ID_FILTER, RUNNING_FILTER, name_filter])
# In case 2 this loop has no iterations
for instance in instances:
temp_exim_node = EximNode.from_boto_instance(instance)
# Skip this node because it's already in our replacements list
if temp_exim_node in replacements_so_far:
print(f'Skipping matching node {temp_exim_node.instance_id} because it is already in replacements list')
continue
# This catches case 1, and case 3 where the old node still exists
if old_exim_node.instance_id == temp_exim_node.instance_id:
print(f'Old instance {old_exim_node.instance_id} for {old_exim_node.name} not yet replaced')
continue
# If we expect elastic IPs for this group and we haven't assigned it yet, keep waiting
if expect_elastic_ips and (len(instance.network_interfaces) != 1 or instance.network_interfaces[0].association_attribute["IpOwnerId"] == 'amazon'):
print(f'New instance {temp_exim_node.instance_id} expected to use elastic IP, but isn\'t associated yet')
continue
# If the hostname didn't change with the instance id this is probably a bug
assert(old_exim_node.hostname != temp_exim_node.hostname)
return temp_exim_node
return None
def wait_for_replacements(nodes_to_replace, expect_elastic_ips):
original_to_replacement = {node: None for node in nodes_to_replace}
num_unreplaced_nodes = len(original_to_replacement)
while num_unreplaced_nodes > 0:
for original_node,replacement_node in original_to_replacement.items():
if replacement_node == None:
replacements_so_far = [replacement for replacement in original_to_replacement.values() if replacement != None]
original_to_replacement[original_node] = check_for_replacement(original_node, expect_elastic_ips, replacements_so_far)
num_unreplaced_nodes = len([node for node in original_to_replacement.values() if node == None])
print(f'Still waiting for {num_unreplaced_nodes} unreplaced nodes')
if num_unreplaced_nodes > 0:
sleep_seconds = 20
print(f'Sleeping for {sleep_seconds} seconds before polling again')
time.sleep(sleep_seconds)
return original_to_replacement.values()
class EximNode:
def __init__(self, hostname, name, region, instance_id):
self.hostname = hostname
self.name = name
self.region = region
self.instance_id = instance_id
def __eq__(self, other):
if not isinstance(other, type(self)):
return False
return (self.hostname, self.name, self.region, self.instance_id) == (other.hostname, other.name, other.region, other.instance_id)
def __ne__(self, other):
return not(self == other)
def __hash__(self):
return hash((self.hostname, self.name, self.region, self.instance_id))
@classmethod
def from_json_list(cls, json_list):
assert(len(json_list) == 3)
hostname = json_list[0]
name = json_list[1]
comment = json_list[2]
split_comment = comment.split(':')
assert(len(split_comment) == 2)
region = split_comment[0]
instance_id = split_comment[1]
return cls(hostname, name, region, instance_id)
@classmethod
def from_boto_instance(cls, instance):
hostname = instance.public_dns_name
name_tag = [tag for tag in instance.tags if tag['Key'] == 'Name']
name = name_tag[0]['Value']
region_tag = [tag for tag in instance.tags if tag['Key'] == 'Region']
region = region_tag[0]['Value']
instance_id = instance.instance_id
return cls(hostname, name, region, instance_id)
# Serializes to ssh-to input style list
def to_json_list(self, args):
ssh_hostname = f'{args.ssh_user}@{self.hostname}'
comment = f'{self.region}:{self.instance_id}'
return [ssh_hostname, self.name, comment]
# Main script body
args = parse_args()
expect_elastic_ips = args.refresh_group in ELASTIC_IP_GROUPS and not args.no_elastic_ips
with open(IN_FILE, 'r') as f:
input = json.load(f)
nodes_to_replace = [EximNode.from_json_list(node) for node in input[args.refresh_group]]
replacement_nodes = wait_for_replacements(nodes_to_replace, expect_elastic_ips)
replacement_node_json_list = [node.to_json_list(args) for node in replacement_nodes]
output = {args.refresh_group: replacement_node_json_list}
print(f'Dumping server config with replacement instances for group {args.refresh_group} to {OUT_FILE}')
with open(OUT_FILE, 'w') as f:
json.dump(output, f, indent=2)