changeset 2379:7ac09514a178 beta

created rhodecode-api binary script for working with api via cli - created docs - moved the backup script to bin folder
author Marcin Kuzminski <marcin@python-works.com>
date Sun, 03 Jun 2012 20:35:13 +0200
parents 04ef27ce939e
children 0c7dc3402efa
files .hgignore docs/api/api.rst docs/changelog.rst rhodecode/bin/__init__.py rhodecode/bin/rhodecode_api.py rhodecode/bin/rhodecode_backup.py rhodecode/controllers/api/__init__.py rhodecode/controllers/api/api.py rhodecode/lib/backup_manager.py rhodecode/templates/admin/admin_log.html setup.py
diffstat 10 files changed, 394 insertions(+), 119 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Sun Jun 03 20:24:02 2012 +0200
+++ b/.hgignore	Sun Jun 03 20:35:13 2012 +0200
@@ -19,3 +19,4 @@
 ^RhodeCode\.egg-info$
 ^rc\.ini$
 ^fabfile.py
+^\.rhodecode$
--- a/docs/api/api.rst	Sun Jun 03 20:24:02 2012 +0200
+++ b/docs/api/api.rst	Sun Jun 03 20:35:13 2012 +0200
@@ -59,6 +59,47 @@
 calling api *error* key from response will contain failure description
 and result will be null.
 
+
+API CLIENT
+++++++++++
+
+From version 1.4 RhodeCode adds a binary script that allows to easily
+communicate with API. After installing RhodeCode a `rhodecode-api` script
+will be available.
+
+To get started quickly simply run::
+
+  rhodecode-api _create_config --apikey=<youapikey> --apihost=<rhodecode host>
+ 
+This will create a file named .config in the directory you executed it storing
+json config file with credentials. You can skip this step and always provide
+both of the arguments to be able to communicate with server
+
+
+after that simply run any api command for example get_repo::
+ 
+ rhodecode-api get_repo
+
+ calling {"api_key": "<apikey>", "id": 75, "args": {}, "method": "get_repo"} to http://127.0.0.1:5000
+ rhodecode said:
+ {'error': 'Missing non optional `repoid` arg in JSON DATA',
+  'id': 75,
+  'result': None}
+
+Ups looks like we forgot to add an argument
+
+Let's try again now giving the repoid as parameters::
+
+    rhodecode-api get_repo repoid:rhodecode   
+ 
+    calling {"api_key": "<apikey>", "id": 39, "args": {"repoid": "rhodecode"}, "method": "get_repo"} to http://127.0.0.1:5000
+    rhodecode said:
+    {'error': None,
+     'id': 39,
+     'result': <json data...>}
+
+
+
 API METHODS
 +++++++++++
 
--- a/docs/changelog.rst	Sun Jun 03 20:24:02 2012 +0200
+++ b/docs/changelog.rst	Sun Jun 03 20:35:13 2012 +0200
@@ -22,6 +22,7 @@
 - #465 mentions autocomplete inside comments boxes
 - #469 added --update-only option to whoosh to re-index only given list
   of repos in index 
+- rhodecode-api CLI client
 
 fixes
 +++++
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/bin/rhodecode_api.py	Sun Jun 03 20:35:13 2012 +0200
@@ -0,0 +1,216 @@
+# -*- coding: utf-8 -*-
+"""
+    rhodecode.bin.backup_manager
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Api CLI client for RhodeCode
+
+    :created_on: Jun 3, 2012
+    :author: marcink
+    :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
+    :license: GPLv3, see COPYING for more details.
+"""
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import with_statement
+import os
+import sys
+import random
+import urllib2
+import pprint
+import argparse
+
+try:
+    from rhodecode.lib.ext_json import json
+except ImportError:
+    try:
+        import simplejson as json
+    except ImportError:
+        import json
+
+
+CONFIG_NAME = '.rhodecode'
+
+
+class RcConf(object):
+    """
+    RhodeCode config for API
+
+    conf = RcConf()
+    conf['key']
+
+    """
+
+    def __init__(self, autoload=True, autocreate=False, config=None):
+        self._conf_name = CONFIG_NAME
+        self._conf = {}
+        if autocreate:
+            self.make_config(config)
+        if autoload:
+            self._conf = self.load_config()
+
+    def __getitem__(self, key):
+        return self._conf[key]
+
+    def __nonzero__(self):
+        if self._conf:
+            return True
+        return False
+
+    def __eq__(self):
+        return self._conf.__eq__()
+
+    def __repr__(self):
+        return 'RcConf<%s>' % self._conf.__repr__()
+
+    def make_config(self, config):
+        """
+        Saves given config as a JSON dump in the _conf_name location
+
+        :param config:
+        :type config:
+        """
+        with open(self._conf_name, 'wb') as f:
+            json.dump(config, f, indent=4)
+            sys.stdout.write('Updated conf\n')
+
+    def update_config(self, new_config):
+        """
+        Reads the JSON config updates it's values with new_config and
+        saves it back as JSON dump
+
+        :param new_config:
+        """
+        config = {}
+        try:
+            with open(self._conf_name, 'rb') as conf:
+                config = json.load(conf)
+        except IOError, e:
+            sys.stderr.write(str(e) + '\n')
+
+        config.update(new_config)
+        self.make_config(config)
+
+    def load_config(self):
+        """
+        Loads config from file and returns loaded JSON object
+        """
+        try:
+            with open(self._conf_name, 'rb') as conf:
+                return  json.load(conf)
+        except IOError, e:
+            #sys.stderr.write(str(e) + '\n')
+            pass
+
+
+def api_call(apikey, apihost, method=None, **kw):
+    """
+    Api_call wrapper for RhodeCode
+
+    :param apikey:
+    :param apihost:
+    :param method:
+    """
+    def _build_data(random_id):
+        """
+        Builds API data with given random ID
+
+        :param random_id:
+        :type random_id:
+        """
+        return {
+            "id": random_id,
+            "api_key": apikey,
+            "method": method,
+            "args": kw
+        }
+
+    if not method:
+        raise Exception('please specify method name !')
+    id_ = random.randrange(1, 200)
+    req = urllib2.Request('%s/_admin/api' % apihost,
+                      data=json.dumps(_build_data(id_)),
+                      headers={'content-type': 'text/plain'})
+    print 'calling %s to %s' % (req.get_data(), apihost)
+    ret = urllib2.urlopen(req)
+    json_data = json.loads(ret.read())
+    id_ret = json_data['id']
+    _formatted_json = pprint.pformat(json_data)
+    if id_ret == id_:
+        print 'rhodecode said:\n%s' % (_formatted_json)
+    else:
+        raise Exception('something went wrong. '
+                        'ID mismatch got %s, expected %s | %s' % (
+                                            id_ret, id_, _formatted_json))
+
+
+def argparser(argv):
+    usage = ("rhodecode_api [-h] [--apikey APIKEY] [--apihost APIHOST] "
+             "_create_config or METHOD <key:val> <key2:val> ...")
+
+    parser = argparse.ArgumentParser(description='RhodeCode API cli',
+                                     usage=usage)
+
+    ## config
+    group = parser.add_argument_group('config')
+    group.add_argument('--apikey', help='api access key')
+    group.add_argument('--apihost', help='api host')
+
+    group = parser.add_argument_group('API')
+    group.add_argument('method', metavar='METHOD', type=str,
+            help='API method name to call followed by key:value attributes',
+    )
+
+    args, other = parser.parse_known_args()
+    return parser, args, other
+
+
+def main(argv=None):
+    """
+    Main execution function for cli
+
+    :param argv:
+    :type argv:
+    """
+    if argv is None:
+        argv = sys.argv
+
+    conf = None
+    parser, args, other = argparser(argv)
+
+    api_credentials_given = (args.apikey and args.apihost)
+    if args.method == '_create_config':
+        if not api_credentials_given:
+            raise parser.error('_create_config requires --apikey and --apihost')
+        conf = RcConf(autocreate=True, config={'apikey': args.apikey,
+                                               'apihost': args.apihost})
+        sys.stdout.write('Create new config in %s\n' % CONFIG_NAME)
+
+    if not conf:
+        conf = RcConf(autoload=True)
+        if not conf:
+            if not api_credentials_given:
+                parser.error('Could not find config file and missing '
+                             '--apikey or --apihost in params')
+
+    apikey = args.apikey or conf['apikey']
+    host = args.apihost or conf['apihost']
+    method = args.method
+    margs = dict(map(lambda s: s.split(':', 1), other))
+
+    api_call(apikey, host, method, **margs)
+    return 0
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/bin/rhodecode_backup.py	Sun Jun 03 20:35:13 2012 +0200
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+"""
+    rhodecode.bin.backup_manager
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Repositories backup manager, it allows to backups all
+    repositories and send it to backup server using RSA key via ssh.
+
+    :created_on: Feb 28, 2010
+    :author: marcink
+    :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
+    :license: GPLv3, see COPYING for more details.
+"""
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import sys
+
+import logging
+import tarfile
+import datetime
+import subprocess
+
+logging.basicConfig(level=logging.DEBUG,
+                    format="%(asctime)s %(levelname)-5.5s %(message)s")
+
+
+class BackupManager(object):
+    def __init__(self, repos_location, rsa_key, backup_server):
+        today = datetime.datetime.now().weekday() + 1
+        self.backup_file_name = "rhodecode_repos.%s.tar.gz" % today
+
+        self.id_rsa_path = self.get_id_rsa(rsa_key)
+        self.repos_path = self.get_repos_path(repos_location)
+        self.backup_server = backup_server
+
+        self.backup_file_path = '/tmp'
+
+        logging.info('starting backup for %s', self.repos_path)
+        logging.info('backup target %s', self.backup_file_path)
+
+    def get_id_rsa(self, rsa_key):
+        if not os.path.isfile(rsa_key):
+            logging.error('Could not load id_rsa key file in %s', rsa_key)
+            sys.exit()
+        return rsa_key
+
+    def get_repos_path(self, path):
+        if not os.path.isdir(path):
+            logging.error('Wrong location for repositories in %s', path)
+            sys.exit()
+        return path
+
+    def backup_repos(self):
+        bckp_file = os.path.join(self.backup_file_path, self.backup_file_name)
+        tar = tarfile.open(bckp_file, "w:gz")
+
+        for dir_name in os.listdir(self.repos_path):
+            logging.info('backing up %s', dir_name)
+            tar.add(os.path.join(self.repos_path, dir_name), dir_name)
+        tar.close()
+        logging.info('finished backup of mercurial repositories')
+
+    def transfer_files(self):
+        params = {
+                  'id_rsa_key': self.id_rsa_path,
+                  'backup_file': os.path.join(self.backup_file_path,
+                                             self.backup_file_name),
+                  'backup_server': self.backup_server
+                  }
+        cmd = ['scp', '-l', '40000', '-i', '%(id_rsa_key)s' % params,
+               '%(backup_file)s' % params,
+               '%(backup_server)s' % params]
+
+        subprocess.call(cmd)
+        logging.info('Transfered file %s to %s', self.backup_file_name, cmd[4])
+
+    def rm_file(self):
+        logging.info('Removing file %s', self.backup_file_name)
+        os.remove(os.path.join(self.backup_file_path, self.backup_file_name))
+
+if __name__ == "__main__":
+
+    repo_location = '/home/repo_path'
+    backup_server = 'root@192.168.1.100:/backups/mercurial'
+    rsa_key = '/home/id_rsa'
+
+    B_MANAGER = BackupManager(repo_location, rsa_key, backup_server)
+    B_MANAGER.backup_repos()
+    B_MANAGER.transfer_files()
+    B_MANAGER.rm_file()
--- a/rhodecode/controllers/api/__init__.py	Sun Jun 03 20:24:02 2012 +0200
+++ b/rhodecode/controllers/api/__init__.py	Sun Jun 03 20:35:13 2012 +0200
@@ -57,15 +57,16 @@
         return str(self.message)
 
 
-def jsonrpc_error(message, code=None):
+def jsonrpc_error(message, retid=None, code=None):
     """
     Generate a Response object with a JSON-RPC error body
     """
     from pylons.controllers.util import Response
-    resp = Response(body=json.dumps(dict(id=None, result=None, error=message)),
-                    status=code,
-                    content_type='application/json')
-    return resp
+    return Response(
+            body=json.dumps(dict(id=retid, result=None, error=message)),
+            status=code,
+            content_type='application/json'
+    )
 
 
 class JSONRPCController(WSGIController):
@@ -94,9 +95,11 @@
         Parse the request body as JSON, look up the method on the
         controller and if it exists, dispatch to it.
         """
+        self._req_id = None
         if 'CONTENT_LENGTH' not in environ:
             log.debug("No Content-Length")
-            return jsonrpc_error(message="No Content-Length in request")
+            return jsonrpc_error(retid=self._req_id,
+                                 message="No Content-Length in request")
         else:
             length = environ['CONTENT_LENGTH'] or 0
             length = int(environ['CONTENT_LENGTH'])
@@ -104,7 +107,8 @@
 
         if length == 0:
             log.debug("Content-Length is 0")
-            return jsonrpc_error(message="Content-Length is 0")
+            return jsonrpc_error(retid=self._req_id,
+                                 message="Content-Length is 0")
 
         raw_body = environ['wsgi.input'].read(length)
 
@@ -112,7 +116,8 @@
             json_body = json.loads(urllib.unquote_plus(raw_body))
         except ValueError, e:
             # catch JSON errors Here
-            return jsonrpc_error(message="JSON parse error ERR:%s RAW:%r" \
+            return jsonrpc_error(retid=self._req_id,
+                                 message="JSON parse error ERR:%s RAW:%r" \
                                  % (e, urllib.unquote_plus(raw_body)))
 
         # check AUTH based on API KEY
@@ -126,22 +131,26 @@
                                             self._request_params)
             )
         except KeyError, e:
-            return jsonrpc_error(message='Incorrect JSON query missing %s' % e)
+            return jsonrpc_error(retid=self._req_id,
+                                 message='Incorrect JSON query missing %s' % e)
 
         # check if we can find this session using api_key
         try:
             u = User.get_by_api_key(self._req_api_key)
             if u is None:
-                return jsonrpc_error(message='Invalid API KEY')
+                return jsonrpc_error(retid=self._req_id,
+                                     message='Invalid API KEY')
             auth_u = AuthUser(u.user_id, self._req_api_key)
         except Exception, e:
-            return jsonrpc_error(message='Invalid API KEY')
+            return jsonrpc_error(retid=self._req_id,
+                                 message='Invalid API KEY')
 
         self._error = None
         try:
             self._func = self._find_method()
         except AttributeError, e:
-            return jsonrpc_error(message=str(e))
+            return jsonrpc_error(retid=self._req_id,
+                                 message=str(e))
 
         # now that we have a method, add self._req_params to
         # self.kargs and dispatch control to WGIController
@@ -164,9 +173,12 @@
         USER_SESSION_ATTR = 'apiuser'
 
         if USER_SESSION_ATTR not in arglist:
-            return jsonrpc_error(message='This method [%s] does not support '
-                                 'authentication (missing %s param)' %
-                                 (self._func.__name__, USER_SESSION_ATTR))
+            return jsonrpc_error(
+                retid=self._req_id,
+                message='This method [%s] does not support '
+                         'authentication (missing %s param)' % (
+                                    self._func.__name__, USER_SESSION_ATTR)
+            )
 
         # get our arglist and check if we provided them as args
         for arg, default in func_kwargs.iteritems():
@@ -179,6 +191,7 @@
             # NotImplementedType (default_empty)
             if (default == default_empty and arg not in self._request_params):
                 return jsonrpc_error(
+                    retid=self._req_id,
                     message=(
                         'Missing non optional `%s` arg in JSON DATA' % arg
                     )
--- a/rhodecode/controllers/api/api.py	Sun Jun 03 20:24:02 2012 +0200
+++ b/rhodecode/controllers/api/api.py	Sun Jun 03 20:35:13 2012 +0200
@@ -389,7 +389,7 @@
 
         repo = RepoModel().get_repo(repoid)
         if repo is None:
-            raise JSONRPCError('unknown repository %s' % repo)
+            raise JSONRPCError('unknown repository "%s"' % (repo or repoid))
 
         members = []
         for user in repo.repo_to_perm:
--- a/rhodecode/lib/backup_manager.py	Sun Jun 03 20:24:02 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,102 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-    rhodecode.lib.backup_manager
-    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-    Mercurial repositories backup manager, it allows to backups all
-    repositories and send it to backup server using RSA key via ssh.
-
-    :created_on: Feb 28, 2010
-    :author: marcink
-    :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
-    :license: GPLv3, see COPYING for more details.
-"""
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU 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 General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import os
-import sys
-
-import logging
-import tarfile
-import datetime
-import subprocess
-
-logging.basicConfig(level=logging.DEBUG,
-                    format="%(asctime)s %(levelname)-5.5s %(message)s")
-
-
-class BackupManager(object):
-    def __init__(self, repos_location, rsa_key, backup_server):
-        today = datetime.datetime.now().weekday() + 1
-        self.backup_file_name = "mercurial_repos.%s.tar.gz" % today
-
-        self.id_rsa_path = self.get_id_rsa(rsa_key)
-        self.repos_path = self.get_repos_path(repos_location)
-        self.backup_server = backup_server
-
-        self.backup_file_path = '/tmp'
-
-        logging.info('starting backup for %s', self.repos_path)
-        logging.info('backup target %s', self.backup_file_path)
-
-    def get_id_rsa(self, rsa_key):
-        if not os.path.isfile(rsa_key):
-            logging.error('Could not load id_rsa key file in %s', rsa_key)
-            sys.exit()
-        return rsa_key
-
-    def get_repos_path(self, path):
-        if not os.path.isdir(path):
-            logging.error('Wrong location for repositories in %s', path)
-            sys.exit()
-        return path
-
-    def backup_repos(self):
-        bckp_file = os.path.join(self.backup_file_path, self.backup_file_name)
-        tar = tarfile.open(bckp_file, "w:gz")
-
-        for dir_name in os.listdir(self.repos_path):
-            logging.info('backing up %s', dir_name)
-            tar.add(os.path.join(self.repos_path, dir_name), dir_name)
-        tar.close()
-        logging.info('finished backup of mercurial repositories')
-
-    def transfer_files(self):
-        params = {
-                  'id_rsa_key': self.id_rsa_path,
-                  'backup_file': os.path.join(self.backup_file_path,
-                                             self.backup_file_name),
-                  'backup_server': self.backup_server
-                  }
-        cmd = ['scp', '-l', '40000', '-i', '%(id_rsa_key)s' % params,
-               '%(backup_file)s' % params,
-               '%(backup_server)s' % params]
-
-        subprocess.call(cmd)
-        logging.info('Transfered file %s to %s', self.backup_file_name, cmd[4])
-
-    def rm_file(self):
-        logging.info('Removing file %s', self.backup_file_name)
-        os.remove(os.path.join(self.backup_file_path, self.backup_file_name))
-
-if __name__ == "__main__":
-
-    repo_location = '/home/repo_path'
-    backup_server = 'root@192.168.1.100:/backups/mercurial'
-    rsa_key = '/home/id_rsa'
-
-    B_MANAGER = BackupManager(repo_location, rsa_key, backup_server)
-    B_MANAGER.backup_repos()
-    B_MANAGER.transfer_files()
-    B_MANAGER.rm_file()
--- a/rhodecode/templates/admin/admin_log.html	Sun Jun 03 20:24:02 2012 +0200
+++ b/rhodecode/templates/admin/admin_log.html	Sun Jun 03 20:35:13 2012 +0200
@@ -14,7 +14,7 @@
 		<td>${h.link_to(l.user.username,h.url('edit_user', id=l.user.user_id))}</td>
 		<td>${h.action_parser(l)[0]()}
 		  <div class="journal_action_params">
-            ${h.literal(h.action_parser(l)[1]())} 
+            ${h.literal(h.action_parser(l)[1]())}
           </div>
 		</td>
 		<td>
--- a/setup.py	Sun Jun 03 20:24:02 2012 +0200
+++ b/setup.py	Sun Jun 03 20:35:13 2012 +0200
@@ -87,6 +87,9 @@
     zip_safe=False,
     paster_plugins=['PasteScript', 'Pylons'],
     entry_points="""
+    [console_scripts]
+    rhodecode-api =  rhodecode.bin.rhodecode_api:main
+
     [paste.app_factory]
     main = rhodecode.config.middleware:make_app