blob: 0f6c6fca73eff2b3b482afb881325a48e2900efc [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
koder aka kdanilov39e449e2016-12-17 15:15:26 +020010from typing import Dict, Any, Iterable, Iterator, NamedTuple, Optional, List, Tuple, Set
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 kdanilovbbbe1dc2016-12-20 01:19:56 +020019from .utils import Timeout, to_ip
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020020from .node_interfaces import NodeInfo
koder aka kdanilov39e449e2016-12-17 15:15:26 +020021from .ssh_utils 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
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020048# TODO(koder): should correctly process different sources, not only env????
49def get_openstack_credentials() -> OSCreds:
50 is_insecure = os.environ.get('OS_INSECURE', 'false').lower() in ('true', 'yes')
51
52 return OSCreds(os.environ.get('OS_USERNAME'),
53 os.environ.get('OS_PASSWORD'),
54 os.environ.get('OS_TENANT_NAME'),
55 os.environ.get('OS_AUTH_URL'),
56 is_insecure)
koder aka kdanilov1c2b5112015-04-10 16:53:51 +030057
58
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020059class OSConnection:
60 def __init__(self, nova: NovaClient, cinder: CinderClient, glance: GlanceClient) -> None:
61 self.nova = nova
62 self.cinder = cinder
63 self.glance = glance
koder aka kdanilov1c2b5112015-04-10 16:53:51 +030064
65
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020066def os_connect(os_creds: OSCreds, version: str = "2") -> OSConnection:
67 loader = loading.get_plugin_loader('password')
68 auth = loader.load_from_options(auth_url=os_creds.auth_url,
69 username=os_creds.name,
70 password=os_creds.passwd,
71 project_id=os_creds.tenant)
72 auth_sess = session.Session(auth=auth)
koder aka kdanilove87ae652015-04-20 02:14:35 +030073
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020074 glance = GlanceClient(version, session=auth_sess)
75 nova = NovaClient(version, session=auth_sess)
76 cinder = CinderClient(os_creds.name, os_creds.passwd, os_creds.tenant, os_creds.auth_url,
77 insecure=os_creds.insecure, api_version=version)
78 return OSConnection(nova, cinder, glance)
koder aka kdanilove87ae652015-04-20 02:14:35 +030079
80
koder aka kdanilov73084622016-11-16 21:51:08 +020081def find_vms(conn: OSConnection, name_prefix: str) -> Iterable[Tuple[str, int]]:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020082 for srv in conn.nova.servers.list():
koder aka kdanilovf86d7af2015-05-06 04:01:54 +030083 if srv.name.startswith(name_prefix):
koder aka kdanilov73084622016-11-16 21:51:08 +020084 # need to exit after found server first external IP
85 # so have to rollout two cycles to avoid using exceptions
86 all_ip = [] # type: List[Any]
koder aka kdanilovf86d7af2015-05-06 04:01:54 +030087 for ips in srv.addresses.values():
koder aka kdanilov73084622016-11-16 21:51:08 +020088 all_ip.extend(ips)
89
90 for ip in all_ip:
91 if ip.get("OS-EXT-IPS:type", None) == 'floating':
92 yield ip['addr'], srv.id
93 break
koder aka kdanilovf86d7af2015-05-06 04:01:54 +030094
95
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020096def pause(conn: OSConnection, ids: Iterable[int], executor: ThreadPoolExecutor) -> None:
97 def pause_vm(vm_id: str) -> None:
98 vm = conn.nova.servers.get(vm_id)
koder aka kdanilov416b87a2015-05-12 00:26:04 +030099 if vm.status == 'ACTIVE':
100 vm.pause()
101
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200102 for future in executor.map(pause_vm, ids):
103 future.result()
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300104
105
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200106def unpause(conn: OSConnection, ids: Iterable[int], executor: ThreadPoolExecutor, max_resume_time=10) -> None:
107 def unpause(vm_id: str) -> None:
108 vm = conn.nova.servers.get(vm_id)
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300109 if vm.status == 'PAUSED':
110 vm.unpause()
111
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200112 for _ in Timeout(max_resume_time):
113 vm = conn.nova.servers.get(vm_id)
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300114 if vm.status != 'PAUSED':
115 return
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300116 raise RuntimeError("Can't unpause vm {0}".format(vm_id))
117
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200118 for future in executor.map(unpause, ids):
119 future.result()
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300120
121
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200122def prepare_os(conn: OSConnection, params: Dict[str, Any], max_vm_per_node: int = 8) -> None:
koder aka kdanilov34052012015-08-27 18:32:11 +0300123 """prepare openstack for futher usage
124
125 Creates server groups, security rules, keypair, flavor
126 and upload VM image from web. In case if object with
127 given name already exists, skip preparation part.
128 Don't check, that existing object has required attributes
129
130 params:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200131 nova: OSConnection
koder aka kdanilov34052012015-08-27 18:32:11 +0300132 params: dict {
133 security_group:str - security group name with allowed ssh and ping
134 aa_group_name:str - template for anti-affinity group names. Should
135 receive one integer parameter, like "cbt_aa_{0}"
136 keypair_name: str - OS keypair name
137 keypair_file_public: str - path to public key file
138 keypair_file_private: str - path to private key file
139
140 flavor:dict - flavor params
141 name, ram_size, hdd_size, cpu_count
142 as for novaclient.Client.flavor.create call
143
144 image:dict - image params
145 'name': image name
146 'url': image url
147 }
148 os_creds: OSCreds
149 max_vm_per_compute: int=8 maximum expected amount of VM, per
150 compute host. Used to create appropriate
151 count of server groups for even placement
koder aka kdanilov34052012015-08-27 18:32:11 +0300152 """
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200153 allow_ssh_and_ping(conn, params['security_group'])
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300154
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200155 for idx in range(max_vm_per_node):
156 get_or_create_aa_group(conn, params['aa_group_name'].format(idx))
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300157
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200158 create_keypair(conn, params['keypair_name'], params['keypair_file_public'], params['keypair_file_private'])
159 create_image(conn, params['image']['name'], params['image']['url'])
160 create_flavor(conn, **params['flavor'])
koder aka kdanilov4e9f3ed2015-04-14 11:26:12 +0300161
162
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200163def create_keypair(conn: OSConnection, name: str, pub_key_path: str, priv_key_path: str):
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300164 """create and upload keypair into nova, if doesn't exists yet
165
166 Create and upload keypair into nova, if keypair with given bane
167 doesn't exists yet. Uses key from files, if file doesn't exists -
168 create new keys, and store'em into files.
169
170 parameters:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200171 conn: OSConnection
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300172 name: str - ketpair name
173 pub_key_path: str - path for public key
174 priv_key_path: str - path for private key
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300175 """
176
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300177 pub_key_exists = os.path.exists(pub_key_path)
178 priv_key_exists = os.path.exists(priv_key_path)
179
180 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200181 kpair = conn.nova.keypairs.find(name=name)
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300182 # if file not found- delete and recreate
183 except NotFound:
184 kpair = None
185
186 if pub_key_exists and not priv_key_exists:
187 raise EnvironmentError("Private key file doesn't exists")
188
189 if not pub_key_exists and priv_key_exists:
190 raise EnvironmentError("Public key file doesn't exists")
191
192 if kpair is None:
193 if pub_key_exists:
194 with open(pub_key_path) as pub_key_fd:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200195 return conn.nova.keypairs.create(name, pub_key_fd.read())
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300196 else:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200197 key = conn.nova.keypairs.create(name)
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300198
199 with open(priv_key_path, "w") as priv_key_fd:
200 priv_key_fd.write(key.private_key)
201 os.chmod(priv_key_path, stat.S_IREAD | stat.S_IWRITE)
202
203 with open(pub_key_path, "w") as pub_key_fd:
204 pub_key_fd.write(key.public_key)
205 elif not priv_key_exists:
206 raise EnvironmentError("Private key file doesn't exists," +
207 " but key uploaded openstack." +
208 " Either set correct path to private key" +
209 " or remove key from openstack")
210
211
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200212def get_or_create_aa_group(conn: OSConnection, name: str) -> int:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300213 """create anti-affinity server group, if doesn't exists yet
214
215 parameters:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200216 conn: OSConnection
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300217 name: str - group name
218
219 returns: str - group id
220 """
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300221 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200222 return conn.nova.server_groups.find(name=name).id
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300223 except NotFound:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200224 return conn.nova.server_groups.create(name=name, policies=['anti-affinity']).id
koder aka kdanilov652cd802015-04-13 12:21:07 +0300225
226
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200227def allow_ssh_and_ping(conn: OSConnection, group_name: str) -> int:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300228 """create sequrity group for ping and ssh
229
230 parameters:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200231 conn:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300232 group_name: str - group name
233
234 returns: str - group id
235 """
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300236 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200237 secgroup = conn.nova.security_groups.find(name=group_name)
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300238 except NotFound:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200239 secgroup = conn.nova.security_groups.create(group_name, "allow ssh/ping to node")
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300240
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200241 conn.nova.security_group_rules.create(secgroup.id,
242 ip_protocol="tcp",
243 from_port="22",
244 to_port="22",
245 cidr="0.0.0.0/0")
koder aka kdanilov652cd802015-04-13 12:21:07 +0300246
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200247 conn.nova.security_group_rules.create(secgroup.id,
248 ip_protocol="icmp",
249 from_port=-1,
250 cidr="0.0.0.0/0",
251 to_port=-1)
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300252 return secgroup.id
koder aka kdanilov652cd802015-04-13 12:21:07 +0300253
254
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200255def create_image(conn: OSConnection, name: str, url: str) -> None:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300256 """upload image into glance from given URL, if given image doesn't exisis yet
257
258 parameters:
259 nova: nova connection
260 os_creds: OSCreds object - openstack credentials, should be same,
261 as used when connectiong given novaclient
262 name: str - image name
263 url: str - image download url
264
265 returns: None
266 """
267 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200268 conn.nova.images.find(name=name)
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300269 return
270 except NotFound:
271 pass
272
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200273 ok = False
274 with tempfile.NamedTemporaryFile() as temp_fd:
275 try:
276 cmd = "wget --dns-timeout=30 --connect-timeout=30 --read-timeout=30 -o {} {}"
277 subprocess.check_call(cmd.format(temp_fd.name, url))
278 ok = True
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300279
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200280 # TODO(koder): add proper error handling
281 except Exception:
282 pass
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300283
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200284 if not ok:
285 urllib.request.urlretrieve(url, temp_fd.name)
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300286
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200287 image = conn.glance.images.create(name=name)
288 with open(temp_fd.name, 'rb') as fd:
289 conn.glance.images.upload(image.id, fd)
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300290
291
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200292def create_flavor(conn: OSConnection, name: str, ram_size: int, hdd_size: int, cpu_count: int) -> None:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300293 """create flavor, if doesn't exisis yet
294
295 parameters:
296 nova: nova connection
297 name: str - flavor name
298 ram_size: int - ram size (UNIT?)
299 hdd_size: int - root hdd size (UNIT?)
300 cpu_count: int - cpu cores
301
302 returns: None
303 """
304 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200305 conn.nova.flavors.find(name)
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300306 return
307 except NotFound:
308 pass
309
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200310 conn.nova.flavors.create(name, cpu_count, ram_size, hdd_size)
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300311
312
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200313def create_volume(conn: OSConnection, size: int, name: str) -> Any:
314 vol = conn.cinder.volumes.create(size=size, display_name=name)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800315 err_count = 0
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300316
koder aka kdanilove87ae652015-04-20 02:14:35 +0300317 while vol.status != 'available':
318 if vol.status == 'error':
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800319 if err_count == 3:
koder aka kdanilove21d7472015-02-14 19:02:04 -0800320 logger.critical("Fail to create volume")
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800321 raise RuntimeError("Fail to create volume")
322 else:
323 err_count += 1
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200324 conn.cinder.volumes.delete(vol)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800325 time.sleep(1)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200326 vol = conn.cinder.volumes.create(size=size, display_name=name)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800327 continue
328 time.sleep(1)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200329 vol = conn.cinder.volumes.get(vol.id)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800330 return vol
331
332
koder aka kdanilov73084622016-11-16 21:51:08 +0200333def wait_for_server_active(conn: OSConnection, server: Any, timeout: int = 300) -> bool:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300334 """waiting till server became active
335
336 parameters:
337 nova: nova connection
338 server: server object
339 timeout: int - seconds to wait till raise an exception
340
341 returns: None
342 """
343
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200344 for _ in Timeout(timeout, no_exc=True):
345 server_state = getattr(server, 'OS-EXT-STS:vm_state').lower()
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800346
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200347 if server_state == 'active':
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800348 return True
349
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200350 if server_state == 'error':
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800351 return False
352
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200353 server = conn.nova.servers.get(server)
354 return False
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800355
356
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800357class Allocate(object):
358 pass
359
360
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200361def get_floating_ips(conn: OSConnection, pool: Optional[str], amount: int) -> List[str]:
362 """allocate floating ips
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300363
364 parameters:
365 nova: nova connection
366 pool:str floating ip pool name
367 amount:int - ip count
368
369 returns: [ip object]
370 """
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200371 ip_list = conn.nova.floating_ips.list()
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800372
373 if pool is not None:
374 ip_list = [ip for ip in ip_list if ip.pool == pool]
375
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800376 return [ip for ip in ip_list if ip.instance_id is None][:amount]
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800377
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800378
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200379def launch_vms(conn: OSConnection,
380 params: Dict[str, Any],
381 executor: ThreadPoolExecutor,
382 already_has_count: int = 0) -> Iterator[NodeInfo]:
koder aka kdanilov34052012015-08-27 18:32:11 +0300383 """launch virtual servers
384
385 Parameters:
386 nova: nova client
387 params: dict {
388 count: str or int - server count. If count is string it should be in
389 one of bext forms: "=INT" or "xINT". First mean
390 to spawn (INT - already_has_count) servers, and
391 all should be evenly distributed across all compute
392 nodes. xINT mean spawn COMPUTE_COUNT * INT servers.
393 image: dict {'name': str - image name}
394 flavor: dict {'name': str - flavor name}
395 group_name: str - group name, used to create uniq server name
396 keypair_name: str - ssh keypais name
397 keypair_file_private: str - path to private key
398 user: str - vm user name
399 vol_sz: int or None - volume size, or None, if no volume
400 network_zone_name: str - network zone name
401 flt_ip_pool: str - floating ip pool
402 name_templ: str - server name template, should receive two parameters
403 'group and id, like 'cbt-{group}-{id}'
404 aa_group_name: str scheduler group name
405 security_group: str - security group name
406 }
407 already_has_count: int=0 - how many servers already exists. Used to distribute
408 new servers evenly across all compute nodes, taking
409 old server in accout
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200410 returns: generator of NodeInfo - server credentials, in format USER@IP:KEY_PATH
koder aka kdanilov34052012015-08-27 18:32:11 +0300411
412 """
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300413 logger.debug("Calculating new vm count")
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200414 count = params['count'] # type: int
415 lst = conn.nova.services.list(binary='nova-compute')
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300416 srv_count = len([srv for srv in lst if srv.status == 'enabled'])
koder aka kdanilovda45e882015-04-06 02:24:42 +0300417
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200418 if isinstance(count, str):
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300419 if count.startswith("x"):
420 count = srv_count * int(count[1:])
421 else:
422 assert count.startswith('=')
423 count = int(count[1:]) - already_has_count
424
425 if count <= 0:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300426 logger.debug("Not need new vms")
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300427 return
koder aka kdanilovda45e882015-04-06 02:24:42 +0300428
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300429 logger.debug("Starting new nodes on openstack")
430
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200431 assert isinstance(count, int)
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300432
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300433 srv_params = "img: {image[name]}, flavor: {flavor[name]}".format(**params)
koder aka kdanilov66839a92015-04-11 13:22:31 +0300434 msg_templ = "Will start {0} servers with next params: {1}"
koder aka kdanilovcee43342015-04-14 22:52:53 +0300435 logger.info(msg_templ.format(count, srv_params))
koder aka kdanilovda45e882015-04-06 02:24:42 +0300436
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300437 vm_params = dict(
438 img_name=params['image']['name'],
439 flavor_name=params['flavor']['name'],
440 group_name=params['group_name'],
441 keypair_name=params['keypair_name'],
442 vol_sz=params.get('vol_sz'),
443 network_zone_name=params.get("network_zone_name"),
444 flt_ip_pool=params.get('flt_ip_pool'),
445 name_templ=params.get('name_templ'),
446 scheduler_hints={"group": params['aa_group_name']},
447 security_group=params['security_group'],
448 sec_group_size=srv_count
449 )
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300450
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300451 # precache all errors before start creating vms
452 private_key_path = params['keypair_file_private']
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200453 user = params['image']['user']
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300454
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200455 for ip, os_node in create_vms_mt(conn, count, executor, **vm_params):
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200456 info = NodeInfo(ConnCreds(to_ip(ip), user, key_file=private_key_path), set())
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200457 info.os_vm_id = os_node.id
458 yield info
koder aka kdanilovda45e882015-04-06 02:24:42 +0300459
460
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200461def get_free_server_groups(conn: OSConnection, template: str) -> Iterator[str]:
koder aka kdanilov34052012015-08-27 18:32:11 +0300462 """get fre server groups, that match given name template
463
464 parameters:
465 nova: nova connection
466 template:str - name template
467 amount:int - ip count
468
469 returns: generator or str - server group names
470 """
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200471 for server_group in conn.nova.server_groups.list():
472 if not server_group.members:
473 if re.match(template, server_group.name):
474 yield str(server_group.id)
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300475
476
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200477def create_vms_mt(conn: OSConnection,
478 amount: int,
479 executor: ThreadPoolExecutor,
480 group_name: str,
481 keypair_name: str,
482 img_name: str,
483 flavor_name: str,
484 vol_sz: int = None,
485 network_zone_name: str = None,
486 flt_ip_pool: str = None,
487 name_templ: str ='wally-{id}',
488 scheduler_hints: Dict = None,
489 security_group: str = None,
490 sec_group_size: int = None) -> List[Tuple[str, Any]]:
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800491
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200492 if network_zone_name is not None:
493 network_future = executor.submit(conn.nova.networks.find,
494 label=network_zone_name)
495 else:
496 network_future = None
koder aka kdanilov97644f92015-02-13 11:11:08 -0800497
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200498 fl_future = executor.submit(conn.nova.flavors.find, name=flavor_name)
499 img_future = executor.submit(conn.nova.images.find, name=img_name)
koder aka kdanilov97644f92015-02-13 11:11:08 -0800500
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200501 if flt_ip_pool is not None:
502 ips_future = executor.submit(get_floating_ips,
503 conn, flt_ip_pool, amount)
504 logger.debug("Wait for floating ip")
505 ips = ips_future.result()
506 ips += [Allocate] * (amount - len(ips))
507 else:
508 ips = [None] * amount
koder aka kdanilov97644f92015-02-13 11:11:08 -0800509
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200510 logger.debug("Getting flavor object")
511 fl = fl_future.result()
512 logger.debug("Getting image object")
513 img = img_future.result()
koder aka kdanilov97644f92015-02-13 11:11:08 -0800514
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200515 if network_future is not None:
516 logger.debug("Waiting for network results")
517 nics = [{'net-id': network_future.result().id}]
518 else:
519 nics = None
koder aka kdanilov97644f92015-02-13 11:11:08 -0800520
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200521 names = [] # type: List[str]
522 for i in range(amount):
523 names.append(name_templ.format(group=group_name, id=i))
koder aka kdanilov97644f92015-02-13 11:11:08 -0800524
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200525 futures = []
526 logger.debug("Requesting new vm's")
koder aka kdanilov6e2ae792015-03-04 18:02:24 -0800527
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200528 orig_scheduler_hints = scheduler_hints.copy()
529 group_name_template = scheduler_hints['group'].format("\\d+")
530 groups = list(get_free_server_groups(conn, group_name_template + "$"))
531 groups.sort()
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300532
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200533 for idx, (name, flt_ip) in enumerate(zip(names, ips), 2):
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300534
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200535 scheduler_hints = None
536 if orig_scheduler_hints is not None and sec_group_size is not None:
537 if "group" in orig_scheduler_hints:
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300538 scheduler_hints = orig_scheduler_hints.copy()
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200539 scheduler_hints['group'] = groups[idx // sec_group_size]
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300540
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200541 if scheduler_hints is None:
542 scheduler_hints = orig_scheduler_hints.copy()
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800543
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200544 params = (conn, name, keypair_name, img, fl,
545 nics, vol_sz, flt_ip, scheduler_hints,
546 flt_ip_pool, [security_group])
547
548 futures.append(executor.submit(create_vm, *params))
549 res = [future.result() for future in futures]
550 logger.debug("Done spawning")
551 return res
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800552
553
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200554def create_vm(conn: OSConnection,
555 name: str,
556 keypair_name: str,
557 img: Any,
558 flavor: Any,
559 nics: List,
560 vol_sz: int = None,
561 flt_ip: Any = False,
562 scheduler_hints: Dict = None,
563 pool: str = None,
564 security_groups=None,
565 max_retry: int = 3,
566 delete_timeout: int = 120) -> Tuple[str, Any]:
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800567
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200568 # make mypy/pylint happy
569 srv = None # type: Any
570 for i in range(max_retry):
571 srv = conn.nova.servers.create(name, flavor=flavor, image=img, nics=nics, key_name=keypair_name,
572 scheduler_hints=scheduler_hints, security_groups=security_groups)
573
574 if not wait_for_server_active(conn, srv):
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200575 logger.debug("Server {} fails to start. Kill it and try again".format(srv))
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200576 conn.nova.servers.delete(srv)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800577
koder aka kdanilovf86d7af2015-05-06 04:01:54 +0300578 try:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200579 for _ in Timeout(delete_timeout, "Server {} delete timeout".format(srv.id)):
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200580 srv = conn.nova.servers.get(srv.id)
koder aka kdanilovf86d7af2015-05-06 04:01:54 +0300581 except NotFound:
582 pass
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800583 else:
584 break
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300585 else:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200586 raise RuntimeError("Failed to start server {}".format(srv.id))
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800587
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800588 if vol_sz is not None:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200589 vol = create_volume(conn, vol_sz, name)
590 conn.nova.volumes.create_server_volume(srv.id, vol.id, None)
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800591
592 if flt_ip is Allocate:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200593 flt_ip = conn.nova.floating_ips.create(pool)
koder aka kdanilovda45e882015-04-06 02:24:42 +0300594
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800595 if flt_ip is not None:
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800596 srv.add_floating_ip(flt_ip)
Yulia Portnova0e64ea22015-03-20 17:27:22 +0200597
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200598 # pylint: disable=E1101
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200599 return flt_ip.ip, conn.nova.servers.get(srv.id)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800600
601
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200602def clear_nodes(conn: OSConnection,
603 ids: List[int] = None,
604 name_templ: str = None,
605 max_server_delete_time: int = 120):
koder aka kdanilov765920a2016-04-12 00:35:48 +0300606 try:
607 def need_delete(srv):
608 if name_templ is not None:
609 return re.match(name_templ.format("\\d+"), srv.name) is not None
610 else:
611 return srv.id in ids
612
613 volumes_to_delete = []
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200614 for vol in conn.cinder.volumes.list():
koder aka kdanilov765920a2016-04-12 00:35:48 +0300615 for attachment in vol.attachments:
616 if attachment['server_id'] in ids:
617 volumes_to_delete.append(vol)
618 break
619
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200620 still_alive = set()
621 for srv in conn.nova.servers.list():
koder aka kdanilov765920a2016-04-12 00:35:48 +0300622 if need_delete(srv):
623 logger.debug("Deleting server {0}".format(srv.name))
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200624 conn.nova.servers.delete(srv)
625 still_alive.add(srv.id)
koder aka kdanilov765920a2016-04-12 00:35:48 +0300626
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200627 if still_alive:
628 logger.debug("Waiting till all servers are actually deleted")
629 tout = Timeout(max_server_delete_time, no_exc=True)
630 while tout.tick() and still_alive:
631 all_id = set(srv.id for srv in conn.nova.servers.list())
632 still_alive = still_alive.intersection(all_id)
koder aka kdanilove87ae652015-04-20 02:14:35 +0300633
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200634 if still_alive:
635 logger.warning("Failed to remove servers {}. ".format(",".join(still_alive)) +
636 "You, probably, need to remove them manually (and volumes as well)")
637 return
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800638
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200639 if volumes_to_delete:
640 logger.debug("Deleting volumes")
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800641
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200642 # wait till vm actually deleted
643
644 # logger.warning("Volume deletion commented out")
645 for vol in volumes_to_delete:
646 logger.debug("Deleting volume " + vol.display_name)
647 conn.cinder.volumes.delete(vol)
648
649 logger.debug("Clearing complete (yet some volumes may still be deleting)")
650 except Exception:
koder aka kdanilov765920a2016-04-12 00:35:48 +0300651 logger.exception("During removing servers. " +
652 "You, probably, need to remove them manually")