blob: 302d8982c115cca0763f775e3feb7640936a87d9 [file] [log] [blame]
koder aka kdanilov4643fd62015-02-10 16:20:13 -08001import re
2import os
koder aka kdanilov0fdaaee2015-06-30 11:10:48 +03003import stat
koder aka kdanilov4643fd62015-02-10 16:20:13 -08004import time
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +03005import os.path
koder aka kdanilove21d7472015-02-14 19:02:04 -08006import logging
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +02007import tempfile
8import subprocess
9import urllib.request
kdanylov aka koderb0833332017-05-13 20:39:17 +030010from 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
kdanylov aka koderb0833332017-05-13 20:39:17 +030019from cephlib.common import Timeout, to_ip
20
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +020021from .node_interfaces import NodeInfo
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 kdanilov962ee5f2016-12-19 02:40:08 +020038logger = logging.getLogger("wally")
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):
kdanylov aka koderb0833332017-05-13 20:39:17 +0300457 node_ip = to_ip(ip)
458 if ip != node_ip:
459 logger.info("Will use ip_addr %r instead of hostname %r", node_ip, ip)
460 info = NodeInfo(ConnCreds(node_ip, user, key_file=private_key_path), set())
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200461 info.os_vm_id = os_node.id
462 yield info
koder aka kdanilovda45e882015-04-06 02:24:42 +0300463
464
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200465def get_free_server_groups(conn: OSConnection, template: str) -> Iterator[str]:
koder aka kdanilov34052012015-08-27 18:32:11 +0300466 """get fre server groups, that match given name template
467
468 parameters:
469 nova: nova connection
470 template:str - name template
471 amount:int - ip count
472
473 returns: generator or str - server group names
474 """
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200475 for server_group in conn.nova.server_groups.list():
476 if not server_group.members:
477 if re.match(template, server_group.name):
478 yield str(server_group.id)
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300479
480
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200481def create_vms_mt(conn: OSConnection,
482 amount: int,
483 executor: ThreadPoolExecutor,
484 group_name: str,
485 keypair_name: str,
486 img_name: str,
487 flavor_name: str,
488 vol_sz: int = None,
489 network_zone_name: str = None,
490 flt_ip_pool: str = None,
491 name_templ: str ='wally-{id}',
492 scheduler_hints: Dict = None,
493 security_group: str = None,
494 sec_group_size: int = None) -> List[Tuple[str, Any]]:
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800495
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200496 if network_zone_name is not None:
497 network_future = executor.submit(conn.nova.networks.find,
498 label=network_zone_name)
499 else:
500 network_future = None
koder aka kdanilov97644f92015-02-13 11:11:08 -0800501
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200502 fl_future = executor.submit(conn.nova.flavors.find, name=flavor_name)
503 img_future = executor.submit(conn.nova.images.find, name=img_name)
koder aka kdanilov97644f92015-02-13 11:11:08 -0800504
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200505 if flt_ip_pool is not None:
506 ips_future = executor.submit(get_floating_ips,
507 conn, flt_ip_pool, amount)
508 logger.debug("Wait for floating ip")
509 ips = ips_future.result()
510 ips += [Allocate] * (amount - len(ips))
511 else:
512 ips = [None] * amount
koder aka kdanilov97644f92015-02-13 11:11:08 -0800513
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200514 logger.debug("Getting flavor object")
515 fl = fl_future.result()
516 logger.debug("Getting image object")
517 img = img_future.result()
koder aka kdanilov97644f92015-02-13 11:11:08 -0800518
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200519 if network_future is not None:
520 logger.debug("Waiting for network results")
521 nics = [{'net-id': network_future.result().id}]
522 else:
523 nics = None
koder aka kdanilov97644f92015-02-13 11:11:08 -0800524
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200525 names = [] # type: List[str]
526 for i in range(amount):
527 names.append(name_templ.format(group=group_name, id=i))
koder aka kdanilov97644f92015-02-13 11:11:08 -0800528
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200529 futures = []
530 logger.debug("Requesting new vm's")
koder aka kdanilov6e2ae792015-03-04 18:02:24 -0800531
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200532 orig_scheduler_hints = scheduler_hints.copy()
533 group_name_template = scheduler_hints['group'].format("\\d+")
534 groups = list(get_free_server_groups(conn, group_name_template + "$"))
535 groups.sort()
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300536
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200537 for idx, (name, flt_ip) in enumerate(zip(names, ips), 2):
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300538
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200539 scheduler_hints = None
540 if orig_scheduler_hints is not None and sec_group_size is not None:
541 if "group" in orig_scheduler_hints:
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300542 scheduler_hints = orig_scheduler_hints.copy()
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200543 scheduler_hints['group'] = groups[idx // sec_group_size]
koder aka kdanilovc368eb62015-04-28 18:22:01 +0300544
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200545 if scheduler_hints is None:
546 scheduler_hints = orig_scheduler_hints.copy()
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800547
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200548 params = (conn, name, keypair_name, img, fl,
549 nics, vol_sz, flt_ip, scheduler_hints,
550 flt_ip_pool, [security_group])
551
552 futures.append(executor.submit(create_vm, *params))
553 res = [future.result() for future in futures]
554 logger.debug("Done spawning")
555 return res
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800556
557
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200558def create_vm(conn: OSConnection,
559 name: str,
560 keypair_name: str,
561 img: Any,
562 flavor: Any,
563 nics: List,
564 vol_sz: int = None,
565 flt_ip: Any = False,
566 scheduler_hints: Dict = None,
567 pool: str = None,
568 security_groups=None,
569 max_retry: int = 3,
570 delete_timeout: int = 120) -> Tuple[str, Any]:
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800571
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200572 # make mypy/pylint happy
573 srv = None # type: Any
574 for i in range(max_retry):
575 srv = conn.nova.servers.create(name, flavor=flavor, image=img, nics=nics, key_name=keypair_name,
576 scheduler_hints=scheduler_hints, security_groups=security_groups)
577
578 if not wait_for_server_active(conn, srv):
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200579 logger.debug("Server {} fails to start. Kill it and try again".format(srv))
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200580 conn.nova.servers.delete(srv)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800581
koder aka kdanilovf86d7af2015-05-06 04:01:54 +0300582 try:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200583 for _ in Timeout(delete_timeout, "Server {} delete timeout".format(srv.id)):
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200584 srv = conn.nova.servers.get(srv.id)
koder aka kdanilovf86d7af2015-05-06 04:01:54 +0300585 except NotFound:
586 pass
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800587 else:
588 break
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300589 else:
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200590 raise RuntimeError("Failed to start server {}".format(srv.id))
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800591
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800592 if vol_sz is not None:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200593 vol = create_volume(conn, vol_sz, name)
594 conn.nova.volumes.create_server_volume(srv.id, vol.id, None)
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800595
596 if flt_ip is Allocate:
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200597 flt_ip = conn.nova.floating_ips.create(pool)
koder aka kdanilovda45e882015-04-06 02:24:42 +0300598
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -0800599 if flt_ip is not None:
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800600 srv.add_floating_ip(flt_ip)
Yulia Portnova0e64ea22015-03-20 17:27:22 +0200601
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200602 # pylint: disable=E1101
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200603 return flt_ip.ip, conn.nova.servers.get(srv.id)
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800604
605
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200606def clear_nodes(conn: OSConnection,
607 ids: List[int] = None,
608 name_templ: str = None,
609 max_server_delete_time: int = 120):
koder aka kdanilov765920a2016-04-12 00:35:48 +0300610 try:
611 def need_delete(srv):
612 if name_templ is not None:
613 return re.match(name_templ.format("\\d+"), srv.name) is not None
614 else:
615 return srv.id in ids
616
617 volumes_to_delete = []
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200618 for vol in conn.cinder.volumes.list():
koder aka kdanilov765920a2016-04-12 00:35:48 +0300619 for attachment in vol.attachments:
620 if attachment['server_id'] in ids:
621 volumes_to_delete.append(vol)
622 break
623
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200624 still_alive = set()
625 for srv in conn.nova.servers.list():
koder aka kdanilov765920a2016-04-12 00:35:48 +0300626 if need_delete(srv):
627 logger.debug("Deleting server {0}".format(srv.name))
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200628 conn.nova.servers.delete(srv)
629 still_alive.add(srv.id)
koder aka kdanilov765920a2016-04-12 00:35:48 +0300630
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200631 if still_alive:
632 logger.debug("Waiting till all servers are actually deleted")
633 tout = Timeout(max_server_delete_time, no_exc=True)
634 while tout.tick() and still_alive:
635 all_id = set(srv.id for srv in conn.nova.servers.list())
636 still_alive = still_alive.intersection(all_id)
koder aka kdanilove87ae652015-04-20 02:14:35 +0300637
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200638 if still_alive:
639 logger.warning("Failed to remove servers {}. ".format(",".join(still_alive)) +
640 "You, probably, need to remove them manually (and volumes as well)")
641 return
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800642
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200643 if volumes_to_delete:
644 logger.debug("Deleting volumes")
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800645
koder aka kdanilov3d2bc4f2016-11-12 18:31:18 +0200646 # wait till vm actually deleted
647
648 # logger.warning("Volume deletion commented out")
649 for vol in volumes_to_delete:
650 logger.debug("Deleting volume " + vol.display_name)
651 conn.cinder.volumes.delete(vol)
652
653 logger.debug("Clearing complete (yet some volumes may still be deleting)")
654 except Exception:
koder aka kdanilov765920a2016-04-12 00:35:48 +0300655 logger.exception("During removing servers. " +
656 "You, probably, need to remove them manually")