From 3d994ff07971b932fd074b6fd00e3011cb40290f Mon Sep 17 00:00:00 2001 From: goodwillcoding Date: Sat, 14 Jun 2014 12:11:34 -0700 Subject: [PATCH 1/4] Create host-only intreface for virtualbox after prompting user --- nixops/backends/virtualbox.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/nixops/backends/virtualbox.py b/nixops/backends/virtualbox.py index f1c28a39d..a0a61c405 100644 --- a/nixops/backends/virtualbox.py +++ b/nixops/backends/virtualbox.py @@ -66,6 +66,7 @@ def get_type(cls): def __init__(self, depl, name, id): MachineState.__init__(self, depl, name, id) self._disk_attached = False + self.vbox_hostonlyif_name = "vboxnet0" @property def resource_id(self): @@ -110,6 +111,18 @@ def _vbox_flag_sataportcount(self): v = self._vbox_version return '--portcount' if (int(v[0]) >= 4 and int(v[1]) >= 3) else '--sataportcount' + @property + def _vbox_hostonlyif_names(self): + ''' + Return a list of names of all vbox hostonly interfaces + (i.e. ['vboxnet0', 'vboxnet1']) + ''' + lines = self._logged_exec( + ["VBoxManage", "list", "hostonlyifs"], + capture_stdout=True, check=False).splitlines() + + return [v[5:].lstrip() for v in lines if v.startswith("Name:")] + def _get_vm_info(self, can_fail=False): '''Return the output of ‘VBoxManage showvminfo’ in a dictionary.''' lines = self._logged_exec( @@ -192,6 +205,10 @@ def _wait_for_ip(self): def create(self, defn, check, allow_reboot, allow_recreate): assert isinstance(defn, VirtualBoxDefinition) + if self.vbox_hostonlyif_name not in self._vbox_hostonlyif_names: + if self.depl.logger.confirm("To control VirtualBox VMs ‘{0}’ Host-Only interface is needed, create one?".format(self.vbox_hostonlyif_name)): + self._logged_exec(["VBoxManage", "hostonlyif", "create"]) + if self.state != self.UP or check: self.check() self.set_common_state(defn) @@ -365,7 +382,7 @@ def create(self, defn, check, allow_reboot, allow_recreate): ["VBoxManage", "modifyvm", self.vm_id, "--memory", defn.memory_size, "--vram", "10", "--nictype1", "virtio", "--nictype2", "virtio", - "--nic2", "hostonly", "--hostonlyadapter2", "vboxnet0", + "--nic2", "hostonly", "--hostonlyadapter2", self.vbox_hostonlyif_name, "--nestedpaging", "off"]) self._headless = defn.headless From d64dbb653847dcbd7b8d72c6f96fa3e2f50c82ce Mon Sep 17 00:00:00 2001 From: goodwillcoding Date: Fri, 20 Jun 2014 18:44:27 -0700 Subject: [PATCH 2/4] Add functionality to ensure the control host-only interface (vboxnet0) with a configured DHCP server exists before proceeding to deploy a virtualbox. Create the interface and the DHCP servers as needed. Added unittests for this functionality --- nixops/backends/tests/__init__.py | 1 + nixops/backends/tests/virtualbox/__init__.py | 1 + .../test_ensure_control_hostonly_interface.py | 594 ++++++++++++++++++ nixops/backends/virtualbox.py | 241 ++++++- 4 files changed, 819 insertions(+), 18 deletions(-) create mode 100644 nixops/backends/tests/__init__.py create mode 100644 nixops/backends/tests/virtualbox/__init__.py create mode 100644 nixops/backends/tests/virtualbox/test_ensure_control_hostonly_interface.py diff --git a/nixops/backends/tests/__init__.py b/nixops/backends/tests/__init__.py new file mode 100644 index 000000000..40a96afc6 --- /dev/null +++ b/nixops/backends/tests/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/nixops/backends/tests/virtualbox/__init__.py b/nixops/backends/tests/virtualbox/__init__.py new file mode 100644 index 000000000..40a96afc6 --- /dev/null +++ b/nixops/backends/tests/virtualbox/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/nixops/backends/tests/virtualbox/test_ensure_control_hostonly_interface.py b/nixops/backends/tests/virtualbox/test_ensure_control_hostonly_interface.py new file mode 100644 index 000000000..9383781b2 --- /dev/null +++ b/nixops/backends/tests/virtualbox/test_ensure_control_hostonly_interface.py @@ -0,0 +1,594 @@ +# -*- coding: utf-8 -*- + +from unittest import TestCase + +from textwrap import dedent + +# --------------------------------------------------------------------------- # +class DummyLogger(object): + + # ....................................................................... # + def __init__(self): + pass + + # ....................................................................... # + def confirm(self, question): + pass + + # ....................................................................... # + def log(self, message): + pass + +# --------------------------------------------------------------------------- # +class DummyDeployment(object): + + # ....................................................................... # + def __init__(self): + self.logger = DummyLogger() + + +# --------------------------------------------------------------------------- # +class TestVirtualBoxBackend_ensure_control_hostonly_interface(TestCase): + + # ....................................................................... # + def _makeOne(self): + + from nixops.backends import MachineState + + # patch MachineState because none of this functionality uses + # MachineState + def dummy_init(obj, depl, name, id): + obj.depl = depl + obj.logger = depl.logger + MachineState.__init__ = dummy_init + + from nixops.backends.virtualbox import VirtualBoxState + + # create the VirtualBoxState instance + self.vbox_state = VirtualBoxState( + depl=DummyDeployment(), + name="test_name", + id="test_id") + + return self.vbox_state + + # ....................................................................... # + def _callFUT(self): + # call function under test, and we do not care about the return + self.vbox_state.ensure_control_hostonly_interface() + + # ....................................................................... # + def test_not_vboxnet0_missing_interface_exception(self): + """ + Control Host-Only interface not vboxnet0, are no interfaces exception + + Testing for: + - VirtualBoxBackendError with the correct error message + """ + + from nixops.backends.virtualbox import VirtualBoxBackendError + + test_hostonlyif_name = "vboxnet1" + + # ~~~~~~~~~~~~~~~~ # + # create test instance + vbox_state = self._makeOne() + + # patch in the non-vboxnet0 interface + vbox_state.vbox_control_hostonlyif_name = test_hostonlyif_name + + # desired exception message + desired_err_msg = "VirtualBox Host-Only Interface {0} does not exist".format(test_hostonlyif_name) + + # ~~~~~~~~~~~~~~~~ # + # setup and patch logged_exec + def logged_exec(command, **kwargs): + + # dummy the return of "VBoxManage list hostonlyifs" that echos + # no interfaces + if command == ["VBoxManage", "list", "hostonlyifs"]: + return "" + + # dummy the return of "VBoxManage list dhcpservers" that echos + # no dhcpservers + if command == ["VBoxManage", "list", "dhcpservers"]: + return "" + vbox_state._logged_exec = logged_exec + + # ~~~~~~~~~~~~~~~~ # + # test that an exception with correct message was raised + self.assertRaisesRegexp( + VirtualBoxBackendError, + desired_err_msg, + self._callFUT) + + # ....................................................................... # + def test_not_vboxnet0_dhcpserver_not_configured_exception(self): + """ + Control Host-Only interface not vboxnet0, DHCP not configured exception + (self.vbox_control_hostonlyif_name is set to vboxne1) + + Testing for: + - VirtualBoxBackendError with the correct error message + """ + + from nixops.backends.virtualbox import VirtualBoxBackendError + + test_hostonlyif_name = "vboxnet1" + + # ~~~~~~~~~~~~~~~~ # + # create test instance + vbox_state = self._makeOne() + + # patch in the non-vboxnet0 interface + vbox_state.vbox_control_hostonlyif_name = test_hostonlyif_name + + # desired exception message + desired_err_msg = "VirtualBox Host-Only Interface {0} does not have a DHCP server attached".format(test_hostonlyif_name) + + # ~~~~~~~~~~~~~~~~ # + # setup and patch logged_exec + def logged_exec(command, **kwargs): + + # dummy the return of "VBoxManage list hostonlyifs" that echos + # only vboxnet1 host-only interface and no vboxnet0 interface + if command == ["VBoxManage", "list", "hostonlyifs"]: + return dedent("""\ + Name: vboxnet1 + GUID: 786f6276-656e-4074-8000-0a0027000000 + DHCP: Disabled + IPAddress: 192.168.56.1 + NetworkMask: 255.255.255.0 + IPV6Address: + IPV6NetworkMaskPrefixLength: 0 + HardwareAddress: 0a:00:27:00:00:00 + MediumType: Ethernet + Status: Down + VBoxNetworkName: HostInterfaceNetworking-vboxnet1 + + """) + + # dummy the return of "VBoxManage list dhcpservers" that has no + # DHCP server for any interfaces + if command == ["VBoxManage", "list", "dhcpservers"]: + return "" + vbox_state._logged_exec = logged_exec + + # ~~~~~~~~~~~~~~~~ # + # test that an exception with correct message was raised + self.assertRaisesRegexp( + VirtualBoxBackendError, + desired_err_msg, + self._callFUT) + + # ....................................................................... # + def test_not_vboxnet0_dhcpserver_disabled_exception(self): + """ + Control Host-Only inteface not vboxnet0, DHCP disabled exception + (self.vbox_control_hostonlyif_name is set to vboxne1) + + Testing for: + - VirtualBoxBackendError with the correct error message + """ + + from nixops.backends.virtualbox import VirtualBoxBackendError + + test_hostonlyif_name = "vboxnet1" + + # ~~~~~~~~~~~~~~~~ # + # create test instance + vbox_state = self._makeOne() + + # patch in the non-vboxnet0 interface + vbox_state.vbox_control_hostonlyif_name = test_hostonlyif_name + + # desired exception returns + desired_err_msg = "VirtualBox Host-Only Interface {0} DHCP server is disabled".format(test_hostonlyif_name) + + # ~~~~~~~~~~~~~~~~ # + # setup and patch logged_exec + def logged_exec(command, **kwargs): + + # dummy the return of "VBoxManage list dhcpservers" that echos + # only vboxnet1 host-only interface and no vboxnet0 interface + if command == ["VBoxManage", "list", "hostonlyifs"]: + return dedent("""\ + Name: vboxnet1 + GUID: 786f6276-656e-4074-8000-0a0027000000 + DHCP: Disabled + IPAddress: 192.168.56.1 + NetworkMask: 255.255.255.0 + IPV6Address: + IPV6NetworkMaskPrefixLength: 0 + HardwareAddress: 0a:00:27:00:00:00 + MediumType: Ethernet + Status: Down + VBoxNetworkName: HostInterfaceNetworking-vboxnet1 + + """) + + # dummy the return of "VBoxManage list dhcpservers" that echos + # the disabled DHCP server for the vboxnet1 + if command == ["VBoxManage", "list", "dhcpservers"]: + return dedent("""\ + NetworkName: HostInterfaceNetworking-vboxnet1 + IP: 0.0.0.0 + NetworkMask: 0.0.0.0 + lowerIPAddress: 0.0.0.0 + upperIPAddress: 0.0.0.0 + Enabled: No + + """) + + vbox_state._logged_exec = logged_exec + + # ~~~~~~~~~~~~~~~~ # + # test that an exception with correct message was raised + self.assertRaisesRegexp( + VirtualBoxBackendError, + desired_err_msg, + self._callFUT) + + # ....................................................................... # + def test_when_vboxnet0_is_missing_create_and_add_dhcpserver(self): + """ + Creating a missing vboxnet0 interface and its DHCP server, test logging + + Testing for: + - User confirmation that we want to create the interface by + regexing the ask message + - creation of the vboxnet0 interface + - configuring of the vboxnet interface with proper settings + - creation for the DHCP server for the vboxnet and configuring + it with the proper settings + + Also testing for logging of the following: + - Control Host-Only Interface Name + - Control Host-Only Interface IP Address + - Control Host-Only Interface Network Mask + - Control Host-Only Interface DHCP Server IP Address + - Control Host-Only Interface DHCP Server Network Mask + - Control Host-Only Interface DHCP Server Lower IP + - Control HostOnly Interface DHCP Server Upper IP + + """ + + # setup the test values + test_hostonlyif_name = "vboxnet0" + test_host_ip4 = "test_host_ip4" + test_host_ip4_netmask = "test_host_ip4_netmask" + test_dhcpserver_ip = "test_dhcpserver_ip" + test_dhcpserver_netmask = "test_dhcpserver_netmask" + test_dhcpserver_lowerip = "test_dhcpserver_lowerip" + test_dhcpserver_upperip = "test_dhcpserver_upperip" + + desired_log_message = dedent("""\ + Control Host-Only Interface Name : {0} + Control Host-Only Interface IP Address : {1} + Control Host-Only Interface Network Mask : {2} + Control Host-Only Interface DHCP Server IP Address : {3} + Control Host-Only Interface DHCP Server Network Mask: {4} + Control Host-Only Interface DHCP Server Lower IP : {5} + Control Host-Only Interface DHCP Server Upper IP : {6} + """).format( + test_hostonlyif_name, + test_host_ip4, + test_host_ip4_netmask, + test_dhcpserver_ip, + test_dhcpserver_netmask, + test_dhcpserver_lowerip, + test_dhcpserver_upperip).rstrip() + + + # ~~~~~~~~~~~~~~~~ # + # create test instance + vbox_state = self._makeOne() + + # patch in host ip and dhcp server ip settings + vbox_state.vbox_control_hostonlyif_name = test_hostonlyif_name + vbox_state.vbox_control_host_ip4 = test_host_ip4 + vbox_state.vbox_control_host_ip4_netmask = test_host_ip4_netmask + vbox_state.vbox_control_dhcpserver_ip = test_dhcpserver_ip + vbox_state.vbox_control_dhcpserver_netmask = test_dhcpserver_netmask + vbox_state.vbox_control_dhcpserver_lowerip = test_dhcpserver_lowerip + vbox_state.vbox_control_dhcpserver_upperip = test_dhcpserver_upperip + + # ~~~~~~~~~~~~~~~~ # + # setup all the test values to False + test_results = { + "confirm_interface_create_ask_prompt": False, + "confirm_create_command": False, + "confirm_ipconfig_command": False, + "confirm_dhcpserver_add_command": False, + "returned_log_message": [], + } + + # ~~~~~~~~~~~~~~~~ # + # setup and patch logged_exec + def logged_exec(command, **kwargs): + + # dummy the return of "VBoxManage list hostonlyifs" that echos + # no interfaces + if command == ["VBoxManage", "list", "hostonlyifs"]: + return "" + + # dummy the return of "VBoxManage list dhcpsservers" that echos + # no DHCP servers + if command == ["VBoxManage", "list", "dhcpservers"]: + return "" + + # record the call for creation virtualbox hostonlyif interface + if command == ["VBoxManage", "hostonlyif", "create"]: + test_results["confirm_create_command"] = True + return True + + # record the call for for virtualbox hostonly ipconfig + if command == ["VBoxManage", "hostonlyif", "ipconfig", + test_hostonlyif_name, + "--ip", test_host_ip4, + "--netmask", test_host_ip4_netmask]: + test_results["confirm_ipconfig_command"] = True + return True + + # record the call for for virtualbox dhcpserver additions + if command == ["VBoxManage", "dhcpserver", "add", + "--ifname", test_hostonlyif_name, + "--ip", test_dhcpserver_ip, + "--netmask", test_dhcpserver_netmask, + "--lowerip", test_dhcpserver_lowerip, + "--upperip", test_dhcpserver_upperip, + "--enable"]: + test_results["confirm_dhcpserver_add_command"] = True + return True + vbox_state._logged_exec = logged_exec + + # ~~~~~~~~~~~~~~~~ # + # patch .deploy.confirm method for deployment logger to + # says 'yes' when asked to create host-only interface + def logger_confirm_yes(question): + msg = "To control VirtualBox VMs ‘{0}’ Host-Only interface is "\ + "needed, create one?".format(test_hostonlyif_name) + if question == msg: + test_results["confirm_interface_create_ask_prompt"] = True + return True + return False + vbox_state.depl.logger.confirm = logger_confirm_yes + + + # ~~~~~~~~~~~~~~~~ # + # patch .log method for the state to test the logging statements + # recording all information about + def log_return(msg): + test_results["returned_log_message"].append(msg) + vbox_state.log = log_return + + # ~~~~~~~~~~~~~~~~ # + # call function under test # + self._callFUT() + + # ~~~~~~~~~~~~~~~~ # + # test that ask prompt, create, ipconfiog and DHCP server add commands + # ran successfully + self.assertTrue(test_results["confirm_interface_create_ask_prompt"]) + self.assertTrue(test_results["confirm_create_command"]) + self.assertTrue(test_results["confirm_ipconfig_command"]) + self.assertTrue(test_results["confirm_dhcpserver_add_command"]) + + # ~~~~~~~~~~~~~~~~ # + + print "---" + print '\n'.join(test_results["returned_log_message"]) + print "---" + print desired_log_message + print "---" + + # test logging output + self.assertEquals(desired_log_message, '\n'.join(test_results["returned_log_message"])) + + + # ....................................................................... # + def test_enabling_dhcpserver_on_existing_vboxnet0(self): + """ + Enable an existing DHCP server on an existing vboxnet0 + + Testing for: + - User confirmation that we want to have the DHCP server enabled + and configured with our settings + - enabling a DHCP server for the vboxnet and configuring + it with the proper settings + """ + + # setup the test values + test_hostonlyif_name = "vboxnet0" + test_dhcpserver_ip = "test_dhcpserver_ip" + test_dhcpserver_netmask = "test_dhcpserver_netmask" + test_dhcpserver_lowerip = "test_dhcpserver_lowerip" + test_dhcpserver_upperip = "test_dhcpserver_upperip" + + # ~~~~~~~~~~~~~~~~ # + # create test instance + vbox_state = self._makeOne() + + # patch in host ip and dhcp server ip settings + vbox_state.vbox_control_dhcpserver_ip = test_dhcpserver_ip + vbox_state.vbox_control_dhcpserver_netmask = test_dhcpserver_netmask + vbox_state.vbox_control_dhcpserver_lowerip = test_dhcpserver_lowerip + vbox_state.vbox_control_dhcpserver_upperip = test_dhcpserver_upperip + + # ~~~~~~~~~~~~~~~~ # + # setup all the test values to False + test_results = { + "confirm_enable_dhcpserver_ask_prompt": False, + "confirm_dhcpserver_add_command": False, + } + + # ~~~~~~~~~~~~~~~~ # + # setup and patch logged_exec + def logged_exec(command, **kwargs): + + # dummy the return of "VBoxManage list hostonlyifs" that echos + # a vboxnet0 interface + if command == ["VBoxManage", "list", "hostonlyifs"]: + return dedent("""\ + Name: vboxnet0 + GUID: 786f6276-656e-4074-8000-0a0027000000 + DHCP: Disabled + IPAddress: 192.168.56.1 + NetworkMask: 255.255.255.0 + IPV6Address: + IPV6NetworkMaskPrefixLength: 0 + HardwareAddress: 0a:00:27:00:00:00 + MediumType: Ethernet + Status: Down + VBoxNetworkName: HostInterfaceNetworking-vboxnet0 + + """) + + # dummy the return of "VBoxManage list dhcpsservers" that echos + # no DHCP servers + if command == ["VBoxManage", "list", "dhcpservers"]: + return "" + + # record the call for for virtualbox dhcpserver additions + if command == ["VBoxManage", "dhcpserver", "add", + "--ifname", test_hostonlyif_name, + "--ip", test_dhcpserver_ip, + "--netmask", test_dhcpserver_netmask, + "--lowerip", test_dhcpserver_lowerip, + "--upperip", test_dhcpserver_upperip, + "--enable"]: + test_results["confirm_dhcpserver_add_command"] = True + return True + vbox_state._logged_exec = logged_exec + + # ~~~~~~~~~~~~~~~~ # + # patch confirm method always says yes when asked enable and configure + # the DHCP server + def logger_confirm_yes(question): + msg = "To control VirtualBox VMs ‘{0}’ Host-Only interface "\ + "needs to have DHCP Server enabled and it settings "\ + "configured. This may potentially override your previous "\ + "VirtualBox setup. Continue?"\ + .format(test_hostonlyif_name) + if question == msg: + test_results["confirm_enable_dhcpserver_ask_prompt"] = True + return True + return False + vbox_state.depl.logger.confirm = logger_confirm_yes + + # ~~~~~~~~~~~~~~~~ # + # call function under test # + self._callFUT() + + # ~~~~~~~~~~~~~~~~ # + # test that we asked for a prompts to enable and configure DHCP server + self.assertTrue(test_results["confirm_enable_dhcpserver_ask_prompt"]) + # test that we issued DHCP server add command + self.assertTrue(test_results["confirm_dhcpserver_add_command"]) + + # ....................................................................... # + def test_adding_dhcpserver_to_existing_vboxnet0(self): + """ + Adding a missing DHCP server to vboxnet0 + + Testing for: + - User confirmation that we want to have the DHCP server enabled + and configured with our settings + - creation for the DHCP server for the vboxnet and configuring + it with the proper settings + """ + + # setup the test values + test_hostonlyif_name = "vboxnet0" + test_dhcpserver_ip = "test_dhcpserver_ip" + test_dhcpserver_netmask = "test_dhcpserver_netmask" + test_dhcpserver_lowerip = "test_dhcpserver_lowerip" + test_dhcpserver_upperip = "test_dhcpserver_upperip" + + # ~~~~~~~~~~~~~~~~ # + # create test instance + vbox_state = self._makeOne() + + # patch in host ip and dhcp server ip settings + vbox_state.vbox_control_dhcpserver_ip = test_dhcpserver_ip + vbox_state.vbox_control_dhcpserver_netmask = test_dhcpserver_netmask + vbox_state.vbox_control_dhcpserver_lowerip = test_dhcpserver_lowerip + vbox_state.vbox_control_dhcpserver_upperip = test_dhcpserver_upperip + + # ~~~~~~~~~~~~~~~~ # + # setup all the test values to False + test_results = { + "confirm_enable_dhcpserver_ask_prompt": False, + "confirm_dhcpserver_modify_command": False, + } + + # ~~~~~~~~~~~~~~~~ # + # setup and patch logged_exec + def logged_exec(command, **kwargs): + + # dummy the return of "VBoxManage list hostonlyifs" that echos + # a vboxnet0 interface + if command == ["VBoxManage", "list", "hostonlyifs"]: + return dedent("""\ + Name: vboxnet0 + GUID: 786f6276-656e-4074-8000-0a0027000000 + DHCP: Disabled + IPAddress: 192.168.56.1 + NetworkMask: 255.255.255.0 + IPV6Address: + IPV6NetworkMaskPrefixLength: 0 + HardwareAddress: 0a:00:27:00:00:00 + MediumType: Ethernet + Status: Down + VBoxNetworkName: HostInterfaceNetworking-vboxnet0 + + """) + + # dummy the return of "VBoxManage list dhcpsservers" that echos + # no DHCP servers + if command == ["VBoxManage", "list", "dhcpservers"]: + return dedent("""\ + NetworkName: HostInterfaceNetworking-vboxnet0 + IP: 192.168.56.100 + NetworkMask: 255.255.255.0 + lowerIPAddress: 192.168.56.101 + upperIPAddress: 192.168.56.254 + Enabled: No + + """) + + # record the call for for virtualbox dhcpserver additions + if command == ["VBoxManage", "dhcpserver", "modify", + "--ifname", test_hostonlyif_name, + "--ip", test_dhcpserver_ip, + "--netmask", test_dhcpserver_netmask, + "--lowerip", test_dhcpserver_lowerip, + "--upperip", test_dhcpserver_upperip, + "--enable"]: + test_results["confirm_dhcpserver_modify_command"] = True + return True + vbox_state._logged_exec = logged_exec + + # ~~~~~~~~~~~~~~~~ # + # patch confirm always says yes + def logger_confirm_yes(question): + msg = "To control VirtualBox VMs ‘{0}’ Host-Only interface "\ + "needs to have DHCP Server enabled and it settings "\ + "configured. This may potentially override your previous "\ + "VirtualBox setup. Continue?"\ + .format(test_hostonlyif_name) + if question == msg: + test_results["confirm_enable_dhcpserver_ask_prompt"] = True + return True + return False + vbox_state.depl.logger.confirm = logger_confirm_yes + + # ~~~~~~~~~~~~~~~~ # + # call function under test # + self._callFUT() + + # ~~~~~~~~~~~~~~~~ # + # test that we asked for a prompts to enable and configure DHCP server + self.assertTrue(test_results["confirm_enable_dhcpserver_ask_prompt"]) + # test that we issued DHCP server modify command + self.assertTrue(test_results["confirm_dhcpserver_modify_command"]) diff --git a/nixops/backends/virtualbox.py b/nixops/backends/virtualbox.py index a0a61c405..34d3d5596 100644 --- a/nixops/backends/virtualbox.py +++ b/nixops/backends/virtualbox.py @@ -12,6 +12,8 @@ sata_ports = 8 +class VirtualBoxBackendError(Exception): + pass class VirtualBoxDefinition(MachineDefinition): """Definition of a VirtualBox machine.""" @@ -66,7 +68,20 @@ def get_type(cls): def __init__(self, depl, name, id): MachineState.__init__(self, depl, name, id) self._disk_attached = False - self.vbox_hostonlyif_name = "vboxnet0" + + # host only interface and its DHCP server settings + # =================================================================== # + # VERY IMPORTANT: + # before you change vbox_control_hostonlyif_name PLEASE read the + # warning in _ensure_control_hostonly_interface method + # =================================================================== # + self.vbox_control_hostonlyif_name = "vboxnet0" + self.vbox_control_host_ip4 = "192.168.56.1" + self.vbox_control_host_ip4_netmask = "255.255.255.0" + self.vbox_control_dhcpserver_ip = "192.168.56.100" + self.vbox_control_dhcpserver_netmask = "255.255.255.0" + self.vbox_control_dhcpserver_lowerip = "192.168.56.101" + self.vbox_control_dhcpserver_upperip = "192.168.56.254" @property def resource_id(self): @@ -111,18 +126,6 @@ def _vbox_flag_sataportcount(self): v = self._vbox_version return '--portcount' if (int(v[0]) >= 4 and int(v[1]) >= 3) else '--sataportcount' - @property - def _vbox_hostonlyif_names(self): - ''' - Return a list of names of all vbox hostonly interfaces - (i.e. ['vboxnet0', 'vboxnet1']) - ''' - lines = self._logged_exec( - ["VBoxManage", "list", "hostonlyifs"], - capture_stdout=True, check=False).splitlines() - - return [v[5:].lstrip() for v in lines if v.startswith("Name:")] - def _get_vm_info(self, can_fail=False): '''Return the output of ‘VBoxManage showvminfo’ in a dictionary.''' lines = self._logged_exec( @@ -190,6 +193,208 @@ def _update_shared_folder(self, name, state): shared_folders[name] = state self.shared_folders = shared_folders + def _parse_output_to_dict(self, lines, key_name): + groups = {} + + group = {} + for line in lines: + if line != '': + key, dirty_value = line.split(":", 1) + value = dirty_value.lstrip() + group[key] = value + else: + groups[group[key_name]] = group + group = {} + return groups + + def ensure_control_hostonly_interface(self): + ''' + Check for and if necessary create the control host-only interface + necessary for communication to the VM. Also, if needed, configure the + interface to have a DHCP Server with settings. + + .. warning :: + + WARNING! WARNING! DANGER! DANGER!: + Only create one interface and the number for it based on the + previous number of existing interfaces due to how VirtualBox works + limitations. As such the only reason this works is because the + self.vbox_control_hostonlyif_name is hard-coded to vboxnet0 which is the + interface which will be created due to the fact that VirtualBox + creates these interfaces starting from the top and filling in any + missing ones (i.e if you have vboxnet1, but not not vboxnet0, + vboxnet0 will be created). THis is why we all only create an + interface if self.vbox_control_hostonlyif_name is set to 'vboxnet0' + ''' + + hostonlyifs = self._vbox_get_hostonly_interfaces() + + # sanity checks + if self.vbox_control_hostonlyif_name != "vboxnet0": + if self.vbox_control_hostonlyif_name not in hostonlyifs: + # host-only interface does not exist + raise VirtualBoxBackendError("VirtualBox Host-Only Interface {0} does not exist".format(self.vbox_control_hostonlyif_name) ) + else: + dhcp = hostonlyifs[self.vbox_control_hostonlyif_name]["_dhcp"] + if len(dhcp) == 0: + # no DHCP server assigned + raise VirtualBoxBackendError("VirtualBox Host-Only Interface {0} does not have a DHCP server attached".format(self.vbox_control_hostonlyif_name) ) + elif dhcp["Enabled"] == 'No': + # DHCP server not enabled + raise VirtualBoxBackendError("VirtualBox Host-Only Interface {0} DHCP server is disabled".format(self.vbox_control_hostonlyif_name) ) + + # if control host-only interfaces is 'vboxnet0' + if self.vbox_control_hostonlyif_name == "vboxnet0": + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # + # if interface does not exits create it + if self.vbox_control_hostonlyif_name not in hostonlyifs: + # ask user to confirm creation + msg = "To control VirtualBox VMs ‘{0}’ Host-Only interface is "\ + "needed, create one?".format(self.vbox_control_hostonlyif_name) + if self.depl.logger.confirm(msg): + # create host-only interface and setup DHCP server for it. + self._vbox_create_hostonly_interface() + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # + # if interface exist, check if we need to add or enable the DHCP + # server (and configure default settings for it) + else: + + # get the DHCP settings + dhcp_settings = hostonlyifs[self.vbox_control_hostonlyif_name]["_dhcp"] + + action = None + # decide if we need to add the server depending on whether + # it is missing or disabled. If the server is enabled, + # do nothing + if len(dhcp_settings) == 0: + action = "add" + elif dhcp_settings["Enabled"] == 'No': + action = "modify" + + if action is not None: + msg = "To control VirtualBox VMs ‘{0}’ Host-Only interface "\ + "needs to have DHCP Server enabled and it settings "\ + "configured. This may potentially override your previous "\ + "VirtualBox setup. Continue?"\ + .format(self.vbox_control_hostonlyif_name) + if self.depl.logger.confirm(msg): + self._vbox_hostonly_interface_setup_dhcpserver(action=action) + + self.log("Control Host-Only Interface Name : {0}"\ + .format(self.vbox_control_hostonlyif_name)) + self.log("Control Host-Only Interface IP Address : {0}"\ + .format(self.vbox_control_host_ip4)) + self.log("Control Host-Only Interface Network Mask : {0}"\ + .format(self.vbox_control_host_ip4_netmask)) + self.log("Control Host-Only Interface DHCP Server IP Address : {0}"\ + .format(self.vbox_control_dhcpserver_ip)) + self.log("Control Host-Only Interface DHCP Server Network Mask: {0}"\ + .format(self.vbox_control_dhcpserver_netmask)) + self.log("Control Host-Only Interface DHCP Server Lower IP : {0}"\ + .format(self.vbox_control_dhcpserver_lowerip)) + self.log("Control Host-Only Interface DHCP Server Upper IP : {0}"\ + .format(self.vbox_control_dhcpserver_upperip)) + + def _vbox_get_hostonly_interfaces(self): + ''' + Return a dictionary of Host-Only interfaces and their settings and + their associated DHCP servers settings in a '_dhcp' key. + + Note: + "VBoxManage list hostonlyifs" does return a "DHCP" setting, + but it appears to always be "Disabled", maybe its a status? + + :raise: CommandFailed exception from logged_exec + + :return: + Example: + {'vboxnet0': {'DHCP': 'Disabled', + 'GUID': '786f6276-656e-4074-8000-0a0027000000', + 'HardwareAddress': '0a:00:27:00:00:00', + 'IPAddress': '192.168.56.1', + 'IPV6Address': 'fe80:0000:0000:0000:0800:27ff:fe00:0000', + 'IPV6NetworkMaskPrefixLength': '64', + 'MediumType': 'Ethernet', + 'Name': 'vboxnet0', + 'NetworkMask': '255.255.255.0', + 'Status': 'Up', + 'VBoxNetworkName': 'HostInterfaceNetworking-vboxnet0', + '_dhcp': {'Enabled': 'Yes', + 'IP': '192.168.56.100', + 'NetworkMask': '255.255.255.0', + 'NetworkName': 'HostInterfaceNetworking-vboxnet0', + 'lowerIPAddress': '192.168.56.101', + 'upperIPAddress': '192.168.56.254'}}} + ''' + + # get host-only interfaces and parse them to dict. + # Key for each entry is the value found in the 'Name' + lines = self._logged_exec( + ["VBoxManage", "list", "hostonlyifs"], + capture_stdout=True, check=False).splitlines() + hostonlyifs = self._parse_output_to_dict(lines, "Name") + + # get all DHCP servers and parse them to dict + # Key for each entry is the value found in the 'NetworkName' + # Note: 'NetworkName' is the same as hostonlyifs 'VBoxNetworkName' + lines = self._logged_exec( + ["VBoxManage", "list", "dhcpservers"], + capture_stdout=True, check=False).splitlines() + dhcpservers = self._parse_output_to_dict(lines, "NetworkName") + + # set '_dhcp' key to the the dhcp server dictionary for each + # host-only interface. if no dhcp server is found the set an + # empty dictionary + for if_name in hostonlyifs.keys(): + network_name = hostonlyifs[if_name]["VBoxNetworkName"] + if network_name in dhcpservers: + hostonlyifs[if_name]["_dhcp"] = dhcpservers[network_name] + else: + hostonlyifs[if_name]["_dhcp"] = {} + + return hostonlyifs + + def _vbox_create_hostonly_interface(self): + ''' + Create a control host-only interface and add a DHCP server configured + settings. + ''' + + # create inteface + self._logged_exec(["VBoxManage", "hostonlyif", "create"]) + # configure ip for the interface + self._logged_exec( + ["VBoxManage", "hostonlyif", "ipconfig", + self.vbox_control_hostonlyif_name, + "--ip", self.vbox_control_host_ip4, + "--netmask", self.vbox_control_host_ip4_netmask, + ]) + # add DHCP server to the intefaces + self._vbox_hostonly_interface_setup_dhcpserver(action="add") + + def _vbox_hostonly_interface_setup_dhcpserver(self, action): + ''' + Add or Modify DHCP server for the control hostonly interface. + + :param action: type of actions to give to "VBoxManage dhcpserver. + only 'add' or 'modify' actions are allowed. + ''' + assert action in ['add', 'modify'] + + # either add or modify the DHCP server for the control host-only + # interface, configure DHCP server ip, netmask, lower and upper + # boundries and enabled it. + self._logged_exec( + ["VBoxManage", "dhcpserver", action, + "--ifname", self.vbox_control_hostonlyif_name, + "--ip", self.vbox_control_dhcpserver_ip, + "--netmask", self.vbox_control_dhcpserver_netmask, + "--lowerip", self.vbox_control_dhcpserver_lowerip, + "--upperip", self.vbox_control_dhcpserver_upperip, + "--enable", + ]) def _wait_for_ip(self): self.log_start("waiting for IP address...") @@ -201,13 +406,12 @@ def _wait_for_ip(self): self.log_end(" " + self.private_ipv4) nixops.known_hosts.remove(self.private_ipv4) - def create(self, defn, check, allow_reboot, allow_recreate): assert isinstance(defn, VirtualBoxDefinition) - if self.vbox_hostonlyif_name not in self._vbox_hostonlyif_names: - if self.depl.logger.confirm("To control VirtualBox VMs ‘{0}’ Host-Only interface is needed, create one?".format(self.vbox_hostonlyif_name)): - self._logged_exec(["VBoxManage", "hostonlyif", "create"]) + self.ensure_control_hostonly_interface() + # debug + return False if self.state != self.UP or check: self.check() @@ -382,7 +586,8 @@ def create(self, defn, check, allow_reboot, allow_recreate): ["VBoxManage", "modifyvm", self.vm_id, "--memory", defn.memory_size, "--vram", "10", "--nictype1", "virtio", "--nictype2", "virtio", - "--nic2", "hostonly", "--hostonlyadapter2", self.vbox_hostonlyif_name, + "--nic2", "hostonly", + "--hostonlyadapter2",self.vbox_control_hostonlyif_name, "--nestedpaging", "off"]) self._headless = defn.headless From 06aea1fc9b3d09c8a535634d042fdbd64b76d375 Mon Sep 17 00:00:00 2001 From: goodwillcoding Date: Fri, 20 Jun 2014 18:55:19 -0700 Subject: [PATCH 3/4] clean out debug statement --- nixops/backends/virtualbox.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nixops/backends/virtualbox.py b/nixops/backends/virtualbox.py index 34d3d5596..b69eaf949 100644 --- a/nixops/backends/virtualbox.py +++ b/nixops/backends/virtualbox.py @@ -409,9 +409,8 @@ def _wait_for_ip(self): def create(self, defn, check, allow_reboot, allow_recreate): assert isinstance(defn, VirtualBoxDefinition) + # ensure the control host-only interface exists and has a DHCP server self.ensure_control_hostonly_interface() - # debug - return False if self.state != self.UP or check: self.check() From b84db4e0296fcaebf7b74c31ee9d735dcb545ae9 Mon Sep 17 00:00:00 2001 From: goodwillcoding Date: Sat, 21 Jun 2014 01:16:21 -0700 Subject: [PATCH 4/4] small fixes based on PR feedback --- nixops/backends/virtualbox.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/nixops/backends/virtualbox.py b/nixops/backends/virtualbox.py index b69eaf949..f3b514482 100644 --- a/nixops/backends/virtualbox.py +++ b/nixops/backends/virtualbox.py @@ -73,7 +73,7 @@ def __init__(self, depl, name, id): # =================================================================== # # VERY IMPORTANT: # before you change vbox_control_hostonlyif_name PLEASE read the - # warning in _ensure_control_hostonly_interface method + # warning in ensure_control_hostonly_interface method # =================================================================== # self.vbox_control_hostonlyif_name = "vboxnet0" self.vbox_control_host_ip4 = "192.168.56.1" @@ -215,16 +215,19 @@ def ensure_control_hostonly_interface(self): .. warning :: - WARNING! WARNING! DANGER! DANGER!: - Only create one interface and the number for it based on the - previous number of existing interfaces due to how VirtualBox works - limitations. As such the only reason this works is because the - self.vbox_control_hostonlyif_name is hard-coded to vboxnet0 which is the - interface which will be created due to the fact that VirtualBox - creates these interfaces starting from the top and filling in any - missing ones (i.e if you have vboxnet1, but not not vboxnet0, - vboxnet0 will be created). THis is why we all only create an - interface if self.vbox_control_hostonlyif_name is set to 'vboxnet0' + WARNING! WARNING! DANGER! DANGER!: Only create one interface since + the number for it based on the previous number of existing + interfaces due to how VirtualBox works. As such the only reason + this works is because the self.vbox_control_hostonlyif_name is + hard-coded to vboxnet0 which is the interface which will be created + due to the fact that VirtualBox creates these interfaces starting + from the top and filling in any missing ones (i.e if you have + vboxnet1, but not not vboxnet0, vboxnet0 will be created). This is + why we all only create an interface if + self.vbox_control_hostonlyif_name is set to 'vboxnet0' + + Eventually, there will be support to pass in the interface using + command line. ''' hostonlyifs = self._vbox_get_hostonly_interfaces() @@ -251,7 +254,7 @@ def ensure_control_hostonly_interface(self): if self.vbox_control_hostonlyif_name not in hostonlyifs: # ask user to confirm creation msg = "To control VirtualBox VMs ‘{0}’ Host-Only interface is "\ - "needed, create one?".format(self.vbox_control_hostonlyif_name) + "needed, create one?".format(self.vbox_control_hostonlyif_name) if self.depl.logger.confirm(msg): # create host-only interface and setup DHCP server for it. self._vbox_create_hostonly_interface() @@ -275,9 +278,9 @@ def ensure_control_hostonly_interface(self): if action is not None: msg = "To control VirtualBox VMs ‘{0}’ Host-Only interface "\ - "needs to have DHCP Server enabled and it settings "\ - "configured. This may potentially override your previous "\ - "VirtualBox setup. Continue?"\ + "needs to have DHCP Server enabled and it settings "\ + "configured. This may potentially override your previous "\ + "VirtualBox setup. Continue?"\ .format(self.vbox_control_hostonlyif_name) if self.depl.logger.confirm(msg): self._vbox_hostonly_interface_setup_dhcpserver(action=action)