summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJeff Forcier <jeff@bitprophet.org>2014-01-29 14:38:58 -0800
committerJeff Forcier <jeff@bitprophet.org>2014-01-29 14:38:58 -0800
commitc636b273daf657eb05041ce2528959910e7db162 (patch)
treeca4ce0e03286b5d6c9fbcac8d9db6d4e2d5bf1ef
parent473a9cdf5b22c8bbc4d4e95d419c8bd153f05e21 (diff)
parentb8d1724f5714c27ad02ae013e87f5531aec041ea (diff)
Merge branch '1.10' into 1.11
Conflicts: dev-requirements.txt tox.ini
-rw-r--r--.gitignore2
-rw-r--r--.travis.yml14
-rw-r--r--dev-requirements.txt8
-rw-r--r--sites/_shared_static/logo.pngbin0 -> 6401 bytes
-rw-r--r--sites/docs/conf.py4
-rw-r--r--sites/docs/index.rst6
-rw-r--r--sites/shared_conf.py40
-rw-r--r--sites/www/_templates/rss.xml19
-rw-r--r--sites/www/blog.py140
-rw-r--r--sites/www/blog.rst16
-rw-r--r--sites/www/blog/first-post.rst7
-rw-r--r--sites/www/blog/second-post.rst7
-rw-r--r--sites/www/changelog.rst77
-rw-r--r--sites/www/conf.py35
-rw-r--r--sites/www/contact.rst11
-rw-r--r--sites/www/contributing.rst19
-rw-r--r--sites/www/index.rst38
-rw-r--r--sites/www/installing.rst105
-rw-r--r--tasks.py23
-rw-r--r--tox-requirements.txt2
-rw-r--r--tox.ini2
21 files changed, 572 insertions, 3 deletions
diff --git a/.gitignore b/.gitignore
index 4b578950..9e1febf3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,5 @@ dist/
paramiko.egg-info/
test.log
docs/
+!sites/docs
+_build
diff --git a/.travis.yml b/.travis.yml
index c9802a80..df7c225a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,8 +5,18 @@ python:
install:
# Self-install for setup.py-driven deps
- pip install -e .
- - pip install coveralls
-script: coverage run --source=paramiko test.py --verbose
+ # Dev (doc/test running) requirements
+ - pip install coveralls # For coveralls.io specifically
+ - pip install -r dev-requirements.txt
+script:
+ # Main tests, with coverage!
+ - coverage run --source=paramiko test.py --verbose
+ # Ensure documentation & invoke pipeline run OK.
+ # Run 'docs' first since its objects.inv is referred to by 'www'.
+ # Also force warnings to be errors since most of them tend to be actual
+ # problems.
+ - invoke docs -o -W
+ - invoke www -o -W
notifications:
irc:
channels: "irc.freenode.org#paramiko"
diff --git a/dev-requirements.txt b/dev-requirements.txt
index f706c46f..43c21e38 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,2 +1,10 @@
+# Older junk
tox>=1.4,<1.5
epydoc>=3.0,<3.1
+# For newer tasks like building Sphinx docs.
+# NOTE: Requires Python >=2.6
+invoke>=0.7.0
+invocations>=0.4.4
+sphinx>=1.1.3
+alabaster>=0.2.0
+releases>=0.2.4
diff --git a/sites/_shared_static/logo.png b/sites/_shared_static/logo.png
new file mode 100644
index 00000000..bc76697e
--- /dev/null
+++ b/sites/_shared_static/logo.png
Binary files differ
diff --git a/sites/docs/conf.py b/sites/docs/conf.py
new file mode 100644
index 00000000..0c7ffe55
--- /dev/null
+++ b/sites/docs/conf.py
@@ -0,0 +1,4 @@
+# Obtain shared config values
+import os, sys
+sys.path.append(os.path.abspath('..'))
+from shared_conf import *
diff --git a/sites/docs/index.rst b/sites/docs/index.rst
new file mode 100644
index 00000000..08b34320
--- /dev/null
+++ b/sites/docs/index.rst
@@ -0,0 +1,6 @@
+Welcome to Paramiko's documentation!
+====================================
+
+This site covers Paramiko's usage & API documentation. For basic info on what
+Paramiko is, including its public changelog & how the project is maintained,
+please see `the main website <http://paramiko.org>`_.
diff --git a/sites/shared_conf.py b/sites/shared_conf.py
new file mode 100644
index 00000000..2b98654f
--- /dev/null
+++ b/sites/shared_conf.py
@@ -0,0 +1,40 @@
+from datetime import datetime
+import os
+import sys
+
+import alabaster
+
+
+# Alabaster theme
+html_theme_path = [alabaster.get_path()]
+# Paths relative to invoking conf.py - not this shared file
+html_static_path = ['../_shared_static']
+html_theme = 'alabaster'
+html_theme_options = {
+ 'description': "A Python implementation of SSHv2.",
+ 'github_user': 'paramiko',
+ 'github_repo': 'paramiko',
+ 'gittip_user': 'bitprophet',
+ 'analytics_id': 'UA-18486793-2',
+
+ 'link': '#3782BE',
+ 'link_hover': '#3782BE',
+}
+html_sidebars = {
+ '**': [
+ 'about.html',
+ 'navigation.html',
+ 'searchbox.html',
+ 'donate.html',
+ ]
+}
+
+# Regular settings
+project = u'Paramiko'
+year = datetime.now().year
+copyright = u'2013-%d Jeff Forcier, 2003-2012 Robey Pointer' % year
+master_doc = 'index'
+templates_path = ['_templates']
+exclude_trees = ['_build']
+source_suffix = '.rst'
+default_role = 'obj'
diff --git a/sites/www/_templates/rss.xml b/sites/www/_templates/rss.xml
new file mode 100644
index 00000000..f6f9cbd1
--- /dev/null
+++ b/sites/www/_templates/rss.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
+ <channel>
+ <atom:link href="{{ atom }}" rel="self" type="application/rss+xml" />
+ <title>{{ title }}</title>
+ <link>{{ link }}</link>
+ <description>{{ description }}</description>
+ <pubDate>{{ date }}</pubDate>
+ {% for link, title, desc, date in posts %}
+ <item>
+ <link>{{ link }}</link>
+ <guid>{{ link }}</guid>
+ <title><![CDATA[{{ title }}]]></title>
+ <description><![CDATA[{{ desc }}]]></description>
+ <pubDate>{{ date }}</pubDate>
+ </item>
+ {% endfor %}
+ </channel>
+</rss>
diff --git a/sites/www/blog.py b/sites/www/blog.py
new file mode 100644
index 00000000..3b129ebf
--- /dev/null
+++ b/sites/www/blog.py
@@ -0,0 +1,140 @@
+from collections import namedtuple
+from datetime import datetime
+import time
+import email.utils
+
+from sphinx.util.compat import Directive
+from docutils import nodes
+
+
+class BlogDateDirective(Directive):
+ """
+ Used to parse/attach date info to blog post documents.
+
+ No nodes generated, since none are needed.
+ """
+ has_content = True
+
+ def run(self):
+ # Tag parent document with parsed date value.
+ self.state.document.blog_date = datetime.strptime(
+ self.content[0], "%Y-%m-%d"
+ )
+ # Don't actually insert any nodes, we're already done.
+ return []
+
+class blog_post_list(nodes.General, nodes.Element):
+ pass
+
+class BlogPostListDirective(Directive):
+ """
+ Simply spits out a 'blog_post_list' temporary node for replacement.
+
+ Gets replaced at doctree-resolved time - only then will all blog post
+ documents be written out (& their date directives executed).
+ """
+ def run(self):
+ return [blog_post_list('')]
+
+
+Post = namedtuple('Post', 'name doc title date opener')
+
+def get_posts(app):
+ # Obtain blog posts
+ post_names = filter(lambda x: x.startswith('blog/'), app.env.found_docs)
+ posts = map(lambda x: (x, app.env.get_doctree(x)), post_names)
+ # Obtain common data used for list page & RSS
+ data = []
+ for post, doc in sorted(posts, key=lambda x: x[1].blog_date, reverse=True):
+ # Welp. No "nice" way to get post title. Thanks Sphinx.
+ title = doc[0][0][0]
+ # Date. This may or may not end up reflecting the required
+ # *input* format, but doing it here gives us flexibility.
+ date = doc.blog_date
+ # 1st paragraph as opener. TODO: allow a role or something marking
+ # where to actually pull from?
+ opener = doc.traverse(nodes.paragraph)[0]
+ data.append(Post(post, doc, title, date, opener))
+ return data
+
+def replace_blog_post_lists(app, doctree, fromdocname):
+ """
+ Replace blog_post_list nodes with ordered list-o-links to posts.
+ """
+ # Obtain blog posts
+ post_names = filter(lambda x: x.startswith('blog/'), app.env.found_docs)
+ posts = map(lambda x: (x, app.env.get_doctree(x)), post_names)
+ # Build "list" of links/etc
+ post_links = []
+ for post, doc, title, date, opener in get_posts(app):
+ # Link itself
+ uri = app.builder.get_relative_uri(fromdocname, post)
+ link = nodes.reference('', '', refdocname=post, refuri=uri)
+ # Title, bolded. TODO: use 'topic' or something maybe?
+ link.append(nodes.strong('', title))
+ date = date.strftime("%Y-%m-%d")
+ # Meh @ not having great docutils nodes which map to this.
+ html = '<div class="timestamp"><span>%s</span></div>' % date
+ timestamp = nodes.raw(text=html, format='html')
+ # NOTE: may group these within another element later if styling
+ # necessitates it
+ group = [timestamp, nodes.paragraph('', '', link), opener]
+ post_links.extend(group)
+
+ # Replace temp node(s) w/ expanded list-o-links
+ for node in doctree.traverse(blog_post_list):
+ node.replace_self(post_links)
+
+def rss_timestamp(timestamp):
+ # Use horribly inappropriate module for its magical daylight-savings-aware
+ # timezone madness. Props to Tinkerer for the idea.
+ return email.utils.formatdate(
+ time.mktime(timestamp.timetuple()),
+ localtime=True
+ )
+
+def generate_rss(app):
+ # Meh at having to run this subroutine like 3x per build. Not worth trying
+ # to be clever for now tho.
+ posts_ = get_posts(app)
+ # LOL URLs
+ root = app.config.rss_link
+ if not root.endswith('/'):
+ root += '/'
+ # Oh boy
+ posts = [
+ (
+ root + app.builder.get_target_uri(x.name),
+ x.title,
+ str(x.opener[0]), # Grab inner text element from paragraph
+ rss_timestamp(x.date),
+ )
+ for x in posts_
+ ]
+ location = 'blog/rss.xml'
+ context = {
+ 'title': app.config.project,
+ 'link': root,
+ 'atom': root + location,
+ 'description': app.config.rss_description,
+ # 'posts' is sorted by date already
+ 'date': rss_timestamp(posts_[0].date),
+ 'posts': posts,
+ }
+ yield (location, context, 'rss.xml')
+
+def setup(app):
+ # Link in RSS feed back to main website, e.g. 'http://paramiko.org'
+ app.add_config_value('rss_link', None, '')
+ # Ditto for RSS description field
+ app.add_config_value('rss_description', None, '')
+ # Interprets date metadata in blog post documents
+ app.add_directive('date', BlogDateDirective)
+ # Inserts blog post list node (in e.g. a listing page) for replacement
+ # below
+ app.add_node(blog_post_list)
+ app.add_directive('blog-posts', BlogPostListDirective)
+ # Performs abovementioned replacement
+ app.connect('doctree-resolved', replace_blog_post_lists)
+ # Generates RSS page from whole cloth at page generation step
+ app.connect('html-collect-pages', generate_rss)
diff --git a/sites/www/blog.rst b/sites/www/blog.rst
new file mode 100644
index 00000000..af9651e4
--- /dev/null
+++ b/sites/www/blog.rst
@@ -0,0 +1,16 @@
+====
+Blog
+====
+
+.. blog-posts directive gets replaced with an ordered list of blog posts.
+
+.. blog-posts::
+
+
+.. The following toctree ensures blog posts get processed.
+
+.. toctree::
+ :hidden:
+ :glob:
+
+ blog/*
diff --git a/sites/www/blog/first-post.rst b/sites/www/blog/first-post.rst
new file mode 100644
index 00000000..7b075073
--- /dev/null
+++ b/sites/www/blog/first-post.rst
@@ -0,0 +1,7 @@
+===========
+First post!
+===========
+
+A blog post.
+
+.. date:: 2013-12-04
diff --git a/sites/www/blog/second-post.rst b/sites/www/blog/second-post.rst
new file mode 100644
index 00000000..c4463f33
--- /dev/null
+++ b/sites/www/blog/second-post.rst
@@ -0,0 +1,7 @@
+===========
+Another one
+===========
+
+.. date:: 2013-12-05
+
+Indeed!
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
new file mode 100644
index 00000000..bba78949
--- /dev/null
+++ b/sites/www/changelog.rst
@@ -0,0 +1,77 @@
+=========
+Changelog
+=========
+
+* :release:`1.10.6 <2014-01-21>`
+* :bug:`193` (and its attentant PRs :issue:`230` & :issue:`253`): Fix SSH agent
+ problems present on Windows. Thanks to David Hobbs for initial report and to
+ Aarni Koskela & Olle Lundberg for the patches.
+* :release:`1.10.5 <2014-01-08>`
+* :bug:`176` Fix AttributeError bugs in known_hosts file (re)loading. Thanks
+ to Nathan Scowcroft for the patch & Martin Blumenstingl for the initial test
+ case.
+* :release:`1.10.4 <2013-09-27>`
+* :bug:`179` Fix a missing variable causing errors when an ssh_config file has
+ a non-default AddressFamily set. Thanks to Ed Marshall & Tomaz Muraus for
+ catch & patch.
+* :bug:`200` Fix an exception-causing typo in `demo_simple.py`. Thanks to Alex
+ Buchanan for catch & Dave Foster for patch.
+* :bug:`199` Typo fix in the license header cross-project. Thanks to Armin
+ Ronacher for catch & patch.
+* :release:`1.10.3 <2013-09-20>`
+* :bug:`162` Clean up HMAC module import to avoid deadlocks in certain uses of
+ SSHClient. Thanks to Gernot Hillier for the catch & suggested fix.
+* :bug:`36` Fix the port-forwarding demo to avoid file descriptor errors.
+ Thanks to Jonathan Halcrow for catch & patch.
+* :bug:`168` Update config handling to properly handle multiple 'localforward'
+ and 'remoteforward' keys. Thanks to Emre Yılmaz for the patch.
+* :release:`1.10.2 <2013-07-26>`
+* :bug:`153` (also :issue:`67`) Warn on parse failure when reading known_hosts
+ file. Thanks to `@glasserc` for patch.
+* :bug:`146` Indentation fixes for readability. Thanks to Abhinav Upadhyay for
+ catch & patch.
+* :release:`1.10.1 <2013-04-05>`
+* :bug:`142` (`Fabric #811 <https://github.com/fabric/fabric/issues/811>`_)
+ SFTP put of empty file will still return the attributes of the put file.
+ Thanks to Jason R. Coombs for the patch.
+* :bug:`154` (`Fabric #876 <https://github.com/fabric/fabric/issues/876>`_)
+ Forwarded SSH agent connections left stale local pipes lying around, which
+ could cause local (and sometimes remote or network) resource starvation when
+ running many agent-using remote commands. Thanks to Kevin Tegtmeier for catch
+ & patch.
+* :release:`1.10.0 <2013-03-01>`
+* :feature:`66` Batch SFTP writes to help speed up file transfers. Thanks to
+ Olle Lundberg for the patch.
+* :bug:`133 major` Fix handling of window-change events to be on-spec and not
+ attempt to wait for a response from the remote sshd; this fixes problems with
+ less common targets such as some Cisco devices. Thanks to Phillip Heller for
+ catch & patch.
+* :feature:`93` Overhaul SSH config parsing to be in line with `man
+ ssh_config` (& the behavior of `ssh` itself), including addition of parameter
+ expansion within config values. Thanks to Olle Lundberg for the patch.
+* :feature:`110` Honor SSH config `AddressFamily` setting when looking up
+ local host's FQDN. Thanks to John Hensley for the patch.
+* :feature:`128` Defer FQDN resolution until needed, when parsing SSH config
+ files. Thanks to Parantapa Bhattacharya for catch & patch.
+* :bug:`102 major` Forego random padding for packets when running under
+ `*-ctr` ciphers. This corrects some slowdowns on platforms where random byte
+ generation is inefficient (e.g. Windows). Thanks to `@warthog618` for catch
+ & patch, and Michael van der Kolff for code/technique review.
+* :feature:`127` Turn `SFTPFile` into a context manager. Thanks to Michael
+ Williamson for the patch.
+* :feature:`116` Limit `Message.get_bytes` to an upper bound of 1MB to protect
+ against potential DoS vectors. Thanks to `@mvschaik` for catch & patch.
+* :feature:`115` Add convenience `get_pty` kwarg to `Client.exec_command` so
+ users not manually controlling a channel object can still toggle PTY
+ creation. Thanks to Michael van der Kolff for the patch.
+* :feature:`71` Add `SFTPClient.putfo` and `.getfo` methods to allow direct
+ uploading/downloading of file-like objects. Thanks to Eric Buehl for the
+ patch.
+* :feature:`113` Add `timeout` parameter to `SSHClient.exec_command` for
+ easier setting of the command's internal channel object's timeout. Thanks to
+ Cernov Vladimir for the patch.
+* :support:`94` Remove duplication of SSH port constant. Thanks to Olle
+ Lundberg for the catch.
+* :feature:`80` Expose the internal "is closed" property of the file transfer
+ class `BufferedFile` as `.closed`, better conforming to Python's file
+ interface. Thanks to `@smunaut` and James Hiscock for catch & patch.
diff --git a/sites/www/conf.py b/sites/www/conf.py
new file mode 100644
index 00000000..1f11c1e2
--- /dev/null
+++ b/sites/www/conf.py
@@ -0,0 +1,35 @@
+# Obtain shared config values
+import sys
+import os
+from os.path import abspath, join, dirname
+
+sys.path.append(abspath(join(dirname(__file__), '..')))
+from shared_conf import *
+
+# Local blog extension
+sys.path.append(abspath('.'))
+extensions = ['blog']
+rss_link = 'http://paramiko.org'
+rss_description = 'Paramiko project news'
+
+# Releases changelog extension
+extensions.append('releases')
+releases_release_uri = "https://github.com/paramiko/paramiko/tree/%s"
+releases_issue_uri = "https://github.com/paramiko/paramiko/issues/%s"
+
+# Intersphinx for referencing API/usage docs
+extensions.append('sphinx.ext.intersphinx')
+# Default is 'local' building, but reference the public docs site when building
+# under RTD.
+target = join(dirname(__file__), '..', 'docs', '_build')
+if os.environ.get('READTHEDOCS') == 'True':
+ # TODO: switch to docs.paramiko.org post go-live of sphinx API docs
+ target = 'http://paramiko-docs.readthedocs.org/en/latest/'
+#intersphinx_mapping = {
+# 'docs': (target, None),
+#}
+
+# Sister-site links to API docs
+html_theme_options['extra_nav_links'] = {
+ "API Docs": 'http://docs.paramiko.org',
+}
diff --git a/sites/www/contact.rst b/sites/www/contact.rst
new file mode 100644
index 00000000..b479f170
--- /dev/null
+++ b/sites/www/contact.rst
@@ -0,0 +1,11 @@
+=======
+Contact
+=======
+
+You can get in touch with the developer & user community in any of the
+following ways:
+
+* IRC: ``#paramiko`` on Freenode
+* Mailing list: ``paramiko@librelist.com`` (see `the LibreList homepage
+ <http://librelist.com>`_ for usage details).
+* This website's :doc:`blog </blog>`.
diff --git a/sites/www/contributing.rst b/sites/www/contributing.rst
new file mode 100644
index 00000000..b121e64b
--- /dev/null
+++ b/sites/www/contributing.rst
@@ -0,0 +1,19 @@
+============
+Contributing
+============
+
+How to get the code
+===================
+
+Our primary Git repository is on Github at `paramiko/paramiko
+<https://github.com/paramiko/paramiko>`; please follow their instruction for
+cloning to your local system. (If you intend to submit patches/pull requests,
+we recommend forking first, then cloning your fork. Github has excellent
+documentation for all this.)
+
+
+How to submit bug reports or new code
+=====================================
+
+Please see `this project-agnostic contribution guide
+<http://contribution-guide.org>`_ - we follow it explicitly.
diff --git a/sites/www/index.rst b/sites/www/index.rst
new file mode 100644
index 00000000..7fefedd2
--- /dev/null
+++ b/sites/www/index.rst
@@ -0,0 +1,38 @@
+Welcome to Paramiko!
+====================
+
+Paramiko is a Python (2.5+) implementation of the SSHv2 protocol [#]_,
+providing both client and server functionality. While it leverages a Python C
+extension for low level cryptography (`PyCrypto <http://pycrypto.org>`_),
+Paramiko itself is a pure Python interface around SSH networking concepts.
+
+This website covers project information for Paramiko such as the changelog,
+contribution guidelines, development roadmap, news/blog, and so forth. Detailed
+usage and API documentation can be found at our code documentation site,
+`docs.paramiko.org <http://docs.paramiko.org>`_.
+
+.. toctree::
+ changelog
+ installing
+ contributing
+ contact
+
+.. Hide blog in hidden toctree for now (to avoid warnings.)
+
+.. toctree::
+ :hidden:
+
+ blog
+
+
+.. rubric:: Footnotes
+
+.. [#]
+ SSH is defined in RFCs
+ `4251 <http://www.rfc-editor.org/rfc/rfc4251.txt>`_,
+ `4252 <http://www.rfc-editor.org/rfc/rfc4252.txt>`_,
+ `4253 <http://www.rfc-editor.org/rfc/rfc4253.txt>`_, and
+ `4254 <http://www.rfc-editor.org/rfc/rfc4254.txt>`_;
+ the primary working implementation of the protocol is the `OpenSSH project
+ <http://openssh.org>`_. Paramiko implements a large portion of the SSH
+ feature set, but there are occasional gaps.
diff --git a/sites/www/installing.rst b/sites/www/installing.rst
new file mode 100644
index 00000000..0d4dc1ac
--- /dev/null
+++ b/sites/www/installing.rst
@@ -0,0 +1,105 @@
+==========
+Installing
+==========
+
+Paramiko itself
+===============
+
+The recommended way to get Invoke is to **install the latest stable release**
+via `pip <http://pip-installer.org>`_::
+
+ $ pip install paramiko
+
+.. note::
+ Users who want the bleeding edge can install the development version via
+ ``pip install paramiko==dev``.
+
+We currently support **Python 2.5/2.6/2.7**, with support for Python 3 coming
+soon. Users on Python 2.4 or older are urged to upgrade. Paramiko *may* work on
+Python 2.4 still, but there is no longer any support guarantee.
+
+Paramiko has two dependencies: the pure-Python ECDSA module `ecdsa`, and the
+PyCrypto C extension. `ecdsa` is easily installable from wherever you
+obtained Paramiko's package; PyCrypto may require more work. Read on for
+details.
+
+PyCrypto
+========
+
+`PyCrypto <https://www.dlitz.net/software/pycrypto/>`_ provides the low-level
+(C-based) encryption algorithms we need to implement the SSH protocol. There
+are a couple gotchas associated with installing PyCrypto: its compatibility
+with Python's package tools, and the fact that it is a C-based extension.
+
+.. _pycrypto-and-pip:
+
+Possible gotcha on older Python and/or pip versions
+---------------------------------------------------
+
+We strongly recommend using ``pip`` to as it is newer and generally better than
+``easy_install``. However, a combination of bugs in specific (now rather old)
+versions of Python, ``pip`` and PyCrypto can prevent installation of PyCrypto.
+Specifically:
+
+* Python = 2.5.x
+* PyCrypto >= 2.1 (required for most modern versions of Paramiko)
+* ``pip`` < 0.8.1
+
+When all three criteria are met, you may encounter ``No such file or
+directory`` IOErrors when trying to ``pip install paramiko`` or ``pip install
+PyCrypto``.
+
+The fix is to make sure at least one of the above criteria is not met, by doing
+the following (in order of preference):
+
+* Upgrade to ``pip`` 0.8.1 or above, e.g. by running ``pip install -U pip``.
+* Upgrade to Python 2.6 or above.
+* Downgrade to Paramiko 1.7.6 or 1.7.7, which do not require PyCrypto >= 2.1,
+ and install PyCrypto 2.0.1 (the oldest version on PyPI which works with
+ Paramiko 1.7.6/1.7.7)
+
+
+C extension
+-----------
+
+Unless you are installing from a precompiled source such as a Debian apt
+repository or RedHat RPM, or using :ref:`pypm <pypm>`, you will also need the
+ability to build Python C-based modules from source in order to install
+PyCrypto. Users on **Unix-based platforms** such as Ubuntu or Mac OS X will
+need the traditional C build toolchain installed (e.g. Developer Tools / XCode
+Tools on the Mac, or the ``build-essential`` package on Ubuntu or Debian Linux
+-- basically, anything with ``gcc``, ``make`` and so forth) as well as the
+Python development libraries, often named ``python-dev`` or similar.
+
+For **Windows** users we recommend using :ref:`pypm`, installing a C
+development environment such as `Cygwin <http://cygwin.com>`_ or obtaining a
+precompiled Win32 PyCrypto package from `voidspace's Python modules page
+<http://www.voidspace.org.uk/python/modules.shtml#pycrypto>`_.
+
+.. note::
+ Some Windows users whose Python is 64-bit have found that the PyCrypto
+ dependency ``winrandom`` may not install properly, leading to ImportErrors.
+ In this scenario, you'll probably need to compile ``winrandom`` yourself
+ via e.g. MS Visual Studio. See `Fabric #194
+ <https://github.com/fabric/fabric/issues/194>`_ for info.
+
+
+.. _pypm:
+
+ActivePython and PyPM
+=====================
+
+Windows users who already have ActiveState's `ActivePython
+<http://www.activestate.com/activepython/downloads>`_ distribution installed
+may find Paramiko is best installed with `its package manager, PyPM
+<http://code.activestate.com/pypm/>`_. Below is example output from an
+installation of Paramiko via ``pypm``::
+
+ C:\> pypm install paramiko
+ The following packages will be installed into "%APPDATA%\Python" (2.7):
+ paramiko-1.7.8 pycrypto-2.4
+ Get: [pypm-free.activestate.com] paramiko 1.7.8
+ Get: [pypm-free.activestate.com] pycrypto 2.4
+ Installing paramiko-1.7.8
+ Installing pycrypto-2.4
+ C:\>
diff --git a/tasks.py b/tasks.py
new file mode 100644
index 00000000..c7164158
--- /dev/null
+++ b/tasks.py
@@ -0,0 +1,23 @@
+from os.path import join
+
+from invoke import Collection
+from invocations import docs as _docs, testing
+
+
+d = 'sites'
+
+# Usage doc/API site (published as docs.paramiko.org)
+path = join(d, 'docs')
+docs = Collection.from_module(_docs, name='docs', config={
+ 'sphinx.source': path,
+ 'sphinx.target': join(path, '_build'),
+})
+
+# Main/about/changelog site ((www.)?paramiko.org)
+path = join(d, 'www')
+www = Collection.from_module(_docs, name='www', config={
+ 'sphinx.source': path,
+ 'sphinx.target': join(path, '_build'),
+})
+
+ns = Collection(testing.test, docs=docs, www=www)
diff --git a/tox-requirements.txt b/tox-requirements.txt
new file mode 100644
index 00000000..26224ce6
--- /dev/null
+++ b/tox-requirements.txt
@@ -0,0 +1,2 @@
+# Not sure why tox can't just read setup.py?
+pycrypto
diff --git a/tox.ini b/tox.ini
index e2a8dcf4..af4fbf20 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,5 +2,5 @@
envlist = py25,py26,py27
[testenv]
-commands = pip install --use-mirrors -q -r dev-requirements.txt
+commands = pip install --use-mirrors -q -r tox-requirements.txt
python test.py