koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 1 | import os.path |
| 2 | import socket |
| 3 | import logging |
koder aka kdanilov | 23e6bdf | 2016-12-24 02:18:54 +0200 | [diff] [blame] | 4 | from typing import Dict, Any, List, Tuple, cast, Optional |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 5 | |
| 6 | from .node_interfaces import NodeInfo |
| 7 | from .config import ConfigBlock, Config |
| 8 | from .ssh_utils import ConnCreds |
| 9 | from .openstack_api import (os_connect, find_vms, |
| 10 | OSCreds, get_openstack_credentials, prepare_os, launch_vms, clear_nodes) |
| 11 | from .test_run_class import TestRun |
| 12 | from .stage import Stage, StepOrder |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 13 | from .utils import LogError, StopTestError, get_creds_openrc, to_ip |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 14 | |
| 15 | |
koder aka kdanilov | 962ee5f | 2016-12-19 02:40:08 +0200 | [diff] [blame] | 16 | logger = logging.getLogger("wally") |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 17 | |
| 18 | |
| 19 | def get_floating_ip(vm: Any) -> str: |
| 20 | """Get VM floating IP address""" |
| 21 | |
| 22 | for net_name, ifaces in vm.addresses.items(): |
| 23 | for iface in ifaces: |
| 24 | if iface.get('OS-EXT-IPS:type') == "floating": |
| 25 | return iface['addr'] |
| 26 | |
| 27 | raise ValueError("VM {} has no floating ip".format(vm)) |
| 28 | |
| 29 | |
| 30 | def ensure_connected_to_openstack(ctx: TestRun) -> None: |
| 31 | if not ctx.os_connection is None: |
| 32 | if ctx.os_creds is None: |
| 33 | ctx.os_creds = get_OS_credentials(ctx) |
| 34 | ctx.os_connection = os_connect(ctx.os_creds) |
| 35 | |
| 36 | |
| 37 | def get_OS_credentials(ctx: TestRun) -> OSCreds: |
koder aka kdanilov | ffaf48d | 2016-12-27 02:25:29 +0200 | [diff] [blame] | 38 | stored = ctx.storage.get("openstack_openrc", None) |
| 39 | if stored is not None: |
| 40 | return OSCreds(*cast(List, stored)) |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 41 | |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 42 | creds = None # type: OSCreds |
| 43 | os_creds = None # type: OSCreds |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 44 | force_insecure = False |
| 45 | cfg = ctx.config |
| 46 | |
| 47 | if 'openstack' in cfg.clouds: |
| 48 | os_cfg = cfg.clouds['openstack'] |
| 49 | if 'OPENRC' in os_cfg: |
| 50 | logger.info("Using OS credentials from " + os_cfg['OPENRC']) |
| 51 | creds_tuple = get_creds_openrc(os_cfg['OPENRC']) |
| 52 | os_creds = OSCreds(*creds_tuple) |
| 53 | elif 'ENV' in os_cfg: |
| 54 | logger.info("Using OS credentials from shell environment") |
| 55 | os_creds = get_openstack_credentials() |
| 56 | elif 'OS_TENANT_NAME' in os_cfg: |
| 57 | logger.info("Using predefined credentials") |
| 58 | os_creds = OSCreds(os_cfg['OS_USERNAME'].strip(), |
| 59 | os_cfg['OS_PASSWORD'].strip(), |
| 60 | os_cfg['OS_TENANT_NAME'].strip(), |
| 61 | os_cfg['OS_AUTH_URL'].strip(), |
| 62 | os_cfg.get('OS_INSECURE', False)) |
| 63 | |
| 64 | elif 'OS_INSECURE' in os_cfg: |
| 65 | force_insecure = os_cfg.get('OS_INSECURE', False) |
| 66 | |
| 67 | if os_creds is None and 'fuel' in cfg.clouds and 'openstack_env' in cfg.clouds['fuel'] and \ |
| 68 | ctx.fuel_openstack_creds is not None: |
| 69 | logger.info("Using fuel creds") |
| 70 | creds = ctx.fuel_openstack_creds |
| 71 | elif os_creds is None: |
| 72 | logger.error("Can't found OS credentials") |
| 73 | raise StopTestError("Can't found OS credentials", None) |
| 74 | |
| 75 | if creds is None: |
| 76 | creds = os_creds |
| 77 | |
| 78 | if force_insecure and not creds.insecure: |
| 79 | creds = OSCreds(creds.name, creds.passwd, creds.tenant, creds.auth_url, True) |
| 80 | |
| 81 | logger.debug(("OS_CREDS: user={0.name} tenant={0.tenant} " + |
| 82 | "auth_url={0.auth_url} insecure={0.insecure}").format(creds)) |
| 83 | |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 84 | ctx.storage.put(list(creds), "openstack_openrc") |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 85 | return creds |
| 86 | |
| 87 | |
| 88 | def get_vm_keypair_path(cfg: Config) -> Tuple[str, str]: |
| 89 | key_name = cfg.vm_configs['keypair_name'] |
| 90 | private_path = os.path.join(cfg.settings_dir, key_name + "_private.pem") |
| 91 | public_path = os.path.join(cfg.settings_dir, key_name + "_public.pub") |
| 92 | return (private_path, public_path) |
| 93 | |
| 94 | |
| 95 | class DiscoverOSStage(Stage): |
| 96 | """Discover openstack nodes and VMS""" |
| 97 | |
| 98 | config_block = 'openstack' |
| 99 | |
| 100 | # discover FUEL cluster first |
| 101 | priority = StepOrder.DISCOVER + 1 |
| 102 | |
| 103 | @classmethod |
| 104 | def validate(cls, conf: ConfigBlock) -> None: |
| 105 | pass |
| 106 | |
| 107 | def run(self, ctx: TestRun) -> None: |
kdanylov aka koder | cdfcdaf | 2017-04-29 10:03:39 +0300 | [diff] [blame] | 108 | if 'openstack' not in ctx.config.discover: |
kdanylov aka koder | 150b219 | 2017-04-01 16:53:01 +0300 | [diff] [blame] | 109 | logger.debug("Skip openstack discovery due to settings") |
| 110 | return |
| 111 | |
koder aka kdanilov | 962ee5f | 2016-12-19 02:40:08 +0200 | [diff] [blame] | 112 | if 'all_nodes' in ctx.storage: |
| 113 | logger.debug("Skip openstack discovery, use previously discovered nodes") |
| 114 | return |
| 115 | |
| 116 | ensure_connected_to_openstack(ctx) |
| 117 | |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 118 | cfg = ctx.config.openstack |
| 119 | os_nodes_auth = cfg.auth # type: str |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 120 | if os_nodes_auth.count(":") == 2: |
| 121 | user, password, key_file = os_nodes_auth.split(":") # type: str, Optional[str], Optional[str] |
| 122 | if not password: |
| 123 | password = None |
| 124 | else: |
| 125 | user, password = os_nodes_auth.split(":") |
| 126 | key_file = None |
| 127 | |
kdanylov aka koder | cdfcdaf | 2017-04-29 10:03:39 +0300 | [diff] [blame] | 128 | if 'metadata' not in ctx.config.discover: |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 129 | services = ctx.os_connection.nova.services.list() # type: List[Any] |
| 130 | host_services_mapping = {} # type: Dict[str, List[str]] |
| 131 | |
| 132 | for service in services: |
| 133 | ip = cast(str, socket.gethostbyname(service.host)) |
| 134 | host_services_mapping.get(ip, []).append(service.binary) |
| 135 | |
| 136 | logger.debug("Found %s openstack service nodes" % len(host_services_mapping)) |
| 137 | |
| 138 | for host, services in host_services_mapping.items(): |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 139 | creds = ConnCreds(host=to_ip(host), user=user, passwd=password, key_file=key_file) |
koder aka kdanilov | 962ee5f | 2016-12-19 02:40:08 +0200 | [diff] [blame] | 140 | ctx.merge_node(creds, set(services)) |
| 141 | # TODO: log OS nodes discovery results |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 142 | else: |
kdanylov aka koder | 150b219 | 2017-04-01 16:53:01 +0300 | [diff] [blame] | 143 | logger.info("Skip OS cluster discovery due to 'discovery' setting value") |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 144 | |
koder aka kdanilov | 962ee5f | 2016-12-19 02:40:08 +0200 | [diff] [blame] | 145 | private_key_path = get_vm_keypair_path(ctx.config)[0] |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 146 | |
koder aka kdanilov | 962ee5f | 2016-12-19 02:40:08 +0200 | [diff] [blame] | 147 | vm_creds = None # type: str |
| 148 | for vm_creds in cfg.get("vms", []): |
| 149 | user_name, vm_name_pattern = vm_creds.split("@", 1) |
| 150 | msg = "Vm like {} lookup failed".format(vm_name_pattern) |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 151 | |
koder aka kdanilov | 962ee5f | 2016-12-19 02:40:08 +0200 | [diff] [blame] | 152 | with LogError(msg): |
| 153 | msg = "Looking for vm with name like {0}".format(vm_name_pattern) |
| 154 | logger.debug(msg) |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 155 | |
koder aka kdanilov | 962ee5f | 2016-12-19 02:40:08 +0200 | [diff] [blame] | 156 | ensure_connected_to_openstack(ctx) |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 157 | |
koder aka kdanilov | 962ee5f | 2016-12-19 02:40:08 +0200 | [diff] [blame] | 158 | for ip, vm_id in find_vms(ctx.os_connection, vm_name_pattern): |
koder aka kdanilov | bbbe1dc | 2016-12-20 01:19:56 +0200 | [diff] [blame] | 159 | creds = ConnCreds(host=to_ip(ip), user=user_name, key_file=private_key_path) |
koder aka kdanilov | 962ee5f | 2016-12-19 02:40:08 +0200 | [diff] [blame] | 160 | info = NodeInfo(creds, {'testnode'}) |
| 161 | info.os_vm_id = vm_id |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 162 | nid = info.node_id |
koder aka kdanilov | 962ee5f | 2016-12-19 02:40:08 +0200 | [diff] [blame] | 163 | if nid in ctx.nodes_info: |
| 164 | logger.error("Test VM node has the same id(%s), as existing node %s", nid, ctx.nodes_info[nid]) |
| 165 | raise StopTestError() |
| 166 | ctx.nodes_info[nid] = info |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 167 | |
| 168 | |
| 169 | class CreateOSVMSStage(Stage): |
| 170 | "Spawn new VM's in Openstack cluster" |
| 171 | |
| 172 | priority = StepOrder.SPAWN # type: int |
| 173 | config_block = 'spawn_os_vms' # type: str |
| 174 | |
| 175 | def run(self, ctx: TestRun) -> None: |
koder aka kdanilov | 962ee5f | 2016-12-19 02:40:08 +0200 | [diff] [blame] | 176 | if 'all_nodes' in ctx.storage: |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 177 | ctx.os_spawned_nodes_ids = ctx.storage.get('os_spawned_nodes_ids') |
koder aka kdanilov | 962ee5f | 2016-12-19 02:40:08 +0200 | [diff] [blame] | 178 | logger.info("Skipping OS VMS discovery/spawn as all data found in storage") |
| 179 | return |
| 180 | |
| 181 | if 'os_spawned_nodes_ids' in ctx.storage: |
| 182 | logger.error("spawned_os_nodes_ids is found in storage, but no nodes_info is stored." + |
| 183 | "Fix this before continue") |
| 184 | raise StopTestError() |
| 185 | |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 186 | vm_spawn_config = ctx.config.spawn_os_vms |
| 187 | vm_image_config = ctx.config.vm_configs[vm_spawn_config.cfg_name] |
| 188 | |
koder aka kdanilov | 962ee5f | 2016-12-19 02:40:08 +0200 | [diff] [blame] | 189 | ensure_connected_to_openstack(ctx) |
| 190 | params = vm_image_config.copy() |
| 191 | params.update(vm_spawn_config) |
| 192 | params.update(get_vm_keypair_path(ctx.config)) |
| 193 | params['group_name'] = ctx.config.run_uuid |
| 194 | params['keypair_name'] = ctx.config.vm_configs['keypair_name'] |
| 195 | |
| 196 | if not ctx.config.openstack.get("skip_preparation", False): |
| 197 | logger.info("Preparing openstack") |
| 198 | prepare_os(ctx.os_connection, params) |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 199 | else: |
koder aka kdanilov | 962ee5f | 2016-12-19 02:40:08 +0200 | [diff] [blame] | 200 | logger.info("Scip openstack preparation as 'skip_preparation' is set") |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 201 | |
koder aka kdanilov | 962ee5f | 2016-12-19 02:40:08 +0200 | [diff] [blame] | 202 | ctx.os_spawned_nodes_ids = [] |
| 203 | with ctx.get_pool() as pool: |
| 204 | for info in launch_vms(ctx.os_connection, params, pool): |
| 205 | info.roles.add('testnode') |
koder aka kdanilov | 108ac36 | 2017-01-19 20:17:16 +0200 | [diff] [blame] | 206 | nid = info.node_id |
koder aka kdanilov | 962ee5f | 2016-12-19 02:40:08 +0200 | [diff] [blame] | 207 | if nid in ctx.nodes_info: |
| 208 | logger.error("Test VM node has the same id(%s), as existing node %s", nid, ctx.nodes_info[nid]) |
| 209 | raise StopTestError() |
| 210 | ctx.nodes_info[nid] = info |
| 211 | ctx.os_spawned_nodes_ids.append(info.os_vm_id) |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 212 | |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 213 | ctx.storage.put(ctx.os_spawned_nodes_ids, 'os_spawned_nodes_ids') |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 214 | |
| 215 | def cleanup(self, ctx: TestRun) -> None: |
| 216 | # keep nodes in case of error for future test restart |
| 217 | if not ctx.config.keep_vm and ctx.os_spawned_nodes_ids: |
| 218 | logger.info("Removing nodes") |
| 219 | |
| 220 | clear_nodes(ctx.os_connection, ctx.os_spawned_nodes_ids) |
koder aka kdanilov | 7f59d56 | 2016-12-26 01:34:23 +0200 | [diff] [blame] | 221 | ctx.storage.rm('spawned_os_nodes') |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 222 | |
koder aka kdanilov | 962ee5f | 2016-12-19 02:40:08 +0200 | [diff] [blame] | 223 | logger.info("OS spawned nodes has been successfully removed") |
koder aka kdanilov | 39e449e | 2016-12-17 15:15:26 +0200 | [diff] [blame] | 224 | |
| 225 | |
| 226 | |
| 227 | # @contextlib.contextmanager |
| 228 | # def suspend_vm_nodes_ctx(ctx: TestRun, unused_nodes: List[IRPCNode]) -> Iterator[List[int]]: |
| 229 | # |
| 230 | # pausable_nodes_ids = [cast(int, node.info.os_vm_id) |
| 231 | # for node in unused_nodes |
| 232 | # if node.info.os_vm_id is not None] |
| 233 | # |
| 234 | # non_pausable = len(unused_nodes) - len(pausable_nodes_ids) |
| 235 | # |
| 236 | # if non_pausable: |
| 237 | # logger.warning("Can't pause {} nodes".format(non_pausable)) |
| 238 | # |
| 239 | # if pausable_nodes_ids: |
| 240 | # logger.debug("Try to pause {} unused nodes".format(len(pausable_nodes_ids))) |
| 241 | # with ctx.get_pool() as pool: |
| 242 | # openstack_api.pause(ctx.os_connection, pausable_nodes_ids, pool) |
| 243 | # |
| 244 | # try: |
| 245 | # yield pausable_nodes_ids |
| 246 | # finally: |
| 247 | # if pausable_nodes_ids: |
| 248 | # logger.debug("Unpausing {} nodes".format(len(pausable_nodes_ids))) |
| 249 | # with ctx.get_pool() as pool: |
| 250 | # openstack_api.unpause(ctx.os_connection, pausable_nodes_ids, pool) |
| 251 | # def clouds_connect_stage(ctx: TestRun) -> None: |
| 252 | # TODO(koder): need to use this to connect to openstack in upper code |
| 253 | # conn = ctx.config['clouds/openstack'] |
| 254 | # user, passwd, tenant = parse_creds(conn['creds']) |
| 255 | # auth_data = dict(auth_url=conn['auth_url'], |
| 256 | # username=user, |
| 257 | # api_key=passwd, |
| 258 | # project_id=tenant) # type: Dict[str, str] |
| 259 | # logger.debug("Discovering openstack nodes with connection details: %r", conn) |
| 260 | # connect to openstack, fuel |
| 261 | |
| 262 | # # parse FUEL REST credentials |
| 263 | # username, tenant_name, password = parse_creds(fuel_data['creds']) |
| 264 | # creds = {"username": username, |
| 265 | # "tenant_name": tenant_name, |
| 266 | # "password": password} |
| 267 | # |
| 268 | # # connect to FUEL |
| 269 | # conn = fuel_rest_api.KeystoneAuth(fuel_data['url'], creds, headers=None) |
| 270 | # pass |