Switch to unified view

a/scripts/tracker-rip.py b/scripts/tracker-rip.py
1
#!/usr/bin/python
1
#!/usr/bin/python
2
import sys
3
import getpass
4
from urlparse import urljoin
2
5
3
from sys import stdout
6
from pyforge.lib import rest_api
4
import hmac, hashlib
5
from datetime import datetime
6
import urllib
7
from urlparse import urlparse, urljoin
8
import urllib2
9
import json
10
from formencode import variabledecode
11
7
12
def smart_str(s, encoding='utf-8', strings_only=False, errors='strict'):
8
SRC_CRED=dict(
13
    """
9
        api_key='b15b105ee580a8652616',
14
    Returns a bytestring version of 's', encoded as specified in 'encoding'.
10
        secret_key='d2a3315517ba81491ed7b7498636495023b8fb2d72e461457a20aa4b9e7ad032e9a8d07187f36661',
11
        http_username=raw_input('LDAP username: '),
12
        http_password=getpass.getpass('LDAP password: '))
13
SRC_SERVER='https://newforge.sf.geek.net/'
14
SRC_TOOL='/rest/p/forge/tickets/'
15
15
16
    If strings_only is True, don't convert (some) non-string-like objects.
16
# Credentials for sf-overlords
17
DST_CRED=dict(
18
    api_key='a4a88c67179137053d70',
19
    secret_key='fcc48a0c31459e99a88cc42cdd7f908fad78b283ca30a86caac1ab65036ff71fc195a18e56534dc5')
20
DST_SERVER='http://sourceforge.net/'
21
DST_TOOL='/rest/p/forge/tickets/'
17
22
18
    This function was borrowed from Django
23
FAKE_TICKET={
19
    """
24
    u'created_date': u'2010-03-08 17:29:42.802000',
20
    if strings_only and isinstance(s, (types.NoneType, int)):
25
    u'assigned_to_id': u'',
21
        return s
26
    u'assigned_to': u'',
22
    elif not isinstance(s, basestring):
27
    u'custom_fields': {'_component':'', '_size':0, '_priority':'', '_type':''},
23
        try:
28
    u'description': u'Ticket was not present in source',
24
            return str(s)
29
    u'milestone': u'',
25
        except UnicodeEncodeError:
30
    u'reported_by': u'',
26
            if isinstance(s, Exception):
31
    u'reported_by_id': u'',
27
                # An Exception subclass containing non-ASCII data that doesn't
32
    u'status': u'closed',
28
                # know how to print itself properly. We shouldn't raise a
33
    u'sub_ids': [],
29
                # further exception.
34
    u'summary': u'Placeholder ticket',
30
                return ' '.join([smart_str(arg, encoding, strings_only,
35
    u'super_id': u'None'}
31
                        errors) for arg in s])
32
            return unicode(s).encode(encoding, errors)
33
    elif isinstance(s, unicode):
34
        r = s.encode(encoding, errors)
35
        return r
36
    elif s and encoding != 'utf-8':
37
        return s.decode('utf-8', errors).encode(encoding, errors)
38
    else:
39
        return s
40
36
41
def generate_smart_str(params):
37
def main():
42
    for (key, value) in params:
38
    src_cli = rest_api.RestClient(
43
        yield smart_str(key), smart_str(value)
39
        base_uri=SRC_SERVER,
40
        **SRC_CRED)
41
    dst_cli = rest_api.RestClient(
42
        base_uri=DST_SERVER,
43
        **DST_CRED)
44
    src = TicketAPI(src_cli, SRC_TOOL)
45
    dst = TicketAPI(dst_cli, DST_TOOL)
46
    for ticket in src.iter_tickets(check=True):
47
        print 'Migrating ticket %s:\n%s' % (ticket['ticket_num'], ticket)
48
        print 'Create ticket on %s' % DST_SERVER
49
        dst.create_ticket(ticket)
50
        print 'Create discussion on %s' % DST_SERVER
51
        src_thread = src.load_thread(ticket)
52
        if not src_thread or not src_thread['posts']:
53
            print '... no posts'
54
            continue
55
        dst_thread = dst.load_thread(ticket)
56
        slug_map = {}
57
        for post in src.iter_posts(src_thread):
58
            print '... migrate post %s:\n%r' % (post['slug'], post['text'])
59
            dst.create_post(dst_thread, post, slug_map)
44
60
45
def urlencode(params):
61
class TicketAPI(object):
46
    """
47
    A version of Python's urllib.urlencode() function that can operate on
48
    unicode strings. The parameters are first case to UTF-8 encoded strings and
49
    then encoded as per normal.
50
    """
51
    return urllib.urlencode([i for i in generate_smart_str(params)])
52
62
63
    def __init__(self, client, path):
64
        self.client = client
65
        self.path = path
53
66
54
class Signer(object):
67
    def iter_tickets(self, min_ticket=1, max_ticket=None, check=False):
68
        if check:
69
            tickets = self.client.request('GET', self.path)['tickets']
70
            valid_tickets = set(t['ticket_num'] for t in tickets)
71
            max_valid_ticket = max(valid_tickets)
72
        cur_ticket = min_ticket
73
        while True:
74
            if check and cur_ticket not in valid_tickets:
75
                if cur_ticket > max_valid_ticket: break
76
                yield dict(FAKE_TICKET, ticket_num=cur_ticket)
77
                cur_ticket += 1
78
                continue
79
            ticket = self.client.request('GET', self.ticket_path(cur_ticket))['ticket']
80
            if ticket is None: break
81
            yield ticket
82
            cur_ticket += 1
83
            if max_ticket and cur_ticket > max_ticket: break
55
84
56
    def __init__(self, secret_key, api_key):
85
    def load_thread(self, ticket):
57
        self.secret_key = secret_key
86
        discussion = self.client.request('GET', self.discussion_path())['discussion']
58
        self.api_key = api_key
87
        for thd in discussion['threads']:
88
            if thd['subject'].startswith('#%d ' % ticket['ticket_num']):
89
                break
90
        else:
91
            return None
92
        thread = self.client.request(
93
            'GET',self.thread_path(thd['_id']))['thread']
94
        return thread
59
95
60
    def __call__(self, path, params):
96
    def iter_posts(self, thread):
61
        params.append(('api_key', self.api_key))
97
        for p in sorted(thread['posts'], key=lambda p:p['slug']):
62
        params.append(('api_timestamp', datetime.utcnow().isoformat()))
98
            post = self.client.request(
63
        message = path + '?' + urlencode(sorted(params))
99
                'GET', self.post_path(thread['_id'], p['slug']))['post']
64
        digest = hmac.new(self.secret_key, message, hashlib.sha256).hexdigest()
100
            yield post
65
        params.append(('api_signature', digest))
66
        return params
67
101
68
102
    def create_ticket(self, ticket):
69
class TicketIterator(object):
103
        ticket = dict(ticket, labels='')
70
104
        ticket['description'] = 'Created by: %s\nCreated date: %s\nAssigned to:%s\n\n%s' % (
71
    def __init__(self, secret_key, api_key, url, max_ticket, min_ticket=1):
105
            ticket['reported_by'], ticket['created_date'], ticket['assigned_to'], ticket['description'])
72
        self.sign = Signer(secret_key, api_key)
73
        self.cur_ticket_num = min_ticket
74
        self.max_ticket_num = max_ticket
75
        self.url = url
76
77
    def __iter__(self):
78
        return self
79
80
    def next(self):
81
        if self.cur_ticket_num > self.max_ticket_num:
82
            raise StopIteration
83
        url = urljoin(self.url, str(self.cur_ticket_num))+'/'
84
        self.cur_ticket_num += 1
85
        params = self.sign(urlparse(url).path, [])
86
        try:
87
            f = urllib2.urlopen(url+'?'+urlencode(params))
88
        except urllib2.HTTPError, e:
89
            if e.code == 404:
90
                raise StopIteration
91
            else:
92
                raise
93
        ticket = json.loads(f.read())['ticket'] or {}
94
        for bad_key in ('assigned_to_id', 'created_date', 'reported_by', 'reported_by_id', 'super_id', 'sub_ids', '_id'):
106
        for bad_key in ('assigned_to_id', 'created_date', 'reported_by', 'reported_by_id', 'super_id', 'sub_ids', '_id'):
95
            if bad_key in ticket:
107
            if bad_key in ticket:
96
                del ticket[bad_key]
108
                del ticket[bad_key]
109
        ticket.setdefault('labels', '')
110
        ticket['custom_fields'].setdefault('_size', 0)
111
        ticket['custom_fields'].setdefault('_priority', 'low')
112
        ticket['custom_fields'].setdefault('_type', 'Bug')
113
        ticket['custom_fields'].setdefault('_type', 'Component')
114
        if ticket['custom_fields']['_size'] is None:
115
            ticket['custom_fields']['_size'] = 0
116
        if ticket['milestone'] not in ('backlog', 'public2',  'GA', 'post-GA'):
97
        ticket['labels'] = ''
117
            ticket['milestone'] = ''
98
        return ticket
118
        if ticket['status'] not in 'open in-progress code-review validation closed'.split():
119
            ticket['status'] = 'open'
120
        r = self.client.request('POST', self.new_ticket_path(), ticket_form=ticket)
121
        self.client.request(
122
            'POST', self.ticket_path(r['ticket']['ticket_num'], 'save'),
123
            ticket_form=ticket)
99
124
125
    def create_post(self, thread, post, slug_map):
126
        text = 'Post by %s:\n%s' % (
127
            post['author'], post['text'])
128
        if '/' in post['slug']:
129
            parent_post = slug_map[post['slug'].rsplit('/', 1)[0]]
130
            new_post = self.client.request(
131
                'POST', self.post_path(thread['_id'], parent_post, 'reply'),
132
                text=text)['post']
133
        else:
134
            new_post = self.client.request(
135
                'POST', self.thread_path(thread['_id'], 'new'),
136
                text=text)['post']
137
        slug_map[post['slug']] = new_post['slug']
138
        return new_post
100
139
101
class TicketPoster(object):
140
    def new_ticket_path(self):
141
        return urljoin(self.path, 'new')
102
142
103
    def __init__(self, secret_key, api_key, url):
143
    def ticket_path(self, ticket_num, suffix=''):
104
        self.sign = Signer(secret_key, api_key)
144
        return urljoin(self.path, str(ticket_num)) + '/' + suffix
105
        self.url = urljoin(url, 'new')
106
145
107
    def __call__(self, ticket):
146
    def discussion_path(self):
108
        ticket = variabledecode.variable_encode(ticket, add_repetitions=False)
147
        return '%s_discuss/' % (self.path)
109
        params = [('ticket_form', json.dumps(ticket))]
110
        params = self.sign(urlparse(self.url).path, params)
111
        try:
112
            f = urllib2.urlopen(self.url, urlencode(params))
113
        except urllib2.HTTPError, e:
114
            stdout.write(e.read())
115
148
149
    def thread_path(self, thread_id, suffix=''):
150
        return '%s_discuss/thread/%s/%s' % (self.path, thread_id, suffix)
116
151
117
def main():
152
    def post_path(self, thread_id, post_slug, suffix=''):
118
    pw_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
153
        return '%s_discuss/thread/%s/%s/%s' % (self.path, thread_id, post_slug, suffix)
119
    pw_mgr.add_password(None, 'https://newforge.sf.geek.net/', '<REALM USER>', '<REALM PASSWORD>')
120
    auth_handler = urllib2.HTTPBasicAuthHandler(pw_mgr)
121
    opener = urllib2.build_opener(auth_handler)
122
    urllib2.install_opener(opener)
123
154
124
    newforge = TicketIterator(
155
def pm(etype, value, tb): # pragma no cover
125
        secret_key='<YOUR NEWFORGE SECRET KEY>',
156
    import pdb, traceback
126
        api_key='<YOUR NEWFORGE API KEY>',
157
    try:
127
        url='https://newforge.sf.geek.net/rest/p/forge/tickets/',
158
        from IPython.ipapi import make_session; make_session()
128
#        max_ticket=672,
159
        from IPython.Debugger import Pdb
129
        max_ticket=5,
160
        sys.stderr.write('Entering post-mortem IPDB shell\n')
130
        min_ticket=1)
161
        p = Pdb(color_scheme='Linux')
162
        p.reset()
163
        p.setup(None, tb)
164
        p.print_stack_trace()
165
        sys.stderr.write('%s: %s\n' % ( etype, value))
166
        p.cmdloop()
167
        p.forget()
168
        # p.interaction(None, tb)
169
    except ImportError:
170
        sys.stderr.write('Entering post-mortem PDB shell\n')
171
        traceback.print_exception(etype, value, tb)
172
        pdb.post_mortem(tb)
131
173
132
    # testing with a demo project, update this URL to a new tracker in the forge project
174
sys.excepthook = pm
133
    post_to_production = TicketPoster(
134
        secret_key='<YOUR SOURCEFORGE SECRET KEY>',
135
        api_key='<YOUR SOURCEFORGE API KEY>',
136
        url='https://sourceforge.net/rest/p/wolftest/newtix/')
137
138
    for ticket in newforge:
139
        post_to_production(ticket)
140
141
175
142
if __name__ == '__main__':
176
if __name__ == '__main__':
143
    main()
177
    main()