#!/usr/bin/python3 # Copyright 2017-2018 Lars Wirzenius # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import configparser import json import logging import os import sys import cliapp import yaml import ick2 def scopes(base): patterns = [ 'uapi_{}_get', 'uapi_{}_post', 'uapi_{}_id_get', 'uapi_{}_id_put', 'uapi_{}_id_delete', ] return [x.format(base) for x in patterns] def scopes_for_types(typelist): result = [] for type_name in typelist: result.extend(scopes(type_name)) return result types = [ 'projects', 'pipelines', 'workers', 'work', 'builds', 'logs', ] class Icktool(cliapp.Application): _default_scopes = [ 'uapi_version_get', 'uapi_work_post', 'uapi_projects_id_pipelines_id_get', 'uapi_projects_id_pipelines_id_put', 'uapi_blobs_id_get', 'uapi_blobs_id_put', ] + scopes_for_types(types) def add_settings(self): self.settings.string( ['controller', 'c'], 'use URL as the controller base URL', metavar='URL', ) self.settings.string( ['auth-url'], 'use URL as the authentication URL', metavar='URL', ) self.settings.string( ['secrets'], 'use URL as the controller base URL', metavar='URL', default=os.path.expanduser('~/.config/icktool/credentials.conf'), ) self.settings.boolean( ['verify-tls'], 'verify API provider TLS certificate ' '(default is verify, use --no-verify-tls)', default=True, ) self.settings.string( ['token'], 'use TOKEN instead of generating a new one', metavar='TOKEN', ) self.settings.string_list( ['scope'], 'add SCOPE to the list of scope in requested token', metavar='SCOPE', default=self._default_scopes, ) def setup(self): if not self.settings['verify-tls']: logging.captureWarnings(True) def cmd_scopes(self, args): for scope in self.settings['scope']: self.output.write('{}\n'.format(scope)) def cmd_token(self, args): token = self._new_token() self.output.write('{}\n'.format(token)) def cmd_version(self, args): token = self._new_token() api = self._new_api() api.set_token(token) version = api.get_version() self._prettyson(version) def cmd_trigger(self, args): project_name = args[0] pipeline_name = args[1] token = self._new_token() api = self._new_api() api.set_token(token) result = api.trigger(project_name, pipeline_name) self._prettyson(result) def cmd_make_it_so(self, argv): obj = self._read_object() token = self._new_token() api = self._new_api() api.set_token(token) self._create_resources( api, '/projects', 'project', obj.get('projects', [])) self._create_resources( api, '/pipelines', 'pipeline', obj.get('pipelines', [])) def _read_object(self): return yaml.load(sys.stdin) def _create_resources(self, api, path, field, objs): for obj in objs: try: api.create(path, obj) except ick2.HttpError: if field in obj: obj_path = '{}/{}'.format(path, obj[field]) api.update(obj_path, obj) def cmd_show(self, args): token = self._new_token() api = self._new_api() api.set_token(token) if not args: args = [ 'projects', 'pipelines', ] for kind in args: objs = api.show('/' + kind) self._prettyson(objs) def cmd_status(self, args): token = self._new_token() api = self._new_api() api.set_token(token) rows = [] projects = api.show('/projects')['projects'] projects = sorted(projects, key=lambda p: p.get('project')) builds = api.show('/builds')['builds'] for project in projects: pipeline_names = sorted(project['pipelines']) for pipeline_name in pipeline_names: build = self._get_latest_build( project['project'], pipeline_name, builds) if build is None: build = { 'build_id': 'never', 'log': 'none', 'status': 'n/a', } status = api.get_build_status( project['project'], pipeline_name) row = { 'project': project['project'], 'pipeline': pipeline_name, 'build_id': build['build_id'], 'status': status['status'], 'build_status': build['status'], 'log': build['log'], } rows.append(row) self._pretty_table( rows, ['project', 'pipeline', 'status', 'build_status', 'log']) def _get_latest_build(self, project_name, pipeline_name, builds): wanted = [ b for b in builds if b['project'] == project_name and b['pipeline'] == pipeline_name ] if wanted: return wanted[-1] return None def cmd_show_latest_log(self, args): token = self._new_token() api = self._new_api() api.set_token(token) project_name = args[0] builds = api.show('/builds')['builds'] project_builds = [b for b in builds if b['project'] == project_name] if project_builds: b = project_builds[-1] log = api.get_log(b['build_id']) self.output.write(log.decode('UTF-8')) def cmd_show_log(self, args): token = self._new_token() api = self._new_api() api.set_token(token) build_id = args[0] log = api.get_log(build_id) self.output.write(log.decode('UTF-8')) def _new_api(self): api = ick2.ControllerClient() api.set_verify_tls(self.settings['verify-tls']) api.set_controller_url(self.settings['controller']) return api def _new_auth(self): url = self.settings['auth-url'] if not url: api = self._new_api() url = api.get_auth_url() client_id, client_secret = self._get_client_creds(url) ac = ick2.AuthClient() ac.set_auth_url(url) ac.set_client_creds(client_id, client_secret) return ac def _new_token(self): if self.settings['token']: return self.settings['token'] ac = self._new_auth() wanted_scopes = ' '.join(self.settings['scope']) return ac.get_token(wanted_scopes) def _get_client_creds(self, url): cp = configparser.ConfigParser() cp.read(self.settings['secrets']) client_id = cp.get(url, 'client_id') client_secret = cp.get(url, 'client_secret') return client_id, client_secret def _prettyson(self, obj): json.dump(obj, self.output, indent=4, sort_keys=True) self.output.write('\n') def _pretty_table(self, rows, columns): headings = { column: column for column in columns } widths = { column: 0 for column in columns } for row in [headings] + rows: for column in columns: widths[column] = max(widths[column], len(str(row[column]))) underlines = { column: '-' * widths[column] for column in columns } for row in [headings, underlines] + rows: self.output.write( '{}\n'.format(self._pretty_row(widths, row, columns))) def _pretty_row(self, widths, row, columns): parts = ['%*s' % (widths[c], row[c]) for c in columns] return ' | '.join(parts) class Command: def __init__(self, api): self._api = api def execute(self): raise NotImplementedError() class VersionCommand(Command): def execute(self): self._api.get_version() Icktool(version=ick2.__version__).run()