Dennis Dmitriev | f5f2e60 | 2017-11-03 15:36:19 +0200 | [diff] [blame] | 1 | # 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 | |
| 15 | import os |
| 16 | import netaddr |
| 17 | import yaml |
| 18 | |
| 19 | from devops.helpers import helpers |
| 20 | from devops.helpers.helpers import ssh_client |
| 21 | from retry import retry |
| 22 | |
| 23 | from cached_property import cached_property |
| 24 | |
| 25 | from heatclient import client as heatclient |
| 26 | from heatclient import exc as heat_exceptions |
| 27 | from heatclient.common import template_utils |
| 28 | from keystoneauth1.identity import v3 as keystone_v3 |
| 29 | from keystoneauth1 import session as keystone_session |
| 30 | |
| 31 | import requests |
| 32 | from requests.packages.urllib3.exceptions import InsecureRequestWarning |
| 33 | |
| 34 | from oslo_config import cfg |
| 35 | from paramiko.ssh_exception import ( |
| 36 | AuthenticationException, |
| 37 | BadAuthenticationType) |
| 38 | |
| 39 | from tcp_tests import settings |
| 40 | from tcp_tests import settings_oslo |
| 41 | from tcp_tests.helpers import exceptions |
| 42 | from tcp_tests import logger |
| 43 | |
| 44 | LOG = logger.logger |
| 45 | |
| 46 | EXPECTED_STACK_STATUS = "CREATE_COMPLETE" |
| 47 | BAD_STACK_STATUSES = ["CREATE_FAILED"] |
| 48 | |
| 49 | # Disable multiple notifications like: |
| 50 | # "InsecureRequestWarning: Unverified HTTPS request is being made." |
| 51 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning) |
| 52 | |
| 53 | |
| 54 | class EnvironmentManagerHeat(object): |
| 55 | """Class-helper for creating VMs via devops environments""" |
| 56 | |
| 57 | __config = None |
| 58 | |
| 59 | # Do not use self.__heatclient directly! Use properties |
| 60 | # for necessary resources with catching HTTPUnauthorized exception |
| 61 | __heatclient = None |
| 62 | |
| 63 | def __init__(self, config=None): |
| 64 | """Create/connect to the Heat stack with test environment |
| 65 | |
| 66 | :param config: oslo.config object |
| 67 | :param config.hardware.heat_version: Heat version |
| 68 | :param config.hardware.os_auth_url: OS auth URL to access heat |
| 69 | :param config.hardware.os_username: OS username |
| 70 | :param config.hardware.os_password: OS password |
| 71 | :param config.hardware.os_project_name: OS tenant name |
| 72 | """ |
| 73 | self.__config = config |
| 74 | |
| 75 | if not self.__config.hardware.heat_stack_name: |
| 76 | self.__config.hardware.heat_stack_name = settings.ENV_NAME |
| 77 | |
| 78 | self.__init_heatclient() |
| 79 | |
| 80 | try: |
| 81 | stack_status = self._current_stack.stack_status |
| 82 | if stack_status != EXPECTED_STACK_STATUS: |
| 83 | raise exceptions.EnvironmentWrongStatus( |
| 84 | self.__config.hardware.heat_stack_name, |
| 85 | EXPECTED_STACK_STATUS, |
| 86 | stack_status |
| 87 | ) |
| 88 | LOG.info("Heat stack '{0}' already exists".format( |
| 89 | self.__config.hardware.heat_stack_name)) |
| 90 | except heat_exceptions.HTTPNotFound: |
| 91 | self._create_environment() |
| 92 | LOG.info("Heat stack '{0}' created".format( |
| 93 | self.__config.hardware.heat_stack_name)) |
| 94 | |
| 95 | self.set_address_pools_config() |
| 96 | self.set_dhcp_ranges_config() |
| 97 | |
| 98 | @cached_property |
| 99 | def _keystone_session(self): |
| 100 | keystone_auth = keystone_v3.Password( |
| 101 | auth_url=settings.OS_AUTH_URL, |
| 102 | username=settings.OS_USERNAME, |
| 103 | password=settings.OS_PASSWORD, |
| 104 | project_name=settings.OS_PROJECT_NAME, |
Dennis Dmitriev | c902ad8 | 2019-04-12 13:41:30 +0300 | [diff] [blame] | 105 | user_domain_name=settings.OS_USER_DOMAIN_NAME, |
Dennis Dmitriev | f5f2e60 | 2017-11-03 15:36:19 +0200 | [diff] [blame] | 106 | project_domain_name='Default') |
| 107 | return keystone_session.Session(auth=keystone_auth, verify=False) |
| 108 | |
| 109 | def __init_heatclient(self): |
| 110 | token = self._keystone_session.get_token() |
| 111 | endpoint_url = self._keystone_session.get_endpoint( |
| 112 | service_type='orchestration', endpoint_type='publicURL') |
| 113 | self.__heatclient = heatclient.Client( |
| 114 | version=settings.OS_HEAT_VERSION, endpoint=endpoint_url, |
| 115 | token=token, insecure=True) |
| 116 | |
| 117 | @property |
| 118 | def _current_stack(self): |
| 119 | return self.__stacks.get( |
| 120 | self.__config.hardware.heat_stack_name) |
| 121 | |
| 122 | @property |
| 123 | def __stacks(self): |
| 124 | try: |
| 125 | return self.__heatclient.stacks |
| 126 | except heat_exceptions.HTTPUnauthorized: |
| 127 | LOG.warning("Authorization token outdated, refreshing") |
| 128 | self.__init_heatclient() |
| 129 | return self.__heatclient.stacks |
| 130 | |
| 131 | @property |
| 132 | def __resources(self): |
| 133 | try: |
| 134 | return self.__heatclient.resources |
| 135 | except heat_exceptions.HTTPUnauthorized: |
| 136 | LOG.warning("Authorization token outdated, refreshing") |
| 137 | self.__init_heatclient() |
| 138 | return self.__heatclient.resources |
| 139 | |
Dennis Dmitriev | 4015adc | 2019-04-15 18:33:44 +0300 | [diff] [blame] | 140 | def __get_stack_parent(self, stack_id, stacks): |
| 141 | """Find the parent ID of the specified stack_id in the 'stacks' list""" |
| 142 | for stack in stacks: |
| 143 | if stack_id == stack.id: |
| 144 | if stack.parent: |
| 145 | return self.__get_stack_parent(stack.parent, stacks) |
| 146 | else: |
| 147 | return stack.id |
| 148 | raise Exception("stack with ID {} not found!".format(stack_id)) |
| 149 | |
| 150 | @property |
| 151 | def __nested_resources(self): |
| 152 | resources = [] |
| 153 | stacks = [s for s in self.__stacks.list(show_nested=True)] |
| 154 | current_stack_id = self._current_stack.id |
| 155 | for stack in stacks: |
| 156 | parent_stack_id = self.__get_stack_parent(stack.id, stacks) |
| 157 | if parent_stack_id == current_stack_id: |
| 158 | # Add resources to list |
| 159 | LOG.info("Get resources from stack {0}" |
| 160 | .format(stack.stack_name)) |
| 161 | resources.extend([ |
| 162 | res for res in self.__resources.list(stack.id) |
| 163 | ]) |
| 164 | LOG.info("Found {0} resources".format(len(resources))) |
| 165 | return resources |
| 166 | |
Dennis Dmitriev | f5f2e60 | 2017-11-03 15:36:19 +0200 | [diff] [blame] | 167 | def _get_resources_by_type(self, resource_type): |
| 168 | res = [] |
Dennis Dmitriev | 4015adc | 2019-04-15 18:33:44 +0300 | [diff] [blame] | 169 | for item in self.__nested_resources: |
Dennis Dmitriev | f5f2e60 | 2017-11-03 15:36:19 +0200 | [diff] [blame] | 170 | if item.resource_type == resource_type: |
| 171 | resource = self.__resources.get( |
Dennis Dmitriev | 4015adc | 2019-04-15 18:33:44 +0300 | [diff] [blame] | 172 | item.stack_name, |
Dennis Dmitriev | f5f2e60 | 2017-11-03 15:36:19 +0200 | [diff] [blame] | 173 | item.resource_name) |
| 174 | res.append(resource) |
| 175 | return res |
| 176 | |
| 177 | @cached_property |
| 178 | def _nodes(self): |
| 179 | """Get list of nodenames from heat |
| 180 | |
| 181 | Returns list of dicts. |
| 182 | Example: |
| 183 | - name: cfg01 |
| 184 | roles: |
| 185 | - salt_master |
| 186 | addresses: # Optional. May be an empty dict |
| 187 | admin-pool01: p.p.p.202 |
| 188 | - name: ctl01 |
| 189 | roles: |
| 190 | - salt_minion |
| 191 | - openstack_controller |
| 192 | - openstack_messaging |
| 193 | - openstack_database |
| 194 | addresses: {} # Optional. May be an empty dict |
| 195 | |
| 196 | 'name': taken from heat template resource's ['name'] parameter |
| 197 | 'roles': a list taken from resource's ['metadata']['roles'] parameter |
| 198 | """ |
| 199 | address_pools = self._address_pools |
| 200 | nodes = [] |
| 201 | for heat_node in self._get_resources_by_type("OS::Nova::Server"): |
| 202 | # addresses will have the following dict structure: |
| 203 | # {'admin-pool01': <floating_ip1>, |
| 204 | # 'private-pool01': <floating_ip2>, |
| 205 | # 'external-pool01': <floating_ip3> |
| 206 | # } |
| 207 | # , where key is one of roles from OS::Neutron::Subnet, |
| 208 | # and value is a floating IP associated to the fixed IP |
| 209 | # in this subnet (if exists). |
| 210 | # If no floating IPs associated to the server, |
| 211 | # then addresses will be an empty list. |
| 212 | addresses = {} |
| 213 | for network in heat_node.attributes['addresses']: |
| 214 | fixed = None |
| 215 | floating = None |
| 216 | for address in heat_node.attributes['addresses'][network]: |
| 217 | addr_type = address['OS-EXT-IPS:type'] |
| 218 | if addr_type == 'fixed': |
| 219 | fixed = address['addr'] |
| 220 | elif addr_type == 'floating': |
| 221 | floating = address['addr'] |
| 222 | else: |
| 223 | LOG.error("Unexpected OS-EXT-IPS:type={0} " |
| 224 | "in node '{1}' for network '{2}'" |
| 225 | .format(addr_type, |
| 226 | heat_node.attributes['name'], |
| 227 | network)) |
Dennis Dmitriev | 4015adc | 2019-04-15 18:33:44 +0300 | [diff] [blame] | 228 | if fixed is None and floating is None: |
Dennis Dmitriev | f5f2e60 | 2017-11-03 15:36:19 +0200 | [diff] [blame] | 229 | LOG.error("Unable to determine the correct IP address " |
| 230 | "in node '{0}' for network '{1}'" |
| 231 | .format(heat_node.attributes['name'], network)) |
| 232 | continue |
| 233 | # Check which address pool has the fixed address, and set |
| 234 | # the floating address as the access to this address pool. |
| 235 | for address_pool in address_pools: |
| 236 | pool_net = netaddr.IPNetwork(address_pool['cidr']) |
| 237 | if fixed in pool_net: |
| 238 | for role in address_pool['roles']: |
Dennis Dmitriev | c902ad8 | 2019-04-12 13:41:30 +0300 | [diff] [blame] | 239 | # addresses[role] = floating |
| 240 | # Use fixed addresses for SSH access |
| 241 | addresses[role] = fixed |
Dennis Dmitriev | 4015adc | 2019-04-15 18:33:44 +0300 | [diff] [blame] | 242 | if 'metadata' not in heat_node.attributes or \ |
| 243 | 'roles' not in heat_node.attributes['metadata']: |
| 244 | raise Exception("Node {} doesn't have metadata:roles:[...,...]" |
| 245 | .format(heat_node.attributes['name'])) |
Dennis Dmitriev | f5f2e60 | 2017-11-03 15:36:19 +0200 | [diff] [blame] | 246 | |
| 247 | nodes.append({ |
| 248 | 'name': heat_node.attributes['name'], |
| 249 | 'roles': yaml.load(heat_node.attributes['metadata']['roles']), |
| 250 | 'addresses': addresses, |
| 251 | }) |
| 252 | return nodes |
| 253 | |
| 254 | @cached_property |
| 255 | def _address_pools(self): |
| 256 | """Get address pools from subnets OS::Neutron::Subnet |
| 257 | |
| 258 | Returns list of dicts. |
| 259 | Example: |
| 260 | - roles: |
| 261 | - admin-pool01 |
| 262 | cidr: x.x.x.x/y |
| 263 | start: x.x.x.2 |
| 264 | end: x.x.x.254 |
| 265 | gateway: x.x.x.1 # or None |
| 266 | """ |
| 267 | pools = [] |
| 268 | for heat_subnet in self._get_resources_by_type("OS::Neutron::Subnet"): |
| 269 | pools.append({ |
| 270 | 'roles': heat_subnet.attributes['tags'], |
| 271 | 'cidr': heat_subnet.attributes['cidr'], |
| 272 | 'gateway': heat_subnet.attributes['gateway_ip'], |
| 273 | 'start': heat_subnet.attributes[ |
| 274 | 'allocation_pools'][0]['start'], |
| 275 | 'end': heat_subnet.attributes['allocation_pools'][0]['end'], |
| 276 | }) |
| 277 | return pools |
| 278 | |
| 279 | def _get_nodes_by_roles(self, roles=None): |
| 280 | nodes = [] |
| 281 | if roles is None: |
| 282 | return self._nodes |
| 283 | |
| 284 | for node in self._nodes: |
| 285 | if set(node['roles']).intersection(set(roles)): |
| 286 | nodes.append(node) |
| 287 | return nodes |
| 288 | |
| 289 | def get_ssh_data(self, roles=None): |
| 290 | """Generate ssh config for Underlay |
| 291 | |
| 292 | :param roles: list of strings |
| 293 | """ |
| 294 | if roles is None: |
| 295 | raise Exception("No roles specified for the environment!") |
| 296 | |
| 297 | config_ssh = [] |
| 298 | for d_node in self._get_nodes_by_roles(roles=roles): |
| 299 | for pool_name in d_node['addresses']: |
| 300 | ssh_data = { |
| 301 | 'node_name': d_node['name'], |
| 302 | 'minion_id': d_node['name'], |
| 303 | 'roles': d_node['roles'], |
| 304 | 'address_pool': pool_name, |
| 305 | 'host': d_node['addresses'][pool_name], |
| 306 | 'login': settings.SSH_NODE_CREDENTIALS['login'], |
| 307 | 'password': settings.SSH_NODE_CREDENTIALS['password'], |
| 308 | 'keys': [k['private'] |
| 309 | for k in self.__config.underlay.ssh_keys] |
| 310 | } |
| 311 | config_ssh.append(ssh_data) |
| 312 | return config_ssh |
| 313 | |
| 314 | def _get_resources_with_wrong_status(self): |
| 315 | res = [] |
Dennis Dmitriev | 4015adc | 2019-04-15 18:33:44 +0300 | [diff] [blame] | 316 | for item in self.__nested_resources: |
Dennis Dmitriev | f5f2e60 | 2017-11-03 15:36:19 +0200 | [diff] [blame] | 317 | if item.resource_status in BAD_STACK_STATUSES: |
| 318 | res.append({ |
| 319 | 'resource_name': item.resource_name, |
| 320 | 'resource_status': item.resource_status, |
| 321 | 'resource_status_reason': item.resource_status_reason, |
| 322 | 'resource_type': item.resource_type |
| 323 | }) |
| 324 | wrong_resources = '\n'.join([ |
| 325 | "*** Heat stack resource '{0}' ({1}) has wrong status '{2}': {3}" |
| 326 | .format(item['resource_name'], |
| 327 | item['resource_type'], |
| 328 | item['resource_status'], |
| 329 | item['resource_status_reason']) |
| 330 | for item in res |
| 331 | ]) |
| 332 | return wrong_resources |
| 333 | |
| 334 | def wait_of_stack_status(self, status, delay=30, tries=60): |
| 335 | |
| 336 | @retry(exceptions.EnvironmentWrongStatus, delay=delay, tries=tries) |
| 337 | def wait(): |
| 338 | st = self._current_stack.stack_status |
| 339 | if st == status: |
| 340 | return |
| 341 | elif st in BAD_STACK_STATUSES: |
| 342 | wrong_resources = self._get_resources_with_wrong_status() |
| 343 | raise exceptions.EnvironmentBadStatus( |
| 344 | self.__config.hardware.heat_stack_name, |
| 345 | status, |
| 346 | st, |
| 347 | wrong_resources |
| 348 | ) |
| 349 | else: |
| 350 | LOG.info("Stack {0} status: {1}".format( |
| 351 | self.__config.hardware.heat_stack_name, st)) |
| 352 | raise exceptions.EnvironmentWrongStatus( |
| 353 | self.__config.hardware.heat_stack_name, |
| 354 | status, |
| 355 | st |
| 356 | ) |
| 357 | LOG.info("Waiting for stack '{0}' status <{1}>".format( |
| 358 | self.__config.hardware.heat_stack_name, status)) |
| 359 | wait() |
| 360 | |
| 361 | def revert_snapshot(self, name): |
| 362 | """Revert snapshot by name |
| 363 | |
| 364 | - Revert the heat snapshot in the environment |
| 365 | - Try to reload 'config' object from a file 'config_<name>.ini' |
| 366 | If the file not found, then pass with defaults. |
| 367 | - Set <name> as the current state of the environment after reload |
| 368 | |
| 369 | :param name: string |
| 370 | """ |
| 371 | LOG.info("Reading INI config (without reverting env to snapshot) " |
| 372 | "named '{0}'".format(name)) |
| 373 | |
| 374 | try: |
| 375 | test_config_path = self._get_snapshot_config_name(name) |
| 376 | settings_oslo.reload_snapshot_config(self.__config, |
| 377 | test_config_path) |
| 378 | except cfg.ConfigFilesNotFoundError as conf_err: |
| 379 | LOG.error("Config file(s) {0} not found!".format( |
| 380 | conf_err.config_files)) |
| 381 | |
| 382 | self.__config.hardware.current_snapshot = name |
| 383 | |
| 384 | def create_snapshot(self, name, *args, **kwargs): |
| 385 | """Create named snapshot of current env. |
| 386 | |
| 387 | - Create a snapshot for the environment |
| 388 | - Save 'config' object to a file 'config_<name>.ini' |
| 389 | |
| 390 | :name: string |
| 391 | """ |
| 392 | LOG.info("Store INI config (without env snapshot) named '{0}'" |
| 393 | .format(name)) |
| 394 | self.__config.hardware.current_snapshot = name |
| 395 | settings_oslo.save_config(self.__config, |
| 396 | name, |
| 397 | self.__config.hardware.heat_stack_name) |
| 398 | |
| 399 | def _get_snapshot_config_name(self, snapshot_name): |
| 400 | """Get config name for the environment""" |
| 401 | env_name = self.__config.hardware.heat_stack_name |
| 402 | if env_name is None: |
| 403 | env_name = 'config' |
| 404 | test_config_path = os.path.join( |
| 405 | settings.LOGS_DIR, '{0}_{1}.ini'.format(env_name, snapshot_name)) |
| 406 | return test_config_path |
| 407 | |
| 408 | def has_snapshot(self, name): |
| 409 | # Heat doesn't support live snapshots, so just |
| 410 | # check if an INI file was created for this environment, |
| 411 | # assuming that the environment has the configuration |
| 412 | # described in this INI. |
| 413 | return self.has_snapshot_config(name) |
| 414 | |
| 415 | def has_snapshot_config(self, name): |
| 416 | test_config_path = self._get_snapshot_config_name(name) |
| 417 | return os.path.isfile(test_config_path) |
| 418 | |
| 419 | def start(self, underlay_node_roles, timeout=480): |
| 420 | """Start environment""" |
| 421 | LOG.warning("HEAT Manager doesn't support start environment feature. " |
| 422 | "Waiting for finish the bootstrap process on the nodes " |
| 423 | "with accessible SSH") |
| 424 | |
| 425 | check_cloudinit_started = '[ -f /is_cloud_init_started ]' |
| 426 | check_cloudinit_finished = ('[ -f /is_cloud_init_finished ] || ' |
| 427 | '[ -f /var/log/mcp/.bootstrap_done ]') |
| 428 | check_cloudinit_failed = 'cat /is_cloud_init_failed' |
| 429 | passed = {} |
| 430 | for node in self._get_nodes_by_roles(roles=underlay_node_roles): |
| 431 | |
| 432 | try: |
| 433 | node_ip = self.node_ip(node) |
| 434 | except exceptions.EnvironmentNodeAccessError: |
| 435 | LOG.warning("Node {0} doesn't have accessible IP address" |
| 436 | ", skipping".format(node['name'])) |
| 437 | continue |
| 438 | |
| 439 | LOG.info("Waiting for SSH on node '{0}' / {1} ...".format( |
| 440 | node['name'], node_ip)) |
| 441 | |
| 442 | def _ssh_check(host, |
| 443 | port, |
| 444 | username=settings.SSH_NODE_CREDENTIALS['login'], |
| 445 | password=settings.SSH_NODE_CREDENTIALS['password'], |
| 446 | timeout=0): |
| 447 | try: |
| 448 | ssh = ssh_client.SSHClient( |
| 449 | host=host, port=port, |
| 450 | auth=ssh_client.SSHAuth( |
| 451 | username=username, |
| 452 | password=password)) |
| 453 | |
| 454 | # If '/is_cloud_init_started' exists, then wait for |
| 455 | # the flag /is_cloud_init_finished |
| 456 | if ssh.execute(check_cloudinit_started)['exit_code'] == 0: |
| 457 | result = ssh.execute(check_cloudinit_failed) |
| 458 | if result['exit_code'] == 0: |
| 459 | raise exceptions.EnvironmentNodeIsNotStarted( |
| 460 | "{0}:{1}".format(host, port), |
| 461 | result.stdout_str) |
| 462 | |
| 463 | status = ssh.execute( |
| 464 | check_cloudinit_finished)['exit_code'] == 0 |
| 465 | # Else, just wait for SSH |
| 466 | else: |
| 467 | status = ssh.execute('echo ok')['exit_code'] == 0 |
| 468 | return status |
| 469 | |
| 470 | except (AuthenticationException, BadAuthenticationType): |
| 471 | return True |
| 472 | except Exception: |
| 473 | return False |
| 474 | |
| 475 | def _ssh_wait(host, |
| 476 | port, |
| 477 | username=settings.SSH_NODE_CREDENTIALS['login'], |
| 478 | password=settings.SSH_NODE_CREDENTIALS['password'], |
| 479 | timeout=0): |
| 480 | |
| 481 | if host in passed and passed[host] >= 2: |
| 482 | # host already passed the check |
| 483 | return True |
| 484 | |
| 485 | for node in self._get_nodes_by_roles( |
| 486 | roles=underlay_node_roles): |
| 487 | ip = node_ip |
| 488 | if ip not in passed: |
| 489 | passed[ip] = 0 |
| 490 | if _ssh_check(ip, port): |
| 491 | passed[ip] += 1 |
| 492 | else: |
| 493 | passed[ip] = 0 |
| 494 | |
| 495 | helpers.wait( |
| 496 | lambda: _ssh_wait(node_ip, 22), |
| 497 | timeout=timeout, |
| 498 | timeout_msg="Node '{}' didn't open SSH in {} sec".format( |
| 499 | node['name'], timeout |
| 500 | ) |
| 501 | ) |
| 502 | LOG.info('Heat stack "{0}" ready' |
| 503 | .format(self.__config.hardware.heat_stack_name)) |
| 504 | |
| 505 | def _create_environment(self): |
| 506 | tpl_files, template = template_utils.get_template_contents( |
| 507 | self.__config.hardware.heat_conf_path) |
| 508 | env_files_list = [] |
| 509 | env_files, env = ( |
| 510 | template_utils.process_multiple_environments_and_files( |
| 511 | env_paths=[self.__config.hardware.heat_env_path], |
| 512 | env_list_tracker=env_files_list)) |
| 513 | |
| 514 | fields = { |
| 515 | 'stack_name': self.__config.hardware.heat_stack_name, |
| 516 | 'template': template, |
| 517 | 'files': dict(list(tpl_files.items()) + list(env_files.items())), |
| 518 | 'environment': env, |
Dennis Dmitriev | c902ad8 | 2019-04-12 13:41:30 +0300 | [diff] [blame] | 519 | 'parameters': { |
| 520 | 'mcp_version': settings.MCP_VERSION, |
| 521 | 'env_name': settings.ENV_NAME, |
| 522 | } |
Dennis Dmitriev | f5f2e60 | 2017-11-03 15:36:19 +0200 | [diff] [blame] | 523 | } |
| 524 | |
| 525 | if env_files_list: |
| 526 | fields['environment_files'] = env_files_list |
| 527 | |
| 528 | self.__stacks.create(**fields) |
| 529 | self.wait_of_stack_status(EXPECTED_STACK_STATUS) |
| 530 | LOG.info("Stack '{0}' created" |
| 531 | .format(self.__config.hardware.heat_stack_name)) |
| 532 | |
| 533 | def stop(self): |
| 534 | """Stop environment""" |
| 535 | LOG.warning("HEAT Manager doesn't support stop environment feature") |
| 536 | pass |
| 537 | |
| 538 | # TODO(ddmitriev): add all Environment methods |
| 539 | @staticmethod |
| 540 | def node_ip(node, address_pool_name='admin-pool01'): |
| 541 | """Determine node's IP |
| 542 | |
| 543 | :param node: a dict element from the self._nodes |
| 544 | :return: string |
| 545 | """ |
| 546 | if address_pool_name in node['addresses']: |
| 547 | addr = node['addresses'][address_pool_name] |
| 548 | LOG.debug('{0} IP= {1}'.format(node['name'], addr)) |
| 549 | return addr |
| 550 | else: |
| 551 | raise exceptions.EnvironmentNodeAccessError( |
| 552 | node['name'], |
| 553 | "No addresses available for the subnet {0}" |
| 554 | .format(address_pool_name)) |
| 555 | |
| 556 | def set_address_pools_config(self): |
| 557 | """Store address pools CIDRs in config object""" |
| 558 | for ap in self._address_pools: |
| 559 | for role in ap['roles']: |
| 560 | self.__config.underlay.address_pools[role] = ap['cidr'] |
| 561 | |
| 562 | def set_dhcp_ranges_config(self): |
| 563 | """Store DHCP ranges in config object""" |
| 564 | for ap in self._address_pools: |
| 565 | for role in ap['roles']: |
| 566 | self.__config.underlay.dhcp_ranges[role] = { |
| 567 | "cidr": ap['cidr'], |
| 568 | "start": ap['start'], |
| 569 | "end": ap['end'], |
| 570 | "gateway": ap['gateway'], |
| 571 | } |
| 572 | |
| 573 | def wait_for_node_state(self, node_name, state, timeout): |
| 574 | raise NotImplementedError() |
| 575 | |
| 576 | def warm_shutdown_nodes(self, underlay, nodes_prefix, timeout=600): |
| 577 | raise NotImplementedError() |
| 578 | |
| 579 | def warm_restart_nodes(self, underlay, nodes_prefix, timeout=600): |
| 580 | raise NotImplementedError() |
| 581 | |
| 582 | @property |
| 583 | def slave_nodes(self): |
| 584 | raise NotImplementedError() |