← Back to team overview

cloud-init-dev team mailing list archive

[Merge] ~chad.smith/cloud-init:ubuntu/xenial into cloud-init:ubuntu/xenial

 

Chad Smith has proposed merging ~chad.smith/cloud-init:ubuntu/xenial into cloud-init:ubuntu/xenial.

Requested reviews:
  cloud-init commiters (cloud-init-dev)
Related bugs:
  Bug #1645824 in cloud-init: "NoCloud source doesn't work on FreeBSD"
  https://bugs.launchpad.net/cloud-init/+bug/1645824
  Bug #1669875 in cloud-init: "identify openstack vmware platform"
  https://bugs.launchpad.net/cloud-init/+bug/1669875
  Bug #1827238 in cloud-init: "Machines fail to deploy because cloud-init needs to accept both netplan spellings for grat arp"
  https://bugs.launchpad.net/cloud-init/+bug/1827238

For more details, see:
https://code.launchpad.net/~chad.smith/cloud-init/+git/cloud-init/+merge/368355
-- 
Your team cloud-init commiters is requested to review the proposed merge of ~chad.smith/cloud-init:ubuntu/xenial into cloud-init:ubuntu/xenial.
diff --git a/cloudinit/config/cc_growpart.py b/cloudinit/config/cc_growpart.py
index bafca9d..564f376 100644
--- a/cloudinit/config/cc_growpart.py
+++ b/cloudinit/config/cc_growpart.py
@@ -215,7 +215,8 @@ def device_part_info(devpath):
     # FreeBSD doesn't know of sysfs so just get everything we need from
     # the device, like /dev/vtbd0p2.
     if util.is_FreeBSD():
-        m = re.search('^(/dev/.+)p([0-9])$', devpath)
+        freebsd_part = "/dev/" + util.find_freebsd_part(devpath)
+        m = re.search('^(/dev/.+)p([0-9])$', freebsd_part)
         return (m.group(1), m.group(2))
 
     if not os.path.exists(syspath):
diff --git a/cloudinit/config/cc_resizefs.py b/cloudinit/config/cc_resizefs.py
index 076b9d5..afd2e06 100644
--- a/cloudinit/config/cc_resizefs.py
+++ b/cloudinit/config/cc_resizefs.py
@@ -81,7 +81,7 @@ def _resize_xfs(mount_point, devpth):
 
 
 def _resize_ufs(mount_point, devpth):
-    return ('growfs', '-y', devpth)
+    return ('growfs', '-y', mount_point)
 
 
 def _resize_zfs(mount_point, devpth):
@@ -101,7 +101,7 @@ def _can_skip_resize_ufs(mount_point, devpth):
     """
     # dumpfs -m /
     # newfs command for / (/dev/label/rootfs)
-      newfs -O 2 -U -a 4 -b 32768 -d 32768 -e 4096 -f 4096 -g 16384
+      newfs -L rootf -O 2 -U -a 4 -b 32768 -d 32768 -e 4096 -f 4096 -g 16384
             -h 64 -i 8192 -j -k 6408 -m 8 -o time -s 58719232 /dev/label/rootf
     """
     cur_fs_sz = None
@@ -110,7 +110,7 @@ def _can_skip_resize_ufs(mount_point, devpth):
     for line in dumpfs_res.splitlines():
         if not line.startswith('#'):
             newfs_cmd = shlex.split(line)
-            opt_value = 'O:Ua:s:b:d:e:f:g:h:i:jk:m:o:'
+            opt_value = 'O:Ua:s:b:d:e:f:g:h:i:jk:m:o:L:'
             optlist, _args = getopt.getopt(newfs_cmd[1:], opt_value)
             for o, a in optlist:
                 if o == "-s":
diff --git a/cloudinit/config/cc_ubuntu_advantage.py b/cloudinit/config/cc_ubuntu_advantage.py
index f488123..f846e9a 100644
--- a/cloudinit/config/cc_ubuntu_advantage.py
+++ b/cloudinit/config/cc_ubuntu_advantage.py
@@ -36,7 +36,7 @@ schema = {
         """),
     'distros': distros,
     'examples': [dedent("""\
-        # Attach the machine to a Ubuntu Advantage support contract with a
+        # Attach the machine to an Ubuntu Advantage support contract with a
         # UA contract token obtained from %s.
         ubuntu_advantage:
           token: <ua_contract_token>
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index 4d19f56..3702130 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -707,6 +707,14 @@ class NetworkStateInterpreter(object):
             item_params = dict((key, value) for (key, value) in
                                item_cfg.items() if key not in
                                NETWORK_V2_KEY_FILTER)
+            # we accept the fixed spelling, but write the old for compatability
+            # Xenial does not have an updated netplan which supports the
+            # correct spelling.  LP: #1756701
+            params = item_params['parameters']
+            grat_value = params.pop('gratuitous-arp', None)
+            if grat_value:
+                params['gratuitious-arp'] = grat_value
+
             v1_cmd = {
                 'type': cmd_type,
                 'name': item_name,
diff --git a/cloudinit/sources/DataSourceNoCloud.py b/cloudinit/sources/DataSourceNoCloud.py
index fcf5d58..8a9e5dd 100644
--- a/cloudinit/sources/DataSourceNoCloud.py
+++ b/cloudinit/sources/DataSourceNoCloud.py
@@ -35,6 +35,26 @@ class DataSourceNoCloud(sources.DataSource):
         root = sources.DataSource.__str__(self)
         return "%s [seed=%s][dsmode=%s]" % (root, self.seed, self.dsmode)
 
+    def _get_devices(self, label):
+        if util.is_FreeBSD():
+            devlist = [
+                p for p in ['/dev/msdosfs/' + label, '/dev/iso9660/' + label]
+                if os.path.exists(p)]
+        else:
+            # Query optical drive to get it in blkid cache for 2.6 kernels
+            util.find_devs_with(path="/dev/sr0")
+            util.find_devs_with(path="/dev/sr1")
+
+            fslist = util.find_devs_with("TYPE=vfat")
+            fslist.extend(util.find_devs_with("TYPE=iso9660"))
+
+            label_list = util.find_devs_with("LABEL=%s" % label.upper())
+            label_list.extend(util.find_devs_with("LABEL=%s" % label.lower()))
+
+            devlist = list(set(fslist) & set(label_list))
+            devlist.sort(reverse=True)
+        return devlist
+
     def _get_data(self):
         defaults = {
             "instance-id": "nocloud",
@@ -99,20 +119,7 @@ class DataSourceNoCloud(sources.DataSource):
 
         label = self.ds_cfg.get('fs_label', "cidata")
         if label is not None:
-            # Query optical drive to get it in blkid cache for 2.6 kernels
-            util.find_devs_with(path="/dev/sr0")
-            util.find_devs_with(path="/dev/sr1")
-
-            fslist = util.find_devs_with("TYPE=vfat")
-            fslist.extend(util.find_devs_with("TYPE=iso9660"))
-
-            label_list = util.find_devs_with("LABEL=%s" % label.upper())
-            label_list.extend(util.find_devs_with("LABEL=%s" % label.lower()))
-
-            devlist = list(set(fslist) & set(label_list))
-            devlist.sort(reverse=True)
-
-            for dev in devlist:
+            for dev in self._get_devices(label):
                 try:
                     LOG.debug("Attempting to use data from %s", dev)
 
@@ -120,9 +127,8 @@ class DataSourceNoCloud(sources.DataSource):
                         seeded = util.mount_cb(dev, _pp2d_callback,
                                                pp2d_kwargs)
                     except ValueError:
-                        if dev in label_list:
-                            LOG.warning("device %s with label=%s not a"
-                                        "valid seed.", dev, label)
+                        LOG.warning("device %s with label=%s not a"
+                                    "valid seed.", dev, label)
                         continue
 
                     mydata = _merge_new_seed(mydata, seeded)
diff --git a/cloudinit/util.py b/cloudinit/util.py
index ea4199c..aa23b3f 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -2337,17 +2337,21 @@ def parse_mtab(path):
     return None
 
 
-def find_freebsd_part(label_part):
-    if label_part.startswith("/dev/label/"):
-        target_label = label_part[5:]
-        (label_part, _err) = subp(['glabel', 'status', '-s'])
-        for labels in label_part.split("\n"):
+def find_freebsd_part(fs):
+    splitted = fs.split('/')
+    if len(splitted) == 3:
+        return splitted[2]
+    elif splitted[2] in ['label', 'gpt', 'ufs']:
+        target_label = fs[5:]
+        (part, _err) = subp(['glabel', 'status', '-s'])
+        for labels in part.split("\n"):
             items = labels.split()
-            if len(items) > 0 and items[0].startswith(target_label):
-                label_part = items[2]
+            if len(items) > 0 and items[0] == target_label:
+                part = items[2]
                 break
-        label_part = str(label_part)
-    return label_part
+        return str(part)
+    else:
+        LOG.warning("Unexpected input in find_freebsd_part: %s", fs)
 
 
 def get_path_dev_freebsd(path, mnt_list):
diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl
index 25db43e..684c747 100644
--- a/config/cloud.cfg.tmpl
+++ b/config/cloud.cfg.tmpl
@@ -32,8 +32,8 @@ preserve_hostname: false
 
 {% if variant in ["freebsd"] %}
 # This should not be required, but leave it in place until the real cause of
-# not beeing able to find -any- datasources is resolved.
-datasource_list: ['ConfigDrive', 'Azure', 'OpenStack', 'Ec2']
+# not finding -any- datasources is resolved.
+datasource_list: ['NoCloud', 'ConfigDrive', 'Azure', 'OpenStack', 'Ec2']
 {% endif %}
 # Example datasource config
 # datasource:
diff --git a/debian/changelog b/debian/changelog
index 270b0f3..d05a3d4 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+cloud-init (19.1-1-gbaa47854-0ubuntu1~16.04.2) UNRELEASED; urgency=medium
+
+  * refresh patches:
+   + debian/patches/ubuntu-advantage-revert-tip.patch
+
+ -- Chad Smith <chad.smith@xxxxxxxxxxxxx>  Tue, 04 Jun 2019 14:17:46 -0600
+
 cloud-init (19.1-1-gbaa47854-0ubuntu1~16.04.1) xenial; urgency=medium
 
   * debian/patches/ubuntu-advantage-revert-tip.patch
diff --git a/debian/patches/ubuntu-advantage-revert-tip.patch b/debian/patches/ubuntu-advantage-revert-tip.patch
index 08bdc81..5f1e043 100644
--- a/debian/patches/ubuntu-advantage-revert-tip.patch
+++ b/debian/patches/ubuntu-advantage-revert-tip.patch
@@ -9,257 +9,9 @@ Forwarded: not-needed
 Last-Update: 2019-05-10
 ---
 This patch header follows DEP-3: http://dep.debian.net/deps/dep3/
-Index: cloud-init/cloudinit/config/cc_ubuntu_advantage.py
-===================================================================
---- cloud-init.orig/cloudinit/config/cc_ubuntu_advantage.py
-+++ cloud-init/cloudinit/config/cc_ubuntu_advantage.py
-@@ -1,143 +1,150 @@
-+# Copyright (C) 2018 Canonical Ltd.
-+#
- # This file is part of cloud-init. See LICENSE file for license information.
- 
--"""ubuntu_advantage: Configure Ubuntu Advantage support services"""
-+"""Ubuntu advantage: manage ubuntu-advantage offerings from Canonical."""
- 
-+import sys
- from textwrap import dedent
- 
--import six
--
-+from cloudinit import log as logging
- from cloudinit.config.schema import (
-     get_schema_doc, validate_cloudconfig_schema)
--from cloudinit import log as logging
- from cloudinit.settings import PER_INSTANCE
-+from cloudinit.subp import prepend_base_command
- from cloudinit import util
- 
- 
--UA_URL = 'https://ubuntu.com/advantage'
--
- distros = ['ubuntu']
-+frequency = PER_INSTANCE
-+
-+LOG = logging.getLogger(__name__)
- 
- schema = {
-     'id': 'cc_ubuntu_advantage',
-     'name': 'Ubuntu Advantage',
--    'title': 'Configure Ubuntu Advantage support services',
-+    'title': 'Install, configure and manage ubuntu-advantage offerings',
-     'description': dedent("""\
--        Attach machine to an existing Ubuntu Advantage support contract and
--        enable or disable support services such as Livepatch, ESM,
--        FIPS and FIPS Updates. When attaching a machine to Ubuntu Advantage,
--        one can also specify services to enable.  When the 'enable'
--        list is present, any named service will be enabled and all absent
--        services will remain disabled.
--
--        Note that when enabling FIPS or FIPS updates you will need to schedule
--        a reboot to ensure the machine is running the FIPS-compliant kernel.
--        See :ref:`Power State Change` for information on how to configure
--        cloud-init to perform this reboot.
-+        This module provides configuration options to setup ubuntu-advantage
-+        subscriptions.
-+
-+        .. note::
-+            Both ``commands`` value can be either a dictionary or a list. If
-+            the configuration provided is a dictionary, the keys are only used
-+            to order the execution of the commands and the dictionary is
-+            merged with any vendor-data ubuntu-advantage configuration
-+            provided. If a ``commands`` is provided as a list, any vendor-data
-+            ubuntu-advantage ``commands`` are ignored.
-+
-+        Ubuntu-advantage ``commands`` is a dictionary or list of
-+        ubuntu-advantage commands to run on the deployed machine.
-+        These commands can be used to enable or disable subscriptions to
-+        various ubuntu-advantage products. See 'man ubuntu-advantage' for more
-+        information on supported subcommands.
-+
-+        .. note::
-+           Each command item can be a string or list. If the item is a list,
-+           'ubuntu-advantage' can be omitted and it will automatically be
-+           inserted as part of the command.
-         """),
-     'distros': distros,
-     'examples': [dedent("""\
--        # Attach the machine to a Ubuntu Advantage support contract with a
--        # UA contract token obtained from %s.
--        ubuntu_advantage:
--          token: <ua_contract_token>
--    """ % UA_URL), dedent("""\
--        # Attach the machine to an Ubuntu Advantage support contract enabling
--        # only fips and esm services. Services will only be enabled if
--        # the environment supports said service. Otherwise warnings will
--        # be logged for incompatible services specified.
-+        # Enable Extended Security Maintenance using your service auth token
-+        ubuntu-advantage:
-+            commands:
-+              00: ubuntu-advantage enable-esm <token>
-+    """), dedent("""\
-+        # Enable livepatch by providing your livepatch token
-         ubuntu-advantage:
--          token: <ua_contract_token>
--          enable:
--          - fips
--          - esm
-+            commands:
-+                00: ubuntu-advantage enable-livepatch <livepatch-token>
-+
-     """), dedent("""\
--        # Attach the machine to an Ubuntu Advantage support contract and enable
--        # the FIPS service.  Perform a reboot once cloud-init has
--        # completed.
--        power_state:
--          mode: reboot
-+        # Convenience: the ubuntu-advantage command can be omitted when
-+        # specifying commands as a list and 'ubuntu-advantage' will
-+        # automatically be prepended.
-+        # The following commands are equivalent
-         ubuntu-advantage:
--          token: <ua_contract_token>
--          enable:
--          - fips
--        """)],
-+            commands:
-+                00: ['enable-livepatch', 'my-token']
-+                01: ['ubuntu-advantage', 'enable-livepatch', 'my-token']
-+                02: ubuntu-advantage enable-livepatch my-token
-+                03: 'ubuntu-advantage enable-livepatch my-token'
-+    """)],
-     'frequency': PER_INSTANCE,
-     'type': 'object',
-     'properties': {
--        'ubuntu_advantage': {
-+        'ubuntu-advantage': {
-             'type': 'object',
-             'properties': {
--                'enable': {
--                    'type': 'array',
--                    'items': {'type': 'string'},
--                },
--                'token': {
--                    'type': 'string',
--                    'description': (
--                        'A contract token obtained from %s.' % UA_URL)
-+                'commands': {
-+                    'type': ['object', 'array'],  # Array of strings or dict
-+                    'items': {
-+                        'oneOf': [
-+                            {'type': 'array', 'items': {'type': 'string'}},
-+                            {'type': 'string'}]
-+                    },
-+                    'additionalItems': False,  # Reject non-string & non-list
-+                    'minItems': 1,
-+                    'minProperties': 1,
-                 }
-             },
--            'required': ['token'],
--            'additionalProperties': False
-+            'additionalProperties': False,  # Reject keys not in schema
-+            'required': ['commands']
-         }
-     }
- }
- 
-+# TODO schema for 'assertions' and 'commands' are too permissive at the moment.
-+# Once python-jsonschema supports schema draft 6 add support for arbitrary
-+# object keys with 'patternProperties' constraint to validate string values.
-+
- __doc__ = get_schema_doc(schema)  # Supplement python help()
- 
--LOG = logging.getLogger(__name__)
-+UA_CMD = "ubuntu-advantage"
- 
- 
--def configure_ua(token=None, enable=None):
--    """Call ua commandline client to attach or enable services."""
--    error = None
--    if not token:
--        error = ('ubuntu_advantage: token must be provided')
--        LOG.error(error)
--        raise RuntimeError(error)
--
--    if enable is None:
--        enable = []
--    elif isinstance(enable, six.string_types):
--        LOG.warning('ubuntu_advantage: enable should be a list, not'
--                    ' a string; treating as a single enable')
--        enable = [enable]
--    elif not isinstance(enable, list):
--        LOG.warning('ubuntu_advantage: enable should be a list, not'
--                    ' a %s; skipping enabling services',
--                    type(enable).__name__)
--        enable = []
-+def run_commands(commands):
-+    """Run the commands provided in ubuntu-advantage:commands config.
- 
--    attach_cmd = ['ua', 'attach', token]
--    LOG.debug('Attaching to Ubuntu Advantage. %s', ' '.join(attach_cmd))
--    try:
--        util.subp(attach_cmd)
--    except util.ProcessExecutionError as e:
--        msg = 'Failure attaching Ubuntu Advantage:\n{error}'.format(
--            error=str(e))
--        util.logexc(LOG, msg)
--        raise RuntimeError(msg)
--    enable_errors = []
--    for service in enable:
-+     Commands are run individually. Any errors are collected and reported
-+     after attempting all commands.
-+
-+     @param commands: A list or dict containing commands to run. Keys of a
-+         dict will be used to order the commands provided as dict values.
-+     """
-+    if not commands:
-+        return
-+    LOG.debug('Running user-provided ubuntu-advantage commands')
-+    if isinstance(commands, dict):
-+        # Sort commands based on dictionary key
-+        commands = [v for _, v in sorted(commands.items())]
-+    elif not isinstance(commands, list):
-+        raise TypeError(
-+            'commands parameter was not a list or dict: {commands}'.format(
-+                commands=commands))
-+
-+    fixed_ua_commands = prepend_base_command('ubuntu-advantage', commands)
-+
-+    cmd_failures = []
-+    for command in fixed_ua_commands:
-+        shell = isinstance(command, str)
-         try:
--            cmd = ['ua', 'enable', service]
--            util.subp(cmd, capture=True)
-+            util.subp(command, shell=shell, status_cb=sys.stderr.write)
-         except util.ProcessExecutionError as e:
--            enable_errors.append((service, e))
--    if enable_errors:
--        for service, error in enable_errors:
--            msg = 'Failure enabling "{service}":\n{error}'.format(
--                service=service, error=str(error))
--            util.logexc(LOG, msg)
--        raise RuntimeError(
--            'Failure enabling Ubuntu Advantage service(s): {}'.format(
--                ', '.join('"{}"'.format(service)
--                          for service, _ in enable_errors)))
-+            cmd_failures.append(str(e))
-+    if cmd_failures:
-+        msg = (
-+            'Failures running ubuntu-advantage commands:\n'
-+            '{cmd_failures}'.format(
-+                cmd_failures=cmd_failures))
-+        util.logexc(LOG, msg)
-+        raise RuntimeError(msg)
- 
- 
- def maybe_install_ua_tools(cloud):
-     """Install ubuntu-advantage-tools if not present."""
--    if util.which('ua'):
-+    if util.which('ubuntu-advantage'):
-         return
-     try:
-         cloud.distro.update_package_sources()
-@@ -152,28 +159,14 @@ def maybe_install_ua_tools(cloud):
+--- a/cloudinit/config/cc_ubuntu_advantage.py
++++ b/cloudinit/config/cc_ubuntu_advantage.py
+@@ -152,28 +152,14 @@ def maybe_install_ua_tools(cloud):
  
  
  def handle(name, cfg, cloud, log, args):
@@ -294,10 +46,8 @@ Index: cloud-init/cloudinit/config/cc_ubuntu_advantage.py
 +    run_commands(cfgin.get('commands', []))
  
  # vi: ts=4 expandtab
-Index: cloud-init/cloudinit/config/tests/test_ubuntu_advantage.py
-===================================================================
---- cloud-init.orig/cloudinit/config/tests/test_ubuntu_advantage.py
-+++ cloud-init/cloudinit/config/tests/test_ubuntu_advantage.py
+--- a/cloudinit/config/tests/test_ubuntu_advantage.py
++++ b/cloudinit/config/tests/test_ubuntu_advantage.py
 @@ -1,7 +1,10 @@
  # This file is part of cloud-init. See LICENSE file for license information.
  
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
index 427ab7e..afb614e 100644
--- a/tests/unittests/test_datasource/test_azure.py
+++ b/tests/unittests/test_datasource/test_azure.py
@@ -6,7 +6,6 @@ from cloudinit import url_helper
 from cloudinit.sources import (
     UNSET, DataSourceAzure as dsaz, InvalidMetaDataException)
 from cloudinit.util import (b64e, decode_binary, load_file, write_file,
-                            find_freebsd_part, get_path_dev_freebsd,
                             MountFailedError, json_dumps, load_json)
 from cloudinit.version import version_string as vs
 from cloudinit.tests.helpers import (
@@ -391,29 +390,6 @@ scbus-1 on xpt0 bus 0
         dev = ds.get_resource_disk_on_freebsd(1)
         self.assertEqual("da1", dev)
 
-    @mock.patch('cloudinit.util.subp')
-    def test_find_freebsd_part_on_Azure(self, mock_subp):
-        glabel_out = '''
-gptid/fa52d426-c337-11e6-8911-00155d4c5e47  N/A  da0p1
-                              label/rootfs  N/A  da0p2
-                                label/swap  N/A  da0p3
-'''
-        mock_subp.return_value = (glabel_out, "")
-        res = find_freebsd_part("/dev/label/rootfs")
-        self.assertEqual("da0p2", res)
-
-    def test_get_path_dev_freebsd_on_Azure(self):
-        mnt_list = '''
-/dev/label/rootfs  /                ufs     rw              1 1
-devfs              /dev             devfs   rw,multilabel   0 0
-fdescfs            /dev/fd          fdescfs rw              0 0
-/dev/da1s1         /mnt/resource    ufs     rw              2 2
-'''
-        with mock.patch.object(os.path, 'exists',
-                               return_value=True):
-            res = get_path_dev_freebsd('/etc', mnt_list)
-            self.assertIsNotNone(res)
-
     @mock.patch(MOCKPATH + '_is_platform_viable')
     def test_call_is_platform_viable_seed(self, m_is_platform_viable):
         """Check seed_dir using _is_platform_viable and return False."""
diff --git a/tests/unittests/test_datasource/test_nocloud.py b/tests/unittests/test_datasource/test_nocloud.py
index b785362..18bea0b 100644
--- a/tests/unittests/test_datasource/test_nocloud.py
+++ b/tests/unittests/test_datasource/test_nocloud.py
@@ -278,6 +278,24 @@ class TestNoCloudDataSource(CiTestCase):
         self.assertEqual(netconf, dsrc.network_config)
         self.assertNotIn(gateway, str(dsrc.network_config))
 
+    @mock.patch("cloudinit.util.blkid")
+    def test_nocloud_get_devices_freebsd(self, m_is_lxd, fake_blkid):
+        populate_dir(os.path.join(self.paths.seed_dir, "nocloud"),
+                     {'user-data': b"ud", 'meta-data': "instance-id: IID\n"})
+
+        sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}
+
+        self.mocks.enter_context(
+            mock.patch.object(util, 'is_FreeBSD', return_value=True))
+
+        self.mocks.enter_context(
+            mock.patch.object(os.path, 'exists', return_value=True))
+
+        dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
+        ret = dsrc._get_devices('foo')
+        self.assertEqual(['/dev/msdosfs/foo', '/dev/iso9660/foo'], ret)
+        fake_blkid.assert_not_called()
+
 
 class TestParseCommandLineData(CiTestCase):
 
diff --git a/tests/unittests/test_distros/test_freebsd.py b/tests/unittests/test_distros/test_freebsd.py
new file mode 100644
index 0000000..8af253a
--- /dev/null
+++ b/tests/unittests/test_distros/test_freebsd.py
@@ -0,0 +1,45 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+from cloudinit.util import (find_freebsd_part, get_path_dev_freebsd)
+from cloudinit.tests.helpers import (CiTestCase, mock)
+
+import os
+
+
+class TestDeviceLookUp(CiTestCase):
+
+    @mock.patch('cloudinit.util.subp')
+    def test_find_freebsd_part_label(self, mock_subp):
+        glabel_out = '''
+gptid/fa52d426-c337-11e6-8911-00155d4c5e47  N/A  da0p1
+                              label/rootfs  N/A  da0p2
+                                label/swap  N/A  da0p3
+'''
+        mock_subp.return_value = (glabel_out, "")
+        res = find_freebsd_part("/dev/label/rootfs")
+        self.assertEqual("da0p2", res)
+
+    @mock.patch('cloudinit.util.subp')
+    def test_find_freebsd_part_gpt(self, mock_subp):
+        glabel_out = '''
+                                gpt/bootfs  N/A  vtbd0p1
+gptid/3f4cbe26-75da-11e8-a8f2-002590ec6166  N/A  vtbd0p1
+                                gpt/swapfs  N/A  vtbd0p2
+                                gpt/rootfs  N/A  vtbd0p3
+                            iso9660/cidata  N/A  vtbd2
+'''
+        mock_subp.return_value = (glabel_out, "")
+        res = find_freebsd_part("/dev/gpt/rootfs")
+        self.assertEqual("vtbd0p3", res)
+
+    def test_get_path_dev_freebsd_label(self):
+        mnt_list = '''
+/dev/label/rootfs  /                ufs     rw              1 1
+devfs              /dev             devfs   rw,multilabel   0 0
+fdescfs            /dev/fd          fdescfs rw              0 0
+/dev/da1s1         /mnt/resource    ufs     rw              2 2
+'''
+        with mock.patch.object(os.path, 'exists',
+                               return_value=True):
+            res = get_path_dev_freebsd('/etc', mnt_list)
+            self.assertIsNotNone(res)
diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py
index 8c18aa1..7575223 100644
--- a/tests/unittests/test_ds_identify.py
+++ b/tests/unittests/test_ds_identify.py
@@ -435,6 +435,14 @@ class TestDsIdentify(DsIdentifyBase):
         """Open Telecom identification."""
         self._test_ds_found('OpenStack-OpenTelekom')
 
+    def test_openstack_asset_tag_nova(self):
+        """OpenStack identification via asset tag OpenStack Nova."""
+        self._test_ds_found('OpenStack-AssetTag-Nova')
+
+    def test_openstack_asset_tag_copute(self):
+        """OpenStack identification via asset tag OpenStack Compute."""
+        self._test_ds_found('OpenStack-AssetTag-Compute')
+
     def test_openstack_on_non_intel_is_maybe(self):
         """On non-Intel, openstack without dmi info is maybe.
 
@@ -759,6 +767,18 @@ VALID_CFG = {
         'files': {P_CHASSIS_ASSET_TAG: 'OpenTelekomCloud\n'},
         'mocks': [MOCK_VIRT_IS_XEN],
     },
+    'OpenStack-AssetTag-Nova': {
+        # VMware vSphere can't modify product-name, LP: #1669875
+        'ds': 'OpenStack',
+        'files': {P_CHASSIS_ASSET_TAG: 'OpenStack Nova\n'},
+        'mocks': [MOCK_VIRT_IS_XEN],
+    },
+    'OpenStack-AssetTag-Compute': {
+        # VMware vSphere can't modify product-name, LP: #1669875
+        'ds': 'OpenStack',
+        'files': {P_CHASSIS_ASSET_TAG: 'OpenStack Compute\n'},
+        'mocks': [MOCK_VIRT_IS_XEN],
+    },
     'OVF-seed': {
         'ds': 'OVF',
         'files': {
diff --git a/tests/unittests/test_handler/test_handler_resizefs.py b/tests/unittests/test_handler/test_handler_resizefs.py
index 3518784..db9a041 100644
--- a/tests/unittests/test_handler/test_handler_resizefs.py
+++ b/tests/unittests/test_handler/test_handler_resizefs.py
@@ -147,7 +147,7 @@ class TestResizefs(CiTestCase):
     def test_resize_ufs_cmd_return(self):
         mount_point = '/'
         devpth = '/dev/sda2'
-        self.assertEqual(('growfs', '-y', devpth),
+        self.assertEqual(('growfs', '-y', mount_point),
                          _resize_ufs(mount_point, devpth))
 
     @mock.patch('cloudinit.util.is_container', return_value=False)
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index e85e964..b936bc9 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -407,6 +407,37 @@ network:
                 - maas
 """
 
+NETPLAN_BOND_GRAT_ARP = """
+network:
+    bonds:
+        bond0:
+            interfaces:
+            - ens3
+            macaddress: 68:05:ca:64:d3:6c
+            mtu: 9000
+            parameters:
+                gratuitious-arp: 1
+        bond1:
+            interfaces:
+            - ens4
+            macaddress: 68:05:ca:64:d3:6d
+            mtu: 9000
+            parameters:
+                gratuitous-arp: 2
+    ethernets:
+        ens3:
+            dhcp4: false
+            dhcp6: false
+            match:
+                macaddress: 52:54:00:ab:cd:ef
+        ens4:
+            dhcp4: false
+            dhcp6: false
+            match:
+                macaddress: 52:54:00:11:22:ff
+    version: 2
+"""
+
 NETPLAN_DHCP_FALSE = """
 version: 2
 ethernets:
@@ -3589,6 +3620,21 @@ class TestNetplanRoundTrip(CiTestCase):
             entry['expected_netplan'].splitlines(),
             files['/etc/netplan/50-cloud-init.yaml'].splitlines())
 
+    def test_render_output_supports_both_grat_arp_spelling(self):
+        entry = {
+            'yaml': NETPLAN_BOND_GRAT_ARP,
+            'expected_netplan': NETPLAN_BOND_GRAT_ARP.replace('gratuitous',
+                                                              'gratuitious'),
+        }
+        network_config = yaml.load(entry['yaml']).get('network')
+        files = self._render_and_read(network_config=network_config)
+        print(entry['expected_netplan'])
+        print('-- expected ^ | v rendered --')
+        print(files['/etc/netplan/50-cloud-init.yaml'])
+        self.assertEqual(
+            entry['expected_netplan'].splitlines(),
+            files['/etc/netplan/50-cloud-init.yaml'].splitlines())
+
 
 class TestEniRoundTrip(CiTestCase):
 
diff --git a/tools/ds-identify b/tools/ds-identify
index 6518901..e16708f 100755
--- a/tools/ds-identify
+++ b/tools/ds-identify
@@ -979,6 +979,14 @@ dscheck_OpenStack() {
         return ${DS_FOUND}
     fi
 
+    # LP: #1669875 : allow identification of OpenStack by asset tag
+    if dmi_chassis_asset_tag_matches "$nova"; then
+        return ${DS_FOUND}
+    fi
+    if dmi_chassis_asset_tag_matches "$compute"; then
+        return ${DS_FOUND}
+    fi
+
     # LP: #1715241 : arch other than intel are not identified properly.
     case "$DI_UNAME_MACHINE" in
         i?86|x86_64) :;;
diff --git a/tools/render-cloudcfg b/tools/render-cloudcfg
index 8b7cb87..0957c32 100755
--- a/tools/render-cloudcfg
+++ b/tools/render-cloudcfg
@@ -4,7 +4,7 @@ import argparse
 import os
 import sys
 
-VARIANTS = ["bsd", "centos", "fedora", "rhel", "suse", "ubuntu", "unknown"]
+VARIANTS = ["freebsd", "centos", "fedora", "rhel", "suse", "ubuntu", "unknown"]
 
 if "avoid-pep8-E402-import-not-top-of-file":
     _tdir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
diff --git a/tools/run-container b/tools/run-container
index 852f4d1..1d24e15 100755
--- a/tools/run-container
+++ b/tools/run-container
@@ -373,7 +373,7 @@ wait_for_boot() {
             inside "$name" sh -c "echo proxy=$http_proxy >> /etc/yum.conf"
             inside "$name" sed -i s/enabled=1/enabled=0/ \
                 /etc/yum/pluginconf.d/fastestmirror.conf
-            inside "$name" sh -c "sed -i '/^#baseurl=/s/#//' /etc/yum.repos.d/*.repo"
+            inside "$name" sh -c "sed -i '/^#baseurl=/s/#// ; s/^mirrorlist/#mirrorlist/' /etc/yum.repos.d/*.repo"
         else
             debug 1 "do not know how to configure proxy on $OS_NAME"
         fi

Follow ups