From f0ceb33f3a31fc5a3c8ed18971cc19009a10259f Mon Sep 17 00:00:00 2001 From: Aurelien Lourot Date: Thu, 18 Jun 2020 15:03:17 +0200 Subject: [PATCH 1/6] Add functional tests for neutron-arista --- zaza/openstack/charm_tests/neutron/tests.py | 21 +++++- .../charm_tests/neutron_arista/__init__.py | 15 +++++ .../charm_tests/neutron_arista/setup.py | 39 +++++++++++ .../charm_tests/neutron_arista/tests.py | 53 +++++++++++++++ .../charm_tests/neutron_arista/utils.py | 67 +++++++++++++++++++ 5 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 zaza/openstack/charm_tests/neutron_arista/__init__.py create mode 100644 zaza/openstack/charm_tests/neutron_arista/setup.py create mode 100644 zaza/openstack/charm_tests/neutron_arista/tests.py create mode 100644 zaza/openstack/charm_tests/neutron_arista/utils.py diff --git a/zaza/openstack/charm_tests/neutron/tests.py b/zaza/openstack/charm_tests/neutron/tests.py index 7e28112..ba014e2 100644 --- a/zaza/openstack/charm_tests/neutron/tests.py +++ b/zaza/openstack/charm_tests/neutron/tests.py @@ -293,8 +293,23 @@ class NeutronCreateNetworkTest(test_utils.OpenStackBaseTest): cls.neutron_client = ( openstack_utils.get_neutron_session_client(cls.keystone_session)) + def _test_400_additional_validation(self, expected_network_names): + """Additional assertions for test_400_create_network. + + Can be overridden in derived classes. + + :type expected_network_names: List[str] + """ + pass + def test_400_create_network(self): - """Create a network, verify that it exists, and then delete it.""" + """Create a network, verify that it exists, and then delete it. + + Additional verifications on the created network can be performed by + deriving this class and overriding _test_400_additional_validation(). + """ + self._test_400_additional_validation([]) + logging.debug('Creating neutron network...') self.neutron_client.format = 'json' net_name = 'test_net' @@ -319,10 +334,14 @@ class NeutronCreateNetworkTest(test_utils.OpenStackBaseTest): network = networks['networks'][0] assert network['name'] == net_name, "network ext_net not found" + self._test_400_additional_validation([net_name]) + # Cleanup logging.debug('Deleting neutron network...') self.neutron_client.delete_network(network['id']) + self._test_400_additional_validation([]) + class NeutronApiTest(NeutronCreateNetworkTest): """Test basic Neutron API Charm functionality.""" diff --git a/zaza/openstack/charm_tests/neutron_arista/__init__.py b/zaza/openstack/charm_tests/neutron_arista/__init__.py new file mode 100644 index 0000000..8d3f5c9 --- /dev/null +++ b/zaza/openstack/charm_tests/neutron_arista/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2020 Canonical Ltd. +# +# 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. + +"""Collection of code for setting up and testing neutron-arista.""" diff --git a/zaza/openstack/charm_tests/neutron_arista/setup.py b/zaza/openstack/charm_tests/neutron_arista/setup.py new file mode 100644 index 0000000..50878e6 --- /dev/null +++ b/zaza/openstack/charm_tests/neutron_arista/setup.py @@ -0,0 +1,39 @@ +# Copyright 2020 Canonical Ltd. +# +# 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. + +"""Code for setting up neutron-arista.""" + +import logging +import tenacity +import zaza +import zaza.openstack.charm_tests.neutron_arista.utils as arista_utils + + +def test_fixture(): + """Pass arista-virt-test-fixture's IP address to neutron-arista.""" + fixture_ip_addr = arista_utils.fixture_ip_addr() + logging.info( + "{}'s IP address is '{}'. Passing it to neutron-arista...".format( + arista_utils.FIXTURE_APP_NAME, fixture_ip_addr)) + zaza.model.set_application_config('neutron-arista', + {'eapi-host': fixture_ip_addr}) + + logging.info('Waiting for {} to become ready...'.format( + arista_utils.FIXTURE_APP_NAME)) + for attempt in tenacity.Retrying( + wait=tenacity.wait_fixed(10), # seconds + stop=tenacity.stop_after_attempt(30), + reraise=True): + with attempt: + arista_utils.query_fixture_networks(fixture_ip_addr) diff --git a/zaza/openstack/charm_tests/neutron_arista/tests.py b/zaza/openstack/charm_tests/neutron_arista/tests.py new file mode 100644 index 0000000..c47c9c8 --- /dev/null +++ b/zaza/openstack/charm_tests/neutron_arista/tests.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 + +# Copyright 2020 Canonical Ltd. +# +# 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. + +"""Encapsulating `neutron-arista` testing.""" + +import logging +import tenacity +import zaza.openstack.charm_tests.neutron.tests as neutron_tests +import zaza.openstack.charm_tests.neutron_arista.utils as arista_utils + + +class NeutronCreateAristaNetworkTest(neutron_tests.NeutronCreateNetworkTest): + """Test creating an Arista Neutron network through the API.""" + + @classmethod + def setUpClass(cls): + """Run class setup for running Neutron Arista tests.""" + super(NeutronCreateAristaNetworkTest, cls).setUpClass() + + logging.info('Waiting for Neutron to become ready...') + for attempt in tenacity.Retrying( + wait=tenacity.wait_fixed(5), # seconds + stop=tenacity.stop_after_attempt(12), + reraise=True): + with attempt: + cls.neutron_client.list_networks() + + def _test_400_additional_validation(self, expected_network_names): + """Arista-specific assertions for test_400_create_network. + + :type expected_network_names: List[str] + """ + logging.info("Querying Arista CVX's networks...") + actual_network_names = arista_utils.query_fixture_networks( + arista_utils.fixture_ip_addr()) + + # NOTE(lourot): the assertion name is misleading as it's not only + # checking the item count but also that all items are present in + # both lists, without checking the order. + self.assertCountEqual(actual_network_names, expected_network_names) diff --git a/zaza/openstack/charm_tests/neutron_arista/utils.py b/zaza/openstack/charm_tests/neutron_arista/utils.py new file mode 100644 index 0000000..3c68f33 --- /dev/null +++ b/zaza/openstack/charm_tests/neutron_arista/utils.py @@ -0,0 +1,67 @@ +# Copyright 2020 Canonical Ltd. +# +# 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. + +"""Common Arista-related utils.""" + +import json +import requests +import urllib3 +import zaza + +FIXTURE_APP_NAME = 'arista-virt-test-fixture' + + +def fixture_ip_addr(): + """Return the public IP address of the Arista test fixture.""" + return zaza.model.get_units(FIXTURE_APP_NAME)[0].public_address + + +_FIXTURE_LOGIN = 'admin' +_FIXTURE_PASSWORD = 'password123' + + +def query_fixture_networks(ip_addr): + """Query the Arista test fixture's list of networks.""" + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + session = requests.Session() + session.headers['Content-Type'] = 'application/json' + session.headers['Accept'] = 'application/json' + session.verify = False + session.auth = (_FIXTURE_LOGIN, _FIXTURE_PASSWORD) + + data = { + 'id': 'Zaza neutron-arista tests', + 'method': 'runCmds', + 'jsonrpc': '2.0', + 'params': { + 'timestamps': False, + 'format': 'json', + 'version': 1, + 'cmds': ['show openstack networks'] + } + } + + response = session.post( + 'https://{}/command-api/'.format(ip_addr), + data=json.dumps(data), + timeout=10 # seconds + ) + + result = [] + for _, region in response.json()['result'][0]['regions'].items(): + for _, tenant in region['tenants'].items(): + for _, network in tenant['tenantNetworks'].items(): + result.append(network['networkName']) + return result From 13b590ea12376ad5d2fc3568db2ada0d490beb3c Mon Sep 17 00:00:00 2001 From: Aurelien Lourot Date: Tue, 23 Jun 2020 13:03:04 +0200 Subject: [PATCH 2/6] Refactor overriding mechanism --- zaza/openstack/charm_tests/neutron/tests.py | 61 ++++++++----------- .../charm_tests/neutron_arista/tests.py | 20 +++--- 2 files changed, 35 insertions(+), 46 deletions(-) diff --git a/zaza/openstack/charm_tests/neutron/tests.py b/zaza/openstack/charm_tests/neutron/tests.py index ba014e2..f270604 100644 --- a/zaza/openstack/charm_tests/neutron/tests.py +++ b/zaza/openstack/charm_tests/neutron/tests.py @@ -292,55 +292,44 @@ class NeutronCreateNetworkTest(test_utils.OpenStackBaseTest): # set up clients cls.neutron_client = ( openstack_utils.get_neutron_session_client(cls.keystone_session)) + cls.neutron_client.format = 'json' - def _test_400_additional_validation(self, expected_network_names): - """Additional assertions for test_400_create_network. - - Can be overridden in derived classes. - - :type expected_network_names: List[str] - """ - pass + _TEST_NET_NAME = 'test_net' def test_400_create_network(self): - """Create a network, verify that it exists, and then delete it. - - Additional verifications on the created network can be performed by - deriving this class and overriding _test_400_additional_validation(). - """ - self._test_400_additional_validation([]) + """Create a network, verify that it exists, and then delete it.""" + self._assert_test_network_doesnt_exist() + self._create_test_network() + net_id = self._assert_test_network_exists_and_return_id() + self._delete_test_network(net_id) + self._assert_test_network_doesnt_exist() + def _create_test_network(self): logging.debug('Creating neutron network...') - self.neutron_client.format = 'json' - net_name = 'test_net' - - # Verify that the network doesn't exist - networks = self.neutron_client.list_networks(name=net_name) - net_count = len(networks['networks']) - assert net_count == 0, ( - "Expected zero networks, found {}".format(net_count)) - - # Create a network and verify that it exists - network = {'name': net_name} + network = {'name': self._TEST_NET_NAME} self.neutron_client.create_network({'network': network}) - networks = self.neutron_client.list_networks(name=net_name) + def _delete_test_network(self, net_id): + logging.debug('Deleting neutron network...') + self.neutron_client.delete_network(net_id) + + def _assert_test_network_exists_and_return_id(self): + logging.debug('Confirming new neutron network...') + networks = self.neutron_client.list_networks(name=self._TEST_NET_NAME) logging.debug('Networks: {}'.format(networks)) net_len = len(networks['networks']) assert net_len == 1, ( "Expected 1 network, found {}".format(net_len)) - - logging.debug('Confirming new neutron network...') network = networks['networks'][0] - assert network['name'] == net_name, "network ext_net not found" + assert network['name'] == self._TEST_NET_NAME, \ + "network {} not found".format(self._TEST_NET_NAME) + return network['id'] - self._test_400_additional_validation([net_name]) - - # Cleanup - logging.debug('Deleting neutron network...') - self.neutron_client.delete_network(network['id']) - - self._test_400_additional_validation([]) + def _assert_test_network_doesnt_exist(self): + networks = self.neutron_client.list_networks(name=self._TEST_NET_NAME) + net_count = len(networks['networks']) + assert net_count == 0, ( + "Expected zero networks, found {}".format(net_count)) class NeutronApiTest(NeutronCreateNetworkTest): diff --git a/zaza/openstack/charm_tests/neutron_arista/tests.py b/zaza/openstack/charm_tests/neutron_arista/tests.py index c47c9c8..4f6b1de 100644 --- a/zaza/openstack/charm_tests/neutron_arista/tests.py +++ b/zaza/openstack/charm_tests/neutron_arista/tests.py @@ -38,16 +38,16 @@ class NeutronCreateAristaNetworkTest(neutron_tests.NeutronCreateNetworkTest): with attempt: cls.neutron_client.list_networks() - def _test_400_additional_validation(self, expected_network_names): - """Arista-specific assertions for test_400_create_network. - - :type expected_network_names: List[str] - """ - logging.info("Querying Arista CVX's networks...") + def _assert_test_network_exists_and_return_id(self): actual_network_names = arista_utils.query_fixture_networks( arista_utils.fixture_ip_addr()) + self.assertEqual(actual_network_names, [self._TEST_NET_NAME]) + return super(NeutronCreateAristaNetworkTest, + self)._assert_test_network_exists_and_return_id() - # NOTE(lourot): the assertion name is misleading as it's not only - # checking the item count but also that all items are present in - # both lists, without checking the order. - self.assertCountEqual(actual_network_names, expected_network_names) + def _assert_test_network_doesnt_exist(self): + actual_network_names = arista_utils.query_fixture_networks( + arista_utils.fixture_ip_addr()) + self.assertEqual(actual_network_names, []) + super(NeutronCreateAristaNetworkTest, + self)._assert_test_network_doesnt_exist() From a04cc1f077abe1a96859744ca6039960e0356c9a Mon Sep 17 00:00:00 2001 From: Aurelien Lourot Date: Tue, 23 Jun 2020 13:11:50 +0200 Subject: [PATCH 3/6] Cosmetic improvements --- zaza/openstack/charm_tests/neutron_arista/setup.py | 4 ++-- zaza/openstack/charm_tests/neutron_arista/utils.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/zaza/openstack/charm_tests/neutron_arista/setup.py b/zaza/openstack/charm_tests/neutron_arista/setup.py index 50878e6..b990751 100644 --- a/zaza/openstack/charm_tests/neutron_arista/setup.py +++ b/zaza/openstack/charm_tests/neutron_arista/setup.py @@ -24,8 +24,8 @@ def test_fixture(): """Pass arista-virt-test-fixture's IP address to neutron-arista.""" fixture_ip_addr = arista_utils.fixture_ip_addr() logging.info( - "{}'s IP address is '{}'. Passing it to neutron-arista...".format( - arista_utils.FIXTURE_APP_NAME, fixture_ip_addr)) + "{}'s IP address is '{}'. Passing it to neutron-arista..." + .format(arista_utils.FIXTURE_APP_NAME, fixture_ip_addr)) zaza.model.set_application_config('neutron-arista', {'eapi-host': fixture_ip_addr}) diff --git a/zaza/openstack/charm_tests/neutron_arista/utils.py b/zaza/openstack/charm_tests/neutron_arista/utils.py index 3c68f33..78732d6 100644 --- a/zaza/openstack/charm_tests/neutron_arista/utils.py +++ b/zaza/openstack/charm_tests/neutron_arista/utils.py @@ -60,8 +60,8 @@ def query_fixture_networks(ip_addr): ) result = [] - for _, region in response.json()['result'][0]['regions'].items(): - for _, tenant in region['tenants'].items(): - for _, network in tenant['tenantNetworks'].items(): + for region in response.json()['result'][0]['regions'].values(): + for tenant in region['tenants'].values(): + for network in tenant['tenantNetworks'].values(): result.append(network['networkName']) return result From b1ccc4bd24522c11b138c3a713b8ab09d11417bc Mon Sep 17 00:00:00 2001 From: Aurelien Lourot Date: Tue, 23 Jun 2020 15:50:34 +0200 Subject: [PATCH 4/6] Rename neutron arista charm --- zaza/openstack/charm_tests/neutron_arista/__init__.py | 2 +- zaza/openstack/charm_tests/neutron_arista/setup.py | 8 ++++---- zaza/openstack/charm_tests/neutron_arista/tests.py | 2 +- zaza/openstack/charm_tests/neutron_arista/utils.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/zaza/openstack/charm_tests/neutron_arista/__init__.py b/zaza/openstack/charm_tests/neutron_arista/__init__.py index 8d3f5c9..c0eae4e 100644 --- a/zaza/openstack/charm_tests/neutron_arista/__init__.py +++ b/zaza/openstack/charm_tests/neutron_arista/__init__.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Collection of code for setting up and testing neutron-arista.""" +"""Collection of code for setting up and testing neutron-api-plugin-arista.""" diff --git a/zaza/openstack/charm_tests/neutron_arista/setup.py b/zaza/openstack/charm_tests/neutron_arista/setup.py index b990751..2b15d5a 100644 --- a/zaza/openstack/charm_tests/neutron_arista/setup.py +++ b/zaza/openstack/charm_tests/neutron_arista/setup.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Code for setting up neutron-arista.""" +"""Code for setting up neutron-api-plugin-arista.""" import logging import tenacity @@ -21,12 +21,12 @@ import zaza.openstack.charm_tests.neutron_arista.utils as arista_utils def test_fixture(): - """Pass arista-virt-test-fixture's IP address to neutron-arista.""" + """Pass arista-virt-test-fixture's IP address to Neutron.""" fixture_ip_addr = arista_utils.fixture_ip_addr() logging.info( - "{}'s IP address is '{}'. Passing it to neutron-arista..." + "{}'s IP address is '{}'. Passing it to neutron-api-plugin-arista..." .format(arista_utils.FIXTURE_APP_NAME, fixture_ip_addr)) - zaza.model.set_application_config('neutron-arista', + zaza.model.set_application_config('neutron-api-plugin-arista', {'eapi-host': fixture_ip_addr}) logging.info('Waiting for {} to become ready...'.format( diff --git a/zaza/openstack/charm_tests/neutron_arista/tests.py b/zaza/openstack/charm_tests/neutron_arista/tests.py index 4f6b1de..3f78bf7 100644 --- a/zaza/openstack/charm_tests/neutron_arista/tests.py +++ b/zaza/openstack/charm_tests/neutron_arista/tests.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Encapsulating `neutron-arista` testing.""" +"""Encapsulating `neutron-api-plugin-arista` testing.""" import logging import tenacity diff --git a/zaza/openstack/charm_tests/neutron_arista/utils.py b/zaza/openstack/charm_tests/neutron_arista/utils.py index 78732d6..bc86e4b 100644 --- a/zaza/openstack/charm_tests/neutron_arista/utils.py +++ b/zaza/openstack/charm_tests/neutron_arista/utils.py @@ -42,7 +42,7 @@ def query_fixture_networks(ip_addr): session.auth = (_FIXTURE_LOGIN, _FIXTURE_PASSWORD) data = { - 'id': 'Zaza neutron-arista tests', + 'id': 'Zaza neutron-api-plugin-arista tests', 'method': 'runCmds', 'jsonrpc': '2.0', 'params': { From 87d5667990c2eab293bb6e97716dcfb0b27ec4a4 Mon Sep 17 00:00:00 2001 From: Aurelien Lourot Date: Wed, 24 Jun 2020 10:38:02 +0200 Subject: [PATCH 5/6] Download Arista image --- .../charm_tests/neutron_arista/setup.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/zaza/openstack/charm_tests/neutron_arista/setup.py b/zaza/openstack/charm_tests/neutron_arista/setup.py index 2b15d5a..a1ade9e 100644 --- a/zaza/openstack/charm_tests/neutron_arista/setup.py +++ b/zaza/openstack/charm_tests/neutron_arista/setup.py @@ -15,9 +15,41 @@ """Code for setting up neutron-api-plugin-arista.""" import logging +import os import tenacity import zaza import zaza.openstack.charm_tests.neutron_arista.utils as arista_utils +import zaza.openstack.utilities.openstack as openstack_utils + + +def download_arista_image(): + """Download arista-cvx-virt-test.qcow2 from a web server. + + If TEST_ARISTA_IMAGE_LOCAL isn't set, set it to + `/tmp/arista-cvx-virt-test.qcow2`. If TEST_ARISTA_IMAGE_REMOTE is set (e.g. + to `http://example.com/swift/v1/images/arista-cvx-virt-test.qcow2`), + download it to TEST_ARISTA_IMAGE_LOCAL. + """ + try: + os.environ['TEST_ARISTA_IMAGE_LOCAL'] + except KeyError: + os.environ['TEST_ARISTA_IMAGE_LOCAL'] = '' + if not os.environ['TEST_ARISTA_IMAGE_LOCAL']: + os.environ['TEST_ARISTA_IMAGE_LOCAL'] \ + = '/tmp/arista-cvx-virt-test.qcow2' + + try: + if os.environ['TEST_ARISTA_IMAGE_REMOTE']: + logging.info('Downloading Arista image from {}' + .format(os.environ['TEST_ARISTA_IMAGE_REMOTE'])) + openstack_utils.download_image( + os.environ['TEST_ARISTA_IMAGE_REMOTE'], + os.environ['TEST_ARISTA_IMAGE_LOCAL']) + except KeyError: + pass + + logging.info('Arista image can be found at {}' + .format(os.environ['TEST_ARISTA_IMAGE_LOCAL'])) def test_fixture(): From 68af0d2079089fbbc4f3d1c6cc4bb1fd73ca5ce9 Mon Sep 17 00:00:00 2001 From: Aurelien Lourot Date: Wed, 24 Jun 2020 16:27:35 +0200 Subject: [PATCH 6/6] Clarification --- zaza/openstack/charm_tests/neutron_arista/setup.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/zaza/openstack/charm_tests/neutron_arista/setup.py b/zaza/openstack/charm_tests/neutron_arista/setup.py index a1ade9e..4b8dfe7 100644 --- a/zaza/openstack/charm_tests/neutron_arista/setup.py +++ b/zaza/openstack/charm_tests/neutron_arista/setup.py @@ -25,10 +25,13 @@ import zaza.openstack.utilities.openstack as openstack_utils def download_arista_image(): """Download arista-cvx-virt-test.qcow2 from a web server. - If TEST_ARISTA_IMAGE_LOCAL isn't set, set it to - `/tmp/arista-cvx-virt-test.qcow2`. If TEST_ARISTA_IMAGE_REMOTE is set (e.g. - to `http://example.com/swift/v1/images/arista-cvx-virt-test.qcow2`), - download it to TEST_ARISTA_IMAGE_LOCAL. + The download will happen only if the env var TEST_ARISTA_IMAGE_REMOTE has + been set, so you don't have to set it if you already have the image + locally. + + If the env var TEST_ARISTA_IMAGE_LOCAL isn't set, it will be set to + `/tmp/arista-cvx-virt-test.qcow2`. This is where the image will be + downloaded to if TEST_ARISTA_IMAGE_REMOTE has been set. """ try: os.environ['TEST_ARISTA_IMAGE_LOCAL'] @@ -46,6 +49,8 @@ def download_arista_image(): os.environ['TEST_ARISTA_IMAGE_REMOTE'], os.environ['TEST_ARISTA_IMAGE_LOCAL']) except KeyError: + # TEST_ARISTA_IMAGE_REMOTE isn't set, which means the image is already + # available at TEST_ARISTA_IMAGE_LOCAL pass logging.info('Arista image can be found at {}'