-
Notifications
You must be signed in to change notification settings - Fork 1.8k
[dhcp-relay] make DHCP relay an extension #6531
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
renukamanavalan
merged 66 commits into
sonic-net:master
from
stepanblyschak:dhcp-relay-ext
Jul 15, 2021
Merged
Changes from all commits
Commits
Show all changes
66 commits
Select commit
Hold shift + click to select a range
a624e0c
[dockers] Tag all docker images with a version number
stepanblyschak 875650f
[dockers] Tag all docker images with a version number
stepanblyschak 954cad9
[dockers] label SONiC Docker with manifest
stepanblyschak 855fa60
[dockers] add package name to manifest
stepanblyschak 44685ba
remove swss dependency from dhcp-relay & router-advertiser
stepanblyschak d337e08
Merge branch 'master' of github.com:azure/sonic-buildimage into docke…
stepanblyschak 5eb48a9
Merge branch 'master' of github.com:azure/sonic-buildimage into docke…
stepanblyschak 99d9d12
Merge branch 'master' of github.com:azure/sonic-buildimage into docke…
stepanblyschak ed1bd3f
remove sonic-sdk added by mistake in this change
stepanblyschak 50237b4
Merge branch 'master' into dockers_version_tags
stepanblyschak caf5c9e
Merge branch 'master' of github.com:azure/sonic-buildimage into docke…
stepanblyschak 9bc4464
[dhcp-relay] make DHCP relay an extension
stepanblyschak efc69f5
[slave.mk] align lines in dependencies
stepanblyschak 31d4bbf
Merge branch 'master' of github.com:azure/sonic-buildimage into docke…
stepanblyschak 823eede
[docker-macsec] add version number for docker-macsec
stepanblyschak bdf0113
Merge branch 'dockers_version_tags' of github.com:stepanblyschak/soni…
stepanblyschak 05df4ed
[docker-macsec] add manifest for macsec docker
stepanblyschak e966fef
[slave.mk] fix missing comma
stepanblyschak d79293e
[slave.mk] fix missing comma
stepanblyschak a90f9b4
[slave.mk] fix missing comma
stepanblyschak 74cd875
[sonic_debian_extension.j2] use --enable instead of --enabled
stepanblyschak 2e5f097
[slave.mk] fix target path
stepanblyschak e1b2c29
[docker-dhcp-relay] add cli to docker
stepanblyschak db7ef7d
[docker-dhcp-relay] add cli to docker
stepanblyschak e0735d2
[sonic_debian_extension.j2] fix merge conflict markers
stepanblyschak 4897003
align install from tarball command
stepanblyschak 2066aa9
[dockers] use single manifest template
stepanblyschak 113bf30
[dockers] use single manifest template
stepanblyschak d6abc30
[docker-dhcp-relay] define manifest parameters in makefile
stepanblyschak 422c245
tag SONiC images the old way
stepanblyschak 5dd9278
Merge branch 'dockers_version_tags' into dockers_manifest
stepanblyschak 888a0ea
check if docker has correct manifest after loading it
stepanblyschak 8ad8a05
address review comment
stepanblyschak 53631d7
fix checking manifest
stepanblyschak a0d3124
Merge branch 'master' into dockers_manifest
stepanblyschak 9d76601
Merge branch 'master' of github.com:azure/sonic-buildimage into docke…
stepanblyschak 3de0cb3
remove obsolete file
stepanblyschak a665d73
Merge branch 'dockers_manifest' of github.com:stepanblyschak/sonic-bu…
stepanblyschak b2f8fc9
remove obsolete field from manifest. will be replaced by base-os comp…
stepanblyschak 46d75eb
Merge branch 'master' of github.com:azure/sonic-buildimage into docke…
stepanblyschak 13f3453
Merge branch 'master' into dhcp-relay-ext
stepanblyschak 84bf4dd
fix review comments
stepanblyschak b427b71
fix review comments
stepanblyschak 92ad8b1
add comment
stepanblyschak 57ec2f2
remove not needed ARG in dockerfiles
stepanblyschak c74ea21
Merge branch 'master' into dockers_manifest
stepanblyschak 119b47c
Merge branch 'dockers_manifest' of github.com:stepanblyschak/sonic-bu…
stepanblyschak 7c11312
tests for cli plugins
stepanblyschak 662aa3d
add tests
stepanblyschak 628071f
Merge branch 'master' into dhcp-relay-ext
stepanblyschak 66e2333
default-owner to set-owner
stepanblyschak 60076ed
fix lgtm warnings
stepanblyschak 368e9d3
Merge branch 'master' of github.com:azure/sonic-buildimage into dhcp-…
stepanblyschak 0d5b19e
fix review comments
stepanblyschak dffcee0
add coverage report
c1f7e32
[sonic-app-ext] support app extensions installation during build
7cc9102
Merge branch 'master' of github.com:azure/sonic-buildimage into app-e…
stepanblyschak abe88c0
bring back dhcp_relay
stepanblyschak 82b52be
Merge branch 'master' into dhcp-relay-ext
stepanblyschak 9807d93
introduce _INSTALL_PYTHON_WHEELS and _INSTALL_DEBS to not install son…
stepanblyschak fa1b606
Merge branch 'app-ext-build' of github.com:stepanblyschak/sonic-build…
stepanblyschak 306feba
add install targets for dhcp-relay
stepanblyschak a2e33be
Merge branch 'master' into dhcp-relay-ext
stepanblyschak fe19b0e
Merge branch 'master' into dhcp-relay-ext
stepanblyschak 42f8b36
remove obsolete changes added back by mistake during upstream merge
stepanblyschak c86bb45
add missing INCLUDE_DHCP_RELAY condition
stepanblyschak File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import pytest | ||
| import mock_tables # lgtm [py/unused-import] | ||
| from unittest import mock | ||
|
|
||
| @pytest.fixture() | ||
| def mock_cfgdb(): | ||
| cfgdb = mock.Mock() | ||
| CONFIG = { | ||
| 'VLAN': { | ||
| 'Vlan1000': { | ||
| 'dhcp_servers': ['192.0.0.1'] | ||
| } | ||
| } | ||
| } | ||
|
|
||
| def get_entry(table, key): | ||
| return CONFIG[table][key] | ||
|
|
||
| def set_entry(table, key, data): | ||
| CONFIG[table].setdefault(key, {}) | ||
| CONFIG[table][key] = data | ||
|
|
||
| cfgdb.get_entry = mock.Mock(side_effect=get_entry) | ||
| cfgdb.set_entry = mock.Mock(side_effect=set_entry) | ||
|
|
||
| yield cfgdb | ||
|
|
154 changes: 154 additions & 0 deletions
154
dockers/docker-dhcp-relay/cli-plugin-tests/mock_tables.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| # MONKEY PATCH!!! | ||
| import json | ||
| import os | ||
| from unittest import mock | ||
|
|
||
| import mockredis | ||
| import redis | ||
| import swsssdk | ||
| from sonic_py_common import multi_asic | ||
| from swsssdk import SonicDBConfig, SonicV2Connector, ConfigDBConnector, ConfigDBPipeConnector | ||
| from swsscommon import swsscommon | ||
|
|
||
|
|
||
| topo = None | ||
| dedicated_dbs = {} | ||
|
|
||
| def clean_up_config(): | ||
| # Set SonicDBConfig variables to initial state | ||
| # so that it can be loaded with single or multiple | ||
| # namespaces before the test begins. | ||
| SonicDBConfig._sonic_db_config = {} | ||
| SonicDBConfig._sonic_db_global_config_init = False | ||
| SonicDBConfig._sonic_db_config_init = False | ||
|
|
||
| def load_namespace_config(): | ||
| # To support multi asic testing | ||
| # SonicDBConfig load_sonic_global_db_config | ||
| # is invoked to load multiple namespaces | ||
| clean_up_config() | ||
| SonicDBConfig.load_sonic_global_db_config( | ||
| global_db_file_path=os.path.join( | ||
| os.path.dirname(os.path.abspath(__file__)), 'database_global.json')) | ||
|
|
||
| def load_database_config(): | ||
| # Load local database_config.json for single namespace test scenario | ||
| clean_up_config() | ||
| SonicDBConfig.load_sonic_db_config( | ||
| sonic_db_file_path=os.path.join( | ||
| os.path.dirname(os.path.abspath(__file__)), 'database_config.json')) | ||
|
|
||
|
|
||
| _old_connect_SonicV2Connector = SonicV2Connector.connect | ||
|
|
||
| def connect_SonicV2Connector(self, db_name, retry_on=True): | ||
| # add topo to kwargs for testing different topology | ||
| self.dbintf.redis_kwargs['topo'] = topo | ||
| # add the namespace to kwargs for testing multi asic | ||
| self.dbintf.redis_kwargs['namespace'] = self.namespace | ||
| # Mock DB filename for unit-test | ||
| global dedicated_dbs | ||
| if dedicated_dbs and dedicated_dbs.get(db_name): | ||
| self.dbintf.redis_kwargs['db_name'] = dedicated_dbs[db_name] | ||
| else: | ||
| self.dbintf.redis_kwargs['db_name'] = db_name | ||
| self.dbintf.redis_kwargs['decode_responses'] = True | ||
| _old_connect_SonicV2Connector(self, db_name, retry_on) | ||
|
|
||
| def _subscribe_keyspace_notification(self, db_name, client): | ||
| pass | ||
|
|
||
|
|
||
| def config_set(self, *args): | ||
| pass | ||
|
|
||
|
|
||
| class MockPubSub: | ||
| def get_message(self): | ||
| return None | ||
|
|
||
| def psubscribe(self, *args, **kwargs): | ||
| pass | ||
|
|
||
| def __call__(self, *args, **kwargs): | ||
| return self | ||
|
|
||
| def listen(self): | ||
| return [] | ||
|
|
||
| def punsubscribe(self, *args, **kwargs): | ||
| pass | ||
|
|
||
| def clear(self): | ||
| pass | ||
|
|
||
| INPUT_DIR = os.path.dirname(os.path.abspath(__file__)) | ||
|
|
||
|
|
||
| class SwssSyncClient(mockredis.MockRedis): | ||
| def __init__(self, *args, **kwargs): | ||
| super(SwssSyncClient, self).__init__(strict=True, *args, **kwargs) | ||
| # Namespace is added in kwargs specifically for unit-test | ||
| # to identify the file path to load the db json files. | ||
| topo = kwargs.pop('topo') | ||
| namespace = kwargs.pop('namespace') | ||
| db_name = kwargs.pop('db_name') | ||
| self.decode_responses = kwargs.pop('decode_responses', False) == True | ||
| fname = db_name.lower() + ".json" | ||
| self.pubsub = MockPubSub() | ||
|
|
||
| if namespace is not None and namespace is not multi_asic.DEFAULT_NAMESPACE: | ||
| fname = os.path.join(INPUT_DIR, namespace, fname) | ||
| elif topo is not None: | ||
| fname = os.path.join(INPUT_DIR, topo, fname) | ||
| else: | ||
| fname = os.path.join(INPUT_DIR, fname) | ||
|
|
||
| if os.path.exists(fname): | ||
| with open(fname) as f: | ||
| js = json.load(f) | ||
| for k, v in js.items(): | ||
| if 'expireat' in v and 'ttl' in v and 'type' in v and 'value' in v: | ||
| # database is in redis-dump format | ||
| if v['type'] == 'hash': | ||
| # ignore other types for now since sonic has hset keys only in the db | ||
| for attr, value in v['value'].items(): | ||
| self.hset(k, attr, value) | ||
| else: | ||
| for attr, value in v.items(): | ||
| self.hset(k, attr, value) | ||
|
|
||
| # Patch mockredis/mockredis/client.py | ||
| # The offical implementation assume decode_responses=False | ||
| # Here we detect the option and decode after doing encode | ||
| def _encode(self, value): | ||
| "Return a bytestring representation of the value. Taken from redis-py connection.py" | ||
|
|
||
| value = super(SwssSyncClient, self)._encode(value) | ||
|
|
||
| if self.decode_responses: | ||
| return value.decode('utf-8') | ||
|
|
||
| # Patch mockredis/mockredis/client.py | ||
| # The official implementation will filter out keys with a slash '/' | ||
| # ref: https://github.com/locationlabs/mockredis/blob/master/mockredis/client.py | ||
| def keys(self, pattern='*'): | ||
| """Emulate keys.""" | ||
| import fnmatch | ||
| import re | ||
|
|
||
| # Make regex out of glob styled pattern. | ||
| regex = fnmatch.translate(pattern) | ||
| regex = re.compile(regex) | ||
|
|
||
| # Find every key that matches the pattern | ||
| return [key for key in self.redis if regex.match(key)] | ||
|
|
||
|
|
||
| swsssdk.interface.DBInterface._subscribe_keyspace_notification = _subscribe_keyspace_notification | ||
| mockredis.MockRedis.config_set = config_set | ||
| redis.StrictRedis = SwssSyncClient | ||
| SonicV2Connector.connect = connect_SonicV2Connector | ||
| swsscommon.SonicV2Connector = SonicV2Connector | ||
| swsscommon.ConfigDBConnector = ConfigDBConnector | ||
| swsscommon.ConfigDBPipeConnector = ConfigDBPipeConnector |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| [pytest] | ||
| addopts = --cov-config=.coveragerc --cov --cov-report html --cov-report term --cov-report xml --junitxml=test-results.xml -vv | ||
|
|
141 changes: 141 additions & 0 deletions
141
dockers/docker-dhcp-relay/cli-plugin-tests/test_config_dhcp_relay.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| import os | ||
| import sys | ||
| import traceback | ||
| from unittest import mock | ||
|
|
||
| from click.testing import CliRunner | ||
|
|
||
| from utilities_common.db import Db | ||
|
|
||
| import pytest | ||
|
|
||
| sys.path.append('../cli/config/plugins/') | ||
| import dhcp_relay | ||
|
|
||
| config_vlan_add_dhcp_relay_output="""\ | ||
| Added DHCP relay destination address 192.0.0.100 to Vlan1000 | ||
| Restarting DHCP relay service... | ||
| """ | ||
|
|
||
| config_vlan_del_dhcp_relay_output="""\ | ||
tahmed-dev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Removed DHCP relay destination address 192.0.0.100 from Vlan1000 | ||
| Restarting DHCP relay service... | ||
| """ | ||
|
|
||
| class TestConfigVlanDhcpRelay(object): | ||
| def test_plugin_registration(self): | ||
| cli = mock.MagicMock() | ||
| dhcp_relay.register(cli) | ||
| cli.commands['vlan'].add_command.assert_called_once_with(dhcp_relay.vlan_dhcp_relay) | ||
|
|
||
| def test_config_vlan_add_dhcp_relay_with_nonexist_vlanid(self): | ||
| runner = CliRunner() | ||
|
|
||
| with mock.patch('utilities_common.cli.run_command') as mock_run_command: | ||
| result = runner.invoke(dhcp_relay.vlan_dhcp_relay.commands["add"], | ||
| ["1001", "192.0.0.100"]) | ||
| print(result.exit_code) | ||
| print(result.output) | ||
| # traceback.print_tb(result.exc_info[2]) | ||
| assert result.exit_code != 0 | ||
| assert "Error: Vlan1001 doesn't exist" in result.output | ||
| assert mock_run_command.call_count == 0 | ||
|
|
||
| def test_config_vlan_add_dhcp_relay_with_invalid_vlanid(self): | ||
| runner = CliRunner() | ||
|
|
||
| with mock.patch('utilities_common.cli.run_command') as mock_run_command: | ||
| result = runner.invoke(dhcp_relay.vlan_dhcp_relay.commands["add"], | ||
| ["4096", "192.0.0.100"]) | ||
| print(result.exit_code) | ||
| print(result.output) | ||
| # traceback.print_tb(result.exc_info[2]) | ||
| assert result.exit_code != 0 | ||
| assert "Error: Vlan4096 doesn't exist" in result.output | ||
| assert mock_run_command.call_count == 0 | ||
|
|
||
| def test_config_vlan_add_dhcp_relay_with_invalid_ip(self): | ||
| runner = CliRunner() | ||
|
|
||
| with mock.patch('utilities_common.cli.run_command') as mock_run_command: | ||
| result = runner.invoke(dhcp_relay.vlan_dhcp_relay.commands["add"], | ||
| ["1000", "192.0.0.1000"]) | ||
| print(result.exit_code) | ||
| print(result.output) | ||
| # traceback.print_tb(result.exc_info[2]) | ||
| assert result.exit_code != 0 | ||
| assert "Error: 192.0.0.1000 is invalid IP address" in result.output | ||
| assert mock_run_command.call_count == 0 | ||
|
|
||
| def test_config_vlan_add_dhcp_relay_with_exist_ip(self, mock_cfgdb): | ||
| runner = CliRunner() | ||
| db = Db() | ||
| db.cfgdb = mock_cfgdb | ||
|
|
||
| with mock.patch('utilities_common.cli.run_command') as mock_run_command: | ||
| result = runner.invoke(dhcp_relay.vlan_dhcp_relay.commands["add"], | ||
| ["1000", "192.0.0.1"], obj=db) | ||
| print(result.exit_code) | ||
| print(result.output) | ||
| assert result.exit_code == 0 | ||
| assert "192.0.0.1 is already a DHCP relay destination for Vlan1000" in result.output | ||
| assert mock_run_command.call_count == 0 | ||
|
|
||
| def test_config_vlan_add_del_dhcp_relay_dest(self, mock_cfgdb): | ||
| runner = CliRunner() | ||
| db = Db() | ||
| db.cfgdb = mock_cfgdb | ||
|
|
||
| # add new relay dest | ||
| with mock.patch("utilities_common.cli.run_command") as mock_run_command: | ||
| result = runner.invoke(dhcp_relay.vlan_dhcp_relay.commands["add"], | ||
| ["1000", "192.0.0.100"], obj=db) | ||
| print(result.exit_code) | ||
| print(result.output) | ||
| assert result.exit_code == 0 | ||
| assert result.output == config_vlan_add_dhcp_relay_output | ||
| assert mock_run_command.call_count == 3 | ||
| db.cfgdb.set_entry.assert_called_once_with('VLAN', 'Vlan1000', {'dhcp_servers': ['192.0.0.1', '192.0.0.100']}) | ||
|
|
||
| db.cfgdb.set_entry.reset_mock() | ||
|
|
||
| # del relay dest | ||
| with mock.patch("utilities_common.cli.run_command") as mock_run_command: | ||
tahmed-dev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| result = runner.invoke(dhcp_relay.vlan_dhcp_relay.commands["del"], | ||
| ["1000", "192.0.0.100"], obj=db) | ||
| print(result.exit_code) | ||
| print(result.output) | ||
| assert result.exit_code == 0 | ||
| assert result.output == config_vlan_del_dhcp_relay_output | ||
| assert mock_run_command.call_count == 3 | ||
| db.cfgdb.set_entry.assert_called_once_with('VLAN', 'Vlan1000', {'dhcp_servers': ['192.0.0.1']}) | ||
|
|
||
| def test_config_vlan_remove_nonexist_dhcp_relay_dest(self, mock_cfgdb): | ||
| runner = CliRunner() | ||
| db = Db() | ||
| db.cfgdb = mock_cfgdb | ||
|
|
||
| with mock.patch('utilities_common.cli.run_command') as mock_run_command: | ||
| result = runner.invoke(dhcp_relay.vlan_dhcp_relay.commands["del"], | ||
| ["1000", "192.0.0.100"], obj=db) | ||
| print(result.exit_code) | ||
| print(result.output) | ||
| # traceback.print_tb(result.exc_info[2]) | ||
| assert result.exit_code != 0 | ||
| assert "Error: 192.0.0.100 is not a DHCP relay destination for Vlan1000" in result.output | ||
| assert mock_run_command.call_count == 0 | ||
|
|
||
| def test_config_vlan_remove_dhcp_relay_dest_with_nonexist_vlanid(self, mock_cfgdb): | ||
| runner = CliRunner() | ||
| db = Db() | ||
| db.cfgdb = mock_cfgdb | ||
|
|
||
| with mock.patch('utilities_common.cli.run_command') as mock_run_command: | ||
| result = runner.invoke(dhcp_relay.vlan_dhcp_relay.commands["del"], | ||
| ["1001", "192.0.0.1"], obj=Db) | ||
| print(result.exit_code) | ||
| print(result.output) | ||
| # traceback.print_tb(result.exc_info[2]) | ||
| assert result.exit_code != 0 | ||
| assert "Error: Vlan1001 doesn't exist" in result.output | ||
| assert mock_run_command.call_count == 0 | ||
28 changes: 28 additions & 0 deletions
28
dockers/docker-dhcp-relay/cli-plugin-tests/test_show_dhcp_relay.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import os | ||
| import sys | ||
| import traceback | ||
| from unittest import mock | ||
|
|
||
| from click.testing import CliRunner | ||
|
|
||
| import show.vlan as vlan | ||
| from utilities_common.db import Db | ||
|
|
||
| sys.path.insert(0, '../cli/show/plugins/') | ||
| import show_dhcp_relay | ||
|
|
||
|
|
||
| class TestVlanDhcpRelay(object): | ||
| def test_plugin_registration(self): | ||
| cli = mock.MagicMock() | ||
| show_dhcp_relay.register(cli) | ||
| assert 'DHCP Helper Address' in dict(vlan.VlanBrief.COLUMNS) | ||
|
|
||
| def test_dhcp_relay_column_output(self): | ||
| ctx = ( | ||
| ({'Vlan100': {'dhcp_servers': ['192.0.0.1', '192.168.0.2']}}, {}, {}), | ||
| (), | ||
| ) | ||
| assert show_dhcp_relay.get_dhcp_helper_address(ctx, 'Vlan100') == '192.0.0.1\n192.168.0.2' | ||
|
|
||
|
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.