diff options
author | ryanwe <ryanwe@google.com> | 2018-01-30 13:08:12 -0800 |
---|---|---|
committer | Max Illfelder <illfelder@users.noreply.github.com> | 2018-01-30 13:08:12 -0800 |
commit | 1db5329f4a728d1505976d7d87430b9f6da99108 (patch) | |
tree | 5b34dccdb2993c8dcc7f79be854ae4980829cfe6 /google_compute_engine | |
parent | 8ff26d9198e9c60e6c8416e88ce25f19403d99eb (diff) | |
download | google-compute-image-packages-1db5329f4a728d1505976d7d87430b9f6da99108.tar.gz |
Create directories in the Python package for distro-specific logic. (#535)
- Finish creating distro directories.
- Import correct module based on distro type.
- Refactor 'network_setup' to use these new directories.
Diffstat (limited to 'google_compute_engine')
26 files changed, 671 insertions, 177 deletions
diff --git a/google_compute_engine/compat.py b/google_compute_engine/compat.py index 00d9d45..bafb1f7 100644 --- a/google_compute_engine/compat.py +++ b/google_compute_engine/compat.py @@ -16,9 +16,32 @@ """A module for resolving compatibility issues between Python 2 and Python 3.""" import logging +import platform import subprocess import sys +# Set distro-specific utils. +distribution = platform.linux_distribution() +distro_name = distribution[0].lower() +distro_version = distribution[1].split('.')[0] +distro_utils = None + +if 'centos' in distro_name and distro_version == '6': + import google_compute_engine.distro.el_6.utils as distro_utils +elif 'centos' in distro_name and distro_version == '7': + import google_compute_engine.distro.el_7.utils as distro_utils +elif 'red hat enterprise linux' in distro_name and distro_version == '6': + import google_compute_engine.distro.el_6.utils as distro_utils +elif 'red hat enterprise linux' in distro_name and distro_version == '7': + import google_compute_engine.distro.el_7.utils as distro_utils +elif 'debian' in distro_name and distro_version == '8': + import google_compute_engine.distro.debian_8.utils as distro_utils +elif 'debian' in distro_name and distro_version == '9': + import google_compute_engine.distro.debian_9.utils as distro_utils +else: + # Default to Debian 9. + import google_compute_engine.distro.debian_9.utils as distro_utils + RETRY_LIMIT = 3 TIMEOUT = 10 diff --git a/google_compute_engine/distro/__init__.py b/google_compute_engine/distro/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/google_compute_engine/distro/__init__.py diff --git a/google_compute_engine/distro/debian_8/__init__.py b/google_compute_engine/distro/debian_8/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/google_compute_engine/distro/debian_8/__init__.py diff --git a/google_compute_engine/distro/debian_8/tests/__init__.py b/google_compute_engine/distro/debian_8/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/google_compute_engine/distro/debian_8/tests/__init__.py diff --git a/google_compute_engine/distro/debian_8/tests/utils_test.py b/google_compute_engine/distro/debian_8/tests/utils_test.py new file mode 100644 index 0000000..0bb6d39 --- /dev/null +++ b/google_compute_engine/distro/debian_8/tests/utils_test.py @@ -0,0 +1,37 @@ +#!/usr/bin/python +# Copyright 2018 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unittest for utils.py module.""" + +from google_compute_engine.distro.debian_8 import utils +from google_compute_engine.test_compat import mock +from google_compute_engine.test_compat import unittest + + +class UtilsTest(unittest.TestCase): + + def setUp(self): + self.mock_logger = mock.Mock() + self.mock_setup = mock.create_autospec(utils.Utils) + + @mock.patch('google_compute_engine.distro.helpers.CallDhclient') + def testEnableNetworkInterfaces(self, mock_call): + mocks = mock.Mock() + mocks.attach_mock(mock_call, 'call') + + utils.Utils.EnableNetworkInterfaces( + self.mock_setup, ['A', 'B'], self.mock_logger) + expected_calls = [mock.call.call(['A', 'B'], mock.ANY)] + self.assertEqual(mocks.mock_calls, expected_calls) diff --git a/google_compute_engine/distro/debian_8/utils.py b/google_compute_engine/distro/debian_8/utils.py new file mode 100644 index 0000000..b4aade5 --- /dev/null +++ b/google_compute_engine/distro/debian_8/utils.py @@ -0,0 +1,34 @@ +#!/usr/bin/python +# Copyright 2018 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities that are distro specific for use on Debian 8.""" + +from google_compute_engine.distro import helpers +from google_compute_engine.distro import utils + + +class Utils(utils.Utils): + """Utilities used by Linux guest services on Debian 8.""" + + def EnableNetworkInterfaces( + self, interfaces, logger, dhclient_script=None): + """Enable the list of network interfaces. + + Args: + interfaces: list of string, the output device names to enable. + logger: logger object, used to write to SysLog and serial port. + dhclient_script: string, the path to a dhclient script used by dhclient. + """ + helpers.CallDhclient(interfaces, logger) diff --git a/google_compute_engine/distro/debian_9/__init__.py b/google_compute_engine/distro/debian_9/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/google_compute_engine/distro/debian_9/__init__.py diff --git a/google_compute_engine/distro/debian_9/tests/__init__.py b/google_compute_engine/distro/debian_9/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/google_compute_engine/distro/debian_9/tests/__init__.py diff --git a/google_compute_engine/distro/debian_9/tests/utils_test.py b/google_compute_engine/distro/debian_9/tests/utils_test.py new file mode 100644 index 0000000..633ba9b --- /dev/null +++ b/google_compute_engine/distro/debian_9/tests/utils_test.py @@ -0,0 +1,37 @@ +#!/usr/bin/python +# Copyright 2018 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unittest for utils.py module.""" + +from google_compute_engine.distro.debian_9 import utils +from google_compute_engine.test_compat import mock +from google_compute_engine.test_compat import unittest + + +class UtilsTest(unittest.TestCase): + + def setUp(self): + self.mock_logger = mock.Mock() + self.mock_setup = mock.create_autospec(utils.Utils) + + @mock.patch('google_compute_engine.distro.helpers.CallDhclient') + def testEnableNetworkInterfaces(self, mock_call): + mocks = mock.Mock() + mocks.attach_mock(mock_call, 'call') + + utils.Utils.EnableNetworkInterfaces( + self.mock_setup, ['A', 'B'], self.mock_logger) + expected_calls = [mock.call.call(['A', 'B'], mock.ANY)] + self.assertEqual(mocks.mock_calls, expected_calls) diff --git a/google_compute_engine/distro/debian_9/utils.py b/google_compute_engine/distro/debian_9/utils.py new file mode 100644 index 0000000..f235723 --- /dev/null +++ b/google_compute_engine/distro/debian_9/utils.py @@ -0,0 +1,34 @@ +#!/usr/bin/python +# Copyright 2018 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities that are distro specific for use on Debian 9.""" + +from google_compute_engine.distro import helpers +from google_compute_engine.distro import utils + + +class Utils(utils.Utils): + """Utilities used by Linux guest services on Debian 9.""" + + def EnableNetworkInterfaces( + self, interfaces, logger, dhclient_script=None): + """Enable the list of network interfaces. + + Args: + interfaces: list of string, the output device names to enable. + logger: logger object, used to write to SysLog and serial port. + dhclient_script: string, the path to a dhclient script used by dhclient. + """ + helpers.CallDhclient(interfaces, logger) diff --git a/google_compute_engine/distro/el_6/__init__.py b/google_compute_engine/distro/el_6/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/google_compute_engine/distro/el_6/__init__.py diff --git a/google_compute_engine/distro/el_6/tests/__init__.py b/google_compute_engine/distro/el_6/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/google_compute_engine/distro/el_6/tests/__init__.py diff --git a/google_compute_engine/distro/el_6/tests/utils_test.py b/google_compute_engine/distro/el_6/tests/utils_test.py new file mode 100644 index 0000000..363046c --- /dev/null +++ b/google_compute_engine/distro/el_6/tests/utils_test.py @@ -0,0 +1,43 @@ +#!/usr/bin/python +# Copyright 2018 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unittest for utils.py module.""" + +from google_compute_engine.distro.el_6 import utils +from google_compute_engine.test_compat import mock +from google_compute_engine.test_compat import unittest + + +class UtilsTest(unittest.TestCase): + + def setUp(self): + self.mock_logger = mock.Mock() + self.mock_setup = mock.create_autospec(utils.Utils) + + def tearDown(self): + pass + + @mock.patch('google_compute_engine.distro.helpers.CallDhclient') + def testEnableNetworkInterfaces(self, mock_call): + mocks = mock.Mock() + mocks.attach_mock(mock_call, 'call') + + utils.Utils.EnableNetworkInterfaces( + self.mock_setup, ['A', 'B'], self.mock_logger, + dhclient_script='test_script') + expected_calls = [ + mock.call.call(['A', 'B'], mock.ANY, dhclient_script='test_script'), + ] + self.assertEqual(mocks.mock_calls, expected_calls) diff --git a/google_compute_engine/distro/el_6/utils.py b/google_compute_engine/distro/el_6/utils.py new file mode 100644 index 0000000..fc70541 --- /dev/null +++ b/google_compute_engine/distro/el_6/utils.py @@ -0,0 +1,34 @@ +#!/usr/bin/python +# Copyright 2018 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities that are distro specific for use on EL 6.""" + +from google_compute_engine.distro import helpers +from google_compute_engine.distro import utils + + +class Utils(utils.Utils): + """Utilities used by Linux guest services on Debian 8.""" + + def EnableNetworkInterfaces( + self, interfaces, logger, dhclient_script=None): + """Enable the list of network interfaces. + + Args: + interfaces: list of string, the output device names to enable. + logger: logger object, used to write to SysLog and serial port. + dhclient_script: string, the path to a dhclient script used by dhclient. + """ + helpers.CallDhclient(interfaces, logger, dhclient_script=dhclient_script) diff --git a/google_compute_engine/distro/el_7/__init__.py b/google_compute_engine/distro/el_7/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/google_compute_engine/distro/el_7/__init__.py diff --git a/google_compute_engine/distro/el_7/tests/__init__.py b/google_compute_engine/distro/el_7/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/google_compute_engine/distro/el_7/tests/__init__.py diff --git a/google_compute_engine/distro/el_7/tests/utils_test.py b/google_compute_engine/distro/el_7/tests/utils_test.py new file mode 100644 index 0000000..95ef298 --- /dev/null +++ b/google_compute_engine/distro/el_7/tests/utils_test.py @@ -0,0 +1,122 @@ +#!/usr/bin/python +# Copyright 2018 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unittest for utils.py module.""" + +import os +import shutil +import tempfile + +from google_compute_engine.distro.el_7 import utils +from google_compute_engine.test_compat import builtin +from google_compute_engine.test_compat import mock +from google_compute_engine.test_compat import unittest + + +class UtilsTest(unittest.TestCase): + + def setUp(self): + # Create a temporary directory. + self.test_dir = tempfile.mkdtemp() + self.mock_logger = mock.Mock() + self.mock_setup = mock.create_autospec(utils.Utils) + self.mock_setup.network_path = '/etc/sysconfig/network-scripts' + + def tearDown(self): + # Remove the directory after the test. + shutil.rmtree(self.test_dir) + + def testModifyInterface(self): + config_file = os.path.join(self.test_dir, 'config.cfg') + config_content = [ + '# File comment.\n', + 'A="apple"\n', + 'B=banana\n', + 'B=banana\n', + ] + with open(config_file, 'w') as config: + for line in config_content: + config.write(line) + + # Write a value for an existing config without overriding it. + utils.Utils._ModifyInterface( + self.mock_setup, config_file, 'A', 'aardvark', replace=False) + self.assertEquals(open(config_file).readlines(), config_content) + # Write a value for a config that is not already set. + utils.Utils._ModifyInterface( + self.mock_setup, config_file, 'C', 'none', replace=False) + config_content.append('C=none\n') + self.assertEquals(open(config_file).readlines(), config_content) + # Write a value for an existing config with replacement. + utils.Utils._ModifyInterface( + self.mock_setup, config_file, 'A', 'aardvark', replace=True) + config_content[1] = 'A=aardvark\n' + self.assertEquals(open(config_file).readlines(), config_content) + # Write a value for an existing config with multiple occurrences. + utils.Utils._ModifyInterface( + self.mock_setup, config_file, 'B', '"banana"', replace=True) + config_content[2] = config_content[3] = 'B="banana"\n' + self.assertEquals(open(config_file).readlines(), config_content) + + @mock.patch('google_compute_engine.distro.el_7.utils.os.path.exists') + def testDisableNetworkManager(self, mock_exists): + mock_open = mock.mock_open() + mocks = mock.Mock() + mocks.attach_mock(mock_exists, 'exists') + mocks.attach_mock(mock_open, 'open') + mocks.attach_mock(self.mock_logger, 'logger') + mocks.attach_mock(self.mock_setup._ModifyInterface, 'modify') + mock_exists.side_effect = [True, False] + + with mock.patch('%s.open' % builtin, mock_open, create=False): + utils.Utils._DisableNetworkManager( + self.mock_setup, ['eth0', 'eth1'], self.mock_logger) + expected_calls = [ + mock.call.exists('/etc/sysconfig/network-scripts/ifcfg-eth0'), + mock.call.modify(mock.ANY, 'DEVICE', 'eth0', replace=False), + mock.call.modify(mock.ANY, 'NM_CONTROLLED', 'no', replace=True), + mock.call.exists('/etc/sysconfig/network-scripts/ifcfg-eth1'), + mock.call.open('/etc/sysconfig/network-scripts/ifcfg-eth1', 'w'), + mock.call.open().__enter__(), + mock.call.open().write(mock.ANY), + mock.call.open().__exit__(None, None, None), + mock.call.logger.info(mock.ANY, 'eth1'), + ] + self.assertEqual(mocks.mock_calls, expected_calls) + + @mock.patch('google_compute_engine.distro.el_7.utils.os.path.exists') + @mock.patch('google_compute_engine.distro.helpers.CallDhclient') + def testEnableNetworkInterfaces(self, mock_call, mock_exists): + mocks = mock.Mock() + mocks.attach_mock(mock_exists, 'exists') + mocks.attach_mock(self.mock_logger, 'logger') + mocks.attach_mock(self.mock_setup._DisableNetworkManager, 'disable') + mocks.attach_mock(mock_call, 'call_dhclient') + mock_exists.side_effect = [True, False] + + # Enable interfaces with network manager enabled. + utils.Utils.EnableNetworkInterfaces( + self.mock_setup, ['A', 'B'], self.mock_logger) + # Enable interfaces with network manager is not present. + utils.Utils.EnableNetworkInterfaces( + self.mock_setup, ['C', 'D'], self.mock_logger) + expected_calls = [ + mock.call.exists('/etc/sysconfig/network-scripts'), + mock.call.disable(['A', 'B'], mock.ANY), + mock.call.call_dhclient(['A', 'B'], mock.ANY), + mock.call.exists('/etc/sysconfig/network-scripts'), + mock.call.call_dhclient(['C', 'D'], mock.ANY), + ] + self.assertEqual(mocks.mock_calls, expected_calls) diff --git a/google_compute_engine/distro/el_7/utils.py b/google_compute_engine/distro/el_7/utils.py new file mode 100644 index 0000000..a17cf56 --- /dev/null +++ b/google_compute_engine/distro/el_7/utils.py @@ -0,0 +1,91 @@ +#!/usr/bin/python +# Copyright 2018 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities that are distro specific for use on EL 7.""" + +import fileinput +import os +import re + +from google_compute_engine import constants +from google_compute_engine.distro import helpers +from google_compute_engine.distro import utils + + +class Utils(utils.Utils): + """Utilities used by Linux guest services on EL 7.""" + + network_path = constants.LOCALBASE + '/etc/sysconfig/network-scripts' + + def EnableNetworkInterfaces( + self, interfaces, logger, dhclient_script=None): + """Enable the list of network interfaces. + + Args: + interfaces: list of string, the output device names to enable. + logger: logger object, used to write to SysLog and serial port. + dhclient_script: string, the path to a dhclient script used by dhclient. + """ + # Should always exist in EL 7. + if os.path.exists(self.network_path): + self._DisableNetworkManager(interfaces, logger) + helpers.CallDhclient(interfaces, logger) + + def _DisableNetworkManager(self, interfaces, logger): + """Disable network manager management on a list of network interfaces. + + Args: + interfaces: list of string, the output device names enable. + logger: logger object, used to write to SysLog and serial port. + """ + for interface in interfaces: + interface_config = os.path.join(self.network_path, 'ifcfg-%s' % interface) + if os.path.exists(interface_config): + self._ModifyInterface( + interface_config, 'DEVICE', interface, replace=False) + self._ModifyInterface( + interface_config, 'NM_CONTROLLED', 'no', replace=True) + else: + with open(interface_config, 'w') as interface_file: + interface_content = [ + '# Added by Google.', + 'BOOTPROTO=none', + 'DEFROUTE=no', + 'DEVICE=%s' % interface, + 'IPV6INIT=no', + 'NM_CONTROLLED=no', + 'NOZEROCONF=yes', + '', + ] + interface_file.write('\n'.join(interface_content)) + logger.info('Created config file for interface %s.', interface) + + def _ModifyInterface( + self, interface_config, config_key, config_value, replace=False): + """Write a value to a config file if not already present. + + Args: + interface_config: string, the path to a config file. + config_key: string, the configuration key to set. + config_value: string, the value to set for the configuration key. + replace: bool, replace the configuration option if already present. + """ + config_entry = '%s=%s' % (config_key, config_value) + if not open(interface_config).read().count(config_key): + with open(interface_config, 'a') as config: + config.write('%s\n' % config_entry) + elif replace: + for line in fileinput.input(interface_config, inplace=True): + print(re.sub(r'%s=.*' % config_key, config_entry, line.rstrip())) diff --git a/google_compute_engine/distro/helpers.py b/google_compute_engine/distro/helpers.py new file mode 100644 index 0000000..7e7272c --- /dev/null +++ b/google_compute_engine/distro/helpers.py @@ -0,0 +1,42 @@ +#!/usr/bin/python +# Copyright 2018 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Distro helpers.""" + +import os +import subprocess + + +def CallDhclient( + interfaces, logger, dhclient_script=None): + """Configure the network interfaces using dhclient. + + Args: + interfaces: list of string, the output device names to enable. + logger: logger object, used to write to SysLog and serial port. + dhclient_script: string, the path to a dhclient script used by dhclient. + """ + logger.info('Enabling the Ethernet interfaces %s.', interfaces) + + dhclient_command = ['dhclient'] + + if dhclient_script and os.path.exists(dhclient_script): + dhclient_command += ['-sf', dhclient_script] + + try: + subprocess.check_call(dhclient_command + ['-x'] + interfaces) + subprocess.check_call(dhclient_command + interfaces) + except subprocess.CalledProcessError: + logger.warning('Could not enable interfaces %s.', interfaces) diff --git a/google_compute_engine/distro/tests/helpers_test.py b/google_compute_engine/distro/tests/helpers_test.py new file mode 100644 index 0000000..75a366d --- /dev/null +++ b/google_compute_engine/distro/tests/helpers_test.py @@ -0,0 +1,66 @@ +#!/usr/bin/python +# Copyright 2018 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unittest for helpers.py module.""" + +import subprocess + +from google_compute_engine.distro import helpers +from google_compute_engine.test_compat import mock +from google_compute_engine.test_compat import unittest + + +class HelpersTest(unittest.TestCase): + + def setUp(self): + self.mock_logger = mock.Mock() + + @mock.patch('google_compute_engine.distro.helpers.os.path.exists') + @mock.patch('google_compute_engine.distro.helpers.subprocess.check_call') + def testCallDhclient(self, mock_call, mock_exists): + mocks = mock.Mock() + mocks.attach_mock(mock_exists, 'exists') + mocks.attach_mock(mock_call, 'call') + mocks.attach_mock(self.mock_logger, 'logger') + + mock_exists.side_effect = [False, True] + mock_call.side_effect = [ + None, None, None, None, None, None, + subprocess.CalledProcessError(1, 'Test'), + ] + + helpers.CallDhclient(['a', 'b'], self.mock_logger, 'test_script') + helpers.CallDhclient(['c', 'd'], self.mock_logger, 'test_script') + helpers.CallDhclient(['e', 'f'], self.mock_logger, None) + helpers.CallDhclient(['g', 'h'], self.mock_logger, None) + + expected_calls = [ + mock.call.logger.info(mock.ANY, ['a', 'b']), + mock.call.exists('test_script'), + mock.call.call(['dhclient', '-x', 'a', 'b']), + mock.call.call(['dhclient', 'a', 'b']), + mock.call.logger.info(mock.ANY, ['c', 'd']), + mock.call.exists('test_script'), + mock.call.call(['dhclient', '-sf', 'test_script', '-x', 'c', 'd']), + mock.call.call(['dhclient', '-sf', 'test_script', 'c', 'd']), + mock.call.logger.info(mock.ANY, ['e', 'f']), + mock.call.call(['dhclient', '-x', 'e', 'f']), + mock.call.call(['dhclient', 'e', 'f']), + mock.call.logger.info(mock.ANY, ['g', 'h']), + mock.call.call(['dhclient', '-x', 'g', 'h']), + mock.call.logger.warning(mock.ANY, ['g', 'h']), + ] + + self.assertEqual(mocks.mock_calls, expected_calls) diff --git a/google_compute_engine/distro/utils.py b/google_compute_engine/distro/utils.py new file mode 100644 index 0000000..08f856c --- /dev/null +++ b/google_compute_engine/distro/utils.py @@ -0,0 +1,49 @@ +#!/usr/bin/python +# Copyright 2018 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities that are distro specific.""" + +import logging.handlers + +from google_compute_engine import logger as utils_logger +from google_compute_engine import network_utils + + +class Utils(object): + """Utilities used by Linux guest services.""" + + def __init__(self, debug=False, logger=None): + """Constructor. + + Args: + debug: bool, True if debug output should write to the console. + logger: logger object, used to write to SysLog and serial port. + """ + self.debug = debug + facility = logging.handlers.SysLogHandler.LOG_DAEMON + self.logger = logger or utils_logger.Logger( + name='google-utils', debug=self.debug, facility=facility) + self.network_utils = network_utils.NetworkUtils(logger=self.logger) + + def EnableNetworkInterfaces( + self, interfaces, logger, dhclient_script=None): + """Enable the list of network interfaces. + + Args: + interfaces: list of string, the output device names to enable. + logger: logger object, used to write to SysLog and serial port. + dhclient_script: string, the path to a dhclient script used by dhclient. + """ + pass diff --git a/google_compute_engine/network_setup/network_setup.py b/google_compute_engine/network_setup/network_setup.py index 8058458..954fda4 100755 --- a/google_compute_engine/network_setup/network_setup.py +++ b/google_compute_engine/network_setup/network_setup.py @@ -15,11 +15,8 @@ """Enables the network interfaces provided in metadata.""" -import fileinput import logging.handlers import optparse -import os -import re import subprocess from google_compute_engine import config_manager @@ -27,6 +24,7 @@ from google_compute_engine import constants from google_compute_engine import logger from google_compute_engine import metadata_watcher from google_compute_engine import network_utils +from google_compute_engine.compat import distro_utils class NetworkSetup(object): @@ -50,70 +48,9 @@ class NetworkSetup(object): name='network-setup', debug=debug, facility=facility) self.watcher = metadata_watcher.MetadataWatcher(logger=self.logger) self.network_utils = network_utils.NetworkUtils(logger=self.logger) + self.distro_utils = distro_utils.Utils(debug=debug) self._SetupNetworkInterfaces() - def _ModifyInterface( - self, interface_config, config_key, config_value, replace=False): - """Write a value to a config file if not already present. - - Args: - interface_config: string, the path to a config file. - config_key: string, the configuration key to set. - config_value: string, the value to set for the configuration key. - replace: bool, replace the configuration option if already present. - """ - config_entry = '%s=%s' % (config_key, config_value) - if not open(interface_config).read().count(config_key): - with open(interface_config, 'a') as config: - config.write('%s\n' % config_entry) - elif replace: - for line in fileinput.input(interface_config, inplace=True): - print(re.sub(r'%s=.*' % config_key, config_entry, line.rstrip())) - - def _DisableNetworkManager(self, interfaces): - """Disable network manager management on a list of network interfaces. - - Args: - interfaces: list of string, the output device names enable. - """ - for interface in interfaces: - interface_config = os.path.join(self.network_path, 'ifcfg-%s' % interface) - if os.path.exists(interface_config): - self._ModifyInterface( - interface_config, 'DEVICE', interface, replace=False) - self._ModifyInterface( - interface_config, 'NM_CONTROLLED', 'no', replace=True) - else: - with open(interface_config, 'w') as interface_file: - interface_content = [ - '# Added by Google.', - 'BOOTPROTO=none', - 'DEFROUTE=no', - 'DEVICE=%s' % interface, - 'IPV6INIT=no', - 'NM_CONTROLLED=no', - 'NOZEROCONF=yes', - '', - ] - interface_file.write('\n'.join(interface_content)) - self.logger.info('Created config file for interface %s.', interface) - - def _ConfigureNetwork(self, interfaces): - """Enable the list of network interfaces. - - Args: - interfaces: list of string, the output device names enable. - """ - self.logger.info('Enabling the Ethernet interfaces %s.', interfaces) - dhclient_command = ['dhclient'] - if os.path.exists(self.dhclient_script): - dhclient_command += ['-sf', self.dhclient_script] - try: - subprocess.check_call(dhclient_command + ['-x'] + interfaces) - subprocess.check_call(dhclient_command + interfaces) - except subprocess.CalledProcessError: - self.logger.warning('Could not enable interfaces %s.', interfaces) - def _EnableNetworkInterfaces(self, interfaces): """Enable the list of network interfaces. @@ -130,10 +67,11 @@ class NetworkSetup(object): subprocess.check_call([self.dhcp_command]) except subprocess.CalledProcessError: self.logger.warning('Could not enable Ethernet interfaces.') - else: - if os.path.exists(self.network_path): - self._DisableNetworkManager(interfaces) - self._ConfigureNetwork(interfaces) + return + + # Distro-specific setup for network interfaces. + self.distro_utils.EnableNetworkInterfaces( + interfaces, self.logger, dhclient_script=self.dhclient_script) def _SetupNetworkInterfaces(self): """Get network interfaces metadata and enable each Ethernet interface.""" diff --git a/google_compute_engine/network_setup/tests/network_setup_test.py b/google_compute_engine/network_setup/tests/network_setup_test.py index ad6af9c..072dc00 100644 --- a/google_compute_engine/network_setup/tests/network_setup_test.py +++ b/google_compute_engine/network_setup/tests/network_setup_test.py @@ -15,13 +15,11 @@ """Unittest for network_setup.py module.""" -import os import shutil import subprocess import tempfile from google_compute_engine.network_setup import network_setup -from google_compute_engine.test_compat import builtin from google_compute_engine.test_compat import mock from google_compute_engine.test_compat import unittest @@ -37,12 +35,14 @@ class NetworkSetupTest(unittest.TestCase): self.mock_ip_forwarding_utils = mock.Mock() self.mock_network_utils = mock.Mock() self.metadata_key = 'metadata_key' + self.mock_distro_utils = mock.Mock() self.mock_setup = mock.create_autospec(network_setup.NetworkSetup) self.mock_setup.logger = self.mock_logger self.mock_setup.watcher = self.mock_watcher self.mock_setup.network_utils = self.mock_network_utils self.mock_setup.network_interfaces = self.metadata_key + self.mock_setup.distro_utils = self.mock_distro_utils self.mock_setup.network_path = '/etc/sysconfig/network-scripts' self.mock_setup.dhclient_script = '/bin/script' self.mock_setup.dhcp_command = '' @@ -63,6 +63,7 @@ class NetworkSetupTest(unittest.TestCase): mocks.attach_mock(mock_network_utils, 'network') with mock.patch.object( network_setup.NetworkSetup, '_SetupNetworkInterfaces'): + network_setup.NetworkSetup(debug=True) expected_calls = [ mock.call.logger.Logger(name=mock.ANY, debug=True, facility=mock.ANY), @@ -71,107 +72,12 @@ class NetworkSetupTest(unittest.TestCase): ] self.assertEqual(mocks.mock_calls, expected_calls) - def testModifyInterface(self): - config_file = os.path.join(self.test_dir, 'config.cfg') - config_content = [ - '# File comment.\n', - 'A="apple"\n', - 'B=banana\n', - 'B=banana\n', - ] - with open(config_file, 'w') as config: - for line in config_content: - config.write(line) - - # Write a value for an existing config without overriding it. - network_setup.NetworkSetup._ModifyInterface( - self.mock_setup, config_file, 'A', 'aardvark', replace=False) - self.assertEquals(open(config_file).readlines(), config_content) - # Write a value for a config that is not already set. - network_setup.NetworkSetup._ModifyInterface( - self.mock_setup, config_file, 'C', 'none', replace=False) - config_content.append('C=none\n') - self.assertEquals(open(config_file).readlines(), config_content) - # Write a value for an existing config with replacement. - network_setup.NetworkSetup._ModifyInterface( - self.mock_setup, config_file, 'A', 'aardvark', replace=True) - config_content[1] = 'A=aardvark\n' - self.assertEquals(open(config_file).readlines(), config_content) - # Write a value for an existing config with multiple occurrences. - network_setup.NetworkSetup._ModifyInterface( - self.mock_setup, config_file, 'B', '"banana"', replace=True) - config_content[2] = config_content[3] = 'B="banana"\n' - self.assertEquals(open(config_file).readlines(), config_content) - - @mock.patch('google_compute_engine.network_setup.network_setup.os.path.exists') - def testDisableNetworkManager(self, mock_exists): - mock_open = mock.mock_open() - mocks = mock.Mock() - mocks.attach_mock(mock_exists, 'exists') - mocks.attach_mock(mock_open, 'open') - mocks.attach_mock(self.mock_logger, 'logger') - mocks.attach_mock(self.mock_setup._ModifyInterface, 'modify') - mock_exists.side_effect = [True, False] - - with mock.patch('%s.open' % builtin, mock_open, create=False): - network_setup.NetworkSetup._DisableNetworkManager( - self.mock_setup, ['eth0', 'eth1']) - expected_calls = [ - mock.call.exists('/etc/sysconfig/network-scripts/ifcfg-eth0'), - mock.call.modify(mock.ANY, 'DEVICE', 'eth0', replace=False), - mock.call.modify(mock.ANY, 'NM_CONTROLLED', 'no', replace=True), - mock.call.exists('/etc/sysconfig/network-scripts/ifcfg-eth1'), - mock.call.open('/etc/sysconfig/network-scripts/ifcfg-eth1', 'w'), - mock.call.open().__enter__(), - mock.call.open().write(mock.ANY), - mock.call.open().__exit__(None, None, None), - mock.call.logger.info(mock.ANY, 'eth1'), - ] - self.assertEqual(mocks.mock_calls, expected_calls) - @mock.patch('google_compute_engine.network_setup.network_setup.subprocess.check_call') - @mock.patch('google_compute_engine.network_setup.network_setup.os.path.exists') - def testConfigureNetwork(self, mock_exists, mock_call): + def testEnableNetworkInterfaces(self, mock_call): mocks = mock.Mock() - mocks.attach_mock(mock_exists, 'exists') mocks.attach_mock(mock_call, 'call') mocks.attach_mock(self.mock_logger, 'logger') - mock_exists.side_effect = [True, False, False] - mock_call.side_effect = [ - None, None, None, None, subprocess.CalledProcessError(1, 'Test')] - - network_setup.NetworkSetup._ConfigureNetwork(self.mock_setup, ['a', 'b']) - network_setup.NetworkSetup._ConfigureNetwork(self.mock_setup, ['c']) - network_setup.NetworkSetup._ConfigureNetwork(self.mock_setup, []) - expected_calls = [ - # Successfully configure the network using a managed dhclient script. - mock.call.logger.info(mock.ANY, ['a', 'b']), - mock.call.exists('/bin/script'), - mock.call.call(['dhclient', '-sf', '/bin/script', '-x', 'a', 'b']), - mock.call.call(['dhclient', '-sf', '/bin/script', 'a', 'b']), - # Successfully configure the network using the default dhclient script. - mock.call.logger.info(mock.ANY, ['c']), - mock.call.exists('/bin/script'), - mock.call.call(['dhclient', '-x', 'c']), - mock.call.call(['dhclient', 'c']), - # Exception while enabling the network interface. - mock.call.logger.info(mock.ANY, []), - mock.call.exists('/bin/script'), - mock.call.call(['dhclient', '-x']), - mock.call.logger.warning(mock.ANY, []), - ] - self.assertEqual(mocks.mock_calls, expected_calls) - - @mock.patch('google_compute_engine.network_setup.network_setup.subprocess.check_call') - @mock.patch('google_compute_engine.network_setup.network_setup.os.path.exists') - def testEnableNetworkInterfaces(self, mock_exists, mock_call): - mocks = mock.Mock() - mocks.attach_mock(mock_exists, 'exists') - mocks.attach_mock(mock_call, 'call') - mocks.attach_mock(self.mock_logger, 'logger') - mocks.attach_mock(self.mock_setup._DisableNetworkManager, 'disable') - mocks.attach_mock(self.mock_setup._ConfigureNetwork, 'configure') - mock_exists.side_effect = [True, False] + mocks.attach_mock(self.mock_setup.distro_utils.EnableNetworkInterfaces, 'enable') mock_call.side_effect = [None, subprocess.CalledProcessError(1, 'Test')] # Return immediately with fewer than two interfaces. @@ -191,13 +97,10 @@ class NetworkSetupTest(unittest.TestCase): self.mock_setup.dhcp_command = 'failure' network_setup.NetworkSetup._EnableNetworkInterfaces( self.mock_setup, ['G', 'H']) - expected_calls = [ - mock.call.exists('/etc/sysconfig/network-scripts'), - mock.call.disable(['A', 'B']), - mock.call.configure(['A', 'B']), - mock.call.exists('/etc/sysconfig/network-scripts'), - mock.call.configure(['C', 'D']), + # First calls with empty `interfaces` were no-ops. + mock.call.enable(['A', 'B'], mock.ANY, dhclient_script='/bin/script'), + mock.call.enable(['C', 'D'], mock.ANY, dhclient_script='/bin/script'), mock.call.call(['success']), mock.call.call(['failure']), mock.call.logger.warning(mock.ANY), @@ -214,10 +117,10 @@ class NetworkSetupTest(unittest.TestCase): {'mac': '1'}, {'mac': '2'}, {'mac': '3'}, {}] self.mock_network_utils.GetNetworkInterface.side_effect = [ 'eth0', 'eth1', None, None] - with mock.patch.object( network_setup.NetworkSetup, '_EnableNetworkInterfaces'): self.mock_setup.dhcp_command = 'command' + network_setup.NetworkSetup._SetupNetworkInterfaces(self.mock_setup) expected_calls = [ mock.call.watcher.GetMetadata( diff --git a/google_compute_engine/test_compat.py b/google_compute_engine/test_compat.py index 232be4c..71fb12a 100644 --- a/google_compute_engine/test_compat.py +++ b/google_compute_engine/test_compat.py @@ -37,3 +37,9 @@ else: import unittest2 as unittest builtin = 'builtins' if sys.version_info >= (3,) else '__builtin__' + +# Import the reload module to re-import modules for testing compat. +if sys.version_info >= (3, 4): + from importlib import reload as reload_import +else: + from imp import reload as reload_import diff --git a/google_compute_engine/tests/compat_test.py b/google_compute_engine/tests/compat_test.py index a25972f..6f7671d 100644 --- a/google_compute_engine/tests/compat_test.py +++ b/google_compute_engine/tests/compat_test.py @@ -13,11 +13,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Unittest for logger.py module.""" +"""Unittest for compat.py module.""" import sys +import google_compute_engine.compat from google_compute_engine.test_compat import mock +from google_compute_engine.test_compat import reload_import from google_compute_engine.test_compat import unittest from google_compute_engine.test_compat import urlretrieve @@ -66,6 +68,39 @@ class CompatTest(unittest.TestCase): else: pass + @mock.patch('google_compute_engine.compat.platform.linux_distribution') + def testDistroCompat(self, mock_call): + test_cases = { + ('debian', '8.10', ''): + google_compute_engine.distro.debian_8.utils, + ('debian', '9.3', ''): + google_compute_engine.distro.debian_9.utils, + ('SUSE Linux Enterprise Server ', '12', 'x86_64'): + google_compute_engine.distro.debian_9.utils, + ('CentOS Linux', '6.4.3', 'Core'): + google_compute_engine.distro.el_6.utils, + ('CentOS Linux', '7.4.1708', 'Core'): + google_compute_engine.distro.el_7.utils, + ('CentOS Linux', '8.4.3', 'Core'): + google_compute_engine.distro.debian_9.utils, + ('Red Hat Enterprise Linux Server', '6.3.2', ''): + google_compute_engine.distro.el_6.utils, + ('Red Hat Enterprise Linux Server', '7.4', ''): + google_compute_engine.distro.el_7.utils, + ('Red Hat Enterprise Linux Server', '8.5.1', ''): + google_compute_engine.distro.debian_9.utils, + ('', '', ''): + google_compute_engine.distro.debian_9.utils, + ('xxxx', 'xxxx', 'xxxx'): + google_compute_engine.distro.debian_9.utils, + } + + for distro in test_cases: + mock_call.return_value = distro + reload_import(google_compute_engine.compat) + self.assertEqual( + test_cases[distro], google_compute_engine.compat.distro_utils) + if __name__ == '__main__': unittest.main() diff --git a/google_compute_engine/tests/file_utils_test.py b/google_compute_engine/tests/file_utils_test.py index 0d67807..1b170a3 100644 --- a/google_compute_engine/tests/file_utils_test.py +++ b/google_compute_engine/tests/file_utils_test.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Unittest for file_utils_test.py module.""" +"""Unittest for file_utils.py module.""" from google_compute_engine import file_utils from google_compute_engine.test_compat import mock |