Console interface to Storyboard
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

251 lines
9.2 KiB

# Copyright 2014 OpenStack Foundation
# Copyright 2014 Hewlett-Packard Development Company, L.P.
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import collections
import getpass
import os
import re
import ordereddict
import yaml
from six.moves.urllib import parse as urlparse
import voluptuous as v
import boartty.commentlink
import boartty.palette
import boartty.keymap
OrderedDict = collections.OrderedDict
except AttributeError:
OrderedDict = ordereddict.OrderedDict
class ConfigSchema(object):
server = {v.Required('name'): str,
v.Required('url'): str,
v.Required('token'): str,
'verify-ssl': bool,
'ssl-ca-path': str,
'dburi': str,
'log-file': str,
'socket': str,
'auth-type': v.Any('basic', 'digest', 'form'),
servers = [server]
sort_by = v.Any('number', 'updated', 'last-seen')
text_replacement = {'text': v.Any(str,
{'color': str,
v.Required('text'): str})}
link_replacement = {'link': {v.Required('url'): str,
v.Required('text'): str}}
search_replacement = {'search': {v.Required('query'): str,
v.Required('text'): str}}
replacement = v.Any(text_replacement, link_replacement, search_replacement)
palette = {v.Required('name'): str,
v.Match('(?!name)'): [str]}
palettes = [palette]
commentlink = {v.Required('match'): str,
v.Required('replacements'): [replacement],
'test-result': str}
commentlinks = [commentlink]
dashboard = {v.Required('name'): str,
v.Required('query'): str,
v.Optional('sort-by'): sort_by,
v.Optional('reverse'): bool,
v.Required('key'): str}
dashboards = [dashboard]
reviewkey_approval = {v.Required('category'): str,
v.Required('value'): int}
reviewkey = {v.Required('approvals'): [reviewkey_approval],
'submit': bool,
v.Required('key'): str}
reviewkeys = [reviewkey]
hide_comment = {v.Required('author'): str}
hide_comments = [hide_comment]
story_list_options = {'sort-by': sort_by,
'reverse': bool}
keymap = {v.Required('name'): str,
v.Match('(?!name)'): v.Any([[str], str], [str], str)}
keymaps = [keymap]
def getSchema(self, data):
schema = v.Schema({v.Required('servers'): self.servers,
'palettes': self.palettes,
'palette': str,
'keymaps': self.keymaps,
'keymap': str,
'commentlinks': self.commentlinks,
'dashboards': self.dashboards,
'reviewkeys': self.reviewkeys,
'story-list-query': str,
'diff-view': str,
'hide-comments': self.hide_comments,
'display-times-in-utc': bool,
'handle-mouse': bool,
'breadcrumbs': bool,
'story-list-options': self.story_list_options,
'expire-age': str,
return schema
class Config(object):
def __init__(self, server=None, palette='default', keymap='default',
self.path = os.path.expanduser(path)
if not os.path.exists(self.path):
self.config = yaml.load(open(self.path))
schema = ConfigSchema().getSchema(self.config)
server = self.getServer(server)
self.server = server
url = server['url']
if not url.endswith('/'):
url += '/'
self.url = url
result = urlparse.urlparse(url)
self.hostname = result.netloc
self.token = server['token']
self.username = '' # TODO: storyboard
# Ensure file is only readable by user as password is stored in
# file.
mode = os.stat(self.path).st_mode & 0o0777
if not mode == 0o600:
print (
"Error: Config file '{}' contains an api key and does "
"not have permissions set to 0600.\n"
"Permissions are: {}".format(self.path, oct(mode)))
self.auth_type = server.get('auth-type', 'digest')
self.verify_ssl = server.get('verify-ssl', True)
if not self.verify_ssl:
self.ssl_ca_path = server.get('ssl-ca-path', None)
if self.ssl_ca_path is not None:
self.ssl_ca_path = os.path.expanduser(self.ssl_ca_path)
# Boardtty itself uses the Requests library
os.environ['REQUESTS_CA_BUNDLE'] = self.ssl_ca_path
# And this is to allow Git callouts
os.environ['GIT_SSL_CAINFO'] = self.ssl_ca_path
self.dburi = server.get('dburi',
'sqlite:///' + os.path.expanduser('~/.boartty.db'))
socket_path = server.get('socket', '~/.boartty.sock')
self.socket_path = os.path.expanduser(socket_path)
log_file = server.get('log-file', '~/.boartty.log')
self.log_file = os.path.expanduser(log_file)
lock_file = server.get('lock-file', '~/.boartty.%s.lock' % server['name'])
self.lock_file = os.path.expanduser(lock_file)
self.palettes = {'default': boartty.palette.Palette({}),
'light': boartty.palette.Palette(boartty.palette.LIGHT_PALETTE),
for p in self.config.get('palettes', []):
if p['name'] not in self.palettes:
self.palettes[p['name']] = boartty.palette.Palette(p)
self.palette = self.palettes[self.config.get('palette', palette)]
self.keymaps = {'default': boartty.keymap.KeyMap({}),
'vi': boartty.keymap.KeyMap(boartty.keymap.VI_KEYMAP)}
for p in self.config.get('keymaps', []):
if p['name'] not in self.keymaps:
self.keymaps[p['name']] = boartty.keymap.KeyMap(p)
self.keymap = self.keymaps[self.config.get('keymap', keymap)]
self.commentlinks = [boartty.commentlink.CommentLink(c)
for c in self.config.get('commentlinks', [])]
self.project_story_list_query = self.config.get('story-list-query', '')
self.diff_view = self.config.get('diff-view', 'side-by-side')
self.dashboards = OrderedDict()
for d in self.config.get('dashboards', []):
self.dashboards[d['key']] = d
self.reviewkeys = OrderedDict()
for k in self.config.get('reviewkeys', []):
self.reviewkeys[k['key']] = k
self.hide_comments = []
for h in self.config.get('hide-comments', []):
self.utc = self.config.get('display-times-in-utc', False)
self.breadcrumbs = self.config.get('breadcrumbs', True)
self.handle_mouse = self.config.get('handle-mouse', True)
story_list_options = self.config.get('story-list-options', {})
self.story_list_options = {
'sort-by': story_list_options.get('sort-by', 'number'),
'reverse': story_list_options.get('reverse', False)}
self.expire_age = self.config.get('expire-age', '2 months')
def getServer(self, name=None):
for server in self.config['servers']:
if name is None or name == server['name']:
return server
return None
def printSample(self):
filename = 'share/boartty/examples'
print("""Boardtty requires a configuration file at ~/.boartty.yaml
If the file contains a password then permissions must be set to 0600.
Several sample configuration files were installed with Boardtty and are
available in %s in the root of the installation.
For more information, please see the README.
""" % (filename,))