blob: b1d91723f80052f03cf7f95c0183274e5c3211d0 [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
kdanylov aka koder13e58452018-07-15 02:51:51 +030010from typing import Dict, Iterable, Iterator, NamedTuple, Optional, List, Tuple, Any
11from concurrent.futures import ThreadPoolExecutor, Future
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
kdanylov aka koderb0833332017-05-13 20:39:17 +030019from cephlib.common import Timeout, to_ip
kdanylov aka koder026e5f22017-05-15 01:04:39 +030020from cephlib.node import NodeInfo
21from cephlib.ssh import ConnCreds
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020022
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
koder aka kdanilov73084622016-11-16 21:51:08 +020028 get_openstack_credentials - extract openstack credentials from different sources
29 os_connect - connect to nova, cinder and glance API
30 find_vms - find VM's with given prefix in name
31 prepare_os - prepare tenant for usage
koder aka kdanilova94dfe12015-08-19 13:04:51 +030032 launch_vms - reliably start set of VM in parallel with volumes and floating IP
koder aka kdanilov73084622016-11-16 21:51:08 +020033 clear_nodes - clear VM and volumes
koder aka kdanilova94dfe12015-08-19 13:04:51 +030034"""
35
koder aka kdanilov4643fd62015-02-10 16:20:13 -080036
koder aka kdanilov962ee5f2016-12-19 02:40:08 +020037logger = logging.getLogger("wally")
koder aka kdanilove21d7472015-02-14 19:02:04 -080038
39
koder aka kdanilov22d134e2016-11-08 11:33:19 +020040OSCreds = NamedTuple("OSCreds",
41 [("name", str),
42 ("passwd", str),
43 ("tenant", str),
44 ("auth_url", str),
45 ("insecure", bool)])
koder aka kdanilovb7197432015-07-15 00:40:43 +030046
47
kdanylov aka koder13e58452018-07-15 02:51:51 +030048def get_openstack_credentials_from_env() -> OSCreds:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020049 is_insecure = os.environ.get('OS_INSECURE', 'false').lower() in ('true', 'yes')
kdanylov aka koder13e58452018-07-15 02:51:51 +030050 try:
51 return OSCreds(os.environ['OS_USERNAME'],
52 os.environ['OS_PASSWORD'],
53 os.environ['OS_TENANT_NAME'],
54 os.environ['OS_AUTH_URL'],
55 is_insecure)
56 except KeyError:
57 logger.error("One of openstack enviroment variable is not defined - check for " +
58 "OS_USERNAME, OS_PASSWORD, OS_TENANT_NAME, OS_AUTH_URL")
59 raise
koder aka kdanilov1c2b5112015-04-10 16:53:51 +030060
61
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020062class OSConnection:
63 def __init__(self, nova: NovaClient, cinder: CinderClient, glance: GlanceClient) -> None:
64 self.nova = nova
65 self.cinder = cinder
66 self.glance = glance
koder aka kdanilov1c2b5112015-04-10 16:53:51 +030067
68
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020069def os_connect(os_creds: OSCreds, version: str = "2") -> OSConnection:
70 loader = loading.get_plugin_loader('password')
71 auth = loader.load_from_options(auth_url=os_creds.auth_url,
72 username=os_creds.name,
73 password=os_creds.passwd,
74 project_id=os_creds.tenant)
75 auth_sess = session.Session(auth=auth)
koder aka kdanilove87ae652015-04-20 02:14:35 +030076
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020077 glance = GlanceClient(version, session=auth_sess)
78 nova = NovaClient(version, session=auth_sess)
79 cinder = CinderClient(os_creds.name, os_creds.passwd, os_creds.tenant, os_creds.auth_url,
80 insecure=os_creds.insecure, api_version=version)
81 return OSConnection(nova, cinder, glance)
koder aka kdanilove87ae652015-04-20 02:14:35 +030082
83
koder aka kdanilov73084622016-11-16 21:51:08 +020084def find_vms(conn: OSConnection, name_prefix: str) -> Iterable[Tuple[str, int]]:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020085 for srv in conn.nova.servers.list():
koder aka kdanilovf86d7af2015-05-06 04:01:54 +030086 if srv.name.startswith(name_prefix):
koder aka kdanilov73084622016-11-16 21:51:08 +020087 # need to exit after found server first external IP
88 # so have to rollout two cycles to avoid using exceptions
89 all_ip = [] # type: List[Any]
koder aka kdanilovf86d7af2015-05-06 04:01:54 +030090 for ips in srv.addresses.values():
koder aka kdanilov73084622016-11-16 21:51:08 +020091 all_ip.extend(ips)
92
93 for ip in all_ip:
94 if ip.get("OS-EXT-IPS:type", None) == 'floating':
95 yield ip['addr'], srv.id
96 break
koder aka kdanilovf86d7af2015-05-06 04:01:54 +030097
98
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020099def pause(conn: OSConnection, ids: Iterable[int], executor: ThreadPoolExecutor) -> None:
100 def pause_vm(vm_id: str) -> None:
101 vm = conn.nova.servers.get(vm_id)
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300102 if vm.status == 'ACTIVE':
103 vm.pause()
104
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200105 for future in executor.map(pause_vm, ids):
kdanylov aka koder13e58452018-07-15 02:51:51 +0300106 future.result() # type: ignore
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300107
108
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200109def unpause(conn: OSConnection, ids: Iterable[int], executor: ThreadPoolExecutor, max_resume_time=10) -> None:
110 def unpause(vm_id: str) -> None:
111 vm = conn.nova.servers.get(vm_id)
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300112 if vm.status == 'PAUSED':
113 vm.unpause()
114
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200115 for _ in Timeout(max_resume_time):
116 vm = conn.nova.servers.get(vm_id)
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300117 if vm.status != 'PAUSED':
118 return
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300119 raise RuntimeError("Can't unpause vm {0}".format(vm_id))
120
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200121 for future in executor.map(unpause, ids):
kdanylov aka koder13e58452018-07-15 02:51:51 +0300122 future.result() # type: ignore
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300123
124
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200125def prepare_os(conn: OSConnection, params: Dict[str, Any], max_vm_per_node: int = 8) -> None:
koder aka kdanilov34052012015-08-27 18:32:11 +0300126 """prepare openstack for futher usage
127
128 Creates server groups, security rules, keypair, flavor
129 and upload VM image from web. In case if object with
130 given name already exists, skip preparation part.
131 Don't check, that existing object has required attributes
132
133 params:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200134 nova: OSConnection
koder aka kdanilov34052012015-08-27 18:32:11 +0300135 params: dict {
136 security_group:str - security group name with allowed ssh and ping
137 aa_group_name:str - template for anti-affinity group names. Should
138 receive one integer parameter, like "cbt_aa_{0}"
139 keypair_name: str - OS keypair name
140 keypair_file_public: str - path to public key file
141 keypair_file_private: str - path to private key file
142
143 flavor:dict - flavor params
144 name, ram_size, hdd_size, cpu_count
145 as for novaclient.Client.flavor.create call
146
147 image:dict - image params
148 'name': image name
149 'url': image url
150 }
151 os_creds: OSCreds
152 max_vm_per_compute: int=8 maximum expected amount of VM, per
153 compute host. Used to create appropriate
154 count of server groups for even placement
koder aka kdanilov34052012015-08-27 18:32:11 +0300155 """
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200156 allow_ssh_and_ping(conn, params['security_group'])
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300157
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200158 for idx in range(max_vm_per_node):
159 get_or_create_aa_group(conn, params['aa_group_name'].format(idx))
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300160
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200161 create_keypair(conn, params['keypair_name'], params['keypair_file_public'], params['keypair_file_private'])
162 create_image(conn, params['image']['name'], params['image']['url'])
163 create_flavor(conn, **params['flavor'])
koder aka kdanilov4e9f3ed2015-04-14 11:26:12 +0300164
165
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200166def create_keypair(conn: OSConnection, name: str, pub_key_path: str, priv_key_path: str):
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300167 """create and upload keypair into nova, if doesn't exists yet
168
169 Create and upload keypair into nova, if keypair with given bane
170 doesn't exists yet. Uses key from files, if file doesn't exists -
171 create new keys, and store'em into files.
172
173 parameters:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200174 conn: OSConnection
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300175 name: str - ketpair name
176 pub_key_path: str - path for public key
177 priv_key_path: str - path for private key
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300178 """
179
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300180 pub_key_exists = os.path.exists(pub_key_path)
181 priv_key_exists = os.path.exists(priv_key_path)
182
183 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200184 kpair = conn.nova.keypairs.find(name=name)
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300185 # if file not found- delete and recreate
186 except NotFound:
187 kpair = None
188
189 if pub_key_exists and not priv_key_exists:
190 raise EnvironmentError("Private key file doesn't exists")
191
192 if not pub_key_exists and priv_key_exists:
193 raise EnvironmentError("Public key file doesn't exists")
194
195 if kpair is None:
196 if pub_key_exists:
197 with open(pub_key_path) as pub_key_fd:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200198 return conn.nova.keypairs.create(name, pub_key_fd.read())
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300199 else:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200200 key = conn.nova.keypairs.create(name)
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300201
202 with open(priv_key_path, "w") as priv_key_fd:
203 priv_key_fd.write(key.private_key)
204 os.chmod(priv_key_path, stat.S_IREAD | stat.S_IWRITE)
205
206 with open(pub_key_path, "w") as pub_key_fd:
207 pub_key_fd.write(key.public_key)
208 elif not priv_key_exists:
209 raise EnvironmentError("Private key file doesn't exists," +
210 " but key uploaded openstack." +
211 " Either set correct path to private key" +
212 " or remove key from openstack")
213
214
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200215def get_or_create_aa_group(conn: OSConnection, name: str) -> int:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300216 """create anti-affinity server group, if doesn't exists yet
217
218 parameters:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200219 conn: OSConnection
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300220 name: str - group name
221
222 returns: str - group id
223 """
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300224 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200225 return conn.nova.server_groups.find(name=name).id
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300226 except NotFound:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200227 return conn.nova.server_groups.create(name=name, policies=['anti-affinity']).id
koder aka kdanilov652cd802015-04-13 12:21:07 +0300228
229
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200230def allow_ssh_and_ping(conn: OSConnection, group_name: str) -> int:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300231 """create sequrity group for ping and ssh
232
233 parameters:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200234 conn:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300235 group_name: str - group name
236
237 returns: str - group id
238 """
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300239 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200240 secgroup = conn.nova.security_groups.find(name=group_name)
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300241 except NotFound:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200242 secgroup = conn.nova.security_groups.create(group_name, "allow ssh/ping to node")
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300243
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200244 conn.nova.security_group_rules.create(secgroup.id,
245 ip_protocol="tcp",
246 from_port="22",
247 to_port="22",
248 cidr="0.0.0.0/0")
koder aka kdanilov652cd802015-04-13 12:21:07 +0300249
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200250 conn.nova.security_group_rules.create(secgroup.id,
251 ip_protocol="icmp",
252 from_port=-1,
253 cidr="0.0.0.0/0",
254 to_port=-1)
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300255 return secgroup.id
koder aka kdanilov652cd802015-04-13 12:21:07 +0300256
257
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200258def create_image(conn: OSConnection, name: str, url: str) -> None:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300259 """upload image into glance from given URL, if given image doesn't exisis yet
260
261 parameters:
262 nova: nova connection
263 os_creds: OSCreds object - openstack credentials, should be same,
264 as used when connectiong given novaclient
265 name: str - image name
266 url: str - image download url
267
268 returns: None
269 """
270 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200271 conn.nova.images.find(name=name)
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300272 return
273 except NotFound:
274 pass
275
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200276 ok = False
277 with tempfile.NamedTemporaryFile() as temp_fd:
278 try:
279 cmd = "wget --dns-timeout=30 --connect-timeout=30 --read-timeout=30 -o {} {}"
280 subprocess.check_call(cmd.format(temp_fd.name, url))
281 ok = True
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300282
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200283 # TODO(koder): add proper error handling
284 except Exception:
285 pass
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300286
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200287 if not ok:
288 urllib.request.urlretrieve(url, temp_fd.name)
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300289
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200290 image = conn.glance.images.create(name=name)
291 with open(temp_fd.name, 'rb') as fd:
292 conn.glance.images.upload(image.id, fd)
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300293
294
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200295def create_flavor(conn: OSConnection, name: str, ram_size: int, hdd_size: int, cpu_count: int) -> None:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300296 """create flavor, if doesn't exisis yet
297
298 parameters:
299 nova: nova connection
300 name: str - flavor name
301 ram_size: int - ram size (UNIT?)
302 hdd_size: int - root hdd size (UNIT?)
303 cpu_count: int - cpu cores
304
305 returns: None
306 """
307 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200308 conn.nova.flavors.find(name)
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300309 return
310 except NotFound:
311 pass
312
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200313 conn.nova.flavors.create(name, cpu_count, ram_size, hdd_size)
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300314
315
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200316def create_volume(conn: OSConnection, size: int, name: str) -> Any:
317 vol = conn.cinder.volumes.create(size=size, display_name=name)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800318 err_count = 0
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300319
koder aka kdanilove87ae652015-04-20 02:14:35 +0300320 while vol.status != 'available':
321 if vol.status == 'error':
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800322 if err_count == 3:
koder aka kdanilove21d7472015-02-14 19:02:04 -0800323 logger.critical("Fail to create volume")
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800324 raise RuntimeError("Fail to create volume")
325 else:
326 err_count += 1
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200327 conn.cinder.volumes.delete(vol)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800328 time.sleep(1)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200329 vol = conn.cinder.volumes.create(size=size, display_name=name)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800330 continue
331 time.sleep(1)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200332 vol = conn.cinder.volumes.get(vol.id)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800333 return vol
334
335
koder aka kdanilov73084622016-11-16 21:51:08 +0200336def wait_for_server_active(conn: OSConnection, server: Any, timeout: int = 300) -> bool:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300337 """waiting till server became active
338
339 parameters:
340 nova: nova connection
341 server: server object
342 timeout: int - seconds to wait till raise an exception
343
344 returns: None
345 """
346
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200347 for _ in Timeout(timeout, no_exc=True):
348 server_state = getattr(server, 'OS-EXT-STS:vm_state').lower()
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800349
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200350 if server_state == 'active':
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800351 return True
352
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200353 if server_state == 'error':
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800354 return False
355
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200356 server = conn.nova.servers.get(server)
357 return False
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800358
359
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800360class Allocate(object):
361 pass
362
363
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200364def get_floating_ips(conn: OSConnection, pool: Optional[str], amount: int) -> List[str]:
365 """allocate floating ips
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300366
367 parameters:
368 nova: nova connection
369 pool:str floating ip pool name
370 amount:int - ip count
371
372 returns: [ip object]
373 """
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200374 ip_list = conn.nova.floating_ips.list()
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800375
376 if pool is not None:
377 ip_list = [ip for ip in ip_list if ip.pool == pool]
378
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800379 return [ip for ip in ip_list if ip.instance_id is None][:amount]
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800380
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800381
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200382def launch_vms(conn: OSConnection,
383 params: Dict[str, Any],
384 executor: ThreadPoolExecutor,
385 already_has_count: int = 0) -> Iterator[NodeInfo]:
koder aka kdanilov34052012015-08-27 18:32:11 +0300386 """launch virtual servers
387
388 Parameters:
389 nova: nova client
390 params: dict {
391 count: str or int - server count. If count is string it should be in
392 one of bext forms: "=INT" or "xINT". First mean
393 to spawn (INT - already_has_count) servers, and
394 all should be evenly distributed across all compute
395 nodes. xINT mean spawn COMPUTE_COUNT * INT servers.
396 image: dict {'name': str - image name}
397 flavor: dict {'name': str - flavor name}
398 group_name: str - group name, used to create uniq server name
399 keypair_name: str - ssh keypais name
400 keypair_file_private: str - path to private key
401 user: str - vm user name
402 vol_sz: int or None - volume size, or None, if no volume
403 network_zone_name: str - network zone name
404 flt_ip_pool: str - floating ip pool
405 name_templ: str - server name template, should receive two parameters
406 'group and id, like 'cbt-{group}-{id}'
407 aa_group_name: str scheduler group name
408 security_group: str - security group name
409 }
410 already_has_count: int=0 - how many servers already exists. Used to distribute
411 new servers evenly across all compute nodes, taking
412 old server in accout
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200413 returns: generator of NodeInfo - server credentials, in format USER@IP:KEY_PATH
koder aka kdanilov34052012015-08-27 18:32:11 +0300414
415 """
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300416 logger.debug("Calculating new vm count")
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200417 count = params['count'] # type: int
418 lst = conn.nova.services.list(binary='nova-compute')
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300419 srv_count = len([srv for srv in lst if srv.status == 'enabled'])
koder aka kdanilovda45e882015-04-06 02:24:42 +0300420
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200421 if isinstance(count, str):
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300422 if count.startswith("x"):
423 count = srv_count * int(count[1:])
424 else:
425 assert count.startswith('=')
426 count = int(count[1:]) - already_has_count
427
428 if count <= 0:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300429 logger.debug("Not need new vms")
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300430 return
koder aka kdanilovda45e882015-04-06 02:24:42 +0300431
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300432 logger.debug("Starting new nodes on openstack")
433
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200434 assert isinstance(count, int)
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300435
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300436 srv_params = "img: {image[name]}, flavor: {flavor[name]}".format(**params)
koder aka kdanilov66839a92015-04-11 13:22:31 +0300437 msg_templ = "Will start {0} servers with next params: {1}"
koder aka kdanilovcee43342015-04-14 22:52:53 +0300438 logger.info(msg_templ.format(count, srv_params))
koder aka kdanilovda45e882015-04-06 02:24:42 +0300439
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300440 vm_params = dict(
441 img_name=params['image']['name'],
442 flavor_name=params['flavor']['name'],
443 group_name=params['group_name'],
444 keypair_name=params['keypair_name'],
445 vol_sz=params.get('vol_sz'),
446 network_zone_name=params.get("network_zone_name"),
447 flt_ip_pool=params.get('flt_ip_pool'),
448 name_templ=params.get('name_templ'),
449 scheduler_hints={"group": params['aa_group_name']},
450 security_group=params['security_group'],
451 sec_group_size=srv_count
452 )
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300453
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300454 # precache all errors before start creating vms
455 private_key_path = params['keypair_file_private']
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200456 user = params['image']['user']
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300457
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200458 for ip, os_node in create_vms_mt(conn, count, executor, **vm_params):
kdanylov aka koderb0833332017-05-13 20:39:17 +0300459 node_ip = to_ip(ip)
460 if ip != node_ip:
461 logger.info("Will use ip_addr %r instead of hostname %r", node_ip, ip)
462 info = NodeInfo(ConnCreds(node_ip, user, key_file=private_key_path), set())
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200463 info.os_vm_id = os_node.id
464 yield info
koder aka kdanilovda45e882015-04-06 02:24:42 +0300465
466
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200467def get_free_server_groups(conn: OSConnection, template: str) -> Iterator[str]:
koder aka kdanilov34052012015-08-27 18:32:11 +0300468 """get fre server groups, that match given name template
469
470 parameters:
471 nova: nova connection
472 template:str - name template
473 amount:int - ip count
474
475 returns: generator or str - server group names
476 """
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200477 for server_group in conn.nova.server_groups.list():
478 if not server_group.members:
479 if re.match(template, server_group.name):
480 yield str(server_group.id)
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300481
482
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200483def create_vms_mt(conn: OSConnection,
484 amount: int,
485 executor: ThreadPoolExecutor,
486 group_name: str,
487 keypair_name: str,
488 img_name: str,
489 flavor_name: str,
490 vol_sz: int = None,
491 network_zone_name: str = None,
492 flt_ip_pool: str = None,
493 name_templ: str ='wally-{id}',
494 scheduler_hints: Dict = None,
495 security_group: str = None,
496 sec_group_size: int = None) -> List[Tuple[str, Any]]:
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800497
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200498 if network_zone_name is not None:
kdanylov aka koder13e58452018-07-15 02:51:51 +0300499 network_future: Optional[Future] = executor.submit(conn.nova.networks.find,
500 label=network_zone_name)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200501 else:
502 network_future = None
koder aka kdanilov97644f92015-02-13 11:11:08 -0800503
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200504 fl_future = executor.submit(conn.nova.flavors.find, name=flavor_name)
505 img_future = executor.submit(conn.nova.images.find, name=img_name)
koder aka kdanilov97644f92015-02-13 11:11:08 -0800506
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200507 if flt_ip_pool is not None:
508 ips_future = executor.submit(get_floating_ips,
509 conn, flt_ip_pool, amount)
510 logger.debug("Wait for floating ip")
kdanylov aka koder13e58452018-07-15 02:51:51 +0300511 ips: List[Any] = ips_future.result()
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200512 ips += [Allocate] * (amount - len(ips))
513 else:
514 ips = [None] * amount
koder aka kdanilov97644f92015-02-13 11:11:08 -0800515
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200516 logger.debug("Getting flavor object")
517 fl = fl_future.result()
518 logger.debug("Getting image object")
519 img = img_future.result()
koder aka kdanilov97644f92015-02-13 11:11:08 -0800520
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200521 if network_future is not None:
522 logger.debug("Waiting for network results")
kdanylov aka koder13e58452018-07-15 02:51:51 +0300523 nics: Any = [{'net-id': network_future.result().id}]
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200524 else:
525 nics = None
koder aka kdanilov97644f92015-02-13 11:11:08 -0800526
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200527 names = [] # type: List[str]
528 for i in range(amount):
529 names.append(name_templ.format(group=group_name, id=i))
koder aka kdanilov97644f92015-02-13 11:11:08 -0800530
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200531 futures = []
532 logger.debug("Requesting new vm's")
koder aka kdanilov6e2ae792015-03-04 18:02:24 -0800533
kdanylov aka koder13e58452018-07-15 02:51:51 +0300534 if scheduler_hints:
535 orig_scheduler_hints = scheduler_hints.copy() # type: ignore
536 group_name_template = scheduler_hints['group'].format("\\d+")
537 groups = list(get_free_server_groups(conn, group_name_template + "$"))
538 groups.sort()
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300539
kdanylov aka koder13e58452018-07-15 02:51:51 +0300540 for idx, (name, flt_ip) in enumerate(zip(names, ips), 2):
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300541
kdanylov aka koder13e58452018-07-15 02:51:51 +0300542 scheduler_hints = None
543 if orig_scheduler_hints is not None and sec_group_size is not None:
544 if "group" in orig_scheduler_hints:
545 scheduler_hints = orig_scheduler_hints.copy()
546 scheduler_hints['group'] = groups[idx // sec_group_size]
547
548 if scheduler_hints is None:
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300549 scheduler_hints = orig_scheduler_hints.copy()
550
kdanylov aka koder13e58452018-07-15 02:51:51 +0300551 params = (conn, name, keypair_name, img, fl,
552 nics, vol_sz, flt_ip, scheduler_hints,
553 flt_ip_pool, [security_group])
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800554
kdanylov aka koder13e58452018-07-15 02:51:51 +0300555 futures.append(executor.submit(create_vm, *params))
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200556 res = [future.result() for future in futures]
557 logger.debug("Done spawning")
558 return res
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800559
560
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200561def create_vm(conn: OSConnection,
562 name: str,
563 keypair_name: str,
564 img: Any,
565 flavor: Any,
566 nics: List,
567 vol_sz: int = None,
568 flt_ip: Any = False,
569 scheduler_hints: Dict = None,
570 pool: str = None,
571 security_groups=None,
572 max_retry: int = 3,
573 delete_timeout: int = 120) -> Tuple[str, Any]:
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800574
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200575 # make mypy/pylint happy
kdanylov aka koder13e58452018-07-15 02:51:51 +0300576 srv: Any = None
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200577 for i in range(max_retry):
578 srv = conn.nova.servers.create(name, flavor=flavor, image=img, nics=nics, key_name=keypair_name,
579 scheduler_hints=scheduler_hints, security_groups=security_groups)
580
581 if not wait_for_server_active(conn, srv):
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200582 logger.debug("Server {} fails to start. Kill it and try again".format(srv))
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200583 conn.nova.servers.delete(srv)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800584
koder aka kdanilovf86d7af2015-05-06 04:01:54 +0300585 try:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200586 for _ in Timeout(delete_timeout, "Server {} delete timeout".format(srv.id)):
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200587 srv = conn.nova.servers.get(srv.id)
koder aka kdanilovf86d7af2015-05-06 04:01:54 +0300588 except NotFound:
589 pass
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800590 else:
591 break
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300592 else:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200593 raise RuntimeError("Failed to start server {}".format(srv.id))
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800594
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800595 if vol_sz is not None:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200596 vol = create_volume(conn, vol_sz, name)
597 conn.nova.volumes.create_server_volume(srv.id, vol.id, None)
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800598
599 if flt_ip is Allocate:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200600 flt_ip = conn.nova.floating_ips.create(pool)
koder aka kdanilovda45e882015-04-06 02:24:42 +0300601
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800602 if flt_ip is not None:
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800603 srv.add_floating_ip(flt_ip)
Yulia Portnova0e64ea22015-03-20 17:27:22 +0200604
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200605 # pylint: disable=E1101
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200606 return flt_ip.ip, conn.nova.servers.get(srv.id)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800607
608
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200609def clear_nodes(conn: OSConnection,
610 ids: List[int] = None,
611 name_templ: str = None,
612 max_server_delete_time: int = 120):
koder aka kdanilov765920a2016-04-12 00:35:48 +0300613 try:
614 def need_delete(srv):
615 if name_templ is not None:
616 return re.match(name_templ.format("\\d+"), srv.name) is not None
617 else:
618 return srv.id in ids
619
620 volumes_to_delete = []
kdanylov aka koder13e58452018-07-15 02:51:51 +0300621 if ids:
622 for vol in conn.cinder.volumes.list():
623 for attachment in vol.attachments:
624 if attachment['server_id'] in ids:
625 volumes_to_delete.append(vol)
626 break
koder aka kdanilov765920a2016-04-12 00:35:48 +0300627
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200628 still_alive = set()
629 for srv in conn.nova.servers.list():
koder aka kdanilov765920a2016-04-12 00:35:48 +0300630 if need_delete(srv):
631 logger.debug("Deleting server {0}".format(srv.name))
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200632 conn.nova.servers.delete(srv)
633 still_alive.add(srv.id)
koder aka kdanilov765920a2016-04-12 00:35:48 +0300634
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200635 if still_alive:
636 logger.debug("Waiting till all servers are actually deleted")
637 tout = Timeout(max_server_delete_time, no_exc=True)
638 while tout.tick() and still_alive:
639 all_id = set(srv.id for srv in conn.nova.servers.list())
640 still_alive = still_alive.intersection(all_id)
koder aka kdanilove87ae652015-04-20 02:14:35 +0300641
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200642 if still_alive:
643 logger.warning("Failed to remove servers {}. ".format(",".join(still_alive)) +
644 "You, probably, need to remove them manually (and volumes as well)")
645 return
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800646
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200647 if volumes_to_delete:
648 logger.debug("Deleting volumes")
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800649
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200650 # wait till vm actually deleted
651
652 # logger.warning("Volume deletion commented out")
653 for vol in volumes_to_delete:
654 logger.debug("Deleting volume " + vol.display_name)
655 conn.cinder.volumes.delete(vol)
656
657 logger.debug("Clearing complete (yet some volumes may still be deleting)")
658 except Exception:
koder aka kdanilov765920a2016-04-12 00:35:48 +0300659 logger.exception("During removing servers. " +
660 "You, probably, need to remove them manually")