1# Copyright (C) 2009-2010 Canonical Ltd. 2# Copyright (C) 2012 Hewlett-Packard Development Company, L.P. 3# Copyright (C) 2012 Yahoo! Inc. 4# 5# Author: Scott Moser <scott.moser@canonical.com> 6# Author: Juerg Hafliger <juerg.haefliger@hp.com> 7# Author: Joshua Harlow <harlowja@yahoo-inc.com> 8# 9# This file is part of cloud-init. See LICENSE file for license information. 10 11import copy 12import os 13import time 14 15from cloudinit import dmi 16from cloudinit import ec2_utils as ec2 17from cloudinit import log as logging 18from cloudinit import net 19from cloudinit.net.dhcp import EphemeralDHCPv4, NoDHCPLeaseError 20from cloudinit import sources 21from cloudinit import url_helper as uhelp 22from cloudinit import util 23from cloudinit import warnings 24from cloudinit.event import EventScope, EventType 25 26LOG = logging.getLogger(__name__) 27 28SKIP_METADATA_URL_CODES = frozenset([uhelp.NOT_FOUND]) 29 30STRICT_ID_PATH = ("datasource", "Ec2", "strict_id") 31STRICT_ID_DEFAULT = "warn" 32 33API_TOKEN_ROUTE = 'latest/api/token' 34AWS_TOKEN_TTL_SECONDS = '21600' 35AWS_TOKEN_PUT_HEADER = 'X-aws-ec2-metadata-token' 36AWS_TOKEN_REQ_HEADER = AWS_TOKEN_PUT_HEADER + '-ttl-seconds' 37AWS_TOKEN_REDACT = [AWS_TOKEN_PUT_HEADER, AWS_TOKEN_REQ_HEADER] 38 39 40class CloudNames(object): 41 ALIYUN = "aliyun" 42 AWS = "aws" 43 BRIGHTBOX = "brightbox" 44 ZSTACK = "zstack" 45 E24CLOUD = "e24cloud" 46 # UNKNOWN indicates no positive id. If strict_id is 'warn' or 'false', 47 # then an attempt at the Ec2 Metadata service will be made. 48 UNKNOWN = "unknown" 49 # NO_EC2_METADATA indicates this platform does not have a Ec2 metadata 50 # service available. No attempt at the Ec2 Metadata service will be made. 51 NO_EC2_METADATA = "no-ec2-metadata" 52 53 54class DataSourceEc2(sources.DataSource): 55 56 dsname = 'Ec2' 57 # Default metadata urls that will be used if none are provided 58 # They will be checked for 'resolveability' and some of the 59 # following may be discarded if they do not resolve 60 metadata_urls = ["http://169.254.169.254", "http://instance-data.:8773"] 61 62 # The minimum supported metadata_version from the ec2 metadata apis 63 min_metadata_version = '2009-04-04' 64 65 # Priority ordered list of additional metadata versions which will be tried 66 # for extended metadata content. IPv6 support comes in 2016-09-02 67 extended_metadata_versions = ['2018-09-24', '2016-09-02'] 68 69 # Setup read_url parameters per get_url_params. 70 url_max_wait = 120 71 url_timeout = 50 72 73 _api_token = None # API token for accessing the metadata service 74 _network_config = sources.UNSET # Used to cache calculated network cfg v1 75 76 # Whether we want to get network configuration from the metadata service. 77 perform_dhcp_setup = False 78 79 supported_update_events = {EventScope.NETWORK: { 80 EventType.BOOT_NEW_INSTANCE, 81 EventType.BOOT, 82 EventType.BOOT_LEGACY, 83 EventType.HOTPLUG, 84 }} 85 86 def __init__(self, sys_cfg, distro, paths): 87 super(DataSourceEc2, self).__init__(sys_cfg, distro, paths) 88 self.metadata_address = None 89 90 def _get_cloud_name(self): 91 """Return the cloud name as identified during _get_data.""" 92 return identify_platform() 93 94 def _get_data(self): 95 strict_mode, _sleep = read_strict_mode( 96 util.get_cfg_by_path(self.sys_cfg, STRICT_ID_PATH, 97 STRICT_ID_DEFAULT), ("warn", None)) 98 99 LOG.debug("strict_mode: %s, cloud_name=%s cloud_platform=%s", 100 strict_mode, self.cloud_name, self.platform) 101 if strict_mode == "true" and self.cloud_name == CloudNames.UNKNOWN: 102 return False 103 elif self.cloud_name == CloudNames.NO_EC2_METADATA: 104 return False 105 106 if self.perform_dhcp_setup: # Setup networking in init-local stage. 107 if util.is_FreeBSD(): 108 LOG.debug("FreeBSD doesn't support running dhclient with -sf") 109 return False 110 try: 111 with EphemeralDHCPv4(self.fallback_interface): 112 self._crawled_metadata = util.log_time( 113 logfunc=LOG.debug, msg='Crawl of metadata service', 114 func=self.crawl_metadata) 115 except NoDHCPLeaseError: 116 return False 117 else: 118 self._crawled_metadata = util.log_time( 119 logfunc=LOG.debug, msg='Crawl of metadata service', 120 func=self.crawl_metadata) 121 if not self._crawled_metadata: 122 return False 123 self.metadata = self._crawled_metadata.get('meta-data', None) 124 self.userdata_raw = self._crawled_metadata.get('user-data', None) 125 self.identity = self._crawled_metadata.get( 126 'dynamic', {}).get('instance-identity', {}).get('document', {}) 127 return True 128 129 def is_classic_instance(self): 130 """Report if this instance type is Ec2 Classic (non-vpc).""" 131 if not self.metadata: 132 # Can return False on inconclusive as we are also called in 133 # network_config where metadata will be present. 134 # Secondary call site is in packaging postinst script. 135 return False 136 ifaces_md = self.metadata.get('network', {}).get('interfaces', {}) 137 for _mac, mac_data in ifaces_md.get('macs', {}).items(): 138 if 'vpc-id' in mac_data: 139 return False 140 return True 141 142 @property 143 def launch_index(self): 144 if not self.metadata: 145 return None 146 return self.metadata.get('ami-launch-index') 147 148 @property 149 def platform(self): 150 # Handle upgrade path of pickled ds 151 if not hasattr(self, '_platform_type'): 152 self._platform_type = DataSourceEc2.dsname.lower() 153 if not self._platform_type: 154 self._platform_type = DataSourceEc2.dsname.lower() 155 return self._platform_type 156 157 def get_metadata_api_version(self): 158 """Get the best supported api version from the metadata service. 159 160 Loop through all extended support metadata versions in order and 161 return the most-fully featured metadata api version discovered. 162 163 If extended_metadata_versions aren't present, return the datasource's 164 min_metadata_version. 165 """ 166 # Assumes metadata service is already up 167 url_tmpl = '{0}/{1}/meta-data/instance-id' 168 headers = self._get_headers() 169 for api_ver in self.extended_metadata_versions: 170 url = url_tmpl.format(self.metadata_address, api_ver) 171 try: 172 resp = uhelp.readurl(url=url, headers=headers, 173 headers_redact=AWS_TOKEN_REDACT) 174 except uhelp.UrlError as e: 175 LOG.debug('url %s raised exception %s', url, e) 176 else: 177 if resp.code == 200: 178 LOG.debug('Found preferred metadata version %s', api_ver) 179 return api_ver 180 elif resp.code == 404: 181 msg = 'Metadata api version %s not present. Headers: %s' 182 LOG.debug(msg, api_ver, resp.headers) 183 return self.min_metadata_version 184 185 def get_instance_id(self): 186 if self.cloud_name == CloudNames.AWS: 187 # Prefer the ID from the instance identity document, but fall back 188 if not getattr(self, 'identity', None): 189 # If re-using cached datasource, it's get_data run didn't 190 # setup self.identity. So we need to do that now. 191 api_version = self.get_metadata_api_version() 192 self.identity = ec2.get_instance_identity( 193 api_version, self.metadata_address, 194 headers_cb=self._get_headers, 195 headers_redact=AWS_TOKEN_REDACT, 196 exception_cb=self._refresh_stale_aws_token_cb).get( 197 'document', {}) 198 return self.identity.get( 199 'instanceId', self.metadata['instance-id']) 200 else: 201 return self.metadata['instance-id'] 202 203 def _maybe_fetch_api_token(self, mdurls, timeout=None, max_wait=None): 204 """ Get an API token for EC2 Instance Metadata Service. 205 206 On EC2. IMDS will always answer an API token, unless 207 the instance owner has disabled the IMDS HTTP endpoint or 208 the network topology conflicts with the configured hop-limit. 209 """ 210 if self.cloud_name != CloudNames.AWS: 211 return 212 213 urls = [] 214 url2base = {} 215 url_path = API_TOKEN_ROUTE 216 request_method = 'PUT' 217 for url in mdurls: 218 cur = '{0}/{1}'.format(url, url_path) 219 urls.append(cur) 220 url2base[cur] = url 221 222 # use the self._imds_exception_cb to check for Read errors 223 LOG.debug('Fetching Ec2 IMDSv2 API Token') 224 225 response = None 226 url = None 227 url_params = self.get_url_params() 228 try: 229 url, response = uhelp.wait_for_url( 230 urls=urls, max_wait=url_params.max_wait_seconds, 231 timeout=url_params.timeout_seconds, status_cb=LOG.warning, 232 headers_cb=self._get_headers, 233 exception_cb=self._imds_exception_cb, 234 request_method=request_method, 235 headers_redact=AWS_TOKEN_REDACT) 236 except uhelp.UrlError: 237 # We use the raised exception to interupt the retry loop. 238 # Nothing else to do here. 239 pass 240 241 if url and response: 242 self._api_token = response 243 return url2base[url] 244 245 # If we get here, then wait_for_url timed out, waiting for IMDS 246 # or the IMDS HTTP endpoint is disabled 247 return None 248 249 def wait_for_metadata_service(self): 250 mcfg = self.ds_cfg 251 252 url_params = self.get_url_params() 253 if url_params.max_wait_seconds <= 0: 254 return False 255 256 # Remove addresses from the list that wont resolve. 257 mdurls = mcfg.get("metadata_urls", self.metadata_urls) 258 filtered = [x for x in mdurls if util.is_resolvable_url(x)] 259 260 if set(filtered) != set(mdurls): 261 LOG.debug("Removed the following from metadata urls: %s", 262 list((set(mdurls) - set(filtered)))) 263 264 if len(filtered): 265 mdurls = filtered 266 else: 267 LOG.warning("Empty metadata url list! using default list") 268 mdurls = self.metadata_urls 269 270 # try the api token path first 271 metadata_address = self._maybe_fetch_api_token(mdurls) 272 # When running on EC2, we always access IMDS with an API token. 273 # If we could not get an API token, then we assume the IMDS 274 # endpoint was disabled and we move on without a data source. 275 # Fallback to IMDSv1 if not running on EC2 276 if not metadata_address and self.cloud_name != CloudNames.AWS: 277 # if we can't get a token, use instance-id path 278 urls = [] 279 url2base = {} 280 url_path = '{ver}/meta-data/instance-id'.format( 281 ver=self.min_metadata_version) 282 request_method = 'GET' 283 for url in mdurls: 284 cur = '{0}/{1}'.format(url, url_path) 285 urls.append(cur) 286 url2base[cur] = url 287 288 start_time = time.time() 289 url, _ = uhelp.wait_for_url( 290 urls=urls, max_wait=url_params.max_wait_seconds, 291 timeout=url_params.timeout_seconds, status_cb=LOG.warning, 292 headers_redact=AWS_TOKEN_REDACT, headers_cb=self._get_headers, 293 request_method=request_method) 294 295 if url: 296 metadata_address = url2base[url] 297 298 if metadata_address: 299 self.metadata_address = metadata_address 300 LOG.debug("Using metadata source: '%s'", self.metadata_address) 301 elif self.cloud_name == CloudNames.AWS: 302 LOG.warning("IMDS's HTTP endpoint is probably disabled") 303 else: 304 LOG.critical("Giving up on md from %s after %s seconds", 305 urls, int(time.time() - start_time)) 306 307 return bool(metadata_address) 308 309 def device_name_to_device(self, name): 310 # Consult metadata service, that has 311 # ephemeral0: sdb 312 # and return 'sdb' for input 'ephemeral0' 313 if 'block-device-mapping' not in self.metadata: 314 return None 315 316 # Example: 317 # 'block-device-mapping': 318 # {'ami': '/dev/sda1', 319 # 'ephemeral0': '/dev/sdb', 320 # 'root': '/dev/sda1'} 321 found = None 322 bdm = self.metadata['block-device-mapping'] 323 if not isinstance(bdm, dict): 324 LOG.debug("block-device-mapping not a dictionary: '%s'", bdm) 325 return None 326 327 for (entname, device) in bdm.items(): 328 if entname == name: 329 found = device 330 break 331 # LP: #513842 mapping in Euca has 'ephemeral' not 'ephemeral0' 332 if entname == "ephemeral" and name == "ephemeral0": 333 found = device 334 335 if found is None: 336 LOG.debug("Unable to convert %s to a device", name) 337 return None 338 339 ofound = found 340 if not found.startswith("/"): 341 found = "/dev/%s" % found 342 343 if os.path.exists(found): 344 return found 345 346 remapped = self._remap_device(os.path.basename(found)) 347 if remapped: 348 LOG.debug("Remapped device name %s => %s", found, remapped) 349 return remapped 350 351 # On t1.micro, ephemeral0 will appear in block-device-mapping from 352 # metadata, but it will not exist on disk (and never will) 353 # at this point, we've verified that the path did not exist 354 # in the special case of 'ephemeral0' return None to avoid bogus 355 # fstab entry (LP: #744019) 356 if name == "ephemeral0": 357 return None 358 return ofound 359 360 @property 361 def availability_zone(self): 362 try: 363 if self.cloud_name == CloudNames.AWS: 364 return self.identity.get( 365 'availabilityZone', 366 self.metadata['placement']['availability-zone']) 367 else: 368 return self.metadata['placement']['availability-zone'] 369 except KeyError: 370 return None 371 372 @property 373 def region(self): 374 if self.cloud_name == CloudNames.AWS: 375 region = self.identity.get('region') 376 # Fallback to trimming the availability zone if region is missing 377 if self.availability_zone and not region: 378 region = self.availability_zone[:-1] 379 return region 380 else: 381 az = self.availability_zone 382 if az is not None: 383 return az[:-1] 384 return None 385 386 def activate(self, cfg, is_new_instance): 387 if not is_new_instance: 388 return 389 if self.cloud_name == CloudNames.UNKNOWN: 390 warn_if_necessary( 391 util.get_cfg_by_path(cfg, STRICT_ID_PATH, STRICT_ID_DEFAULT), 392 cfg) 393 394 @property 395 def network_config(self): 396 """Return a network config dict for rendering ENI or netplan files.""" 397 if self._network_config != sources.UNSET: 398 return self._network_config 399 400 if self.metadata is None: 401 # this would happen if get_data hadn't been called. leave as UNSET 402 LOG.warning( 403 "Unexpected call to network_config when metadata is None.") 404 return None 405 406 result = None 407 no_network_metadata_on_aws = bool( 408 'network' not in self.metadata and 409 self.cloud_name == CloudNames.AWS) 410 if no_network_metadata_on_aws: 411 LOG.debug("Metadata 'network' not present:" 412 " Refreshing stale metadata from prior to upgrade.") 413 util.log_time( 414 logfunc=LOG.debug, msg='Re-crawl of metadata service', 415 func=self.get_data) 416 417 iface = self.fallback_interface 418 net_md = self.metadata.get('network') 419 if isinstance(net_md, dict): 420 # SRU_BLOCKER: xenial, bionic and eoan should default 421 # apply_full_imds_network_config to False to retain original 422 # behavior on those releases. 423 result = convert_ec2_metadata_network_config( 424 net_md, fallback_nic=iface, 425 full_network_config=util.get_cfg_option_bool( 426 self.ds_cfg, 'apply_full_imds_network_config', True)) 427 428 # RELEASE_BLOCKER: xenial should drop the below if statement, 429 # because the issue being addressed doesn't exist pre-netplan. 430 # (This datasource doesn't implement check_instance_id() so the 431 # datasource object is recreated every boot; this means we don't 432 # need to modify update_events on cloud-init upgrade.) 433 434 # Non-VPC (aka Classic) Ec2 instances need to rewrite the 435 # network config file every boot due to MAC address change. 436 if self.is_classic_instance(): 437 self.default_update_events = copy.deepcopy( 438 self.default_update_events) 439 self.default_update_events[EventScope.NETWORK].add( 440 EventType.BOOT) 441 self.default_update_events[EventScope.NETWORK].add( 442 EventType.BOOT_LEGACY) 443 else: 444 LOG.warning("Metadata 'network' key not valid: %s.", net_md) 445 self._network_config = result 446 447 return self._network_config 448 449 @property 450 def fallback_interface(self): 451 if self._fallback_interface is None: 452 # fallback_nic was used at one point, so restored objects may 453 # have an attribute there. respect that if found. 454 _legacy_fbnic = getattr(self, 'fallback_nic', None) 455 if _legacy_fbnic: 456 self._fallback_interface = _legacy_fbnic 457 self.fallback_nic = None 458 else: 459 return super(DataSourceEc2, self).fallback_interface 460 return self._fallback_interface 461 462 def crawl_metadata(self): 463 """Crawl metadata service when available. 464 465 @returns: Dictionary of crawled metadata content containing the keys: 466 meta-data, user-data and dynamic. 467 """ 468 if not self.wait_for_metadata_service(): 469 return {} 470 api_version = self.get_metadata_api_version() 471 redact = AWS_TOKEN_REDACT 472 crawled_metadata = {} 473 if self.cloud_name == CloudNames.AWS: 474 exc_cb = self._refresh_stale_aws_token_cb 475 exc_cb_ud = self._skip_or_refresh_stale_aws_token_cb 476 else: 477 exc_cb = exc_cb_ud = None 478 try: 479 crawled_metadata['user-data'] = ec2.get_instance_userdata( 480 api_version, self.metadata_address, 481 headers_cb=self._get_headers, headers_redact=redact, 482 exception_cb=exc_cb_ud) 483 crawled_metadata['meta-data'] = ec2.get_instance_metadata( 484 api_version, self.metadata_address, 485 headers_cb=self._get_headers, headers_redact=redact, 486 exception_cb=exc_cb) 487 if self.cloud_name == CloudNames.AWS: 488 identity = ec2.get_instance_identity( 489 api_version, self.metadata_address, 490 headers_cb=self._get_headers, headers_redact=redact, 491 exception_cb=exc_cb) 492 crawled_metadata['dynamic'] = {'instance-identity': identity} 493 except Exception: 494 util.logexc( 495 LOG, "Failed reading from metadata address %s", 496 self.metadata_address) 497 return {} 498 crawled_metadata['_metadata_api_version'] = api_version 499 return crawled_metadata 500 501 def _refresh_api_token(self, seconds=AWS_TOKEN_TTL_SECONDS): 502 """Request new metadata API token. 503 @param seconds: The lifetime of the token in seconds 504 505 @return: The API token or None if unavailable. 506 """ 507 if self.cloud_name != CloudNames.AWS: 508 return None 509 LOG.debug("Refreshing Ec2 metadata API token") 510 request_header = {AWS_TOKEN_REQ_HEADER: seconds} 511 token_url = '{}/{}'.format(self.metadata_address, API_TOKEN_ROUTE) 512 try: 513 response = uhelp.readurl(token_url, headers=request_header, 514 headers_redact=AWS_TOKEN_REDACT, 515 request_method="PUT") 516 except uhelp.UrlError as e: 517 LOG.warning( 518 'Unable to get API token: %s raised exception %s', 519 token_url, e) 520 return None 521 return response.contents 522 523 def _skip_or_refresh_stale_aws_token_cb(self, msg, exception): 524 """Callback will not retry on SKIP_USERDATA_CODES or if no token 525 is available.""" 526 retry = ec2.skip_retry_on_codes( 527 ec2.SKIP_USERDATA_CODES, msg, exception) 528 if not retry: 529 return False # False raises exception 530 return self._refresh_stale_aws_token_cb(msg, exception) 531 532 def _refresh_stale_aws_token_cb(self, msg, exception): 533 """Exception handler for Ec2 to refresh token if token is stale.""" 534 if isinstance(exception, uhelp.UrlError) and exception.code == 401: 535 # With _api_token as None, _get_headers will _refresh_api_token. 536 LOG.debug("Clearing cached Ec2 API token due to expiry") 537 self._api_token = None 538 return True # always retry 539 540 def _imds_exception_cb(self, msg, exception=None): 541 """Fail quickly on proper AWS if IMDSv2 rejects API token request 542 543 Guidance from Amazon is that if IMDSv2 had disabled token requests 544 by returning a 403, or cloud-init malformed requests resulting in 545 other 40X errors, we want the datasource detection to fail quickly 546 without retries as those symptoms will likely not be resolved by 547 retries. 548 549 Exceptions such as requests.ConnectionError due to IMDS being 550 temporarily unroutable or unavailable will still retry due to the 551 callsite wait_for_url. 552 """ 553 if isinstance(exception, uhelp.UrlError): 554 # requests.ConnectionError will have exception.code == None 555 if exception.code and exception.code >= 400: 556 if exception.code == 403: 557 LOG.warning('Ec2 IMDS endpoint returned a 403 error. ' 558 'HTTP endpoint is disabled. Aborting.') 559 else: 560 LOG.warning('Fatal error while requesting ' 561 'Ec2 IMDSv2 API tokens') 562 raise exception 563 564 def _get_headers(self, url=''): 565 """Return a dict of headers for accessing a url. 566 567 If _api_token is unset on AWS, attempt to refresh the token via a PUT 568 and then return the updated token header. 569 """ 570 if self.cloud_name != CloudNames.AWS: 571 return {} 572 # Request a 6 hour token if URL is API_TOKEN_ROUTE 573 request_token_header = {AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS} 574 if API_TOKEN_ROUTE in url: 575 return request_token_header 576 if not self._api_token: 577 # If we don't yet have an API token, get one via a PUT against 578 # API_TOKEN_ROUTE. This _api_token may get unset by a 403 due 579 # to an invalid or expired token 580 self._api_token = self._refresh_api_token() 581 if not self._api_token: 582 return {} 583 return {AWS_TOKEN_PUT_HEADER: self._api_token} 584 585 586class DataSourceEc2Local(DataSourceEc2): 587 """Datasource run at init-local which sets up network to query metadata. 588 589 In init-local, no network is available. This subclass sets up minimal 590 networking with dhclient on a viable nic so that it can talk to the 591 metadata service. If the metadata service provides network configuration 592 then render the network configuration for that instance based on metadata. 593 """ 594 perform_dhcp_setup = True # Use dhcp before querying metadata 595 596 def get_data(self): 597 supported_platforms = (CloudNames.AWS,) 598 if self.cloud_name not in supported_platforms: 599 LOG.debug("Local Ec2 mode only supported on %s, not %s", 600 supported_platforms, self.cloud_name) 601 return False 602 return super(DataSourceEc2Local, self).get_data() 603 604 605def read_strict_mode(cfgval, default): 606 try: 607 return parse_strict_mode(cfgval) 608 except ValueError as e: 609 LOG.warning(e) 610 return default 611 612 613def parse_strict_mode(cfgval): 614 # given a mode like: 615 # true, false, warn,[sleep] 616 # return tuple with string mode (true|false|warn) and sleep. 617 if cfgval is True: 618 return 'true', None 619 if cfgval is False: 620 return 'false', None 621 622 if not cfgval: 623 return 'warn', 0 624 625 mode, _, sleep = cfgval.partition(",") 626 if mode not in ('true', 'false', 'warn'): 627 raise ValueError( 628 "Invalid mode '%s' in strict_id setting '%s': " 629 "Expected one of 'true', 'false', 'warn'." % (mode, cfgval)) 630 631 if sleep: 632 try: 633 sleep = int(sleep) 634 except ValueError as e: 635 raise ValueError( 636 "Invalid sleep '%s' in strict_id setting '%s': not an integer" 637 % (sleep, cfgval) 638 ) from e 639 else: 640 sleep = None 641 642 return mode, sleep 643 644 645def warn_if_necessary(cfgval, cfg): 646 try: 647 mode, sleep = parse_strict_mode(cfgval) 648 except ValueError as e: 649 LOG.warning(e) 650 return 651 652 if mode == "false": 653 return 654 655 warnings.show_warning('non_ec2_md', cfg, mode=True, sleep=sleep) 656 657 658def identify_aws(data): 659 # data is a dictionary returned by _collect_platform_data. 660 if (data['uuid'].startswith('ec2') and 661 (data['uuid_source'] == 'hypervisor' or 662 data['uuid'] == data['serial'])): 663 return CloudNames.AWS 664 665 return None 666 667 668def identify_brightbox(data): 669 if data['serial'].endswith('.brightbox.com'): 670 return CloudNames.BRIGHTBOX 671 672 673def identify_zstack(data): 674 if data['asset_tag'].endswith('.zstack.io'): 675 return CloudNames.ZSTACK 676 677 678def identify_e24cloud(data): 679 if data['vendor'] == 'e24cloud': 680 return CloudNames.E24CLOUD 681 682 683def identify_platform(): 684 # identify the platform and return an entry in CloudNames. 685 data = _collect_platform_data() 686 checks = (identify_aws, identify_brightbox, identify_zstack, 687 identify_e24cloud, lambda x: CloudNames.UNKNOWN) 688 for checker in checks: 689 try: 690 result = checker(data) 691 if result: 692 return result 693 except Exception as e: 694 LOG.warning("calling %s with %s raised exception: %s", 695 checker, data, e) 696 697 698def _collect_platform_data(): 699 """Returns a dictionary of platform info from dmi or /sys/hypervisor. 700 701 Keys in the dictionary are as follows: 702 uuid: system-uuid from dmi or /sys/hypervisor 703 uuid_source: 'hypervisor' (/sys/hypervisor/uuid) or 'dmi' 704 serial: dmi 'system-serial-number' (/sys/.../product_serial) 705 asset_tag: 'dmidecode -s chassis-asset-tag' 706 vendor: dmi 'system-manufacturer' (/sys/.../sys_vendor) 707 708 On Ec2 instances experimentation is that product_serial is upper case, 709 and product_uuid is lower case. This returns lower case values for both. 710 """ 711 data = {} 712 try: 713 uuid = util.load_file("/sys/hypervisor/uuid").strip() 714 data['uuid_source'] = 'hypervisor' 715 except Exception: 716 uuid = dmi.read_dmi_data('system-uuid') 717 data['uuid_source'] = 'dmi' 718 719 if uuid is None: 720 uuid = '' 721 data['uuid'] = uuid.lower() 722 723 serial = dmi.read_dmi_data('system-serial-number') 724 if serial is None: 725 serial = '' 726 727 data['serial'] = serial.lower() 728 729 asset_tag = dmi.read_dmi_data('chassis-asset-tag') 730 if asset_tag is None: 731 asset_tag = '' 732 733 data['asset_tag'] = asset_tag.lower() 734 735 vendor = dmi.read_dmi_data('system-manufacturer') 736 data['vendor'] = (vendor if vendor else '').lower() 737 738 return data 739 740 741def convert_ec2_metadata_network_config( 742 network_md, macs_to_nics=None, fallback_nic=None, 743 full_network_config=True): 744 """Convert ec2 metadata to network config version 2 data dict. 745 746 @param: network_md: 'network' portion of EC2 metadata. 747 generally formed as {"interfaces": {"macs": {}} where 748 'macs' is a dictionary with mac address as key and contents like: 749 {"device-number": "0", "interface-id": "...", "local-ipv4s": ...} 750 @param: macs_to_nics: Optional dict of mac addresses and nic names. If 751 not provided, get_interfaces_by_mac is called to get it from the OS. 752 @param: fallback_nic: Optionally provide the primary nic interface name. 753 This nic will be guaranteed to minimally have a dhcp4 configuration. 754 @param: full_network_config: Boolean set True to configure all networking 755 presented by IMDS. This includes rendering secondary IPv4 and IPv6 756 addresses on all NICs and rendering network config on secondary NICs. 757 If False, only the primary nic will be configured and only with dhcp 758 (IPv4/IPv6). 759 760 @return A dict of network config version 2 based on the metadata and macs. 761 """ 762 netcfg = {'version': 2, 'ethernets': {}} 763 if not macs_to_nics: 764 macs_to_nics = net.get_interfaces_by_mac() 765 macs_metadata = network_md['interfaces']['macs'] 766 767 if not full_network_config: 768 for mac, nic_name in macs_to_nics.items(): 769 if nic_name == fallback_nic: 770 break 771 dev_config = {'dhcp4': True, 772 'dhcp6': False, 773 'match': {'macaddress': mac.lower()}, 774 'set-name': nic_name} 775 nic_metadata = macs_metadata.get(mac) 776 if nic_metadata.get('ipv6s'): # Any IPv6 addresses configured 777 dev_config['dhcp6'] = True 778 netcfg['ethernets'][nic_name] = dev_config 779 return netcfg 780 # Apply network config for all nics and any secondary IPv4/v6 addresses 781 nic_idx = 0 782 for mac, nic_name in sorted(macs_to_nics.items()): 783 nic_metadata = macs_metadata.get(mac) 784 if not nic_metadata: 785 continue # Not a physical nic represented in metadata 786 # device-number is zero-indexed, we want it 1-indexed for the 787 # multiplication on the following line 788 nic_idx = int(nic_metadata.get('device-number', nic_idx)) + 1 789 dhcp_override = {'route-metric': nic_idx * 100} 790 dev_config = {'dhcp4': True, 'dhcp4-overrides': dhcp_override, 791 'dhcp6': False, 792 'match': {'macaddress': mac.lower()}, 793 'set-name': nic_name} 794 if nic_metadata.get('ipv6s'): # Any IPv6 addresses configured 795 dev_config['dhcp6'] = True 796 dev_config['dhcp6-overrides'] = dhcp_override 797 dev_config['addresses'] = get_secondary_addresses(nic_metadata, mac) 798 if not dev_config['addresses']: 799 dev_config.pop('addresses') # Since we found none configured 800 netcfg['ethernets'][nic_name] = dev_config 801 # Remove route-metric dhcp overrides if only one nic configured 802 if len(netcfg['ethernets']) == 1: 803 for nic_name in netcfg['ethernets'].keys(): 804 netcfg['ethernets'][nic_name].pop('dhcp4-overrides') 805 netcfg['ethernets'][nic_name].pop('dhcp6-overrides', None) 806 return netcfg 807 808 809def get_secondary_addresses(nic_metadata, mac): 810 """Parse interface-specific nic metadata and return any secondary IPs 811 812 :return: List of secondary IPv4 or IPv6 addresses to configure on the 813 interface 814 """ 815 ipv4s = nic_metadata.get('local-ipv4s') 816 ipv6s = nic_metadata.get('ipv6s') 817 addresses = [] 818 # In version < 2018-09-24 local_ipv4s or ipv6s is a str with one IP 819 if bool(isinstance(ipv4s, list) and len(ipv4s) > 1): 820 addresses.extend( 821 _get_secondary_addresses( 822 nic_metadata, 'subnet-ipv4-cidr-block', mac, ipv4s, '24')) 823 if bool(isinstance(ipv6s, list) and len(ipv6s) > 1): 824 addresses.extend( 825 _get_secondary_addresses( 826 nic_metadata, 'subnet-ipv6-cidr-block', mac, ipv6s, '128')) 827 return sorted(addresses) 828 829 830def _get_secondary_addresses(nic_metadata, cidr_key, mac, ips, default_prefix): 831 """Return list of IP addresses as CIDRs for secondary IPs 832 833 The CIDR prefix will be default_prefix if cidr_key is absent or not 834 parseable in nic_metadata. 835 """ 836 addresses = [] 837 cidr = nic_metadata.get(cidr_key) 838 prefix = default_prefix 839 if not cidr or len(cidr.split('/')) != 2: 840 ip_type = 'ipv4' if 'ipv4' in cidr_key else 'ipv6' 841 LOG.warning( 842 'Could not parse %s %s for mac %s. %s network' 843 ' config prefix defaults to /%s', 844 cidr_key, cidr, mac, ip_type, prefix) 845 else: 846 prefix = cidr.split('/')[1] 847 # We know we have > 1 ips for in metadata for this IP type 848 for ip in ips[1:]: 849 addresses.append( 850 '{ip}/{prefix}'.format(ip=ip, prefix=prefix)) 851 return addresses 852 853 854# Used to match classes to dependencies 855datasources = [ 856 (DataSourceEc2Local, (sources.DEP_FILESYSTEM,)), # Run at init-local 857 (DataSourceEc2, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)), 858] 859 860 861# Return a list of data sources that match this set of dependencies 862def get_datasource_list(depends): 863 return sources.list_from_depends(depends, datasources) 864 865# vi: ts=4 expandtab 866