blob: af814631b749acb615508563a7dd4dac0b4dacac [file] [log] [blame]
koder aka kdanilov4643fd62015-02-10 16:20:13 -08001import re
2import os
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +03003import stat
koder aka kdanilov4643fd62015-02-10 16:20:13 -08004import time
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +03005import os.path
koder aka kdanilove21d7472015-02-14 19:02:04 -08006import logging
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +02007import tempfile
8import subprocess
9import urllib.request
10from typing import Dict, Any, Iterable, Iterator, NamedTuple, Optional, List, Tuple
koder aka kdanilovfd2cfa52015-05-20 03:17:42 +030011from concurrent.futures import ThreadPoolExecutor
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -080012
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020013from keystoneauth1 import loading, session
koder aka kdanilov4500a5f2015-04-17 16:55:17 +030014from novaclient.exceptions import NotFound
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020015from novaclient.client import Client as NovaClient
16from cinderclient.client import Client as CinderClient
17from glanceclient import Client as GlanceClient
koder aka kdanilov4643fd62015-02-10 16:20:13 -080018
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020019
20from .utils import Timeout
21from .node_interfaces import NodeInfo
22
koder aka kdanilov34052012015-08-27 18:32:11 +030023
koder aka kdanilova94dfe12015-08-19 13:04:51 +030024__doc__ = """
25Module used to reliably spawn set of VM's, evenly distributed across
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020026compute servers in openstack cluster. Main functions:
koder aka kdanilova94dfe12015-08-19 13:04:51 +030027
28 get_OS_credentials - extract openstack credentials from different sources
29 nova_connect - connect to nova api
30 cinder_connect - connect to cinder api
31 find - find VM with given prefix in name
32 prepare_OS - prepare tenant for usage
33 launch_vms - reliably start set of VM in parallel with volumes and floating IP
34 clear_all - clear VM and volumes
35"""
36
koder aka kdanilov4643fd62015-02-10 16:20:13 -080037
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030038logger = logging.getLogger("wally.vms")
koder aka kdanilove21d7472015-02-14 19:02:04 -080039
40
koder aka kdanilov22d134e2016-11-08 11:33:19 +020041OSCreds = NamedTuple("OSCreds",
42 [("name", str),
43 ("passwd", str),
44 ("tenant", str),
45 ("auth_url", str),
46 ("insecure", bool)])
koder aka kdanilovb7197432015-07-15 00:40:43 +030047
48
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020049# TODO(koder): should correctly process different sources, not only env????
50def get_openstack_credentials() -> OSCreds:
51 is_insecure = os.environ.get('OS_INSECURE', 'false').lower() in ('true', 'yes')
52
53 return OSCreds(os.environ.get('OS_USERNAME'),
54 os.environ.get('OS_PASSWORD'),
55 os.environ.get('OS_TENANT_NAME'),
56 os.environ.get('OS_AUTH_URL'),
57 is_insecure)
koder aka kdanilov1c2b5112015-04-10 16:53:51 +030058
59
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020060class OSConnection:
61 def __init__(self, nova: NovaClient, cinder: CinderClient, glance: GlanceClient) -> None:
62 self.nova = nova
63 self.cinder = cinder
64 self.glance = glance
koder aka kdanilov1c2b5112015-04-10 16:53:51 +030065
66
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020067def os_connect(os_creds: OSCreds, version: str = "2") -> OSConnection:
68 loader = loading.get_plugin_loader('password')
69 auth = loader.load_from_options(auth_url=os_creds.auth_url,
70 username=os_creds.name,
71 password=os_creds.passwd,
72 project_id=os_creds.tenant)
73 auth_sess = session.Session(auth=auth)
koder aka kdanilove87ae652015-04-20 02:14:35 +030074
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020075 glance = GlanceClient(version, session=auth_sess)
76 nova = NovaClient(version, session=auth_sess)
77 cinder = CinderClient(os_creds.name, os_creds.passwd, os_creds.tenant, os_creds.auth_url,
78 insecure=os_creds.insecure, api_version=version)
79 return OSConnection(nova, cinder, glance)
koder aka kdanilove87ae652015-04-20 02:14:35 +030080
81
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020082def find_vms(conn: OSConnection, name_prefix: str) -> Iterable[str, int]:
83 for srv in conn.nova.servers.list():
koder aka kdanilovf86d7af2015-05-06 04:01:54 +030084 if srv.name.startswith(name_prefix):
85 for ips in srv.addresses.values():
86 for ip in ips:
87 if ip.get("OS-EXT-IPS:type", None) == 'floating':
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +030088 yield ip['addr'], srv.id
koder aka kdanilovf86d7af2015-05-06 04:01:54 +030089 break
90
91
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020092def pause(conn: OSConnection, ids: Iterable[int], executor: ThreadPoolExecutor) -> None:
93 def pause_vm(vm_id: str) -> None:
94 vm = conn.nova.servers.get(vm_id)
koder aka kdanilov416b87a2015-05-12 00:26:04 +030095 if vm.status == 'ACTIVE':
96 vm.pause()
97
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020098 for future in executor.map(pause_vm, ids):
99 future.result()
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300100
101
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200102def unpause(conn: OSConnection, ids: Iterable[int], executor: ThreadPoolExecutor, max_resume_time=10) -> None:
103 def unpause(vm_id: str) -> None:
104 vm = conn.nova.servers.get(vm_id)
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300105 if vm.status == 'PAUSED':
106 vm.unpause()
107
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200108 for _ in Timeout(max_resume_time):
109 vm = conn.nova.servers.get(vm_id)
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300110 if vm.status != 'PAUSED':
111 return
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300112 raise RuntimeError("Can't unpause vm {0}".format(vm_id))
113
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200114 for future in executor.map(unpause, ids):
115 future.result()
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300116
117
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200118def prepare_os(conn: OSConnection, params: Dict[str, Any], max_vm_per_node: int = 8) -> None:
koder aka kdanilov34052012015-08-27 18:32:11 +0300119 """prepare openstack for futher usage
120
121 Creates server groups, security rules, keypair, flavor
122 and upload VM image from web. In case if object with
123 given name already exists, skip preparation part.
124 Don't check, that existing object has required attributes
125
126 params:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200127 nova: OSConnection
koder aka kdanilov34052012015-08-27 18:32:11 +0300128 params: dict {
129 security_group:str - security group name with allowed ssh and ping
130 aa_group_name:str - template for anti-affinity group names. Should
131 receive one integer parameter, like "cbt_aa_{0}"
132 keypair_name: str - OS keypair name
133 keypair_file_public: str - path to public key file
134 keypair_file_private: str - path to private key file
135
136 flavor:dict - flavor params
137 name, ram_size, hdd_size, cpu_count
138 as for novaclient.Client.flavor.create call
139
140 image:dict - image params
141 'name': image name
142 'url': image url
143 }
144 os_creds: OSCreds
145 max_vm_per_compute: int=8 maximum expected amount of VM, per
146 compute host. Used to create appropriate
147 count of server groups for even placement
koder aka kdanilov34052012015-08-27 18:32:11 +0300148 """
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200149 allow_ssh_and_ping(conn, params['security_group'])
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300150
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200151 for idx in range(max_vm_per_node):
152 get_or_create_aa_group(conn, params['aa_group_name'].format(idx))
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300153
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200154 create_keypair(conn, params['keypair_name'], params['keypair_file_public'], params['keypair_file_private'])
155 create_image(conn, params['image']['name'], params['image']['url'])
156 create_flavor(conn, **params['flavor'])
koder aka kdanilov4e9f3ed2015-04-14 11:26:12 +0300157
158
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200159def create_keypair(conn: OSConnection, name: str, pub_key_path: str, priv_key_path: str):
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300160 """create and upload keypair into nova, if doesn't exists yet
161
162 Create and upload keypair into nova, if keypair with given bane
163 doesn't exists yet. Uses key from files, if file doesn't exists -
164 create new keys, and store'em into files.
165
166 parameters:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200167 conn: OSConnection
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300168 name: str - ketpair name
169 pub_key_path: str - path for public key
170 priv_key_path: str - path for private key
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300171 """
172
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300173 pub_key_exists = os.path.exists(pub_key_path)
174 priv_key_exists = os.path.exists(priv_key_path)
175
176 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200177 kpair = conn.nova.keypairs.find(name=name)
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300178 # if file not found- delete and recreate
179 except NotFound:
180 kpair = None
181
182 if pub_key_exists and not priv_key_exists:
183 raise EnvironmentError("Private key file doesn't exists")
184
185 if not pub_key_exists and priv_key_exists:
186 raise EnvironmentError("Public key file doesn't exists")
187
188 if kpair is None:
189 if pub_key_exists:
190 with open(pub_key_path) as pub_key_fd:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200191 return conn.nova.keypairs.create(name, pub_key_fd.read())
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300192 else:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200193 key = conn.nova.keypairs.create(name)
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300194
195 with open(priv_key_path, "w") as priv_key_fd:
196 priv_key_fd.write(key.private_key)
197 os.chmod(priv_key_path, stat.S_IREAD | stat.S_IWRITE)
198
199 with open(pub_key_path, "w") as pub_key_fd:
200 pub_key_fd.write(key.public_key)
201 elif not priv_key_exists:
202 raise EnvironmentError("Private key file doesn't exists," +
203 " but key uploaded openstack." +
204 " Either set correct path to private key" +
205 " or remove key from openstack")
206
207
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200208def get_or_create_aa_group(conn: OSConnection, name: str) -> int:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300209 """create anti-affinity server group, if doesn't exists yet
210
211 parameters:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200212 conn: OSConnection
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300213 name: str - group name
214
215 returns: str - group id
216 """
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300217 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200218 return conn.nova.server_groups.find(name=name).id
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300219 except NotFound:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200220 return conn.nova.server_groups.create(name=name, policies=['anti-affinity']).id
koder aka kdanilov652cd802015-04-13 12:21:07 +0300221
222
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200223def allow_ssh_and_ping(conn: OSConnection, group_name: str) -> int:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300224 """create sequrity group for ping and ssh
225
226 parameters:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200227 conn:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300228 group_name: str - group name
229
230 returns: str - group id
231 """
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300232 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200233 secgroup = conn.nova.security_groups.find(name=group_name)
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300234 except NotFound:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200235 secgroup = conn.nova.security_groups.create(group_name, "allow ssh/ping to node")
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300236
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200237 conn.nova.security_group_rules.create(secgroup.id,
238 ip_protocol="tcp",
239 from_port="22",
240 to_port="22",
241 cidr="0.0.0.0/0")
koder aka kdanilov652cd802015-04-13 12:21:07 +0300242
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200243 conn.nova.security_group_rules.create(secgroup.id,
244 ip_protocol="icmp",
245 from_port=-1,
246 cidr="0.0.0.0/0",
247 to_port=-1)
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300248 return secgroup.id
koder aka kdanilov652cd802015-04-13 12:21:07 +0300249
250
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200251def create_image(conn: OSConnection, name: str, url: str) -> None:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300252 """upload image into glance from given URL, if given image doesn't exisis yet
253
254 parameters:
255 nova: nova connection
256 os_creds: OSCreds object - openstack credentials, should be same,
257 as used when connectiong given novaclient
258 name: str - image name
259 url: str - image download url
260
261 returns: None
262 """
263 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200264 conn.nova.images.find(name=name)
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300265 return
266 except NotFound:
267 pass
268
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200269 ok = False
270 with tempfile.NamedTemporaryFile() as temp_fd:
271 try:
272 cmd = "wget --dns-timeout=30 --connect-timeout=30 --read-timeout=30 -o {} {}"
273 subprocess.check_call(cmd.format(temp_fd.name, url))
274 ok = True
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300275
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200276 # TODO(koder): add proper error handling
277 except Exception:
278 pass
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300279
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200280 if not ok:
281 urllib.request.urlretrieve(url, temp_fd.name)
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300282
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200283 image = conn.glance.images.create(name=name)
284 with open(temp_fd.name, 'rb') as fd:
285 conn.glance.images.upload(image.id, fd)
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300286
287
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200288def create_flavor(conn: OSConnection, name: str, ram_size: int, hdd_size: int, cpu_count: int) -> None:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300289 """create flavor, if doesn't exisis yet
290
291 parameters:
292 nova: nova connection
293 name: str - flavor name
294 ram_size: int - ram size (UNIT?)
295 hdd_size: int - root hdd size (UNIT?)
296 cpu_count: int - cpu cores
297
298 returns: None
299 """
300 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200301 conn.nova.flavors.find(name)
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300302 return
303 except NotFound:
304 pass
305
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200306 conn.nova.flavors.create(name, cpu_count, ram_size, hdd_size)
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300307
308
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200309def create_volume(conn: OSConnection, size: int, name: str) -> Any:
310 vol = conn.cinder.volumes.create(size=size, display_name=name)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800311 err_count = 0
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300312
koder aka kdanilove87ae652015-04-20 02:14:35 +0300313 while vol.status != 'available':
314 if vol.status == 'error':
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800315 if err_count == 3:
koder aka kdanilove21d7472015-02-14 19:02:04 -0800316 logger.critical("Fail to create volume")
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800317 raise RuntimeError("Fail to create volume")
318 else:
319 err_count += 1
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200320 conn.cinder.volumes.delete(vol)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800321 time.sleep(1)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200322 vol = conn.cinder.volumes.create(size=size, display_name=name)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800323 continue
324 time.sleep(1)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200325 vol = conn.cinder.volumes.get(vol.id)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800326 return vol
327
328
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200329def wait_for_server_active(conn: OSConnection, server: Any, timeout: int = 300)-> None:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300330 """waiting till server became active
331
332 parameters:
333 nova: nova connection
334 server: server object
335 timeout: int - seconds to wait till raise an exception
336
337 returns: None
338 """
339
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200340 for _ in Timeout(timeout, no_exc=True):
341 server_state = getattr(server, 'OS-EXT-STS:vm_state').lower()
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800342
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200343 if server_state == 'active':
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800344 return True
345
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200346 if server_state == 'error':
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800347 return False
348
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200349 server = conn.nova.servers.get(server)
350 return False
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800351
352
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800353class Allocate(object):
354 pass
355
356
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200357def get_floating_ips(conn: OSConnection, pool: Optional[str], amount: int) -> List[str]:
358 """allocate floating ips
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300359
360 parameters:
361 nova: nova connection
362 pool:str floating ip pool name
363 amount:int - ip count
364
365 returns: [ip object]
366 """
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200367 ip_list = conn.nova.floating_ips.list()
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800368
369 if pool is not None:
370 ip_list = [ip for ip in ip_list if ip.pool == pool]
371
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800372 return [ip for ip in ip_list if ip.instance_id is None][:amount]
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800373
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800374
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200375def launch_vms(conn: OSConnection,
376 params: Dict[str, Any],
377 executor: ThreadPoolExecutor,
378 already_has_count: int = 0) -> Iterator[NodeInfo]:
koder aka kdanilov34052012015-08-27 18:32:11 +0300379 """launch virtual servers
380
381 Parameters:
382 nova: nova client
383 params: dict {
384 count: str or int - server count. If count is string it should be in
385 one of bext forms: "=INT" or "xINT". First mean
386 to spawn (INT - already_has_count) servers, and
387 all should be evenly distributed across all compute
388 nodes. xINT mean spawn COMPUTE_COUNT * INT servers.
389 image: dict {'name': str - image name}
390 flavor: dict {'name': str - flavor name}
391 group_name: str - group name, used to create uniq server name
392 keypair_name: str - ssh keypais name
393 keypair_file_private: str - path to private key
394 user: str - vm user name
395 vol_sz: int or None - volume size, or None, if no volume
396 network_zone_name: str - network zone name
397 flt_ip_pool: str - floating ip pool
398 name_templ: str - server name template, should receive two parameters
399 'group and id, like 'cbt-{group}-{id}'
400 aa_group_name: str scheduler group name
401 security_group: str - security group name
402 }
403 already_has_count: int=0 - how many servers already exists. Used to distribute
404 new servers evenly across all compute nodes, taking
405 old server in accout
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200406 returns: generator of NodeInfo - server credentials, in format USER@IP:KEY_PATH
koder aka kdanilov34052012015-08-27 18:32:11 +0300407
408 """
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300409 logger.debug("Calculating new vm count")
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200410 count = params['count'] # type: int
411 lst = conn.nova.services.list(binary='nova-compute')
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300412 srv_count = len([srv for srv in lst if srv.status == 'enabled'])
koder aka kdanilovda45e882015-04-06 02:24:42 +0300413
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200414 if isinstance(count, str):
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300415 if count.startswith("x"):
416 count = srv_count * int(count[1:])
417 else:
418 assert count.startswith('=')
419 count = int(count[1:]) - already_has_count
420
421 if count <= 0:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300422 logger.debug("Not need new vms")
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300423 return
koder aka kdanilovda45e882015-04-06 02:24:42 +0300424
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300425 logger.debug("Starting new nodes on openstack")
426
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200427 assert isinstance(count, int)
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300428
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300429 srv_params = "img: {image[name]}, flavor: {flavor[name]}".format(**params)
koder aka kdanilov66839a92015-04-11 13:22:31 +0300430 msg_templ = "Will start {0} servers with next params: {1}"
koder aka kdanilovcee43342015-04-14 22:52:53 +0300431 logger.info(msg_templ.format(count, srv_params))
koder aka kdanilovda45e882015-04-06 02:24:42 +0300432
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300433 vm_params = dict(
434 img_name=params['image']['name'],
435 flavor_name=params['flavor']['name'],
436 group_name=params['group_name'],
437 keypair_name=params['keypair_name'],
438 vol_sz=params.get('vol_sz'),
439 network_zone_name=params.get("network_zone_name"),
440 flt_ip_pool=params.get('flt_ip_pool'),
441 name_templ=params.get('name_templ'),
442 scheduler_hints={"group": params['aa_group_name']},
443 security_group=params['security_group'],
444 sec_group_size=srv_count
445 )
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300446
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300447 # precache all errors before start creating vms
448 private_key_path = params['keypair_file_private']
449 creds = params['image']['creds']
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300450
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200451 for ip, os_node in create_vms_mt(conn, count, executor, **vm_params):
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300452 conn_uri = creds.format(ip=ip, private_key_path=private_key_path)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200453 info = NodeInfo(conn_uri, set())
454 info.os_vm_id = os_node.id
455 yield info
koder aka kdanilovda45e882015-04-06 02:24:42 +0300456
457
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200458def get_free_server_groups(conn: OSConnection, template: str) -> Iterator[str]:
koder aka kdanilov34052012015-08-27 18:32:11 +0300459 """get fre server groups, that match given name template
460
461 parameters:
462 nova: nova connection
463 template:str - name template
464 amount:int - ip count
465
466 returns: generator or str - server group names
467 """
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200468 for server_group in conn.nova.server_groups.list():
469 if not server_group.members:
470 if re.match(template, server_group.name):
471 yield str(server_group.id)
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300472
473
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200474def create_vms_mt(conn: OSConnection,
475 amount: int,
476 executor: ThreadPoolExecutor,
477 group_name: str,
478 keypair_name: str,
479 img_name: str,
480 flavor_name: str,
481 vol_sz: int = None,
482 network_zone_name: str = None,
483 flt_ip_pool: str = None,
484 name_templ: str ='wally-{id}',
485 scheduler_hints: Dict = None,
486 security_group: str = None,
487 sec_group_size: int = None) -> List[Tuple[str, Any]]:
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800488
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200489 if network_zone_name is not None:
490 network_future = executor.submit(conn.nova.networks.find,
491 label=network_zone_name)
492 else:
493 network_future = None
koder aka kdanilov97644f92015-02-13 11:11:08 -0800494
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200495 fl_future = executor.submit(conn.nova.flavors.find, name=flavor_name)
496 img_future = executor.submit(conn.nova.images.find, name=img_name)
koder aka kdanilov97644f92015-02-13 11:11:08 -0800497
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200498 if flt_ip_pool is not None:
499 ips_future = executor.submit(get_floating_ips,
500 conn, flt_ip_pool, amount)
501 logger.debug("Wait for floating ip")
502 ips = ips_future.result()
503 ips += [Allocate] * (amount - len(ips))
504 else:
505 ips = [None] * amount
koder aka kdanilov97644f92015-02-13 11:11:08 -0800506
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200507 logger.debug("Getting flavor object")
508 fl = fl_future.result()
509 logger.debug("Getting image object")
510 img = img_future.result()
koder aka kdanilov97644f92015-02-13 11:11:08 -0800511
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200512 if network_future is not None:
513 logger.debug("Waiting for network results")
514 nics = [{'net-id': network_future.result().id}]
515 else:
516 nics = None
koder aka kdanilov97644f92015-02-13 11:11:08 -0800517
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200518 names = [] # type: List[str]
519 for i in range(amount):
520 names.append(name_templ.format(group=group_name, id=i))
koder aka kdanilov97644f92015-02-13 11:11:08 -0800521
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200522 futures = []
523 logger.debug("Requesting new vm's")
koder aka kdanilov6e2ae792015-03-04 18:02:24 -0800524
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200525 orig_scheduler_hints = scheduler_hints.copy()
526 group_name_template = scheduler_hints['group'].format("\\d+")
527 groups = list(get_free_server_groups(conn, group_name_template + "$"))
528 groups.sort()
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300529
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200530 for idx, (name, flt_ip) in enumerate(zip(names, ips), 2):
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300531
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200532 scheduler_hints = None
533 if orig_scheduler_hints is not None and sec_group_size is not None:
534 if "group" in orig_scheduler_hints:
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300535 scheduler_hints = orig_scheduler_hints.copy()
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200536 scheduler_hints['group'] = groups[idx // sec_group_size]
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300537
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200538 if scheduler_hints is None:
539 scheduler_hints = orig_scheduler_hints.copy()
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800540
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200541 params = (conn, name, keypair_name, img, fl,
542 nics, vol_sz, flt_ip, scheduler_hints,
543 flt_ip_pool, [security_group])
544
545 futures.append(executor.submit(create_vm, *params))
546 res = [future.result() for future in futures]
547 logger.debug("Done spawning")
548 return res
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800549
550
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200551def create_vm(conn: OSConnection,
552 name: str,
553 keypair_name: str,
554 img: Any,
555 flavor: Any,
556 nics: List,
557 vol_sz: int = None,
558 flt_ip: Any = False,
559 scheduler_hints: Dict = None,
560 pool: str = None,
561 security_groups=None,
562 max_retry: int = 3,
563 delete_timeout: int = 120) -> Tuple[str, Any]:
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800564
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200565 # make mypy/pylint happy
566 srv = None # type: Any
567 for i in range(max_retry):
568 srv = conn.nova.servers.create(name, flavor=flavor, image=img, nics=nics, key_name=keypair_name,
569 scheduler_hints=scheduler_hints, security_groups=security_groups)
570
571 if not wait_for_server_active(conn, srv):
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800572 msg = "Server {0} fails to start. Kill it and try again"
koder aka kdanilove21d7472015-02-14 19:02:04 -0800573 logger.debug(msg.format(srv))
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200574 conn.nova.servers.delete(srv)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800575
koder aka kdanilovf86d7af2015-05-06 04:01:54 +0300576 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200577 for _ in Timeout(delete_timeout, "Server {0} delete timeout".format(srv.id)):
578 srv = conn.nova.servers.get(srv.id)
koder aka kdanilovf86d7af2015-05-06 04:01:54 +0300579 except NotFound:
580 pass
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800581 else:
582 break
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300583 else:
584 raise RuntimeError("Failed to start server".format(srv.id))
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800585
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800586 if vol_sz is not None:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200587 vol = create_volume(conn, vol_sz, name)
588 conn.nova.volumes.create_server_volume(srv.id, vol.id, None)
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800589
590 if flt_ip is Allocate:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200591 flt_ip = conn.nova.floating_ips.create(pool)
koder aka kdanilovda45e882015-04-06 02:24:42 +0300592
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800593 if flt_ip is not None:
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800594 srv.add_floating_ip(flt_ip)
Yulia Portnova0e64ea22015-03-20 17:27:22 +0200595
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200596 return flt_ip.ip, conn.nova.servers.get(srv.id)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800597
598
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200599def clear_nodes(conn: OSConnection,
600 ids: List[int] = None,
601 name_templ: str = None,
602 max_server_delete_time: int = 120):
koder aka kdanilov765920a2016-04-12 00:35:48 +0300603 try:
604 def need_delete(srv):
605 if name_templ is not None:
606 return re.match(name_templ.format("\\d+"), srv.name) is not None
607 else:
608 return srv.id in ids
609
610 volumes_to_delete = []
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200611 for vol in conn.cinder.volumes.list():
koder aka kdanilov765920a2016-04-12 00:35:48 +0300612 for attachment in vol.attachments:
613 if attachment['server_id'] in ids:
614 volumes_to_delete.append(vol)
615 break
616
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200617 still_alive = set()
618 for srv in conn.nova.servers.list():
koder aka kdanilov765920a2016-04-12 00:35:48 +0300619 if need_delete(srv):
620 logger.debug("Deleting server {0}".format(srv.name))
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200621 conn.nova.servers.delete(srv)
622 still_alive.add(srv.id)
koder aka kdanilov765920a2016-04-12 00:35:48 +0300623
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200624 if still_alive:
625 logger.debug("Waiting till all servers are actually deleted")
626 tout = Timeout(max_server_delete_time, no_exc=True)
627 while tout.tick() and still_alive:
628 all_id = set(srv.id for srv in conn.nova.servers.list())
629 still_alive = still_alive.intersection(all_id)
koder aka kdanilove87ae652015-04-20 02:14:35 +0300630
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200631 if still_alive:
632 logger.warning("Failed to remove servers {}. ".format(",".join(still_alive)) +
633 "You, probably, need to remove them manually (and volumes as well)")
634 return
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800635
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200636 if volumes_to_delete:
637 logger.debug("Deleting volumes")
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800638
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200639 # wait till vm actually deleted
640
641 # logger.warning("Volume deletion commented out")
642 for vol in volumes_to_delete:
643 logger.debug("Deleting volume " + vol.display_name)
644 conn.cinder.volumes.delete(vol)
645
646 logger.debug("Clearing complete (yet some volumes may still be deleting)")
647 except Exception:
koder aka kdanilov765920a2016-04-12 00:35:48 +0300648 logger.exception("During removing servers. " +
649 "You, probably, need to remove them manually")