ubuntu-advantage: determine on error if token was invalid

In case of error, u-a-c exits with status 1. When the contract token
supplied is invalid, it is also considered an error ; which previously
made us unable to make the distinction between:
 * an invalid token
 * a more generic error such as network error or service unavailable error

Thanks to an update in u-a-c, we can now determine if the token was
invalid, by parsing the standard output of the process, even when it
exits with status 1.

The output is expected to be a JSON object that includes an array of
errors where each error has a specific error code.

The error code for an invalid token is attach-invalid-token ; which we
now look for in the output to determine if the contract token was
invalid.

Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
This commit is contained in:
Olivier Gayot 2022-07-06 10:40:02 +02:00
parent 0fa8c54296
commit 9d6bbf2b9c
2 changed files with 67 additions and 20 deletions

View File

@ -13,7 +13,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from subprocess import CalledProcessError, CompletedProcess
from subprocess import CompletedProcess
import unittest
from unittest.mock import patch, AsyncMock
@ -87,9 +87,9 @@ class TestUAClientUAInterfaceStrategy(unittest.IsolatedAsyncioTestCase):
mock_arun.return_value = CompletedProcess([], 0)
mock_arun.return_value.stdout = "{}"
await strategy.query_info(token="123456789")
mock_arun.assert_called_once_with(command, check=True)
mock_arun.assert_called_once_with(command, check=False)
async def test_query_info_failed(self):
async def test_query_info_unknown_error(self):
strategy = UAClientUAInterfaceStrategy()
command = (
"ubuntu-advantage",
@ -99,12 +99,42 @@ class TestUAClientUAInterfaceStrategy(unittest.IsolatedAsyncioTestCase):
)
with patch(self.arun_command_sym) as mock_arun:
mock_arun.side_effect = CalledProcessError(returncode=1,
cmd=command)
mock_arun.return_value.returncode = 2
mock_arun.return_value.stdout = "{}"
with self.assertRaises(CheckSubscriptionError):
await strategy.query_info(token="123456789")
mock_arun.assert_called_once_with(command, check=True)
mock_arun.assert_called_once_with(command, check=False)
async def test_query_info_invalid_token(self):
strategy = UAClientUAInterfaceStrategy()
command = (
"ubuntu-advantage",
"status",
"--format", "json",
"--simulate-with-token", "123456789",
)
with patch(self.arun_command_sym) as mock_arun:
mock_arun.return_value.returncode = 1
mock_arun.return_value.stdout = """\
{
"environment_vars": [],
"errors": [
{
"message": "Invalid token. See https://ubuntu.com/advantage",
"message_code": "attach-invalid-token",
"service": null,
"type": "system"
}
],
"result": "failure",
"services": [],
"warnings": []
}
"""
with self.assertRaises(InvalidTokenError):
await strategy.query_info(token="123456789")
mock_arun.assert_called_once_with(command, check=False)
async def test_query_info_invalid_json(self):
strategy = UAClientUAInterfaceStrategy()
@ -120,7 +150,7 @@ class TestUAClientUAInterfaceStrategy(unittest.IsolatedAsyncioTestCase):
mock_arun.return_value.stdout = "invalid-json"
with self.assertRaises(CheckSubscriptionError):
await strategy.query_info(token="123456789")
mock_arun.assert_called_once_with(command, check=True)
mock_arun.assert_called_once_with(command, check=False)
class TestUAInterface(unittest.IsolatedAsyncioTestCase):

View File

@ -17,9 +17,10 @@ helper. """
from abc import ABC, abstractmethod
from datetime import datetime as dt
import contextlib
import json
import logging
from subprocess import CalledProcessError, CompletedProcess
from subprocess import CompletedProcess
from typing import List, Sequence, Union
import asyncio
@ -132,20 +133,36 @@ class UAClientUAInterfaceStrategy(UAInterfaceStrategy):
"--format", "json",
"--simulate-with-token", token,
)
try:
proc: CompletedProcess = await utils.arun_command(command,
check=True)
# On error, the command will exit with status 1. When that happens, the
# output should still be formatted as a JSON object and we can inspect
# it to know the reason of the failure. This is how we figure out if
# the contract token was invalid.
proc: CompletedProcess = await utils.arun_command(command, check=False)
if proc.returncode == 0:
# TODO check if we're not returning a string or a list
try:
return json.loads(proc.stdout)
except CalledProcessError:
log.exception("Failed to execute command %r", command)
# TODO Check if the command failed because the token is invalid.
# Currently, ubuntu-advantage fails with the following error when
# the token is invalid:
# * Failed to connect to authentication server
# * Check your Internet connection and try again.
except json.JSONDecodeError:
log.exception("Failed to parse output of command %r", command)
elif proc.returncode == 1:
try:
data = json.loads(proc.stdout)
except json.JSONDecodeError:
log.exception("Failed to parse output of command %r", command)
else:
token_invalid = False
with contextlib.suppress(KeyError):
for error in data["errors"]:
if error["message_code"] == "attach-invalid-token":
token_invalid = True
log.debug("error reported by u-a-c: %s: %s",
error["message_code"], error["message"])
if token_invalid:
raise InvalidTokenError(token)
else:
log.exception("Failed to execute command %r", command)
message = "Unable to retrieve subscription information."
raise CheckSubscriptionError(token, message=message)