From 9d6bbf2b9ce83d91b24be4750ddb2ba25d4590e5 Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Wed, 6 Jul 2022 10:40:02 +0200 Subject: [PATCH] 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 --- .../server/tests/test_ubuntu_advantage.py | 44 ++++++++++++++++--- subiquity/server/ubuntu_advantage.py | 43 ++++++++++++------ 2 files changed, 67 insertions(+), 20 deletions(-) diff --git a/subiquity/server/tests/test_ubuntu_advantage.py b/subiquity/server/tests/test_ubuntu_advantage.py index 2b963e2b..ef542019 100644 --- a/subiquity/server/tests/test_ubuntu_advantage.py +++ b/subiquity/server/tests/test_ubuntu_advantage.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -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): diff --git a/subiquity/server/ubuntu_advantage.py b/subiquity/server/ubuntu_advantage.py index 4c0e79c5..7ac5ff2a 100644 --- a/subiquity/server/ubuntu_advantage.py +++ b/subiquity/server/ubuntu_advantage.py @@ -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 - return json.loads(proc.stdout) - except CalledProcessError: + try: + return json.loads(proc.stdout) + 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) - # 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) message = "Unable to retrieve subscription information." raise CheckSubscriptionError(token, message=message)