diff --git a/docs/index.rst b/docs/index.rst index 01b5904..7a50843 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,6 +21,12 @@ Builds .. autoclass:: quickbuild.endpoints.builds.Builds :members: +Tokens +~~~~~~ + +.. autoclass:: quickbuild.endpoints.tokens.Tokens + :members: + Users ~~~~~ diff --git a/quickbuild/core.py b/quickbuild/core.py index ac4078a..35ae6d8 100644 --- a/quickbuild/core.py +++ b/quickbuild/core.py @@ -4,6 +4,7 @@ from typing import Any, Callable, NamedTuple, Optional from quickbuild.endpoints.builds import Builds +from quickbuild.endpoints.tokens import Tokens from quickbuild.endpoints.users import Users from quickbuild.exceptions import ( QBError, @@ -23,6 +24,7 @@ class QuickBuild(ABC): def __init__(self): self.builds = Builds(self) + self.tokens = Tokens(self) self.users = Users(self) @staticmethod diff --git a/quickbuild/endpoints/tokens.py b/quickbuild/endpoints/tokens.py new file mode 100644 index 0000000..3e94b48 --- /dev/null +++ b/quickbuild/endpoints/tokens.py @@ -0,0 +1,81 @@ +from typing import List, Optional + +import xmltodict + + +class Tokens: + """With tokens, one can authorize/unauthorize agents, or access agent + details including token value and latest usage information. + """ + + def __init__(self, quickbuild): + self.quickbuild = quickbuild + + def get(self, agent_address: Optional[str] = None) -> List[dict]: + """ + Get token value and latest used information of agents. + + Args: + agent_address (Optional[str]): + Build agent address, eg. my-agent:8811. + If param address is set to None, details of all agents will be returned. + + Returns: + List[dict]: List of token and agent details + """ + def callback(response: str) -> List[dict]: + root = xmltodict.parse(response) + + tokens = [] + if root['list'] is not None: + tokens = root['list']['com.pmease.quickbuild.model.Token'] + if isinstance(tokens, list) is False: + tokens = [tokens] + return tokens + + params_agent_address = dict(address=agent_address) if agent_address else {} + + return self.quickbuild._request( + 'GET', + 'tokens', + callback, + params=params_agent_address, + ) + + def authorize(self, agent_ip: str, agent_port: int = 8811) -> str: + """ + Authorize a build agent to join the build grid. + + Args: + agent_ip (str): The build agent IP address. + agent_port (int): The build agent port (default: 8811). + + Returns: + str: identifier of the newly created token for the build agent + """ + response = self.quickbuild._request( + 'GET', + 'tokens/authorize', + params=dict(ip=agent_ip, port=agent_port), + ) + + return response + + def unauthorize(self, agent_ip: str, agent_port: int = 8811) -> str: + """ + Unauthorize an already authorized build agent. + + Args: + agent_ip (str): The build agent IP address. + agent_port (int): The build agent port (default: 8811). + + Returns: + str: identifier of the removed token representing the build agent. + """ + response = self.quickbuild._request( + 'GET', + 'tokens/unauthorize', + params=dict(ip=agent_ip, port=agent_port), + ) + + return response diff --git a/tests/test_tokens.py b/tests/test_tokens.py new file mode 100644 index 0000000..f653613 --- /dev/null +++ b/tests/test_tokens.py @@ -0,0 +1,189 @@ + +import re + +import pytest +import responses + +from quickbuild import AsyncQBClient, QBClient + + +REGEX_IP = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}' +REGEX_PORT = '\d+' + +TOKEN_XML = r""" + + + + 120204 + 84858611-a1fe-4f88-a49c-f600cf0ecf11 + 192.168.1.100 + 8811 + false + 2021-02-08T20:01:09.426Z + Run step (configuration: root/pipelineC, build: B.50906, step: master>build) + quickbuild-agent-192-168-1-100 + true + + +""" + +TOKENS_XML = r""" + + + + 117554 + bee7e8ff-fd9a-475a-8cf5-42a5353b8875 + 192.168.1.100 + 8811 + false + 2021-02-08T20:08:29.360Z + Check build condition (configuration: root/pipelineA) + quickbuild-agent-192-168-1-100 + true + + + 115672 + 27350640-d9f9-4a10-96ae-b6ec8fee998b + 192.168.1.101 + 8811 + false + 2021-02-08T20:01:10.175Z + Run step (configuration: root/pipelineA, build: B.1234, step: master>finalize) + quickbuild-agent-192-168-1-101 + true + + + 116545 + 8f604c48-b9f4-4bbe-847c-c073b2aebc81 + 192.168.1.102 + 8811 + false + 2021-02-08T20:01:10.013Z + Run step (configuration: root/pipelineB, build: B.123, step: master>publish) + quickbuild-agent-192-168-1-102 + true + + +""" + +EMPTY_TOKEN_XML = r""" + + +""" + + +@responses.activate +def test_authorize(): + RESPONSE_DATA = '120123' + + responses.add( + responses.GET, + re.compile(r'.*/rest/tokens/authorize\?ip={}&port={}'.format(REGEX_IP, REGEX_PORT)), + content_type='text/plain', + body=RESPONSE_DATA, + match_querystring=True, + ) + + response = QBClient('http://server').tokens.authorize('192.168.1.100', 8811) + assert response == '120123' + + response = QBClient('http://server').tokens.authorize('192.168.1.100') + assert response == '120123' + + +@responses.activate +def test_unauthorize(): + RESPONSE_DATA = '120123' + + responses.add( + responses.GET, + re.compile(r'.*/rest/tokens/unauthorize\?ip={}&port={}'.format(REGEX_IP, REGEX_PORT)), + content_type='text/plain', + body=RESPONSE_DATA, + match_querystring=True, + ) + + response = QBClient('http://server').tokens.unauthorize('192.168.1.100', 8811) + assert response == '120123' + + response = QBClient('http://server').tokens.unauthorize('192.168.1.100') + assert response == '120123' + + +@responses.activate +def test_token_and_agent_details(): + responses.add( + responses.GET, + re.compile(r'.*/rest/tokens\?address=quickbuild-agent-192-168-1-100%3A8811'), + content_type='application/xml', + body=TOKEN_XML + ) + + response = QBClient('http://server').tokens.get('quickbuild-agent-192-168-1-100:8811') + assert len(response) == 1 + assert response[0]['id'] == '120204' + + +@responses.activate +def test_tokens_and_agent_details(): + responses.add( + responses.GET, + re.compile(r'.*/rest/tokens'), + content_type='application/xml', + body=TOKENS_XML, + ) + + response = QBClient('http://server').tokens.get() + assert len(response) == 3 + assert response[0]['id'] == '117554' + assert response[1]['id'] == '115672' + assert response[2]['id'] == '116545' + + +@responses.activate +def test_tokens_and_agent_details_with_unknown_address(): + responses.add( + responses.GET, + re.compile(r'.*/rest/tokens\?address=unknown'), + content_type='application/xml', + body=EMPTY_TOKEN_XML, + match_querystring=True, + ) + + response = QBClient('http://server').tokens.get('unknown') + assert len(response) == 0 + assert response == [] + + +@pytest.mark.asyncio +async def test_authorize_async(aiohttp_mock): + RESPONSE_DATA = '120123' + + client = AsyncQBClient('http://server') + try: + aiohttp_mock.get( + re.compile(r'.*/rest/tokens/authorize\?ip={}&port={}'.format(REGEX_IP, REGEX_PORT)), + body=RESPONSE_DATA, + ) + + response = await client.tokens.authorize('192.168.1.100') + assert response == '120123' + finally: + await client.close() + + +@pytest.mark.asyncio +async def test_unauthorize_async(aiohttp_mock): + RESPONSE_DATA = '120123' + + client = AsyncQBClient('http://server') + try: + aiohttp_mock.get( + re.compile(r'.*/rest/tokens/unauthorize\?ip={}&port={}'.format(REGEX_IP, REGEX_PORT)), + body=RESPONSE_DATA, + ) + + response = await client.tokens.unauthorize('192.168.1.100') + assert response == '120123' + finally: + await client.close()