blob: a55fdbf979208dbea80f27d0c9d96f69bf220915 [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 +020019from .utils import Timeout
20from .node_interfaces import NodeInfo
koder aka kdanilov73084622016-11-16 21:51:08 +020021from .storage import IStorable
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 kdanilovcff7b2e2015-04-18 20:48:15 +030037logger = logging.getLogger("wally.vms")
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
85 # need to exit after found server first external IP
86 # so have to rollout two cycles to avoid using exceptions
87 all_ip = [] # type: List[Any]
koder aka kdanilovf86d7af2015-05-06 04:01:54 +030088 for ips in srv.addresses.values():
koder aka kdanilov73084622016-11-16 21:51:08 +020089 all_ip.extend(ips)
90
91 for ip in all_ip:
92 if ip.get("OS-EXT-IPS:type", None) == 'floating':
93 yield ip['addr'], srv.id
94 break
koder aka kdanilovf86d7af2015-05-06 04:01:54 +030095
96
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020097def pause(conn: OSConnection, ids: Iterable[int], executor: ThreadPoolExecutor) -> None:
98 def pause_vm(vm_id: str) -> None:
99 vm = conn.nova.servers.get(vm_id)
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300100 if vm.status == 'ACTIVE':
101 vm.pause()
102
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200103 for future in executor.map(pause_vm, ids):
104 future.result()
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300105
106
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200107def unpause(conn: OSConnection, ids: Iterable[int], executor: ThreadPoolExecutor, max_resume_time=10) -> None:
108 def unpause(vm_id: str) -> None:
109 vm = conn.nova.servers.get(vm_id)
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300110 if vm.status == 'PAUSED':
111 vm.unpause()
112
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200113 for _ in Timeout(max_resume_time):
114 vm = conn.nova.servers.get(vm_id)
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300115 if vm.status != 'PAUSED':
116 return
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300117 raise RuntimeError("Can't unpause vm {0}".format(vm_id))
118
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200119 for future in executor.map(unpause, ids):
120 future.result()
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300121
122
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200123def prepare_os(conn: OSConnection, params: Dict[str, Any], max_vm_per_node: int = 8) -> None:
koder aka kdanilov34052012015-08-27 18:32:11 +0300124 """prepare openstack for futher usage
125
126 Creates server groups, security rules, keypair, flavor
127 and upload VM image from web. In case if object with
128 given name already exists, skip preparation part.
129 Don't check, that existing object has required attributes
130
131 params:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200132 nova: OSConnection
koder aka kdanilov34052012015-08-27 18:32:11 +0300133 params: dict {
134 security_group:str - security group name with allowed ssh and ping
135 aa_group_name:str - template for anti-affinity group names. Should
136 receive one integer parameter, like "cbt_aa_{0}"
137 keypair_name: str - OS keypair name
138 keypair_file_public: str - path to public key file
139 keypair_file_private: str - path to private key file
140
141 flavor:dict - flavor params
142 name, ram_size, hdd_size, cpu_count
143 as for novaclient.Client.flavor.create call
144
145 image:dict - image params
146 'name': image name
147 'url': image url
148 }
149 os_creds: OSCreds
150 max_vm_per_compute: int=8 maximum expected amount of VM, per
151 compute host. Used to create appropriate
152 count of server groups for even placement
koder aka kdanilov34052012015-08-27 18:32:11 +0300153 """
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200154 allow_ssh_and_ping(conn, params['security_group'])
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300155
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200156 for idx in range(max_vm_per_node):
157 get_or_create_aa_group(conn, params['aa_group_name'].format(idx))
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300158
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200159 create_keypair(conn, params['keypair_name'], params['keypair_file_public'], params['keypair_file_private'])
160 create_image(conn, params['image']['name'], params['image']['url'])
161 create_flavor(conn, **params['flavor'])
koder aka kdanilov4e9f3ed2015-04-14 11:26:12 +0300162
163
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200164def create_keypair(conn: OSConnection, name: str, pub_key_path: str, priv_key_path: str):
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300165 """create and upload keypair into nova, if doesn't exists yet
166
167 Create and upload keypair into nova, if keypair with given bane
168 doesn't exists yet. Uses key from files, if file doesn't exists -
169 create new keys, and store'em into files.
170
171 parameters:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200172 conn: OSConnection
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300173 name: str - ketpair name
174 pub_key_path: str - path for public key
175 priv_key_path: str - path for private key
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300176 """
177
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300178 pub_key_exists = os.path.exists(pub_key_path)
179 priv_key_exists = os.path.exists(priv_key_path)
180
181 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200182 kpair = conn.nova.keypairs.find(name=name)
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300183 # if file not found- delete and recreate
184 except NotFound:
185 kpair = None
186
187 if pub_key_exists and not priv_key_exists:
188 raise EnvironmentError("Private key file doesn't exists")
189
190 if not pub_key_exists and priv_key_exists:
191 raise EnvironmentError("Public key file doesn't exists")
192
193 if kpair is None:
194 if pub_key_exists:
195 with open(pub_key_path) as pub_key_fd:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200196 return conn.nova.keypairs.create(name, pub_key_fd.read())
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300197 else:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200198 key = conn.nova.keypairs.create(name)
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +0300199
200 with open(priv_key_path, "w") as priv_key_fd:
201 priv_key_fd.write(key.private_key)
202 os.chmod(priv_key_path, stat.S_IREAD | stat.S_IWRITE)
203
204 with open(pub_key_path, "w") as pub_key_fd:
205 pub_key_fd.write(key.public_key)
206 elif not priv_key_exists:
207 raise EnvironmentError("Private key file doesn't exists," +
208 " but key uploaded openstack." +
209 " Either set correct path to private key" +
210 " or remove key from openstack")
211
212
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200213def get_or_create_aa_group(conn: OSConnection, name: str) -> int:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300214 """create anti-affinity server group, if doesn't exists yet
215
216 parameters:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200217 conn: OSConnection
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300218 name: str - group name
219
220 returns: str - group id
221 """
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300222 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200223 return conn.nova.server_groups.find(name=name).id
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300224 except NotFound:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200225 return conn.nova.server_groups.create(name=name, policies=['anti-affinity']).id
koder aka kdanilov652cd802015-04-13 12:21:07 +0300226
227
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200228def allow_ssh_and_ping(conn: OSConnection, group_name: str) -> int:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300229 """create sequrity group for ping and ssh
230
231 parameters:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200232 conn:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300233 group_name: str - group name
234
235 returns: str - group id
236 """
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300237 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200238 secgroup = conn.nova.security_groups.find(name=group_name)
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300239 except NotFound:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200240 secgroup = conn.nova.security_groups.create(group_name, "allow ssh/ping to node")
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300241
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200242 conn.nova.security_group_rules.create(secgroup.id,
243 ip_protocol="tcp",
244 from_port="22",
245 to_port="22",
246 cidr="0.0.0.0/0")
koder aka kdanilov652cd802015-04-13 12:21:07 +0300247
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200248 conn.nova.security_group_rules.create(secgroup.id,
249 ip_protocol="icmp",
250 from_port=-1,
251 cidr="0.0.0.0/0",
252 to_port=-1)
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300253 return secgroup.id
koder aka kdanilov652cd802015-04-13 12:21:07 +0300254
255
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200256def create_image(conn: OSConnection, name: str, url: str) -> None:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300257 """upload image into glance from given URL, if given image doesn't exisis yet
258
259 parameters:
260 nova: nova connection
261 os_creds: OSCreds object - openstack credentials, should be same,
262 as used when connectiong given novaclient
263 name: str - image name
264 url: str - image download url
265
266 returns: None
267 """
268 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200269 conn.nova.images.find(name=name)
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300270 return
271 except NotFound:
272 pass
273
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200274 ok = False
275 with tempfile.NamedTemporaryFile() as temp_fd:
276 try:
277 cmd = "wget --dns-timeout=30 --connect-timeout=30 --read-timeout=30 -o {} {}"
278 subprocess.check_call(cmd.format(temp_fd.name, url))
279 ok = True
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300280
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200281 # TODO(koder): add proper error handling
282 except Exception:
283 pass
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300284
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200285 if not ok:
286 urllib.request.urlretrieve(url, temp_fd.name)
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300287
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200288 image = conn.glance.images.create(name=name)
289 with open(temp_fd.name, 'rb') as fd:
290 conn.glance.images.upload(image.id, fd)
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300291
292
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200293def create_flavor(conn: OSConnection, name: str, ram_size: int, hdd_size: int, cpu_count: int) -> None:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300294 """create flavor, if doesn't exisis yet
295
296 parameters:
297 nova: nova connection
298 name: str - flavor name
299 ram_size: int - ram size (UNIT?)
300 hdd_size: int - root hdd size (UNIT?)
301 cpu_count: int - cpu cores
302
303 returns: None
304 """
305 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200306 conn.nova.flavors.find(name)
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300307 return
308 except NotFound:
309 pass
310
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200311 conn.nova.flavors.create(name, cpu_count, ram_size, hdd_size)
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300312
313
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200314def create_volume(conn: OSConnection, size: int, name: str) -> Any:
315 vol = conn.cinder.volumes.create(size=size, display_name=name)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800316 err_count = 0
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300317
koder aka kdanilove87ae652015-04-20 02:14:35 +0300318 while vol.status != 'available':
319 if vol.status == 'error':
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800320 if err_count == 3:
koder aka kdanilove21d7472015-02-14 19:02:04 -0800321 logger.critical("Fail to create volume")
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800322 raise RuntimeError("Fail to create volume")
323 else:
324 err_count += 1
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200325 conn.cinder.volumes.delete(vol)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800326 time.sleep(1)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200327 vol = conn.cinder.volumes.create(size=size, display_name=name)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800328 continue
329 time.sleep(1)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200330 vol = conn.cinder.volumes.get(vol.id)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800331 return vol
332
333
koder aka kdanilov73084622016-11-16 21:51:08 +0200334def wait_for_server_active(conn: OSConnection, server: Any, timeout: int = 300) -> bool:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300335 """waiting till server became active
336
337 parameters:
338 nova: nova connection
339 server: server object
340 timeout: int - seconds to wait till raise an exception
341
342 returns: None
343 """
344
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200345 for _ in Timeout(timeout, no_exc=True):
346 server_state = getattr(server, 'OS-EXT-STS:vm_state').lower()
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800347
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200348 if server_state == 'active':
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800349 return True
350
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200351 if server_state == 'error':
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800352 return False
353
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200354 server = conn.nova.servers.get(server)
355 return False
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800356
357
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800358class Allocate(object):
359 pass
360
361
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200362def get_floating_ips(conn: OSConnection, pool: Optional[str], amount: int) -> List[str]:
363 """allocate floating ips
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300364
365 parameters:
366 nova: nova connection
367 pool:str floating ip pool name
368 amount:int - ip count
369
370 returns: [ip object]
371 """
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200372 ip_list = conn.nova.floating_ips.list()
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800373
374 if pool is not None:
375 ip_list = [ip for ip in ip_list if ip.pool == pool]
376
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800377 return [ip for ip in ip_list if ip.instance_id is None][:amount]
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800378
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800379
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200380def launch_vms(conn: OSConnection,
381 params: Dict[str, Any],
382 executor: ThreadPoolExecutor,
383 already_has_count: int = 0) -> Iterator[NodeInfo]:
koder aka kdanilov34052012015-08-27 18:32:11 +0300384 """launch virtual servers
385
386 Parameters:
387 nova: nova client
388 params: dict {
389 count: str or int - server count. If count is string it should be in
390 one of bext forms: "=INT" or "xINT". First mean
391 to spawn (INT - already_has_count) servers, and
392 all should be evenly distributed across all compute
393 nodes. xINT mean spawn COMPUTE_COUNT * INT servers.
394 image: dict {'name': str - image name}
395 flavor: dict {'name': str - flavor name}
396 group_name: str - group name, used to create uniq server name
397 keypair_name: str - ssh keypais name
398 keypair_file_private: str - path to private key
399 user: str - vm user name
400 vol_sz: int or None - volume size, or None, if no volume
401 network_zone_name: str - network zone name
402 flt_ip_pool: str - floating ip pool
403 name_templ: str - server name template, should receive two parameters
404 'group and id, like 'cbt-{group}-{id}'
405 aa_group_name: str scheduler group name
406 security_group: str - security group name
407 }
408 already_has_count: int=0 - how many servers already exists. Used to distribute
409 new servers evenly across all compute nodes, taking
410 old server in accout
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200411 returns: generator of NodeInfo - server credentials, in format USER@IP:KEY_PATH
koder aka kdanilov34052012015-08-27 18:32:11 +0300412
413 """
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300414 logger.debug("Calculating new vm count")
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200415 count = params['count'] # type: int
416 lst = conn.nova.services.list(binary='nova-compute')
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300417 srv_count = len([srv for srv in lst if srv.status == 'enabled'])
koder aka kdanilovda45e882015-04-06 02:24:42 +0300418
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200419 if isinstance(count, str):
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300420 if count.startswith("x"):
421 count = srv_count * int(count[1:])
422 else:
423 assert count.startswith('=')
424 count = int(count[1:]) - already_has_count
425
426 if count <= 0:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300427 logger.debug("Not need new vms")
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300428 return
koder aka kdanilovda45e882015-04-06 02:24:42 +0300429
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300430 logger.debug("Starting new nodes on openstack")
431
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200432 assert isinstance(count, int)
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300433
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300434 srv_params = "img: {image[name]}, flavor: {flavor[name]}".format(**params)
koder aka kdanilov66839a92015-04-11 13:22:31 +0300435 msg_templ = "Will start {0} servers with next params: {1}"
koder aka kdanilovcee43342015-04-14 22:52:53 +0300436 logger.info(msg_templ.format(count, srv_params))
koder aka kdanilovda45e882015-04-06 02:24:42 +0300437
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300438 vm_params = dict(
439 img_name=params['image']['name'],
440 flavor_name=params['flavor']['name'],
441 group_name=params['group_name'],
442 keypair_name=params['keypair_name'],
443 vol_sz=params.get('vol_sz'),
444 network_zone_name=params.get("network_zone_name"),
445 flt_ip_pool=params.get('flt_ip_pool'),
446 name_templ=params.get('name_templ'),
447 scheduler_hints={"group": params['aa_group_name']},
448 security_group=params['security_group'],
449 sec_group_size=srv_count
450 )
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300451
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300452 # precache all errors before start creating vms
453 private_key_path = params['keypair_file_private']
454 creds = params['image']['creds']
koder aka kdanilov4500a5f2015-04-17 16:55:17 +0300455
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200456 for ip, os_node in create_vms_mt(conn, count, executor, **vm_params):
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300457 conn_uri = creds.format(ip=ip, private_key_path=private_key_path)
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200458 info = NodeInfo(conn_uri, set())
459 info.os_vm_id = os_node.id
460 yield info
koder aka kdanilovda45e882015-04-06 02:24:42 +0300461
462
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200463def get_free_server_groups(conn: OSConnection, template: str) -> Iterator[str]:
koder aka kdanilov34052012015-08-27 18:32:11 +0300464 """get fre server groups, that match given name template
465
466 parameters:
467 nova: nova connection
468 template:str - name template
469 amount:int - ip count
470
471 returns: generator or str - server group names
472 """
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200473 for server_group in conn.nova.server_groups.list():
474 if not server_group.members:
475 if re.match(template, server_group.name):
476 yield str(server_group.id)
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300477
478
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200479def create_vms_mt(conn: OSConnection,
480 amount: int,
481 executor: ThreadPoolExecutor,
482 group_name: str,
483 keypair_name: str,
484 img_name: str,
485 flavor_name: str,
486 vol_sz: int = None,
487 network_zone_name: str = None,
488 flt_ip_pool: str = None,
489 name_templ: str ='wally-{id}',
490 scheduler_hints: Dict = None,
491 security_group: str = None,
492 sec_group_size: int = None) -> List[Tuple[str, Any]]:
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800493
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200494 if network_zone_name is not None:
495 network_future = executor.submit(conn.nova.networks.find,
496 label=network_zone_name)
497 else:
498 network_future = None
koder aka kdanilov97644f92015-02-13 11:11:08 -0800499
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200500 fl_future = executor.submit(conn.nova.flavors.find, name=flavor_name)
501 img_future = executor.submit(conn.nova.images.find, name=img_name)
koder aka kdanilov97644f92015-02-13 11:11:08 -0800502
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200503 if flt_ip_pool is not None:
504 ips_future = executor.submit(get_floating_ips,
505 conn, flt_ip_pool, amount)
506 logger.debug("Wait for floating ip")
507 ips = ips_future.result()
508 ips += [Allocate] * (amount - len(ips))
509 else:
510 ips = [None] * amount
koder aka kdanilov97644f92015-02-13 11:11:08 -0800511
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200512 logger.debug("Getting flavor object")
513 fl = fl_future.result()
514 logger.debug("Getting image object")
515 img = img_future.result()
koder aka kdanilov97644f92015-02-13 11:11:08 -0800516
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200517 if network_future is not None:
518 logger.debug("Waiting for network results")
519 nics = [{'net-id': network_future.result().id}]
520 else:
521 nics = None
koder aka kdanilov97644f92015-02-13 11:11:08 -0800522
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200523 names = [] # type: List[str]
524 for i in range(amount):
525 names.append(name_templ.format(group=group_name, id=i))
koder aka kdanilov97644f92015-02-13 11:11:08 -0800526
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200527 futures = []
528 logger.debug("Requesting new vm's")
koder aka kdanilov6e2ae792015-03-04 18:02:24 -0800529
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200530 orig_scheduler_hints = scheduler_hints.copy()
531 group_name_template = scheduler_hints['group'].format("\\d+")
532 groups = list(get_free_server_groups(conn, group_name_template + "$"))
533 groups.sort()
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300534
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200535 for idx, (name, flt_ip) in enumerate(zip(names, ips), 2):
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300536
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200537 scheduler_hints = None
538 if orig_scheduler_hints is not None and sec_group_size is not None:
539 if "group" in orig_scheduler_hints:
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300540 scheduler_hints = orig_scheduler_hints.copy()
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200541 scheduler_hints['group'] = groups[idx // sec_group_size]
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300542
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200543 if scheduler_hints is None:
544 scheduler_hints = orig_scheduler_hints.copy()
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800545
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200546 params = (conn, name, keypair_name, img, fl,
547 nics, vol_sz, flt_ip, scheduler_hints,
548 flt_ip_pool, [security_group])
549
550 futures.append(executor.submit(create_vm, *params))
551 res = [future.result() for future in futures]
552 logger.debug("Done spawning")
553 return res
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800554
555
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200556def create_vm(conn: OSConnection,
557 name: str,
558 keypair_name: str,
559 img: Any,
560 flavor: Any,
561 nics: List,
562 vol_sz: int = None,
563 flt_ip: Any = False,
564 scheduler_hints: Dict = None,
565 pool: str = None,
566 security_groups=None,
567 max_retry: int = 3,
568 delete_timeout: int = 120) -> Tuple[str, Any]:
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800569
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200570 # make mypy/pylint happy
571 srv = None # type: Any
572 for i in range(max_retry):
573 srv = conn.nova.servers.create(name, flavor=flavor, image=img, nics=nics, key_name=keypair_name,
574 scheduler_hints=scheduler_hints, security_groups=security_groups)
575
576 if not wait_for_server_active(conn, srv):
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800577 msg = "Server {0} fails to start. Kill it and try again"
koder aka kdanilove21d7472015-02-14 19:02:04 -0800578 logger.debug(msg.format(srv))
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200579 conn.nova.servers.delete(srv)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800580
koder aka kdanilovf86d7af2015-05-06 04:01:54 +0300581 try:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200582 for _ in Timeout(delete_timeout, "Server {0} delete timeout".format(srv.id)):
583 srv = conn.nova.servers.get(srv.id)
koder aka kdanilovf86d7af2015-05-06 04:01:54 +0300584 except NotFound:
585 pass
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800586 else:
587 break
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300588 else:
589 raise RuntimeError("Failed to start server".format(srv.id))
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800590
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800591 if vol_sz is not None:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200592 vol = create_volume(conn, vol_sz, name)
593 conn.nova.volumes.create_server_volume(srv.id, vol.id, None)
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800594
595 if flt_ip is Allocate:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200596 flt_ip = conn.nova.floating_ips.create(pool)
koder aka kdanilovda45e882015-04-06 02:24:42 +0300597
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800598 if flt_ip is not None:
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800599 srv.add_floating_ip(flt_ip)
Yulia Portnova0e64ea22015-03-20 17:27:22 +0200600
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200601 return flt_ip.ip, conn.nova.servers.get(srv.id)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800602
603
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200604def clear_nodes(conn: OSConnection,
605 ids: List[int] = None,
606 name_templ: str = None,
607 max_server_delete_time: int = 120):
koder aka kdanilov765920a2016-04-12 00:35:48 +0300608 try:
609 def need_delete(srv):
610 if name_templ is not None:
611 return re.match(name_templ.format("\\d+"), srv.name) is not None
612 else:
613 return srv.id in ids
614
615 volumes_to_delete = []
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200616 for vol in conn.cinder.volumes.list():
koder aka kdanilov765920a2016-04-12 00:35:48 +0300617 for attachment in vol.attachments:
618 if attachment['server_id'] in ids:
619 volumes_to_delete.append(vol)
620 break
621
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200622 still_alive = set()
623 for srv in conn.nova.servers.list():
koder aka kdanilov765920a2016-04-12 00:35:48 +0300624 if need_delete(srv):
625 logger.debug("Deleting server {0}".format(srv.name))
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200626 conn.nova.servers.delete(srv)
627 still_alive.add(srv.id)
koder aka kdanilov765920a2016-04-12 00:35:48 +0300628
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200629 if still_alive:
630 logger.debug("Waiting till all servers are actually deleted")
631 tout = Timeout(max_server_delete_time, no_exc=True)
632 while tout.tick() and still_alive:
633 all_id = set(srv.id for srv in conn.nova.servers.list())
634 still_alive = still_alive.intersection(all_id)
koder aka kdanilove87ae652015-04-20 02:14:35 +0300635
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200636 if still_alive:
637 logger.warning("Failed to remove servers {}. ".format(",".join(still_alive)) +
638 "You, probably, need to remove them manually (and volumes as well)")
639 return
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800640
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200641 if volumes_to_delete:
642 logger.debug("Deleting volumes")
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800643
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200644 # wait till vm actually deleted
645
646 # logger.warning("Volume deletion commented out")
647 for vol in volumes_to_delete:
648 logger.debug("Deleting volume " + vol.display_name)
649 conn.cinder.volumes.delete(vol)
650
651 logger.debug("Clearing complete (yet some volumes may still be deleting)")
652 except Exception:
koder aka kdanilov765920a2016-04-12 00:35:48 +0300653 logger.exception("During removing servers. " +
654 "You, probably, need to remove them manually")