From 0d8e396011416d64ac36bce91eba8babb51b8c77 Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Tue, 3 Jun 2025 16:10:08 -0700 Subject: [PATCH] Add nodes/nodeset-requests commands This adds the ability to list and manipulate nodes and nodeset requests. No release note or documentation is added since these are related to nodepool-in-zuul which is in stealth mode. Change-Id: Ia3e987062122e017f19376ab30d9587becedca23 --- zuulclient/api/__init__.py | 56 +++++++++++++++++++++++ zuulclient/cmd/__init__.py | 82 ++++++++++++++++++++++++++++++++++ zuulclient/utils/formatters.py | 44 ++++++++++++++++++ 3 files changed, 182 insertions(+) diff --git a/zuulclient/api/__init__.py b/zuulclient/api/__init__.py index 3a17235..e63902c 100644 --- a/zuulclient/api/__init__.py +++ b/zuulclient/api/__init__.py @@ -371,3 +371,59 @@ class ZuulRESTClient(object): req = self.session.get(url) self._check_request_status(req) return req.json() + + def get_nodes(self, tenant): + if self.info.get('tenant'): + self._check_scope(tenant) + suffix = 'nodes' + else: + suffix = f'tenant/{tenant}/nodes' + url = urllib.parse.urljoin( + self.base_url, + suffix) + req = self.session.get(url) + self._check_request_status(req) + return req.json() + + def put_node(self, tenant, node, **args): + if not self.auth_token: + raise Exception('Auth Token required') + if self.info.get('tenant'): + self._check_scope(tenant) + suffix = f'nodes/{node}' + else: + suffix = f'tenant/{tenant}/nodes/{node}' + url = urllib.parse.urljoin( + self.base_url, + suffix) + req = self.session.put(url, json=args) + self._check_request_status(req) + return req.json() + + def get_nodeset_requests(self, tenant): + if self.info.get('tenant'): + self._check_scope(tenant) + suffix = 'nodeset-requests' + else: + suffix = f'tenant/{tenant}/nodeset-requests' + url = urllib.parse.urljoin( + self.base_url, + suffix) + req = self.session.get(url) + self._check_request_status(req) + return req.json() + + def delete_nodeset_request(self, tenant, request): + if not self.auth_token: + raise Exception('Auth Token required') + if self.info.get('tenant'): + self._check_scope(tenant) + suffix = f'nodeset-requests/{request}' + else: + suffix = f'tenant/{tenant}/nodeset-requests/{request}' + url = urllib.parse.urljoin( + self.base_url, + suffix) + req = self.session.delete(url) + self._check_request_status(req) + return (req.status_code == 204) diff --git a/zuulclient/cmd/__init__.py b/zuulclient/cmd/__init__.py index df74583..402e0c8 100644 --- a/zuulclient/cmd/__init__.py +++ b/zuulclient/cmd/__init__.py @@ -130,6 +130,10 @@ class ZuulClient(): self.add_build_info_subparser(subparsers) self.add_job_graph_subparser(subparsers) self.add_freeze_job_subparser(subparsers) + self.add_node_list_subparser(subparsers) + self.add_node_set_state_subparser(subparsers) + self.add_nodeset_request_list_subparser(subparsers) + self.add_nodeset_request_delete_subparser(subparsers) return subparsers @@ -556,6 +560,84 @@ class ZuulClient(): client.tenant_state(tenant, **kwargs) return True + def add_node_list_subparser(self, subparsers): + cmd = subparsers.add_parser('node-list', + help='list nodes') + cmd.add_argument('--tenant', help='tenant name', + required=False, default='') + cmd.set_defaults(func=self.node_list) + self.cmd_node_list = cmd + + def node_list(self): + client = self.get_client() + self._check_tenant_scope(client) + tenant = self.tenant() + nodes = client.get_nodes(tenant) + formatted_result = self.formatter('NodeList')(nodes) + print(formatted_result) + + return True + + def add_node_set_state_subparser(self, subparsers): + cmd = subparsers.add_parser('node-set-state', + help='set the state of a node') + cmd.add_argument('--tenant', help='tenant name', + required=False, default='') + cmd.add_argument('node', help='node id') + cmd.add_argument('state', + help='new node state', + choices=[ + 'hold', + 'used', + ]) + cmd.set_defaults(func=self.node_set_state) + self.cmd_node_set_state = cmd + + def node_set_state(self): + client = self.get_client() + self._check_tenant_scope(client) + tenant = self.tenant() + self.log.info('Setting node with arguments: %s %s', + self.args.node, self.args.state) + r = client.put_node(tenant, self.args.node, state=self.args.state) + return not r + + def add_nodeset_request_list_subparser(self, subparsers): + cmd = subparsers.add_parser('nodeset-request-list', + help='list nodeset requests') + cmd.add_argument('--tenant', help='tenant name', + required=False, default='') + cmd.set_defaults(func=self.nodeset_request_list) + self.cmd_nodeset_request_list = cmd + + def nodeset_request_list(self): + client = self.get_client() + self._check_tenant_scope(client) + tenant = self.tenant() + requests = client.get_nodeset_requests(tenant) + formatted_result = self.formatter('NodesetRequestList')(requests) + print(formatted_result) + + return True + + def add_nodeset_request_delete_subparser(self, subparsers): + cmd = subparsers.add_parser('nodeset-request-delete', + help='delete a nodeset request') + cmd.add_argument('--tenant', help='tenant name', + required=False, default='') + cmd.add_argument('request', help='nodeset request id') + cmd.set_defaults(func=self.nodeset_request_delete) + self.cmd_nodeset_request_delete = cmd + + def nodeset_request_delete(self): + client = self.get_client() + self._check_tenant_scope(client) + tenant = self.tenant() + self.log.info('Deleting nodeset request: %s', + self.args.request) + r = client.delete_nodeset_request(tenant, self.args.request) + return r + def get_config_section(self): conf_sections = self.config.sections() if len(conf_sections) == 1 and self.args.zuul_config is None: diff --git a/zuulclient/utils/formatters.py b/zuulclient/utils/formatters.py index c5fdcb8..b22a0df 100644 --- a/zuulclient/utils/formatters.py +++ b/zuulclient/utils/formatters.py @@ -70,6 +70,12 @@ class BaseFormatter: def formatFreezeJob(self, data): raise NotImplementedError + def formatNodeList(self, data): + raise NotImplementedError + + def formatNodesetRequestList(self, data): + raise NotImplementedError + class JSONFormatter(BaseFormatter): def __call__(self, data) -> str: @@ -332,6 +338,44 @@ class PrettyTableFormatter(BaseFormatter): ret += printer.pformat(data['vars']) return ret + def formatNodeList(self, data) -> str: + table = prettytable.PrettyTable( + field_names=[ + 'UUID', 'Labels', 'Connection', 'Provider', 'State', + 'State Time', 'Comment' + ]) + + for node in data: + table.add_row([ + node.get('uuid', 'N/A'), + node.get('type', 'N/A'), + node.get('connection_type', 'N/A'), + node.get('provider', 'N/A'), + node.get('state', 'N/A'), + node.get('state_time', 'N/A'), + node.get('comment', 'N/A'), + ]) + return str(table) + + def formatNodesetRequestList(self, data) -> str: + table = prettytable.PrettyTable( + field_names=[ + 'UUID', 'Labels', 'State', 'Request Time', 'Buildset', + 'Pipeline', 'Job' + ]) + + for node in data: + table.add_row([ + node.get('uuid', 'N/A'), + node.get('labels', 'N/A'), + node.get('state', 'N/A'), + node.get('request_time', 'N/A'), + node.get('buildset_uuid', 'N/A'), + node.get('pipeline_name', 'N/A'), + node.get('job_name', 'N/A'), + ]) + return str(table) + class DotFormatter(BaseFormatter): """Format for graphviz"""