Freezing and Thawing Droplets in a DigitalOcean

April 21, 2014

DigitalOcean has been a game-changer for me.  Why create another space-hungry VM locally when you can spin up a new machine in 60 seconds on DO?  And it gets better: Tugboat.  Now, I can manage my droplets from the command line.  Since, I typically use (and reuse) droplets like I did local VMs, I often want to set a droplet aside for a while (maybe weeks or months) and return to it later.  Fortunately, DO provides a way to put snapshots into cold storage and then retrieve them later.  But freezing and thawing droplets wasn’t easy enough.  I made a suggestion to DO, but I doubt it’ll be implemented anytime soon (if ever), so I used Tugboat and some Groovy scripts to roll my own.

Here is what I was looking for:

Freezing a droplet

Thawing a droplet

The goal:

$ # Assume we have a droplet foo
$ tugboat create foo
$ # Imagine you're done working with foo for now
$ freeze foo
$ # Foo is a snapshot & the droplet is destroyed.
$ # ... weeks pass and you have a hankering for foo ...
$ thaw foo
$ # Foo is back, Baby!

While it would be great to have freeze & thaw button on the DO website freeze & thaw parameters for Tugboat, I didn’t have the time to make a pull request for Tugboat… so here are the scripts:

~/bin/freeze

This script will snapshot a droplet, replacing any snapshot of the same name, and destroy the droplet.

#!/usr/bin/env groovy

class TugboatException {
	// Our very own little exception is born. It's a buoy!
}

def getImageInfo = {
	"tugboat images".execute().text.split("\n").find{
		it.startsWith(imageName+" ")
	}
}

def waitFor = { imageName, to='appear' -> /* to='appear' or 'disappear' */
	attempts = 0
	while (true) {
		attempts++
		imageInfo = "tugboat images".execute().text.split("\n").find{
			it.startsWith(imageName+" ")
		}
		if ((imageInfo && to=='appear') || (!imageInfo && to=='disappear')) break
		if (imageInfo || attempts > 20) {
			throw new TugboatException("$imageName did not $to within 3 min. Gave up.")
		}
		sleep(10000) // wait 10 seconds between checks
	}
}

def cmd = { description, command ->
	print description
	response = command.execute().text
	println "done."
}

def cli = new CliBuilder(usage:'freeze DROPLET_NAME')
cli.q(longOpt:'quiet', '')
def options = cli.parse(args)

if (!options.arguments() || options.arguments().size != 1) {
	cli.usage()
	System.exit(0)
}

imageName = options.arguments()[0]

if (! "tugboat droplets".execute().text ==~ /(?ims).*^$imageName\s.*/) {
	println "$imageName droplet does not exist"
	System.exit(1)
}

imageInfo = getImageInfo()
if (imageInfo) {
	imageId = (imageInfo =~ /id:\s*(\d+)/)[0][1]
	if (imageId) {
		cmd("Destroying old $imageName image...", "tugboat destroy-image -c -i $imageId")
		waitFor(imageName, 'disappear')
	}
}

cmd('Telling droplet to halt...', "tugboat halt $imageName")

cmd('Waiting for droplet to shut down...', "tugboat wait $imageName -s off")

sleep(3000)

cmd('Taking snapshot of droplet...', "tugboat snapshot $imageName $imageName")

print "Waiting for image to complete..."
waitFor(imageName)
println "done."

cmd("Destroying $imageName droplet...", "tugboat destroy -c $imageName")

~/bin/thaw

This script will restore a frozen droplet and start it up for you.

#!/usr/bin/env groovy

def getImageInfo = {
	"tugboat images".execute().text.split("\n").find{
		it.startsWith(imageName+" ")
	}
}

def cmd = { description, command ->
	print description
	response = command.execute().text
	println "done."
}

def cli = new CliBuilder(usage:'thaw IMAGE_NAME')
cli.q(longOpt:'quiet', '')
def options = cli.parse(args)

if (!options.arguments() || options.arguments().size != 1) {
	cli.usage()
	System.exit(0)
}

imageName = options.arguments()[0]

if ("tugboat droplets".execute().text ==~ /(?ims).*^$imageName\s.*/) {
	println "$imageName droplet already exists"
	System.exit(1)
}

imageInfo = getImageInfo()
if (!imageInfo) {
	println "Image $imageName not found"
	System.exit(2)
}

imageId = (imageInfo =~ /id:\s*(\d+)/)[0][1]
if (!imageId) {
	println "Unable to parse $imageName image id"
	System.exit(3)
}

print "Thawing image..."
response = "tugboat create $imageName -i $imageId".execute().text
println "done."

print "Waiting for droplet to start..."
response = "tugboat wait $imageName".execute().text
println "done."

Guns make a difference

April 4, 2014

Guns make a difference

There are now two definitions for insanity:

in·san·i·ty [in-san-i-tee]
  1. Doing the same thing over and over again and expecting different results.
  2. Believing  the solution to gun violence is more guns.

 

Hack in the Right Direction

March 12, 2014
Hack in the right direction!
For over a decade, I’ve been using the phrase “hacking in the right direction” and, while I’ve explained it several times over the years, I was surprised to discover that I never blogged about it. So, here’s an explanation…

As any experienced coder knows, there’s some code that they are proud of and other code that is just there to get the job done. The latter is often referred to as “a hack” or “hacky code.” While it’s fair to say that coders wish all of their code could be beautiful, the reality of implementation requirements, delivery timelines, and competing demands nearly always force some amount of compromise. This leads many developers to approach projects in one of two ways:

Conditions Approach
  • Sufficient timeline
  • Critical code
  • Shared or peer-reviewed code
Write good code
  • Short timelines
  • Not critical code
  • Short term need
  • Used by a single persion/group
Hack

While it’s not always this black & white, most developers are balancing these two approaches in their head and their code is an amalgam of these two approaches. In OpenMRS, we often contrast “Development” with “Implementation coding,” where the latter tends to involve more hacking to meet time constraints.

How can we leverage these two approaches? Do these approaches have to contradict each other or can we make them complimentary? This is where hacking in the right direction comes in:

Hack in the Right Direction

The next time life intervenes and prevents you from taking the time to generate beautiful code, before you just hack something out, take a moment to consider how you could hack in the right direction. This approach takes your ultimate goal(s) into consideration, but allows for guilt-free compromises.

The fundamental requirement of hacking in the right direction is understanding where you want to be. While a journey of a thousand miles may begin with a single step, it helps to take that step toward the destination.

patient selection widgetHow does this work in real life? I’ll give you an example that came up not too long ago. Imagine that you are asked to make a patient selection widget for an application. It would be pretty natural to have a method like this:

/* Get the selected patient */
public Patient getSelection();

But what if you took 20-30 minutes to review how the patient selection widget will be used in the application and learn that we want to use it for all patient selections, including for report generation where more than one patient may be selected? Then you would probably change your method to something like this:

/* Get the selected patient(s) */
public Patient[] getSelection();

It’s a subtle change, but it could have a dramatic effect on how you write and use your code. In this example, it’s a simple change to the method signature, so worth the effort. The trick is finding these easy wins without over-designing your hack. This approach will allow you to meet tight time constraints while at least building toward a longer-term vision.

A recipe for Hacking in the Right Direction

  1. Spend 20-30 minutes with one or more informed product owners to understand, as best as you can, their longterm vision.
  2. As you hack, look for inexpensive adjustments to your design that will support the longterm vision.

It’s not too hard and the investment is small, even when time constraints are tight. The next time you need to hack, take a moment, and try to hack in the right direction!

Faster resets for the OpenMRS Demo

March 11, 2014

I’ve long been bothered by how long it takes for our demo site (http://demo.openmrs.org) to be reset.  The site goes down for a few minutes each hour as the database is restored and the application restarted.  I was working on the demo site today in the middle of one of these resets and so I decided to look around for a better solution.

Fortunately, Akshay Suryawanshi from Percona (percona.com) recently shared a nifty rename_db script that can rename a MySQL database in a couple of seconds.  So, we put our credentials and host information into a file (don’t forget chmod 600 .my.cnf to protect those credentials):

.my.cnf

[client]
user=openmrs
password=secret
host=127.0.0.1
port=3306

Assuming the credentials and other settings are correct, then this script should show existing databases:

show-databases

#!/bin/bash
mysql --defaults-extra-file=.my.cnf -e "show databases"

Then we take Akshay’s rename_db script, define DEFAULTS=.my.cnf at the beginning, replace the “-h $1” references with --defaults-extra-file=$DEFAULTS, and remove the initial host parameter:

rename_db

Adapted from Akshay Suryawanshi’s blog post, thanks to the generous folks at Percona for sharing.

#!/bin/bash
# Copyright 2013 Percona LLC and/or its affiliates
# http://www.mysqlperformanceblog.com/2013/12/24/renaming-database-schema-mysql/
#
# Changes
# - Uses --defaults-extra-file for credentials, host, and port settings
set -e
DEFAULTS=.my.cnf
if [ -z "$2" ]; then
    echo "rename_db  "
    echo "(assumes $DEFAULTS defines user, password, host, and port under [client] section)"
    exit 1
fi
db_exists=`mysql --defaults-extra-file=$DEFAULTS -e "show databases like '$2'" -sss`
if [ -n "$db_exists" ]; then
    echo "ERROR: New database already exists $2"
    exit 1
fi
TIMESTAMP=`date +%s`
character_set=`mysql --defaults-extra-file=$DEFAULTS -e "show create database $1\G" -sss | grep ^Create | awk -F'CHARACTER SET ' '{print $2}' | awk '{print $1}'`
TABLES=`mysql --defaults-extra-file=$DEFAULTS -e "select TABLE_NAME from information_schema.tables where table_schema='$1' and TABLE_TYPE='BASE TABLE'" -sss`
STATUS=$?
if [ "$STATUS" != 0 ] || [ -z "$TABLES" ]; then
    echo "Error retrieving tables from $1"
    exit 1
fi
echo "create database $2 DEFAULT CHARACTER SET $character_set"
mysql --defaults-extra-file=$DEFAULTS -e "create database $2 DEFAULT CHARACTER SET $character_set"
TRIGGERS=`mysql --defaults-extra-file=$DEFAULTS $1 -e "show triggers\G" | grep Trigger: | awk '{print $2}'`
VIEWS=`mysql --defaults-extra-file=$DEFAULTS -e "select TABLE_NAME from information_schema.tables where table_schema='$1' and TABLE_TYPE='VIEW'" -sss`
if [ -n "$VIEWS" ]; then
    mysqldump --defaults-extra-file=$DEFAULTS $1 $VIEWS > /tmp/${1}_views${TIMESTAMP}.dump
fi
mysqldump --defaults-extra-file=$DEFAULTS $1 -d -t -R -E > /tmp/${1}_triggers${TIMESTAMP}.dump
for TRIGGER in $TRIGGERS; do
    echo "drop trigger $TRIGGER"
    mysql --defaults-extra-file=$DEFAULTS $1 -e "drop trigger $TRIGGER"
done
for TABLE in $TABLES; do
    echo "rename table $1.$TABLE to $2.$TABLE"
    mysql --defaults-extra-file=$DEFAULTS $1 -e "SET FOREIGN_KEY_CHECKS=0; rename table $1.$TABLE to $2.$TABLE"
done
if [ -n "$VIEWS" ]; then
    echo "loading views"
    mysql --defaults-extra-file=$DEFAULTS $2 < /tmp/${1}_views${TIMESTAMP}.dump
fi
echo "loading triggers, routines and events"
mysql --defaults-extra-file=$DEFAULTS $2 < /tmp/${1}_triggers${TIMESTAMP}.dump
TABLES=`mysql --defaults-extra-file=$DEFAULTS -e "select TABLE_NAME from information_schema.tables where table_schema='$1' and TABLE_TYPE='BASE TABLE'" -sss`
if [ -z "$TABLES" ]; then
    echo "Dropping database $1"
    mysql --defaults-extra-file=$DEFAULTS $1 -e "drop database $1"
fi
if [ `mysql --defaults-extra-file=$DEFAULTS -e "select count(*) from mysql.columns_priv where db='$1'" -sss` -gt 0 ]; then
    COLUMNS_PRIV="    UPDATE mysql.columns_priv set db='$2' WHERE db='$1';"
fi
if [ `mysql --defaults-extra-file=$DEFAULTS -e "select count(*) from mysql.procs_priv where db='$1'" -sss` -gt 0 ]; then
    PROCS_PRIV="    UPDATE mysql.procs_priv set db='$2' WHERE db='$1';"
fi
if [ `mysql --defaults-extra-file=$DEFAULTS -e "select count(*) from mysql.tables_priv where db='$1'" -sss` -gt 0 ]; then
    TABLES_PRIV="    UPDATE mysql.tables_priv set db='$2' WHERE db='$1';"
fi
if [ `mysql --defaults-extra-file=$DEFAULTS -e "select count(*) from mysql.db where db='$1'" -sss` -gt 0 ]; then
    DB_PRIV="    UPDATE mysql.db set db='$2' WHERE db='$1';"
fi
if [ -n "$COLUMNS_PRIV" ] || [ -n "$PROCS_PRIV" ] || [ -n "$TABLES_PRIV" ] || [ -n "$DB_PRIV" ]; then
    echo "IF YOU WANT TO RENAME the GRANTS YOU NEED TO RUN ALL OUTPUT BELOW:"
    if [ -n "$COLUMNS_PRIV" ]; then echo "$COLUMNS_PRIV"; fi
    if [ -n "$PROCS_PRIV" ]; then echo "$PROCS_PRIV"; fi
    if [ -n "$TABLES_PRIV" ]; then echo "$TABLES_PRIV"; fi
    if [ -n "$DB_PRIV" ]; then echo "$DB_PRIV"; fi
    echo "    flush privileges;"
fi

We use the following script to backup the OpenMRS database:

backup-openmrs

#!/bin/bash
mysqldump --defaults-extra-file=.my.cnf --add-drop-database --extended-insert \
    --single-transaction openmrs > openmrs.sql

Then we create the following scripts:

drop-openmrs

#!/bin/bash
mysql --defaults-extra-file=.my.cnf -e "drop database openmrs"

restore-openmrs1

#!/bin/bash
mysql --defaults-extra-file=.my.cnf \
    -e "create database openmrs1 DEFAULT CHARACTER SET utf8"
mysql --defaults-extra-file=.my.cnf openmrs1 < openmrs.sql

replace-openmrs

#!/bin/bash
./drop-openmrs
./rename_db openmrs1 openmrs

Now, when we are preparing to reset the openmrs database, we can execute the restore-openmrs1 script, which will place a fresh copy of the default OpenMRS data into the openmrs1 database (restoring the database can take several seconds, but since we are doing this into openmrs1 and the demo is using openmrs, we can perform this step before involving the demo site). Then, when we are ready to reset the database, simply executing replace-openmrs will reset the openmrs database in 2-3 seconds or less. Given that the demo data is a non-production system, we could even perform this data replacement without restarting the demo application.

Now, instead of taking 30-90 seconds or more, the OpenMRS Demo can be reset in 2-3 seconds or less.

Thank you Akshay Suryawanshi and the folks at Percona for sharing!

Fun with HTTP

January 9, 2014

Playing around with cURL and learning about some handy online tools…

requestbin

 

 

Visit requestb.in and you’ve instantly got a link against which you can post data and see the results.

When posting a text file with curl, I usually remember the -H “Content-type: text/plain”, but I try -d @filename.txt and get frustrated before I rediscover –data-binary @filename.txt.

CLBIN

clbin.com is a nifty tool for directing command line output straight to a short url.

IRCCloud Last Read Bookmarklet

January 3, 2014

IRCCloud Last Read Bookmarklet

If you are like me and participate in several IRC channels but don’t get to be in them all the time and sometime are away from them for several days, then IRCCloud is the perfect solution (a web-based IRC client that rivals any other and awesome mobile clients).  While IRCCloud makes it very easy to load a chunk of history (simply scrolling up or clicking on a bar), loading the history of a busy IRC channel for several days or weeks that can span multiple chunks of history is cumbersome.  I’ve bugged James a few times to ask for a click-once-to-load-all-history-back-to-last-read-message link.  When I recently suggested it again in the #feedback channel, I got some javascript tips from James that allowed me to do the next best thing: make a bookmarklet. Simply drag this link to your bookmark bar:

When you are using IRCCloud and enter a channel that you haven’t visited in a while (your last read message is several hours or days ago), click the bookmarklet and it will load history until it reaches your last read message.  In the rare case that you’ve been away for weeks or months and it takes more than a minute to load all the history you’ve missed, the bookmarklet will stop after a minute; just click it again to keep loading.

OpenMRS SDK

October 24, 2013

I’m very excited about the OpenMRS SDK and what it can mean for the OpenMRS Developer Community.

Chris Niesel (h3llborn) brought this beautiful creature to life during Google Summer of Code 2013 under the mentorship of Rafał Korytkowski.  Strong work, guys!  Looking forward to watching the SDK grow into both the first thing a new OpenMRS Developer touches and an invaluable tool for daily development of our most experienced developers!

Open Source: Focus on the Source, not the Code

September 19, 2013

I Am The SourceAs I’ve mentioned previously, if there’s one thing I’ve learned working with OpenMRS, it’s that the value is not in the code; it’s in the people.

In a recent branding discussion at Regenstrief, someone suggested we adopt the tag line
“The Source for Biomedical Informatics.”
I didn’t care for the tag line at all for a few reasons: (1) it’s imperious, (2) it focuses on the code, and (3) it’s seems counter to our mission to be a leader in open collaboration.  Later on in the discussion, the tag line was placed on a t-shirt and changed to “I am the source for biomedical informatics.”  While we ended up going with a different tag line for Regenstrief, I found the t-shirt provocative.  First of all, I liked the twist of saying “I am the source for biomedical informatics,” since it was less high & mighty;  but more importantly, those words caught me: “I am the source.”

“I am the source.”

There’s something hidden in that phrase.

Aha!  That was what I had been missing and it was there – right in front of me – the whole time.  All of this time that I’ve been struggling with the term “Open Source” focusing too much on the code, I didn’t realize that the “Source” wasn’t referring to the code at all; rather, the person creating the code.  Code is code.  The “Source” is the developer.

Code is code.
The “Source” is
the developer.

Now I look at the term “Open Source” differently.  No longer do I think “Open Source (Code).”  Now, I think “Open (Person who creates code).”  When I see the term “Source Code,” I no longer think “code that is the source for the application;” I think “code from a source – i.e., code from a developer.” This solves two problems for me.  First, it helps put the emphasis on getting people to behave openly  – obviously, an open coder is someone who is willing to share her code.  Secondly, it underscores that the real value is in the source – the people – and not the code.  Code can be thrown away, refactored, and easily replaced.  People cannot.

Love your coders.  Make them behave openly.

They are the source.

Open Behavior

Too often in open source, people focus on the code instead of being open. In a recent discussion with Paul, trying to clarify our thoughts on “open source” and realized that we now see open source as more of an approach than about simply sharing code. When we chose the topic for an “open-focused” Regenstrief conference, we chose “Open methodologies.”  That term is closer to what we value.

When we reviewed our values, they included:

These ideas lead to some thoughts about which behaviors or consistent with these values vs. behaviors that work against them:

 

Open Behaviors

  • Going out of your way to be transparent – e.g., when someone sends an e-mail that belongs on a community mailing list to the mailing list, replying to the list.
  • Committing code first, making it “perfect” later.
  • Thinking long-term by planning for and investing the additional (often nine times) effort needed to collaborate & re-use code.
Closed Behaviors

  • “Reply All” to an off-list e-mail that belongs on a community mailing list (instead of replying on list).
  • “My code isn’t ready to be shared yet.”
  • The engineer’s creed: “I can do it better.”

What would happen if all of your cells became self-aware?

June 6, 2013

What would happen if all of your cells became self-aware? Would each movement of your arms be made by committee? Would you fall into an anarchic pool of slush on the floor? Would there be a funeral for every skin & intestinal cell that perished… and, if so, which cells would attend? Would your kidneys go on strike? Would your liver invade your gallbladder?

Now think of yourself as a cell and the entire human race as the organism.

What is your role in the body human?