Merge ~chad.smith/cloud-init:feature/azure-network-on-boot into cloud-init:master

Proposed by Chad Smith
Status: Superseded
Proposed branch: ~chad.smith/cloud-init:feature/azure-network-on-boot
Merge into: cloud-init:master
Prerequisite: ~chad.smith/cloud-init:feature/maintain-network-on-boot
Diff against target: 1073 lines (+651/-83)
5 files modified
cloudinit/cmd/devel/net_convert.py (+7/-2)
cloudinit/sources/DataSourceAzure.py (+239/-29)
cloudinit/sources/__init__.py (+0/-19)
cloudinit/sources/tests/test_init.py (+0/-5)
tests/unittests/test_datasource/test_azure.py (+405/-28)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
cloud-init Commiters Pending
Review via email: mp+348704@code.launchpad.net

Commit message

azure: allow azure to generate network configuration from IMDS on each
boot

Azure datasource now queries IMDS metadata service for network
configuration at link local address
http://169.254.169.254/metadata/instance?api-version=2017-12-01. The
azure metadata service presents a list of macs and allocated ip addresses
associated with this instance. Azure will now also regenerate network
configuration on every boot because it subscribes to EventType.BOOT
maintenance events as well as the 'first boot'
EventType.BOOT_NEW_INSTANCE.

Also refactor _get_data into 3 discrete methods:
  - is_platform_viable: check quickly whether the datasource is
    potentially compatible with the platform on which is is running
  - crawl_metadata: walk all potential metadata candidates, returning a
    structured dict of all metadata and userdata. Raise InvalidMetaData on
    error.
  - _get_data: call crawl_metadata and process results or error. Cache
    instance data on class attributes: metadata, userdata_raw etc.

Description of the change

As a note: current supported Ubuntu images contain some magic udev rules and/or netplan config which automatically setup dhcp on any nic eth1 or greater which shows up on an azure instance during boot. With this functionality in cloud-init proper, we can drop the static script definitions from supported ubuntu images because cloud-init will own the network config on any new device that shows up across boot.

to test:
create an Azure vm;
ssh azcurl -H Metadata:true "http://169.254.169.254/metadata/instance?api-version=2017-12-01" > azure.json

python3 -m cloudinit.cmd.main devel net_convert --kind azure-imds --network-data azure.json -d output -O netplan

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:155c7688e9d7efa125a68c14b1148b2f06b32416
https://jenkins.ubuntu.com/server/job/cloud-init-ci/129/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    FAILED: Ubuntu LTS: Build

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/129/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:4bcee7ca6f7b5d2e22197f48b12dcaa5eaf3867e
https://jenkins.ubuntu.com/server/job/cloud-init-ci/138/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    FAILED: Ubuntu LTS: Build

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/138/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:68e932545678c8330c753ad1c10be7d7c65cf424
https://jenkins.ubuntu.com/server/job/cloud-init-ci/139/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    FAILED: Ubuntu LTS: Build

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/139/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:0f5857a45285c2eb0a78f25e85ae7c0a55778fb4
https://jenkins.ubuntu.com/server/job/cloud-init-ci/140/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/140/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Ryan Harper (raharper) wrote :

This is coming together.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:c7562a61e272daa91c6e01363f5cb3d2abd88c6f
https://jenkins.ubuntu.com/server/job/cloud-init-ci/154/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/154/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:a8fe7604191724d72d895d26fbb6e8299fac3180
https://jenkins.ubuntu.com/server/job/cloud-init-ci/159/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/159/rebuild

review: Needs Fixing (continuous-integration)
0e7a589... by Chad Smith

azure: azure to generate network configuration from IMDS on each boot

Azure datasource now queries IMDS metadata service for network
configuration at link local address
http://169.254.169.254/metadata/instance?api-version=2017-12-01. The
azure metadata service presents a list of macs and allocated ip addresses
associated with this instance. Azure will now also regenerate network
configuration on every boot because it subscribes to EventType.BOOT
maintenance events as well as the 'first boot'
EventType.BOOT_NEW_INSTANCE.

Also refactor _get_data into 3 discrete methods:
  - is_platform_viable: check quickly whether the datasource is
    potentially compatible with the platform on which is is running
  - crawl_metadata: walk all potential metadata candidates, returning a
    structured dict of all metadata and userdata. Raise InvalidMetaData on
    error.
  - _get_data: call crawl_metadata and process results or error. Cache
    instance data on class attributes: metadata, userdata_raw etc.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:fd6b0b18f6e99e7997b4cd198ed05ff533569442
https://jenkins.ubuntu.com/server/job/cloud-init-ci/190/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/190/rebuild

review: Needs Fixing (continuous-integration)
fb7381b... by Chad Smith

pyflakes

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:6bcbd81055f16cbc2dec096884ca8463f37985a3
https://jenkins.ubuntu.com/server/job/cloud-init-ci/193/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    FAILED: Ubuntu LTS: Build

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/193/rebuild

review: Needs Fixing (continuous-integration)
01ffbf2... by Chad Smith

mock find_fallback_nic to avoid udevadm calls leaking in tests

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:b1fffef776e344ee3a1b0842129f8ad260e4680c
https://jenkins.ubuntu.com/server/job/cloud-init-ci/194/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/194/rebuild

review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

some little things/food for thought.
and you have some conflicts.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:ffabafc1137f3aee1a1eb8d685742f7262333024
https://jenkins.ubuntu.com/server/job/cloud-init-ci/199/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/199/rebuild

review: Approve (continuous-integration)
8390979... by Chad Smith

avoid side-effects in ds.network_config. remove ubuntu scripts from get_data.

9235c59... by Chad Smith

create separate private _is_platform_viable function called by DataSourceAzure method

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:799d6b68a347ea21fd28530cc1d53b4d07fb128e
https://jenkins.ubuntu.com/server/job/cloud-init-ci/201/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    FAILED: MAAS Compatability Testing

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/201/rebuild

review: Needs Fixing (continuous-integration)
c27cec6... by Chad Smith

do not leak calls to systemd-detect-virt from unit tests. Also check if systemd-detect-virt exists before trying to run it

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:0182941850252bdf7d3c10466b43aed98e140f3f
https://jenkins.ubuntu.com/server/job/cloud-init-ci/202/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/202/rebuild

review: Approve (continuous-integration)
097df03... by Chad Smith

move DataSourceAzure.network_config logic to _parse_network_config

31145bc... by Chad Smith

since maybe_remove_ubuntu_network_config_scripts is moved to get_data, limit unit tests to only get_data calls, not network_config

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:a6e28e077d179d558bb48f135675d8845ece990e
https://jenkins.ubuntu.com/server/job/cloud-init-ci/204/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/204/rebuild

review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

there are some merge issues shown below ("<<<<<").

I have some comments inline.

How feasible would it be to add support for azure conversion in the net-convert sub-command?

3f4a384... by Chad Smith

Removing network scripts log is info instead of debug. Unit test fixes

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:bc725f13f319cb75defeb39928f280efc7626031
https://jenkins.ubuntu.com/server/job/cloud-init-ci/205/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/205/rebuild

review: Approve (continuous-integration)
1d429c3... by Chad Smith

make Azure's parse_network_config function public and call it from net-convert cli

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:1d429c3cb514b35b84efc40533f5935ec2abdf33
https://jenkins.ubuntu.com/server/job/cloud-init-ci/206/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/206/rebuild

review: Approve (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) wrote :

andy thanks for this update: couple style/flakes errors showed up
cloudinit/reporting/handlers.py:287:80: E501 line too long (83 > 79 characters)
tests/unittests/test_reporting_hyperv.py:8: 'tempfile' imported but unused

Unmerged commits

1d429c3... by Chad Smith

make Azure's parse_network_config function public and call it from net-convert cli

3f4a384... by Chad Smith

Removing network scripts log is info instead of debug. Unit test fixes

31145bc... by Chad Smith

since maybe_remove_ubuntu_network_config_scripts is moved to get_data, limit unit tests to only get_data calls, not network_config

097df03... by Chad Smith

move DataSourceAzure.network_config logic to _parse_network_config

c27cec6... by Chad Smith

do not leak calls to systemd-detect-virt from unit tests. Also check if systemd-detect-virt exists before trying to run it

9235c59... by Chad Smith

create separate private _is_platform_viable function called by DataSourceAzure method

8390979... by Chad Smith

avoid side-effects in ds.network_config. remove ubuntu scripts from get_data.

01ffbf2... by Chad Smith

mock find_fallback_nic to avoid udevadm calls leaking in tests

fb7381b... by Chad Smith

pyflakes

0e7a589... by Chad Smith

azure: azure to generate network configuration from IMDS on each boot

Azure datasource now queries IMDS metadata service for network
configuration at link local address
http://169.254.169.254/metadata/instance?api-version=2017-12-01. The
azure metadata service presents a list of macs and allocated ip addresses
associated with this instance. Azure will now also regenerate network
configuration on every boot because it subscribes to EventType.BOOT
maintenance events as well as the 'first boot'
EventType.BOOT_NEW_INSTANCE.

Also refactor _get_data into 3 discrete methods:
  - is_platform_viable: check quickly whether the datasource is
    potentially compatible with the platform on which is is running
  - crawl_metadata: walk all potential metadata candidates, returning a
    structured dict of all metadata and userdata. Raise InvalidMetaData on
    error.
  - _get_data: call crawl_metadata and process results or error. Cache
    instance data on class attributes: metadata, userdata_raw etc.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/cloudinit/cmd/devel/net_convert.py b/cloudinit/cmd/devel/net_convert.py
index 1ec08a3..271dc5e 100755
--- a/cloudinit/cmd/devel/net_convert.py
+++ b/cloudinit/cmd/devel/net_convert.py
@@ -8,6 +8,7 @@ import sys
8import yaml8import yaml
99
10from cloudinit.sources.helpers import openstack10from cloudinit.sources.helpers import openstack
11from cloudinit.sources import DataSourceAzure as azure
1112
12from cloudinit.net import eni, netplan, network_state, sysconfig13from cloudinit.net import eni, netplan, network_state, sysconfig
13from cloudinit import log14from cloudinit import log
@@ -28,7 +29,8 @@ def get_parser(parser=None):
28 parser.add_argument("-p", "--network-data", type=open,29 parser.add_argument("-p", "--network-data", type=open,
29 metavar="PATH", required=True)30 metavar="PATH", required=True)
30 parser.add_argument("-k", "--kind",31 parser.add_argument("-k", "--kind",
31 choices=['eni', 'network_data.json', 'yaml'],32 choices=['eni', 'network_data.json', 'yaml',
33 'azure-imds'],
32 required=True)34 required=True)
33 parser.add_argument("-d", "--directory",35 parser.add_argument("-d", "--directory",
34 metavar="PATH",36 metavar="PATH",
@@ -78,10 +80,13 @@ def handle_args(name, args):
78 ["Input YAML",80 ["Input YAML",
79 yaml.dump(pre_ns, default_flow_style=False, indent=4), ""]))81 yaml.dump(pre_ns, default_flow_style=False, indent=4), ""]))
80 ns = network_state.parse_net_config_data(pre_ns)82 ns = network_state.parse_net_config_data(pre_ns)
81 else:83 elif args.kind == 'network_data.json':
82 pre_ns = openstack.convert_net_json(84 pre_ns = openstack.convert_net_json(
83 json.loads(net_data), known_macs=known_macs)85 json.loads(net_data), known_macs=known_macs)
84 ns = network_state.parse_net_config_data(pre_ns)86 ns = network_state.parse_net_config_data(pre_ns)
87 elif args.kind == 'azure-imds':
88 pre_ns = azure.parse_network_config(json.loads(net_data))
89 ns = network_state.parse_net_config_data(pre_ns)
8590
86 if not ns:91 if not ns:
87 raise RuntimeError("No valid network_state object created from"92 raise RuntimeError("No valid network_state object created from"
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index 7007d9e..d8170c3 100644
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -8,6 +8,7 @@ import base64
8import contextlib8import contextlib
9import crypt9import crypt
10from functools import partial10from functools import partial
11import json
11import os12import os
12import os.path13import os.path
13import re14import re
@@ -17,6 +18,7 @@ import xml.etree.ElementTree as ET
1718
18from cloudinit import log as logging19from cloudinit import log as logging
19from cloudinit import net20from cloudinit import net
21from cloudinit.event import EventType
20from cloudinit.net.dhcp import EphemeralDHCPv422from cloudinit.net.dhcp import EphemeralDHCPv4
21from cloudinit import sources23from cloudinit import sources
22from cloudinit.sources.helpers.azure import get_metadata_from_fabric24from cloudinit.sources.helpers.azure import get_metadata_from_fabric
@@ -49,7 +51,17 @@ DEFAULT_FS = 'ext4'
49AZURE_CHASSIS_ASSET_TAG = '7783-7084-3265-9085-8269-3286-77'51AZURE_CHASSIS_ASSET_TAG = '7783-7084-3265-9085-8269-3286-77'
50REPROVISION_MARKER_FILE = "/var/lib/cloud/data/poll_imds"52REPROVISION_MARKER_FILE = "/var/lib/cloud/data/poll_imds"
51REPORTED_READY_MARKER_FILE = "/var/lib/cloud/data/reported_ready"53REPORTED_READY_MARKER_FILE = "/var/lib/cloud/data/reported_ready"
52IMDS_URL = "http://169.254.169.254/metadata/reprovisiondata"54AGENT_SEED_DIR = '/var/lib/waagent'
55IMDS_URL = "http://169.254.169.254/metadata/"
56
57# List of static scripts and network config artifacts created by
58# stock ubuntu suported images.
59UBUNTU_EXTENDED_NETWORK_SCRIPTS = [
60 '/etc/netplan/90-azure-hotplug.yaml',
61 '/usr/local/sbin/ephemeral_eth.sh',
62 '/etc/udev/rules.d/10-net-device-added.rules',
63 '/run/network/interfaces.ephemeral.d',
64]
5365
5466
55def find_storvscid_from_sysctl_pnpinfo(sysctl_out, deviceid):67def find_storvscid_from_sysctl_pnpinfo(sysctl_out, deviceid):
@@ -185,7 +197,7 @@ if util.is_FreeBSD():
185197
186BUILTIN_DS_CONFIG = {198BUILTIN_DS_CONFIG = {
187 'agent_command': AGENT_START_BUILTIN,199 'agent_command': AGENT_START_BUILTIN,
188 'data_dir': "/var/lib/waagent",200 'data_dir': AGENT_SEED_DIR,
189 'set_hostname': True,201 'set_hostname': True,
190 'hostname_bounce': {202 'hostname_bounce': {
191 'interface': DEFAULT_PRIMARY_NIC,203 'interface': DEFAULT_PRIMARY_NIC,
@@ -252,6 +264,10 @@ class DataSourceAzure(sources.DataSource):
252264
253 dsname = 'Azure'265 dsname = 'Azure'
254 _negotiated = False266 _negotiated = False
267 _metadata_imds = sources.UNSET
268
269 # Regenerate network config new_instance boot and every boot
270 update_events = {'network': [EventType.BOOT_NEW_INSTANCE, EventType.BOOT]}
255271
256 def __init__(self, sys_cfg, distro, paths):272 def __init__(self, sys_cfg, distro, paths):
257 sources.DataSource.__init__(self, sys_cfg, distro, paths)273 sources.DataSource.__init__(self, sys_cfg, distro, paths)
@@ -336,15 +352,17 @@ class DataSourceAzure(sources.DataSource):
336 metadata['public-keys'] = key_value or pubkeys_from_crt_files(fp_files)352 metadata['public-keys'] = key_value or pubkeys_from_crt_files(fp_files)
337 return metadata353 return metadata
338354
339 def _get_data(self):355 def crawl_metadata(self):
356 """Walk all instance metadata sources returning a dict on success.
357
358 @return: A dictionary of any metadata content for this instance.
359 @raise: InvalidMetaDataException when the expected metadata service is
360 unavailable, broken or disabled.
361 """
362 crawled_data = {}
340 # azure removes/ejects the cdrom containing the ovf-env.xml363 # azure removes/ejects the cdrom containing the ovf-env.xml
341 # file on reboot. So, in order to successfully reboot we364 # file on reboot. So, in order to successfully reboot we
342 # need to look in the datadir and consider that valid365 # need to look in the datadir and consider that valid
343 asset_tag = util.read_dmi_data('chassis-asset-tag')
344 if asset_tag != AZURE_CHASSIS_ASSET_TAG:
345 LOG.debug("Non-Azure DMI asset tag '%s' discovered.", asset_tag)
346 return False
347
348 ddir = self.ds_cfg['data_dir']366 ddir = self.ds_cfg['data_dir']
349367
350 candidates = [self.seed_dir]368 candidates = [self.seed_dir]
@@ -373,41 +391,84 @@ class DataSourceAzure(sources.DataSource):
373 except NonAzureDataSource:391 except NonAzureDataSource:
374 continue392 continue
375 except BrokenAzureDataSource as exc:393 except BrokenAzureDataSource as exc:
376 raise exc394 msg = 'BrokenAzureDataSource: %s' % exc
395 raise sources.InvalidMetaDataException(msg)
377 except util.MountFailedError:396 except util.MountFailedError:
378 LOG.warning("%s was not mountable", cdev)397 LOG.warning("%s was not mountable", cdev)
379 continue398 continue
380399
381 if reprovision or self._should_reprovision(ret):400 if reprovision or self._should_reprovision(ret):
382 ret = self._reprovision()401 ret = self._reprovision()
383 (md, self.userdata_raw, cfg, files) = ret402 imds_md = get_metadata_from_imds(
403 self.fallback_interface, retries=3)
404 (md, userdata_raw, cfg, files) = ret
384 self.seed = cdev405 self.seed = cdev
385 self.metadata = util.mergemanydict([md, DEFAULT_METADATA])406 crawled_data.update({
386 self.cfg = util.mergemanydict([cfg, BUILTIN_CLOUD_CONFIG])407 'cfg': cfg,
408 'files': files,
409 'metadata': util.mergemanydict(
410 [md, {'imds': imds_md}]),
411 'userdata_raw': userdata_raw})
387 found = cdev412 found = cdev
388413
389 LOG.debug("found datasource in %s", cdev)414 LOG.debug("found datasource in %s", cdev)
390 break415 break
391416
392 if not found:417 if not found:
393 return False418 raise sources.InvalidMetaDataException('No Azure metadata found')
394419
395 if found == ddir:420 if found == ddir:
396 LOG.debug("using files cached in %s", ddir)421 LOG.debug("using files cached in %s", ddir)
397422
398 seed = _get_random_seed()423 seed = _get_random_seed()
399 if seed:424 if seed:
400 self.metadata['random_seed'] = seed425 crawled_data['metadata']['random_seed'] = seed
426 crawled_data['metadata']['instance-id'] = util.read_dmi_data(
427 'system-uuid')
428 return crawled_data
429
430 def _is_platform_viable(self):
431 """Check platform environment to report if this datasource may run."""
432 return _is_platform_viable(self.seed_dir)
433
434 def clear_cached_attrs(self, attr_defaults=()):
435 """Reset any cached class attributes to defaults."""
436 super(DataSourceAzure, self).clear_cached_attrs(attr_defaults)
437 self._metadata_imds = sources.UNSET
438
439 def _get_data(self):
440 """Crawl and process datasource metadata caching metadata as attrs.
441
442 @return: True on success, False on error, invalid or disabled
443 datasource.
444 """
445 if not self._is_platform_viable():
446 return False
447 if self.distro and self.distro.name == 'ubuntu':
448 maybe_remove_ubuntu_network_config_scripts()
449 try:
450 crawled_data = util.log_time(
451 logfunc=LOG.debug, msg='Crawl of metadata service',
452 func=self.crawl_metadata)
453 except sources.InvalidMetaDataException as e:
454 LOG.warning('Could not crawl Azure metadata: %s', e)
455 return False
456
457 # Process crawled data and augment with various config defaults
458 self.cfg = util.mergemanydict(
459 [crawled_data['cfg'], BUILTIN_CLOUD_CONFIG])
460 self._metadata_imds = crawled_data['metadata']['imds']
461 self.metadata = util.mergemanydict(
462 [crawled_data['metadata'], DEFAULT_METADATA])
463 self.userdata_raw = crawled_data['userdata_raw']
401464
402 user_ds_cfg = util.get_cfg_by_path(self.cfg, DS_CFG_PATH, {})465 user_ds_cfg = util.get_cfg_by_path(self.cfg, DS_CFG_PATH, {})
403 self.ds_cfg = util.mergemanydict([user_ds_cfg, self.ds_cfg])466 self.ds_cfg = util.mergemanydict([user_ds_cfg, self.ds_cfg])
404467
405 # walinux agent writes files world readable, but expects468 # walinux agent writes files world readable, but expects
406 # the directory to be protected.469 # the directory to be protected.
407 write_files(ddir, files, dirmode=0o700)470 write_files(
408471 self.ds_cfg['data_dir'], crawled_data['files'], dirmode=0o700)
409 self.metadata['instance-id'] = util.read_dmi_data('system-uuid')
410
411 return True472 return True
412473
413 def device_name_to_device(self, name):474 def device_name_to_device(self, name):
@@ -436,7 +497,7 @@ class DataSourceAzure(sources.DataSource):
436 def _poll_imds(self):497 def _poll_imds(self):
437 """Poll IMDS for the new provisioning data until we get a valid498 """Poll IMDS for the new provisioning data until we get a valid
438 response. Then return the returned JSON object."""499 response. Then return the returned JSON object."""
439 url = IMDS_URL + "?api-version=2017-04-02"500 url = IMDS_URL + "reprovisiondata?api-version=2017-04-02"
440 headers = {"Metadata": "true"}501 headers = {"Metadata": "true"}
441 report_ready = bool(not os.path.isfile(REPORTED_READY_MARKER_FILE))502 report_ready = bool(not os.path.isfile(REPORTED_READY_MARKER_FILE))
442 LOG.debug("Start polling IMDS")503 LOG.debug("Start polling IMDS")
@@ -487,7 +548,7 @@ class DataSourceAzure(sources.DataSource):
487 jump back into the polling loop in order to retrieve the ovf_env."""548 jump back into the polling loop in order to retrieve the ovf_env."""
488 if not ret:549 if not ret:
489 return False550 return False
490 (_md, self.userdata_raw, cfg, _files) = ret551 (_md, _userdata_raw, cfg, _files) = ret
491 path = REPROVISION_MARKER_FILE552 path = REPROVISION_MARKER_FILE
492 if (cfg.get('PreprovisionedVm') is True or553 if (cfg.get('PreprovisionedVm') is True or
493 os.path.isfile(path)):554 os.path.isfile(path)):
@@ -543,22 +604,15 @@ class DataSourceAzure(sources.DataSource):
543 @property604 @property
544 def network_config(self):605 def network_config(self):
545 """Generate a network config like net.generate_fallback_network() with606 """Generate a network config like net.generate_fallback_network() with
546 the following execptions.607 the following exceptions.
547608
548 1. Probe the drivers of the net-devices present and inject them in609 1. Probe the drivers of the net-devices present and inject them in
549 the network configuration under params: driver: <driver> value610 the network configuration under params: driver: <driver> value
550 2. Generate a fallback network config that does not include any of611 2. Generate a fallback network config that does not include any of
551 the blacklisted devices.612 the blacklisted devices.
552 """613 """
553 blacklist = ['mlx4_core']
554 if not self._network_config:614 if not self._network_config:
555 LOG.debug('Azure: generating fallback configuration')615 self._network_config = parse_network_config(self._metadata_imds)
556 # generate a network config, blacklist picking any mlx4_core devs
557 netconfig = net.generate_fallback_config(
558 blacklist_drivers=blacklist, config_driver=True)
559
560 self._network_config = netconfig
561
562 return self._network_config616 return self._network_config
563617
564618
@@ -1025,6 +1079,162 @@ def load_azure_ds_dir(source_dir):
1025 return (md, ud, cfg, {'ovf-env.xml': contents})1079 return (md, ud, cfg, {'ovf-env.xml': contents})
10261080
10271081
1082def parse_network_config(imds_metadata):
1083 """Convert imds_metadata dictionary to network v2 configuration.
1084
1085 Parses network configuration from imds metadata if present or generate
1086 fallback network config excluding mlx4_core devices.
1087
1088 @param: imds_metadata: Dict of content read from IMDS network service.
1089 @return: Dictionary containing network version 2 standard configuration.
1090 """
1091 if imds_metadata != sources.UNSET and imds_metadata:
1092 netconfig = {'version': 2, 'ethernets': {}}
1093 LOG.debug('Azure: generating network configuration from IMDS')
1094 network_metadata = imds_metadata['network']
1095 for idx, intf in enumerate(network_metadata['interface']):
1096 nicname = 'eth{idx}'.format(idx=idx)
1097 dev_config = {}
1098 for addr4 in intf['ipv4']['ipAddress']:
1099 privateIpv4 = addr4['privateIpAddress']
1100 if privateIpv4:
1101 if dev_config.get('dhcp4', False):
1102 # Append static address config for nic > 1
1103 netPrefix = intf['ipv4']['subnet'][0].get(
1104 'prefix', '24')
1105 if not dev_config.get('addresses'):
1106 dev_config['addresses'] = []
1107 dev_config['addresses'].append(
1108 '{ip}/{prefix}'.format(
1109 ip=privateIpv4, prefix=netPrefix))
1110 else:
1111 dev_config['dhcp4'] = True
1112 for addr6 in intf['ipv6']['ipAddress']:
1113 privateIpv6 = addr6['privateIpAddress']
1114 if privateIpv6:
1115 dev_config['dhcp6'] = True
1116 break
1117 if dev_config:
1118 mac = ':'.join(re.findall(r'..', intf['macAddress']))
1119 dev_config.update(
1120 {'match': {'macaddress': mac.lower()},
1121 'set-name': nicname})
1122 netconfig['ethernets'][nicname] = dev_config
1123 else:
1124 blacklist = ['mlx4_core']
1125 LOG.debug('Azure: generating fallback configuration')
1126 # generate a network config, blacklist picking mlx4_core devs
1127 netconfig = net.generate_fallback_config(
1128 blacklist_drivers=blacklist, config_driver=True)
1129 return netconfig
1130
1131
1132def get_metadata_from_imds(fallback_nic, retries):
1133 """Query Azure's network metadata service, returning a dictionary.
1134
1135 If network is not up, setup ephemeral dhcp on fallback_nic to talk to the
1136 IMDS. For more info on IMDS:
1137 https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service
1138
1139 @param fallback_nic: String. The name of the nic which requires active
1140 networ in order to query IMDS.
1141 @param retries: The number of retries of the IMDS_URL.
1142
1143 @return: A dict of instance metadata containing compute and network
1144 info.
1145 """
1146 if net.is_up(fallback_nic):
1147 return util.log_time(
1148 logfunc=LOG.debug,
1149 msg='Crawl of Azure Instance Metadata Service (IMDS)',
1150 func=_get_metadata_from_imds, args=(retries,))
1151 else:
1152 with EphemeralDHCPv4(fallback_nic):
1153 return util.log_time(
1154 logfunc=LOG.debug,
1155 msg='Crawl of Azure Instance Metadata Service (IMDS)',
1156 func=_get_metadata_from_imds, args=(retries,))
1157
1158
1159def _get_metadata_from_imds(retries):
1160
1161 def retry_on_url_error(msg, exception):
1162 if isinstance(exception, UrlError) and exception.code == 404:
1163 return True # Continue retries
1164 return False # Stop retries on all other exceptions, including 404s
1165
1166 url = IMDS_URL + "instance?api-version=2017-12-01"
1167 headers = {"Metadata": "true"}
1168 try:
1169 response = readurl(
1170 url, timeout=1, headers=headers, retries=retries,
1171 exception_cb=retry_on_url_error)
1172 except Exception as e:
1173 LOG.debug('Ignoring IMDS instance metadata: %s', e)
1174 return {}
1175 try:
1176 return util.load_json(str(response))
1177 except json.decoder.JSONDecodeError:
1178 LOG.warning(
1179 'Ignoring non-json IMDS instance metadata: %s', str(response))
1180 return {}
1181
1182
1183def maybe_remove_ubuntu_network_config_scripts(paths=None):
1184 """Remove Azure-specific ubuntu network config for non-primary nics.
1185
1186 @param paths: List of networking scripts or directories to remove when
1187 present.
1188
1189 In certain supported ubuntu images, static udev rules or netplan yaml
1190 config is delivered in the base ubuntu image to support dhcp on any
1191 additional interfaces which get attached by a customer at some point
1192 after initial boot. Since the Azure datasource can now regenerate
1193 network configuration as metadata reports these new devices, we no longer
1194 want the udev rules or netplan's 90-azure-hotplug.yaml to configure
1195 networking on eth1 or greater as it might collide with cloud-init's
1196 configuration.
1197
1198 Remove the any existing extended network scripts if the datasource is
1199 enabled to write network per-boot.
1200 """
1201 if not paths:
1202 paths = UBUNTU_EXTENDED_NETWORK_SCRIPTS
1203 logged = False
1204 for path in paths:
1205 if os.path.exists(path):
1206 if not logged:
1207 LOG.info(
1208 'Removing Ubuntu extended network scripts because'
1209 ' cloud-init updates Azure network configuration on the'
1210 ' following event: %s.',
1211 EventType.BOOT)
1212 logged = True
1213 if os.path.isdir(path):
1214 util.del_dir(path)
1215 else:
1216 util.del_file(path)
1217
1218
1219def _is_platform_viable(seed_dir):
1220 """Check platform environment to report if this datasource may run."""
1221 asset_tag = util.read_dmi_data('chassis-asset-tag')
1222 if asset_tag == AZURE_CHASSIS_ASSET_TAG:
1223 return True
1224 LOG.debug("Non-Azure DMI asset tag '%s' discovered.", asset_tag)
1225 for path in (AGENT_SEED_DIR, seed_dir):
1226 if os.path.exists(os.path.join(path, 'ovf-env.xml')):
1227 return True
1228 if util.which('systemd-detect-virt'):
1229 (virt_type, _err) = util.subp(
1230 ['systemd-detect-virt'], rcs=[0, 1], capture=True)
1231 if virt_type.strip() == 'microsoft':
1232 return True
1233 if util.find_devs_with(criteria='LABEL=rd_rdfe_*'):
1234 return True
1235 return False
1236
1237
1028class BrokenAzureDataSource(Exception):1238class BrokenAzureDataSource(Exception):
1029 pass1239 pass
10301240
diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py
index c50750a..06e613f 100644
--- a/cloudinit/sources/__init__.py
+++ b/cloudinit/sources/__init__.py
@@ -103,7 +103,6 @@ class DataSource(object):
103 url_timeout = 10 # timeout for each metadata url read attempt103 url_timeout = 10 # timeout for each metadata url read attempt
104 url_retries = 5 # number of times to retry url upon 404104 url_retries = 5 # number of times to retry url upon 404
105105
106<<<<<<< cloudinit/sources/__init__.py
107 # The datasource defines a set of supported EventTypes during which106 # The datasource defines a set of supported EventTypes during which
108 # the datasource can react to changes in metadata and regenerate107 # the datasource can react to changes in metadata and regenerate
109 # network configuration on metadata changes.108 # network configuration on metadata changes.
@@ -112,16 +111,6 @@ class DataSource(object):
112111
113 # Default: generate network config on new instance id (first boot).112 # Default: generate network config on new instance id (first boot).
114 update_events = {'network': set([EventType.BOOT_NEW_INSTANCE])}113 update_events = {'network': set([EventType.BOOT_NEW_INSTANCE])}
115=======
116 # The datasource defines a list of supported EventTypes during which
117 # the datasource can react to changes in metadata and regenerate
118 # network configuration on metadata changes.
119 # A datasource which supports writing network config on each system boot
120 # would set update_events = {'network': [EventType.BOOT]}
121
122 # Default: generate network config on new instance id (first boot).
123 update_events = {'network': [EventType.BOOT_NEW_INSTANCE]}
124>>>>>>> cloudinit/sources/__init__.py
125114
126 # N-tuple listing default values for any metadata-related class115 # N-tuple listing default values for any metadata-related class
127 # attributes cached on an instance by a process_data runs. These attribute116 # attributes cached on an instance by a process_data runs. These attribute
@@ -486,13 +475,8 @@ class DataSource(object):
486 for update_scope, update_events in self.update_events.items():475 for update_scope, update_events in self.update_events.items():
487 if event in update_events:476 if event in update_events:
488 if not supported_events.get(update_scope):477 if not supported_events.get(update_scope):
489<<<<<<< cloudinit/sources/__init__.py
490 supported_events[update_scope] = set()478 supported_events[update_scope] = set()
491 supported_events[update_scope].add(event)479 supported_events[update_scope].add(event)
492=======
493 supported_events[update_scope] = []
494 supported_events[update_scope].append(event)
495>>>>>>> cloudinit/sources/__init__.py
496 for scope, matched_events in supported_events.items():480 for scope, matched_events in supported_events.items():
497 LOG.debug(481 LOG.debug(
498 "Update datasource metadata and %s config due to events: %s",482 "Update datasource metadata and %s config due to events: %s",
@@ -506,11 +490,8 @@ class DataSource(object):
506 result = self.get_data()490 result = self.get_data()
507 if result:491 if result:
508 return True492 return True
509<<<<<<< cloudinit/sources/__init__.py
510 LOG.debug("Datasource %s not updated for events: %s", self,493 LOG.debug("Datasource %s not updated for events: %s", self,
511 ', '.join(source_event_types))494 ', '.join(source_event_types))
512=======
513>>>>>>> cloudinit/sources/__init__.py
514 return False495 return False
515496
516 def check_instance_id(self, sys_cfg):497 def check_instance_id(self, sys_cfg):
diff --git a/cloudinit/sources/tests/test_init.py b/cloudinit/sources/tests/test_init.py
index 30573cc..9e939c1 100644
--- a/cloudinit/sources/tests/test_init.py
+++ b/cloudinit/sources/tests/test_init.py
@@ -429,14 +429,9 @@ class TestDataSource(CiTestCase):
429429
430 def test_update_metadata_only_acts_on_supported_update_events(self):430 def test_update_metadata_only_acts_on_supported_update_events(self):
431 """update_metadata won't get_data on unsupported update events."""431 """update_metadata won't get_data on unsupported update events."""
432<<<<<<< cloudinit/sources/tests/test_init.py
433 self.datasource.update_events['network'].discard(EventType.BOOT)432 self.datasource.update_events['network'].discard(EventType.BOOT)
434 self.assertEqual(433 self.assertEqual(
435 {'network': set([EventType.BOOT_NEW_INSTANCE])},434 {'network': set([EventType.BOOT_NEW_INSTANCE])},
436=======
437 self.assertEqual(
438 {'network': [EventType.BOOT_NEW_INSTANCE]},
439>>>>>>> cloudinit/sources/tests/test_init.py
440 self.datasource.update_events)435 self.datasource.update_events)
441436
442 def fake_get_data():437 def fake_get_data():
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
index e82716e..5eae247 100644
--- a/tests/unittests/test_datasource/test_azure.py
+++ b/tests/unittests/test_datasource/test_azure.py
@@ -1,15 +1,21 @@
1# This file is part of cloud-init. See LICENSE file for license information.1# This file is part of cloud-init. See LICENSE file for license information.
22
3from cloudinit import distros
3from cloudinit import helpers4from cloudinit import helpers
4from cloudinit.sources import DataSourceAzure as dsaz5from cloudinit import url_helper
6from cloudinit.sources import (
7 UNSET, DataSourceAzure as dsaz, InvalidMetaDataException)
5from cloudinit.util import (b64e, decode_binary, load_file, write_file,8from cloudinit.util import (b64e, decode_binary, load_file, write_file,
6 find_freebsd_part, get_path_dev_freebsd,9 find_freebsd_part, get_path_dev_freebsd,
7 MountFailedError)10 MountFailedError)
8from cloudinit.version import version_string as vs11from cloudinit.version import version_string as vs
9from cloudinit.tests.helpers import (CiTestCase, TestCase, populate_dir, mock,12from cloudinit.tests.helpers import (
10 ExitStack, PY26, SkipTest)13 HttprettyTestCase, CiTestCase, populate_dir, mock, wrap_and_call,
14 ExitStack, PY26, SkipTest)
1115
12import crypt16import crypt
17import httpretty
18import json
13import os19import os
14import stat20import stat
15import xml.etree.ElementTree as ET21import xml.etree.ElementTree as ET
@@ -77,6 +83,106 @@ def construct_valid_ovf_env(data=None, pubkeys=None,
77 return content83 return content
7884
7985
86NETWORK_METADATA = {
87 "network": {
88 "interface": [
89 {
90 "macAddress": "000D3A047598",
91 "ipv6": {
92 "ipAddress": []
93 },
94 "ipv4": {
95 "subnet": [
96 {
97 "prefix": "24",
98 "address": "10.0.0.0"
99 }
100 ],
101 "ipAddress": [
102 {
103 "privateIpAddress": "10.0.0.4",
104 "publicIpAddress": "104.46.124.81"
105 }
106 ]
107 }
108 }
109 ]
110 }
111}
112
113
114class TestGetMetadataFromIMDS(HttprettyTestCase):
115
116 with_logs = True
117
118 def setUp(self):
119 super(TestGetMetadataFromIMDS, self).setUp()
120 self.network_md_url = dsaz.IMDS_URL + "instance?api-version=2017-12-01"
121
122 @mock.patch('cloudinit.sources.DataSourceAzure.readurl')
123 @mock.patch('cloudinit.sources.DataSourceAzure.EphemeralDHCPv4')
124 @mock.patch('cloudinit.sources.DataSourceAzure.net.is_up')
125 def test_get_metadata_does_not_dhcp_if_network_is_up(
126 self, m_net_is_up, m_dhcp, m_readurl):
127 """Do not perform DHCP setup when nic is already up."""
128 m_net_is_up.return_value = True
129 m_readurl.return_value = url_helper.StringResponse(
130 json.dumps(NETWORK_METADATA).encode('utf-8'))
131 self.assertEqual(
132 NETWORK_METADATA,
133 dsaz.get_metadata_from_imds('eth9', retries=3))
134
135 m_net_is_up.assert_called_with('eth9')
136 m_dhcp.assert_not_called()
137 self.assertIn(
138 "Crawl of Azure Instance Metadata Service (IMDS) took", # log_time
139 self.logs.getvalue())
140
141 @mock.patch('cloudinit.sources.DataSourceAzure.readurl')
142 @mock.patch('cloudinit.sources.DataSourceAzure.EphemeralDHCPv4')
143 @mock.patch('cloudinit.sources.DataSourceAzure.net.is_up')
144 def test_get_metadata_performs_dhcp_when_network_is_down(
145 self, m_net_is_up, m_dhcp, m_readurl):
146 """Perform DHCP setup when nic is not up."""
147 m_net_is_up.return_value = False
148 m_readurl.return_value = url_helper.StringResponse(
149 json.dumps(NETWORK_METADATA).encode('utf-8'))
150
151 self.assertEqual(
152 NETWORK_METADATA,
153 dsaz.get_metadata_from_imds('eth9', retries=2))
154
155 m_net_is_up.assert_called_with('eth9')
156 m_dhcp.assert_called_with('eth9')
157 self.assertIn(
158 "Crawl of Azure Instance Metadata Service (IMDS) took", # log_time
159 self.logs.getvalue())
160
161 m_readurl.assert_called_with(
162 self.network_md_url, exception_cb=mock.ANY,
163 headers={'Metadata': 'true'}, retries=2, timeout=1)
164
165 @mock.patch('cloudinit.url_helper.time.sleep')
166 @mock.patch('cloudinit.sources.DataSourceAzure.net.is_up')
167 def test_get_metadata_from_imds_empty_when_no_imds_present(
168 self, m_net_is_up, m_sleep):
169 """Return empty dict when IMDS network metadata is absent."""
170 httpretty.register_uri(
171 httpretty.GET,
172 dsaz.IMDS_URL + 'instance?api-version=2017-12-01',
173 body={}, status=404)
174
175 m_net_is_up.return_value = True # skips dhcp
176
177 self.assertEqual({}, dsaz.get_metadata_from_imds('eth9', retries=2))
178
179 m_net_is_up.assert_called_with('eth9')
180 self.assertEqual([mock.call(1), mock.call(1)], m_sleep.call_args_list)
181 self.assertIn(
182 "Crawl of Azure Instance Metadata Service (IMDS) took", # log_time
183 self.logs.getvalue())
184
185
80class TestAzureDataSource(CiTestCase):186class TestAzureDataSource(CiTestCase):
81187
82 with_logs = True188 with_logs = True
@@ -95,8 +201,19 @@ class TestAzureDataSource(CiTestCase):
95 self.patches = ExitStack()201 self.patches = ExitStack()
96 self.addCleanup(self.patches.close)202 self.addCleanup(self.patches.close)
97203
98 self.patches.enter_context(mock.patch.object(dsaz, '_get_random_seed'))204 self.patches.enter_context(mock.patch.object(
99205 dsaz, '_get_random_seed', return_value='wild'))
206 self.m_get_metadata_from_imds = self.patches.enter_context(
207 mock.patch.object(
208 dsaz, 'get_metadata_from_imds',
209 mock.MagicMock(return_value=NETWORK_METADATA)))
210 self.m_fallback_nic = self.patches.enter_context(
211 mock.patch('cloudinit.sources.net.find_fallback_nic',
212 return_value='eth9'))
213 self.m_remove_ubuntu_network_scripts = self.patches.enter_context(
214 mock.patch.object(
215 dsaz, 'maybe_remove_ubuntu_network_config_scripts',
216 mock.MagicMock()))
100 super(TestAzureDataSource, self).setUp()217 super(TestAzureDataSource, self).setUp()
101218
102 def apply_patches(self, patches):219 def apply_patches(self, patches):
@@ -137,7 +254,7 @@ scbus-1 on xpt0 bus 0
137 ])254 ])
138 return dsaz255 return dsaz
139256
140 def _get_ds(self, data, agent_command=None):257 def _get_ds(self, data, agent_command=None, distro=None):
141258
142 def dsdevs():259 def dsdevs():
143 return data.get('dsdevs', [])260 return data.get('dsdevs', [])
@@ -186,8 +303,11 @@ scbus-1 on xpt0 bus 0
186 side_effect=_wait_for_files)),303 side_effect=_wait_for_files)),
187 ])304 ])
188305
306 if distro is not None:
307 distro_cls = distros.fetch(distro)
308 distro = distro_cls(distro, data.get('sys_cfg', {}), self.paths)
189 dsrc = dsaz.DataSourceAzure(309 dsrc = dsaz.DataSourceAzure(
190 data.get('sys_cfg', {}), distro=None, paths=self.paths)310 data.get('sys_cfg', {}), distro=distro, paths=self.paths)
191 if agent_command is not None:311 if agent_command is not None:
192 dsrc.ds_cfg['agent_command'] = agent_command312 dsrc.ds_cfg['agent_command'] = agent_command
193313
@@ -260,29 +380,20 @@ fdescfs /dev/fd fdescfs rw 0 0
260 res = get_path_dev_freebsd('/etc', mnt_list)380 res = get_path_dev_freebsd('/etc', mnt_list)
261 self.assertIsNotNone(res)381 self.assertIsNotNone(res)
262382
263 @mock.patch('cloudinit.sources.DataSourceAzure.util.read_dmi_data')383 @mock.patch('cloudinit.sources.DataSourceAzure._is_platform_viable')
264 def test_non_azure_dmi_chassis_asset_tag(self, m_read_dmi_data):384 def test_call_is_platform_viable_seed(self, m_is_platform_viable):
265 """Report non-azure when DMI's chassis asset tag doesn't match.385 """Check seed_dir using _is_platform_viable and return False."""
266
267 Return False when the asset tag doesn't match Azure's static
268 AZURE_CHASSIS_ASSET_TAG.
269 """
270 # Return a non-matching asset tag value386 # Return a non-matching asset tag value
271 nonazure_tag = dsaz.AZURE_CHASSIS_ASSET_TAG + 'X'387 m_is_platform_viable.return_value = False
272 m_read_dmi_data.return_value = nonazure_tag
273 dsrc = dsaz.DataSourceAzure(388 dsrc = dsaz.DataSourceAzure(
274 {}, distro=None, paths=self.paths)389 {}, distro=None, paths=self.paths)
275 self.assertFalse(dsrc.get_data())390 self.assertFalse(dsrc.get_data())
276 self.assertEqual(391 m_is_platform_viable.assert_called_with(dsrc.seed_dir)
277 "DEBUG: Non-Azure DMI asset tag '{0}' discovered.\n".format(
278 nonazure_tag),
279 self.logs.getvalue())
280392
281 def test_basic_seed_dir(self):393 def test_basic_seed_dir(self):
282 odata = {'HostName': "myhost", 'UserName': "myuser"}394 odata = {'HostName': "myhost", 'UserName': "myuser"}
283 data = {'ovfcontent': construct_valid_ovf_env(data=odata),395 data = {'ovfcontent': construct_valid_ovf_env(data=odata),
284 'sys_cfg': {}}396 'sys_cfg': {}}
285
286 dsrc = self._get_ds(data)397 dsrc = self._get_ds(data)
287 ret = dsrc.get_data()398 ret = dsrc.get_data()
288 self.assertTrue(ret)399 self.assertTrue(ret)
@@ -291,6 +402,82 @@ fdescfs /dev/fd fdescfs rw 0 0
291 self.assertTrue(os.path.isfile(402 self.assertTrue(os.path.isfile(
292 os.path.join(self.waagent_d, 'ovf-env.xml')))403 os.path.join(self.waagent_d, 'ovf-env.xml')))
293404
405 def test_get_data_non_ubuntu_will_not_remove_network_scripts(self):
406 """get_data on non-Ubuntu will not remove ubuntu net scripts."""
407 odata = {'HostName': "myhost", 'UserName': "myuser"}
408 data = {'ovfcontent': construct_valid_ovf_env(data=odata),
409 'sys_cfg': {}}
410
411 dsrc = self._get_ds(data, distro='debian')
412 dsrc.get_data()
413 self.m_remove_ubuntu_network_scripts.assert_not_called()
414
415 def test_get_data_on_ubuntu_will_remove_network_scripts(self):
416 """get_data will remove ubuntu net scripts on Ubuntu distro."""
417 odata = {'HostName': "myhost", 'UserName': "myuser"}
418 data = {'ovfcontent': construct_valid_ovf_env(data=odata),
419 'sys_cfg': {}}
420
421 dsrc = self._get_ds(data, distro='ubuntu')
422 dsrc.get_data()
423 self.m_remove_ubuntu_network_scripts.assert_called_once_with()
424
425 def test_crawl_metadata_returns_structured_data_and_caches_nothing(self):
426 """Return all structured metadata and cache no class attributes."""
427 yaml_cfg = "{agent_command: my_command}\n"
428 odata = {'HostName': "myhost", 'UserName': "myuser",
429 'UserData': {'text': 'FOOBAR', 'encoding': 'plain'},
430 'dscfg': {'text': yaml_cfg, 'encoding': 'plain'}}
431 data = {'ovfcontent': construct_valid_ovf_env(data=odata),
432 'sys_cfg': {}}
433 dsrc = self._get_ds(data)
434 expected_cfg = {
435 'PreprovisionedVm': False,
436 'datasource': {'Azure': {'agent_command': 'my_command'}},
437 'system_info': {'default_user': {'name': u'myuser'}}}
438 expected_metadata = {
439 'azure_data': {
440 'configurationsettype': 'LinuxProvisioningConfiguration'},
441 'imds': {'network': {'interface': [{
442 'ipv4': {'ipAddress': [
443 {'privateIpAddress': '10.0.0.4',
444 'publicIpAddress': '104.46.124.81'}],
445 'subnet': [{'address': '10.0.0.0', 'prefix': '24'}]},
446 'ipv6': {'ipAddress': []},
447 'macAddress': '000D3A047598'}]}},
448 'instance-id': 'test-instance-id',
449 'local-hostname': u'myhost',
450 'random_seed': 'wild'}
451
452 crawled_metadata = dsrc.crawl_metadata()
453
454 self.assertItemsEqual(
455 crawled_metadata.keys(),
456 ['cfg', 'files', 'metadata', 'userdata_raw'])
457 self.assertEqual(crawled_metadata['cfg'], expected_cfg)
458 self.assertEqual(
459 list(crawled_metadata['files'].keys()), ['ovf-env.xml'])
460 self.assertIn(
461 b'<HostName>myhost</HostName>',
462 crawled_metadata['files']['ovf-env.xml'])
463 self.assertEqual(crawled_metadata['metadata'], expected_metadata)
464 self.assertEqual(crawled_metadata['userdata_raw'], 'FOOBAR')
465 self.assertEqual(dsrc.userdata_raw, None)
466 self.assertEqual(dsrc.metadata, {})
467 self.assertEqual(dsrc._metadata_imds, UNSET)
468 self.assertFalse(os.path.isfile(
469 os.path.join(self.waagent_d, 'ovf-env.xml')))
470
471 def test_crawl_metadata_raises_invalid_metadata_on_error(self):
472 """crawl_metadata raises an exception on invalid ovf-env.xml."""
473 data = {'ovfcontent': "BOGUS", 'sys_cfg': {}}
474 dsrc = self._get_ds(data)
475 error_msg = ('BrokenAzureDataSource: Invalid ovf-env.xml:'
476 ' syntax error: line 1, column 0')
477 with self.assertRaises(InvalidMetaDataException) as cm:
478 dsrc.crawl_metadata()
479 self.assertEqual(str(cm.exception), error_msg)
480
294 def test_waagent_d_has_0700_perms(self):481 def test_waagent_d_has_0700_perms(self):
295 # we expect /var/lib/waagent to be created 0700482 # we expect /var/lib/waagent to be created 0700
296 dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()})483 dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
@@ -314,6 +501,20 @@ fdescfs /dev/fd fdescfs rw 0 0
314 self.assertTrue(ret)501 self.assertTrue(ret)
315 self.assertEqual(data['agent_invoked'], cfg['agent_command'])502 self.assertEqual(data['agent_invoked'], cfg['agent_command'])
316503
504 def test_network_config_set_from_imds(self):
505 """Datasource.network_config returns IMDS network data."""
506 odata = {}
507 data = {'ovfcontent': construct_valid_ovf_env(data=odata)}
508 expected_network_config = {
509 'ethernets': {
510 'eth0': {'set-name': 'eth0',
511 'match': {'macaddress': '00:0d:3a:04:75:98'},
512 'dhcp4': True}},
513 'version': 2}
514 dsrc = self._get_ds(data)
515 dsrc.get_data()
516 self.assertEqual(expected_network_config, dsrc.network_config)
517
317 def test_user_cfg_set_agent_command(self):518 def test_user_cfg_set_agent_command(self):
318 # set dscfg in via base64 encoded yaml519 # set dscfg in via base64 encoded yaml
319 cfg = {'agent_command': "my_command"}520 cfg = {'agent_command': "my_command"}
@@ -579,12 +780,34 @@ fdescfs /dev/fd fdescfs rw 0 0
579 self.assertEqual(780 self.assertEqual(
580 [mock.call("/dev/cd0")], m_check_fbsd_cdrom.call_args_list)781 [mock.call("/dev/cd0")], m_check_fbsd_cdrom.call_args_list)
581782
783 @mock.patch('cloudinit.net.generate_fallback_config')
784 def test_imds_network_config(self, mock_fallback):
785 """Network config is generated from IMDS network data when present."""
786 odata = {'HostName': "myhost", 'UserName': "myuser"}
787 data = {'ovfcontent': construct_valid_ovf_env(data=odata),
788 'sys_cfg': {}}
789
790 dsrc = self._get_ds(data)
791 ret = dsrc.get_data()
792 self.assertTrue(ret)
793
794 expected_cfg = {
795 'ethernets': {
796 'eth0': {'dhcp4': True,
797 'match': {'macaddress': '00:0d:3a:04:75:98'},
798 'set-name': 'eth0'}},
799 'version': 2}
800
801 self.assertEqual(expected_cfg, dsrc.network_config)
802 mock_fallback.assert_not_called()
803
582 @mock.patch('cloudinit.net.get_interface_mac')804 @mock.patch('cloudinit.net.get_interface_mac')
583 @mock.patch('cloudinit.net.get_devicelist')805 @mock.patch('cloudinit.net.get_devicelist')
584 @mock.patch('cloudinit.net.device_driver')806 @mock.patch('cloudinit.net.device_driver')
585 @mock.patch('cloudinit.net.generate_fallback_config')807 @mock.patch('cloudinit.net.generate_fallback_config')
586 def test_network_config(self, mock_fallback, mock_dd,808 def test_fallback_network_config(self, mock_fallback, mock_dd,
587 mock_devlist, mock_get_mac):809 mock_devlist, mock_get_mac):
810 """On absent IMDS network data, generate network fallback config."""
588 odata = {'HostName': "myhost", 'UserName': "myuser"}811 odata = {'HostName': "myhost", 'UserName': "myuser"}
589 data = {'ovfcontent': construct_valid_ovf_env(data=odata),812 data = {'ovfcontent': construct_valid_ovf_env(data=odata),
590 'sys_cfg': {}}813 'sys_cfg': {}}
@@ -605,6 +828,8 @@ fdescfs /dev/fd fdescfs rw 0 0
605 mock_get_mac.return_value = '00:11:22:33:44:55'828 mock_get_mac.return_value = '00:11:22:33:44:55'
606829
607 dsrc = self._get_ds(data)830 dsrc = self._get_ds(data)
831 # Represent empty response from network imds
832 self.m_get_metadata_from_imds.return_value = {}
608 ret = dsrc.get_data()833 ret = dsrc.get_data()
609 self.assertTrue(ret)834 self.assertTrue(ret)
610835
@@ -617,8 +842,9 @@ fdescfs /dev/fd fdescfs rw 0 0
617 @mock.patch('cloudinit.net.get_devicelist')842 @mock.patch('cloudinit.net.get_devicelist')
618 @mock.patch('cloudinit.net.device_driver')843 @mock.patch('cloudinit.net.device_driver')
619 @mock.patch('cloudinit.net.generate_fallback_config')844 @mock.patch('cloudinit.net.generate_fallback_config')
620 def test_network_config_blacklist(self, mock_fallback, mock_dd,845 def test_fallback_network_config_blacklist(self, mock_fallback, mock_dd,
621 mock_devlist, mock_get_mac):846 mock_devlist, mock_get_mac):
847 """On absent network metadata, blacklist mlx from fallback config."""
622 odata = {'HostName': "myhost", 'UserName': "myuser"}848 odata = {'HostName': "myhost", 'UserName': "myuser"}
623 data = {'ovfcontent': construct_valid_ovf_env(data=odata),849 data = {'ovfcontent': construct_valid_ovf_env(data=odata),
624 'sys_cfg': {}}850 'sys_cfg': {}}
@@ -649,6 +875,8 @@ fdescfs /dev/fd fdescfs rw 0 0
649 mock_get_mac.return_value = '00:11:22:33:44:55'875 mock_get_mac.return_value = '00:11:22:33:44:55'
650876
651 dsrc = self._get_ds(data)877 dsrc = self._get_ds(data)
878 # Represent empty response from network imds
879 self.m_get_metadata_from_imds.return_value = {}
652 ret = dsrc.get_data()880 ret = dsrc.get_data()
653 self.assertTrue(ret)881 self.assertTrue(ret)
654882
@@ -689,9 +917,12 @@ class TestAzureBounce(CiTestCase):
689 mock.patch.object(dsaz, 'get_metadata_from_fabric',917 mock.patch.object(dsaz, 'get_metadata_from_fabric',
690 mock.MagicMock(return_value={})))918 mock.MagicMock(return_value={})))
691 self.patches.enter_context(919 self.patches.enter_context(
692 mock.patch.object(dsaz.util, 'which', lambda x: True))920 mock.patch.object(dsaz, 'get_metadata_from_imds',
921 mock.MagicMock(return_value={})))
693 self.patches.enter_context(922 self.patches.enter_context(
694 mock.patch.object(dsaz, '_get_random_seed'))923 mock.patch.object(dsaz.util, 'which', lambda x: True))
924 self.patches.enter_context(mock.patch.object(
925 dsaz, '_get_random_seed', return_value='wild'))
695926
696 def _dmi_mocks(key):927 def _dmi_mocks(key):
697 if key == 'system-uuid':928 if key == 'system-uuid':
@@ -719,9 +950,12 @@ class TestAzureBounce(CiTestCase):
719 mock.patch.object(dsaz, 'set_hostname'))950 mock.patch.object(dsaz, 'set_hostname'))
720 self.subp = self.patches.enter_context(951 self.subp = self.patches.enter_context(
721 mock.patch('cloudinit.sources.DataSourceAzure.util.subp'))952 mock.patch('cloudinit.sources.DataSourceAzure.util.subp'))
953 self.find_fallback_nic = self.patches.enter_context(
954 mock.patch('cloudinit.net.find_fallback_nic', return_value='eth9'))
722955
723 def tearDown(self):956 def tearDown(self):
724 self.patches.close()957 self.patches.close()
958 super(TestAzureBounce, self).tearDown()
725959
726 def _get_ds(self, ovfcontent=None, agent_command=None):960 def _get_ds(self, ovfcontent=None, agent_command=None):
727 if ovfcontent is not None:961 if ovfcontent is not None:
@@ -927,7 +1161,7 @@ class TestLoadAzureDsDir(CiTestCase):
927 str(context_manager.exception))1161 str(context_manager.exception))
9281162
9291163
930class TestReadAzureOvf(TestCase):1164class TestReadAzureOvf(CiTestCase):
9311165
932 def test_invalid_xml_raises_non_azure_ds(self):1166 def test_invalid_xml_raises_non_azure_ds(self):
933 invalid_xml = "<foo>" + construct_valid_ovf_env(data={})1167 invalid_xml = "<foo>" + construct_valid_ovf_env(data={})
@@ -1188,6 +1422,25 @@ class TestCanDevBeReformatted(CiTestCase):
1188 "(datasource.Azure.never_destroy_ntfs)", msg)1422 "(datasource.Azure.never_destroy_ntfs)", msg)
11891423
11901424
1425class TestClearCachedData(CiTestCase):
1426
1427 def test_clear_cached_attrs_clears_imds(self):
1428 """All class attributes are reset to defaults, including imds data."""
1429 tmp = self.tmp_dir()
1430 paths = helpers.Paths(
1431 {'cloud_dir': tmp, 'run_dir': tmp})
1432 dsrc = dsaz.DataSourceAzure({}, distro=None, paths=paths)
1433 clean_values = [dsrc.metadata, dsrc.userdata, dsrc._metadata_imds]
1434 dsrc.metadata = 'md'
1435 dsrc.userdata = 'ud'
1436 dsrc._metadata_imds = 'imds'
1437 dsrc._dirty_cache = True
1438 dsrc.clear_cached_attrs()
1439 self.assertEqual(
1440 [dsrc.metadata, dsrc.userdata, dsrc._metadata_imds],
1441 clean_values)
1442
1443
1191class TestAzureNetExists(CiTestCase):1444class TestAzureNetExists(CiTestCase):
11921445
1193 def test_azure_net_must_exist_for_legacy_objpkl(self):1446 def test_azure_net_must_exist_for_legacy_objpkl(self):
@@ -1398,4 +1651,128 @@ class TestAzureDataSourcePreprovisioning(CiTestCase):
1398 self.assertEqual(m_net.call_count, 1)1651 self.assertEqual(m_net.call_count, 1)
13991652
14001653
1654class TestRemoveUbuntuNetworkConfigScripts(CiTestCase):
1655
1656 with_logs = True
1657
1658 def setUp(self):
1659 super(TestRemoveUbuntuNetworkConfigScripts, self).setUp()
1660 self.tmp = self.tmp_dir()
1661
1662 def test_remove_network_scripts_removes_both_files_and_directories(self):
1663 """Any files or directories in paths are removed when present."""
1664 file1 = self.tmp_path('file1', dir=self.tmp)
1665 subdir = self.tmp_path('sub1', dir=self.tmp)
1666 subfile = self.tmp_path('leaf1', dir=subdir)
1667 write_file(file1, 'file1content')
1668 write_file(subfile, 'leafcontent')
1669 dsaz.maybe_remove_ubuntu_network_config_scripts(paths=[subdir, file1])
1670
1671 for path in (file1, subdir, subfile):
1672 self.assertFalse(os.path.exists(path),
1673 'Found unremoved: %s' % path)
1674
1675 expected_logs = [
1676 'INFO: Removing Ubuntu extended network scripts because cloud-init'
1677 ' updates Azure network configuration on the following event:'
1678 ' System boot.',
1679 'Recursively deleting %s' % subdir,
1680 'Attempting to remove %s' % file1]
1681 for log in expected_logs:
1682 self.assertIn(log, self.logs.getvalue())
1683
1684 def test_remove_network_scripts_only_attempts_removal_if_path_exists(self):
1685 """Any files or directories absent are skipped without error."""
1686 dsaz.maybe_remove_ubuntu_network_config_scripts(paths=[
1687 self.tmp_path('nodirhere/', dir=self.tmp),
1688 self.tmp_path('notfilehere', dir=self.tmp)])
1689 self.assertNotIn('/not/a', self.logs.getvalue()) # No delete logs
1690
1691 @mock.patch('cloudinit.sources.DataSourceAzure.os.path.exists')
1692 def test_remove_network_scripts_default_removes_stock_scripts(self,
1693 m_exists):
1694 """Azure's stock ubuntu image scripts and artifacts are removed."""
1695 # Report path absent on all to avoid delete operation
1696 m_exists.return_value = False
1697 dsaz.maybe_remove_ubuntu_network_config_scripts()
1698 calls = m_exists.call_args_list
1699 for path in dsaz.UBUNTU_EXTENDED_NETWORK_SCRIPTS:
1700 self.assertIn(mock.call(path), calls)
1701
1702
1703class TestWBIsPlatformViable(CiTestCase):
1704 """White box tests for _is_platform_viable."""
1705 with_logs = True
1706
1707 @mock.patch('cloudinit.sources.DataSourceAzure.util.read_dmi_data')
1708 def test_true_on_non_azure_chassis(self, m_read_dmi_data):
1709 """Return True if DMI chassis-asset-tag is AZURE_CHASSIS_ASSET_TAG."""
1710 m_read_dmi_data.return_value = dsaz.AZURE_CHASSIS_ASSET_TAG
1711 self.assertTrue(dsaz._is_platform_viable('doesnotmatter'))
1712
1713 @mock.patch('cloudinit.sources.DataSourceAzure.os.path.exists')
1714 @mock.patch('cloudinit.sources.DataSourceAzure.util.read_dmi_data')
1715 def test_true_on_azure_ovf_env_in_seed_dir(self, m_read_dmi_data, m_exist):
1716 """Return True if ovf-env.xml exists in known seed dirs."""
1717 # Non-matching Azure chassis-asset-tag
1718 m_read_dmi_data.return_value = dsaz.AZURE_CHASSIS_ASSET_TAG + 'X'
1719
1720 m_exist.side_effect = [False, True]
1721 self.assertTrue(dsaz._is_platform_viable('/other/seed/dir'))
1722 self.assertEqual(
1723 [mock.call('/var/lib/waagent/ovf-env.xml'),
1724 mock.call('/other/seed/dir/ovf-env.xml')],
1725 m_exist.call_args_list)
1726
1727 @mock.patch('cloudinit.sources.DataSourceAzure.util.which')
1728 @mock.patch('cloudinit.sources.DataSourceAzure.util.subp')
1729 def test_true_on_detect_virt_microsoft(self, m_subp, m_which):
1730 """Return True if a partition label prefix rd_rdfe is present."""
1731 m_which.return_value = '/usr/bin/systemd-detect-virt'
1732 m_subp.return_value = ('microsoft\n', '')
1733 self.assertTrue(wrap_and_call(
1734 'cloudinit.sources.DataSourceAzure',
1735 {'os.path.exists': False,
1736 # Non-matching Azure chassis-asset-tag
1737 'util.read_dmi_data': dsaz.AZURE_CHASSIS_ASSET_TAG + 'X'},
1738 dsaz._is_platform_viable, 'doesnotmatter'))
1739 m_which.assert_called_once_with('systemd-detect-virt')
1740 m_subp.assert_called_once_with(
1741 ['systemd-detect-virt'], capture=True, rcs=[0, 1])
1742
1743 @mock.patch('cloudinit.sources.DataSourceAzure.util.find_devs_with')
1744 def test_true_on_azure_when_fs_label_prefix_rd_rdfe(self, m_find_devs):
1745 """Return True if a partition label prefix rd_rdfe is present."""
1746
1747 m_find_devs.return_value = ['/dev/Imatched/azure']
1748 self.assertTrue(wrap_and_call(
1749 'cloudinit.sources.DataSourceAzure',
1750 {'os.path.exists': False,
1751 # Non-matching Azure chassis-asset-tag
1752 'util.read_dmi_data': dsaz.AZURE_CHASSIS_ASSET_TAG + 'X',
1753 'util.which': None},
1754 dsaz._is_platform_viable, 'doesnotmatter'))
1755 m_find_devs.assert_called_once_with(criteria='LABEL=rd_rdfe_*')
1756
1757 def test_false_on_no_matching_azure_criteria(self):
1758 """Report non-azure on unmatched asset tag, ovf-env absent and no dev.
1759
1760 Return False when the asset tag doesn't match Azure's static
1761 AZURE_CHASSIS_ASSET_TAG, no ovf-env.xml files exist in known seed dirs
1762 and no devices have a label starting with prefix 'rd_rdfe_'.
1763 """
1764 self.assertFalse(wrap_and_call(
1765 'cloudinit.sources.DataSourceAzure',
1766 {'os.path.exists': False,
1767 # Non-matching Azure chassis-asset-tag
1768 'util.read_dmi_data': dsaz.AZURE_CHASSIS_ASSET_TAG + 'X',
1769 'util.which': None,
1770 'util.find_devs_with': []},
1771 dsaz._is_platform_viable, 'doesnotmatter'))
1772 self.assertIn(
1773 "DEBUG: Non-Azure DMI asset tag '{0}' discovered.\n".format(
1774 dsaz.AZURE_CHASSIS_ASSET_TAG + 'X'),
1775 self.logs.getvalue())
1776
1777
1401# vi: ts=4 expandtab1778# vi: ts=4 expandtab

Subscribers

People subscribed via source and target branches