Merge lp:~vds/charms/precise/mongodb/mongodb_persistent_volume_support into lp:charms/mongodb

Proposed by Vincenzo Di Somma
Status: Merged
Merged at revision: 33
Proposed branch: lp:~vds/charms/precise/mongodb/mongodb_persistent_volume_support
Merge into: lp:charms/mongodb
Diff against target: 554 lines (+485/-3)
4 files modified
README.md (+5/-0)
config.yaml (+29/-0)
hooks/hooks.py (+231/-3)
scripts/volume-common.sh (+220/-0)
To merge this branch: bzr merge lp:~vds/charms/precise/mongodb/mongodb_persistent_volume_support
Reviewer Review Type Date Requested Status
Juan L. Negron (community) Approve
Review via email: mp+186805@code.launchpad.net

Description of the change

Shamelessly copy the permanent volume support from the postgresql charm.
In order to test it, deploy the charm, attach a permanent volume to the unit VM, change the configuration like in the following line, but using the correct value for volume-dev-regexp and the right volid in volume-map.

juju set mongodb volume-dev-regexp="/dev/vdc" volume-map='{"mongodb/0": "volid-000000000"}' volume-ephemeral-storage=false

To post a comment you must log in.
Revision history for this message
Juan L. Negron (negronjl) wrote :

Approved. Nice work

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'README.md'
--- README.md 2013-02-14 16:21:46 +0000
+++ README.md 2013-09-20 14:13:30 +0000
@@ -327,6 +327,11 @@
327- run sh.status()327- run sh.status()
328You should see your the hosts for your shards in the status output.328You should see your the hosts for your shards in the status output.
329329
330To deploy mongodb using permanent volume on Openstack, the permanent volume should be attached to the mongodb unit just after the deployment, then the configuration should be updated like follows.
331
332### Use a permanent Openstack volume to store mongodb data.
333 juju set mongodb volume-dev-regexp="/dev/vdc" volume-map='{"mongodb/0": "vol-id-00000000000000"}' volume-ephemeral-storage=false
334
330## Backups335## Backups
331336
332Backups can be enabled via config. Note that destroying the service cannot337Backups can be enabled via config. Note that destroying the service cannot
333338
=== modified file 'config.yaml'
--- config.yaml 2013-02-14 16:21:46 +0000
+++ config.yaml 2013-09-20 14:13:30 +0000
@@ -155,3 +155,32 @@
155 default: 7155 default: 7
156 type: int156 type: int
157 description: Number of backups to keep. Keeps one week's worth by default.157 description: Number of backups to keep. Keeps one week's worth by default.
158 #------------------------------------------------------------------------
159 # Volume management
160 # volume-map, volume-dev_regexp are only used
161 # if volume-ephemeral-storage == False
162 #------------------------------------------------------------------------
163 volume-ephemeral-storage:
164 type: boolean
165 default: true
166 description: >
167 If false, a configure-error state will be raised if
168 volume-map[$JUJU_UNIT_NAME] is not set (see "volume-map"
169 below) - see "volume-map" below.
170 If true, service units won't try to use "volume-map" (and
171 related variables) to mount and use external (EBS) volumes,
172 thus storage lifetime will equal VM, thus ephemeral.
173 YOU'VE BEEN WARNED.
174 volume-map:
175 type: string
176 default: ""
177 description: >
178 YAML map as e.g. "{ mongodb/0: vol-0000010, mongodb/1: vol-0000016 }".
179 Service units will raise a "configure-error" condition if no volume-map
180 value is set for it - it's expected a human to set it properly to resolve it.
181 volume-dev-regexp:
182 type: string
183 default: "/dev/vd[b-z]"
184 description: >
185 Block device for attached volumes as seen by the VM, will be "scanned"
186 for an unused device when "volume-map" is valid for the unit.
158187
=== modified file 'hooks/hooks.py'
--- hooks/hooks.py 2013-02-14 16:30:30 +0000
+++ hooks/hooks.py 2013-09-20 14:13:30 +0000
@@ -5,19 +5,22 @@
5@author: negronjl5@author: negronjl
6'''6'''
77
8import commands
9import json
8import os10import os
9import sys
10import json
11import subprocess
12import re11import re
13import signal12import signal
14import socket13import socket
14import subprocess
15import sys
15import time16import time
17import yaml
1618
17from os import chmod19from os import chmod
18from os import remove20from os import remove
19from os.path import exists21from os.path import exists
20from string import Template22from string import Template
23from yaml.constructor import ConstructorError
2124
22###############################################################################25###############################################################################
23# Supporting functions26# Supporting functions
@@ -898,6 +901,34 @@
898 print "config_data: ", config_data901 print "config_data: ", config_data
899 mongodb_config = open(default_mongodb_config).read()902 mongodb_config = open(default_mongodb_config).read()
900903
904 # Trigger volume initialization logic for permanent storage
905 volid = volume_get_volume_id()
906 if not volid:
907 ## Invalid configuration (whether ephemeral, or permanent)
908 stop_hook()
909 mounts = volume_get_all_mounted()
910 if mounts:
911 juju_log("current mounted volumes: {}".format(mounts))
912 juju_log(
913 "Disabled and stopped mongodb service, "
914 "because of broken volume configuration - check "
915 "'volume-ephemeral-storage' and 'volume-map'")
916 sys.exit(1)
917 if volume_is_permanent(volid):
918 ## config_changed_volume_apply will stop the service if it founds
919 ## it necessary, ie: new volume setup
920 if config_changed_volume_apply():
921 start_hook()
922 else:
923 stop_hook()
924 mounts = volume_get_all_mounted()
925 if mounts:
926 juju_log("current mounted volumes: {}".format(mounts))
927 juju_log(
928 "Disabled and stopped mongodb service "
929 "(config_changed_volume_apply failure)")
930 sys.exit(1)
931
901 # current ports932 # current ports
902 current_mongodb_port = re.search('^#*port\s+=\s+(\w+)',933 current_mongodb_port = re.search('^#*port\s+=\s+(\w+)',
903 mongodb_config,934 mongodb_config,
@@ -1007,6 +1038,7 @@
1007 try:1038 try:
1008 retVal = service('mongodb', 'stop')1039 retVal = service('mongodb', 'stop')
1009 os.remove('/var/lib/mongodb/mongod.lock')1040 os.remove('/var/lib/mongodb/mongod.lock')
1041 #FIXME Need to check if this is still needed
1010 except Exception, e:1042 except Exception, e:
1011 juju_log(str(e))1043 juju_log(str(e))
1012 retVal = False1044 retVal = False
@@ -1194,6 +1226,202 @@
1194# return(update_file(default_mongos_list, '\n'.join(config_servers)))1226# return(update_file(default_mongos_list, '\n'.join(config_servers)))
1195 return(True)1227 return(True)
11961228
1229
1230def run(command, exit_on_error=True):
1231 '''Run a command and return the output.'''
1232 try:
1233 juju_log(command)
1234 return subprocess.check_output(
1235 command, stderr=subprocess.STDOUT, shell=True)
1236 except subprocess.CalledProcessError, e:
1237 juju_log("status=%d, output=%s" % (e.returncode, e.output))
1238 if exit_on_error:
1239 sys.exit(e.returncode)
1240 else:
1241 raise
1242
1243
1244###############################################################################
1245# Volume managment
1246###############################################################################
1247#------------------------------
1248# Get volume-id from juju config "volume-map" dictionary as
1249# volume-map[JUJU_UNIT_NAME]
1250# @return volid
1251#
1252#------------------------------
1253def volume_get_volid_from_volume_map():
1254 config_data = config_get()
1255 volume_map = {}
1256 try:
1257 volume_map = yaml.load(config_data['volume-map'].strip())
1258 if volume_map:
1259 juju_unit_name = os.environ['JUJU_UNIT_NAME']
1260 volid = volume_map.get(juju_unit_name)
1261 juju_log("Juju unit name: %s Volid:%s" % (juju_unit_name, volid))
1262 return volid
1263 except ConstructorError as e:
1264 juju_log("invalid YAML in 'volume-map': {}".format(e))
1265 return None
1266
1267
1268# Is this volume_id permanent ?
1269# @returns True if volid set and not --ephemeral, else:
1270# False
1271def volume_is_permanent(volid):
1272 if volid and volid != "--ephemeral":
1273 return True
1274 return False
1275
1276
1277#------------------------------
1278# Returns a mount point from passed vol-id, e.g. /srv/juju/vol-000012345
1279#
1280# @param volid volume id (as e.g. EBS volid)
1281# @return mntpoint_path eg /srv/juju/vol-000012345
1282#------------------------------
1283def volume_mount_point_from_volid(volid):
1284 if volid and volume_is_permanent(volid):
1285 return "/srv/juju/%s" % volid
1286 return None
1287
1288
1289# Do we have a valid storage state?
1290# @returns volid
1291# None config state is invalid - we should not serve
1292def volume_get_volume_id():
1293 config_data = config_get()
1294 ephemeral_storage = config_data['volume-ephemeral-storage']
1295 volid = volume_get_volid_from_volume_map()
1296 juju_unit_name = os.environ['JUJU_UNIT_NAME']
1297 if ephemeral_storage in [True, 'yes', 'Yes', 'true', 'True']:
1298 if volid:
1299 juju_log(
1300 "volume-ephemeral-storage is True, but " +
1301 "volume-map[{!r}] -> {}".format(juju_unit_name, volid))
1302 return None
1303 else:
1304 return "--ephemeral"
1305 else:
1306 if not volid:
1307 juju_log(
1308 "volume-ephemeral-storage is False, but "
1309 "no volid found for volume-map[{!r}]".format(
1310 juju_unit_name))
1311 return None
1312 juju_log("Volid:%s" % (volid))
1313 return volid
1314
1315
1316# Initialize and/or mount permanent storage, it straightly calls
1317# shell helper
1318def volume_init_and_mount(volid):
1319 juju_log("Initialize and mount volume")
1320 command = ("scripts/volume-common.sh call " +
1321 "volume_init_and_mount %s" % volid)
1322 run(command)
1323 return True
1324
1325
1326def volume_get_all_mounted():
1327 command = ("mount |egrep /srv/juju")
1328 status, output = commands.getstatusoutput(command)
1329 if status != 0:
1330 return None
1331 return output
1332
1333#------------------------------------------------------------------------------
1334# Core logic for permanent storage changes:
1335# NOTE the only 2 "True" return points:
1336# 1) symlink already pointing to existing storage (no-op)
1337# 2) new storage properly initialized:
1338# - volume: initialized if not already (fdisk, mkfs),
1339# mounts it to e.g.: /srv/juju/vol-000012345
1340# - if fresh new storage dir: rsync existing data
1341# - manipulate /var/lib/mongodb/VERSION/CLUSTER symlink
1342#------------------------------------------------------------------------------
1343def config_changed_volume_apply():
1344 config_data = config_get()
1345 data_directory_path = config_data["dbpath"]
1346 assert(data_directory_path)
1347 volid = volume_get_volume_id()
1348 if volid:
1349 if volume_is_permanent(volid):
1350 if not volume_init_and_mount(volid):
1351 juju_log(
1352 "volume_init_and_mount failed, not applying changes")
1353 return False
1354
1355 if not os.path.exists(data_directory_path):
1356 juju_log(
1357 "mongodb data dir {} not found, "
1358 "not applying changes.".format(data_directory_path))
1359 return False
1360
1361 mount_point = volume_mount_point_from_volid(volid)
1362 new_mongo_dir = os.path.join(mount_point, "mongodb")
1363 if not mount_point:
1364 juju_log(
1365 "invalid mount point from volid = {}, "
1366 "not applying changes.".format(mount_point))
1367 return False
1368
1369 if os.path.islink(data_directory_path):
1370 juju_log(
1371 "mongodb data dir '%s' already points "
1372 "to %s, skipping storage changes." % (data_directory_path, new_mongo_dir))
1373 juju_log(
1374 "existing-symlink: to fix/avoid UID changes from "
1375 "previous units, doing: "
1376 "chown -R mongodb:mongodb {}".format(new_mongo_dir))
1377 run("chown -R mongodb:mongodb %s" % new_mongo_dir)
1378 return True
1379
1380 # Create a directory structure below "new" mount_point
1381 curr_dir_stat = os.stat(data_directory_path)
1382 if not os.path.isdir(new_mongo_dir):
1383 juju_log("mkdir %s" % new_mongo_dir)
1384 os.mkdir(new_mongo_dir)
1385 # copy permissions from current data_directory_path
1386 os.chown(new_mongo_dir, curr_dir_stat.st_uid, curr_dir_stat.st_gid)
1387 os.chmod(new_mongo_dir, curr_dir_stat.st_mode)
1388 # Carefully build this symlink, e.g.:
1389 # /var/lib/mongodb ->
1390 # /srv/juju/vol-000012345/mongodb
1391 # but keep previous "main/" directory, by renaming it to
1392 # main-$TIMESTAMP
1393 if not stop_hook():
1394 juju_log("stop_hook() failed - can't migrate data.")
1395 return False
1396 if not os.path.exists(new_mongo_dir):
1397 juju_log("migrating mongo data {}/ -> {}/".format(
1398 data_directory_path, new_mongo_dir))
1399 # void copying PID file to perm storage (shouldn't be any...)
1400 command = "rsync -a {}/ {}/".format(
1401 data_directory_path, new_mongo_dir)
1402 juju_log("run: {}".format(command))
1403 run(command)
1404 try:
1405 os.rename(data_directory_path, "{}-{}".format(
1406 data_directory_path, int(time.time())))
1407 juju_log("NOTICE: symlinking {} -> {}".format(
1408 new_mongo_dir, data_directory_path))
1409 os.symlink(new_mongo_dir, data_directory_path)
1410 juju_log(
1411 "after-symlink: to fix/avoid UID changes from "
1412 "previous units, doing: "
1413 "chown -R mongodb:mongodb {}".format(new_mongo_dir))
1414 run("chown -R mongodb:mongodb {}".format(new_mongo_dir))
1415 return True
1416 except OSError:
1417 juju_log("failed to symlink {} -> {}".format(
1418 data_directory_path, mount_point))
1419 return False
1420 else:
1421 juju_log(
1422 "Invalid volume storage configuration, not applying changes")
1423 return False
1424
1197###############################################################################1425###############################################################################
1198# Main section1426# Main section
1199###############################################################################1427###############################################################################
12001428
=== added directory 'scripts'
=== added file 'scripts/volume-common.sh'
--- scripts/volume-common.sh 1970-01-01 00:00:00 +0000
+++ scripts/volume-common.sh 2013-09-20 14:13:30 +0000
@@ -0,0 +1,220 @@
1#!/bin/bash
2# Author: JuanJo Ciarlante <jjo@canonical.com>
3# Copyright: Canonical Ltd. 2012
4# License: GPLv2
5#
6# juju storage common shell library
7#
8
9#------------------------------
10# Returns a mount point from passed vol-id, e.g. /srv/juju/vol-000012345
11#
12# @param $1 volume id
13# @echoes mntpoint-path eg /srv/juju/vol-000012345
14#------------------------------
15_mntpoint_from_volid() {
16 local volid=${1?missing volid}
17 [[ ${volid} != "" ]] && echo /srv/juju/${volid} || echo ""
18}
19
20
21#------------------------------
22# Assert that passed mount points hold different filesystems
23#
24# @param $1 mntpoint1
25# @param $2 mntpoint2
26# @return 0 different FS
27# 1 same FS
28#------------------------------
29_assert_diff_fs() {
30 local mnt1="${1:?missing mntpoint1}"
31 local mnt2="${2:?missing mntpoint2}"
32 local fsid1 fsid2
33 fsid1=$(stat --file-system -c '%i' "${mnt1}" 2>/dev/null)
34 fsid2=$(stat --file-system -c '%i' "${mnt2}" 2>/dev/null)
35 [[ ${fsid1} != ${fsid2} ]]
36 return $?
37}
38
39#------------------------------
40# Initialize volume (sfdisk, mkfs.ext4) IFF NOT already, mount it at
41# /srv/juju/<volume-id>
42#
43# @param $1 volume-id, can be any arbitrary string, better if
44# equal to EC2/OS vol-id name (just for consistency)
45# @return 0 success
46# 1 nil volid/etc
47# 2 error while handling the device (non-block device, sfdisk error, etc)
48#------------------------------
49volume_init_and_mount() {
50 ## Find 1st unused device (reverse sort /dev/vdX)
51 local volid=${1:?missing volid}
52 local dev_regexp
53 local dev found_dev=
54 local label="${volid}"
55 local func=${FUNCNAME[0]}
56 dev_regexp=$(config-get volume-dev-regexp) || return 1
57 mntpoint=$(_mntpoint_from_volid ${volid})
58
59 [[ -z ${mntpoint} ]] && return 1
60 if mount | egrep -qw "${mntpoint}";then
61 _assert_diff_fs "/" "${mntpoint}" || {
62 juju-log "ERROR: returning from ${func} with '${mntpoint}' still at '/' filesystem"
63 return 1
64 }
65 juju-log "NOTICE: mntpoint=${mntpoint} already mounted, skipping volume_init_and_mount"
66 return 0
67 fi
68
69 # Sanitize
70 case "${dev_regexp?}" in
71 # Careful: this is glob matching against an regexp -
72 # quite narrowed
73 /dev/*|/dev/disk/by-*)
74 ;; ## Ok
75 *)
76 juju-log "ERROR: invalid 'volume-dev-regexp' specified"
77 return 1
78 ;;
79 esac
80
81 # Assume udev will create only existing devices
82 for dev in $(ls -rd1 /dev/* | egrep "${dev_regexp}" | egrep -v "[1-9]$" 2>/dev/null);do
83 ## Check it's not already mounted
84 mount | egrep -q "${dev}[1-9]?" || { found_dev=${dev}; break;}
85 done
86 [[ -n "${found_dev}" ]] || {
87 juju-log "ERROR: ${func}: coult not find an unused device for regexp: ${dev_regexp}"
88 return 1
89 }
90 partition1_dev=${found_dev}1
91
92 juju-log "INFO: ${func}: found_dev=${found_dev}"
93 [[ -b ${found_dev?} ]] || {
94 juju-log "ERROR: ${func}: ${found_dev} is not a blockdevice"
95 return 2
96 }
97
98 # Run next set of "dangerous" commands as 'set -e', in a subshell
99 (
100 set -e
101 # Re-read partition - will fail if already in use
102 blockdev --rereadpt ${found_dev}
103
104 # IFF not present, create partition with full disk
105 if [[ -b ${partition1_dev?} ]];then
106 juju-log "INFO: ${func}: ${partition1_dev} already present - skipping sfdisk."
107 else
108 juju-log "NOTICE: ${func}: ${partition1_dev} not present at ${found_dev}, running: sfdisk ${found_dev} ..."
109 # Format partition1_dev as max sized
110 echo ",+," | sfdisk ${found_dev}
111 fi
112
113 # Create an ext4 filesystem if NOT already present
114 # use e.g. LABEl=vol-000012345
115 if file -s ${partition1_dev} | egrep -q ext4 ; then
116 juju-log "INFO: ${func}: ${partition1_dev} already formatted as ext4 - skipping mkfs.ext4."
117 ## Check e2label - log if it has changed (e.g. already used / initialized with a diff label)
118 local curr_label=$(e2label "${partition1_dev}")
119 if [[ ${curr_label} != ${label} ]]; then
120 juju-log "WARNING: ${func}: ${partition1_dev} had label=${curr_label}, overwritting with label=${label}"
121 e2label ${partition1_dev} "${label}"
122 fi
123 else
124 juju-log "NOTICE: ${func}: running: mkfs.ext4 -L ${label} ${partition1_dev}"
125 mkfs.ext4 -L "${label}" ${partition1_dev}
126 fi
127
128 # Mount it at e.g. /srv/juju/vol-000012345
129 [[ -d "${mntpoint}" ]] || mkdir -p "${mntpoint}"
130 mount | fgrep -wq "${partition1_dev}" || {
131 local files_below_mntpoint="$(ls -d "${mntpoint}"/* 2>/dev/null |wc -l )"
132 if [[ ${files_below_mntpoint} -ne 0 ]]; then
133 juju-log "ERROR: *not* doing 'mount "${partition1_dev}" "${mntpoint}"' because there are already ${files_below_mntpoint} files/dirs beneath '${mntpoint}'"
134 exit 1
135 fi
136 ## should always fsck before mounting (e.g. fsck after max time (-i) / max mounts (-c) )
137 fsck "${partition1_dev}"
138 mount "${partition1_dev}" "${mntpoint}"
139 juju-log "INFO: ${func}: mounted as: '$(mount | fgrep -w ${partition1_dev})'"
140 }
141
142 # Add it to fstab is not already there
143 fgrep -wq "LABEL=${label}" /etc/fstab || {
144 echo "LABEL=${label} ${mntpoint} ext4 defaults,nobootwait,comment=${volid}" | tee -a /etc/fstab
145 juju-log "INFO: ${func}: LABEL=${label} added to /etc/fstab"
146 }
147 )
148 # Final assertion: mounted filesystem id is different from '/' (effectively mounted)
149 _assert_diff_fs "/" "${mntpoint}" || {
150 juju-log "ERROR: returning from ${func} with '${mntpoint}' still at '/' filesystem (couldn't mount new volume)"
151 ## try to rmdir mntpoint directory - should not be 'mistakenly' used
152 rmdir ${mntpoint}
153 return 1
154 }
155 return $?
156}
157
158#------------------------------
159# Get volume-id from juju config "volume-map" dictionary as
160# volume-map[JUJU_UNIT_NAME]
161# @return 0 if volume-map value found ( does echo volid or ""), else:
162# 1 if not found or None
163#
164#------------------------------
165volume_get_volid_from_volume_map() {
166 local volid=$(config-get "volume-map"|python -c$'import sys;import os;from yaml import load;from itertools import chain; volume_map = load(sys.stdin)\nif volume_map: print volume_map.get(os.environ["JUJU_UNIT_NAME"])')
167 [[ $volid == None ]] && return 1
168 echo "$volid"
169}
170
171# Returns true if permanent storage (considers --ephemeral)
172# @returns 0 if volid set and not --ephemeral, else:
173# 1
174volume_is_permanent() {
175 local volid=${1:?missing volid}
176 [[ -n ${volid} && ${volid} != --ephemeral ]] && return 0 || return 1
177}
178volume_mount_point_from_volid(){
179 local volid=${1:?missing volid}
180 if volume_is_permanent;then
181 echo "/srv/juju/${volid}"
182 return 0
183 else
184 return 1
185 fi
186}
187# Do we have a valid storage state?
188# @returns 0 does echo $volid (can be "--ephemeral")
189# 1 config state is invalid - we should not serve
190volume_get_volume_id() {
191 local ephemeral_storage
192 local volid
193 ephemeral_storage=$(config-get volume-ephemeral-storage) || return 1
194 volid=$(volume_get_volid_from_volume_map) || return 1
195 if [[ $ephemeral_storage == True ]];then
196 # Ephemeral -> should not have a valid volid
197 if [[ $volid != "" ]];then
198 juju-log "ERROR: volume-ephemeral-storage is True, but $JUJU_UNIT_NAME maps to volid=${volid}"
199 return 1
200 fi
201 else
202 # Durable (not ephemeral) -> must have a valid volid for this unit
203 if [[ $volid == "" ]];then
204 juju-log "ERROR: volume-ephemeral-storage is False, but no volid found for: $JUJU_UNIT_NAME"
205 return 1
206 fi
207 fi
208 echo "$volid"
209 return 0
210}
211
212case "$1" in
213 ## allow non SHELL scripts to call helper functions
214 call)
215 : ${JUJU_UNIT_NAME?} ## Must be called in juju environment
216 shift;
217 function="${1:?usage: ${0##*/} call function arg1 arg2 ...}"
218 shift;
219 ${function} "$@" && exit 0 || exit 1
220esac

Subscribers

People subscribed via source and target branches