From ffc117594bcd7494bc0f567b7517446dcf4350e2 Mon Sep 17 00:00:00 2001 From: Grant Orndorff Date: Mon, 3 Jul 2023 13:15:50 -0400 Subject: [PATCH] contract: support old airgapped server machineInfo --- features/airgapped.feature | 1 + uaclient/contract.py | 33 ++++++++++- uaclient/tests/test_contract.py | 99 +++++++++++++++++++++++++++++---- 3 files changed, 121 insertions(+), 12 deletions(-) diff --git a/features/airgapped.feature b/features/airgapped.feature index 29979711ba..0547b7414e 100644 --- a/features/airgapped.feature +++ b/features/airgapped.feature @@ -47,6 +47,7 @@ Feature: Performing attach using ua-airgapped """ 500 .*:8000/ubuntu jammy-infra-security/main """ + Then I verify that running `pro refresh` `with sudo` exits `0` Examples: ubuntu release | release | diff --git a/uaclient/contract.py b/uaclient/contract.py index a317e6c125..131831f6a4 100644 --- a/uaclient/contract.py +++ b/uaclient/contract.py @@ -78,8 +78,9 @@ def add_contract_machine( activity_info = self._get_activity_info() activity_info["lastAttachment"] = attachment_dt.isoformat() data = {"machineId": machine_id, "activityInfo": activity_info} + backcompat_data = _support_old_machine_info(data) response = self.request_url( - API_V1_ADD_CONTRACT_MACHINE, data=data, headers=headers + API_V1_ADD_CONTRACT_MACHINE, data=backcompat_data, headers=headers ) if response.code == 401: raise exceptions.AttachInvalidTokenError() @@ -380,11 +381,12 @@ def update_contract_machine( "machineId": machine_id, "activityInfo": self._get_activity_info(), } + backcompat_data = _support_old_machine_info(data) url = API_V1_UPDATE_CONTRACT_MACHINE.format( contract=contract_id, machine=machine_id ) response = self.request_url( - url, headers=headers, method="POST", data=data + url, headers=headers, method="POST", data=backcompat_data ) if response.code != 200: raise exceptions.ContractAPIError( @@ -427,6 +429,33 @@ def _get_activity_info(self): } +def _support_old_machine_info(request_body: dict): + """ + Transforms a request_body that has the new activity_info into a body that + includes both old and new forms of machineInfo/activityInfo + + This is necessary because there may be old ua-airgapped contract + servers deployed that we need to support. + This function is used for attach and refresh calls. + """ + activity_info = request_body.get("activityInfo", {}) + + return { + "machineId": request_body.get("machineId"), + "activityInfo": activity_info, + "architecture": activity_info.get("architecture"), + "os": { + "distribution": activity_info.get("distribution"), + "kernel": activity_info.get("kernel"), + "series": activity_info.get("series"), + # These two are required for old ua-airgapped but not sent anymore + # in the new activityInfo + "type": "Linux", + "release": system.get_release_info().release, + }, + } + + def process_entitlements_delta( cfg: UAConfig, past_entitlements: Dict[str, Any], diff --git a/uaclient/tests/test_contract.py b/uaclient/tests/test_contract.py index 117e545106..d7c0ae6d37 100644 --- a/uaclient/tests/test_contract.py +++ b/uaclient/tests/test_contract.py @@ -13,6 +13,7 @@ UAContractClient, _create_attach_forbidden_message, _get_override_weight, + _support_old_machine_info, apply_contract_overrides, get_available_resources, get_contract_information, @@ -103,12 +104,14 @@ class TestUAContractClient: ), ], ) + @mock.patch("uaclient.contract._support_old_machine_info") @mock.patch("uaclient.contract.UAContractClient._get_activity_info") @mock.patch("uaclient.contract.UAContractClient.headers") def test_update_contract_machine( self, m_headers, m_get_activity_info, + m_support_old_machine_info, m_get_machine_id, m_request_url, machine_id, @@ -119,6 +122,9 @@ def test_update_contract_machine( ): m_headers.return_value = {"header": "headerval"} m_get_activity_info.return_value = mock.sentinel.activity_info + m_support_old_machine_info.return_value = ( + mock.sentinel.support_old_machine_info + ) m_request_url.side_effect = request_url_side_effect m_get_machine_id.return_value = "get_machine_id" @@ -131,6 +137,14 @@ def test_update_contract_machine( if expected_result: assert expected_result == result + assert [ + mock.call( + { + "machineId": expected_machine_id, + "activityInfo": mock.sentinel.activity_info, + } + ) + ] == m_support_old_machine_info.call_args_list assert [ mock.call( "/v1/contracts/cId/context/machines/" + expected_machine_id, @@ -139,10 +153,7 @@ def test_update_contract_machine( "Authorization": "Bearer mToken", }, method="POST", - data={ - "machineId": expected_machine_id, - "activityInfo": mock.sentinel.activity_info, - }, + data=mock.sentinel.support_old_machine_info, ) ] == m_request_url.call_args_list @@ -420,12 +431,14 @@ def test_update_activity_token( ], ) @mock.patch("uaclient.contract._create_attach_forbidden_message") + @mock.patch("uaclient.contract._support_old_machine_info") @mock.patch("uaclient.contract.UAContractClient._get_activity_info") @mock.patch("uaclient.contract.UAContractClient.headers") def test_add_contract_machine( self, m_headers, m_get_activity_info, + m_support_old_machine_info, m_create_attach_forbidden_message, m_get_machine_id, m_request_url, @@ -440,6 +453,9 @@ def test_add_contract_machine( m_get_activity_info.return_value = { "activity": "info", } + m_support_old_machine_info.return_value = ( + mock.sentinel.support_old_machine_info + ) m_request_url.side_effect = request_url_side_effect m_get_machine_id.return_value = mock.sentinel.get_machine_id @@ -458,18 +474,23 @@ def test_add_contract_machine( assert [ mock.call( - "/v1/context/machines/token", - headers={ - "header": "headerval", - "Authorization": "Bearer cToken", - }, - data={ + { "machineId": expected_machine_id, "activityInfo": { "activity": "info", "lastAttachment": "2000-01-02T03:04:05+00:00", }, + } + ) + ] == m_support_old_machine_info.call_args_list + assert [ + mock.call( + "/v1/context/machines/token", + headers={ + "header": "headerval", + "Authorization": "Bearer cToken", }, + data=mock.sentinel.support_old_machine_info, ) ] == m_request_url.call_args_list assert ( @@ -850,6 +871,64 @@ def test_get_activity_info( assert expected == client._get_activity_info() +class TestSupportOldMachineInfo: + @pytest.mark.parametrize( + [ + "request_body", + "release_info", + "expected_result", + ], + [ + ( + { + "machineId": "mach", + "activityInfo": { + "architecture": "arch", + "distribution": "dist", + "kernel": "kern", + "series": "seri", + "other": "othe", + }, + }, + system.ReleaseInfo( + distribution="", + release="rele", + series="", + pretty_version="", + ), + { + "machineId": "mach", + "activityInfo": { + "architecture": "arch", + "distribution": "dist", + "kernel": "kern", + "series": "seri", + "other": "othe", + }, + "architecture": "arch", + "os": { + "distribution": "dist", + "kernel": "kern", + "series": "seri", + "type": "Linux", + "release": "rele", + }, + }, + ), + ], + ) + @mock.patch(M_PATH + "system.get_release_info") + def test_support_old_machine_info( + self, + m_get_release_info, + request_body, + release_info, + expected_result, + ): + m_get_release_info.return_value = release_info + assert expected_result == _support_old_machine_info(request_body) + + class TestProcessEntitlementDeltas: def test_error_on_missing_entitlement_type(self, FakeConfig): """Raise an error when neither dict contains entitlement type."""