blob: d26d06d99d2fdb0310b9bf79274482030e704274 [file] [log] [blame]
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +02001# Copyright 2019 Mirantis, Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15import os
16import netaddr
17import yaml
Anna Arhipova9a9fb372023-04-06 12:47:07 +020018import pytest
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +020019
20from devops.helpers import helpers
21from devops.helpers.helpers import ssh_client
22from retry import retry
23
24from cached_property import cached_property
25
26from heatclient import client as heatclient
27from heatclient import exc as heat_exceptions
28from heatclient.common import template_utils
29from keystoneauth1.identity import v3 as keystone_v3
30from keystoneauth1 import session as keystone_session
31
32import requests
33from requests.packages.urllib3.exceptions import InsecureRequestWarning
34
35from oslo_config import cfg
36from paramiko.ssh_exception import (
37 AuthenticationException,
38 BadAuthenticationType)
39
40from tcp_tests import settings
41from tcp_tests import settings_oslo
42from tcp_tests.helpers import exceptions
43from tcp_tests import logger
44
45LOG = logger.logger
46
47EXPECTED_STACK_STATUS = "CREATE_COMPLETE"
Dmitry Tyzhnenkoc800aad2019-05-27 18:10:46 +030048BAD_STACK_STATUSES = ["CREATE_FAILED", "DELETE_FAILED"]
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +020049
50# Disable multiple notifications like:
51# "InsecureRequestWarning: Unverified HTTPS request is being made."
52requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
53
54
55class EnvironmentManagerHeat(object):
56 """Class-helper for creating VMs via devops environments"""
57
58 __config = None
59
60 # Do not use self.__heatclient directly! Use properties
61 # for necessary resources with catching HTTPUnauthorized exception
62 __heatclient = None
63
64 def __init__(self, config=None):
65 """Create/connect to the Heat stack with test environment
66
67 :param config: oslo.config object
68 :param config.hardware.heat_version: Heat version
69 :param config.hardware.os_auth_url: OS auth URL to access heat
70 :param config.hardware.os_username: OS username
71 :param config.hardware.os_password: OS password
72 :param config.hardware.os_project_name: OS tenant name
73 """
74 self.__config = config
75
76 if not self.__config.hardware.heat_stack_name:
77 self.__config.hardware.heat_stack_name = settings.ENV_NAME
78
79 self.__init_heatclient()
80
81 try:
82 stack_status = self._current_stack.stack_status
83 if stack_status != EXPECTED_STACK_STATUS:
84 raise exceptions.EnvironmentWrongStatus(
85 self.__config.hardware.heat_stack_name,
86 EXPECTED_STACK_STATUS,
87 stack_status
88 )
89 LOG.info("Heat stack '{0}' already exists".format(
90 self.__config.hardware.heat_stack_name))
91 except heat_exceptions.HTTPNotFound:
92 self._create_environment()
93 LOG.info("Heat stack '{0}' created".format(
94 self.__config.hardware.heat_stack_name))
95
96 self.set_address_pools_config()
97 self.set_dhcp_ranges_config()
98
99 @cached_property
100 def _keystone_session(self):
101 keystone_auth = keystone_v3.Password(
102 auth_url=settings.OS_AUTH_URL,
103 username=settings.OS_USERNAME,
104 password=settings.OS_PASSWORD,
105 project_name=settings.OS_PROJECT_NAME,
Dennis Dmitrievc902ad82019-04-12 13:41:30 +0300106 user_domain_name=settings.OS_USER_DOMAIN_NAME,
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200107 project_domain_name='Default')
108 return keystone_session.Session(auth=keystone_auth, verify=False)
109
110 def __init_heatclient(self):
111 token = self._keystone_session.get_token()
112 endpoint_url = self._keystone_session.get_endpoint(
113 service_type='orchestration', endpoint_type='publicURL')
114 self.__heatclient = heatclient.Client(
115 version=settings.OS_HEAT_VERSION, endpoint=endpoint_url,
116 token=token, insecure=True)
117
118 @property
119 def _current_stack(self):
120 return self.__stacks.get(
121 self.__config.hardware.heat_stack_name)
122
123 @property
124 def __stacks(self):
125 try:
126 return self.__heatclient.stacks
127 except heat_exceptions.HTTPUnauthorized:
128 LOG.warning("Authorization token outdated, refreshing")
129 self.__init_heatclient()
130 return self.__heatclient.stacks
131
132 @property
133 def __resources(self):
134 try:
135 return self.__heatclient.resources
136 except heat_exceptions.HTTPUnauthorized:
137 LOG.warning("Authorization token outdated, refreshing")
138 self.__init_heatclient()
139 return self.__heatclient.resources
140
Dennis Dmitriev4015adc2019-04-15 18:33:44 +0300141 def __get_stack_parent(self, stack_id, stacks):
142 """Find the parent ID of the specified stack_id in the 'stacks' list"""
143 for stack in stacks:
144 if stack_id == stack.id:
145 if stack.parent:
Oleksii Butenko9cd9cbf2019-08-01 18:30:16 +0300146 try:
147 return self.__get_stack_parent(stack.parent, stacks)
148 except Exception:
149 return stack.id
Dennis Dmitriev4015adc2019-04-15 18:33:44 +0300150 else:
151 return stack.id
152 raise Exception("stack with ID {} not found!".format(stack_id))
153
154 @property
155 def __nested_resources(self):
Anna Arhipova9a9fb372023-04-06 12:47:07 +0200156 if hasattr(pytest, 'resources'):
157 return pytest.resources
158 pytest.resources = list()
Dennis Dmitriev4015adc2019-04-15 18:33:44 +0300159 stacks = [s for s in self.__stacks.list(show_nested=True)]
160 current_stack_id = self._current_stack.id
161 for stack in stacks:
162 parent_stack_id = self.__get_stack_parent(stack.id, stacks)
163 if parent_stack_id == current_stack_id:
164 # Add resources to list
165 LOG.info("Get resources from stack {0}"
166 .format(stack.stack_name))
Anna Arhipova9a9fb372023-04-06 12:47:07 +0200167 pytest.resources.extend([
Dennis Dmitriev4015adc2019-04-15 18:33:44 +0300168 res for res in self.__resources.list(stack.id)
169 ])
Anna Arhipova9a9fb372023-04-06 12:47:07 +0200170 LOG.info("Found {0} resources".format(len(pytest.resources)))
171 return pytest.resources
Dennis Dmitriev4015adc2019-04-15 18:33:44 +0300172
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200173 def _get_resources_by_type(self, resource_type):
174 res = []
Dennis Dmitriev4015adc2019-04-15 18:33:44 +0300175 for item in self.__nested_resources:
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200176 if item.resource_type == resource_type:
177 resource = self.__resources.get(
Dennis Dmitriev4015adc2019-04-15 18:33:44 +0300178 item.stack_name,
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200179 item.resource_name)
180 res.append(resource)
181 return res
182
183 @cached_property
184 def _nodes(self):
185 """Get list of nodenames from heat
186
187 Returns list of dicts.
188 Example:
189 - name: cfg01
190 roles:
191 - salt_master
192 addresses: # Optional. May be an empty dict
193 admin-pool01: p.p.p.202
194 - name: ctl01
195 roles:
196 - salt_minion
197 - openstack_controller
198 - openstack_messaging
199 - openstack_database
200 addresses: {} # Optional. May be an empty dict
201
202 'name': taken from heat template resource's ['name'] parameter
203 'roles': a list taken from resource's ['metadata']['roles'] parameter
204 """
205 address_pools = self._address_pools
206 nodes = []
207 for heat_node in self._get_resources_by_type("OS::Nova::Server"):
208 # addresses will have the following dict structure:
209 # {'admin-pool01': <floating_ip1>,
210 # 'private-pool01': <floating_ip2>,
211 # 'external-pool01': <floating_ip3>
212 # }
213 # , where key is one of roles from OS::Neutron::Subnet,
214 # and value is a floating IP associated to the fixed IP
215 # in this subnet (if exists).
216 # If no floating IPs associated to the server,
217 # then addresses will be an empty list.
218 addresses = {}
219 for network in heat_node.attributes['addresses']:
220 fixed = None
221 floating = None
222 for address in heat_node.attributes['addresses'][network]:
223 addr_type = address['OS-EXT-IPS:type']
224 if addr_type == 'fixed':
225 fixed = address['addr']
226 elif addr_type == 'floating':
227 floating = address['addr']
228 else:
229 LOG.error("Unexpected OS-EXT-IPS:type={0} "
230 "in node '{1}' for network '{2}'"
231 .format(addr_type,
232 heat_node.attributes['name'],
233 network))
Dennis Dmitriev4015adc2019-04-15 18:33:44 +0300234 if fixed is None and floating is None:
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200235 LOG.error("Unable to determine the correct IP address "
236 "in node '{0}' for network '{1}'"
237 .format(heat_node.attributes['name'], network))
238 continue
239 # Check which address pool has the fixed address, and set
240 # the floating address as the access to this address pool.
241 for address_pool in address_pools:
242 pool_net = netaddr.IPNetwork(address_pool['cidr'])
243 if fixed in pool_net:
244 for role in address_pool['roles']:
Dennis Dmitrievc902ad82019-04-12 13:41:30 +0300245 # addresses[role] = floating
246 # Use fixed addresses for SSH access
247 addresses[role] = fixed
Dennis Dmitriev4015adc2019-04-15 18:33:44 +0300248 if 'metadata' not in heat_node.attributes or \
249 'roles' not in heat_node.attributes['metadata']:
250 raise Exception("Node {} doesn't have metadata:roles:[...,...]"
251 .format(heat_node.attributes['name']))
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200252
253 nodes.append({
254 'name': heat_node.attributes['name'],
255 'roles': yaml.load(heat_node.attributes['metadata']['roles']),
256 'addresses': addresses,
257 })
258 return nodes
259
260 @cached_property
261 def _address_pools(self):
262 """Get address pools from subnets OS::Neutron::Subnet
263
264 Returns list of dicts.
265 Example:
266 - roles:
267 - admin-pool01
268 cidr: x.x.x.x/y
269 start: x.x.x.2
270 end: x.x.x.254
271 gateway: x.x.x.1 # or None
272 """
273 pools = []
274 for heat_subnet in self._get_resources_by_type("OS::Neutron::Subnet"):
275 pools.append({
276 'roles': heat_subnet.attributes['tags'],
277 'cidr': heat_subnet.attributes['cidr'],
278 'gateway': heat_subnet.attributes['gateway_ip'],
279 'start': heat_subnet.attributes[
280 'allocation_pools'][0]['start'],
281 'end': heat_subnet.attributes['allocation_pools'][0]['end'],
282 })
283 return pools
284
285 def _get_nodes_by_roles(self, roles=None):
286 nodes = []
287 if roles is None:
288 return self._nodes
289
290 for node in self._nodes:
291 if set(node['roles']).intersection(set(roles)):
292 nodes.append(node)
293 return nodes
294
295 def get_ssh_data(self, roles=None):
296 """Generate ssh config for Underlay
297
298 :param roles: list of strings
299 """
300 if roles is None:
301 raise Exception("No roles specified for the environment!")
302
303 config_ssh = []
304 for d_node in self._get_nodes_by_roles(roles=roles):
305 for pool_name in d_node['addresses']:
306 ssh_data = {
307 'node_name': d_node['name'],
308 'minion_id': d_node['name'],
309 'roles': d_node['roles'],
310 'address_pool': pool_name,
311 'host': d_node['addresses'][pool_name],
312 'login': settings.SSH_NODE_CREDENTIALS['login'],
313 'password': settings.SSH_NODE_CREDENTIALS['password'],
314 'keys': [k['private']
315 for k in self.__config.underlay.ssh_keys]
316 }
317 config_ssh.append(ssh_data)
318 return config_ssh
319
320 def _get_resources_with_wrong_status(self):
321 res = []
Dennis Dmitriev4015adc2019-04-15 18:33:44 +0300322 for item in self.__nested_resources:
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200323 if item.resource_status in BAD_STACK_STATUSES:
324 res.append({
325 'resource_name': item.resource_name,
326 'resource_status': item.resource_status,
327 'resource_status_reason': item.resource_status_reason,
328 'resource_type': item.resource_type
329 })
330 wrong_resources = '\n'.join([
331 "*** Heat stack resource '{0}' ({1}) has wrong status '{2}': {3}"
332 .format(item['resource_name'],
333 item['resource_type'],
334 item['resource_status'],
335 item['resource_status_reason'])
336 for item in res
337 ])
338 return wrong_resources
339
Dmitry Tyzhnenkoc800aad2019-05-27 18:10:46 +0300340 def wait_of_stack_status(self, status, delay=30, tries=60,
341 wait_for_delete=False):
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200342
343 @retry(exceptions.EnvironmentWrongStatus, delay=delay, tries=tries)
344 def wait():
Dmitry Tyzhnenkoc800aad2019-05-27 18:10:46 +0300345 try:
346 st = self._current_stack.stack_status
347 except heat_exceptions.HTTPNotFound as ex:
348 if wait_for_delete is True:
349 return
350 raise ex
Denis V. Meltsaykine68b4452021-01-19 00:48:11 +0100351 except heat_exceptions.HTTPException as ex:
352 # tolerate HTTP timeouts from Heat
353 if ex.code == 504:
354 raise exceptions.EnvironmentWrongStatus(
355 self.__config.hardware.heat_stack_name,
356 status,
357 "Heat API Temporary Unavailable"
358 )
359 else:
360 raise ex
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200361 if st == status:
362 return
363 elif st in BAD_STACK_STATUSES:
364 wrong_resources = self._get_resources_with_wrong_status()
365 raise exceptions.EnvironmentBadStatus(
366 self.__config.hardware.heat_stack_name,
367 status,
368 st,
369 wrong_resources
370 )
371 else:
372 LOG.info("Stack {0} status: {1}".format(
373 self.__config.hardware.heat_stack_name, st))
374 raise exceptions.EnvironmentWrongStatus(
375 self.__config.hardware.heat_stack_name,
376 status,
377 st
378 )
379 LOG.info("Waiting for stack '{0}' status <{1}>".format(
380 self.__config.hardware.heat_stack_name, status))
381 wait()
382
383 def revert_snapshot(self, name):
384 """Revert snapshot by name
385
386 - Revert the heat snapshot in the environment
387 - Try to reload 'config' object from a file 'config_<name>.ini'
388 If the file not found, then pass with defaults.
389 - Set <name> as the current state of the environment after reload
390
391 :param name: string
392 """
393 LOG.info("Reading INI config (without reverting env to snapshot) "
394 "named '{0}'".format(name))
395
396 try:
397 test_config_path = self._get_snapshot_config_name(name)
398 settings_oslo.reload_snapshot_config(self.__config,
399 test_config_path)
400 except cfg.ConfigFilesNotFoundError as conf_err:
401 LOG.error("Config file(s) {0} not found!".format(
402 conf_err.config_files))
403
404 self.__config.hardware.current_snapshot = name
405
406 def create_snapshot(self, name, *args, **kwargs):
407 """Create named snapshot of current env.
408
409 - Create a snapshot for the environment
410 - Save 'config' object to a file 'config_<name>.ini'
411
412 :name: string
413 """
Dennis Dmitrievfa1774a2019-05-28 15:27:44 +0300414 if not settings.MAKE_SNAPSHOT_STAGES:
415 msg = ("[ SKIP snapshot '{0}' because MAKE_SNAPSHOT_STAGES=false ]"
416 .format(name))
417 LOG.info("\n\n{0}\n{1}".format(msg, '*' * len(msg)))
418 return
419
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200420 LOG.info("Store INI config (without env snapshot) named '{0}'"
421 .format(name))
422 self.__config.hardware.current_snapshot = name
423 settings_oslo.save_config(self.__config,
424 name,
425 self.__config.hardware.heat_stack_name)
426
427 def _get_snapshot_config_name(self, snapshot_name):
428 """Get config name for the environment"""
429 env_name = self.__config.hardware.heat_stack_name
430 if env_name is None:
431 env_name = 'config'
432 test_config_path = os.path.join(
433 settings.LOGS_DIR, '{0}_{1}.ini'.format(env_name, snapshot_name))
434 return test_config_path
435
436 def has_snapshot(self, name):
437 # Heat doesn't support live snapshots, so just
438 # check if an INI file was created for this environment,
439 # assuming that the environment has the configuration
440 # described in this INI.
441 return self.has_snapshot_config(name)
442
443 def has_snapshot_config(self, name):
444 test_config_path = self._get_snapshot_config_name(name)
445 return os.path.isfile(test_config_path)
446
447 def start(self, underlay_node_roles, timeout=480):
448 """Start environment"""
449 LOG.warning("HEAT Manager doesn't support start environment feature. "
450 "Waiting for finish the bootstrap process on the nodes "
451 "with accessible SSH")
452
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200453 check_cloudinit_finished = ('[ -f /is_cloud_init_finished ] || '
454 '[ -f /var/log/mcp/.bootstrap_done ]')
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200455 passed = {}
Hanna Arhipovaa02e09a2021-06-04 17:10:38 +0300456 nodes_by_roles = self._get_nodes_by_roles(roles=underlay_node_roles)
457 for node in nodes_by_roles:
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200458 try:
459 node_ip = self.node_ip(node)
460 except exceptions.EnvironmentNodeAccessError:
461 LOG.warning("Node {0} doesn't have accessible IP address"
462 ", skipping".format(node['name']))
463 continue
464
465 LOG.info("Waiting for SSH on node '{0}' / {1} ...".format(
466 node['name'], node_ip))
467
Anna Arhipova9a9fb372023-04-06 12:47:07 +0200468 def _readiness_check(
469 host,
470 port,
471 username=settings.SSH_NODE_CREDENTIALS['login'],
472 password=settings.SSH_NODE_CREDENTIALS['password'],
473 timeout=0):
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200474 try:
475 ssh = ssh_client.SSHClient(
476 host=host, port=port,
477 auth=ssh_client.SSHAuth(
478 username=username,
479 password=password))
480
Anna Arhipova9a9fb372023-04-06 12:47:07 +0200481 def bootstrap_is_successful():
482 is_cloudinit_completed = ssh.execute(
483 "tail -n1 /var/log/cloud-init.log |"
484 "grep -q 'finish: modules-final: SUCCESS'"
485 )['exit_code'] == 0
486 # cfg node doesn't have
487 # 'finish: modules-final: SUCCESS' line
488 # in the logs because the cfg node is rebooted during
489 # bootstrap. Here is /var/log/mcp/.bootstrap_done
490 # file used as a flag
491 is_manually_set_flag = ssh.execute(
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200492 check_cloudinit_finished)['exit_code'] == 0
Anna Arhipova9a9fb372023-04-06 12:47:07 +0200493
494 return is_cloudinit_completed or is_manually_set_flag
495
496 def no_cloudinit():
497 return ssh.execute("[ ! -d /var/lib/cloud/instance ]"
498 )['exit_code'] == 0
499
500 if bootstrap_is_successful():
501 return True
502 elif no_cloudinit():
503 return ssh.execute('echo ok')['exit_code'] == 0
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200504 else:
Anna Arhipova9a9fb372023-04-06 12:47:07 +0200505 return False
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200506
507 except (AuthenticationException, BadAuthenticationType):
508 return True
509 except Exception:
510 return False
511
512 def _ssh_wait(host,
513 port,
514 username=settings.SSH_NODE_CREDENTIALS['login'],
Hanna Arhipovaa02e09a2021-06-04 17:10:38 +0300515 password=settings.SSH_NODE_CREDENTIALS['password']):
516 """
517 Collects hosts have passed the SSH / cloud-init checks.
518 And returns True if host has passed this check
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200519
Hanna Arhipovaa02e09a2021-06-04 17:10:38 +0300520 :param host: str, IP address to connect via SSH
521 :param port: int, port to connect via SSH
522 :param username: str, name of SSH user
523 :param password: str, password for SSH user
524 :return: True if host has passed the probe test
525 """
526 if host in passed and passed[host] >= 1:
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200527 # host already passed the check
528 return True
529
Hanna Arhipovaa02e09a2021-06-04 17:10:38 +0300530 for _node in nodes_by_roles:
531 ip = self.node_ip(_node)
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200532 if ip not in passed:
533 passed[ip] = 0
Anna Arhipova9a9fb372023-04-06 12:47:07 +0200534 if _readiness_check(ip, port):
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200535 passed[ip] += 1
Anna Arhipova9a9fb372023-04-06 12:47:07 +0200536 LOG.info("{} is already ready".format(_node['name']))
537 nodes_by_roles.remove(_node)
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200538 else:
539 passed[ip] = 0
540
541 helpers.wait(
542 lambda: _ssh_wait(node_ip, 22),
543 timeout=timeout,
Hanna Arhipova71c7eac2021-06-01 14:39:31 +0300544 timeout_msg="Node '{}' didn't open SSH "
545 "(or didn't complete cloud-init) "
546 "in {} sec".format(
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200547 node['name'], timeout
548 )
549 )
550 LOG.info('Heat stack "{0}" ready'
551 .format(self.__config.hardware.heat_stack_name))
552
Dmitry Tyzhnenkoc800aad2019-05-27 18:10:46 +0300553 def _verify_resources_status(self, status):
554 """Check that all resources have verified `status`
555
556 In case when all resources have expected status return empty list,
557 otherwise return a list with resources with incorrect status.
558 """
559 ret = [r for r in self.__nested_resources if
560 r.resource_status != status]
561 return ret
562
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200563 def _create_environment(self):
564 tpl_files, template = template_utils.get_template_contents(
565 self.__config.hardware.heat_conf_path)
566 env_files_list = []
567 env_files, env = (
568 template_utils.process_multiple_environments_and_files(
569 env_paths=[self.__config.hardware.heat_env_path],
570 env_list_tracker=env_files_list))
571
Dmitriy Kruglovf40de7e2020-03-03 12:06:00 +0100572 mcp_version = settings.MCP_VERSION
573 if "2019.2" in settings.MCP_VERSION:
574 mcp_version = "2019.2.0"
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200575 fields = {
576 'stack_name': self.__config.hardware.heat_stack_name,
577 'template': template,
578 'files': dict(list(tpl_files.items()) + list(env_files.items())),
579 'environment': env,
Dennis Dmitrievc902ad82019-04-12 13:41:30 +0300580 'parameters': {
Dmitriy Kruglovf40de7e2020-03-03 12:06:00 +0100581 'mcp_version': mcp_version,
Dennis Dmitrievc902ad82019-04-12 13:41:30 +0300582 'env_name': settings.ENV_NAME,
Hanna Arhipova31cb1d82021-01-27 09:41:11 +0200583 'deploy_empty_node': bool(settings.DEPLOY_EMPTY_NODE)
Dennis Dmitrievc902ad82019-04-12 13:41:30 +0300584 }
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200585 }
586
587 if env_files_list:
588 fields['environment_files'] = env_files_list
Dmitry Tyzhnenkoc800aad2019-05-27 18:10:46 +0300589
590 @retry(heat_exceptions.HTTPBadGateway, delay=15, tries=20)
591 def safe_heat_stack_create():
592 self.__stacks.create(**fields)
593
Hanna Arhipova168fc022020-09-04 14:36:17 +0300594 @retry(exceptions.EnvironmentBadStatus, delay=60, tries=1)
Dmitry Tyzhnenkoc800aad2019-05-27 18:10:46 +0300595 def safe_create():
596 self.delete_environment()
597 safe_heat_stack_create()
598 self.wait_of_stack_status(EXPECTED_STACK_STATUS, tries=140)
599 LOG.info("Stack '%s' created",
600 self.__config.hardware.heat_stack_name)
601 incorrect_resources = self._verify_resources_status(
602 EXPECTED_STACK_STATUS)
603 if incorrect_resources:
604 LOG.info("Recreate the stack because some resources have "
605 "incorrect status")
606 for r in incorrect_resources:
607 LOG.error(
608 'The resource %s has status %s. But it should be %s',
609 r.resource_name,
610 r.resource_status,
611 EXPECTED_STACK_STATUS)
612 st = self._current_stack.stack_status
613 raise exceptions.EnvironmentBadStatus(
614 self.__config.hardware.heat_stack_name,
615 EXPECTED_STACK_STATUS,
616 st,
617 incorrect_resources)
618 safe_create()
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200619
620 def stop(self):
621 """Stop environment"""
622 LOG.warning("HEAT Manager doesn't support stop environment feature")
623 pass
624
Dmitry Tyzhnenkoc800aad2019-05-27 18:10:46 +0300625 def delete_environment(self):
626 if list(self.__stacks.list(
627 stack_name=self.__config.hardware.heat_stack_name)):
628 LOG.info("Delete stack '%s'",
629 self.__config.hardware.heat_stack_name)
630
631 @retry(heat_exceptions.HTTPBadGateway, delay=15, tries=20)
632 def safe_heat_stack_delete():
633 self.__stacks.delete(self._current_stack.id)
634
635 safe_heat_stack_delete()
636 self.wait_of_stack_status('DELETE_COMPLETE',
637 delay=30, tries=20,
638 wait_for_delete=True)
639 else:
640 LOG.warning("Can't delete stack '%s' due is absent",
641 self.__config.hardware.heat_stack_name)
642
Dennis Dmitrievf5f2e602017-11-03 15:36:19 +0200643# TODO(ddmitriev): add all Environment methods
644 @staticmethod
645 def node_ip(node, address_pool_name='admin-pool01'):
646 """Determine node's IP
647
648 :param node: a dict element from the self._nodes
649 :return: string
650 """
651 if address_pool_name in node['addresses']:
652 addr = node['addresses'][address_pool_name]
653 LOG.debug('{0} IP= {1}'.format(node['name'], addr))
654 return addr
655 else:
656 raise exceptions.EnvironmentNodeAccessError(
657 node['name'],
658 "No addresses available for the subnet {0}"
659 .format(address_pool_name))
660
661 def set_address_pools_config(self):
662 """Store address pools CIDRs in config object"""
663 for ap in self._address_pools:
664 for role in ap['roles']:
665 self.__config.underlay.address_pools[role] = ap['cidr']
666
667 def set_dhcp_ranges_config(self):
668 """Store DHCP ranges in config object"""
669 for ap in self._address_pools:
670 for role in ap['roles']:
671 self.__config.underlay.dhcp_ranges[role] = {
672 "cidr": ap['cidr'],
673 "start": ap['start'],
674 "end": ap['end'],
675 "gateway": ap['gateway'],
676 }
677
678 def wait_for_node_state(self, node_name, state, timeout):
679 raise NotImplementedError()
680
681 def warm_shutdown_nodes(self, underlay, nodes_prefix, timeout=600):
682 raise NotImplementedError()
683
684 def warm_restart_nodes(self, underlay, nodes_prefix, timeout=600):
685 raise NotImplementedError()
686
687 @property
688 def slave_nodes(self):
689 raise NotImplementedError()