Quantcast
Channel: PythonAnywhere News
Viewing all 354 articles
Browse latest View live

Securing PythonAnywhere from the Heartbleed bug

$
0
0

The short version

The Heartbleed bug impacted PythonAnywhere (along with pretty much every Linux-based web service out there). We don't believe there's any risk that customer data has been leaked as a result of this problem, with the single exception of private keys for HTTPS certificates for custom domains -- that is, for websites hosted with us that don't end with .pythonanywhere.com. We don't have any reason to believe that those private keys were leaked either -- they're just the only data that we think could possibly have been leaked by it.

[UPDATE: Robert Graham at Errata Security points out that Heartbleed could also potentially have been used to harvest session cookies, usernames and passwords from users of affected sites. He's right, though it would be hard to do, and unlikely that someone would have targeted us for that. But just to be sure, we recommend you change your PythonAnywhere password, log out, then log back in again, and get users of your website to do likewise. Just to be clear on this: we don't think this has been used against us, and have no indication that it has. But it's better to be safe than sorry.]

The details

As you may have read, a bug in OpenSSL was announced last night that could potentially have been used to extract data from webservers, for example the private keys used to encrypt websites' SSL certificates. It exploits the SSL heartbeat extension, and has been nicknamed "Heartbleed". There's more information in this TechCrunch article.

All servers running recent versions of Linux were affected -- a very large percentage of the Internet -- and PythonAnywhere's were among them. All of our servers have been patched since early this morning, so the attack is now not possible against us. The only risk is that data might have been leaked before then.

We do not believe at this time that there's any risk that any data apart from SSL certificates' private keys could have been leaked. So for most PythonAnywhere users, everything should be fine. (Our own key for our own certificate for www.pythonanywhere.com might have been leaked, but we've changed it and are working on revoking the old certificate.)

For those customers who host websites on custom domains with PythonAnywhere (that is, domains that don't end with .pythonanywhere.com), there is a possibility that hackers who knew about this bug before this morning could have used it to extract their private keys. We have notified all such customers by email with details on what to do next; if you do have a custom domain with your own certificate and haven't received an email from us, drop us a line and we'll let you know what to do next.

If you have any questions, just let us know.

What we did

Due to some heroic work on the part of the Ubuntu team, patched versions of the affected libraries were ready by the time we started working on this. So patching all of our servers was just a few commands on each server that did HTTPS:

apt-get update
apt-get install openssl libssl-dev libssl1.0.0

And then a service tornado restart or service nginx restart, depending on what the HTTPS service was on the server.

We used @titanous's Heartbleeder command-line tool and Filippo Valsorda's Heartbleed test page both before and after the fix to make sure we really had fixed the problem.

We're confident that the patches we've applied are enough to fix the bug, at least as it's currently understood.


New release: a smorgasbord of changes

$
0
0

We've just released a new version of PythonAnywhere. It has a lot of small changes: we've installed a bunch of new packages, and added the option to put basic HTTP authentication in front of your web app (for example, for sites that are under development).

The big changes in this release are all under the hood. We've completely reworked the way we construct the sandboxes that your code runs in. This means that in the future it will be much easier for us to install new packages when people want them, and -- perhaps more importantly -- we'll soon be able to support different sandbox images for different people. This means that we'll soon be able (for example) to provide Django 1.6 for new users without breaking the web apps of the people who use 1.3.

Git push deployments on PythonAnywhere

$
0
0

Some of our frenemies in the PaaS world, who shall remain nameless, offer a "git push" model for deployment. People are fond of it, and sometimes ask us whether they could do that on PythonAnywhere too.

The answer is: you totally can! Because PythonAnywhere is, at heart, just a web-based UI wrapped around a fully-featured Linux server environment, you can do lots and lots of things.

Here are the ingredients:

  • You'll need a paid account so that SSH access is enabled.
  • You set up a bare repo on PythonAnywhere, and set it as a remote to your local code repo.
  • And then you use a git hook to automatically deploy your code and reload the site on push.

Here are the steps in detail:

(This guide assumes you already have a repo containing a web app which you want to deploy).

Creating a bare repo

We create a directory, and make it into a "bare" git repo, ie, one that be git push'd to.

# From a Bash console on PythonAnywhere:
mkdir -p ~/bare-repos/mysite.git # call this whatever you like
cd !$
git init --bare
ls # should show HEAD  branches  config...

Setting up a post-receive hook on the server

Next, navigate to your bare repository (in the file browser, or using a console-based editor like vim if you prefer), and create a new file at ~/bare-repos/mysite.git/hooks/post-receive. The name matters!

#!/bin/bash
mkdir -p /var/www/sites/mysite
GIT_WORK_TREE=/var/www/sites/mysite git checkout -f

A bare repo doesn't have a working tree, it just stores your repository in the magical git database format, with all the hashes and other voodoo. The git checkout -f along with the GIT_WORK_TREE variable will tell git to checkout an actual working tree with real code, to the specified location. You can put this wherever you like..…

More info in the git docs and in this how-to guide

Now we just need to make the hook executable:

# In a Bash console on PythonAnywhere:
chmod +x ~/bare-repos/mysite.git/hooks/post-receive

Adding PythonAnywhere as a remote in your local repo

Back on your own PC, in the git repo for your web app, adding the remote is a single command, and then we can do our first push to it to make sure everything works:

# In your local code repo. Substitute in your own username + path
git remote add pythonanywhere myusername@ssh.pythonanywhere.com:/home/myusername/bare-repos/mysite.git

git push -u pythonanywhere master
# output should end with "Branch master set up to track remote branch master from pythonanywhere."
  • Reminder: you need a paid account for this to work I'm afraid, because the git protocol relies on SSH.

You may get asked for your password at this point. If so, I strongly recommending setting up private key authentication instead, with a passphrase-encrypted private key on your machine, and adding your public key to ~/.ssh/authorized_keys on the server. There's a good guide here.

Checking it worked

# From a Bash console on PythonAnywhere:
ls /var/www/sites/mysite # should show your code!

Setting up your PythonAnywhere web app

If you want to use your own domain for this step, I'll assume you already have it set up on your registrar with a CNAME pointing at yourusername.pythonanywhere.com. More info.

We've now got our code on the server, let's set it up as a PythonAnywere web app. This should be a one-off operation.

  • Go to the Web tab
  • Add a new web app
  • Enter its domain name
  • Choose "Manual Configuration", and then choose your Python version
  • Hit next and see the "All done" message

Next, edit your WSGI file, and make it point at the wsgi app in your code. I was using a Django app, so mine looked like this:

# /var/www/www_mydomain_com_wsgi.py
import os
import sys

path = '/var/www/sites/mysite'
if path not in sys.path:
    sys.path.append(path)

os.environ['DJANGO_SETTINGS_MODULE'] = 'superlists.settings'

import django.core.handlers.wsgi
application = django.core.handlers.wsgi.WSGIHandler()

Go back to the web app tab, hit "Reload", and make sure your site works. It took me a couple of goes to get it right!

NB: There are some subtleties here, like getting static files and your database config working, which I'll gloss over for now. See the example at the end for some inspiration though -- you can definitely get almost anything to work!

Adding to our post-receive hook to automatically reload the site

This is the last step -- we use a sekrit, undocumented feature of PythonAnywhere, which is that our web workers actually watch the WSGI file for changes, so if you touch it, your web app will reload automatically.

So, back in ~/bare-repos/mysite.git/hooks/post-receive:

#!/bin/bash
GIT_WORK_TREE=/var/www/sites/mysite git checkout -f 
touch /var/www/www_mydomain_com_wsgi.py

Substitute in the path to your own WSGI file.

Testing it works

Back on your own PC, make a trivial but visible change to your app, and then:

git commit -am"test change to see if push to pa works"
git push pythonanywhere

It should just take a few seconds (although sometimes it takes as long as a minute) for the web worker to notice the touch to your web app, and then reload your site in your web browser... You should see your changes!

More fun in the post-receive hook

You can do pretty much anything you like in the post-receive, so for example:

#!/bin/bash

set -e # exit if anything fails

# run unit tests in a temp folder
mkdir -p /tmp/testcheckout
GIT_WORK_TREE=/tmp/testcheckout git checkout -f
python3 /tmp/testcheckout/manage.py test lists
rm -rf /tmp/testcheckout

# checkout new code
mkdir -p /var/www/sites/mysite
GIT_WORK_TREE=/var/www/sites/mysite git checkout -f

# update static files and database
python3 /var/www/sites/mysite/manage.py collectstatic -y
python3 /var/www/sites/mysite/manage.py migrate -y

# bounce web app
touch /var/www/test3_ottg_eu_wsgi.py

And so on. When you get it working, why not let us know! If this turns out to be popular, we may look to automate some of the steps as features...

Image credit: Human Pictogram 2.0 at pictogram2.com

PythonAnywhere News Round-up

$
0
0

It's been about 6 months since we last delivered a state-of-the-PythonAnywhere address and, looking at everything that's happened since the last one, it's long-overdue.

Following the extremely ... mixed ... reaction to our upworthy/buzzfeed spoof report, we decided to gauge the reaction if we went in totally the opposite direction. So let's get straight into PythonAnywhere's very first newsletter of 2014 — the "style is for wimps" edition!

Heartbleed

One of the biggest tech news items of the year so far is the discovery of the Heartbleed bug. Even though we (like almost every other Linux-based host) were vulnerable to it, we patched our servers just hours after it was announced to the public (for some reason we weren't included in the early disclosure group with Google and Facebook). Our users were safe from Heartbleed and they didn't have to do anything!

Securing PythonAnywhere from the Heartbleed bug

Customer stories

The reaction to our customer stories was so great last time that we've got 2 for you this time! You can read about

Credit card payments

Yes, we heard your cries and you can now pay us using your credit card without involving PayPal in any part of the transaction.

PythonAnywhere now accepts credit cards

Create your own pricing plan

Dismayed by the huge gulf between our $12 web developer accounts and our $99 startup accounts? We've got you covered — you can now design a pricing plan that exactly fits your unique requirements.

Custom plans

PythonAnywhere supports Python 3 web apps

That is all— I did warn you that this would be a no-frills newsletter.

In other news

Minor release - bugfixes and performance tweaks

$
0
0

A minor release today, which included:

  • A fix for the cairo/matplotlib regression
  • Tweaks to log file permissions, to prevent an issue where they would become non-readable by the user
  • Moving from several smaller servers to fewer larger ones, for web and console servers. Overall visible performance impact should be minor, but positive.

Happy coding everyone!

New release: Python 3.4, and more!

$
0
0

We released a new version of PythonAnywhere this morning. There were some nasty problems with the go-live (more about that later) but here's what we added:

  • Python 3.4 support, both for web applications and consoles.
  • sftp and rsync
  • Move from Amazon's us-east-1a region to us-east-1c -- this will allow us to switch to newer, faster instances next week!
  • And various minor bugfixes.

Thanks to gregdelozier, Malcolm, robert, aaronzimmerman, Cartroo, barnsey, andmalc, corvax, giorgostzampanakis, dominochinese, stablum, algoqueue for the suggestions.

Outage report: lengthy upgrade this morning

$
0
0

This morning we upgraded PythonAnywhere, and the upgrade process took much longer than expected. Here's what happened.

What we planned to happen

The main visible feature in this new version was Python 3.4. But a secondary reason in doing it was to move from one Amazon "Availablity Zone" to another. PythonAnywhere runs on Amazon Web Services, and availability zones are essentially different Amazon data centers.

We needed to move from the zone us-east-1a to us-east-1c. This is because the 1a zone does not support Amazon's latest generation of servers, called m3. m3 instances are faster than the m1 instances we currently have, and also have local SSD storage. We expect that moving from m1 to m3 instances will make PythonAnywhere -- our site, and sites that are hosted with us -- significantly faster for everyone.

Moving from one availability zone to another is difficult for us, because we use EBS -- essentially network-attached storage volumes -- to hold our customers' data. EBS volumes exist inside a specific availability zone, and machines in one zone can't connect to volumes in another. So, we worked out some clever tricks to move the data across beforehand.

We use a tool called DRBD to keep data safe. DRBD basically allows us to associate a backup server with each of our file servers. Every time you write to your private file storage in PythonAnywhere, it's written to a file server, which stores it on an EBS volume but also then also sends the data over to its associated backup server, which writes it to another EBS volume. This means that if the EBS volume on the file server goes wrong (possible, but unlikely) we always have a backup to switch over to.

So, in our previous release of PythonAnywhere last month, we moved all of the backup servers to us-east-1c. Over the course of a few hours after that move, all of our customers' data was copied over to 1c, and it was then kept in sync on an ongoing basis over the following weeks.

What actually happened

When we pushed our update today, we essentially started a new PythonAnywhere cluster in 1c, but the EBS volumes that were previously attached to the old cluster's backup servers were attached to the new cluster's file servers (after making sure that all pending updates had synced across). Because all updates had been replicated from the old file servers in 1a to these disks in 1c, this meant that we'd transparently migrated everything from 1a to 1c with minimal downtime.

That was the theory. And as far as it went, it worked flawlessly.

But we'd forgotten one thing. Not all customer data is on file servers that are backed up this way. The Dropbox storage, and storage for web app logs, is stored in a different way. So while we'd migrated everyone's normal files -- the stuff in /home/USERNAME, /tmp, and so on -- we'd not migrated the logs or the Dropbox shares. These were stuck in 1a, and we needed to move them to 1c so that they could be attached to the new servers there.

This was a problem. Without the migration trick using DRBD, the best way to move an EBS volume from one availability zone to another is to take a "snapshot" of it (which creates a copy that isn't associated with any specific zone) and then create a fresh volume from the snapshot in the appropriate zone. This is not a quick process. And the problem only became apparent to us when we were committed enough to the move to us-east-1c that undoing the migration would have been risky and slow.

So, 25 minutes into our upgrade, we started the snapshot process. We hoped it would be quick.

After 10 minutes of the snapshots of the Dropbox and the log storage data running, we noticed something worrying. The log storage snapshot was running at a reasonable speed. But the Dropbox storage snapshot hadn't even reached 1% yet. This is when we started talking to Amazon's tech support team. Unfortunately, after much discussion with them, it was determined that there was essentially nothing that could be done to speed up either of the snapshots.

After discussion internally we came to the conclusion that while the system couldn't run safely without the log storage having been migrated, we could run it without the Dropbox storage. We've deprecated Dropbox support recently due to problems with our connection to Dropbox themselves, so we don't think anyone's relying on it. So, we waited until the log storage snapshot completed (which took about 90 minutes), created a new EBS volume in us-east-1c, and brought the system back up.

Where we are now

  • PythonAnywhere is up and running in the us-east-1c availability zone, and we'll be able to start testing higher-speed m3 servers next week.
  • All of our customers' normal file data is in us-east-1c (with the old ones in 1a kept as a third-level backup for the time being)
  • All of the log data is also stored in both 1c and 1a, with the 1c copy live.
  • The Dropbox storage is still in us-east-1a and is still snapshotting (15% done as of this post). Once the snapshot is complete, we'll create a new volume and attach it to the current instance, and start Dropbox synchronisation.

Our apologies for the outage.

Outage Report for 15 July 2014

$
0
0

After a lengthy outage last night, we want to let you know about the events that led up to it and how we can improve our outage responses to reduce or eliminate downtime when things go wrong.

As usual with these things, there is no single cause to be blamed. It was a confluence of a number of things happening together:

  1. An AWS instance that was running a critical piece of infrastructure had a hardware failure
  2. We performed a non-standard deploy earlier in the day
  3. We took too long to realize the severity of the hardware failure

It is a fact of life that our machines run on physical hardware. As much as the cloud, in general, and AWS, in particular, try to insulate us from that fact. Hardware fails and we need to deal with it when it does. In fact, we believe that a large part of the long-term value of PythonAnywhere is that we deal with it so you don't have to.

Since our early days, we have been finding and eliminating single points of failure to increase the robustness of our service, but there are still a few left and we have plans to eliminate them, too. One of the remaining ones is the file server and that's the machine that suffered the hardware failure last night.

The purpose of the file server is to make your private file storage available to all of the web, console, and scheduled task servers. It does this by owning a set of Elastic Block Storage devices, arranged in a RAID cluster, and sharing them out over NFS. This means that we can easily upgrade the file server hardware, and simply move the data storage volumes over from the old hardware to the new.

Under normal operation, we have a backup server that has a live, constantly updated copy of the data from the file server, so in the event of either a file server outage, or a hardware problem with the file server's attached storage, we can switch across to using that instead. However, yesterday, we upgraded the storage space on the backup server and switched to SSDs. This meant that, instead of starting off with a hot backup, we had a period where the backup server disks were empty and were syncing with the file server. So we had no fallback when the file server died. Just to be completely clear -- the data storage volumes themselves were unaffected. But the file server that was connected to them, and which serves them out via NFS, crashed hard.

With all of that in mind, we decided to try to resurrect the file server. On the advice of AWS support, we tried to stop the machine and restart it so it would spin up on new hardware. The stop ended up taking an enormous amount of time and then restarting it took a long time, too. After trying several times and poring over boot logs, we determined that the boot disk of the instance had been corrupted by the hardware failure. Now the only path we could see to a working cluster was to create an entirely new one which could take over and use the storage disks from the dead file server. So we kicked off the build (which takes about 20min) and waited. After re-attaching the disks, we checked that they were intact and switched over to the new cluster.

Lessons and Responses

Long term

  • A file server hardware failure is a big deal for us, it takes everything down. Even under normal circumstances switching across to the backup is a manual process and takes several minutes. And, as we saw, rare circumstances can make it significantly worse. We need to remove it as a single point of failure.

Short term

  • A new cluster may be necessary to prevent extended downtime like we had last night. So our first response to failure of the file server must be to start spinning up a new cluster so it's available if we need it. This should mean that our worst downtime could be about 40 mins from when we start working on it to having everything back up.
  • We need to ensure that all deploys (even ones where we're changing the storage on either the file or backup servers) start with the backup server pre-seeded with data so the initial sync can be completed quickly.

We have learned important lessons from this outage and we'll be using them to improve our service. We would like to extend a big thank you and a grovelling apology to all our loyal customers who were extremely patient with us.


New Release

$
0
0

Here's what's new in the latest version of PythonAnywhere that we released this morning:

  • User files are now on SSDs, so we're expecting to see some performance improvements.
  • We've implemented a fix for the issue that we believe has been causing the recent outages and database access issues.
  • We've improved the general security of the PythonAnywhere web site.
  • We've added some minor fixes to the user interface

PythonAnywhere is looking for a new developer

$
0
0

ancient greek cult initiation

Fancy helping to build the Python world's favourite PaaS (probably)? We're looking for a "junior" programmer with plenty of smarts to come and join the team, learn the stuff we do, and inject some new ideas...

Job spec

Here's some stuff you'd be doing:

  • Working in an Extreme Programming (XP) shop, pair programming and TDD all day, woo*.

  • Coding in Python lots and JavaScript a bit, and maybe other stuff too (OK there's like 5 lines of Lua code somewhere. But you could come along and try and convert us all to ClojureScript or something...)

  • Devops! Or what we take it to mean, which is that you deploy to and administer the servers as well as write code for them. Lots of Linux command-line goodness in SSH sessions, and automated deployment stuff.

  • Sexy CV-padding technologies! Like Docker, nginx, websockets, Django, copy-on-write filesystems, Ansible, GNU/Linux (Ubuntu), virtualbox, vagrant, continuous integration, AWS, redis, Postgres, and even Windows XP! (although we're phasing that last one out, to our great chagrin). Don't worry, you don't have to know any of these before you show up, you'll get to learn them all on the job...

  • Learn vim (if you want to) much faster than you would on your own by being forced to pair program with vim cultists all too happy to explain the abstruse keyboard shortcuts they're using...

  • Get involved in the nonprogramming aspects of the business too (we all do!), like customer support and marketing. Honestly, that can be fun too.

  • Work near enough to Silicon Roundabout that you can walk to the Hacker News meetups, but not so near that you're forced to overhear bad startup ideas being pitched in every coffee shop

* The pair programming thing is an unbelievably good deal for new developers btw, there's just no faster way of learning than sitting down next to someone that knows and being able to ask them questions all day, and they're not allowed to get annoyed.

Person spec

Here's the kind of person we'd like

  • Smart -- academically or otherwise. A degree (CS or other), won't hurt, but it's not required either.
  • An enthusiastic programmer (but not necessarily Python and not necessarily professional)
  • A bit wacky (which doesn't mean you have to be an extrovert, just inclined to left-field ideas)
  • Willing to come and work here in sunny Clerkenwell
  • Willing to get paid less than you would at Google or a bank, in exchange for working in an exciting but relaxed tech-startup environment

Here's some stuff we don't care about:

  • Age
  • Male/Female/Black/White/Number of functioning limbs/Space alien.
  • Gaps in CVs
  • Current country of residence (as long as you're willing to move here promptly! You do need to be able to speak good English, and unfortunately we're too small to sponsor visas, so you need to already have the to right live + work in the UK)
  • Dress codes

Send us an email telling us why you'd like to work here, and a current CV, to jobs@pythonanywhere.com

Image source: wikimedia commons, Eleusinian Mysteries

Slides for Giles Thomas' EuroPython talk now online

New release - a few new packages, some steps towards postgres, and forum post previews

$
0
0

Today's upgrade included a few new packages in the standard server image:

  • OpenSCAD
  • FreeCAD
  • inkscape
  • Pillow for 3.3 and 3.4
  • flask-bootstrap
  • gensim
  • textblob

We also improved the default "Unhandled Exception" page, which is shown when a users' web app allows an exception to bubble up to our part of the stack. We now include a slightly friendlier message, explaining to any of the users' users that there's an error, and explaining to the user where they can find their log files and look for debug info.

And in the background, we've deployed a bunch of infrastructure changes related to postgres support. We're getting there, slowly slowly!

Oh yes, and we've enabled dynamic previews in the forums, so you get an idea of how the markdown syntax will translate. It actually uses the same library as stackoverflow, it's called pagedown. Hope you find 'em useful!

Test-Driving a docker-based Postgres service using py.test

$
0
0

[cross-posted at Obey The Testing Goat!]

We've been working on incorporating a Postgres database service into PythonAnywhere, and we decided to make it into a bit of a standalone project. The shiny is that we're using Docker to containerise Postgres servers for our users, and while we were at it we thought we'd try a bit of a different approach to testing. I'd be interested in feedback -- what do you like, what might you do differently?

Context: A Docker-based Postgres service

The objective is to build a service that, on demand, will spin up a Docker container with Postgres running on it, and listening on a particular port. The service is going to be controlled by a web API. We've got Flask to run the web service, docker-py to control containers, and Ansible to provision servers.

A single loop of integrated tests

Normally we use a "double-loop" TDD process, with an outside loop of functional tests that use selenium to interact with our web app, and an inner loop of more isolated unit tests. For our development of the Postgres service, we still have the outer loop of functional tests -- selenium tests that log into the site via a browser, and test the service from the perspective of the user -- clicking through the right buttons on our UI and seeing if they can get a console that connects to a new Postgres service.

But for the inner loop we were in a green field -- this wasn't going to be another app in our monolithic Django project, we wanted it to be a standalone service, one that you could package up and use in another context. It would provide all its services via an API, and need no knowledge of the rest of PythonAnywhere. So how should we write the self-contained tests for this app? Should it, in turn, have a double loop? Relying on isolated unit tests only felt like a waste of time -- after all, the whole app was basically a thin wrapper that hooks up a web service to a series of Docker commands. All boundaries. Isolated unit tests would end up being all mocks. And from a TDD-process point of view, because we'd never actually used docker-py before, we didn't know its API, so we wouldn't know what mocks to write before we'd actually decided what the code was going to look like, and tried it out. And trying it out would involve either running one of the PythonAnywhere FTs (super-slow, so a tediously and onerous feedback loop), or with manual tests, with all the uncertainty that implies.

So instead, it felt like starting with an intermediate-level layer of integrated tests might be best: we've already got our top-level UI layer full-stack tests in the form of functional tests. The next level down was the API level -- does calling this particular URL on the API actually give us a working container?

An example test

def test_create_starts_container_with_postgres_connectable(docker_cleanup):
    response = post_to_api_create()

    port = response.json()["port"]
    assert port > 1024

    connection = psycopg2.connect(
        database="postgres",
        user="pythonanywhere_helper", password="papwd",
        host="localhost", port=port,
    )
    connection.close()

Where

def post_to_api_create():
    response = requests.post(
        "http://localhost:5000/api/create",
        {"admin_password": "papwd"}
    )
    assert response.status_code == 200
    assert response.json()["status"] == "OK"
    return response

So you can see that's a very integration-ey, end-to-end test -- it does a real POST request, to a place where it expects to see an actual webapp running, and it expects to see a real, connectable database spun up and ready for it.

Now this test runs in about 10 seconds - not super-fast, like the milliseconds you might want a unit test to run in, but much faster than our FT, which takes 5 or 6 minutes. And, meanwhile, we can actually write this test first. To write an isolated, mocky test, we'd need to know the docker-py API already, and be sure that it was going to work, which we weren't.

To illustrate this point, take a look at the difference between an early implementation and a later one:

A first implementation

USER_IMAGE_DOCKERFILE = '''
FROM postgres
USER postgres
RUN /etc/init.d/postgresql start && \\
    psql -c "CREATE USER pythonanywhere_helper WITH SUPERUSER PASSWORD '{hashed}';"
CMD ["/usr/lib/postgresql/9.3/bin/postgres", "-D", "/var/lib/postgresql/9.3/main", "-c", "config_file=/etc/postgresql/9.3/main/postgresql.conf"]
'''

def get_user_dockerfile(admin_password):
    hashed = 'md5' + md5(admin_password + 'pythonanywhere_helper').hexdigest()
    return USER_IMAGE_DOCKERFILE.format(
        hashed=hashed,
    )

def create_container_with_password(password):
    tempdir = tempfile.mkdtemp()
    with open(os.path.join(tempdir, 'Dockerfile'), 'w') as f:
        f.write(get_user_dockerfile(password))

    response = docker.build(path=tempdir)
    response_lines = list(response)

    image_finder = r'Successfully built ([0-9a-f]+)'
    match = re.search(image_finder, response_lines[-1])
    if match:
        image_id = match.group(1)
    else:
        raise Exception('Image failed to build:\n{}'.format(
            '\n'.join(response_lines)
        ))

    container = docker.create_container(
        image=image_id,
    )
    return container

(These are some library functions we wrote, I won't show you the trivial flask app that calls them).

This was one of our first attempts -- we needed to be able to customise the Postgres superuser password for each user, and our initial solution involved building a new image for each user, by generating and running a custom Dockerfile for them.

We were never quite sure whether the Dockerfile voodoo was going to work, and we weren't really Postgres experts either, so having the high-level integration test, which actually tried to spin up a container and connect to the Postgres database that should be running inside it, was a really good way of getting to a solution that worked.

Imagine what a more isolated test for this code might look like:

@patch('containers.docker')
def test_uses_dockerfile_to_build_new_image(mock_docker):
    expected_dockerfile = USER_IMAGE_DOCKERFILE.format(
        'md5sekritpythonanywhere_helper'
    ).hexdigest()
    def check_dockerfile_contents(path):
        with open(os.path.join(path, 'Dockerfile')) as f:
            assert f.read() == expected_dockerfile

    mock_docker.build.side_effect = check_dockerfile_contents

    create_container_with_password('sekrit')

    assert mock_docker.build.called is True

 @patch('containers.docker')
def test_creates_container_from_docker_image(mock_docker):
    create_container_with_password('sekrit')
    mock_docker.create_container.assert_called_once_with(
        mock_docker.build.return_value
    )

There's no way we could have written that test until we actually had a working solution. And, on top of that, the test would have been totally useless when it came to evolving our requirements and our solution

A later implementation -- but minimal change to the main test

To give you an idea, here's what our current implementation looks like:

def start_new_container(storage_dirname, password, requested_port):
    prep_storage_dir(storage_dirname)
    run_command_on_temporary_container_with_mounts(
        command=['chown', '-R', 'postgres:postgres', POSTGRES_DIR],
        storage_dirname=storage_dirname,
        user='root',
    )
    run_command_on_temporary_container_with_mounts(
        command=[
            'bash', '-c', 
            INITIALISE_POSTGRES_AND_SET_PASSWORD.format(password)
        ],
        storage_dirname=storage_dirname
    )
    user_container = create_postgres_container(name=storage_dirname)
    start_container_with_storage(
        user_container, storage_dirname, 
        ports={POSTGRES_PORT: requested_port},
    )
    with open(port_file_path(storage_dirname), 'w') as f:
        f.write(str(requested_port))
    return requested_port

I won't bore you with the details of run_command_on_temporary_container_with_mounts, but one way or another we realised that building separate images for each user wasn't going to work, and that instead we were going to want to have some permanent storage mounted in from outside of Docker, which would contain the Postgres data directory, and which would effectively "save" customisations like the user's password.

So a radically different implementation, but look how little the main test changed:

def post_to_api_create(storage_dir=None, port=None):
    if storage_dir is None:
        storage_dir = uuid.uuid4()
    if port is None:
        port = random.randint(6000, 9999)
    response = requests.post(
        "https://localhost/api/containers/",
        {
            "storage_dir": storage_dir,
            "admin_password": OUR_PASSWORD,
            "port": port,
        },
        verify=False,
    )
    return response

def test_create_starts_container_with_postgres_connectable(docker_cleanup):
    response = post_to_api_create(port=6123)
    # rest of test as before!

And now imagine all the time we'd have had to spend rewriting mocks, if we'd decided to have isolated tests as well.

Aside: py.test observations

One py.test selling point is "less boilerplate". Notice that none of these tests are methods in a class, and there's no self variable. On top of that, we just use assert keywords, no complicated remembering of self.assertIn, self.assertIsNotNone, and so on. Absolutely loving that.

py.test fixtures

Another thing you may be interested in is the docker_cleanup argument to the test. py.test will magically look for a special fixture function named the same as that argument, and use it in the test. Here's how it looks:

from docker import Client
docker = Client(base_url='unix://var/run/docker.sock')

@pytest.fixture()
def docker_cleanup(request):
    containers_before = docker.containers()

    def kill_new_containers():
        current_containers = docker.containers()
        for container in current_containers:
            if container not in containers_before:
                print('killing {}'.format(container['Names'][0]))
                docker.kill(container)

    request.addfinalizer(kill_new_containers)
    return kill_new_containers

The fixture function has a couple of jobs:

  • It adds a "finalizer" (the equivalent of unittest addCleanup or tearDown) which will run at the end of the tests, to kill any containers that have been started by the test

  • It provides that same finalizer, and a helper method to identify new containers, to the tests that use the fixture, as a helper tool (I haven't showed any examples of that here though)

As it's illustrated here, there are no obvious advantages over the unittest setUp/tearDown ideas, although you can see it would make it a little easier to share setup and cleanup code between tests in different files and tests. There's a lot more to them, and if you really want to get #mindblown, go checkout out pytest yield fixtures

Incidentally, until I started using py.test I'd always associated "fixtures" with Django "fixtures", which basically meant serialized versions of model data, but really py.test is using the word in a more correct usage of the term, to mean "state that the world has to be in for the test to run properly".

The pros & cons of the "integrated-tests-only" workflow

Pros:

  • Allowed us to experiment freely with an API that was new to us, and get feedback on whether it was really working
  • Allowed us to refactor code freely, extracting helper functions etc, without needing to rewrite mocky unit tests

Cons:

  • Being end-to-end tests, they ran much slower than unit tests would - on the order of seconds, and later, a minute or two, once we grew from three or four tests to a dozen or two. And, on top of that...

  • Being integrated tests, they're not designed to run on a development machine. Instead, each code change means pushing updated source up to the server using Ansible, restarting the control webapp, and then re-running the tests in an SSH session.

  • Because the tests call across a web API, the code being tested runs in a different process to he test code, meaning tracebacks aren't integrated into your test results. Instead, you have to tail a logfile, and make sure you have logging set up appropriately.

Conclusions and next steps

I can potentially imagine a time when we might start to see value in a layer of "real" unit tests... So far though, there's really no "business logic" that we could extract and write fast unit tests for. Or at least, there's no business logic that I identify as such, and I'd be very pleased for someone to come along and school me about it?

On the other hand, I can definitely see a time where we might want to split out our tests for the web API from the tests for the Postgres and Docker stuff, and I can see value in a setup where a developer can run these tests locally rather than having to push code up to a dev box. Vagrant and VirtualBox might be one solution, but, honestly, installing Docker and Postgres on a dev box doesn't feel that onerous either, as long as we know we'll be testing on a "real" box in CI. Or at least, it doesn't feel onerous until we start talking about my poor laptop with its paltry 120GB SSD. No room here!

And the bonus of being able to see honest-to-God tracebacks in your test run output feels like it might be worth it.

But, overall, at this stage in development, given the almost total lack of "business logic" in our app, and given the fact that we were working with a new API and a new set of technologies -- I've found that doing without "real" unit tests has actually worked very well.

Site updates on 1 October 2014

$
0
0

We've just updated PythonAnywhere, and there's some great news: Postgres is now in beta! We've switched it on for a select list of beta testers; if you'd like to join, drop us a line at support@pythonanywhere.com.

There have also been some minor tweaks and updates:

  • New installed packages: OpenCV, the libproj Ubuntu package (useful for some Python GIS packages), WeasyPrint,
  • General website speedup (improved minifying of CSS and JavaScript).
  • The "Web" and "Databases" tabs remember which sub-tab you're on between visits.
  • Better validation on the web tab.
  • The page displayed for web apps that have their DNS set up to route to PythonAnywhere but haven't been set up has a better explanation of what's going on.

Try PythonAnywhere for free for a month!

$
0
0
Angkor Wat steps
Step Right Up Folks, Step Right Up...

OK, so we already have a free account, but we'd like to give out a free trial of our most popular paid plan, the "Web Developer" plan. Here's what you have to do to claim your free schtuff:

  1. Sign up for PythonAnywhere, if you haven't already
  2. Upgrade to a Web Developer account using Paypal or a credit card (we use this as a basic way of verifying your identity, to mitigate the likelihood of being used as a DDoS platform)
  3. Build a Thing using PythonAnywhere (even a really silly trivial Thing. Say a double-ROT13 encrypter)
  4. Get in touch with us, quoting the code UNGUESSABLEUNIQUECODE1, and show us your Thing
  5. And we'll refund your first month's payment!

Then, play around with the shiny paid features for the rest of the month! Build up to 3 web apps on whatever domains you like, splurge your 3,000 second CPU allowance or your 5GB of storage, use SSH and SSL to your heart's content, whatever it'll be. Then when your month is up, you can cancel if you don't like it (no need to quote any more codes or anything, just hit cancel, no questions asked).

PS -- this isn't some scheme to catch you out and tax you for a month if you forget to cancel. We'll send you a reminder a couple of days before the renewal date, reminding you that your trial is almost over and that you'll be auto-renewed if you don't cancel. And if you happen to miss that, and you get in touch all sore about how we charged you for the second month, we'll totally refund you that too. IT IS MATHEMATICALLY IMPOSSIBLE TO SAY FAIRER THAN THAT.

PPS -- offer expires at the end of this week! You have until 23:59 UTC on the 2nd of Nov.


Maintenance release: trusty + process listings

$
0
0

Hi All,

A maintenance release today, so nothing too exciting. Still, a couple of things you may care about:

  • We've updated to Ubuntu Trusty. Although we weren't vulnerable to shellshock, it's nice to have the updated Bash, and to be on an LTS release

  • We've added an oft-requested feature to be able to view all your running console processes. You'll find it at the bottom of the consoles page. The UI probably needs a bit of work, you need to hit refresh to update the list, but it's a solution for when you think you have some detached processes chewing up your CPU quota! Let us know what you think.

Other than that, we've updated our client-side for our Postgres beta to 9.4, and added some of the PostGIS utilities. (Email us if you want to check out the beta). We also fixed an issue where a redirect loop would break the "reload" button on web apps, and we've added weasyprint and python-svn to the batteries included.

Outage report: 1st November 2014

$
0
0

We had an outage this morning that lasted about an hour. We've established the cause, fixed the problem, and all sites are now back up. Apologies to all those affected. More detail follows.

We were alerted to an initial problem via our monitoring systems around 9:50AM. The symptom was that one of the web servers was seeing intermittent outages, due to a memory leak in one of our users' web applications causing a lot of swapping.

Our failover procedure involves taking the affected server out of rotation on the load balancer, redistributing its workload across to other servers, and rebooting it. We saw the same user using a lot of memory on a second server, so we able to confirm that that was a repeatable issue. We disabled his web app and rebooted this second server.

At this point the larger issue kicked in, which was that the rebooted servers seemed to be non-functional when they came back, which left the remaining servers struggling to keep up with the load, and causing outages to more customers. By this point two of us were working on the issue, and it took us a while to identify the root cause. It turned out to be due to a change in our logging configuration which was causing nginx to hang on startup. Specifically, it only affected users with custom SSL configuration. The reason that this was particularly baffling to us is that our deploy procedure involves a manual check on a sample of custom SSL users, and we confirmed they were functional when we did that deploy two days ago. Our working theory is that nginx will reload happily with broken logging config, but not restart happily:

On deploy:

  1. Start nginx
  2. Add custom SSL webapp configs
  3. Reload nginx

--> not a problem, despite broken SSL webapp logging

On reboot:

  1. Custom SSL configs with broken logging are already present on disk
  2. Nginx refuses to start

We'll be confirming this theory in development environments over the next few days.

In the meantime, we've fixed the offending configuration file template, and confirmed that both regular users and custom-SSL users sites are back up. We're also adding some safeguards to prevent any other users' web apps from using up too much memory.

Once again, we apologise to all those affected.

New release -- new console with 256 colours, some fixes to task logging, and the P-thing.

$
0
0

Exciting new deploy today!

A new console

Obviously, the most important thing we did was to switch out our javascript console for a new one that supports 256 colours! And slightly more sane copy + paste. And it works on Android, or at least it does on Lollipop. Giles recommends the Hackers keyboard. Still doesn't work on my blackberry though.

For the curious, it's based on hterm which is a part of Chromium...

Some new packages

Of secondary importance, we added a few new packages, including TA-lib, pytesseract, and a thing called ruffus.

Improved logging of scheduled tasks

Scheduled tasks now log directly to files in /var/log, rather than storing their output in our database. That means they'll get log-rotated like everything else in there, and if you call flush on your sys.stdout, you may even be able to see live updates while tasks are still running. I think.

New database type supported.

Oh, and we also released a new database type, it's called Postgres, I'm told it's quite popular. Skip on over to the accounts page and get yourself a Custom account if you want to check it out.

Happy coding everyone!

PythonAnywhere now supports Postgres

$
0
0
Finally!

tl;dr: upgrade to a Custom account and you can now add Postgres

Say no to multi-tenancy (ak The Whale vs the Elephant vs the Dolphin)

Postgres has been the top requested feature for as long as we've supported webapps (3 years? who's counting!). We did have a brief beta based on the idea of a single postgres server, and multi-tenanted low-privileged accounts for each user, but it turned out Postgres really doesn't work too well that way (unlike MySQL).

Our new solution uses Linux containers to provide an isolated server for each user, so everyone can have full superuser access. And, yes, it uses the ubiquitous Docker under the hood.

How to get it

  • You'll need to upgrade to a Custom account, and enable Postgres, as well as choosing how much storage you need for your database.

  • Then, head on over to the Databases tab, and click the big button that says "Start a Postgres server".

  • Once it's ready, take a note of its hostname and port

  • Set your superuser passsword. We'll save it to ~/.pgpass for convenience.

  • And now hit "Start a Postgres console" and take a look around!

How it works under the hood

We run several different machines to host postgres containers for our users. When you hit "start my server", we scan through to find a machine with spare capacity, and ask it to build up a container for you.

It's a docker container based on our postgres image, but each individual user's is customised slightly. For example, we create a superuser account called "pythonanywhere_helper" with a unique, random password to enable us to perform some admin functions. (once you have your own superuser account you could theoretically delete this guy, but we'd rather you didn't...)

We also set up a special permanent storage area for your database, which gives us the ability to migrate you to a different server if we need to.

You can read a bit more about our TDD process for developing this part of the codebase here

What it costs

Aha, the dreaded question. We've set the price for the basic service at $15/month which includes 1GB of storage, and subsequent gigs are 20cents/gig, which we think is pretty fair... Feel free to have a moan if you think it isn't!

What to do next

That's up to you! We've tried to supply you with everything you could need, including the bleeding-edge 9.4 version of Postgres, PostGIS, PL-Python and PL-Python3.... Let us know how you get on!

New PythonAnywhere update: Mobile, UI, packages, reliability, and the dreaded EU VAT change

$
0
0

We released a bunch of updates to PythonAnywhere today :-) Short version: we've made some improvements to the iPad and Android experience, applied fixes to our in-browser console, added a bunch of new pre-installed packages, done a big database upgrade that should make unplanned outages rarer and shorter, and made changes required by EU VAT legislation (EU customers will soon be charged their local VAT rate instead of UK VAT).

Here are the details:

iPad and Android

  • The syntax-highlighting in-browser editor that we use, Ace, now supports the iPad and Android devices, so we've upgraded it and changed the mobile version of our site to use it instead of the rather ugly textarea we used to use.
  • We've also re-introduced the "Save and Run" button on the iPad.
  • Combined with the console upgrade we did earlier on this month, our mobile support should now be pretty solid on iPads, iPhones, and new Android (Lollipop) devices. Let us know if you encounter any problems!

User interface

  • Some fixes to our in-browser consoles: fixed problems with zooming in (the bottom line could be cut off if your browser zoom wasn't set to 100%) and with the control key being stuck down if you switched tabs while it was pressed.
  • A tiny change, but one that (we hope) might nudge people in a good direction: we now list Python 3 before Python 2 in our list of links for starting consoles and for starting web apps :-)

New packages

We've added loads of new packages to our "batteries included" list:

  • A quantitative finance library, Quandl (Python 2.7, 3.3 and 3.4)
  • A backtester for financial algorithms, zipline (Python 2.7 only)
  • A Power Spectral Densities estimation package, spectrum (2.7 only)
  • The very cool remix module from Echo Nest: make amazing things from music! (2.7 only)
  • More musical stuff: pyspotify. (2.7 only)
  • Support for the netCDF data format (2.7, 3.3 and 3.4)
  • Image tools: imagemagick and aggdraw (2.7, 3.3 and 3.4)
  • Charting: pychart (2.7 only)
  • For Django devs: django-bootstrap-form (2.7, 3.3 and 3.4)
  • For Flask devs: flask-admin (2.7, 3.3 and 3.4)
  • For web devs who prefer offbeat frameworks: falcon and wheezy.web (2.7, 3.3 and 3.4)
  • A little ORM: peewee (2.7, 3.3 and 3.4)
  • For biologists: simplehmmer (2.7 only)
  • For statistics: Augustus. (2.7 only)
  • For thermodynamicists (?): CoolProp (2.7, 3.3 and 3.4)
  • Read barcodes from images or video: zbar (2.7 only)
  • Sending texts: we've upgraded the Python 2.7 twilio package so that it works from free accounts, and also added it for Python 3.3 and 3.4.
  • Locating people by IP address: pygeoip (2.7, 3.3 and 3.4)
  • We previously had the rpy2 package installed but there was a bug that stopped you from importing rpy2.robjects. That's fixed.

Additionally, for people who like alternative shells to the ubiquitous bash, we've added fish.

Reliability improvement

We've upgraded one of our underlying infrastructural databases to SSD storage. We've had a couple of outages recently caused by problems with this database, which were made much worse by the fact that it took a long time to start up after a failover. Moving it to SSD moved it to new hardware (which we think will make it less likely to fail) and will also mean that if it does fail, it should recover much faster.

EU VAT changes

For customers outside the EU, this won't change anything. But for non-business customers inside the EU, starting 1 January 2015, we'll be charging you VAT at the rate for your country, instead of using the UK VAT rate of 20%. This is the result of some (we think rather badly-thought-through) new EU legislation. We'll write an extended post about this sometime soon. [Update: here it is.]

Viewing all 354 articles
Browse latest View live