diff --git a/api.go b/api.go
index f6959b2f1e23c7610d4e5b1cb96d28845c736da2..1a43d779227b8c694af4447d4aa5e82f438dc4fa 100644
--- a/api.go
+++ b/api.go
@@ -3,7 +3,6 @@ package radioai
 import (
 	"bytes"
 	"encoding/json"
-	"errors"
 	"github.com/coreos/go-etcd/etcd"
 	"strings"
 )
@@ -17,15 +16,19 @@ type RadioAPI struct {
 	client *etcd.Client
 }
 
-// GetMount returns data on a specific mountpoint (returns an error if
-// not found).
+func NewRadioAPI(client *etcd.Client) *RadioAPI {
+	return &RadioAPI{client}
+}
+
+// GetMount returns data on a specific mountpoint (returns nil if not
+// found).
 func (r *RadioAPI) GetMount(mountName string) (*Mount, error) {
 	response, err := r.client.Get(mountPath(mountName))
 	if err != nil {
 		return nil, err
 	}
 	if len(response) != 1 {
-		return nil, errors.New("not found")
+		return nil, nil
 	}
 
 	var m Mount
diff --git a/cmd/radioctl/radioctl.go b/cmd/radioctl/radioctl.go
new file mode 100644
index 0000000000000000000000000000000000000000..a7eb83a093bcb68ccd754bef85cf38a81f1e5910
--- /dev/null
+++ b/cmd/radioctl/radioctl.go
@@ -0,0 +1,155 @@
+package main
+
+import (
+	"crypto/rand"
+	"encoding/base64"
+	"flag"
+	"fmt"
+	"hash/crc32"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"git.autistici.org/ale/radioai"
+)
+
+// Describes the syntax of a command.
+type Command struct {
+	Name     string
+	UsageStr string
+	Help     string
+	Fn       func([]string)
+	MinArgs  int
+	MaxArgs  int
+}
+
+func (c Command) PrintHelp() {
+	fmt.Fprintf(os.Stderr, "%s %s\n  %s\n\n", c.Name, c.UsageStr, c.Help)
+}
+
+func (c Command) Run(args []string) {
+	c.Fn(args)
+}
+
+// List of all known commands.
+type CommandList []Command
+
+func (cl CommandList) PrintHelp() {
+	fmt.Fprintf(os.Stderr, "Usage: %s <COMMAND> [<ARGS>...]\n", filepath.Base(os.Args[0]))
+	fmt.Fprintf(os.Stderr, "Known commands:\n\n")
+	for _, cmd := range cl {
+		cmd.PrintHelp()
+	}
+}
+
+func Run(cl CommandList) {
+	flag.Parse()
+
+	args := flag.Args()
+	if len(args) < 1 {
+		cl.PrintHelp()
+		return
+	}
+
+	cmdArg := args[0]
+	args = args[1:]
+	for _, cmd := range cl {
+		if cmd.Name == cmdArg {
+			if (cmd.MinArgs > 0 && len(args) < cmd.MinArgs) || (cmd.MaxArgs >= 0 && cmd.MaxArgs >= cmd.MinArgs && len(args) > cmd.MaxArgs) {
+				fmt.Fprintf(os.Stderr, "Wrong number of arguments!\n")
+				cmd.PrintHelp()
+			} else {
+				cmd.Run(args)
+			}
+			return
+		}
+	}
+	log.Fatalf("Unknown command '%s'", cmdArg)
+}
+
+func getClient() *radioai.RadioAPI {
+	etc := radioai.NewEtcdClient()
+	return radioai.NewRadioAPI(etc)
+}
+
+func deleteMount(args []string) {
+	if err := getClient().DelMount(args[0]); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func listMounts(args []string) {
+	mounts, err := getClient().ListMounts()
+	if err != nil {
+		log.Fatal(err)
+	}
+	for _, m := range mounts {
+		fmt.Printf("%s\n", m.Name)
+	}
+}
+
+func showMount(args []string) {
+	mount, err := getClient().GetMount(args[0])
+	if err != nil {
+		log.Fatal(err)
+	}
+	if mount == nil {
+		log.Fatal("Mount not found")
+	}
+	fmt.Printf("%+v\n", mount)
+}
+
+func generateUsername(path string) string {
+	return fmt.Sprintf("source%d", crc32.ChecksumIEEE([]byte(path)))
+}
+
+func generatePassword() string {
+	b := make([]byte, 6)
+	rand.Read(b)
+	return base64.StdEncoding.EncodeToString(b)
+}
+
+func createMount(args []string) {
+	path := args[0]
+	if !strings.HasPrefix(path, "/") {
+		log.Fatal("Mount points should specify a full path")
+	}
+
+	// Check if the mount already exists.
+	client := getClient()
+	oldm, err := client.GetMount(path)
+	if err != nil {
+		log.Fatal(err)
+	}
+	if oldm != nil {
+		log.Fatal("A mount with that name already exists!")
+	}
+
+	// Create the new mount, randomly generate source authentication.
+	username := generateUsername(path)
+	password := generatePassword()
+	m := &radioai.Mount{
+		Name:     path,
+		Username: username,
+		Password: password,
+	}
+
+	if err := client.SetMount(m); err != nil {
+		log.Fatal(err)
+	}
+
+	fmt.Printf("%+v\n", m)
+}
+
+var commands = CommandList{
+	{"create-mount", "<path>", "Create a new mountpoint", createMount, 1, 1},
+	{"delete-mount", "<path>", "Delete a mountpoint", deleteMount, 1, 1},
+	{"list-mounts", "", "List all known mountpoints", listMounts, 0, 0},
+	{"show-mount", "<path>", "Show configuration of a mount", showMount, 1, 1},
+}
+
+func main() {
+	log.SetFlags(0)
+	Run(commands)
+}