~james-w/charms/precise/postgresql/metrics

« back to all changes in this revision

Viewing changes to hooks/hooks.py

  • Committer: Stuart Bishop
  • Date: 2014-05-29 13:08:35 UTC
  • mfrom: (75.2.27 postgresql-storage)
  • Revision ID: stuart@stuartbishop.net-20140529130835-afvqvwn11kp5aw0v
[chad.smith] Use storage broker and storage subordinate charm for non-ephemeral disk, per https://code.launchpad.net/~chad.smith/charms/precise/postgresql/postgresql-using-storage-subordinate/+merge/206078

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
from tempfile import NamedTemporaryFile
17
17
from textwrap import dedent
18
18
import time
19
 
import yaml
20
 
from yaml.constructor import ConstructorError
21
19
 
22
20
from charmhelpers import fetch
23
21
from charmhelpers.core import hookenv, host
149
147
        self.save()
150
148
 
151
149
 
152
 
###############################################################################
153
 
# Volume managment
154
 
###############################################################################
155
 
#------------------------------
156
 
# Get volume-id from juju config "volume-map" dictionary as
157
 
#     volume-map[JUJU_UNIT_NAME]
158
 
# @return  volid
159
 
#
160
 
#------------------------------
161
 
def volume_get_volid_from_volume_map():
162
 
    volume_map = {}
163
 
    try:
164
 
        volume_map = yaml.load(hookenv.config('volume-map').strip())
165
 
        if volume_map:
166
 
            return volume_map.get(os.environ['JUJU_UNIT_NAME'])
167
 
    except ConstructorError as e:
168
 
        log("invalid YAML in 'volume-map': {}".format(e), WARNING)
169
 
    return None
170
 
 
171
 
 
172
 
# Is this volume_id permanent ?
173
 
# @returns  True if volid set and not --ephemeral, else:
174
 
#           False
175
 
def volume_is_permanent(volid):
176
 
    if volid and volid != "--ephemeral":
177
 
        return True
178
 
    return False
179
 
 
180
 
 
181
 
#------------------------------
182
 
# Returns a mount point from passed vol-id, e.g. /srv/juju/vol-000012345
183
 
#
184
 
# @param  volid          volume id (as e.g. EBS volid)
185
 
# @return mntpoint_path  eg /srv/juju/vol-000012345
186
 
#------------------------------
187
 
def volume_mount_point_from_volid(volid):
188
 
    if volid and volume_is_permanent(volid):
189
 
        return "/srv/juju/%s" % volid
190
 
    return None
191
 
 
192
 
 
193
 
# Do we have a valid storage state?
194
 
# @returns  volid
195
 
#           None    config state is invalid - we should not serve
196
 
def volume_get_volume_id():
197
 
    ephemeral_storage = hookenv.config('volume-ephemeral-storage')
198
 
    volid = volume_get_volid_from_volume_map()
199
 
    juju_unit_name = hookenv.local_unit()
200
 
    if ephemeral_storage in [True, 'yes', 'Yes', 'true', 'True']:
201
 
        if volid:
202
 
            log(
203
 
                "volume-ephemeral-storage is True, but " +
204
 
                "volume-map[{!r}] -> {}".format(juju_unit_name, volid), ERROR)
205
 
            return None
206
 
        else:
207
 
            return "--ephemeral"
208
 
    else:
209
 
        if not volid:
210
 
            log(
211
 
                "volume-ephemeral-storage is False, but "
212
 
                "no volid found for volume-map[{!r}]".format(
213
 
                    hookenv.local_unit()), ERROR)
214
 
            return None
215
 
    return volid
216
 
 
217
 
 
218
 
# Initialize and/or mount permanent storage, it straightly calls
219
 
# shell helper
220
 
def volume_init_and_mount(volid):
221
 
    command = ("scripts/volume-common.sh call " +
222
 
               "volume_init_and_mount %s" % volid)
223
 
    output = run(command)
224
 
    if output.find("ERROR") >= 0:
225
 
        return False
226
 
    return True
227
 
 
228
 
 
229
150
def volume_get_all_mounted():
230
 
    command = ("mount |egrep /srv/juju")
 
151
    command = ("mount |egrep %s" % external_volume_mount)
231
152
    status, output = commands.getstatusoutput(command)
232
153
    if status != 0:
233
154
        return None
883
804
 
884
805
def ensure_package_status(package, status):
885
806
    selections = ''.join(['{} {}\n'.format(package, status)])
886
 
    dpkg = subprocess.Popen(['dpkg', '--set-selections'],
887
 
                                stdin=subprocess.PIPE)
 
807
    dpkg = subprocess.Popen(
 
808
        ['dpkg', '--set-selections'], stdin=subprocess.PIPE)
888
809
    dpkg.communicate(input=selections)
889
810
 
890
811
 
893
814
# NOTE the only 2 "True" return points:
894
815
#   1) symlink already pointing to existing storage (no-op)
895
816
#   2) new storage properly initialized:
896
 
#     - volume: initialized if not already (fdisk, mkfs),
897
 
#       mounts it to e.g.:  /srv/juju/vol-000012345
898
817
#     - if fresh new storage dir: rsync existing data
899
818
#     - manipulate /var/lib/postgresql/VERSION/CLUSTER symlink
900
819
#------------------------------------------------------------------------------
901
 
def config_changed_volume_apply():
 
820
def config_changed_volume_apply(mount_point):
902
821
    version = pg_version()
903
822
    cluster_name = hookenv.config('cluster_name')
904
823
    data_directory_path = os.path.join(
905
824
        postgresql_data_dir, version, cluster_name)
906
825
 
907
826
    assert(data_directory_path)
908
 
    volid = volume_get_volume_id()
909
 
    if volid:
910
 
        if volume_is_permanent(volid):
911
 
            if not volume_init_and_mount(volid):
912
 
                log(
913
 
                    "volume_init_and_mount failed, not applying changes",
914
 
                    ERROR)
915
 
                return False
916
 
 
917
 
        if not os.path.exists(data_directory_path):
918
 
            log(
919
 
                "postgresql data dir {} not found, "
920
 
                "not applying changes.".format(data_directory_path),
921
 
                CRITICAL)
922
 
            return False
923
 
 
924
 
        mount_point = volume_mount_point_from_volid(volid)
925
 
        new_pg_dir = os.path.join(mount_point, "postgresql")
926
 
        new_pg_version_cluster_dir = os.path.join(
927
 
            new_pg_dir, version, cluster_name)
928
 
        if not mount_point:
929
 
            log(
930
 
                "invalid mount point from volid = {}, "
931
 
                "not applying changes.".format(mount_point), ERROR)
932
 
            return False
933
 
 
934
 
        if ((os.path.islink(data_directory_path) and
935
 
             os.readlink(data_directory_path) == new_pg_version_cluster_dir and
936
 
             os.path.isdir(new_pg_version_cluster_dir))):
937
 
            log(
938
 
                "postgresql data dir '%s' already points "
939
 
                "to {}, skipping storage changes.".format(
940
 
                    data_directory_path, new_pg_version_cluster_dir))
941
 
            log(
942
 
                "existing-symlink: to fix/avoid UID changes from "
943
 
                "previous units, doing: "
944
 
                "chown -R postgres:postgres {}".format(new_pg_dir))
945
 
            run("chown -R postgres:postgres %s" % new_pg_dir)
946
 
            return True
947
 
 
948
 
        # Create a directory structure below "new" mount_point, as e.g.:
949
 
        #   /srv/juju/vol-000012345/postgresql/9.1/main  , which "mimics":
950
 
        #   /var/lib/postgresql/9.1/main
951
 
        curr_dir_stat = os.stat(data_directory_path)
952
 
        for new_dir in [new_pg_dir,
953
 
                        os.path.join(new_pg_dir, version),
954
 
                        new_pg_version_cluster_dir]:
955
 
            if not os.path.isdir(new_dir):
956
 
                log("mkdir %s".format(new_dir))
957
 
                os.mkdir(new_dir)
958
 
                # copy permissions from current data_directory_path
959
 
                os.chown(new_dir, curr_dir_stat.st_uid, curr_dir_stat.st_gid)
960
 
                os.chmod(new_dir, curr_dir_stat.st_mode)
961
 
        # Carefully build this symlink, e.g.:
962
 
        # /var/lib/postgresql/9.1/main ->
963
 
        # /srv/juju/vol-000012345/postgresql/9.1/main
964
 
        # but keep previous "main/"  directory, by renaming it to
965
 
        # main-$TIMESTAMP
966
 
        try:
967
 
            postgresql_stop()
968
 
        except subprocess.CalledProcessError:
969
 
            log("postgresql_stop() failed - can't migrate data.", ERROR)
970
 
            return False
971
 
        if not os.path.exists(os.path.join(
972
 
                new_pg_version_cluster_dir, "PG_VERSION")):
973
 
            log("migrating PG data {}/ -> {}/".format(
974
 
                data_directory_path, new_pg_version_cluster_dir), WARNING)
975
 
            # void copying PID file to perm storage (shouldn't be any...)
976
 
            command = "rsync -a --exclude postmaster.pid {}/ {}/".format(
977
 
                data_directory_path, new_pg_version_cluster_dir)
978
 
            log("run: {}".format(command))
979
 
            run(command)
980
 
        try:
981
 
            os.rename(data_directory_path, "{}-{}".format(
982
 
                data_directory_path, int(time.time())))
983
 
            log("NOTICE: symlinking {} -> {}".format(
984
 
                new_pg_version_cluster_dir, data_directory_path))
985
 
            os.symlink(new_pg_version_cluster_dir, data_directory_path)
986
 
            log(
987
 
                "after-symlink: to fix/avoid UID changes from "
988
 
                "previous units, doing: "
989
 
                "chown -R postgres:postgres {}".format(new_pg_dir))
990
 
            run("chown -R postgres:postgres {}".format(new_pg_dir))
991
 
            return True
992
 
        except OSError:
993
 
            log("failed to symlink {} -> {}".format(
994
 
                data_directory_path, mount_point), CRITICAL)
995
 
            return False
996
 
    else:
997
 
        log(
998
 
            "Invalid volume storage configuration, not applying changes",
999
 
            ERROR)
1000
 
    return False
 
827
 
 
828
    if not os.path.exists(data_directory_path):
 
829
        log(
 
830
            "postgresql data dir {} not found, "
 
831
            "not applying changes.".format(data_directory_path),
 
832
            CRITICAL)
 
833
        return False
 
834
 
 
835
    new_pg_dir = os.path.join(mount_point, "postgresql")
 
836
    new_pg_version_cluster_dir = os.path.join(
 
837
        new_pg_dir, version, cluster_name)
 
838
    if not mount_point:
 
839
        log(
 
840
            "invalid mount point = {}, "
 
841
            "not applying changes.".format(mount_point), ERROR)
 
842
        return False
 
843
 
 
844
    if ((os.path.islink(data_directory_path) and
 
845
         os.readlink(data_directory_path) == new_pg_version_cluster_dir and
 
846
         os.path.isdir(new_pg_version_cluster_dir))):
 
847
        log(
 
848
            "postgresql data dir '{}' already points "
 
849
            "to {}, skipping storage changes.".format(
 
850
                data_directory_path, new_pg_version_cluster_dir))
 
851
        log(
 
852
            "existing-symlink: to fix/avoid UID changes from "
 
853
            "previous units, doing: "
 
854
            "chown -R postgres:postgres {}".format(new_pg_dir))
 
855
        run("chown -R postgres:postgres %s" % new_pg_dir)
 
856
        return True
 
857
 
 
858
    # Create a directory structure below "new" mount_point as
 
859
    #   external_volume_mount/postgresql/9.1/main
 
860
    for new_dir in [new_pg_dir,
 
861
                    os.path.join(new_pg_dir, version),
 
862
                    new_pg_version_cluster_dir]:
 
863
        if not os.path.isdir(new_dir):
 
864
            log("mkdir %s".format(new_dir))
 
865
            host.mkdir(new_dir, owner="postgres", perms=0o700)
 
866
    # Carefully build this symlink, e.g.:
 
867
    # /var/lib/postgresql/9.1/main ->
 
868
    # external_volume_mount/postgresql/9.1/main
 
869
    # but keep previous "main/"  directory, by renaming it to
 
870
    # main-$TIMESTAMP
 
871
    if not postgresql_stop() and postgresql_is_running():
 
872
        log("postgresql_stop() failed - can't migrate data.", ERROR)
 
873
        return False
 
874
    if not os.path.exists(os.path.join(
 
875
            new_pg_version_cluster_dir, "PG_VERSION")):
 
876
        log("migrating PG data {}/ -> {}/".format(
 
877
            data_directory_path, new_pg_version_cluster_dir), WARNING)
 
878
        # void copying PID file to perm storage (shouldn't be any...)
 
879
        command = "rsync -a --exclude postmaster.pid {}/ {}/".format(
 
880
            data_directory_path, new_pg_version_cluster_dir)
 
881
        log("run: {}".format(command))
 
882
        run(command)
 
883
    try:
 
884
        os.rename(data_directory_path, "{}-{}".format(
 
885
            data_directory_path, int(time.time())))
 
886
        log("NOTICE: symlinking {} -> {}".format(
 
887
            new_pg_version_cluster_dir, data_directory_path))
 
888
        os.symlink(new_pg_version_cluster_dir, data_directory_path)
 
889
        run("chown -h postgres:postgres {}".format(data_directory_path))
 
890
        log(
 
891
            "after-symlink: to fix/avoid UID changes from "
 
892
            "previous units, doing: "
 
893
            "chown -R postgres:postgres {}".format(new_pg_dir))
 
894
        run("chown -R postgres:postgres {}".format(new_pg_dir))
 
895
        return True
 
896
    except OSError:
 
897
        log("failed to symlink {} -> {}".format(
 
898
            data_directory_path, mount_point), CRITICAL)
 
899
        return False
1001
900
 
1002
901
 
1003
902
def token_sql_safe(value):
1008
907
 
1009
908
 
1010
909
@hooks.hook()
1011
 
def config_changed(force_restart=False):
 
910
def config_changed(force_restart=False, mount_point=None):
1012
911
    validate_config()
1013
912
    config_data = hookenv.config()
1014
913
    update_repos_and_packages()
1015
914
 
1016
 
    # Trigger volume initialization logic for permanent storage
1017
 
    volid = volume_get_volume_id()
1018
 
    if not volid:
1019
 
        ## Invalid configuration (whether ephemeral, or permanent)
1020
 
        postgresql_autostart(False)
1021
 
        postgresql_stop()
1022
 
        mounts = volume_get_all_mounted()
1023
 
        if mounts:
1024
 
            log("current mounted volumes: {}".format(mounts))
1025
 
        log(
1026
 
            "Disabled and stopped postgresql service, "
1027
 
            "because of broken volume configuration - check "
1028
 
            "'volume-ephemeral-storage' and 'volume-map'", ERROR)
1029
 
        sys.exit(1)
1030
 
 
1031
 
    if volume_is_permanent(volid):
1032
 
        ## config_changed_volume_apply will stop the service if it founds
 
915
    if mount_point is not None:
 
916
        ## config_changed_volume_apply will stop the service if it finds
1033
917
        ## it necessary, ie: new volume setup
1034
 
        if config_changed_volume_apply():
 
918
        if config_changed_volume_apply(mount_point=mount_point):
1035
919
            postgresql_autostart(True)
1036
920
        else:
1037
921
            postgresql_autostart(False)
1139
1023
 
1140
1024
@hooks.hook()
1141
1025
def upgrade_charm():
 
1026
    """Handle saving state during an upgrade-charm hook.
 
1027
 
 
1028
    When upgrading from an installation using volume-map, we migrate
 
1029
    that installation to use the storage subordinate charm by remounting
 
1030
    a mountpath that the storage subordinate maintains. We exit(1) only to
 
1031
    raise visibility to manual procedure that we log in juju logs below for the
 
1032
    juju admin to finish the migration by relating postgresql to the storage
 
1033
    and block-storage-broker services. These steps are generalised in the
 
1034
    README as well.
 
1035
    """
1142
1036
    install(run_pre=False)
1143
1037
    snapshot_relations()
 
1038
    version = pg_version()
 
1039
    cluster_name = hookenv.config('cluster_name')
 
1040
    data_directory_path = os.path.join(
 
1041
        postgresql_data_dir, version, cluster_name)
 
1042
    if (os.path.islink(data_directory_path)):
 
1043
        link_target = os.readlink(data_directory_path)
 
1044
        if "/srv/juju" in link_target:
 
1045
            # Then we just upgraded from an installation that was using
 
1046
            # charm config volume_map definitions. We need to stop postgresql
 
1047
            # and remount the device where the storage subordinate expects to
 
1048
            # control the mount in the future if relations/units change
 
1049
            volume_id = link_target.split("/")[3]
 
1050
            unit_name = hookenv.local_unit()
 
1051
            new_mount_root = external_volume_mount
 
1052
            new_pg_version_cluster_dir = os.path.join(
 
1053
                new_mount_root, "postgresql", version, cluster_name)
 
1054
            if not os.exists(new_mount_root):
 
1055
                os.mkdir(new_mount_root)
 
1056
            log("\n"
 
1057
                "WARNING: %s unit has external volume id %s mounted via the\n"
 
1058
                "deprecated volume-map and volume-ephemeral-storage\n"
 
1059
                "configuration parameters.\n"
 
1060
                "These parameters are no longer available in the postgresql\n"
 
1061
                "charm in favor of using the volume_map parameter in the\n"
 
1062
                "storage subordinate charm.\n"
 
1063
                "We are migrating the attached volume to a mount path which\n"
 
1064
                "can be managed by the storage subordinate charm. To\n"
 
1065
                "continue using this volume_id with the storage subordinate\n"
 
1066
                "follow this procedure.\n-----------------------------------\n"
 
1067
                "1. cat > storage.cfg <<EOF\nstorage:\n"
 
1068
                "  provider: block-storage-broker\n"
 
1069
                "  root: %s\n"
 
1070
                "  volume_map: \"{%s: %s}\"\nEOF\n2. juju deploy "
 
1071
                "--config storage.cfg storage\n"
 
1072
                "3. juju deploy block-storage-broker\n4. juju add-relation "
 
1073
                "block-storage-broker storage\n5. juju resolved --retry "
 
1074
                "%s\n6. juju add-relation postgresql storage\n"
 
1075
                "-----------------------------------\n" %
 
1076
                (unit_name, volume_id, new_mount_root, unit_name, volume_id,
 
1077
                 unit_name), WARNING)
 
1078
            postgresql_stop()
 
1079
            os.unlink(data_directory_path)
 
1080
            log("Unmounting external storage due to charm upgrade: %s" %
 
1081
                link_target)
 
1082
            try:
 
1083
                subprocess.check_output(
 
1084
                    "umount /srv/juju/%s" % volume_id, shell=True)
 
1085
                # Since e2label truncates labels to 16 characters use only the
 
1086
                # first 16 characters of the volume_id as that's what was
 
1087
                # set by old versions of postgresql charm
 
1088
                subprocess.check_call(
 
1089
                    "mount -t ext4 LABEL=%s %s" %
 
1090
                    (volume_id[:16], new_mount_root), shell=True)
 
1091
            except subprocess.CalledProcessError, e:
 
1092
                log("upgrade-charm mount migration failed. %s" % str(e), ERROR)
 
1093
                sys.exit(1)
 
1094
 
 
1095
            log("NOTICE: symlinking {} -> {}".format(
 
1096
                new_pg_version_cluster_dir, data_directory_path))
 
1097
            os.symlink(new_pg_version_cluster_dir, data_directory_path)
 
1098
            run("chown -h postgres:postgres {}".format(data_directory_path))
 
1099
            postgresql_start()  # Will exit(1) if issues
 
1100
            log("Remount and restart success for this external volume.\n"
 
1101
                "This current running installation will break upon\n"
 
1102
                "add/remove postgresql units or relations if you do not\n"
 
1103
                "follow the above procedure to ensure your external\n"
 
1104
                "volumes are preserved by the storage subordinate charm.",
 
1105
                WARNING)
 
1106
            # So juju admins can see the hook fail and note the steps to fix
 
1107
            # per our WARNINGs above
 
1108
            sys.exit(1)
1144
1109
 
1145
1110
 
1146
1111
@hooks.hook()
1604
1569
    packages = fetch.filter_installed_packages(packages)
1605
1570
    # Set package state for main postgresql package if installed
1606
1571
    if 'postgresql-{}'.format(version) not in packages:
1607
 
        ensure_package_status('postgresql-{}'.format(version), 
 
1572
        ensure_package_status('postgresql-{}'.format(version),
1608
1573
                              hookenv.config('package_status'))
1609
1574
    fetch.apt_install(packages, fatal=True)
1610
1575
 
2304
2269
        sanitize(hookenv.local_unit()), sanitize(remote_unit))
2305
2270
 
2306
2271
 
 
2272
@hooks.hook('data-relation-changed')
 
2273
def data_relation_changed():
 
2274
    """Listen for configured mountpoint from storage subordinate relation"""
 
2275
    if not hookenv.relation_get("mountpoint"):
 
2276
        hookenv.log("Waiting for mountpoint from the relation: %s"
 
2277
                    % external_volume_mount, hookenv.DEBUG)
 
2278
    else:
 
2279
        hookenv.log("Storage ready and mounted", hookenv.DEBUG)
 
2280
        config_changed(mount_point=external_volume_mount)
 
2281
 
 
2282
 
 
2283
@hooks.hook('data-relation-joined')
 
2284
def data_relation_joined():
 
2285
    """Request mountpoint from storage subordinate by setting mountpoint"""
 
2286
    hookenv.log("Setting mount point in the relation: %s"
 
2287
                % external_volume_mount, hookenv.DEBUG)
 
2288
    hookenv.relation_set(mountpoint=external_volume_mount)
 
2289
 
 
2290
 
 
2291
@hooks.hook('data-relation-departed')
 
2292
def stop_postgres_on_data_relation_departed():
 
2293
    hookenv.log("Data relation departing. Stopping PostgreSQL",
 
2294
                hookenv.DEBUG)
 
2295
    postgresql_stop()
 
2296
 
 
2297
 
2307
2298
def _get_postgresql_config_dir(config_data=None):
2308
2299
    """ Return the directory path of the postgresql configuration files. """
2309
2300
    if config_data is None:
2327
2318
local_state = State('local_state.pickle')
2328
2319
hook_name = os.path.basename(sys.argv[0])
2329
2320
juju_log_dir = "/var/log/juju"
 
2321
external_volume_mount = "/srv/data"
2330
2322
 
2331
2323
 
2332
2324
if __name__ == '__main__':