Anita Kuno | bf3a97b | 2013-02-28 10:09:46 -0500 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # Copyright (c) 2013 Chmouel Boudjnah, eNovance |
| 3 | # |
| 4 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | # you may not use this file except in compliance with the License. |
| 6 | # You may obtain a copy of the License at |
| 7 | # |
| 8 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | # |
| 10 | # Unless required by applicable law or agreed to in writing, software |
| 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 13 | # License for the specific language governing permissions and limitations |
| 14 | # under the License. |
| 15 | |
| 16 | # This script is designed to generate rss feeds for subscription from updates |
| 17 | # to various gerrit tracked projects. It is intended to be run periodically, |
Anita Kuno | 96a159d | 2013-03-12 14:14:38 -0400 | [diff] [blame] | 18 | # for example hourly via cron. It takes an optional argument to specify the |
| 19 | # path to a configuration file. |
Anita Kuno | bf3a97b | 2013-02-28 10:09:46 -0500 | [diff] [blame] | 20 | # -*- encoding: utf-8 -*- |
Sergey Lukjanov | db8e707 | 2013-07-22 12:13:35 +0400 | [diff] [blame] | 21 | |
| 22 | from __future__ import print_function |
| 23 | |
Anita Kuno | bf3a97b | 2013-02-28 10:09:46 -0500 | [diff] [blame] | 24 | __author__ = "Chmouel Boudjnah <chmouel@chmouel.com>" |
Monty Taylor | 061919f | 2013-06-02 11:35:42 -0400 | [diff] [blame] | 25 | |
| 26 | import ConfigParser |
| 27 | import cStringIO |
Anita Kuno | bf3a97b | 2013-02-28 10:09:46 -0500 | [diff] [blame] | 28 | import datetime |
Monty Taylor | 061919f | 2013-06-02 11:35:42 -0400 | [diff] [blame] | 29 | import json |
Anita Kuno | bf3a97b | 2013-02-28 10:09:46 -0500 | [diff] [blame] | 30 | import os |
| 31 | import sys |
| 32 | import time |
Anita Kuno | bf3a97b | 2013-02-28 10:09:46 -0500 | [diff] [blame] | 33 | |
| 34 | import PyRSS2Gen |
Christian Berendt | 589ade6 | 2014-10-11 14:01:45 +0200 | [diff] [blame^] | 35 | import six.moves.urllib.request as urlrequest |
Anita Kuno | bf3a97b | 2013-02-28 10:09:46 -0500 | [diff] [blame] | 36 | |
| 37 | PROJECTS = ['openstack/nova', 'openstack/keystone', 'opensack/swift'] |
| 38 | JSON_URL = 'https://review.openstack.org/query' |
| 39 | DEBUG = False |
| 40 | OUTPUT_MODE = 'multiple' |
| 41 | |
| 42 | curdir = os.path.dirname(os.path.realpath(sys.argv[0])) |
| 43 | |
| 44 | |
| 45 | class ConfigurationError(Exception): |
| 46 | pass |
| 47 | |
| 48 | |
| 49 | def get_config(config, section, option, default=None): |
| 50 | if not config.has_section(section): |
| 51 | raise ConfigurationError("Invalid configuration, missing section: %s" % |
| 52 | section) |
| 53 | if config.has_option(section, option): |
| 54 | return config.get(section, option) |
| 55 | elif not default is None: |
| 56 | return default |
| 57 | else: |
| 58 | raise ConfigurationError("Invalid configuration, missing " |
| 59 | "section/option: %s/%s" % (section, option)) |
| 60 | |
| 61 | |
| 62 | def parse_ini(inifile): |
| 63 | ret = {} |
| 64 | if not os.path.exists(inifile): |
| 65 | return |
| 66 | config = ConfigParser.RawConfigParser(allow_no_value=True) |
| 67 | config.read(inifile) |
| 68 | |
| 69 | if config.has_section('swift'): |
| 70 | ret['swift'] = dict(config.items('swift')) |
| 71 | |
| 72 | ret['projects'] = get_config(config, 'general', 'projects', PROJECTS) |
| 73 | if type(ret['projects']) is not list: |
| 74 | ret['projects'] = [x.strip() for x in ret['projects'].split(',')] |
| 75 | ret['json_url'] = get_config(config, 'general', 'json_url', JSON_URL) |
| 76 | ret['debug'] = get_config(config, 'general', 'debug', DEBUG) |
| 77 | ret['output_mode'] = get_config(config, 'general', 'output_mode', |
| 78 | OUTPUT_MODE) |
| 79 | return ret |
| 80 | |
Anita Kuno | 96a159d | 2013-03-12 14:14:38 -0400 | [diff] [blame] | 81 | try: |
| 82 | conffile = sys.argv[1] |
| 83 | except IndexError: |
| 84 | conffile = os.path.join(curdir, '..', 'config', 'openstackwatch.ini') |
| 85 | CONFIG = parse_ini(conffile) |
Anita Kuno | bf3a97b | 2013-02-28 10:09:46 -0500 | [diff] [blame] | 86 | |
| 87 | |
| 88 | def debug(msg): |
| 89 | if DEBUG: |
Sergey Lukjanov | db8e707 | 2013-07-22 12:13:35 +0400 | [diff] [blame] | 90 | print(msg) |
Anita Kuno | bf3a97b | 2013-02-28 10:09:46 -0500 | [diff] [blame] | 91 | |
| 92 | |
| 93 | def get_javascript(project=None): |
Anita Kuno | c8cc7a5 | 2013-04-10 21:32:08 -0400 | [diff] [blame] | 94 | url = CONFIG['json_url'] |
Anita Kuno | bf3a97b | 2013-02-28 10:09:46 -0500 | [diff] [blame] | 95 | if project: |
| 96 | url += "+project:" + project |
Christian Berendt | 589ade6 | 2014-10-11 14:01:45 +0200 | [diff] [blame^] | 97 | fp = urlrequest.urlretrieve(url) |
Anita Kuno | bf3a97b | 2013-02-28 10:09:46 -0500 | [diff] [blame] | 98 | ret = open(fp[0]).read() |
| 99 | return ret |
| 100 | |
| 101 | |
| 102 | def parse_javascript(javascript): |
| 103 | for row in javascript.splitlines(): |
| 104 | try: |
| 105 | json_row = json.loads(row) |
| 106 | except(ValueError): |
| 107 | continue |
| 108 | if not json_row or not 'project' in json_row or \ |
| 109 | json_row['project'] not in CONFIG['projects']: |
| 110 | continue |
| 111 | yield json_row |
| 112 | |
| 113 | |
| 114 | def upload_rss(xml, output_object): |
Monty Taylor | 061919f | 2013-06-02 11:35:42 -0400 | [diff] [blame] | 115 | if 'swift' not in CONFIG: |
Sergey Lukjanov | db8e707 | 2013-07-22 12:13:35 +0400 | [diff] [blame] | 116 | print(xml) |
Anita Kuno | bf3a97b | 2013-02-28 10:09:46 -0500 | [diff] [blame] | 117 | return |
| 118 | |
| 119 | import swiftclient |
| 120 | cfg = CONFIG['swift'] |
| 121 | client = swiftclient.Connection(cfg['auth_url'], |
| 122 | cfg['username'], |
| 123 | cfg['password'], |
| 124 | auth_version=cfg.get('auth_version', |
| 125 | '2.0')) |
| 126 | try: |
| 127 | client.get_container(cfg['container']) |
| 128 | except(swiftclient.client.ClientException): |
| 129 | client.put_container(cfg['container']) |
| 130 | # eventual consistenties |
| 131 | time.sleep(1) |
| 132 | |
| 133 | client.put_object(cfg['container'], output_object, |
| 134 | cStringIO.StringIO(xml)) |
| 135 | |
| 136 | |
| 137 | def generate_rss(javascript, project=""): |
| 138 | title = "OpenStack %s watch RSS feed" % (project) |
| 139 | rss = PyRSS2Gen.RSS2( |
| 140 | title=title, |
| 141 | link="http://github.com/chmouel/openstackwatch.rss", |
tanlin | fa936da | 2014-02-13 15:41:41 +0800 | [diff] [blame] | 142 | description="The latest reviews about OpenStack, straight " |
Anita Kuno | bf3a97b | 2013-02-28 10:09:46 -0500 | [diff] [blame] | 143 | "from Gerrit.", |
| 144 | lastBuildDate=datetime.datetime.now() |
| 145 | ) |
| 146 | for row in parse_javascript(javascript): |
| 147 | author = row['owner']['name'] |
| 148 | author += " <%s>" % ('email' in row['owner'] and |
| 149 | row['owner']['email'] |
| 150 | or row['owner']['username']) |
| 151 | rss.items.append( |
| 152 | PyRSS2Gen.RSSItem( |
| 153 | title="%s [%s]: %s" % (os.path.basename(row['project']), |
| 154 | row['status'], |
| 155 | row['subject']), |
| 156 | author=author, |
| 157 | link=row['url'], |
| 158 | guid=PyRSS2Gen.Guid(row['id']), |
| 159 | description=row['subject'], |
| 160 | pubDate=datetime.datetime.fromtimestamp(row['lastUpdated']), |
| 161 | )) |
| 162 | return rss.to_xml() |
| 163 | |
| 164 | |
| 165 | def main(): |
| 166 | if CONFIG['output_mode'] == "combined": |
| 167 | upload_rss(generate_rss(get_javascript()), |
| 168 | CONFIG['swift']['combined_output_object']) |
| 169 | elif CONFIG['output_mode'] == "multiple": |
| 170 | for project in CONFIG['projects']: |
| 171 | upload_rss( |
| 172 | generate_rss(get_javascript(project), project=project), |
| 173 | "%s.xml" % (os.path.basename(project))) |
| 174 | |
| 175 | if __name__ == '__main__': |
| 176 | main() |