def test_show_images(self, mock_expectmore, input_file, test_file): """Expect this to try to list the current and available firmware images.""" test_switch = SwitchMellanoxM7800(switch_ip_address='fakeip', password='******') test_console_response = Path( test_file(input_file)).read_text().splitlines() expected_data = ( { 1: 'X86_64 3.6.4006 2017-07-03 16:17:39 x86_64', 2: 'X86_64 3.6.4006 2017-07-03 16:17:39 x86_64', }, 2, 1, [ ('image-X86_64-3.6.2002.img', 'X86_64 3.6.2002 2016-09-28 21:00:15 x86_64'), ('image-X86_64-3.6.3004.img', 'X86_64 3.6.3004 2017-02-05 17:31:53 x86_64'), ('image-X86_64-3.6.4006.img', 'X86_64 3.6.4006 2017-07-03 16:17:39 x86_64'), ('image-X86_64-3.6.5009.img', 'X86_64 3.6.5009 2018-01-02 07:42:21 x86_64'), ('image-X86_64-3.6.8010.img', 'X86_64 3.6.8010 2018-08-20 18:04:19 x86_64'), ], ) mock_expectmore.return_value.ask.return_value = test_console_response assert (test_switch.show_images() == expected_data) mock_expectmore.return_value.ask.assert_called_with('show images')
def test_show_images_no_data(self, mock_expectmore): """Test that an error is raised when nothing is returned from the switch.""" mock_expectmore.return_value.ask.return_value = [] test_switch = SwitchMellanoxM7800(switch_ip_address='fakeip', password='******') with pytest.raises(SwitchException): test_switch.show_images()
def test_write_configuration_errors(self, mock__get_errors, mock_expectmore): """Expect that a fallback reboot command raises an exception on errors.""" mock__get_errors.return_value = ['a returned error message'] test_switch = SwitchMellanoxM7800(switch_ip_address = 'fakeip', password = '******') with pytest.raises(SwitchException): test_switch.write_configuration()
def test_connect_password_auth_factory_fallback(self, mock_expectmore): """Expect connect to authenticate using the default password and set up for factory default login if the user specified one doesn't work. """ mock_expectmore.return_value.isalive.return_value = False # perform password auth mock_expectmore.return_value.match_index = 0 # set the first say to raise ExpectMoreException and the second to work mock_expectmore.return_value.say.side_effect = ( ExpectMoreException, DEFAULT, ) test_switch = SwitchMellanoxM7800(switch_ip_address = 'fakeip', password = '******') test_switch.connect() # expect the default password to be used mock_expectmore.return_value.say.assert_called_with('admin') # expect the extra factory default step to be prepended to the login sequence mock_expectmore.return_value.conversation.assert_called_with( [ ('', 'no'), ([' >', ''], 'terminal length 999'), (' >', 'enable'), (' #', 'configure terminal'), ('.config. #', ''), ] )
def test__get_boot_partitions(self, mock_expectmore, input_file, test_file): """Test that partitions are found when present.""" test_console_response = Path(test_file(input_file)).read_text().splitlines() expected_data = (2, 1) test_switch = SwitchMellanoxM7800(switch_ip_address = 'fakeip', password = '******') assert(test_switch._get_boot_partitions(command_response = test_console_response) == expected_data)
def test_install_firmware_not_enough_steps(self, mock_expectmore): """Expect an error because there were not enough steps that appeared to be performed.""" test_switch = SwitchMellanoxM7800(switch_ip_address = 'fakeip', password = '******') firmware_name = 'my_amazing_firmware' mock_expectmore.return_value.ask.return_value = ['Step 1 of 4: Verify Image', '100.0%', '100.0%', '100.0%',] with pytest.raises(SwitchException): test_switch.install_firmware(image = firmware_name)
def test_image_boot_next_error(self, mock_expectmore): """Expect this to raise an error if there is an error response.""" test_switch = SwitchMellanoxM7800(switch_ip_address = 'fakeip', password = '******') error_message = '% strange error' mock_expectmore.return_value.ask.return_value = [error_message] with pytest.raises(SwitchException, match=error_message) as exception: test_switch.image_boot_next()
def test_connect(self, mock_mellanoknok, mock_expectmore): """Test that connect runs as expected in the simple case.""" mock_expectmore.return_value.isalive.return_value = False # don't perform password auth mock_expectmore.return_value.match_index = -1 test_switch = SwitchMellanoxM7800(switch_ip_address = 'fakeip', password = '******') test_switch.connect() # expect isalive to be checked mock_expectmore.return_value.isalive.assert_called() # expect the connection to be started mock_expectmore.return_value.start.assert_called_with( f'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -tt {test_switch.username}@{test_switch.switch_ip_address}' ) # expect it to look for the prompts before performing the login sequence mock_expectmore.return_value.wait.assert_called_with(['Password:'******' >']) # expect it to run the login sequence mock_expectmore.return_value.conversation.assert_called_with( [ ([' >', ''], 'terminal length 999'), (' >', 'enable'), (' #', 'configure terminal'), ('.config. #', ''), ] ) # expect it to try to establish an API connection mock_mellanoknok.assert_called_with(test_switch.switch_ip_address, password = test_switch.password)
def test__get_boot_partitions_out_of_order(self, mock_expectmore): """Test that partitions numbers are returned in the correct order (last, next).""" test_console_response = ['Next boot partition: 2', 'Last boot partition: 1',] expected_data = (1, 2) test_switch = SwitchMellanoxM7800(switch_ip_address = 'fakeip', password = '******') assert(test_switch._get_boot_partitions(command_response = test_console_response) == expected_data)
def test__get_expected_errors_no_error_pattern_found(self, mock_expectmore): """Confirm that a string is returned when no error patterns are found""" test_response = ['foobar%%%%%%', 'f%o%b%a%r%', 'foobar%', 'errors!'] test_switch = SwitchMellanoxM7800(switch_ip_address = 'fakeip', password = '******') errors = test_switch._get_expected_errors(command_response = test_response) assert(isinstance(errors, str)) assert(errors)
def test__get_installed_images_errors(self, mock_expectmore, test_response): """Test that a SwitchException is raised when invalid responses are parsed.""" test_switch = SwitchMellanoxM7800(switch_ip_address='fakeip', password='******') with pytest.raises(SwitchException): test_switch._get_installed_images(command_response=test_response)
def update_firmware(self, switch_name, firmware_url, downgrade, **kwargs): try: # connect to the switch and run the firmware upgrade procedure m7800_switch = SwitchMellanoxM7800(switch_name, **kwargs) m7800_switch.connect() # delete all stored images on the switch before sending ours over for image in m7800_switch.show_images().images_fetched_and_available: m7800_switch.image_delete(image = image.filename) m7800_switch.image_fetch(url = firmware_url) # install the firmware we just sent to the switch m7800_switch.install_firmware( # grab the filename from the switch on purpose in case it does something funky with it image = m7800_switch.show_images().images_fetched_and_available[0].filename ) # set the switch to boot from our installed image m7800_switch.image_boot_next() # perform extra downgrade steps if necessary if downgrade: # need to force a boot, even if the old code parsing the new configuration fails. m7800_switch.disable_fallback_reboot() m7800_switch.write_configuration() m7800_switch.reload() # now wait for the switch to come back. reconnected = False # timeout after 30 minutes. We use a no-op lambda because we just want to know when the timer expired. timer = Timer(1800, lambda: ()) timer.start() while timer.is_alive(): # swallow the expected exceptions while trying to connect to a switch that isn't ready yet. with suppress(SwitchException, ExpectMoreException): # use the switch as a context manager so every time the connect or factory reset fails, # we disconnect from the switch. with m7800_switch: m7800_switch.connect() # now factory reset the switch, which will reboot it again. # The successful connect above doesn't seem to guarantee that we can fire a # factory reset command, so we try in this loop. m7800_switch.factory_reset() timer.cancel() reconnected = True if not reconnected: raise CommandError( cmd = self.owner, msg = f'Unable to reconnect {switch_name} to switch while performing downgrade procedure.' ) else: m7800_switch.reload() # Turn some potentially verbose and detailed error messages into something more end user friendly # while keeping the dirty details available in the logs. except (SwitchException, ExpectMoreException) as exception: stack.commands.Log( message = f'Error during firmware update on {switch_name}: {exception}', level = syslog.LOG_ERR ) raise CommandError( cmd = self.owner, msg = f'Failed to update firmware on {switch_name}.' )
def run(self, hosts): if not self.owner.expanded: return {'keys': [], 'values': {}} switch_attrs = self.owner.getHostAttrDict('a:switch') host_info = dict.fromkeys(hosts) for host in dict(host_info): make, model = (switch_attrs[host].get('component.make'), switch_attrs[host].get('component.model')) if (make, model) != ('Mellanox', 'm7800'): # ... but set other hosts to an empty value instead of False host_info[host] = (None, None) continue kwargs = { 'username': switch_attrs[host].get('switch_username'), 'password': switch_attrs[host].get('switch_password'), } # remove username and pass attrs (aka use any pylib defaults) if they aren't host attrs kwargs = {k:v for k, v in kwargs.items() if v is not None} s = SwitchMellanoxM7800(host, **kwargs) try: s.connect() except SwitchException as e: host_info[host] = (None, switch_attrs[host].get('ibfabric', None)) continue host_info[host] = (s.subnet_manager, switch_attrs[host].get('ibfabric', None)) return { 'keys' : ['ib subnet manager', 'ib fabric'], 'values': host_info }
def test__get_expected_errors(self, mock_expectmore): """Confirm that strings starting with '%' are treated as errors.""" test_response = ['foobar%%%%%%', 'f%o%b%a%r%', '%foobar%', '%errors!'] test_switch = SwitchMellanoxM7800(switch_ip_address='fakeip', password='******') expected = sorted(['%foobar%', '%errors!']) assert (sorted(test_switch._get_errors( command_response=test_response)) == expected)
def test_image_delete(self, mock_expectmore): """Expect this to try to delete the user requested firmware.""" test_switch = SwitchMellanoxM7800(switch_ip_address='fakeip', password='******') firmware_name = 'my_amazing_firmware' test_switch.image_delete(image=firmware_name) mock_expectmore.return_value.ask.assert_called_with( f'image delete {firmware_name}')
def test__get_relevant_responses_errors(self, mock_expectmore, test_driver): """Test that SwitchException is raised on parsing errors.""" test_switch = SwitchMellanoxM7800(switch_ip_address = 'fakeip', password = '******') with pytest.raises(SwitchException): test_switch._get_relevant_responses( command_response = test_driver.test_response, start_marker = test_driver.start, end_marker = test_driver.end, )
def test__get_relevant_responses(self, mock_expectmore, test_driver): """Test that only items between start_marker and end_marker are returned.""" test_switch = SwitchMellanoxM7800(switch_ip_address = 'fakeip', password = '******') result = test_switch._get_relevant_responses( command_response = test_driver.test_response, start_marker = test_driver.start, end_marker = test_driver.end, ) assert(result == test_driver.expected_output)
def test_image_fetch_error_with_message(self, mock_expectmore): """Expect an error to be raised due to a missing success indicator and the error message to be captured.""" test_switch = SwitchMellanoxM7800(switch_ip_address = 'fakeip', password = '******') firmware_url = f'{test_switch.SUPPORTED_IMAGE_FETCH_PROTOCOLS[0]}://sometrustworthysite.ru' error_message = '% unauthorized' mock_expectmore.return_value.ask.return_value = ['other junk', error_message,] with pytest.raises(SwitchException, match=error_message) as exception: test_switch.image_fetch(url = firmware_url)
def test_image_delete_error(self, mock_expectmore): """Expect this to raise an error if there is an error response.""" test_switch = SwitchMellanoxM7800(switch_ip_address = 'fakeip', password = '******') firmware_name = 'my_amazing_firmware' error_message = '% file not found' mock_expectmore.return_value.ask.return_value = [error_message] with pytest.raises(SwitchException, match=error_message) as exception: test_switch.image_delete(image = firmware_name)
def test_disable_fallback_reboot(self, mock__get_errors, mock_expectmore): """Expect that a fallback reboot command is asked and that the response is checked for any errors.""" mock_expectmore.return_value.ask.return_value = 'testresults' mock__get_errors.return_value = [] test_switch = SwitchMellanoxM7800(switch_ip_address = 'fakeip', password = '******') test_switch.disable_fallback_reboot() mock_expectmore.return_value.ask.assert_called_with('no boot next fallback-reboot enable') mock__get_errors.assert_called_with(command_response = mock_expectmore.return_value.ask.return_value)
def test_connect_password_auth(self, mock_expectmore): """Expect connect to authenticate using a password.""" mock_expectmore.return_value.isalive.return_value = False # perform password auth mock_expectmore.return_value.match_index = 0 test_switch = SwitchMellanoxM7800(switch_ip_address = 'fakeip', password = '******') test_switch.connect() # expect the password to be used mock_expectmore.return_value.say.assert_called_with(test_switch.password)
def test__get_available_images_errors(self, mock_expectmore, test_response): """Test that an empty list is returned when no images are available.""" test_console_response = test_response test_switch = SwitchMellanoxM7800(switch_ip_address='fakeip', password='******') with pytest.raises(SwitchException): test_switch._get_available_images( command_response=test_console_response)
def test__get_available_images_no_images(self, mock_expectmore): """Test that an empty list is returned when no images are available.""" test_console_response = [ 'Next boot partition: 2', 'No image files are available to be installed.', 'Serve image files via HTTP/HTTPS: no' ] test_switch = SwitchMellanoxM7800(switch_ip_address = 'fakeip', password = '******') assert(test_switch._get_available_images(command_response = test_console_response) == [])
def test_connect_connection_failure(self, mock_expectmore): """Expect a SwitchException to be raised when the initial connection prompt is not found.""" mock_expectmore.return_value.isalive.return_value = False # Have wait raise the ExpectMoreException mock_expectmore.return_value.wait.side_effect = ExpectMoreException with pytest.raises(SwitchException): test_switch = SwitchMellanoxM7800(switch_ip_address = 'fakeip', password = '******') test_switch.connect()
def test_factory_reset(self, mock_expectmore): """Expect the function to send the factory reset sequence.""" test_switch = SwitchMellanoxM7800(switch_ip_address='fakeip', password='******') test_switch.factory_reset() mock_expectmore.return_value.conversation.assert_called_with([ ('', 'reset factory'), ('reset:', 'YES'), ])
def test_connect_already_alive(self, mock_expectmore): """Test that connect short circuits if the process is already alive.""" mock_expectmore.return_value.isalive.return_value = True test_switch = SwitchMellanoxM7800(switch_ip_address = 'fakeip', password = '******') test_switch.connect() # expect isalive to be checked mock_expectmore.return_value.isalive.assert_called_once() # expect no other calls assert(len(mock_expectmore.return_value.method_calls) == 1)
def test_write_configuration(self, mock__get_errors, mock_expectmore): """Expect that a write configuration command is asked and that the response is checked for any errors.""" mock_expectmore.return_value.ask.return_value = 'testresults' mock__get_errors.return_value = [] test_switch = SwitchMellanoxM7800(switch_ip_address = 'fakeip', password = '******') test_switch.write_configuration() mock_expectmore.return_value.ask.assert_called_with('configuration write') mock__get_errors.assert_called_with(command_response = mock_expectmore.return_value.ask.return_value)
def test_image_fetch_bad_protocol(self, mock_expectmore): """Expect an error to be raised on an unsupported protocol.""" test_switch = SwitchMellanoxM7800(switch_ip_address = 'fakeip', password = '******') firmware_url = 'git://[email protected]/sometrustworthyrepo.git' # add success condition return value to ensure the exception is because of the # unsupported protocol. mock_expectmore.return_value.ask.return_value = ['100.0%'] with pytest.raises(SwitchException): test_switch.image_fetch(url = firmware_url)
def test_image_fetch(self, mock_expectmore, protocol): """Expect this to try to fetch the user requested firmware.""" test_switch = SwitchMellanoxM7800(switch_ip_address='fakeip', password='******') firmware_url = f'{protocol}sometrustworthysite.ru' # add success condition return value mock_expectmore.return_value.ask.return_value = ['100.0%'] test_switch.image_fetch(url=firmware_url) mock_expectmore.return_value.ask.assert_called_with( f'image fetch {firmware_url}', timeout=ANY)
def test__get_installed_images(self, mock_expectmore, input_file, test_file): """Test that images are found when present.""" test_console_response = Path(test_file(input_file)).read_text().splitlines() expected_data = { 1: 'X86_64 3.6.4006 2017-07-03 16:17:39 x86_64', 2: 'X86_64 3.6.4006 2017-07-03 16:17:39 x86_64', } test_switch = SwitchMellanoxM7800(switch_ip_address = 'fakeip', password = '******') assert(test_switch._get_installed_images(command_response = test_console_response) == expected_data)