1# -*- coding: utf-8 -*- 2# Copyright: (c) 2017 Ansible Project 3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 5from __future__ import (absolute_import, division, print_function) 6 7__metaclass__ = type 8 9DOCUMENTATION = r''' 10 name: scaleway 11 author: 12 - Remy Leone (@sieben) 13 short_description: Scaleway inventory source 14 description: 15 - Get inventory hosts from Scaleway. 16 requirements: 17 - PyYAML 18 options: 19 plugin: 20 description: Token that ensures this is a source file for the 'scaleway' plugin. 21 required: True 22 choices: ['scaleway', 'community.general.scaleway'] 23 regions: 24 description: Filter results on a specific Scaleway region. 25 type: list 26 default: 27 - ams1 28 - par1 29 - par2 30 - waw1 31 tags: 32 description: Filter results on a specific tag. 33 type: list 34 oauth_token: 35 description: 36 - Scaleway OAuth token. 37 - If not explicitly defined or in environment variables, it will try to lookup in the scaleway-cli configuration file 38 (C($SCW_CONFIG_PATH), C($XDG_CONFIG_HOME/scw/config.yaml), or C(~/.config/scw/config.yaml)). 39 - More details on L(how to generate token, https://www.scaleway.com/en/docs/generate-api-keys/). 40 env: 41 # in order of precedence 42 - name: SCW_TOKEN 43 - name: SCW_API_KEY 44 - name: SCW_OAUTH_TOKEN 45 hostnames: 46 description: List of preference about what to use as an hostname. 47 type: list 48 default: 49 - public_ipv4 50 choices: 51 - public_ipv4 52 - private_ipv4 53 - public_ipv6 54 - hostname 55 - id 56 variables: 57 description: 'Set individual variables: keys are variable names and 58 values are templates. Any value returned by the 59 L(Scaleway API, https://developer.scaleway.com/#servers-server-get) 60 can be used.' 61 type: dict 62''' 63 64EXAMPLES = r''' 65# scaleway_inventory.yml file in YAML format 66# Example command line: ansible-inventory --list -i scaleway_inventory.yml 67 68# use hostname as inventory_hostname 69# use the private IP address to connect to the host 70plugin: community.general.scaleway 71regions: 72 - ams1 73 - par1 74tags: 75 - foobar 76hostnames: 77 - hostname 78variables: 79 ansible_host: private_ip 80 state: state 81 82# use hostname as inventory_hostname and public IP address to connect to the host 83plugin: community.general.scaleway 84hostnames: 85 - hostname 86regions: 87 - par1 88variables: 89 ansible_host: public_ip.address 90 91# Using static strings as variables 92plugin: community.general.scaleway 93hostnames: 94 - hostname 95variables: 96 ansible_host: public_ip.address 97 ansible_connection: "'ssh'" 98 ansible_user: "'admin'" 99''' 100 101import os 102import json 103 104try: 105 import yaml 106except ImportError as exc: 107 YAML_IMPORT_ERROR = exc 108else: 109 YAML_IMPORT_ERROR = None 110 111from ansible.errors import AnsibleError 112from ansible.plugins.inventory import BaseInventoryPlugin, Constructable 113from ansible_collections.community.general.plugins.module_utils.scaleway import SCALEWAY_LOCATION, parse_pagination_link 114from ansible.module_utils.urls import open_url 115from ansible.module_utils.common.text.converters import to_native, to_text 116from ansible.module_utils.six import raise_from 117 118import ansible.module_utils.six.moves.urllib.parse as urllib_parse 119 120 121def _fetch_information(token, url): 122 results = [] 123 paginated_url = url 124 while True: 125 try: 126 response = open_url(paginated_url, 127 headers={'X-Auth-Token': token, 128 'Content-type': 'application/json'}) 129 except Exception as e: 130 raise AnsibleError("Error while fetching %s: %s" % (url, to_native(e))) 131 try: 132 raw_json = json.loads(to_text(response.read())) 133 except ValueError: 134 raise AnsibleError("Incorrect JSON payload") 135 136 try: 137 results.extend(raw_json["servers"]) 138 except KeyError: 139 raise AnsibleError("Incorrect format from the Scaleway API response") 140 141 link = response.headers['Link'] 142 if not link: 143 return results 144 relations = parse_pagination_link(link) 145 if 'next' not in relations: 146 return results 147 paginated_url = urllib_parse.urljoin(paginated_url, relations['next']) 148 149 150def _build_server_url(api_endpoint): 151 return "/".join([api_endpoint, "servers"]) 152 153 154def extract_public_ipv4(server_info): 155 try: 156 return server_info["public_ip"]["address"] 157 except (KeyError, TypeError): 158 return None 159 160 161def extract_private_ipv4(server_info): 162 try: 163 return server_info["private_ip"] 164 except (KeyError, TypeError): 165 return None 166 167 168def extract_hostname(server_info): 169 try: 170 return server_info["hostname"] 171 except (KeyError, TypeError): 172 return None 173 174 175def extract_server_id(server_info): 176 try: 177 return server_info["id"] 178 except (KeyError, TypeError): 179 return None 180 181 182def extract_public_ipv6(server_info): 183 try: 184 return server_info["ipv6"]["address"] 185 except (KeyError, TypeError): 186 return None 187 188 189def extract_tags(server_info): 190 try: 191 return server_info["tags"] 192 except (KeyError, TypeError): 193 return None 194 195 196def extract_zone(server_info): 197 try: 198 return server_info["location"]["zone_id"] 199 except (KeyError, TypeError): 200 return None 201 202 203extractors = { 204 "public_ipv4": extract_public_ipv4, 205 "private_ipv4": extract_private_ipv4, 206 "public_ipv6": extract_public_ipv6, 207 "hostname": extract_hostname, 208 "id": extract_server_id 209} 210 211 212class InventoryModule(BaseInventoryPlugin, Constructable): 213 NAME = 'community.general.scaleway' 214 215 def _fill_host_variables(self, host, server_info): 216 targeted_attributes = ( 217 "arch", 218 "commercial_type", 219 "id", 220 "organization", 221 "state", 222 "hostname", 223 ) 224 for attribute in targeted_attributes: 225 self.inventory.set_variable(host, attribute, server_info[attribute]) 226 227 self.inventory.set_variable(host, "tags", server_info["tags"]) 228 229 if extract_public_ipv6(server_info=server_info): 230 self.inventory.set_variable(host, "public_ipv6", extract_public_ipv6(server_info=server_info)) 231 232 if extract_public_ipv4(server_info=server_info): 233 self.inventory.set_variable(host, "public_ipv4", extract_public_ipv4(server_info=server_info)) 234 235 if extract_private_ipv4(server_info=server_info): 236 self.inventory.set_variable(host, "private_ipv4", extract_private_ipv4(server_info=server_info)) 237 238 def _get_zones(self, config_zones): 239 return set(SCALEWAY_LOCATION.keys()).intersection(config_zones) 240 241 def match_groups(self, server_info, tags): 242 server_zone = extract_zone(server_info=server_info) 243 server_tags = extract_tags(server_info=server_info) 244 245 # If a server does not have a zone, it means it is archived 246 if server_zone is None: 247 return set() 248 249 # If no filtering is defined, all tags are valid groups 250 if tags is None: 251 return set(server_tags).union((server_zone,)) 252 253 matching_tags = set(server_tags).intersection(tags) 254 255 if not matching_tags: 256 return set() 257 return matching_tags.union((server_zone,)) 258 259 def _filter_host(self, host_infos, hostname_preferences): 260 261 for pref in hostname_preferences: 262 if extractors[pref](host_infos): 263 return extractors[pref](host_infos) 264 265 return None 266 267 def do_zone_inventory(self, zone, token, tags, hostname_preferences): 268 self.inventory.add_group(zone) 269 zone_info = SCALEWAY_LOCATION[zone] 270 271 url = _build_server_url(zone_info["api_endpoint"]) 272 raw_zone_hosts_infos = _fetch_information(url=url, token=token) 273 274 for host_infos in raw_zone_hosts_infos: 275 276 hostname = self._filter_host(host_infos=host_infos, 277 hostname_preferences=hostname_preferences) 278 279 # No suitable hostname were found in the attributes and the host won't be in the inventory 280 if not hostname: 281 continue 282 283 groups = self.match_groups(host_infos, tags) 284 285 for group in groups: 286 self.inventory.add_group(group=group) 287 self.inventory.add_host(group=group, host=hostname) 288 self._fill_host_variables(host=hostname, server_info=host_infos) 289 290 # Composed variables 291 self._set_composite_vars(self.get_option('variables'), host_infos, hostname, strict=False) 292 293 def get_oauth_token(self): 294 oauth_token = self.get_option('oauth_token') 295 296 if 'SCW_CONFIG_PATH' in os.environ: 297 scw_config_path = os.getenv('SCW_CONFIG_PATH') 298 elif 'XDG_CONFIG_HOME' in os.environ: 299 scw_config_path = os.path.join(os.getenv('XDG_CONFIG_HOME'), 'scw', 'config.yaml') 300 else: 301 scw_config_path = os.path.join(os.path.expanduser('~'), '.config', 'scw', 'config.yaml') 302 303 if not oauth_token and os.path.exists(scw_config_path): 304 with open(scw_config_path) as fh: 305 scw_config = yaml.safe_load(fh) 306 active_profile = scw_config.get('active_profile', 'default') 307 if active_profile == 'default': 308 oauth_token = scw_config.get('secret_key') 309 else: 310 oauth_token = scw_config['profiles'][active_profile].get('secret_key') 311 312 return oauth_token 313 314 def parse(self, inventory, loader, path, cache=True): 315 if YAML_IMPORT_ERROR: 316 raise_from(AnsibleError('PyYAML is probably missing'), YAML_IMPORT_ERROR) 317 super(InventoryModule, self).parse(inventory, loader, path) 318 self._read_config_data(path=path) 319 320 config_zones = self.get_option("regions") 321 tags = self.get_option("tags") 322 token = self.get_oauth_token() 323 if not token: 324 raise AnsibleError("'oauth_token' value is null, you must configure it either in inventory, envvars or scaleway-cli config.") 325 hostname_preference = self.get_option("hostnames") 326 327 for zone in self._get_zones(config_zones): 328 self.do_zone_inventory(zone=zone, token=token, tags=tags, hostname_preferences=hostname_preference) 329