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

Proposed by Vincenzo Di Somma on 2013-09-20
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) 2013-09-20 Approve on 2013-09-21
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.
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
1=== modified file 'README.md'
2--- README.md 2013-02-14 16:21:46 +0000
3+++ README.md 2013-09-20 14:13:30 +0000
4@@ -327,6 +327,11 @@
5 - run sh.status()
6 You should see your the hosts for your shards in the status output.
7
8+To 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.
9+
10+### Use a permanent Openstack volume to store mongodb data.
11+ juju set mongodb volume-dev-regexp="/dev/vdc" volume-map='{"mongodb/0": "vol-id-00000000000000"}' volume-ephemeral-storage=false
12+
13 ## Backups
14
15 Backups can be enabled via config. Note that destroying the service cannot
16
17=== modified file 'config.yaml'
18--- config.yaml 2013-02-14 16:21:46 +0000
19+++ config.yaml 2013-09-20 14:13:30 +0000
20@@ -155,3 +155,32 @@
21 default: 7
22 type: int
23 description: Number of backups to keep. Keeps one week's worth by default.
24+ #------------------------------------------------------------------------
25+ # Volume management
26+ # volume-map, volume-dev_regexp are only used
27+ # if volume-ephemeral-storage == False
28+ #------------------------------------------------------------------------
29+ volume-ephemeral-storage:
30+ type: boolean
31+ default: true
32+ description: >
33+ If false, a configure-error state will be raised if
34+ volume-map[$JUJU_UNIT_NAME] is not set (see "volume-map"
35+ below) - see "volume-map" below.
36+ If true, service units won't try to use "volume-map" (and
37+ related variables) to mount and use external (EBS) volumes,
38+ thus storage lifetime will equal VM, thus ephemeral.
39+ YOU'VE BEEN WARNED.
40+ volume-map:
41+ type: string
42+ default: ""
43+ description: >
44+ YAML map as e.g. "{ mongodb/0: vol-0000010, mongodb/1: vol-0000016 }".
45+ Service units will raise a "configure-error" condition if no volume-map
46+ value is set for it - it's expected a human to set it properly to resolve it.
47+ volume-dev-regexp:
48+ type: string
49+ default: "/dev/vd[b-z]"
50+ description: >
51+ Block device for attached volumes as seen by the VM, will be "scanned"
52+ for an unused device when "volume-map" is valid for the unit.
53
54=== modified file 'hooks/hooks.py'
55--- hooks/hooks.py 2013-02-14 16:30:30 +0000
56+++ hooks/hooks.py 2013-09-20 14:13:30 +0000
57@@ -5,19 +5,22 @@
58 @author: negronjl
59 '''
60
61+import commands
62+import json
63 import os
64-import sys
65-import json
66-import subprocess
67 import re
68 import signal
69 import socket
70+import subprocess
71+import sys
72 import time
73+import yaml
74
75 from os import chmod
76 from os import remove
77 from os.path import exists
78 from string import Template
79+from yaml.constructor import ConstructorError
80
81 ###############################################################################
82 # Supporting functions
83@@ -898,6 +901,34 @@
84 print "config_data: ", config_data
85 mongodb_config = open(default_mongodb_config).read()
86
87+ # Trigger volume initialization logic for permanent storage
88+ volid = volume_get_volume_id()
89+ if not volid:
90+ ## Invalid configuration (whether ephemeral, or permanent)
91+ stop_hook()
92+ mounts = volume_get_all_mounted()
93+ if mounts:
94+ juju_log("current mounted volumes: {}".format(mounts))
95+ juju_log(
96+ "Disabled and stopped mongodb service, "
97+ "because of broken volume configuration - check "
98+ "'volume-ephemeral-storage' and 'volume-map'")
99+ sys.exit(1)
100+ if volume_is_permanent(volid):
101+ ## config_changed_volume_apply will stop the service if it founds
102+ ## it necessary, ie: new volume setup
103+ if config_changed_volume_apply():
104+ start_hook()
105+ else:
106+ stop_hook()
107+ mounts = volume_get_all_mounted()
108+ if mounts:
109+ juju_log("current mounted volumes: {}".format(mounts))
110+ juju_log(
111+ "Disabled and stopped mongodb service "
112+ "(config_changed_volume_apply failure)")
113+ sys.exit(1)
114+
115 # current ports
116 current_mongodb_port = re.search('^#*port\s+=\s+(\w+)',
117 mongodb_config,
118@@ -1007,6 +1038,7 @@
119 try:
120 retVal = service('mongodb', 'stop')
121 os.remove('/var/lib/mongodb/mongod.lock')
122+ #FIXME Need to check if this is still needed
123 except Exception, e:
124 juju_log(str(e))
125 retVal = False
126@@ -1194,6 +1226,202 @@
127 # return(update_file(default_mongos_list, '\n'.join(config_servers)))
128 return(True)
129
130+
131+def run(command, exit_on_error=True):
132+ '''Run a command and return the output.'''
133+ try:
134+ juju_log(command)
135+ return subprocess.check_output(
136+ command, stderr=subprocess.STDOUT, shell=True)
137+ except subprocess.CalledProcessError, e:
138+ juju_log("status=%d, output=%s" % (e.returncode, e.output))
139+ if exit_on_error:
140+ sys.exit(e.returncode)
141+ else:
142+ raise
143+
144+
145+###############################################################################
146+# Volume managment
147+###############################################################################
148+#------------------------------
149+# Get volume-id from juju config "volume-map" dictionary as
150+# volume-map[JUJU_UNIT_NAME]
151+# @return volid
152+#
153+#------------------------------
154+def volume_get_volid_from_volume_map():
155+ config_data = config_get()
156+ volume_map = {}
157+ try:
158+ volume_map = yaml.load(config_data['volume-map'].strip())
159+ if volume_map:
160+ juju_unit_name = os.environ['JUJU_UNIT_NAME']
161+ volid = volume_map.get(juju_unit_name)
162+ juju_log("Juju unit name: %s Volid:%s" % (juju_unit_name, volid))
163+ return volid
164+ except ConstructorError as e:
165+ juju_log("invalid YAML in 'volume-map': {}".format(e))
166+ return None
167+
168+
169+# Is this volume_id permanent ?
170+# @returns True if volid set and not --ephemeral, else:
171+# False
172+def volume_is_permanent(volid):
173+ if volid and volid != "--ephemeral":
174+ return True
175+ return False
176+
177+
178+#------------------------------
179+# Returns a mount point from passed vol-id, e.g. /srv/juju/vol-000012345
180+#
181+# @param volid volume id (as e.g. EBS volid)
182+# @return mntpoint_path eg /srv/juju/vol-000012345
183+#------------------------------
184+def volume_mount_point_from_volid(volid):
185+ if volid and volume_is_permanent(volid):
186+ return "/srv/juju/%s" % volid
187+ return None
188+
189+
190+# Do we have a valid storage state?
191+# @returns volid
192+# None config state is invalid - we should not serve
193+def volume_get_volume_id():
194+ config_data = config_get()
195+ ephemeral_storage = config_data['volume-ephemeral-storage']
196+ volid = volume_get_volid_from_volume_map()
197+ juju_unit_name = os.environ['JUJU_UNIT_NAME']
198+ if ephemeral_storage in [True, 'yes', 'Yes', 'true', 'True']:
199+ if volid:
200+ juju_log(
201+ "volume-ephemeral-storage is True, but " +
202+ "volume-map[{!r}] -> {}".format(juju_unit_name, volid))
203+ return None
204+ else:
205+ return "--ephemeral"
206+ else:
207+ if not volid:
208+ juju_log(
209+ "volume-ephemeral-storage is False, but "
210+ "no volid found for volume-map[{!r}]".format(
211+ juju_unit_name))
212+ return None
213+ juju_log("Volid:%s" % (volid))
214+ return volid
215+
216+
217+# Initialize and/or mount permanent storage, it straightly calls
218+# shell helper
219+def volume_init_and_mount(volid):
220+ juju_log("Initialize and mount volume")
221+ command = ("scripts/volume-common.sh call " +
222+ "volume_init_and_mount %s" % volid)
223+ run(command)
224+ return True
225+
226+
227+def volume_get_all_mounted():
228+ command = ("mount |egrep /srv/juju")
229+ status, output = commands.getstatusoutput(command)
230+ if status != 0:
231+ return None
232+ return output
233+
234+#------------------------------------------------------------------------------
235+# Core logic for permanent storage changes:
236+# NOTE the only 2 "True" return points:
237+# 1) symlink already pointing to existing storage (no-op)
238+# 2) new storage properly initialized:
239+# - volume: initialized if not already (fdisk, mkfs),
240+# mounts it to e.g.: /srv/juju/vol-000012345
241+# - if fresh new storage dir: rsync existing data
242+# - manipulate /var/lib/mongodb/VERSION/CLUSTER symlink
243+#------------------------------------------------------------------------------
244+def config_changed_volume_apply():
245+ config_data = config_get()
246+ data_directory_path = config_data["dbpath"]
247+ assert(data_directory_path)
248+ volid = volume_get_volume_id()
249+ if volid:
250+ if volume_is_permanent(volid):
251+ if not volume_init_and_mount(volid):
252+ juju_log(
253+ "volume_init_and_mount failed, not applying changes")
254+ return False
255+
256+ if not os.path.exists(data_directory_path):
257+ juju_log(
258+ "mongodb data dir {} not found, "
259+ "not applying changes.".format(data_directory_path))
260+ return False
261+
262+ mount_point = volume_mount_point_from_volid(volid)
263+ new_mongo_dir = os.path.join(mount_point, "mongodb")
264+ if not mount_point:
265+ juju_log(
266+ "invalid mount point from volid = {}, "
267+ "not applying changes.".format(mount_point))
268+ return False
269+
270+ if os.path.islink(data_directory_path):
271+ juju_log(
272+ "mongodb data dir '%s' already points "
273+ "to %s, skipping storage changes." % (data_directory_path, new_mongo_dir))
274+ juju_log(
275+ "existing-symlink: to fix/avoid UID changes from "
276+ "previous units, doing: "
277+ "chown -R mongodb:mongodb {}".format(new_mongo_dir))
278+ run("chown -R mongodb:mongodb %s" % new_mongo_dir)
279+ return True
280+
281+ # Create a directory structure below "new" mount_point
282+ curr_dir_stat = os.stat(data_directory_path)
283+ if not os.path.isdir(new_mongo_dir):
284+ juju_log("mkdir %s" % new_mongo_dir)
285+ os.mkdir(new_mongo_dir)
286+ # copy permissions from current data_directory_path
287+ os.chown(new_mongo_dir, curr_dir_stat.st_uid, curr_dir_stat.st_gid)
288+ os.chmod(new_mongo_dir, curr_dir_stat.st_mode)
289+ # Carefully build this symlink, e.g.:
290+ # /var/lib/mongodb ->
291+ # /srv/juju/vol-000012345/mongodb
292+ # but keep previous "main/" directory, by renaming it to
293+ # main-$TIMESTAMP
294+ if not stop_hook():
295+ juju_log("stop_hook() failed - can't migrate data.")
296+ return False
297+ if not os.path.exists(new_mongo_dir):
298+ juju_log("migrating mongo data {}/ -> {}/".format(
299+ data_directory_path, new_mongo_dir))
300+ # void copying PID file to perm storage (shouldn't be any...)
301+ command = "rsync -a {}/ {}/".format(
302+ data_directory_path, new_mongo_dir)
303+ juju_log("run: {}".format(command))
304+ run(command)
305+ try:
306+ os.rename(data_directory_path, "{}-{}".format(
307+ data_directory_path, int(time.time())))
308+ juju_log("NOTICE: symlinking {} -> {}".format(
309+ new_mongo_dir, data_directory_path))
310+ os.symlink(new_mongo_dir, data_directory_path)
311+ juju_log(
312+ "after-symlink: to fix/avoid UID changes from "
313+ "previous units, doing: "
314+ "chown -R mongodb:mongodb {}".format(new_mongo_dir))
315+ run("chown -R mongodb:mongodb {}".format(new_mongo_dir))
316+ return True
317+ except OSError:
318+ juju_log("failed to symlink {} -> {}".format(
319+ data_directory_path, mount_point))
320+ return False
321+ else:
322+ juju_log(
323+ "Invalid volume storage configuration, not applying changes")
324+ return False
325+
326 ###############################################################################
327 # Main section
328 ###############################################################################
329
330=== added directory 'scripts'
331=== added file 'scripts/volume-common.sh'
332--- scripts/volume-common.sh 1970-01-01 00:00:00 +0000
333+++ scripts/volume-common.sh 2013-09-20 14:13:30 +0000
334@@ -0,0 +1,220 @@
335+#!/bin/bash
336+# Author: JuanJo Ciarlante <jjo@canonical.com>
337+# Copyright: Canonical Ltd. 2012
338+# License: GPLv2
339+#
340+# juju storage common shell library
341+#
342+
343+#------------------------------
344+# Returns a mount point from passed vol-id, e.g. /srv/juju/vol-000012345
345+#
346+# @param $1 volume id
347+# @echoes mntpoint-path eg /srv/juju/vol-000012345
348+#------------------------------
349+_mntpoint_from_volid() {
350+ local volid=${1?missing volid}
351+ [[ ${volid} != "" ]] && echo /srv/juju/${volid} || echo ""
352+}
353+
354+
355+#------------------------------
356+# Assert that passed mount points hold different filesystems
357+#
358+# @param $1 mntpoint1
359+# @param $2 mntpoint2
360+# @return 0 different FS
361+# 1 same FS
362+#------------------------------
363+_assert_diff_fs() {
364+ local mnt1="${1:?missing mntpoint1}"
365+ local mnt2="${2:?missing mntpoint2}"
366+ local fsid1 fsid2
367+ fsid1=$(stat --file-system -c '%i' "${mnt1}" 2>/dev/null)
368+ fsid2=$(stat --file-system -c '%i' "${mnt2}" 2>/dev/null)
369+ [[ ${fsid1} != ${fsid2} ]]
370+ return $?
371+}
372+
373+#------------------------------
374+# Initialize volume (sfdisk, mkfs.ext4) IFF NOT already, mount it at
375+# /srv/juju/<volume-id>
376+#
377+# @param $1 volume-id, can be any arbitrary string, better if
378+# equal to EC2/OS vol-id name (just for consistency)
379+# @return 0 success
380+# 1 nil volid/etc
381+# 2 error while handling the device (non-block device, sfdisk error, etc)
382+#------------------------------
383+volume_init_and_mount() {
384+ ## Find 1st unused device (reverse sort /dev/vdX)
385+ local volid=${1:?missing volid}
386+ local dev_regexp
387+ local dev found_dev=
388+ local label="${volid}"
389+ local func=${FUNCNAME[0]}
390+ dev_regexp=$(config-get volume-dev-regexp) || return 1
391+ mntpoint=$(_mntpoint_from_volid ${volid})
392+
393+ [[ -z ${mntpoint} ]] && return 1
394+ if mount | egrep -qw "${mntpoint}";then
395+ _assert_diff_fs "/" "${mntpoint}" || {
396+ juju-log "ERROR: returning from ${func} with '${mntpoint}' still at '/' filesystem"
397+ return 1
398+ }
399+ juju-log "NOTICE: mntpoint=${mntpoint} already mounted, skipping volume_init_and_mount"
400+ return 0
401+ fi
402+
403+ # Sanitize
404+ case "${dev_regexp?}" in
405+ # Careful: this is glob matching against an regexp -
406+ # quite narrowed
407+ /dev/*|/dev/disk/by-*)
408+ ;; ## Ok
409+ *)
410+ juju-log "ERROR: invalid 'volume-dev-regexp' specified"
411+ return 1
412+ ;;
413+ esac
414+
415+ # Assume udev will create only existing devices
416+ for dev in $(ls -rd1 /dev/* | egrep "${dev_regexp}" | egrep -v "[1-9]$" 2>/dev/null);do
417+ ## Check it's not already mounted
418+ mount | egrep -q "${dev}[1-9]?" || { found_dev=${dev}; break;}
419+ done
420+ [[ -n "${found_dev}" ]] || {
421+ juju-log "ERROR: ${func}: coult not find an unused device for regexp: ${dev_regexp}"
422+ return 1
423+ }
424+ partition1_dev=${found_dev}1
425+
426+ juju-log "INFO: ${func}: found_dev=${found_dev}"
427+ [[ -b ${found_dev?} ]] || {
428+ juju-log "ERROR: ${func}: ${found_dev} is not a blockdevice"
429+ return 2
430+ }
431+
432+ # Run next set of "dangerous" commands as 'set -e', in a subshell
433+ (
434+ set -e
435+ # Re-read partition - will fail if already in use
436+ blockdev --rereadpt ${found_dev}
437+
438+ # IFF not present, create partition with full disk
439+ if [[ -b ${partition1_dev?} ]];then
440+ juju-log "INFO: ${func}: ${partition1_dev} already present - skipping sfdisk."
441+ else
442+ juju-log "NOTICE: ${func}: ${partition1_dev} not present at ${found_dev}, running: sfdisk ${found_dev} ..."
443+ # Format partition1_dev as max sized
444+ echo ",+," | sfdisk ${found_dev}
445+ fi
446+
447+ # Create an ext4 filesystem if NOT already present
448+ # use e.g. LABEl=vol-000012345
449+ if file -s ${partition1_dev} | egrep -q ext4 ; then
450+ juju-log "INFO: ${func}: ${partition1_dev} already formatted as ext4 - skipping mkfs.ext4."
451+ ## Check e2label - log if it has changed (e.g. already used / initialized with a diff label)
452+ local curr_label=$(e2label "${partition1_dev}")
453+ if [[ ${curr_label} != ${label} ]]; then
454+ juju-log "WARNING: ${func}: ${partition1_dev} had label=${curr_label}, overwritting with label=${label}"
455+ e2label ${partition1_dev} "${label}"
456+ fi
457+ else
458+ juju-log "NOTICE: ${func}: running: mkfs.ext4 -L ${label} ${partition1_dev}"
459+ mkfs.ext4 -L "${label}" ${partition1_dev}
460+ fi
461+
462+ # Mount it at e.g. /srv/juju/vol-000012345
463+ [[ -d "${mntpoint}" ]] || mkdir -p "${mntpoint}"
464+ mount | fgrep -wq "${partition1_dev}" || {
465+ local files_below_mntpoint="$(ls -d "${mntpoint}"/* 2>/dev/null |wc -l )"
466+ if [[ ${files_below_mntpoint} -ne 0 ]]; then
467+ juju-log "ERROR: *not* doing 'mount "${partition1_dev}" "${mntpoint}"' because there are already ${files_below_mntpoint} files/dirs beneath '${mntpoint}'"
468+ exit 1
469+ fi
470+ ## should always fsck before mounting (e.g. fsck after max time (-i) / max mounts (-c) )
471+ fsck "${partition1_dev}"
472+ mount "${partition1_dev}" "${mntpoint}"
473+ juju-log "INFO: ${func}: mounted as: '$(mount | fgrep -w ${partition1_dev})'"
474+ }
475+
476+ # Add it to fstab is not already there
477+ fgrep -wq "LABEL=${label}" /etc/fstab || {
478+ echo "LABEL=${label} ${mntpoint} ext4 defaults,nobootwait,comment=${volid}" | tee -a /etc/fstab
479+ juju-log "INFO: ${func}: LABEL=${label} added to /etc/fstab"
480+ }
481+ )
482+ # Final assertion: mounted filesystem id is different from '/' (effectively mounted)
483+ _assert_diff_fs "/" "${mntpoint}" || {
484+ juju-log "ERROR: returning from ${func} with '${mntpoint}' still at '/' filesystem (couldn't mount new volume)"
485+ ## try to rmdir mntpoint directory - should not be 'mistakenly' used
486+ rmdir ${mntpoint}
487+ return 1
488+ }
489+ return $?
490+}
491+
492+#------------------------------
493+# Get volume-id from juju config "volume-map" dictionary as
494+# volume-map[JUJU_UNIT_NAME]
495+# @return 0 if volume-map value found ( does echo volid or ""), else:
496+# 1 if not found or None
497+#
498+#------------------------------
499+volume_get_volid_from_volume_map() {
500+ 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"])')
501+ [[ $volid == None ]] && return 1
502+ echo "$volid"
503+}
504+
505+# Returns true if permanent storage (considers --ephemeral)
506+# @returns 0 if volid set and not --ephemeral, else:
507+# 1
508+volume_is_permanent() {
509+ local volid=${1:?missing volid}
510+ [[ -n ${volid} && ${volid} != --ephemeral ]] && return 0 || return 1
511+}
512+volume_mount_point_from_volid(){
513+ local volid=${1:?missing volid}
514+ if volume_is_permanent;then
515+ echo "/srv/juju/${volid}"
516+ return 0
517+ else
518+ return 1
519+ fi
520+}
521+# Do we have a valid storage state?
522+# @returns 0 does echo $volid (can be "--ephemeral")
523+# 1 config state is invalid - we should not serve
524+volume_get_volume_id() {
525+ local ephemeral_storage
526+ local volid
527+ ephemeral_storage=$(config-get volume-ephemeral-storage) || return 1
528+ volid=$(volume_get_volid_from_volume_map) || return 1
529+ if [[ $ephemeral_storage == True ]];then
530+ # Ephemeral -> should not have a valid volid
531+ if [[ $volid != "" ]];then
532+ juju-log "ERROR: volume-ephemeral-storage is True, but $JUJU_UNIT_NAME maps to volid=${volid}"
533+ return 1
534+ fi
535+ else
536+ # Durable (not ephemeral) -> must have a valid volid for this unit
537+ if [[ $volid == "" ]];then
538+ juju-log "ERROR: volume-ephemeral-storage is False, but no volid found for: $JUJU_UNIT_NAME"
539+ return 1
540+ fi
541+ fi
542+ echo "$volid"
543+ return 0
544+}
545+
546+case "$1" in
547+ ## allow non SHELL scripts to call helper functions
548+ call)
549+ : ${JUJU_UNIT_NAME?} ## Must be called in juju environment
550+ shift;
551+ function="${1:?usage: ${0##*/} call function arg1 arg2 ...}"
552+ shift;
553+ ${function} "$@" && exit 0 || exit 1
554+esac

Subscribers

People subscribed via source and target branches