diff --git a/.gitignore b/.gitignore index af68b39..79b1e54 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ .ropeproject .tox .venv +.testrepository AUTHORS Authors build-stamp diff --git a/cloudbaseinit/metadata/factory.py b/cloudbaseinit/metadata/factory.py index 1de2b9d..b97e246 100644 --- a/cloudbaseinit/metadata/factory.py +++ b/cloudbaseinit/metadata/factory.py @@ -22,8 +22,8 @@ cfg.ListOpt( 'metadata_services', default=[ + 'cloudbaseinit.metadata.services.configdrive.ConfigDriveService', 'cloudbaseinit.metadata.services.httpservice.HttpService', - #'cloudbaseinit.metadata.services.configdrive.ConfigDriveService', #'cloudbaseinit.metadata.services.ec2service.EC2Service', #'cloudbaseinit.metadata.services.maasservice.MaaSHttpService', #'cloudbaseinit.metadata.services.cloudstack.CloudStack', diff --git a/cloudbaseinit/metadata/services/osconfigdrive/factory.py b/cloudbaseinit/metadata/services/osconfigdrive/factory.py index cea0986..90ac516 100644 --- a/cloudbaseinit/metadata/services/osconfigdrive/factory.py +++ b/cloudbaseinit/metadata/services/osconfigdrive/factory.py @@ -19,8 +19,14 @@ def get_config_drive_manager(): class_paths = { - 'win32': 'cloudbaseinit.metadata.services.osconfigdrive.windows.' - 'WindowsConfigDriveManager', + 'freebsd10': ( + 'cloudbaseinit.metadata.services.osconfigdrive.freebsd.' + 'FreeBSDConfigDriveManager' + ), + 'win32': ( + 'cloudbaseinit.metadata.services.osconfigdrive.windows.' + 'WindowsConfigDriveManager' + ), } class_path = class_paths.get(sys.platform) diff --git a/cloudbaseinit/metadata/services/osconfigdrive/freebsd.py b/cloudbaseinit/metadata/services/osconfigdrive/freebsd.py new file mode 100644 index 0000000..fa21112 --- /dev/null +++ b/cloudbaseinit/metadata/services/osconfigdrive/freebsd.py @@ -0,0 +1,136 @@ +from cloudbaseinit.metadata.services.osconfigdrive import base +from cloudbaseinit.openstack.common import log as logging +from oslo.config import cfg +import os +import contextlib +import shutil +import subprocess +import tempfile +import re + +LOG = logging.getLogger(__name__) + +class FreeBSDConfigDriveManager(base.BaseConfigDriveManager): + + def get_config_drive_files(self, target_path, check_raw_hhd=True, + check_cdrom=True, check_vfat=True): + config_drive_found = False + + if check_vfat: + LOG.debug('Looking for Config Drive in VFAT filesystems') + config_drive_found = self._get_conf_drive_from_vfat(target_path) + + if not config_drive_found and check_raw_hhd: + LOG.debug('Looking for Config Drive in raw HDDs') + config_drive_found = self._get_conf_drive_from_raw_hdd(target_path) + + if not config_drive_found and check_cdrom: + LOG.debug('Looking for Config Drive in cdrom drives') + config_drive_found = self._get_conf_drive_from_cdrom_drive(target_path) + + return config_drive_found + + def _get_conf_drive_from_vfat(self, target_path): + return False + + def _get_conf_drive_from_raw_hdd(self, target_path): + return False + + def _get_conf_drive_from_cdrom_drive(self, target_path): + cdrom = self._get_cdrom_device() + if not cdrom: + return False + + with tempdir() as tmp: + umount = False + cdrom_mount_point = self._get_existing_cdrom_mount(cdrom) + if not cdrom_mount_point: + try: + mountcmd = ['mount', '-o', 'ro', '-t', 'cd9660', cdrom, tmp] + subprocess.check_call(mountcmd) + umount = tmp + cdrom_mount_point = tmp + except subprocess.CalledProcessError as exc: + LOG.debug('Failed mount of %s as %s: %s', cdrom, tmp, exc) + return False + + with unmounter(umount): + shutil.copytree(cdrom_mount_point, target_path) + return True + + return False + + def _get_cdrom_device(self): + devices = self._get_devices() + cdrom_drives = ['/dev/%s' % d for d in devices if self._is_cdrom(d)] + if len(cdrom_drives): + return cdrom_drives[0] + return None + + def _get_devices(self): + devices = [] + cmd = 'sysctl -n kern.disks' + try: + output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True) + devices = output.split() + except subprocess.CalledProcessError: + pass + + return devices + + def _is_cdrom(self, device): + cmd = 'glabel status -s %s' % device + try: + output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True) + return output.startswith('iso9660/config-2') + except subprocess.CalledProcessError: + return False + + def _get_existing_cdrom_mount(self, device): + existing_mounts = self._get_mounts() + mount = None + if device in existing_mounts: + mount = existing_mounts[os.path.realpath(device)]['mountpoint'] + return mount + + def _get_mounts(self): + mounts = {} + mountre = r'^(/dev/[\S]+) on (/.*) \((.+), .+, (.+)\)$' + cmd_output = subprocess.check_output('mount', stderr=subprocess.STDOUT, shell=True) + mount_info = cmd_output.split('\n') + for mount in mount_info: + try: + m = re.search(mountre, mount) + dev = m.group(1) + mp = m.group(2) + fstype = m.group(3) + opts = m.group(4) + except: + continue + + mounts[dev] = { + 'fstype': fstype, + 'mountpoint': mp, + 'opts': opts + } + + return mounts + + +@contextlib.contextmanager +def tempdir(**kwargs): + tdir = tempfile.mkdtemp(**kwargs) + try: + yield tdir + finally: + shutil.rmtree(tdir) + + +@contextlib.contextmanager +def unmounter(umount): + try: + yield umount + finally: + if umount: + umount_cmd = ["umount", umount] + subprocess.check_call(umount_cmd) diff --git a/cloudbaseinit/osutils/freebsd.py b/cloudbaseinit/osutils/freebsd.py index 3ac10e1..8d9ed2b 100644 --- a/cloudbaseinit/osutils/freebsd.py +++ b/cloudbaseinit/osutils/freebsd.py @@ -63,7 +63,7 @@ def get_network_adapters(self): """ if_list = subprocess.check_output(['ifconfig', '-l']).split(' ') # Filter out non-network interfaces - if_list = filter(lambda x: x.startswith(('pflog', 'lo', 'plip')), if_list) + if_list = filter(lambda x: not x.startswith(('pflog', 'lo', 'plip')), if_list) return if_list def set_static_network_config(self, adapter_name, address, netmask, @@ -78,14 +78,19 @@ def set_static_network_config(self, adapter_name, address, netmask, if_cmd = 'ifconfig ' + adapter_name + ' inet ' + address + ' netmask ' + netmask + ' broadcast ' + broadcast route_cmd = 'route add default ' + gateway - resolv_conf = ['domain ' + dnsdomain] - resolv_conf_file = os.popen('resolvconf -a vtnet0', 'w', 1) + resolv_conf = [] + if dnsdomain: + resolv_conf.append('domain ' + dnsdomain) + + resolvconf_cmd = 'resolvconf -a %s' % adapter_name + resolv_conf_file = subprocess.Popen( + resolvconf_cmd, shell=True, bufsize=1, stdin=subprocess.PIPE).stdin for i in dnsnameservers: resolv_conf.append('nameserver ' + i) subprocess.check_call(if_cmd, shell=True) subprocess.check_call(route_cmd, shell=True) - self._add_comment(resolv_conf_file); + self._add_comment(resolv_conf_file) for line in resolv_conf: resolv_conf_file.write(line + '\n') self._add_rc_conf({'ifconfig_' + adapter_name: 'inet ' + address + ' netmask ' + netmask + ' broadcast ' + broadcast, diff --git a/cloudbaseinit/plugins/common/execcmd.py b/cloudbaseinit/plugins/common/execcmd.py index e0f0c56..eb6cb03 100644 --- a/cloudbaseinit/plugins/common/execcmd.py +++ b/cloudbaseinit/plugins/common/execcmd.py @@ -182,7 +182,7 @@ class Python(BaseCommand): class Bash(BaseCommand): extension = '.sh' - command = 'bash' + command = 'sh' class PowershellSysnative(BaseCommand): diff --git a/cloudbaseinit/plugins/common/factory.py b/cloudbaseinit/plugins/common/factory.py index 604c4d6..b1ee5b0 100644 --- a/cloudbaseinit/plugins/common/factory.py +++ b/cloudbaseinit/plugins/common/factory.py @@ -20,6 +20,7 @@ cfg.ListOpt( 'plugins', default=[ + 'cloudbaseinit.plugins.freebsd.networkconfig.NetworkConfigPlugin', 'cloudbaseinit.plugins.freebsd.sethostname.SetHostNamePlugin', 'cloudbaseinit.plugins.freebsd.scramblerootpassword.ScrambleRootPassword', 'cloudbaseinit.plugins.freebsd.createuser.CreateUserPlugin', @@ -28,7 +29,7 @@ 'cloudbaseinit.plugins.freebsd.sshpublickeys.' 'SetUserSSHPublicKeysPlugin', #'cloudbaseinit.plugins.freebsd.extendvolumes.ExtendVolumesPlugin', - #'cloudbaseinit.plugins.freebsd.userdata.UserDataPlugin', + 'cloudbaseinit.plugins.common.userdata.UserDataPlugin', ], help='List of enabled plugin classes, ' 'to executed in the provided order'), diff --git a/cloudbaseinit/plugins/freebsd/networkconfig.py b/cloudbaseinit/plugins/freebsd/networkconfig.py index 878266b..c67c0f7 100644 --- a/cloudbaseinit/plugins/freebsd/networkconfig.py +++ b/cloudbaseinit/plugins/freebsd/networkconfig.py @@ -42,32 +42,12 @@ def execute(self, service, shared_data): if not network_details: return (plugin_base.PLUGIN_EXECUTION_DONE, False) - if 'content_path' not in network_details: - return (base.PLUGIN_EXECUTION_DONE, False) - - content_path = network_details['content_path'] - content_name = content_path.rsplit('/', 1)[-1] - debian_network_conf = service.get_content(content_name) - - LOG.debug('network config content:\n%s' % debian_network_conf) - - # TODO(alexpilotti): implement a proper grammar - m = re.search(r'iface eth0 inet static\s+' - r'address\s+(?P
[^\s]+)\s+' - r'netmask\s+(?P[^\s]+)\s+' - r'broadcast\s+(?P[^\s]+)\s+' - r'gateway\s+(?P[^\s]+)\s+' - r'dns\-nameservers\s+(?P[^\r\n]+)\s+', - debian_network_conf) - if not m: - raise exception.CloudbaseInitException( - "network_details format not recognized") - - address = m.group('address') - netmask = m.group('netmask') - broadcast = m.group('broadcast') - gateway = m.group('gateway') - dnsnameservers = m.group('dnsnameservers').strip().split(' ') + address = network_details[0].address + netmask = network_details[0].netmask + broadcast = network_details[0].broadcast + gateway = network_details[0].gateway + dnsdomain = None + dnsnameservers = network_details[0].dnsnameservers osutils = osutils_factory.get_os_utils() @@ -75,6 +55,7 @@ def execute(self, service, shared_data): if not network_adapter_name: # Get the first available one available_adapters = osutils.get_network_adapters() + LOG.debug('available adapters: %s', available_adapters) if not len(available_adapters): raise exception.CloudbaseInitException( "No network adapter available") @@ -84,6 +65,6 @@ def execute(self, service, shared_data): reboot_required = osutils.set_static_network_config( network_adapter_name, address, netmask, broadcast, - gateway, dnsnameservers) + gateway, dnsdomain, dnsnameservers) return (base.PLUGIN_EXECUTION_DONE, reboot_required) diff --git a/cloudbaseinit/tests/metadata/services/osconfigdrive/test_factory.py b/cloudbaseinit/tests/metadata/services/osconfigdrive/test_factory.py index 93ec80a..842879a 100644 --- a/cloudbaseinit/tests/metadata/services/osconfigdrive/test_factory.py +++ b/cloudbaseinit/tests/metadata/services/osconfigdrive/test_factory.py @@ -35,21 +35,29 @@ def tearDown(self): def _test_get_config_drive_manager(self, mock_load_class, platform): sys.platform = platform - if platform is not "win32": - self.assertRaises(NotImplementedError, - factory.get_config_drive_manager) - - else: + caught_notimplemented = False + try: response = factory.get_config_drive_manager() + self.assertIsNotNone(response) + except NotImplementedError: + caught_notimplemented = True + if platform == 'win32': mock_load_class.assert_called_once_with( 'cloudbaseinit.metadata.services.osconfigdrive.' 'windows.WindowsConfigDriveManager') - - self.assertIsNotNone(response) + elif platform == 'freebsd10': + mock_load_class.assert_called_once_with( + 'cloudbaseinit.metadata.services.osconfigdrive.' + 'freebsd.FreeBSDConfigDriveManager') + else: + self.assertTrue(caught_notimplemented, 'NotImplementedError expected') def test_get_config_drive_manager(self): self._test_get_config_drive_manager(platform="win32") def test_get_config_drive_manager_exception(self): self._test_get_config_drive_manager(platform="other") + + def test_get_config_drive_manager(self): + self._test_get_config_drive_manager(platform="freebsd10") diff --git a/cloudbaseinit/tests/metadata/services/osconfigdrive/test_freebsd.py b/cloudbaseinit/tests/metadata/services/osconfigdrive/test_freebsd.py new file mode 100644 index 0000000..d856863 --- /dev/null +++ b/cloudbaseinit/tests/metadata/services/osconfigdrive/test_freebsd.py @@ -0,0 +1,123 @@ +import importlib +import os +import unittest + +try: + import unittest.mock as mock +except ImportError: + import mock +from oslo.config import cfg + +from cloudbaseinit import exception +from cloudbaseinit.tests import testutils + + +CONF = cfg.CONF + + +class TestFreeBSDConfigDriveManager(unittest.TestCase): + + def setUp(self): + self.freebsd = importlib.import_module( + "cloudbaseinit.metadata.services.osconfigdrive.freebsd") + + self._config_manager = self.freebsd.FreeBSDConfigDriveManager() + + def test_get_config_drive_files_default(self): + response = self._config_manager.get_config_drive_files('fake_path') + self.assertFalse(response) + + def test_get_config_drive_files_vfat(self): + target = mock.MagicMock() + with mock.patch.object(self._config_manager, '_get_conf_drive_from_vfat') as cm: + response = self._config_manager.get_config_drive_files(target) + cm.assert_called_once_with(target) + + def test_get_config_drive_files_vfat_returned(self): + target = mock.MagicMock() + with mock.patch.object(self._config_manager, '_get_conf_drive_from_vfat') as cm: + response = self._config_manager.get_config_drive_files(target) + self.assertEqual(response, cm.return_value) + + def test_get_config_drive_files_raw_hdd(self): + target = mock.MagicMock() + with mock.patch.object(self._config_manager, '_get_conf_drive_from_raw_hdd') as cm: + response = self._config_manager.get_config_drive_files(target) + cm.assert_called_once_with(target) + + def test_get_config_drive_files_raw_hdd_returned(self): + target = mock.MagicMock() + with mock.patch.object(self._config_manager, '_get_conf_drive_from_raw_hdd') as cm: + response = self._config_manager.get_config_drive_files(target) + self.assertEqual(response, cm.return_value) + + def test_get_config_drive_files_cdrom_drive(self): + target = mock.MagicMock() + with mock.patch.object(self._config_manager, '_get_conf_drive_from_cdrom_drive') as cm: + response = self._config_manager.get_config_drive_files(target) + cm.assert_called_once_with(target) + + def test_get_config_drive_files_raw_hdd_returned(self): + target = mock.MagicMock() + with mock.patch.object(self._config_manager, '_get_conf_drive_from_cdrom_drive') as cm: + response = self._config_manager.get_config_drive_files(target) + self.assertEqual(response, cm.return_value) + + +class TestGetConfigDriveFromCdromDrive(TestFreeBSDConfigDriveManager): + + def test_default(self): + target = mock.MagicMock() + response = self._config_manager._get_conf_drive_from_cdrom_drive(target) + self.assertFalse(response) + + def test_retrieves_devices(self): + target = mock.MagicMock() + with mock.patch.object(self._config_manager, '_get_devices') as cm: + response = self._config_manager._get_conf_drive_from_cdrom_drive(target) + cm.assert_called_once_with() + + def test_get_devices(self): + mock_output = 'cd0 vtbd0\n' + with mock.patch('cloudbaseinit.metadata.services.osconfigdrive.freebsd.subprocess') as cm: + cm.check_output.return_value = mock_output + devices = self._config_manager._get_devices() + + expected_devices = ['cd0', 'vtbd0'] + self.assertEqual(devices, expected_devices) + + def test_retrieves_mounts(self): + target = mock.MagicMock() + mocks = { + '_get_devices': mock.MagicMock(return_value=['cd0']), + '_is_cdrom': mock.MagicMock(return_value=True), + '_get_mounts': mock.MagicMock() + } + with mock.patch.multiple(self._config_manager, **mocks): + response = self._config_manager._get_conf_drive_from_cdrom_drive(target) + mocks['_get_mounts'].assert_called_once_with() + + def test_get_mounts(self): + mount_output = ( + '/dev/vtbd0p2 on / (ufs, local, journaled soft-updates)\n' + 'devfs on /dev (devfs, local, multilabel)\n' + '/dev/cd0 on /mnt/cdrom (cd9660, local, read-only)\n' + 'procfs on /proc (procfs, local)\n' + ) + with mock.patch('cloudbaseinit.metadata.services.osconfigdrive.freebsd.subprocess') as cm: + cm.check_output.return_value = mount_output + mounts = self._config_manager._get_mounts() + + expected_mount_info = { + '/dev/cd0': { + 'fstype': 'cd9660', + 'mountpoint': '/mnt/cdrom', + 'opts': 'read-only' + }, + '/dev/vtbd0p2': { + 'fstype': 'ufs', + 'mountpoint': '/', + 'opts': 'journaled soft-updates' + } + } + self.assertEqual(mounts, expected_mount_info) diff --git a/requirements.txt b/requirements.txt index d02b335..cf1e2c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pbr>=0.10 +pbr==0.11.1 iso8601 eventlet netaddr>=0.7.6 @@ -9,4 +9,4 @@ Babel>=1.3 oauthlib netifaces PyYAML -tzlocal \ No newline at end of file +tzlocal