blob: 2e9ab637f4221019d9b0a9792253d118d9478edc [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 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 kdanilov39e449e2016-12-17 15:15:26 +020022from .ssh_utils import ConnCreds
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020023
koder aka kdanilov34052012015-08-27 18:32:11 +030024
koder aka kdanilova94dfe12015-08-19 13:04:51 +030025__doc__ = """
26Module used to reliably spawn set of VM's, evenly distributed across
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020027compute servers in openstack cluster. Main functions:
koder aka kdanilova94dfe12015-08-19 13:04:51 +030028
koder aka kdanilov73084622016-11-16 21:51:08 +020029 get_openstack_credentials - extract openstack credentials from different sources
30 os_connect - connect to nova, cinder and glance API
31 find_vms - find VM's with given prefix in name
32 prepare_os - prepare tenant for usage
koder aka kdanilova94dfe12015-08-19 13:04:51 +030033 launch_vms - reliably start set of VM in parallel with volumes and floating IP
koder aka kdanilov73084622016-11-16 21:51:08 +020034 clear_nodes - clear VM and volumes
koder aka kdanilova94dfe12015-08-19 13:04:51 +030035"""
36
koder aka kdanilov4643fd62015-02-10 16:20:13 -080037
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030038logger = logging.getLogger("wally.vms")
koder aka kdanilove21d7472015-02-14 19:02:04 -080039
40
koder aka kdanilov22d134e2016-11-08 11:33:19 +020041OSCreds = NamedTuple("OSCreds",
42 [("name", str),
43 ("passwd", str),
44 ("tenant", str),
45 ("auth_url", str),
46 ("insecure", bool)])
koder aka kdanilovb7197432015-07-15 00:40:43 +030047
48
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020049# TODO(koder): should correctly process different sources, not only env????
50def get_openstack_credentials() -> OSCreds:
51 is_insecure = os.environ.get('OS_INSECURE', 'false').lower() in ('true', 'yes')
52
53 return OSCreds(os.environ.get('OS_USERNAME'),
54 os.environ.get('OS_PASSWORD'),
55 os.environ.get('OS_TENANT_NAME'),
56 os.environ.get('OS_AUTH_URL'),
57 is_insecure)
koder aka kdanilov1c2b5112015-04-10 16:53:51 +030058
59
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020060class OSConnection:
61 def __init__(self, nova: NovaClient, cinder: CinderClient, glance: GlanceClient) -> None:
62 self.nova = nova
63 self.cinder = cinder
64 self.glance = glance
koder aka kdanilov1c2b5112015-04-10 16:53:51 +030065
66
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020067def os_connect(os_creds: OSCreds, version: str = "2") -> OSConnection:
68 loader = loading.get_plugin_loader('password')
69 auth = loader.load_from_options(auth_url=os_creds.auth_url,
70 username=os_creds.name,
71 password=os_creds.passwd,
72 project_id=os_creds.tenant)
73 auth_sess = session.Session(auth=auth)
koder aka kdanilove87ae652015-04-20 02:14:35 +030074
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020075 glance = GlanceClient(version, session=auth_sess)
76 nova = NovaClient(version, session=auth_sess)
77 cinder = CinderClient(os_creds.name, os_creds.passwd, os_creds.tenant, os_creds.auth_url,
78 insecure=os_creds.insecure, api_version=version)
79 return OSConnection(nova, cinder, glance)
koder aka kdanilove87ae652015-04-20 02:14:35 +030080
81
koder aka kdanilov73084622016-11-16 21:51:08 +020082def find_vms(conn: OSConnection, name_prefix: str) -> Iterable[Tuple[str, int]]:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020083 for srv in conn.nova.servers.list():
koder aka kdanilovf86d7af2015-05-06 04:01:54 +030084 if srv.name.startswith(name_prefix):
koder aka kdanilov73084622016-11-16 21:51:08 +020085 # 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']
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200454 user = params['image']['user']
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 kdanilov39e449e2016-12-17 15:15:26 +0200457 info = NodeInfo(ConnCreds(ip, user, key_file=private_key_path), set())
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200458 info.os_vm_id = os_node.id
459 yield info
koder aka kdanilovda45e882015-04-06 02:24:42 +0300460
461
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200462def get_free_server_groups(conn: OSConnection, template: str) -> Iterator[str]:
koder aka kdanilov34052012015-08-27 18:32:11 +0300463 """get fre server groups, that match given name template
464
465 parameters:
466 nova: nova connection
467 template:str - name template
468 amount:int - ip count
469
470 returns: generator or str - server group names
471 """
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200472 for server_group in conn.nova.server_groups.list():
473 if not server_group.members:
474 if re.match(template, server_group.name):
475 yield str(server_group.id)
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300476
477
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200478def create_vms_mt(conn: OSConnection,
479 amount: int,
480 executor: ThreadPoolExecutor,
481 group_name: str,
482 keypair_name: str,
483 img_name: str,
484 flavor_name: str,
485 vol_sz: int = None,
486 network_zone_name: str = None,
487 flt_ip_pool: str = None,
488 name_templ: str ='wally-{id}',
489 scheduler_hints: Dict = None,
490 security_group: str = None,
491 sec_group_size: int = None) -> List[Tuple[str, Any]]:
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800492
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200493 if network_zone_name is not None:
494 network_future = executor.submit(conn.nova.networks.find,
495 label=network_zone_name)
496 else:
497 network_future = None
koder aka kdanilov97644f92015-02-13 11:11:08 -0800498
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200499 fl_future = executor.submit(conn.nova.flavors.find, name=flavor_name)
500 img_future = executor.submit(conn.nova.images.find, name=img_name)
koder aka kdanilov97644f92015-02-13 11:11:08 -0800501
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200502 if flt_ip_pool is not None:
503 ips_future = executor.submit(get_floating_ips,
504 conn, flt_ip_pool, amount)
505 logger.debug("Wait for floating ip")
506 ips = ips_future.result()
507 ips += [Allocate] * (amount - len(ips))
508 else:
509 ips = [None] * amount
koder aka kdanilov97644f92015-02-13 11:11:08 -0800510
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200511 logger.debug("Getting flavor object")
512 fl = fl_future.result()
513 logger.debug("Getting image object")
514 img = img_future.result()
koder aka kdanilov97644f92015-02-13 11:11:08 -0800515
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200516 if network_future is not None:
517 logger.debug("Waiting for network results")
518 nics = [{'net-id': network_future.result().id}]
519 else:
520 nics = None
koder aka kdanilov97644f92015-02-13 11:11:08 -0800521
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200522 names = [] # type: List[str]
523 for i in range(amount):
524 names.append(name_templ.format(group=group_name, id=i))
koder aka kdanilov97644f92015-02-13 11:11:08 -0800525
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200526 futures = []
527 logger.debug("Requesting new vm's")
koder aka kdanilov6e2ae792015-03-04 18:02:24 -0800528
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200529 orig_scheduler_hints = scheduler_hints.copy()
530 group_name_template = scheduler_hints['group'].format("\\d+")
531 groups = list(get_free_server_groups(conn, group_name_template + "$"))
532 groups.sort()
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300533
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200534 for idx, (name, flt_ip) in enumerate(zip(names, ips), 2):
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300535
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200536 scheduler_hints = None
537 if orig_scheduler_hints is not None and sec_group_size is not None:
538 if "group" in orig_scheduler_hints:
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300539 scheduler_hints = orig_scheduler_hints.copy()
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200540 scheduler_hints['group'] = groups[idx // sec_group_size]
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300541
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200542 if scheduler_hints is None:
543 scheduler_hints = orig_scheduler_hints.copy()
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800544
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200545 params = (conn, name, keypair_name, img, fl,
546 nics, vol_sz, flt_ip, scheduler_hints,
547 flt_ip_pool, [security_group])
548
549 futures.append(executor.submit(create_vm, *params))
550 res = [future.result() for future in futures]
551 logger.debug("Done spawning")
552 return res
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800553
554
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200555def create_vm(conn: OSConnection,
556 name: str,
557 keypair_name: str,
558 img: Any,
559 flavor: Any,
560 nics: List,
561 vol_sz: int = None,
562 flt_ip: Any = False,
563 scheduler_hints: Dict = None,
564 pool: str = None,
565 security_groups=None,
566 max_retry: int = 3,
567 delete_timeout: int = 120) -> Tuple[str, Any]:
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800568
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200569 # make mypy/pylint happy
570 srv = None # type: Any
571 for i in range(max_retry):
572 srv = conn.nova.servers.create(name, flavor=flavor, image=img, nics=nics, key_name=keypair_name,
573 scheduler_hints=scheduler_hints, security_groups=security_groups)
574
575 if not wait_for_server_active(conn, srv):
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200576 logger.debug("Server {} fails to start. Kill it and try again".format(srv))
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200577 conn.nova.servers.delete(srv)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800578
koder aka kdanilovf86d7af2015-05-06 04:01:54 +0300579 try:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200580 for _ in Timeout(delete_timeout, "Server {} delete timeout".format(srv.id)):
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200581 srv = conn.nova.servers.get(srv.id)
koder aka kdanilovf86d7af2015-05-06 04:01:54 +0300582 except NotFound:
583 pass
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800584 else:
585 break
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300586 else:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200587 raise RuntimeError("Failed to start server {}".format(srv.id))
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800588
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800589 if vol_sz is not None:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200590 vol = create_volume(conn, vol_sz, name)
591 conn.nova.volumes.create_server_volume(srv.id, vol.id, None)
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800592
593 if flt_ip is Allocate:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200594 flt_ip = conn.nova.floating_ips.create(pool)
koder aka kdanilovda45e882015-04-06 02:24:42 +0300595
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800596 if flt_ip is not None:
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800597 srv.add_floating_ip(flt_ip)
Yulia Portnova0e64ea22015-03-20 17:27:22 +0200598
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200599 # pylint: disable=E1101
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200600 return flt_ip.ip, conn.nova.servers.get(srv.id)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800601
602
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200603def clear_nodes(conn: OSConnection,
604 ids: List[int] = None,
605 name_templ: str = None,
606 max_server_delete_time: int = 120):
koder aka kdanilov765920a2016-04-12 00:35:48 +0300607 try:
608 def need_delete(srv):
609 if name_templ is not None:
610 return re.match(name_templ.format("\\d+"), srv.name) is not None
611 else:
612 return srv.id in ids
613
614 volumes_to_delete = []
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200615 for vol in conn.cinder.volumes.list():
koder aka kdanilov765920a2016-04-12 00:35:48 +0300616 for attachment in vol.attachments:
617 if attachment['server_id'] in ids:
618 volumes_to_delete.append(vol)
619 break
620
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200621 still_alive = set()
622 for srv in conn.nova.servers.list():
koder aka kdanilov765920a2016-04-12 00:35:48 +0300623 if need_delete(srv):
624 logger.debug("Deleting server {0}".format(srv.name))
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200625 conn.nova.servers.delete(srv)
626 still_alive.add(srv.id)
koder aka kdanilov765920a2016-04-12 00:35:48 +0300627
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200628 if still_alive:
629 logger.debug("Waiting till all servers are actually deleted")
630 tout = Timeout(max_server_delete_time, no_exc=True)
631 while tout.tick() and still_alive:
632 all_id = set(srv.id for srv in conn.nova.servers.list())
633 still_alive = still_alive.intersection(all_id)
koder aka kdanilove87ae652015-04-20 02:14:35 +0300634
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200635 if still_alive:
636 logger.warning("Failed to remove servers {}. ".format(",".join(still_alive)) +
637 "You, probably, need to remove them manually (and volumes as well)")
638 return
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800639
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200640 if volumes_to_delete:
641 logger.debug("Deleting volumes")
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800642
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200643 # wait till vm actually deleted
644
645 # logger.warning("Volume deletion commented out")
646 for vol in volumes_to_delete:
647 logger.debug("Deleting volume " + vol.display_name)
648 conn.cinder.volumes.delete(vol)
649
650 logger.debug("Clearing complete (yet some volumes may still be deleting)")
651 except Exception:
koder aka kdanilov765920a2016-04-12 00:35:48 +0300652 logger.exception("During removing servers. " +
653 "You, probably, need to remove them manually")