infocalypse

(djk)
2009-07-02: Added fn-putsite command to insert freesites.

Added fn-putsite command to insert freesites.

diff --git a/infocalypse/__init__.py b/infocalypse/__init__.py
--- a/infocalypse/__init__.py
+++ b/infocalypse/__init__.py
@@ -197,12 +197,52 @@ fms can have pretty high latency. Be pat
 take hours (sometimes a day!) for your notification
 to appear.  Don't send lots of redundant notifications.
 
+FREESITE INSERTION:
+hg fn-putsite --index <n>
+
+inserts a freesite based on the configuration in
+the freesite.cfg file in the root of the repository.
+
+Use:
+hg fn-putsite --createconfig
+
+to create a basic freesite.cfg file that you
+can modify. Look at the comments in it for an
+explanation of the supported parameters.
+
+The default freesite.cfg file inserts using the
+same private key as the repo and a site name
+of 'default'. Editing the name is highly
+recommended.
+
+You can use --key CHK@ to insert a test version of
+the site to a CHK key before writing to the USK.
+
+Limitations:
+o You MUST have fn-pushed the repo at least once
+  in order to insert using the repo's private key.
+  If you haven't fn-push'd you'll see this error:
+  "You don't have the insert URI for this repo.
+  Supply a private key with --key or fn-push the repo."
+o Inserts *all* files in the site_dir directory in
+  the freesite.cfg file.  Run with --dryrun to make
+  sure that you aren't going to insert stuff you don't
+  want too.
+o You must manually specify the USK edition you want
+  to insert on.  You will get a collision error
+  if you specify an index that was already inserted.
+o Don't use this for big sites.  It should be fine
+  for notes on your project.  If you have lots of images
+  or big binary files use a tool like jSite instead.
+o Don't modify site files while the fn-putsite is
+  running.
+
 HINTS:
 The -q, -v and --debug verbosity options are
 supported.
 
 Top level URIs ending in '.R1' are inserted redundantly.
-Don't use this if you are worried about correlation
+Don't use this if you're worried about correlation
 attacks.
 
 If you see 'abort: Connection refused' when you run
@@ -253,6 +293,8 @@ from infcmds import get_config_info, exe
 
 from fmscmds import execute_fmsread, execute_fmsnotify
 
+from sitecmds import read_freesite_cfg, execute_putsite, execute_genkey
+
 def set_target_version(ui_, repo, opts, params, msg_fmt):
     """ INTERNAL: Update TARGET_VERSION in params. """
 
@@ -437,6 +479,56 @@ def infocalypse_fmsnotify(ui_, repo, **o
     params['INSERT_URI'] = insert_uri
     execute_fmsnotify(ui_, repo, params, stored_cfg)
 
+MSG_BAD_INDEX = 'You must set --index to a value >= 0.'
+def infocalypse_putsite(ui_, repo, **opts):
+    """ Insert an update to a freesite.
+    """
+
+    if opts['createconfig']:
+        params = {'SITE_CREATE_CONFIG':True}
+        execute_putsite(ui_, repo, params)
+        return
+
+    params, stored_cfg = get_config_info(ui_, opts)
+    if opts['key'] != '': # order important
+        params['SITE_KEY'] = opts['key']
+        if not (params['SITE_KEY'].startswith('SSK') or
+                params['SITE_KEY'] == 'CHK@'):
+            raise util.Abort("--key must be a valid SSK "
+                             + "insert key or CHK@.")
+    read_freesite_cfg(ui_, repo, params, stored_cfg)
+
+    try:
+        # --index not required for CHK@
+        if not params['SITE_KEY'].startswith('CHK'):
+            params['SITE_INDEX'] = int(opts['index'])
+            if params['SITE_INDEX'] < 0:
+                raise ValueError()
+        else:
+            params['SITE_INDEX'] = -1
+    except ValueError:
+        raise util.Abort(MSG_BAD_INDEX)
+    except TypeError:
+        raise util.Abort(MSG_BAD_INDEX)
+
+    params['DRYRUN'] = opts['dryrun']
+
+    if not params.get('SITE_KEY', None):
+        insert_uri = stored_cfg.get_dir_insert_uri(repo.root)
+        if not insert_uri:
+            ui_.warn("You don't have the insert URI for this repo.\n"
+                     + "Supply a private key with --key or fn-push "
+                     + "the repo.\n")
+            return # REDFLAG: hmmm... abort?
+        params['SITE_KEY'] = 'SSK' + insert_uri.split('/')[0][3:]
+
+    execute_putsite(ui_, repo, params)
+
+def infocalypse_genkey(ui_, **opts):
+    """ Print a new SSK key pair. """
+    params, dummy = get_config_info(ui_, opts)
+    execute_genkey(ui_, params)
+
 def infocalypse_setup(ui_, **opts):
     """ Setup the extension for use for the first time. """
 
@@ -511,6 +603,18 @@ cmdtable = {
                      + FCP_OPTS, # Needs to invert the insert uri
                      "[options]"),
 
+    "fn-putsite": (infocalypse_putsite,
+                     [('', 'dryrun', None, "don't insert site"),
+                     ('', 'index', -1, "edition to insert"),
+                     ('', 'createconfig', None, "create default freesite.cfg"),
+                     ('', 'key', '', "private SSK to insert under"),]
+                     + FCP_OPTS,
+                     "[options]"),
+
+    "fn-genkey": (infocalypse_genkey,
+                  FCP_OPTS,
+                  "[options]"),
+
     "fn-setup": (infocalypse_setup,
                  [('', 'tmpdir', '~/infocalypse_tmp', 'temp directory'),]
                  + FCP_OPTS,
@@ -519,4 +623,5 @@ cmdtable = {
 
 
 commands.norepo += ' fn-setup'
+commands.norepo += ' fn-genkey'
 
diff --git a/infocalypse/sitecmds.py b/infocalypse/sitecmds.py
new file mode 100644
--- /dev/null
+++ b/infocalypse/sitecmds.py
@@ -0,0 +1,209 @@
+""" Implementation of commands to insert freesites.
+
+    Copyright (C) 2009 Darrell Karbott
+
+    This library 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 2.0 of the License, or (at your option) any later version.
+
+    This library 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 library; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+    Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+"""
+
+
+import os
+
+from ConfigParser import ConfigParser
+
+from mercurial import util
+
+from fcpconnection import FCPError
+from fcpclient import FCPClient, get_file_infos, set_index_file
+
+def write_default_config(ui_, repo):
+    """ Write a default freesite.cfg file into the repository root dir. """
+    file_name = os.path.join(repo.root, 'freesite.cfg')
+
+    if os.path.exists(file_name):
+        raise util.Abort("Already exists: %s" % file_name)
+
+    out_file = open(file_name, 'w')
+    try:
+        out_file.write("""[default]
+# Human readable site name.
+site_name = default
+# Directory to insert from relative to the repository root.
+site_dir = site_root
+# Optional external file to load the site key from, relative
+# to the directory your .infocalypse/infocalypse.ini file
+# is stored in. This file should contain ONLY the SSK insert
+# key up to the first slash.
+#
+# If this value is not set the insert SSK for the repo is
+# used.
+#site_key_file = example_freesite_key.txt
+#
+# Optional file to display by default.  If this is not
+# set index.html is used.
+#default_file = index.html
+""")
+    finally:
+        out_file.close()
+
+    ui_.status('Created config file:\n%s\n' % file_name)
+    ui_.status('You probably want to edit at least the site_name.\n')
+
+def read_freesite_cfg(ui_, repo, params, stored_cfg):
+    """ Read param out of the freesite.cfg file. """
+    cfg_file = os.path.join(repo.root, 'freesite.cfg')
+
+    ui_.status('Using config file:\n%s\n' % cfg_file)
+    if not os.path.exists(cfg_file):
+        ui_.warn("Can't read: %s\n" % cfg_file)
+        raise util.Abort("Use --createconfig to create freesite.cfg")
+
+    parser = ConfigParser()
+    parser.read(cfg_file)
+    if not parser.has_section('default'):
+        raise util.Abort("Can't read default section of config file?")
+
+    params['SITE_NAME'] = parser.get('default', 'site_name')
+    params['SITE_DIR'] = parser.get('default', 'site_dir')
+    if parser.has_option('default','default_file'):
+        params['SITE_DEFAULT_FILE'] = parser.get('default', 'default_file')
+    else:
+        params['SITE_DEFAULT_FILE'] = 'index.html'
+
+    if params.get('SITE_KEY'):
+        return # key set on command line
+
+    if not parser.has_option('default','site_key_file'):
+        params['SITE_KEY'] = ''
+        return # Will use the insert SSK for the repo.
+
+    key_file = parser.get('default', 'site_key_file', 'default')
+    if key_file == 'default':
+        ui_.status('Using repo insert key as site key.\n')
+        params['SITE_KEY'] = 'default'
+        return # Use the insert SSK for the repo.
+    try:
+        # Read private key from specified key file relative
+        # to the directory the .infocalypse config file is stored in.
+        key_file = os.path.join(os.path.dirname(stored_cfg.file_name),
+                                key_file)
+        ui_.status('Reading site key from:\n%s\n' % key_file)
+        params['SITE_KEY'] = open(key_file, 'rb').read().strip()
+    except IOError:
+        raise util.Abort("Couldn't read site key from: %s" % key_file)
+
+    if not params['SITE_KEY'].startswith('SSK@'):
+        raise util.Abort("Stored site key not an SSK?")
+
+def get_insert_uri(params):
+    """ Helper function builds the insert URI. """
+    if params['SITE_KEY'] == 'CHK@':
+        return 'CHK@/'
+    return '%s/%s-%i/' % (params['SITE_KEY'],
+                          params['SITE_NAME'], params['SITE_INDEX'])
+
+# Convert SSK to USK so n00b5 don't phr34k out.
+def show_request_uri(ui_, params, uri):
+    """ Helper function to print the request URI."""
+    if uri.startswith('SSK@'):
+        request_uri = 'U%s/%s/%i/' % (uri.split('/')[0][1:],
+                                      params['SITE_NAME'],
+                                      params['SITE_INDEX'])
+    else:
+        request_uri = uri
+    ui_.status('RequestURI:\n%s\n' % request_uri)
+
+def execute_putsite(ui_, repo, params):
+    """ Run the putsite command. """
+    def progress(dummy, msg):
+        """ Message callback which writes to the hg ui instance."""
+
+        if msg[0] == 'SimpleProgress':
+            ui_.status("Progress: (%s/%s/%s)\n" % (msg[1]['Succeeded'],
+                                                   msg[1]['Required'],
+                                                   msg[1]['Total']))
+        else:
+            ui_.status("Progress: %s\n" % msg[0])
+
+
+    if params.get('SITE_CREATE_CONFIG', False):
+        write_default_config(ui_, repo)
+        return
+
+    # Remove trailing /
+    params['SITE_KEY'] = params['SITE_KEY'].split('/')[0].strip()
+    insert_uri = get_insert_uri(params)
+    site_root = os.path.join(repo.root, params['SITE_DIR'])
+
+    ui_.status('Default file: %s\n' % params['SITE_DEFAULT_FILE'])
+    ui_.status('Reading files from:\n%s\n' % site_root)
+
+    infos = get_file_infos(site_root)
+
+    try:
+        set_index_file(infos, params['SITE_DEFAULT_FILE'])
+    except ValueError:
+        raise util.Abort("Couldn't read %s" % params['SITE_DEFAULT_FILE'])
+
+    ui_.status('--- files ---\n')
+
+    for info in infos:
+        ui_.status('%s %s\n' % (info[0], info[1]))
+    ui_.status('---\n')
+
+    if params['DRYRUN']:
+        ui_.status('Would have inserted to:\n%s\n' % insert_uri)
+        ui_.status('But --dryrun was set.')
+        return
+
+    client = FCPClient.connect(params['FCP_HOST'],
+                               params['FCP_PORT'])
+    client.in_params.default_fcp_params['DontCompress'] = False
+    client.message_callback = progress
+    try:
+        ui_.status('Inserting to:\n%s\n' % insert_uri)
+        try:
+            request_uri = client.put_complex_dir(insert_uri, infos)[1]['URI']
+            show_request_uri(ui_, params, request_uri)
+        except FCPError, err:
+            if err.is_code(9): # magick number for collision
+                ui_.warn('An update was already inserted on that index.\n'
+                         + 'Set a later index with --index and try again.\n')
+                raise util.Abort("Key collision.")
+            else:
+                ui_.warn(str(err) + '\n')
+                raise util.Abort("FCP Error")
+    finally:
+        client.close()
+
+MSG_FMT = """InsertURI:
+%s
+RequestURI:
+%s
+
+This is what you need to put in a site_key_file file:
+%s
+"""
+
+def execute_genkey(ui_, params):
+    """ Run the genkey command. """
+    client = FCPClient.connect(params['FCP_HOST'],
+                               params['FCP_PORT'])
+
+    client.message_callback = lambda x, y : None # silence.
+    resp = client.generate_ssk()
+    ui_.status(MSG_FMT % (resp[1]['InsertURI'], resp[1]['RequestURI'],
+                          resp[1]['InsertURI'].split('/')[0] +'/'))