Merge lp:~chad.smith/charms/precise/postgresql/postgresql-using-storage-subordinate into lp:charms/postgresql
- Precise Pangolin (12.04)
- postgresql-using-storage-subordinate
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Stuart Bishop |
Approved revision: | 102 |
Merged at revision: | 98 |
Proposed branch: | lp:~chad.smith/charms/precise/postgresql/postgresql-using-storage-subordinate |
Merge into: | lp:charms/postgresql |
Diff against target: |
961 lines (+313/-457) 6 files modified
README.md (+27/-0) config.yaml (+0/-31) hooks/hooks.py (+188/-193) hooks/test_hooks.py (+94/-13) metadata.yaml (+4/-0) scripts/volume-common.sh (+0/-220) |
To merge this branch: | bzr merge lp:~chad.smith/charms/precise/postgresql/postgresql-using-storage-subordinate |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stuart Bishop (community) | Approve | ||
Chad Smith (community) | Approve | ||
charmers | Pending | ||
Review via email: mp+206078@code.launchpad.net |
Commit message
Description of the change
Both storage subordinate charm and block storage broker are now live in the charm store. I'm unblocking this review now that the storage and block-storage-
-------
There is a new storage subordinate charm that has incorporated all storage mount,fsck and partitioning that the postgresql charm performed as well as talking to the block-storage-
The storage subordinate attempts to avoid duplication of partitioning, mounting and attaching devices in multiple charms by coalescing all that work in one charm. Only thing that the principal (postgresql in this case) has to provide is a preferred mountpoint to the storage subordinate.
This branch does the following:
1. adds the data relation to metadata.yaml for communicating with the storage subordinate to request a mountpoint.
2. drops all postgresql scripts/code used for discovery, partitioning, fsck and mount of attached nova volumes in favor of using the consolidated storage subordinate charm.
3. adds a new config option storage_mount_point (which defaults to /mnt/storage).
4. drops the config options volume-
5. calls posgresql_stop() on data-relation-
6. some more unit tests for code coverage
To deploy and test without any nova volumes created:
$ cat >postgresql-
common:
services:
postgresql:
branch: lp:~chad.smith/charms/precise/postgresql/postgresql-using-storage-subordinate
branch: lp:~charmers/charms/precise/block-storage-broker/trunk
doit-no-
inherits: common
series: precise
services:
storage:
branch: lp:~charmers/charms/precise/storage/trunk
relations:
- [postgresql, storage]
- [storage, block-storage-
doit-with-
inherits: common
series: precise
services:
storage:
branch: lp:~charmers/charms/precise/storage/trunk
relations:
- [postgresql, storage]
- [storage, block-storage-
END
# The following will deploy postgresql related to storage and storage related to block-storage-
$ juju-deployer -c postgresql-
# The following will deploy postgresql related to storage and storage related to block-storage-
$ juju-deployer -c postgresql-
Stuart Bishop (stub) wrote : | # |
Stuart Bishop (stub) : | # |
Stuart Bishop (stub) wrote : | # |
On 17 February 2014 21:27, Stuart Bishop <email address hidden> wrote:
> On trunk you will discover that postgresql_stop() never fails. It is
> important here, so I suggest we add an extra check at the end of
> postgresql_stop() that raises an exception if postgresql_
> still returns true.
Minor correction. If postgresql_stop fails, a
subprocess.
could still be good though to double check that pg_ctlcluster did what
it claims.
Chad Smith (chad.smith) wrote : | # |
Stuart, Thanks for the comments. We discussed this week and agree with you about general direction and trying to incorporate the juju-run functionality when it is released. Since it's not going to be released in 1.16 we'd like to start with this approach and then have a followup that will simplify principal hook requirements even more in the next storage release.
As per the upgrade-charm path, here's what we are thinking (and I'll try incorporating that into this patch when I'm back on Tuesday).
Since upgrade-charm involves having to deploy storage service with a new configuration and add-relation, we can't do all this inside the upgrade-charm hook. So, we'll document the upgrade process in the README and ensure that the upgrade-charm hook exits(1) to get the attention of the juju admin.
upgrade-charm will:
1. stop postgresql,
2. umount the mounted /srv/juju/vol-id
3. unlink /var/lib/juju
4. sys.exit(1) # Expose an upgrade-charm hook error
-- a hook error occurs with config-change errors if you manipulate either volume-map or volume-
5. Log a WARNING to the user in the postgresql-unit.log
WARNING: postgresql/1 unit has external volume id 123-123-12312-312 mounted via the
deprecated volume-map and volume-
configuration parameters.
These parameters are no longer available in the postgresql
charm in favor of using the volume_map parameter in the
storage subordinate charm.
The attached volume is left intact. To remount this
volume_id using the storage subordinate follow these steps:
1. cat > storage.cfg <<EOF\nstorage:
storage:\n"
volume_map: "{postgresql/0: 123-123-12312-312, postgresql/1: 54-645-645-645}"
EOF
2. juju deploy --config storage.cfg storage
3. juju add-relation postgresql storage
How does an approach like this sound Stuart, given that the upgrade-charm hook can't deploy and relate new services?
Stuart Bishop (stub) wrote : | # |
On 20 February 2014 07:21, Chad Smith <email address hidden> wrote:
> Stuart, Thanks for the comments. We discussed this week and agree with you about general direction and trying to incorporate the juju-run functionality when it is released. Since it's not going to be released in 1.16 we'd like to start with this approach and then have a followup that will simplify principal hook requirements even more in the next storage release.
Ok.
> As per the upgrade-charm path, here's what we are thinking (and I'll try incorporating that into this patch when I'm back on Tuesday).
>
> Since upgrade-charm involves having to deploy storage service with a new configuration and add-relation, we can't do all this inside the upgrade-charm hook. So, we'll document the upgrade process in the README and ensure that the upgrade-charm hook exits(1) to get the attention of the juju admin.
>
> upgrade-charm will:
> 1. stop postgresql,
> 2. umount the mounted /srv/juju/vol-id
> 3. unlink /var/lib/juju
> 4. sys.exit(1) # Expose an upgrade-charm hook error
> -- a hook error occurs with config-change errors if you manipulate either volume-map or volume-
>
> 5. Log a WARNING to the user in the postgresql-unit.log
> WARNING: postgresql/1 unit has external volume id 123-123-12312-312 mounted via the
> deprecated volume-map and volume-
> configuration parameters.
> These parameters are no longer available in the postgresql
> charm in favor of using the volume_map parameter in the
> storage subordinate charm.
> The attached volume is left intact. To remount this
> volume_id using the storage subordinate follow these steps:
> 1. cat > storage.cfg <<EOF\nstorage:
> storage:\n"
> volume_map: "{postgresql/0: 123-123-12312-312, postgresql/1: 54-645-645-645}"
> EOF
> 2. juju deploy --config storage.cfg storage
> 3. juju add-relation postgresql storage
>
>
> How does an approach like this sound Stuart, given that the upgrade-charm hook can't deploy and relate new services?
This sounds reasonable. Assuming the directory structures match, no
large database files need to be shuffled around. I am unsure if the
last task in step 5 will actually work, as the operator needs to add
the relation but upgrade-charm has not yet succeeded. Assuming this
works, how does the rest of the process go? After the operator has
performed the tasks given by step 5, we have a PostgreSQL unit in a
failed state and a subordinate charm with pending relation-hooks. Will
'juju resolved' or 'juju resolved --retry' be all that is required?
An alternative would be to not have upgrade-charm fail, but instead
set a flag ensuring that PG will not start up until the subordinate
storage charm has been successfully related. Disable autostart,
local_state could store the flag, and postgrersql_start would check
for it. It might not be this simple though, as other hooks like
config-changed would likely fail instead when PG fails to start on
request and I think config-changed will get invoked immediately after
the upgrade-charm...
And another alternative.... upgrade-charm just leaves the stora...
Chad Smith (chad.smith) wrote : | # |
Hi Stuart,
Thanks for the suggestions here about the upgrade-charm lifecycle. What I ended up doing yesterday was the following to ease the upgrade process
upgrade_charm hook will:
1. stop postgresql;
2. umount the /srv/juju/vol-id;
3. unlink /var/lib/
4. mount LABEL=vol-id /srv/data;
5. symlink /var/lib/
6. chown -h postgres:postgresql /var/lib/
7. log a warning about manual procedure to add storage and block-storage-
WARNING: postgresql/0 unit has external volume id 123-123-123-123 mounted via the
deprecated volume-map and volume-
These parameters are no longer available in the postgresql
charm in favor of using the volume_map parameter in the
storage subordinate charm.
We are migrating the attached volume to a mount path which
can be managed by the storage subordinate charm. To
continue using this volume_id with the storage subordinate
follow this procedure.
-------
1. cat > storage.cfg <<EOF
storage:"
provider:
root: /srv/data
volume_map: "{postgresql/0: 123-123-123-123}"
EOF
2. juju deploy --config storage.cfg storage
3. juju deploy block-storage-
4. juju add-relation block-storage-
5. juju resolved --retry postgresql/0
6. juju add-relation postgresql storage
-------
Remount and restart success for this external volume.
This current running installation will break upon
add/remove postgresql units or reations if you do not
follow the above procedure to ensure your external
volumes are preserved by the storage subordinate charm.
I also just added the ability to handle partitioned volumes into the storage subordinate charm as well. This was needed because the current postgresql charm partitions the volume into a single partition of 100% of the volume size, but the storage subordinate just uses the entire volume without partitioning.
>
> This sounds reasonable. Assuming the directory structures match, no
> large database files need to be shuffled around. I am unsure if the
> last task in step 5 will actually work,
Good point Stuart, as I found too, it didn't work. Even with the resolved --retry, it would still fail at the following config_changed hook that fires after the upgrade_charm hook because I had originally left PG in an unusable state (with the intent of allowing the storage subordinate relation to setup the mounts again). So, I've made sure all mount/link work is now done in the upgrade_charm hook and the postgresql service is still running after remounting the device in the proper location for the storage subordinate to handle it. We still will exit(1) to alert the juju admin to the manual juju deploy & add-relation steps needed for ongoing volume support. But, in this new approach, the service still has integrity, and a resolved --retry will succeed (as well as the following config_changed hook).
> After the operator has performed the tasks given by step 5, we have a PostgreS...
Stuart Bishop (stub) wrote : | # |
This is all looking good. The branch as it stands is an improvement and can be landed. I've got comments below which you might want to act on before I land this. Please let me know when you are happy with the branch and want it landed, or if you want further review.
Thanks :)
=== modified file 'README.md'
+## Upgrade-charm hook notes
Yay documentation.
=== modified file 'config.yaml'
+ storage_
+ default: "/mnt/storage"
+ type: string
+ description: |
+ The location where the storage is going to be mounted.
Do we have a use case for this to be configurable? If not, consider removing it. We already have a huge number of knobs in the PG charm, and this extra one might make further work on the storage subordinate harder.
If storage_mount_point remains, what happens if someone changes it? You will want to add the item to unchangable_config in validate_config().
-def config_
+def config_
Its probably better to not have the default of None here.
+ if not mount_point:
+ log("Invalid volume storage configuration, not applying changes",
ERROR)
+ return False
You abort if mount_point is false here...
+ if not mount_point:
+ log(
+ "invalid mount point = {}, "
+ "not applying changes.
+ return False
... which means this code will never be reached and should be removed.
+ if ((os.path.
+ os.readlink(
+ os.path.
+ log(
+ "postgresql data dir '{}' already points "
+ "to {}, skipping storage changes.".format(
+ data_directory_
+ log(
+ "existing-symlink: to fix/avoid UID changes from "
+ "previous units, doing: "
+ "chown -R postgres:postgres {}".format(
+ run("chown -R postgres:postgres %s" % new_pg_dir)
+ return True
When do you see invalid file ownerships btw? Is this masking some other problem that needs to be addressed?
+ # Create a directory structure below "new" mount_point, as e.g.:
+ # config[
+ # /var/lib/
+ curr_dir_stat = os.stat(
+ for new_dir in [new_pg_dir,
+ os.path.
+ new_pg_
+ if not os.path.
+ log("mkdir %s".format(
+ os.mkdir(new_dir)
+ # copy permissions from current data_directory_path
+ os.chown(new_dir, curr_dir_
+ os.chmod(new_dir, curr_dir_
Rather than this loop, you can use charmhelpers.
+ # Carefully build this symlink, e.g.:
+ # /var/lib/
+ # config[
Chad Smith (chad.smith) wrote : | # |
Stuart, thanks for the final review here:
- I have dropped the storage_mount_point config option as there is no use case we can think of here. The mountpoint can be a global defined within the charm itself external_
- config_
- When do you see invalid file ownerships btw? Is this masking some other problem that needs to be addressed?
+ run("chown -R postgres:postgres %s" % new_pg_dir)
> When do you see invalid file ownerships btw? Is this masking some other problem that needs to be addressed?
I believe we saw this when the subdirectories on the external volume were created by postgresql/0 and we ended up spinning up destroying the unit and attaching the new postgresql/1 later to the previously configured volume. So, if there were a different uid/gid on the new installation for the postgresql user, we could have a mismatch right?
I had also seen that root user (via the charm hooks) created the directories and symlink with root:root and we needed to chown as postgres:postgres to ensure the Pg service could actually read and traverse the directories.
- Now we are using host.mkdir w/ postgres user and perms 700
We'll push on charmers this next week to get both storage subordinate and block-storage-
We are good for a merge up now just ran through a deployment to ensure the changes didn't break anything.
Stuart Bishop (stub) wrote : | # |
On 1 March 2014 06:38, Chad Smith <email address hidden> wrote:
> - When do you see invalid file ownerships btw? Is this masking some other problem that needs to be addressed?
>
> + run("chown -R postgres:postgres %s" % new_pg_dir)
>> When do you see invalid file ownerships btw? Is this masking some other problem that needs to be addressed?
>
> I believe we saw this when the subdirectories on the external volume were created by postgresql/0 and we ended up spinning up destroying the unit and attaching the new postgresql/1 later to the previously configured volume. So, if there were a different uid/gid on the new installation for the postgresql user, we could have a mismatch right?
This makes sense, and will happen normally with some disaster recovery
strategies (bringing up an existing database on a new node).
> We'll push on charmers this next week to get both storage subordinate and block-storage-
>
> We are good for a merge up now just ran through a deployment to ensure the changes didn't break anything.
> --
> https:/
> You are reviewing the proposed merge of lp:~chad.smith/charms/precise/postgresql/postgresql-using-storage-subordinate into lp:charms/postgresql.
I don't think we can land this until after the storage subordinate and
block-storage-
are required to migrate from earlier revisions of the PostgreSQL
charm.
--
Stuart Bishop <<email address hidden>
Chad Smith (chad.smith) wrote : | # |
> I don't think we can land this until after the storage subordinate and
> block-storage-
> are required to migrate from earlier revisions of the PostgreSQL
> charm.
>
>
> --
> Stuart Bishop <<email address hidden>
Sounds good Stuart, we'll get Landscape team to finish the EC2 reviews for storage and block-storage-
Matt Bruzek (mbruzek) wrote : | # |
Moving this merge request to "Work In Progress" until the other charms land. Once those charms land move the propoal back to "Needs review" and that will add it back to the review queue.
Chad Smith (chad.smith) wrote : | # |
Thank you Matt for the "WIP" flag here while we awaited charm release. I have now unblocked this charm as both storage charms are both added to the charm store.
storage subordinate:
https:/
block-storage-
https:/
Stuart Bishop (stub) wrote : | # |
I've gone through this again, and think it is good to land. I had several concerns, all of which turned out to be false when I looked closer.
Preview Diff
1 | === modified file 'README.md' |
2 | --- README.md 2014-02-05 12:29:09 +0000 |
3 | +++ README.md 2014-02-28 23:23:59 +0000 |
4 | @@ -223,6 +223,33 @@ |
5 | hooks.execute(sys.argv) |
6 | |
7 | |
8 | +## Upgrade-charm hook notes |
9 | + |
10 | +The PostgreSQL charm has deprecated volume-map and volume-ephemeral-storage |
11 | +configuration options in favor of using the storage subordinate charm for |
12 | +general external storage management. If the installation being upgraded is |
13 | +using these deprecated options, there are a couple of manual steps necessary |
14 | +to finish migration and continue using the current external volumes. |
15 | +Even though all data will remain intact, and PostgreSQL service will continue |
16 | +running, the upgrade-charm hook will intentionally fail and exit 1 as well to |
17 | +raise awareness of the manual procedure which will also be documented in the |
18 | +juju logs on the PostgreSQL units. |
19 | + |
20 | +The following steps must be additionally performed to continue using external |
21 | +volume maps for the PostgreSQL units once juju upgrade-charm is run from the |
22 | +command line: |
23 | + 1. cat > storage.cfg <<EOF |
24 | + storage: |
25 | + provider:block-storage-broker |
26 | + root: /srv/data |
27 | + volume_map: "{postgresql/0: your-vol-id, postgresql/1: your-2nd-vol-id}" |
28 | + EOF |
29 | + 2. juju deploy --config storage.cfg storage |
30 | + 3. juju deploy block-storage-broker |
31 | + 4. juju add-relation block-storage-broker storage |
32 | + 5. juju resolved --retry postgresql/0 # for each postgresql unit running |
33 | + 6. juju add-relation postgresql storage |
34 | + |
35 | |
36 | # Contact Information |
37 | |
38 | |
39 | === modified file 'config.yaml' |
40 | --- config.yaml 2014-01-23 11:41:03 +0000 |
41 | +++ config.yaml 2014-02-28 23:23:59 +0000 |
42 | @@ -282,36 +282,6 @@ |
43 | default: 4.0 |
44 | type: float |
45 | description: Random page cost |
46 | - #------------------------------------------------------------------------ |
47 | - # Volume management |
48 | - # volume-map, volume-dev_regexp are only used |
49 | - # if volume-ephemeral-storage == False |
50 | - #------------------------------------------------------------------------ |
51 | - volume-ephemeral-storage: |
52 | - type: boolean |
53 | - default: true |
54 | - description: > |
55 | - If false, a configure-error state will be raised if |
56 | - volume-map[$JUJU_UNIT_NAME] is not set (see "volume-map" |
57 | - below) - see "volume-map" below. |
58 | - If true, service units won't try to use "volume-map" (and |
59 | - related variables) to mount and use external (EBS) volumes, |
60 | - thus storage lifetime will equal VM, thus ephemeral. |
61 | - YOU'VE BEEN WARNED. |
62 | - volume-map: |
63 | - type: string |
64 | - default: "" |
65 | - description: > |
66 | - YAML map as e.g. "{ postgres/0: vol-0000010, postgres/1: vol-0000016 }". |
67 | - Service units will raise a "configure-error" condition if no volume-map |
68 | - value is set for it - it's expected a human to set it properly to |
69 | - resolve it. |
70 | - volume-dev-regexp: |
71 | - type: string |
72 | - default: "/dev/vd[b-z]" |
73 | - description: > |
74 | - Block device for attached volumes as seen by the VM, will be "scanned" |
75 | - for an unused device when "volume-map" is valid for the unit. |
76 | backup_dir: |
77 | default: "/var/lib/postgresql/backups" |
78 | type: string |
79 | @@ -366,7 +336,6 @@ |
80 | An advisory lock key used internally by the charm. You do not need |
81 | to change it unless it happens to conflict with an advisory lock key |
82 | being used by your applications. |
83 | - |
84 | # Swift backups and PITR via SwiftWAL |
85 | swiftwal_container_prefix: |
86 | type: string |
87 | |
88 | === added symlink 'hooks/data-relation-changed' |
89 | === target is u'hooks.py' |
90 | === added symlink 'hooks/data-relation-departed' |
91 | === target is u'hooks.py' |
92 | === added symlink 'hooks/data-relation-joined' |
93 | === target is u'hooks.py' |
94 | === modified file 'hooks/hooks.py' |
95 | --- hooks/hooks.py 2014-02-11 09:11:32 +0000 |
96 | +++ hooks/hooks.py 2014-02-28 23:23:59 +0000 |
97 | @@ -15,8 +15,6 @@ |
98 | import sys |
99 | from tempfile import NamedTemporaryFile |
100 | import time |
101 | -import yaml |
102 | -from yaml.constructor import ConstructorError |
103 | |
104 | from charmhelpers import fetch |
105 | from charmhelpers.core import hookenv, host |
106 | @@ -146,85 +144,8 @@ |
107 | self.save() |
108 | |
109 | |
110 | -############################################################################### |
111 | -# Volume managment |
112 | -############################################################################### |
113 | -#------------------------------ |
114 | -# Get volume-id from juju config "volume-map" dictionary as |
115 | -# volume-map[JUJU_UNIT_NAME] |
116 | -# @return volid |
117 | -# |
118 | -#------------------------------ |
119 | -def volume_get_volid_from_volume_map(): |
120 | - volume_map = {} |
121 | - try: |
122 | - volume_map = yaml.load(hookenv.config('volume-map').strip()) |
123 | - if volume_map: |
124 | - return volume_map.get(os.environ['JUJU_UNIT_NAME']) |
125 | - except ConstructorError as e: |
126 | - log("invalid YAML in 'volume-map': {}".format(e), WARNING) |
127 | - return None |
128 | - |
129 | - |
130 | -# Is this volume_id permanent ? |
131 | -# @returns True if volid set and not --ephemeral, else: |
132 | -# False |
133 | -def volume_is_permanent(volid): |
134 | - if volid and volid != "--ephemeral": |
135 | - return True |
136 | - return False |
137 | - |
138 | - |
139 | -#------------------------------ |
140 | -# Returns a mount point from passed vol-id, e.g. /srv/juju/vol-000012345 |
141 | -# |
142 | -# @param volid volume id (as e.g. EBS volid) |
143 | -# @return mntpoint_path eg /srv/juju/vol-000012345 |
144 | -#------------------------------ |
145 | -def volume_mount_point_from_volid(volid): |
146 | - if volid and volume_is_permanent(volid): |
147 | - return "/srv/juju/%s" % volid |
148 | - return None |
149 | - |
150 | - |
151 | -# Do we have a valid storage state? |
152 | -# @returns volid |
153 | -# None config state is invalid - we should not serve |
154 | -def volume_get_volume_id(): |
155 | - ephemeral_storage = hookenv.config('volume-ephemeral-storage') |
156 | - volid = volume_get_volid_from_volume_map() |
157 | - juju_unit_name = hookenv.local_unit() |
158 | - if ephemeral_storage in [True, 'yes', 'Yes', 'true', 'True']: |
159 | - if volid: |
160 | - log( |
161 | - "volume-ephemeral-storage is True, but " + |
162 | - "volume-map[{!r}] -> {}".format(juju_unit_name, volid), ERROR) |
163 | - return None |
164 | - else: |
165 | - return "--ephemeral" |
166 | - else: |
167 | - if not volid: |
168 | - log( |
169 | - "volume-ephemeral-storage is False, but " |
170 | - "no volid found for volume-map[{!r}]".format( |
171 | - hookenv.local_unit()), ERROR) |
172 | - return None |
173 | - return volid |
174 | - |
175 | - |
176 | -# Initialize and/or mount permanent storage, it straightly calls |
177 | -# shell helper |
178 | -def volume_init_and_mount(volid): |
179 | - command = ("scripts/volume-common.sh call " + |
180 | - "volume_init_and_mount %s" % volid) |
181 | - output = run(command) |
182 | - if output.find("ERROR") >= 0: |
183 | - return False |
184 | - return True |
185 | - |
186 | - |
187 | def volume_get_all_mounted(): |
188 | - command = ("mount |egrep /srv/juju") |
189 | + command = ("mount |egrep %s" % external_volume_mount) |
190 | status, output = commands.getstatusoutput(command) |
191 | if status != 0: |
192 | return None |
193 | @@ -834,109 +755,89 @@ |
194 | # NOTE the only 2 "True" return points: |
195 | # 1) symlink already pointing to existing storage (no-op) |
196 | # 2) new storage properly initialized: |
197 | -# - volume: initialized if not already (fdisk, mkfs), |
198 | -# mounts it to e.g.: /srv/juju/vol-000012345 |
199 | # - if fresh new storage dir: rsync existing data |
200 | # - manipulate /var/lib/postgresql/VERSION/CLUSTER symlink |
201 | #------------------------------------------------------------------------------ |
202 | -def config_changed_volume_apply(): |
203 | +def config_changed_volume_apply(mount_point): |
204 | version = pg_version() |
205 | cluster_name = hookenv.config('cluster_name') |
206 | data_directory_path = os.path.join( |
207 | postgresql_data_dir, version, cluster_name) |
208 | |
209 | assert(data_directory_path) |
210 | - volid = volume_get_volume_id() |
211 | - if volid: |
212 | - if volume_is_permanent(volid): |
213 | - if not volume_init_and_mount(volid): |
214 | - log( |
215 | - "volume_init_and_mount failed, not applying changes", |
216 | - ERROR) |
217 | - return False |
218 | - |
219 | - if not os.path.exists(data_directory_path): |
220 | - log( |
221 | - "postgresql data dir {} not found, " |
222 | - "not applying changes.".format(data_directory_path), |
223 | - CRITICAL) |
224 | - return False |
225 | - |
226 | - mount_point = volume_mount_point_from_volid(volid) |
227 | - new_pg_dir = os.path.join(mount_point, "postgresql") |
228 | - new_pg_version_cluster_dir = os.path.join( |
229 | - new_pg_dir, version, cluster_name) |
230 | - if not mount_point: |
231 | - log( |
232 | - "invalid mount point from volid = {}, " |
233 | - "not applying changes.".format(mount_point), ERROR) |
234 | - return False |
235 | - |
236 | - if ((os.path.islink(data_directory_path) and |
237 | - os.readlink(data_directory_path) == new_pg_version_cluster_dir and |
238 | - os.path.isdir(new_pg_version_cluster_dir))): |
239 | - log( |
240 | - "postgresql data dir '%s' already points " |
241 | - "to {}, skipping storage changes.".format( |
242 | - data_directory_path, new_pg_version_cluster_dir)) |
243 | - log( |
244 | - "existing-symlink: to fix/avoid UID changes from " |
245 | - "previous units, doing: " |
246 | - "chown -R postgres:postgres {}".format(new_pg_dir)) |
247 | - run("chown -R postgres:postgres %s" % new_pg_dir) |
248 | - return True |
249 | - |
250 | - # Create a directory structure below "new" mount_point, as e.g.: |
251 | - # /srv/juju/vol-000012345/postgresql/9.1/main , which "mimics": |
252 | - # /var/lib/postgresql/9.1/main |
253 | - curr_dir_stat = os.stat(data_directory_path) |
254 | - for new_dir in [new_pg_dir, |
255 | - os.path.join(new_pg_dir, version), |
256 | - new_pg_version_cluster_dir]: |
257 | - if not os.path.isdir(new_dir): |
258 | - log("mkdir %s".format(new_dir)) |
259 | - os.mkdir(new_dir) |
260 | - # copy permissions from current data_directory_path |
261 | - os.chown(new_dir, curr_dir_stat.st_uid, curr_dir_stat.st_gid) |
262 | - os.chmod(new_dir, curr_dir_stat.st_mode) |
263 | - # Carefully build this symlink, e.g.: |
264 | - # /var/lib/postgresql/9.1/main -> |
265 | - # /srv/juju/vol-000012345/postgresql/9.1/main |
266 | - # but keep previous "main/" directory, by renaming it to |
267 | - # main-$TIMESTAMP |
268 | - if not postgresql_stop(): |
269 | - log("postgresql_stop() failed - can't migrate data.", ERROR) |
270 | - return False |
271 | - if not os.path.exists(os.path.join( |
272 | - new_pg_version_cluster_dir, "PG_VERSION")): |
273 | - log("migrating PG data {}/ -> {}/".format( |
274 | - data_directory_path, new_pg_version_cluster_dir), WARNING) |
275 | - # void copying PID file to perm storage (shouldn't be any...) |
276 | - command = "rsync -a --exclude postmaster.pid {}/ {}/".format( |
277 | - data_directory_path, new_pg_version_cluster_dir) |
278 | - log("run: {}".format(command)) |
279 | - run(command) |
280 | - try: |
281 | - os.rename(data_directory_path, "{}-{}".format( |
282 | - data_directory_path, int(time.time()))) |
283 | - log("NOTICE: symlinking {} -> {}".format( |
284 | - new_pg_version_cluster_dir, data_directory_path)) |
285 | - os.symlink(new_pg_version_cluster_dir, data_directory_path) |
286 | - log( |
287 | - "after-symlink: to fix/avoid UID changes from " |
288 | - "previous units, doing: " |
289 | - "chown -R postgres:postgres {}".format(new_pg_dir)) |
290 | - run("chown -R postgres:postgres {}".format(new_pg_dir)) |
291 | - return True |
292 | - except OSError: |
293 | - log("failed to symlink {} -> {}".format( |
294 | - data_directory_path, mount_point), CRITICAL) |
295 | - return False |
296 | - else: |
297 | - log( |
298 | - "Invalid volume storage configuration, not applying changes", |
299 | - ERROR) |
300 | - return False |
301 | + |
302 | + if not os.path.exists(data_directory_path): |
303 | + log( |
304 | + "postgresql data dir {} not found, " |
305 | + "not applying changes.".format(data_directory_path), |
306 | + CRITICAL) |
307 | + return False |
308 | + |
309 | + new_pg_dir = os.path.join(mount_point, "postgresql") |
310 | + new_pg_version_cluster_dir = os.path.join( |
311 | + new_pg_dir, version, cluster_name) |
312 | + if not mount_point: |
313 | + log( |
314 | + "invalid mount point = {}, " |
315 | + "not applying changes.".format(mount_point), ERROR) |
316 | + return False |
317 | + |
318 | + if ((os.path.islink(data_directory_path) and |
319 | + os.readlink(data_directory_path) == new_pg_version_cluster_dir and |
320 | + os.path.isdir(new_pg_version_cluster_dir))): |
321 | + log( |
322 | + "postgresql data dir '{}' already points " |
323 | + "to {}, skipping storage changes.".format( |
324 | + data_directory_path, new_pg_version_cluster_dir)) |
325 | + log( |
326 | + "existing-symlink: to fix/avoid UID changes from " |
327 | + "previous units, doing: " |
328 | + "chown -R postgres:postgres {}".format(new_pg_dir)) |
329 | + run("chown -R postgres:postgres %s" % new_pg_dir) |
330 | + return True |
331 | + |
332 | + # Create a directory structure below "new" mount_point as |
333 | + # external_volume_mount/postgresql/9.1/main |
334 | + for new_dir in [new_pg_dir, |
335 | + os.path.join(new_pg_dir, version), |
336 | + new_pg_version_cluster_dir]: |
337 | + if not os.path.isdir(new_dir): |
338 | + log("mkdir %s".format(new_dir)) |
339 | + host.mkdir(new_dir, owner="postgres", perms=0o700) |
340 | + # Carefully build this symlink, e.g.: |
341 | + # /var/lib/postgresql/9.1/main -> |
342 | + # external_volume_mount/postgresql/9.1/main |
343 | + # but keep previous "main/" directory, by renaming it to |
344 | + # main-$TIMESTAMP |
345 | + if not postgresql_stop() and postgresql_is_running(): |
346 | + log("postgresql_stop() failed - can't migrate data.", ERROR) |
347 | + return False |
348 | + if not os.path.exists(os.path.join( |
349 | + new_pg_version_cluster_dir, "PG_VERSION")): |
350 | + log("migrating PG data {}/ -> {}/".format( |
351 | + data_directory_path, new_pg_version_cluster_dir), WARNING) |
352 | + # void copying PID file to perm storage (shouldn't be any...) |
353 | + command = "rsync -a --exclude postmaster.pid {}/ {}/".format( |
354 | + data_directory_path, new_pg_version_cluster_dir) |
355 | + log("run: {}".format(command)) |
356 | + run(command) |
357 | + try: |
358 | + os.rename(data_directory_path, "{}-{}".format( |
359 | + data_directory_path, int(time.time()))) |
360 | + log("NOTICE: symlinking {} -> {}".format( |
361 | + new_pg_version_cluster_dir, data_directory_path)) |
362 | + os.symlink(new_pg_version_cluster_dir, data_directory_path) |
363 | + run("chown -h postgres:postgres {}".format(data_directory_path)) |
364 | + log( |
365 | + "after-symlink: to fix/avoid UID changes from " |
366 | + "previous units, doing: " |
367 | + "chown -R postgres:postgres {}".format(new_pg_dir)) |
368 | + run("chown -R postgres:postgres {}".format(new_pg_dir)) |
369 | + return True |
370 | + except OSError: |
371 | + log("failed to symlink {} -> {}".format( |
372 | + data_directory_path, mount_point), CRITICAL) |
373 | + return False |
374 | |
375 | |
376 | def token_sql_safe(value): |
377 | @@ -947,30 +848,15 @@ |
378 | |
379 | |
380 | @hooks.hook() |
381 | -def config_changed(force_restart=False): |
382 | +def config_changed(force_restart=False, mount_point=None): |
383 | validate_config() |
384 | config_data = hookenv.config() |
385 | update_repos_and_packages() |
386 | |
387 | - # Trigger volume initialization logic for permanent storage |
388 | - volid = volume_get_volume_id() |
389 | - if not volid: |
390 | - ## Invalid configuration (whether ephemeral, or permanent) |
391 | - postgresql_autostart(False) |
392 | - postgresql_stop() |
393 | - mounts = volume_get_all_mounted() |
394 | - if mounts: |
395 | - log("current mounted volumes: {}".format(mounts)) |
396 | - log( |
397 | - "Disabled and stopped postgresql service, " |
398 | - "because of broken volume configuration - check " |
399 | - "'volume-ephemeral-storage' and 'volume-map'", ERROR) |
400 | - sys.exit(1) |
401 | - |
402 | - if volume_is_permanent(volid): |
403 | - ## config_changed_volume_apply will stop the service if it founds |
404 | + if mount_point is not None: |
405 | + ## config_changed_volume_apply will stop the service if it finds |
406 | ## it necessary, ie: new volume setup |
407 | - if config_changed_volume_apply(): |
408 | + if config_changed_volume_apply(mount_point=mount_point): |
409 | postgresql_autostart(True) |
410 | else: |
411 | postgresql_autostart(False) |
412 | @@ -1084,8 +970,89 @@ |
413 | |
414 | @hooks.hook() |
415 | def upgrade_charm(): |
416 | + """Handle saving state during an upgrade-charm hook. |
417 | + |
418 | + When upgrading from an installation using volume-map, we migrate |
419 | + that installation to use the storage subordinate charm by remounting |
420 | + a mountpath that the storage subordinate maintains. We exit(1) only to |
421 | + raise visibility to manual procedure that we log in juju logs below for the |
422 | + juju admin to finish the migration by relating postgresql to the storage |
423 | + and block-storage-broker services. These steps are generalised in the |
424 | + README as well. |
425 | + """ |
426 | install(run_pre=False) |
427 | snapshot_relations() |
428 | + version = pg_version() |
429 | + cluster_name = hookenv.config('cluster_name') |
430 | + data_directory_path = os.path.join( |
431 | + postgresql_data_dir, version, cluster_name) |
432 | + if (os.path.islink(data_directory_path)): |
433 | + link_target = os.readlink(data_directory_path) |
434 | + if "/srv/juju" in link_target: |
435 | + # Then we just upgraded from an installation that was using |
436 | + # charm config volume_map definitions. We need to stop postgresql |
437 | + # and remount the device where the storage subordinate expects to |
438 | + # control the mount in the future if relations/units change |
439 | + volume_id = link_target.split("/")[3] |
440 | + unit_name = hookenv.local_unit() |
441 | + new_mount_root = external_volume_mount |
442 | + new_pg_version_cluster_dir = os.path.join( |
443 | + new_mount_root, "postgresql", version, cluster_name) |
444 | + if not os.exists(new_mount_root): |
445 | + os.mkdir(new_mount_root) |
446 | + log("\n" |
447 | + "WARNING: %s unit has external volume id %s mounted via the\n" |
448 | + "deprecated volume-map and volume-ephemeral-storage\n" |
449 | + "configuration parameters.\n" |
450 | + "These parameters are no longer available in the postgresql\n" |
451 | + "charm in favor of using the volume_map parameter in the\n" |
452 | + "storage subordinate charm.\n" |
453 | + "We are migrating the attached volume to a mount path which\n" |
454 | + "can be managed by the storage subordinate charm. To\n" |
455 | + "continue using this volume_id with the storage subordinate\n" |
456 | + "follow this procedure.\n-----------------------------------\n" |
457 | + "1. cat > storage.cfg <<EOF\nstorage:\n" |
458 | + " provider: block-storage-broker\n" |
459 | + " root: %s\n" |
460 | + " volume_map: \"{%s: %s}\"\nEOF\n2. juju deploy " |
461 | + "--config storage.cfg storage\n" |
462 | + "3. juju deploy block-storage-broker\n4. juju add-relation " |
463 | + "block-storage-broker storage\n5. juju resolved --retry " |
464 | + "%s\n6. juju add-relation postgresql storage\n" |
465 | + "-----------------------------------\n" % |
466 | + (unit_name, volume_id, new_mount_root, unit_name, volume_id, |
467 | + unit_name), WARNING) |
468 | + postgresql_stop() |
469 | + os.unlink(data_directory_path) |
470 | + log("Unmounting external storage due to charm upgrade: %s" % |
471 | + link_target) |
472 | + try: |
473 | + subprocess.check_output( |
474 | + "umount /srv/juju/%s" % volume_id, shell=True) |
475 | + # Since e2label truncates labels to 16 characters use only the |
476 | + # first 16 characters of the volume_id as that's what was |
477 | + # set by old versions of postgresql charm |
478 | + subprocess.check_call( |
479 | + "mount -t ext4 LABEL=%s %s" % |
480 | + (volume_id[:16], new_mount_root), shell=True) |
481 | + except subprocess.CalledProcessError, e: |
482 | + log("upgrade-charm mount migration failed. %s" % str(e), ERROR) |
483 | + sys.exit(1) |
484 | + |
485 | + log("NOTICE: symlinking {} -> {}".format( |
486 | + new_pg_version_cluster_dir, data_directory_path)) |
487 | + os.symlink(new_pg_version_cluster_dir, data_directory_path) |
488 | + run("chown -h postgres:postgres {}".format(data_directory_path)) |
489 | + postgresql_start() # Will exit(1) if issues |
490 | + log("Remount and restart success for this external volume.\n" |
491 | + "This current running installation will break upon\n" |
492 | + "add/remove postgresql units or relations if you do not\n" |
493 | + "follow the above procedure to ensure your external\n" |
494 | + "volumes are preserved by the storage subordinate charm.", |
495 | + WARNING) |
496 | + # So juju admins can see the hook fail and note the steps to fix |
497 | + # per our WARNINGs above |
498 | + sys.exit(1) |
499 | |
500 | |
501 | @hooks.hook() |
502 | @@ -2194,6 +2161,32 @@ |
503 | host.service_reload('nagios-nrpe-server') |
504 | |
505 | |
506 | +@hooks.hook('data-relation-changed') |
507 | +def data_relation_changed(): |
508 | + """Listen for configured mountpoint from storage subordinate relation""" |
509 | + if not hookenv.relation_get("mountpoint"): |
510 | + hookenv.log("Waiting for mountpoint from the relation: %s" |
511 | + % external_volume_mount, hookenv.DEBUG) |
512 | + else: |
513 | + hookenv.log("Storage ready and mounted", hookenv.DEBUG) |
514 | + config_changed(mount_point=external_volume_mount) |
515 | + |
516 | + |
517 | +@hooks.hook('data-relation-joined') |
518 | +def data_relation_joined(): |
519 | + """Request mountpoint from storage subordinate by setting mountpoint""" |
520 | + hookenv.log("Setting mount point in the relation: %s" |
521 | + % external_volume_mount, hookenv.DEBUG) |
522 | + hookenv.relation_set(mountpoint=external_volume_mount) |
523 | + |
524 | + |
525 | +@hooks.hook('data-relation-departed') |
526 | +def stop_postgres_on_data_relation_departed(): |
527 | + hookenv.log("Data relation departing. Stopping PostgreSQL", |
528 | + hookenv.DEBUG) |
529 | + postgresql_stop() |
530 | + |
531 | + |
532 | def _get_postgresql_config_dir(config_data=None): |
533 | """ Return the directory path of the postgresql configuration files. """ |
534 | if config_data is None: |
535 | @@ -2202,6 +2195,7 @@ |
536 | cluster_name = config_data['cluster_name'] |
537 | return os.path.join("/etc/postgresql", version, cluster_name) |
538 | |
539 | + |
540 | ############################################################################### |
541 | # Global variables |
542 | ############################################################################### |
543 | @@ -2216,6 +2210,7 @@ |
544 | local_state = State('local_state.pickle') |
545 | hook_name = os.path.basename(sys.argv[0]) |
546 | juju_log_dir = "/var/log/juju" |
547 | +external_volume_mount = "/srv/data" |
548 | |
549 | |
550 | if __name__ == '__main__': |
551 | |
552 | === modified file 'hooks/test_hooks.py' |
553 | --- hooks/test_hooks.py 2014-01-08 08:55:50 +0000 |
554 | +++ hooks/test_hooks.py 2014-02-28 23:23:59 +0000 |
555 | @@ -43,9 +43,23 @@ |
556 | certain data is set. |
557 | """ |
558 | |
559 | - _relation_data = {} |
560 | + _incoming_relation_data = () |
561 | + _outgoing_relation_data = () |
562 | _relation_ids = {} |
563 | _relation_list = ("postgres/0",) |
564 | + _log = () |
565 | + |
566 | + _log_DEBUG = () |
567 | + _log_INFO = () |
568 | + _log_WARNING = () |
569 | + _log_ERROR = () |
570 | + _log_CRITICAL = () |
571 | + |
572 | + DEBUG = "DEBUG" |
573 | + INFO = "INFO" |
574 | + WARNING = "WARNING" |
575 | + ERROR = "ERROR" |
576 | + CRITICAL = "CRITICAL" |
577 | |
578 | def __init__(self): |
579 | self._config = { |
580 | @@ -95,27 +109,30 @@ |
581 | "wal_buffers": "-1", |
582 | "checkpoint_segments": 3, |
583 | "random_page_cost": 4.0, |
584 | - "volume_ephemeral_storage": True, |
585 | "volume_map": "", |
586 | "volume_dev_regexp": "/dev/db[b-z]", |
587 | "backup_dir": "/var/lib/postgresql/backups", |
588 | "backup_schedule": "13 4 * * *", |
589 | "backup_retention_count": 7, |
590 | "nagios_context": "juju", |
591 | + "pgdg": False, |
592 | + "install_sources": "", |
593 | + "install_keys": "", |
594 | "extra_archives": "", |
595 | "advisory_lock_restart_key": 765} |
596 | |
597 | def relation_set(self, *args, **kwargs): |
598 | """ |
599 | - Capture result of relation_set into _relation_data, which |
600 | + Capture result of relation_set into _outgoing_relation_data, which |
601 | can then be checked later. |
602 | """ |
603 | if "relation_id" in kwargs: |
604 | del kwargs["relation_id"] |
605 | - self._relation_data = dict(self._relation_data, **kwargs) |
606 | + |
607 | for arg in args: |
608 | (key, value) = arg.split("=") |
609 | - self._relation_data[key] = value |
610 | + self._outgoing_relation_data = ( |
611 | + self._outgoing_relation_data + ((key, value),)) |
612 | |
613 | def relation_ids(self, relation_name="db-admin"): |
614 | """ |
615 | @@ -156,17 +173,24 @@ |
616 | def juju_log(self, *args, **kwargs): |
617 | pass |
618 | |
619 | - def log(self, *args, **kwargs): |
620 | - pass |
621 | + def log(self, message, level=None): |
622 | + if level is None: |
623 | + level = self.INFO |
624 | + log = getattr(self, "_log_%s" % level) |
625 | + setattr(self, "_log_%s" % level, log + (message,)) |
626 | |
627 | - def config_get(self, scope=None): |
628 | + def config(self, scope=None): |
629 | if scope is None: |
630 | - return self.config |
631 | + return self._config |
632 | else: |
633 | - return self.config[scope] |
634 | + return self._config[scope] |
635 | |
636 | def relation_get(self, scope=None, unit_name=None, relation_id=None): |
637 | - pass |
638 | + if scope: |
639 | + for (key, value) in self._incoming_relation_data: |
640 | + if key == scope: |
641 | + return value |
642 | + return None |
643 | |
644 | |
645 | class TestHooks(mocker.MockerTestCase): |
646 | @@ -175,8 +199,6 @@ |
647 | hooks.hookenv = TestJuju() |
648 | hooks.host = TestJujuHost() |
649 | hooks.juju_log_dir = self.makeDir() |
650 | - hooks.hookenv.config = lambda: hooks.hookenv._config |
651 | - #hooks.hookenv.localunit = lambda: "localhost" |
652 | hooks.os.environ["JUJU_UNIT_NAME"] = "landscape/1" |
653 | hooks.os.environ["CHARM_DIR"] = os.path.abspath( |
654 | os.path.join(os.path.dirname(__file__), os.pardir)) |
655 | @@ -210,6 +232,65 @@ |
656 | |
657 | class TestHooksService(TestHooks): |
658 | |
659 | + def test_data_relation_departed_stops_postgresql(self): |
660 | + """ |
661 | + When the storage subordinate charm relation departs firing the |
662 | + C{data-relation-departed} hook, the charm stops the postgresql service |
663 | + and logs a message. |
664 | + """ |
665 | + postgresql_stop = self.mocker.replace(hooks.postgresql_stop) |
666 | + postgresql_stop() |
667 | + self.mocker.replay() |
668 | + hooks.stop_postgres_on_data_relation_departed() |
669 | + message = "Data relation departing. Stopping PostgreSQL" |
670 | + self.assertIn( |
671 | + message, hooks.hookenv._log_DEBUG, "Not logged- %s" % message) |
672 | + |
673 | + def test_data_relation_joined_requests_configured_mountpoint(self): |
674 | + """ |
675 | + When postgresql is related to the storage subordinate charm via the |
676 | + 'data' relation it will read the configured C{storage_mount_point} and |
677 | + set C{mountpoint} in the relation in order to request a specific |
678 | + mountpoint from the storage charm. |
679 | + """ |
680 | + mount = hooks.external_volume_mount |
681 | + hooks.data_relation_joined() |
682 | + message = "Setting mount point in the relation: %s" % mount |
683 | + self.assertIn( |
684 | + message, hooks.hookenv._log_DEBUG, "Not logged- %s" % message) |
685 | + |
686 | + def test_data_relation_changed_waits_for_data_relation_mountpoint(self): |
687 | + """ |
688 | + C{data_relation_changed} will wait for the storage charm to respond |
689 | + with the properly configured C{mountpoint} in the 'data' relation |
690 | + before calling C{config_changed}. |
691 | + """ |
692 | + mount = hooks.external_volume_mount |
693 | + hooks.hookenv._config["storage_mount_point"] = mount |
694 | + self.assertEqual(hooks.hookenv._incoming_relation_data, ()) |
695 | + hooks.data_relation_changed() |
696 | + message = "Waiting for mountpoint from the relation: %s" % mount |
697 | + self.assertIn( |
698 | + message, hooks.hookenv._log_DEBUG, "Not logged- %s" % message) |
699 | + |
700 | + def test_data_relation_changed_mountpoint_present(self): |
701 | + """ |
702 | + C{data_relation_changed} will call C{config_changed} when it receives |
703 | + the successfuly mounted C{mountpoint} from storage charm. |
704 | + """ |
705 | + mount = hooks.external_volume_mount |
706 | + self.addCleanup( |
707 | + setattr, hooks.hookenv, "_incoming_relation_data", ()) |
708 | + hooks.hookenv._incoming_relation_data = (("mountpoint", mount),) |
709 | + config_changed = self.mocker.replace(hooks.config_changed) |
710 | + config_changed(mount_point=mount) |
711 | + self.mocker.replay() |
712 | + |
713 | + hooks.data_relation_changed() |
714 | + message = "Storage ready and mounted" |
715 | + self.assertIn( |
716 | + message, hooks.hookenv._log_DEBUG, "Not logged- %s" % message) |
717 | + |
718 | def test_create_postgresql_config_wal_no_replication(self): |
719 | """ |
720 | When postgresql is in C{standalone} mode, and participates in no |
721 | |
722 | === modified file 'metadata.yaml' |
723 | --- metadata.yaml 2013-10-08 22:44:28 +0000 |
724 | +++ metadata.yaml 2014-02-28 23:23:59 +0000 |
725 | @@ -23,6 +23,10 @@ |
726 | nrpe-external-master: |
727 | interface: nrpe-external-master |
728 | scope: container |
729 | + data: |
730 | + interface: block-storage |
731 | + scope: container |
732 | + optional: true |
733 | requires: |
734 | persistent-storage: |
735 | interface: directory-path |
736 | |
737 | === removed directory 'scripts' |
738 | === removed file 'scripts/volume-common.sh' |
739 | --- scripts/volume-common.sh 2012-10-23 09:44:43 +0000 |
740 | +++ scripts/volume-common.sh 1970-01-01 00:00:00 +0000 |
741 | @@ -1,220 +0,0 @@ |
742 | -#!/bin/bash |
743 | -# Author: JuanJo Ciarlante <jjo@canonical.com> |
744 | -# Copyright: Canonical Ltd. 2012 |
745 | -# License: GPLv2 |
746 | -# |
747 | -# juju storage common shell library |
748 | -# |
749 | - |
750 | -#------------------------------ |
751 | -# Returns a mount point from passed vol-id, e.g. /srv/juju/vol-000012345 |
752 | -# |
753 | -# @param $1 volume id |
754 | -# @echoes mntpoint-path eg /srv/juju/vol-000012345 |
755 | -#------------------------------ |
756 | -_mntpoint_from_volid() { |
757 | - local volid=${1?missing volid} |
758 | - [[ ${volid} != "" ]] && echo /srv/juju/${volid} || echo "" |
759 | -} |
760 | - |
761 | - |
762 | -#------------------------------ |
763 | -# Assert that passed mount points hold different filesystems |
764 | -# |
765 | -# @param $1 mntpoint1 |
766 | -# @param $2 mntpoint2 |
767 | -# @return 0 different FS |
768 | -# 1 same FS |
769 | -#------------------------------ |
770 | -_assert_diff_fs() { |
771 | - local mnt1="${1:?missing mntpoint1}" |
772 | - local mnt2="${2:?missing mntpoint2}" |
773 | - local fsid1 fsid2 |
774 | - fsid1=$(stat --file-system -c '%i' "${mnt1}" 2>/dev/null) |
775 | - fsid2=$(stat --file-system -c '%i' "${mnt2}" 2>/dev/null) |
776 | - [[ ${fsid1} != ${fsid2} ]] |
777 | - return $? |
778 | -} |
779 | - |
780 | -#------------------------------ |
781 | -# Initialize volume (sfdisk, mkfs.ext4) IFF NOT already, mount it at |
782 | -# /srv/juju/<volume-id> |
783 | -# |
784 | -# @param $1 volume-id, can be any arbitrary string, better if |
785 | -# equal to EC2/OS vol-id name (just for consistency) |
786 | -# @return 0 success |
787 | -# 1 nil volid/etc |
788 | -# 2 error while handling the device (non-block device, sfdisk error, etc) |
789 | -#------------------------------ |
790 | -volume_init_and_mount() { |
791 | - ## Find 1st unused device (reverse sort /dev/vdX) |
792 | - local volid=${1:?missing volid} |
793 | - local dev_regexp |
794 | - local dev found_dev= |
795 | - local label="${volid}" |
796 | - local func=${FUNCNAME[0]} |
797 | - dev_regexp=$(config-get volume-dev-regexp) || return 1 |
798 | - mntpoint=$(_mntpoint_from_volid ${volid}) |
799 | - |
800 | - [[ -z ${mntpoint} ]] && return 1 |
801 | - if mount | egrep -qw "${mntpoint}";then |
802 | - _assert_diff_fs "/" "${mntpoint}" || { |
803 | - juju-log "ERROR: returning from ${func} with '${mntpoint}' still at '/' filesystem" |
804 | - return 1 |
805 | - } |
806 | - juju-log "NOTICE: mntpoint=${mntpoint} already mounted, skipping volume_init_and_mount" |
807 | - return 0 |
808 | - fi |
809 | - |
810 | - # Sanitize |
811 | - case "${dev_regexp?}" in |
812 | - # Careful: this is glob matching against an regexp - |
813 | - # quite narrowed |
814 | - /dev/*|/dev/disk/by-*) |
815 | - ;; ## Ok |
816 | - *) |
817 | - juju-log "ERROR: invalid 'volume-dev-regexp' specified" |
818 | - return 1 |
819 | - ;; |
820 | - esac |
821 | - |
822 | - # Assume udev will create only existing devices |
823 | - for dev in $(ls -rd1 /dev/* | egrep "${dev_regexp}" | egrep -v "[1-9]$" 2>/dev/null);do |
824 | - ## Check it's not already mounted |
825 | - mount | egrep -q "${dev}[1-9]?" || { found_dev=${dev}; break;} |
826 | - done |
827 | - [[ -n "${found_dev}" ]] || { |
828 | - juju-log "ERROR: ${func}: coult not find an unused device for regexp: ${dev_regexp}" |
829 | - return 1 |
830 | - } |
831 | - partition1_dev=${found_dev}1 |
832 | - |
833 | - juju-log "INFO: ${func}: found_dev=${found_dev}" |
834 | - [[ -b ${found_dev?} ]] || { |
835 | - juju-log "ERROR: ${func}: ${found_dev} is not a blockdevice" |
836 | - return 2 |
837 | - } |
838 | - |
839 | - # Run next set of "dangerous" commands as 'set -e', in a subshell |
840 | - ( |
841 | - set -e |
842 | - # Re-read partition - will fail if already in use |
843 | - blockdev --rereadpt ${found_dev} |
844 | - |
845 | - # IFF not present, create partition with full disk |
846 | - if [[ -b ${partition1_dev?} ]];then |
847 | - juju-log "INFO: ${func}: ${partition1_dev} already present - skipping sfdisk." |
848 | - else |
849 | - juju-log "NOTICE: ${func}: ${partition1_dev} not present at ${found_dev}, running: sfdisk ${found_dev} ..." |
850 | - # Format partition1_dev as max sized |
851 | - echo ",+," | sfdisk ${found_dev} |
852 | - fi |
853 | - |
854 | - # Create an ext4 filesystem if NOT already present |
855 | - # use e.g. LABEl=vol-000012345 |
856 | - if file -s ${partition1_dev} | egrep -q ext4 ; then |
857 | - juju-log "INFO: ${func}: ${partition1_dev} already formatted as ext4 - skipping mkfs.ext4." |
858 | - ## Check e2label - log if it has changed (e.g. already used / initialized with a diff label) |
859 | - local curr_label=$(e2label "${partition1_dev}") |
860 | - if [[ ${curr_label} != ${label} ]]; then |
861 | - juju-log "WARNING: ${func}: ${partition1_dev} had label=${curr_label}, overwritting with label=${label}" |
862 | - e2label ${partition1_dev} "${label}" |
863 | - fi |
864 | - else |
865 | - juju-log "NOTICE: ${func}: running: mkfs.ext4 -L ${label} ${partition1_dev}" |
866 | - mkfs.ext4 -L "${label}" ${partition1_dev} |
867 | - fi |
868 | - |
869 | - # Mount it at e.g. /srv/juju/vol-000012345 |
870 | - [[ -d "${mntpoint}" ]] || mkdir -p "${mntpoint}" |
871 | - mount | fgrep -wq "${partition1_dev}" || { |
872 | - local files_below_mntpoint="$(ls -d "${mntpoint}"/* 2>/dev/null |wc -l )" |
873 | - if [[ ${files_below_mntpoint} -ne 0 ]]; then |
874 | - juju-log "ERROR: *not* doing 'mount "${partition1_dev}" "${mntpoint}"' because there are already ${files_below_mntpoint} files/dirs beneath '${mntpoint}'" |
875 | - exit 1 |
876 | - fi |
877 | - ## should always fsck before mounting (e.g. fsck after max time (-i) / max mounts (-c) ) |
878 | - fsck "${partition1_dev}" |
879 | - mount "${partition1_dev}" "${mntpoint}" |
880 | - juju-log "INFO: ${func}: mounted as: '$(mount | fgrep -w ${partition1_dev})'" |
881 | - } |
882 | - |
883 | - # Add it to fstab is not already there |
884 | - fgrep -wq "LABEL=${label}" /etc/fstab || { |
885 | - echo "LABEL=${label} ${mntpoint} ext4 defaults,nobootwait,comment=${volid}" | tee -a /etc/fstab |
886 | - juju-log "INFO: ${func}: LABEL=${label} added to /etc/fstab" |
887 | - } |
888 | - ) |
889 | - # Final assertion: mounted filesystem id is different from '/' (effectively mounted) |
890 | - _assert_diff_fs "/" "${mntpoint}" || { |
891 | - juju-log "ERROR: returning from ${func} with '${mntpoint}' still at '/' filesystem (couldn't mount new volume)" |
892 | - ## try to rmdir mntpoint directory - should not be 'mistakenly' used |
893 | - rmdir ${mntpoint} |
894 | - return 1 |
895 | - } |
896 | - return $? |
897 | -} |
898 | - |
899 | -#------------------------------ |
900 | -# Get volume-id from juju config "volume-map" dictionary as |
901 | -# volume-map[JUJU_UNIT_NAME] |
902 | -# @return 0 if volume-map value found ( does echo volid or ""), else: |
903 | -# 1 if not found or None |
904 | -# |
905 | -#------------------------------ |
906 | -volume_get_volid_from_volume_map() { |
907 | - 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"])') |
908 | - [[ $volid == None ]] && return 1 |
909 | - echo "$volid" |
910 | -} |
911 | - |
912 | -# Returns true if permanent storage (considers --ephemeral) |
913 | -# @returns 0 if volid set and not --ephemeral, else: |
914 | -# 1 |
915 | -volume_is_permanent() { |
916 | - local volid=${1:?missing volid} |
917 | - [[ -n ${volid} && ${volid} != --ephemeral ]] && return 0 || return 1 |
918 | -} |
919 | -volume_mount_point_from_volid(){ |
920 | - local volid=${1:?missing volid} |
921 | - if volume_is_permanent;then |
922 | - echo "/srv/juju/${volid}" |
923 | - return 0 |
924 | - else |
925 | - return 1 |
926 | - fi |
927 | -} |
928 | -# Do we have a valid storage state? |
929 | -# @returns 0 does echo $volid (can be "--ephemeral") |
930 | -# 1 config state is invalid - we should not serve |
931 | -volume_get_volume_id() { |
932 | - local ephemeral_storage |
933 | - local volid |
934 | - ephemeral_storage=$(config-get volume-ephemeral-storage) || return 1 |
935 | - volid=$(volume_get_volid_from_volume_map) || return 1 |
936 | - if [[ $ephemeral_storage == True ]];then |
937 | - # Ephemeral -> should not have a valid volid |
938 | - if [[ $volid != "" ]];then |
939 | - juju-log "ERROR: volume-ephemeral-storage is True, but $JUJU_UNIT_NAME maps to volid=${volid}" |
940 | - return 1 |
941 | - fi |
942 | - else |
943 | - # Durable (not ephemeral) -> must have a valid volid for this unit |
944 | - if [[ $volid == "" ]];then |
945 | - juju-log "ERROR: volume-ephemeral-storage is False, but no volid found for: $JUJU_UNIT_NAME" |
946 | - return 1 |
947 | - fi |
948 | - fi |
949 | - echo "$volid" |
950 | - return 0 |
951 | -} |
952 | - |
953 | -case "$1" in |
954 | - ## allow non SHELL scripts to call helper functions |
955 | - call) |
956 | - : ${JUJU_UNIT_NAME?} ## Must be called in juju environment |
957 | - shift; |
958 | - function="${1:?usage: ${0##*/} call function arg1 arg2 ...}" |
959 | - shift; |
960 | - ${function} "$@" && exit 0 || exit 1 |
961 | -esac |
On 15 February 2014 06:34, Chad Smith <email address hidden> wrote:
> There is a new storage subordinate charm that has incorporated all storage mount,fsck and partitioning that the postgresql charm performed as well as talking to the block-storage- broker charm to automatically create, attach. reattach or detach nova volumes or nfs shares on behalf of your principal postgresql units.
>
> The storage subordinate attempts to avoid duplication of partitioning, mounting and attaching devices in multiple charms by coalescing all that work in one charm. Only thing that the principal (postgresql in this case) has to provide is a preferred mountpoint to the storage subordinate.
Thanks for this. I think this is a good approach.
How is this branch handing upgrading of existing installation? Do we
add a subordinate using the existing disk and mount point, or do we
add a subordinate to use a new mount and migrate data from old-mount
as if it was local disk?
I do think this branch does not go far enough. I would like to see
more storage related code pushed into the subordinate. I believe that
with juju-run (in stable in ~2 weeks I'm told), we can remove *all*
storage related code from the parent charm, lowering the bar or
removing the barrier entirely for charms to use the subordinate
storage service.
For example, the PostgreSQL charm could do nothing except define a relation- joined hook that simply does:
storage-
relation-set storage- path=/var/ lib/postgresql/ 9.1/main
The subordinate charm would be responsible for:
create storage storage- tmp storage- tmp storage- tmp path.juju- `date`
mount storage on /mnt/juju-
juju-run parent-unit hooks/stop
rsync -a $storage-path /mnt/juju-
unmount /mnt/juju-
mv $storage-path $storage-
mount storage on $storage-path
juju-run parent-unit hooks/start
Perhaps start/stop isn't enough and we need support in the parent relation- joined and storage- relation- changed hooks to
charm's storage-
disable the service and reenable it when done (eg. if
db-relation-joined hooks get fired while the subordinate is being
setup, PG could get restarted at the wrong time).
If this was possible, we could add multiple subordinates easily to the
primary service. For example, 1 for the main database and additional
ones for any tablespaces that have been created and are being used.
> === modified file '.bzrignore'
> --- .bzrignore 2012-10-30 16:41:12 +0000
> +++ .bzrignore 2014-02-13 02:24:52 +0000
> @@ -0,0 +1,1 @@
> +hooks/_trial_temp
This is already in place on trunk. There is at least one change on
trunk that affects this work, which I'll comment on later.
> === modified file 'config.yaml' ------- ------- ------- ------- ------- ------- ------- ------- ------- --- ephemeral- storage == False ------- ------- ------- ------- ------- ------- ------- ------- ------- --- ephemeral- storage:
> --- config.yaml 2014-01-22 07:09:38 +0000
> +++ config.yaml 2014-02-13 02:24:52 +0000
> @@ -277,36 +277,6 @@
> default: 4.0
> type: float
> description: Random page cost
> - #------
> - # Volume management
> - # volume-map, volume-dev_regexp are only used
> - # if volume-
> - #------
> - volume-
We need to su...