| #!/usr/bin/python |
| |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you 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 |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # 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 sys |
| import getpass |
| from urlparse import urljoin |
| |
| from allura.lib import rest_api |
| |
| SRC_CRED=dict( |
| api_key='c03efc6cca1cf78be9e9', |
| secret_key='575eda2f25f6490d8cfe5d02f2506c010112894d0ea10660e43157a87a7e620c61ac06397b028af1', |
| http_username=raw_input('LDAP username: '), |
| http_password=getpass.getpass('LDAP password: ')) |
| SRC_SERVER='https://newforge.sf.geek.net/' |
| SRC_TOOL='/rest/p/forge/tickets/' |
| |
| # Credentials for sf-overlords |
| # DST_CRED=dict( |
| # api_key='a4a88c67179137053d70', |
| # secret_key='fcc48a0c31459e99a88cc42cdd7f908fad78b283ca30a86caac1ab65036ff71fc195a18e56534dc5') |
| # DST_SERVER='http://sourceforge.net/' |
| # DST_TOOL='/rest/p/allura/tickets/' |
| DST_CRED=dict( |
| api_key='aa7244645424513d9636', |
| secret_key='cd1d97be98497f7b615b297aa2061177ddf6d42b95a8484193f84690486694234dbf817efc3b2d6e') |
| DST_SERVER='http://localhost:8080/' |
| DST_TOOL='/rest/p/test/bugs/' |
| |
| FAKE_TICKET={ |
| u'created_date': u'2010-03-08 17:29:42.802000', |
| u'assigned_to_id': u'', |
| u'assigned_to': u'', |
| u'custom_fields': {'_component':'', '_size':0, '_priority':'', '_type':''}, |
| u'description': u'Ticket was not present in source', |
| u'milestone': u'', |
| u'reported_by': u'', |
| u'reported_by_id': u'', |
| u'status': u'closed', |
| u'sub_ids': [], |
| u'summary': u'Placeholder ticket', |
| u'super_id': u'None'} |
| |
| def main(): |
| src_cli = rest_api.RestClient( |
| base_uri=SRC_SERVER, |
| **SRC_CRED) |
| dst_cli = rest_api.RestClient( |
| base_uri=DST_SERVER, |
| **DST_CRED) |
| src = TicketAPI(src_cli, SRC_TOOL) |
| dst = TicketAPI(dst_cli, DST_TOOL) |
| for ticket in src.iter_tickets(min_ticket=3, check=True): |
| print 'Migrating ticket %s:\n%s' % (ticket['ticket_num'], ticket) |
| print 'Create ticket on %s' % DST_SERVER |
| dst.create_ticket(ticket) |
| print 'Create discussion on %s' % DST_SERVER |
| src_thread = src.load_thread(ticket) |
| if not src_thread or not src_thread['posts']: |
| print '... no posts' |
| continue |
| dst_thread = dst.load_thread(ticket) |
| slug_map = {} |
| for post in src.iter_posts(src_thread): |
| print '... migrate post %s:\n%r' % (post['slug'], post['text']) |
| dst.create_post(dst_thread, post, slug_map) |
| |
| class TicketAPI(object): |
| |
| def __init__(self, client, path): |
| self.client = client |
| self.path = path |
| |
| def iter_tickets(self, min_ticket=1, max_ticket=None, check=False): |
| if check: |
| tickets = self.client.request('GET', self.path)['tickets'] |
| valid_tickets = set(t['ticket_num'] for t in tickets) |
| max_valid_ticket = max(valid_tickets) |
| cur_ticket = min_ticket |
| while True: |
| if check and cur_ticket not in valid_tickets: |
| if cur_ticket > max_valid_ticket: break |
| yield dict(FAKE_TICKET, ticket_num=cur_ticket) |
| cur_ticket += 1 |
| continue |
| ticket = self.client.request('GET', self.ticket_path(cur_ticket))['ticket'] |
| if ticket is None: break |
| yield ticket |
| cur_ticket += 1 |
| if max_ticket and cur_ticket > max_ticket: break |
| |
| def load_thread(self, ticket): |
| discussion = self.client.request('GET', self.discussion_path())['discussion'] |
| for thd in discussion['threads']: |
| if thd['subject'].startswith('#%d ' % ticket['ticket_num']): |
| break |
| else: |
| return None |
| thread = self.client.request( |
| 'GET',self.thread_path(thd['_id']))['thread'] |
| return thread |
| |
| def iter_posts(self, thread): |
| for p in sorted(thread['posts'], key=lambda p:p['slug']): |
| post = self.client.request( |
| 'GET', self.post_path(thread['_id'], p['slug']))['post'] |
| yield post |
| |
| def create_ticket(self, ticket): |
| ticket = dict(ticket, labels='') |
| ticket['description'] = 'Created by: %s\nCreated date: %s\nAssigned to:%s\n\n%s' % ( |
| ticket['reported_by'], ticket['created_date'], ticket['assigned_to'], ticket['description']) |
| for bad_key in ('assigned_to_id', 'created_date', 'reported_by', 'reported_by_id', 'super_id', 'sub_ids', '_id'): |
| if bad_key in ticket: |
| del ticket[bad_key] |
| ticket.setdefault('labels', '') |
| ticket['custom_fields'].setdefault('_size', 0) |
| ticket['custom_fields'].setdefault('_priority', 'low') |
| ticket['custom_fields'].setdefault('_type', 'Bug') |
| ticket['custom_fields'].setdefault('_type', 'Component') |
| if ticket['custom_fields']['_size'] is None: |
| ticket['custom_fields']['_size'] = 0 |
| if ticket['milestone'] not in ('backlog', 'public2', 'GA', 'post-GA'): |
| ticket['milestone'] = '' |
| if ticket['status'] not in 'open in-progress code-review validation closed'.split(): |
| ticket['status'] = 'open' |
| r = self.client.request('POST', self.new_ticket_path(), ticket_form=ticket) |
| self.client.request( |
| 'POST', self.ticket_path(r['ticket']['ticket_num'], 'save'), |
| ticket_form=ticket) |
| |
| def create_post(self, thread, post, slug_map): |
| text = 'Post by %s:\n%s' % ( |
| post['author'], post['text']) |
| if '/' in post['slug']: |
| parent_post = slug_map[post['slug'].rsplit('/', 1)[0]] |
| new_post = self.client.request( |
| 'POST', self.post_path(thread['_id'], parent_post, 'reply'), |
| text=text)['post'] |
| else: |
| new_post = self.client.request( |
| 'POST', self.thread_path(thread['_id'], 'new'), |
| text=text)['post'] |
| slug_map[post['slug']] = new_post['slug'] |
| return new_post |
| |
| def new_ticket_path(self): |
| return urljoin(self.path, 'new') |
| |
| def ticket_path(self, ticket_num, suffix=''): |
| return urljoin(self.path, str(ticket_num)) + '/' + suffix |
| |
| def discussion_path(self): |
| return '%s_discuss/' % (self.path) |
| |
| def thread_path(self, thread_id, suffix=''): |
| return '%s_discuss/thread/%s/%s' % (self.path, thread_id, suffix) |
| |
| def post_path(self, thread_id, post_slug, suffix=''): |
| return '%s_discuss/thread/%s/%s/%s' % (self.path, thread_id, post_slug, suffix) |
| |
| def pm(etype, value, tb): # pragma no cover |
| import pdb, traceback |
| try: |
| from IPython.ipapi import make_session; make_session() |
| from IPython.Debugger import Pdb |
| sys.stderr.write('Entering post-mortem IPDB shell\n') |
| p = Pdb(color_scheme='Linux') |
| p.reset() |
| p.setup(None, tb) |
| p.print_stack_trace() |
| sys.stderr.write('%s: %s\n' % ( etype, value)) |
| p.cmdloop() |
| p.forget() |
| # p.interaction(None, tb) |
| except ImportError: |
| sys.stderr.write('Entering post-mortem PDB shell\n') |
| traceback.print_exception(etype, value, tb) |
| pdb.post_mortem(tb) |
| |
| sys.excepthook = pm |
| |
| if __name__ == '__main__': |
| main() |