-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathopenstack_cloud.py
433 lines (372 loc) · 17.5 KB
/
openstack_cloud.py
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
import argparse
import os
import time
from keystoneauth1.identity import v3
from keystoneauth1 import session
from novaclient import client as nova_client
from designateclient.v2 import client as designate_client
from collections import defaultdict
from neutronclient.v2_0 import client as neutronclient
import glanceclient
import openstack
class OpenstackCloud:
def __init__(self, cloud_config):
self.cloud_config = cloud_config
self.conn = None
self.project_id = os.environ.get('OS_PROJECT_ID')
self.sess = self.get_session()
self.nova_sess = nova_client.Client(version=2.4, session=self.sess)
self.servers = self.query_servers()
self.glclient = glanceclient.Client(version="2", session=self.sess)
self.neutronClient = neutronclient.Client(session=self.sess)
self.designateClient = designate_client.Client(session=self.sess)
def get_session(self):
options = argparse.ArgumentParser(description='Awesome OpenStack App')
self.conn = openstack.connect(options=options, verify=False)
"""Return keystone session"""
# Extract environment variables set when sourcing the openstack RC file.
user_domain = os.environ.get('OS_USER_DOMAIN_NAME')
user = os.environ.get('OS_USERNAME')
password = os.environ.get('OS_PASSWORD')
auth_url = os.environ.get('OS_AUTH_URL')
# Create user / password based authentication method.
# https://goo.gl/VxD2FQ
auth = v3.Password(user_domain_name=user_domain,
username=user,
password=password,
project_id=self.project_id,
auth_url=auth_url)
# Create OpenStack keystoneauth1 session.
# https://goo.gl/BE7YMt
sess = session.Session(auth=auth, verify=False)
return sess
def query_servers(self):
"""Query list of servers.
Returns a dictionary of server dictionaries with the key being the
server name
"""
servers = defaultdict()
nova_servers = self.nova_sess.servers.list()
for idx, server in enumerate(nova_servers):
server_dict = server.to_dict()
servers[server.human_id] = server_dict
return servers
def find_zone(self, enterprise_url):
zones = self.designateClient.zones.list()
for zone in zones:
if zone['name'] == (enterprise_url + '.'):
return zone
return None
def check_deploy_ok(self, enterprise):
enterprise_url = self.cloud_config['enterprise_url']
zone = self.find_zone(enterprise_url)
if zone is not None:
print(f"Zone already exists: {enterprise_url}.")
return False
server_name_set = {x['name'].strip() for x in self.servers.values()}
deploy_name_set = {x['name'].strip() for x in enterprise['nodes']}
deployed_name_set = deploy_name_set.intersection(server_name_set)
if len(deployed_name_set) != 0:
for name in deployed_name_set:
print(f"Found that server {name} already exists.")
return False
return True
def query_deploy_ok(self, enterprise):
enterprise_url = self.cloud_config['enterprise_url']
zone = self.find_zone(enterprise_url)
if zone is None:
print(f"Zone does not exist. Creating: {enterprise_url}.")
self.designateClient.zones.create(f"{enterprise_url}.", email="root@" + enterprise_url)
else:
print(f"Zone \"{enterprise_url}\" exists. Deleting and re-creating ...")
self.designateClient.zones.delete(enterprise_url + ".")
while self.find_zone(enterprise_url) is not None:
time.sleep(5)
self.designateClient.zones.create(f"{enterprise_url}.", email="root@" + enterprise_url)
server_name_set = {x['name'].strip() for x in self.servers.values()}
deploy_name_set = {x['name'].strip() for x in enterprise['nodes']}
undeployed_name_set = deploy_name_set - server_name_set
if len(undeployed_name_set) != 0:
for name in undeployed_name_set:
print(f"Found that server {name} does not exist.")
return False
return True
def os_to_image(self, os_name):
if os_name not in self.cloud_config['image_map']:
return os_name
return self.cloud_config['image_map'][os_name]
def size_to_flavor(self, size_name):
return self.cloud_config['instance_size_map'].get(size_name, "m1.small")
def find_image_by_name(self, name):
images = self.glclient.images.list()
found_image = None
for image in images:
if image['name'] == name and found_image is None:
found_image = image
elif image['name'] == name and found_image is not None:
str_value = "Duplicate images named " + name
raise NameError(str_value)
if found_image is None:
str_value = "Image not found: " + name
raise NameError(str_value)
print(" Found image id: " + found_image['id'])
return found_image
def get_network_id(self, network_name):
project = self.conn.identity.find_project(self.project_id)
project_name = project.name
stack_resource_map = {
stack.name: list(self.conn.orchestration.resources(stack.name)) for stack in self.conn.list_stacks()
if stack.location.project.id == self.project_id and stack.name.startswith(f"{project_name}_")
}
network_id_list = [
resource.physical_resource_id
for resource_list in stack_resource_map.values()
for resource in resource_list
if resource.id == network_name
]
result = network_id_list[0] if len(network_id_list) > 0 else None
# try to find a network outside the stack list if it might be a public network.
if result is None:
result = self.conn.get_network_by_id(network_name)
if result is None:
result = self.conn.find_network(network_name)
return result
def find_network_by_name(self, name):
ret = self.neutronClient.list_networks()
networks = ret['networks']
found_network = None
for network in networks:
found = network['id'] == name or network['name'] == name
if found and found_network is None:
found_network = network
elif found and found_network is not None:
str_value = f"Duplicate networks named {name}"
raise NameError(str_value)
if found_network is None:
str_value = f"Network not found: {name}"
raise NameError(str_value)
print(" Found network id: " + found_network['id'])
return found_network
def create_nodes(self, enterprise, ret):
ret['nodes'] = []
if not ret['check_deploy_ok']:
errstr = " Found that one or more nodes already exist, aborting deploy."
raise RuntimeError(errstr)
for node in enterprise['nodes']:
name = node['name']
print("Creating server named " + name)
os_name = node['os']
size = node.get('size', "small")
domain = node.get('domain', "")
keypair = self.cloud_config['keypair']
image = self.os_to_image(os_name)
flavor = self.size_to_flavor(size)
security_group = self.cloud_config['security_group']
all_groups = self.conn.list_security_groups()
project_groups = [x for x in all_groups if x.name == security_group or x.id == security_group]
if not len(project_groups) == 1:
errstr = "Found 0 or more than 1 security groups called " + security_group + "\n" + str(project_groups)
raise RuntimeError(errstr)
network = node.get('network', self.cloud_config['external_network'])
nova_image = self.find_image_by_name(image)
# nova_flavor = self.nova_sess.flavors.find(name=flavor)
nova_net = self.find_network_by_name(network)
self.network_name = nova_net['name']
nova_nics = [{'net-id': nova_net['id']}]
nova_instance = self.conn.create_server(
name=name,
image=image,
flavor=flavor,
key_name=keypair,
security_groups=[security_group],
nics=nova_nics
)
time.sleep(5)
print(" Server " + name + " has id " + nova_instance.id)
nova_instance = self.nova_sess.servers.get(nova_instance.id)
# print(dir(nova_instance))
new_node = {
'name': name,
'flavor': flavor,
'size': size,
'os': os_name,
'domain': domain,
'image': image,
'security_group': security_group,
'network': network,
'keypair': keypair,
'nova_image': nova_image,
'nova_nics': nova_nics,
'is_ready': False,
'nova_status': nova_instance.status,
'id': nova_instance.id,
'enterprise_description': node
}
ret['nodes'].append(new_node)
return ret
def query_nodes(self, enterprise, ret):
ret['nodes'] = []
if not ret['check_deploy_ok']:
errstr = " Found that one or more nodes already exist, aborting deploy."
raise RuntimeError(errstr)
for node in enterprise['nodes']:
name = node['name']
print("Querying server named " + name)
os_name = node['os']
size = node.get('size', "small")
domain = node.get('domain', "")
keypair = self.cloud_config['keypair']
image = self.os_to_image(os_name)
flavor = self.size_to_flavor(size)
security_group = self.cloud_config['security_group']
all_groups = self.conn.list_security_groups()
project_groups = [x for x in all_groups if (
x.location.project.id == self.project_id and x.name == security_group) or x.id == security_group]
if not len(project_groups) == 1:
errstr = "Found 0 or more than 1 security groups called " + security_group + "\n" + str(all_groups)
raise RuntimeError(errstr)
network_name = node.get('network', self.cloud_config['external_network'])
nova_image = self.find_image_by_name(image)
# nova_flavor = self.nova_sess.flavors.find(name=flavor)
network_id = self.get_network_id(network_name)
if network_id is None:
raise Exception(f"Could not find network id for \"{network_name}\" network.")
nova_nics = [{'net-id': network_id}]
nova_instance = self.servers[name]
print(" Server " + name + " has id " + nova_instance['id'])
# print(dir(nova_instance))
new_node = {
'name': name,
'flavor': flavor,
'size': size,
'os': os_name,
'domain': domain,
'image': image,
'security_group': security_group,
'network': network_name,
'keypair': keypair,
'nova_image': nova_image,
'nova_nics': nova_nics,
'is_ready': False,
'nova_status': nova_instance['status'],
'id': nova_instance['id'],
'enterprise_description': node
}
ret['nodes'].append(new_node)
return ret
def wait_for_ready(self, ret):
waiting = True
while waiting:
try:
print("Waiting for instances to be ready. Sleeping 5 seconds...")
time.sleep(10)
waiting = False
for node in ret['nodes']:
id_value = node['id']
if not node['is_ready']:
nova_instance = self.nova_sess.servers.get(id_value)
node['nova_status'] = nova_instance.status
if nova_instance.status == 'ACTIVE':
print("Node " + node['name'] + " is ready!")
node['is_ready'] = True
elif nova_instance.status == 'BUILD':
waiting = True
else:
errstr = (
f"Node {node['name']} is neither BUILDing or ACTIVE. "
"Assuming error has occured. Exiting...."
)
raise RuntimeError(errstr)
except Exception as _: # noqa: F841
pass
print('All nodes are ready')
return ret
def collect_info(self, enterprise, enterprise_built):
ret = enterprise_built
for node in enterprise_built['nodes']:
id_value = node['id']
name = node['name']
enterprise_node = next(filter(lambda x: name == x['name'], enterprise['nodes']))
nova_instance = self.nova_sess.servers.get(id_value)
print(f"Addresses = {nova_instance.addresses}")
network_name = self.cloud_config['external_network']
address_list = []
for key, value in nova_instance.addresses.items():
new_value = [address for address in value if address['OS-EXT-IPS:type'] == 'fixed']
if key == network_name:
address_list.insert(0, new_value[0])
else:
address_list.append(new_value[0])
node['addresses'] = address_list
if 'windows' not in enterprise_node['roles']:
print("Skipping password retrieve for non-windows node " + name)
continue
while True:
nova_instance = self.nova_sess.servers.get(id_value)
node['password'] = nova_instance.get_password(private_key=self.cloud_config['private_key_file'])
if node['password'] == '':
print("Waiting for password for node " + name + ".")
time.sleep(5)
else:
break
return ret
def create_zones(self, ret):
enterprise_url = self.cloud_config['enterprise_url']
print("Creating DNS zone " + enterprise_url)
ret['create_zones'] = self.designateClient.zones.create(
enterprise_url + ".", email="root@" + enterprise_url, ttl=60)
return ret
def query_zones(self, ret):
enterprise_url = self.cloud_config['enterprise_url']
print("Querying DNS zone " + enterprise_url)
ret['create_zones'] = self.designateClient.zones.get(enterprise_url + ".")
return ret
def create_dns_names(self, ret):
enterprise_url = self.cloud_config['enterprise_url']
zone = ret['create_zones']['id']
for node in ret['nodes']:
to_deploy_name = node['name']
addresses = node['addresses']
# The DNS records must contain the GAME addresses (if they exist).
# Otherwise, any time an end point tries to refer to the node, it will use
# The control address, and send all the data over the control network.
address = addresses[-1]['addr']
print(f"Creating DNS zone {to_deploy_name}.{enterprise_url} = {address} ")
try:
node['dns_setup'] = self.designateClient.recordsets.create(zone, to_deploy_name, 'A', [address])
except designate_client.exceptions.Conflict as _: # noqa: F841
print(f"WARNING: already a DNS record for {to_deploy_name}")
return ret
def deploy_enterprise(self, enterprise):
ret = {'check_deploy_ok': self.check_deploy_ok(enterprise)}
if not ret['check_deploy_ok']:
errstr = "Found that deploying the network will conflict with existing setup."
raise RuntimeError(errstr)
ret = self.create_zones(ret)
ret = self.create_nodes(enterprise, ret)
ret = self.wait_for_ready(ret)
ret = self.collect_info(enterprise, ret)
ret = self.create_dns_names(ret)
return ret
def query_enterprise(self, enterprise):
ret = {'check_deploy_ok': self.query_deploy_ok(enterprise)}
if not ret['check_deploy_ok']:
errstr = "Found that the network is not fully deployed."
raise RuntimeError(errstr)
ret = self.query_zones(ret)
ret = self.query_nodes(enterprise, ret)
ret = self.collect_info(enterprise, ret)
ret = self.create_dns_names(ret)
return ret
def cleanup_enterprise(self, enterprise):
enterprise_url = self.cloud_config['enterprise_url']
zone = self.find_zone(enterprise_url)
if zone is not None:
self.designateClient.zones.delete(zone['id'])
for node in enterprise['nodes']:
to_deploy_name = node['name']
for instance_key in self.servers:
instance_name = self.servers[instance_key]['name']
if to_deploy_name.strip() == instance_name.strip():
print("Removing server " + instance_name + ".")
self.nova_sess.servers.delete(self.servers[instance_key]['id'])