From b497f4de1ea2658df3ba39e979ddae014a75980d Mon Sep 17 00:00:00 2001 From: Chris MacNaughton Date: Mon, 21 Mar 2016 16:51:09 -0400 Subject: [PATCH] Merge ceph charm into ceph-mon Squashed commit of the following: commit 9b832d9391f9fea2d1491d01da6101585930fc75 Merge: e2432c4 7b36210 Author: Chris MacNaughton Date: Mon Mar 21 16:40:54 2016 -0400 Merge branch 'master' of github.com:openstack/charm-ceph into charm-ceph-mon Change-Id: I42cfe6f1e5887627981f8ce4beff164803cc3957 commit 7b36210bac5bef3bacae2614995e123ef926453f Author: Chris Holcombe Date: Fri Mar 18 15:37:06 2016 -0700 Add ceph-osd to ceph This change adds ceph-osd back into ceph for amulet testing. Change-Id: Ice4aaf7739e8c839189313d3f6175a834cf64219 commit e87e0b7bd22fe5ccae2aafcf6bd30f145405e01b Author: Ryan Beisner Date: Wed Mar 16 17:33:48 2016 +0000 Update amulet test to include a non-existent osd-devices value The osd-devices charm config option is a whitelist, and the charm needs to gracefully handle items in that whitelist which may not exist. Change-Id: I5f9c6c1e4519fd671d6d36b415c9c8f763495dad commit ffce15d52333de4063d04b808cfbca5d890fb996 Merge: fe8bf6e 9614896 Author: Jenkins Date: Wed Mar 16 17:45:25 2016 +0000 Merge "Revert "Make 'blocked' status when node have no storage device"" commit 961489609d85851bd63c6825339a296bdf74e320 Author: Chris Holcombe Date: Wed Mar 16 16:55:02 2016 +0000 Revert "Make 'blocked' status when node have no storage device" This reverts commit fc04dd0fff33639b812627d04645134dd7d4d3de. Change-Id: I9efbf623fc9aa6096725a15e53df426739ac16ff commit fe8bf6e4a5cb466a5efc6403c215e7aece2c6b9c Author: Billy Olsen Date: Tue Mar 15 20:08:20 2016 -0700 Use tox in Makefile targets Modify the Makefile to point at the appropriate tox targets so that tox and Make output can be equivalent. This involves mapping the lint target to the pep8 target and the test target to the py27 target. Change-Id: I99761d2fdf120bacff58d0aa5c2e584382c2e72b commit fc04dd0fff33639b812627d04645134dd7d4d3de Author: Seyeong Kim Date: Fri Mar 11 06:07:52 2016 +0000 Make 'blocked' status when node have no storage device Currently there is an msg for no storage status on ceph node. But it doesn't make this charm state 'blocked'. is_storage_fine function has been created to check storage devices on ceph_hooks.py and using it on assess_status. Change-Id: I790fde0280060fa220ee83de2ad2319ac2c77230 Closes-Bug: lp1424510 commit a7c5e85c408ab8446a18cc6761b1d0b292641ea7 Author: Ryan Beisner Date: Fri Mar 4 14:36:38 2016 +0000 Enable Xenial-Mitaka amulet test target. Change-Id: I0c386fc0c052cc1ac52c0a30f7a39fa914a61100 commit e80c5097c26ac4eb200a289daa272d5c7ac82539 Author: uoscibot Date: Mon Feb 29 10:45:49 2016 +0000 Adapt imports and metadata for github move commit 391ed288fc763b69f0cd92459f236e7581a5f244 Merge: 78250bd 6228ea2 Author: Edward Hope-Morley Date: Thu Feb 25 13:34:27 2016 -0500 [hopem,r=] Support multiple l3 segments. Closes-Bug: 1523871 commit 6228ea2a8fa578c3c6b24b59f621e6e1026a7668 Merge: 6159390 78250bd Author: Edward Hope-Morley Date: Thu Feb 25 09:29:46 2016 -0500 sync /next commit 78250bd65c861adcb321f1c634def29fcfdaa8a9 Author: James Page Date: Wed Feb 24 21:53:28 2016 +0000 Add gitreview prior to migration to openstack commit 61593905939359ba72768ccb8f1a450a571c1d24 Author: Edward Hope-Morley Date: Wed Feb 24 15:56:20 2016 -0500 only use fallback for get_public_addr() if networks not provided in config commit 34841b0aea85b3d5693a5336dbf956a406414474 Merge: 08d1cbc 092368d Author: James Page Date: Wed Feb 24 14:22:20 2016 +0000 Add actions to support configuration of erasure coded pools. commit 092368d646d4e02b2d2ac08026b6cbf2c94a4042 Merge: de98010 08d1cbc Author: Chris Holcombe Date: Tue Feb 23 08:19:56 2016 -0800 Merge upstream commit 08d1cbcdc943493a556e0187d2b3e6fbe83b69e3 Merge: 2d4ff89 414e519 Author: James Page Date: Tue Feb 23 09:49:50 2016 +0000 Fix amulet tests for nova-compute changes. commit 414e5195c939a99adcaf79e27eb057c07c7f4761 Author: Edward Hope-Morley Date: Mon Feb 22 15:21:00 2016 -0500 fix amulet commit e99e991be21c6d98fc670bcafa30684c0ba4d5e0 Author: Edward Hope-Morley Date: Mon Feb 22 12:56:00 2016 -0500 fixup commit de98010f6f8d81e63d47ac03d33aa40bd870c7ea Author: Chris Holcombe Date: Mon Feb 22 08:05:32 2016 -0800 charmhelpers sync commit 2d4ff89e4bba2e93e08a6dd00bc2367e90b708fe Merge: f16e3fa f98627c Author: Liam Young Date: Mon Feb 22 09:26:38 2016 +0000 [james-page, r=gnuoy] Add configuration option for toggling use of direct io for OSD journals commit f3803cb60d55154e35ac2294170b27fb348141b3 Author: Chris Holcombe Date: Fri Feb 19 08:11:18 2016 -0800 Change /usr/bin/python2.7 to /usr/bin/python commit 612ba454c4263d9bfc672fe168a55c2f01599d70 Merge: c3d20a0 f16e3fa Author: Chris Holcombe Date: Thu Feb 18 17:16:55 2016 -0800 Merge upstream and resolve conflicts with actions and actions.yaml commit c3d20a0eb67918d11585851a7b5df55ce0290392 Author: Chris Holcombe Date: Thu Feb 18 17:10:56 2016 -0800 Fix up the niggles and provide feedback to the action user as to why something failed commit ea5cc48ccbb5d6515703bd5c93c13b2147972cd1 Author: Edward Hope-Morley Date: Thu Feb 18 17:42:05 2016 +0000 more commit f58dd864eac130a6bc20b46c1495d7fa34a54894 Author: Edward Hope-Morley Date: Thu Feb 18 17:09:52 2016 +0000 restore sanity commit 32631ccde309040b92ba76ecc12b16bad953f486 Author: Edward Hope-Morley Date: Thu Feb 18 11:40:09 2016 +0000 post-review fixes commit 7ada8f0de65d397648d041fae20ed21b3f38bd15 Author: Edward Hope-Morley Date: Thu Feb 18 11:36:46 2016 +0000 post-review fixes commit f16e3fac5240133c1c7dfd406caacd21b364532a Merge: a0ffb8b 7709b7d Author: James Page Date: Thu Feb 18 11:02:17 2016 +0000 Add pause/resume cluster health actions Add actions to pause and resume cluster health monitoring within ceph for all osd devices. This will ensure that no rebalancing is done whilst maintenance actions are happening within the cluster. commit a0ffb8bf97c9cf3c19d17090c96f2ea60c89da65 Merge: 65439ba 531b40d Author: James Page Date: Thu Feb 18 10:38:53 2016 +0000 Wait for quorom and query the right unit remote_unit when not in radosgw context commit 65439ba7dc3acf494c9a8d11e2cdd274d144b485 Merge: 5e77170 afd390b Author: James Page Date: Wed Feb 17 11:28:44 2016 +0000 Update test target definitions; Wait for unit status. commit 531b40d9b2d216b467cca59d7649ab5bb4577b3d Author: Liam Young Date: Wed Feb 17 10:15:37 2016 +0000 Wait for quorom and query the right unit remote_unit when not in radosgw context commit 5e77170f378be92a3e2e8de3c06dad158b4a14ca Author: James Page Date: Tue Feb 16 06:59:17 2016 +0000 Tidy tox targets commit 732d8e11cd5058e680a5982bce77648952c8532f Author: Chris Holcombe Date: Fri Feb 12 14:17:34 2016 -0800 Used a list as an integer. I meant to use the size of the list commit afd390b3ed4212883a02ca971e5613246c3ae6a8 Author: Ryan Beisner Date: Fri Feb 12 21:24:20 2016 +0000 No need to not wait for nonexistent nrpe commit 9721ce8006720d24b8e4133fbbb8a01d989a71c8 Author: Ryan Beisner Date: Fri Feb 12 21:02:36 2016 +0000 Disable Xenial test re: pending lp1537155 commit d12e2658f5b5e6c38b98ae986134f83df2e0a380 Author: Ryan Beisner Date: Fri Feb 12 20:57:08 2016 +0000 Update test target definitions; Wait for unit status. commit 7709b7d5385757fc6d8fe48fa7646efcdb77564a Author: Chris MacNaughton Date: Fri Feb 12 08:26:13 2016 -0500 rename actions commit 2c945523486227dd1c58a1c1a76a779d9c131a71 Merge: 5b5e6dc 27d5d4b Author: James Page Date: Fri Feb 12 12:34:20 2016 +0000 Resolve symlinks in get_devices(). commit 7edce1dd489a4718a150f7f38ffd366855e49828 Author: Edward Hope-Morley Date: Wed Feb 10 15:20:52 2016 +0000 [hopem,r=] Support multiple l3 segments. Closes-Bug: 1523871 commit 27d5d4b8bb0fd61a3910dad1bdf46adc2b476649 Author: Bjorn Tillenius Date: Tue Feb 2 19:01:53 2016 +0200 Lint. commit 6980d3a3418ba512e65a79a62b140b238d54a17b Author: Bjorn Tillenius Date: Tue Feb 2 17:34:19 2016 +0200 Resolve symlinks in get_devices(). commit f98627c1c163d702ae1142a6153801073d57280c Merge: 4f0dc6d 5b5e6dc Author: James Page Date: Sat Jan 30 15:45:01 2016 +0100 rebase commit eaa365a180e8eda88e6ef9f1a6c975a0b780dee5 Author: Chris Holcombe Date: Fri Jan 22 15:21:45 2016 -0800 Clean up another lint error commit 477cdc96fbe124509995a02c358c24c64451c9e4 Author: Chris Holcombe Date: Fri Jan 22 15:04:27 2016 -0800 Patching up the other unit tests to passing status commit faa7b3ad95ebed02718ff58b3e3203b7d59be709 Author: Chris MacNaughton Date: Fri Jan 22 16:42:58 2016 -0500 remove regex commit 1e3b2f5dd409a02399735aa2aeb5e78d18ea2240 Author: Chris MacNaughton Date: Fri Jan 22 16:10:15 2016 -0500 lint fix commit 620209aeb47900430f039eb2e65bfe00db672e32 Author: Chris MacNaughton Date: Fri Jan 22 16:05:15 2016 -0500 use search instead of match commit 2f47939fa84c43c485042325a925d72797df6480 Author: Chris MacNaughton Date: Fri Jan 22 15:16:22 2016 -0500 fix line length commit f203a5bdfc12a2a99e3695840f16182e037f1df1 Author: Chris MacNaughton Date: Fri Jan 22 15:02:10 2016 -0500 modify regex to not care about order commit 706b272fc91d432921750b3af09689361f4b8bb9 Author: Chris MacNaughton Date: Fri Jan 22 14:16:46 2016 -0500 try with sleeping commit 66d6952a65ceb5c8858f262daf127f96ed03ea81 Merge: e446a77 5b5e6dc Author: Chris Holcombe Date: Fri Jan 22 10:46:50 2016 -0800 Merge upstream and resolve conflicts commit fc714c96f40bac9fb89108cd56962343472f63cf Author: Chris MacNaughton Date: Fri Jan 22 11:10:34 2016 -0500 fix variable name commit 8cb53237c6588a00d86dcc0a564d18eb7cd751ae Author: Chris MacNaughton Date: Fri Jan 22 10:47:26 2016 -0500 update to use correct(?) commands commit b762e9842ca335845fe3a442dfdde838e5246b3b Author: Chris MacNaughton Date: Fri Jan 22 08:01:03 2016 -0500 update tests.yaml commit e446a7731cbe377f30c88bb99083745ba95caa4e Author: Chris Holcombe Date: Thu Jan 21 14:19:53 2016 -0800 Clean up lint warnings. Also added a few more mock unit tests commit 32ff93e8d0166b2346c422cbb9cd53bc4f805256 Author: Chris Holcombe Date: Thu Jan 21 09:38:47 2016 -0800 Adding a unit test file for ceph_ops commit 4f0dc6d8b76b8545453293b2c69e2d6a164db10e Author: James Page Date: Mon Jan 18 16:39:49 2016 +0000 Add configuration option for toggling use of direct io for OSD journals commit 1977cdbde1d0fa7ad57baa07d97f477143d54787 Author: Chris Holcombe Date: Mon Jan 18 08:07:35 2016 -0800 Add actions to lint. Change actions.yaml to use enum and also change underscores to dashes. Log action_fail in addition to exiting -1. Merge v2 requests with v1 requests since this does not break backwards compatibility. Add unit tests. Modify tox.ini to include actions. . commit 3f0e16bcc483952e340fa89505011b7a115ff421 Author: Chris MacNaughton Date: Fri Jan 15 16:45:00 2016 -0500 fix version commit c665092be6f9d07f45a0b9baf2e0f128e4ecdc37 Author: Chris MacNaughton Date: Fri Jan 15 16:20:27 2016 -0500 updating tests commit 80de4d7256efbbc6c2ab7cdfcb1ab292668be607 Author: Chris MacNaughton Date: Thu Jan 14 13:19:10 2016 -0500 update readme commit 44365d58785e9ba63179d092b875c2029024aa8b Author: Chris MacNaughton Date: Thu Jan 14 13:17:19 2016 -0500 add pause/resume actions pause calls: `ceph osd set noout ceoh osd set nodown` resume calls: `ceph osd unset noout ceph osd unset nodown` commit bdd4e69e801e2178532e31216efe7e815b06f864 Author: Chris Holcombe Date: Tue Dec 15 04:54:21 2015 -0800 Missed a few typos commit 0158586bde1a1f878c0a046a97510b8b90a95ce9 Author: Chris Holcombe Date: Tue Dec 15 04:41:22 2015 -0800 lint errors commit 92ad78733279112bbba8e12d3fb19809ab9d0ff7 Author: Chris Holcombe Date: Mon Dec 14 17:44:22 2015 -0800 Actions are working and lightly tested. Need to create a more robust, automated test setup Change-Id: Ia18b19961dab66bb6c19ef7e9c421b2fec60fcc7 --- .gitignore | 4 +- .gitreview | 2 +- README.md | 8 +- actions.yaml | 175 +++++++++++++++++++++ actions/__init__.py | 2 + actions/ceph_ops.py | 103 ++++++++++++ actions/create-erasure-profile | 89 +++++++++++ actions/create-pool | 38 +++++ actions/delete-erasure-profile | 24 +++ actions/delete-pool | 28 ++++ actions/get-erasure-profile | 18 +++ actions/list-erasure-profiles | 22 +++ actions/list-pools | 17 ++ actions/pool-get | 19 +++ actions/pool-set | 23 +++ actions/pool-statistics | 15 ++ actions/remove-pool-snapshot | 19 +++ actions/rename-pool | 16 ++ actions/set-pool-max-bytes | 16 ++ actions/snapshot-pool | 18 +++ config.yaml | 4 + hooks/ceph_broker.py | 278 ++++++++++++++++++++++++++++----- hooks/ceph_hooks.py | 3 +- templates/ceph.conf | 1 - tests/018-basic-trusty-liberty | 0 tests/019-basic-trusty-mitaka | 0 tests/020-basic-wily-liberty | 0 tests/021-basic-xenial-mitaka | 0 tests/basic_deployment.py | 14 +- tests/tests.yaml | 1 + unit_tests/test_ceph_broker.py | 94 ++++++----- unit_tests/test_ceph_ops.py | 217 +++++++++++++++++++++++++ unit_tests/test_status.py | 1 - 33 files changed, 1169 insertions(+), 100 deletions(-) create mode 100755 actions/ceph_ops.py create mode 100755 actions/create-erasure-profile create mode 100755 actions/create-pool create mode 100755 actions/delete-erasure-profile create mode 100755 actions/delete-pool create mode 100755 actions/get-erasure-profile create mode 100755 actions/list-erasure-profiles create mode 100755 actions/list-pools create mode 100755 actions/pool-get create mode 100755 actions/pool-set create mode 100755 actions/pool-statistics create mode 100755 actions/remove-pool-snapshot create mode 100755 actions/rename-pool create mode 100755 actions/set-pool-max-bytes create mode 100755 actions/snapshot-pool mode change 100644 => 100755 tests/018-basic-trusty-liberty mode change 100644 => 100755 tests/019-basic-trusty-mitaka mode change 100644 => 100755 tests/020-basic-wily-liberty mode change 100644 => 100755 tests/021-basic-xenial-mitaka create mode 100644 unit_tests/test_ceph_ops.py diff --git a/.gitignore b/.gitignore index 86e1f1b..7d2fd1f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ bin +.idea .coverage .testrepository .tox *.sw[nop] .idea -*.pyc -.idea +*.pyc \ No newline at end of file diff --git a/.gitreview b/.gitreview index f13dc9d..4700065 100644 --- a/.gitreview +++ b/.gitreview @@ -1,4 +1,4 @@ [gerrit] host=review.openstack.org port=29418 -project=openstack/charm-ceph-mon.git +project=openstack/charm-ceph-mon.git \ No newline at end of file diff --git a/README.md b/README.md index 103e57f..a66ca06 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,15 @@ juju # Usage The ceph charm has two pieces of mandatory configuration for which no defaults -are provided. You _must_ set these configuration options before deployment or the charm will not work: +are provided. You _must_ set these configuration options before deployment or the charm will not work: fsid: uuid specific to a ceph cluster used to ensure that different clusters don't get mixed up - use `uuid` to generate one. - monitor-secret: + monitor-secret: a ceph generated key used by the daemons that manage to cluster - to control security. You can use the ceph-authtool command to + to control security. You can use the ceph-authtool command to generate one: ceph-authtool /dev/stdout --name=mon. --gen-key @@ -30,7 +30,7 @@ At a minimum you must provide a juju config file during initial deployment with the fsid and monitor-secret options (contents of cepy.yaml below): ceph: - fsid: ecbb8960-0e21-11e2-b495-83a88f44db01 + fsid: ecbb8960-0e21-11e2-b495-83a88f44db01 monitor-secret: AQD1P2xQiKglDhAA4NGUF5j38Mhq56qwz+45wg== Boot things up by using: diff --git a/actions.yaml b/actions.yaml index a93054b..3f8e5df 100644 --- a/actions.yaml +++ b/actions.yaml @@ -39,3 +39,178 @@ remove-cache-tier: as the hot pool required: [backer-pool, cache-pool] additionalProperties: false + +create-pool: + description: Creates a pool + params: + name: + type: string + description: The name of the pool + profile-name: + type: string + description: The crush profile to use for this pool. The ruleset must exist first. + pool-type: + type: string + default: "replicated" + enum: [replicated, erasure] + description: | + The pool type which may either be replicated to recover from lost OSDs by keeping multiple copies of the + objects or erasure to get a kind of generalized RAID5 capability. + replicas: + type: integer + default: 3 + description: | + For the replicated pool this is the number of replicas to store of each object. + erasure-profile-name: + type: string + default: default + description: | + The name of the erasure coding profile to use for this pool. Note this profile must exist + before calling create-pool + required: [name] + additionalProperties: false +create-erasure-profile: + description: Create a new erasure code profile to use on a pool. + params: + name: + type: string + description: The name of the profile + failure-domain: + type: string + default: host + enum: [chassis, datacenter, host, osd, pdu, pod, rack, region, room, root, row] + description: | + The failure-domain=host will create a CRUSH ruleset that ensures no two chunks are stored in the same host. + plugin: + type: string + default: "jerasure" + enum: [jerasure, isa, lrc, shec] + description: | + The erasure plugin to use for this profile. + See http://docs.ceph.com/docs/master/rados/operations/erasure-code-profile/ for more details + data-chunks: + type: integer + default: 3 + description: | + The number of data chunks, i.e. the number of chunks in which the original object is divided. For instance + if K = 2 a 10KB object will be divided into K objects of 5KB each. + coding-chunks: + type: integer + default: 2 + description: | + The number of coding chunks, i.e. the number of additional chunks computed by the encoding functions. + If there are 2 coding chunks, it means 2 OSDs can be out without losing data. + locality-chunks: + type: integer + description: | + Group the coding and data chunks into sets of size locality. For instance, for k=4 and m=2, when locality=3 + two groups of three are created. Each set can be recovered without reading chunks from another set. + durability-estimator: + type: integer + description: | + The number of parity chunks each of which includes each data chunk in its calculation range. The number is used + as a durability estimator. For instance, if c=2, 2 OSDs can be down without losing data. + required: [name, data-chunks, coding-chunks] + additionalProperties: false +get-erasure-profile: + description: Display an erasure code profile. + params: + name: + type: string + description: The name of the profile + required: [name] + additionalProperties: false +delete-erasure-profile: + description: Deletes an erasure code profile. + params: + name: + type: string + description: The name of the profile + required: [name] + additionalProperties: false +list-erasure-profiles: + description: List the names of all erasure code profiles + additionalProperties: false +list-pools: + description: List your cluster’s pools + additionalProperties: false +set-pool-max-bytes: + description: Set pool quotas for the maximum number of bytes. + params: + max: + type: integer + description: The name of the pool + pool-name: + type: string + description: The name of the pool + required: [pool-name, max] + additionalProperties: false +delete-pool: + description: Deletes the named pool + params: + pool-name: + type: string + description: The name of the pool + required: [pool-name] + additionalProperties: false +rename-pool: + description: Rename a pool + params: + pool-name: + type: string + description: The name of the pool + new-name: + type: string + description: The new name of the pool + required: [pool-name, new-name] + additionalProperties: false +pool-statistics: + description: Show a pool’s utilization statistics + additionalProperties: false +snapshot-pool: + description: Snapshot a pool + params: + pool-name: + type: string + description: The name of the pool + snapshot-name: + type: string + description: The name of the snapshot + required: [snapshot-name, pool-name] + additionalProperties: false +remove-pool-snapshot: + description: Remove a pool snapshot + params: + pool-name: + type: string + description: The name of the pool + snapshot-name: + type: string + description: The name of the snapshot + required: [snapshot-name, pool-name] + additionalProperties: false +pool-set: + description: Set a value for the pool + params: + pool-name: + type: string + description: The pool to set this variable on. + key: + type: string + description: Any valid Ceph key from http://docs.ceph.com/docs/master/rados/operations/pools/#set-pool-values + value: + type: string + description: The value to set + required: [key, value, pool-name] + additionalProperties: false +pool-get: + description: Get a value for the pool + params: + pool-name: + type: string + description: The pool to get this variable from. + key: + type: string + description: Any valid Ceph key from http://docs.ceph.com/docs/master/rados/operations/pools/#get-pool-values + required: [key, pool-name] + additionalProperties: false diff --git a/actions/__init__.py b/actions/__init__.py index 9847ec9..ff2381c 100644 --- a/actions/__init__.py +++ b/actions/__init__.py @@ -1 +1,3 @@ __author__ = 'chris' +import sys +sys.path.append('hooks') diff --git a/actions/ceph_ops.py b/actions/ceph_ops.py new file mode 100755 index 0000000..e70ebc7 --- /dev/null +++ b/actions/ceph_ops.py @@ -0,0 +1,103 @@ +__author__ = 'chris' +from subprocess import CalledProcessError, check_output +import sys + +sys.path.append('hooks') + +import rados +from charmhelpers.core.hookenv import log, action_get, action_fail +from charmhelpers.contrib.storage.linux.ceph import pool_set, \ + set_pool_quota, snapshot_pool, remove_pool_snapshot + + +# Connect to Ceph via Librados and return a connection +def connect(): + try: + cluster = rados.Rados(conffile='/etc/ceph/ceph.conf') + cluster.connect() + return cluster + except (rados.IOError, + rados.ObjectNotFound, + rados.NoData, + rados.NoSpace, + rados.PermissionError) as rados_error: + log("librados failed with error: {}".format(str(rados_error))) + + +def create_crush_rule(): + # Shell out + pass + + +def list_pools(): + try: + cluster = connect() + pool_list = cluster.list_pools() + cluster.shutdown() + return pool_list + except (rados.IOError, + rados.ObjectNotFound, + rados.NoData, + rados.NoSpace, + rados.PermissionError) as e: + action_fail(e.message) + + +def pool_get(): + key = action_get("key") + pool_name = action_get("pool_name") + try: + value = check_output(['ceph', 'osd', 'pool', 'get', pool_name, key]) + return value + except CalledProcessError as e: + action_fail(e.message) + + +def set_pool(): + key = action_get("key") + value = action_get("value") + pool_name = action_get("pool_name") + pool_set(service='ceph', pool_name=pool_name, key=key, value=value) + + +def pool_stats(): + try: + pool_name = action_get("pool-name") + cluster = connect() + ioctx = cluster.open_ioctx(pool_name) + stats = ioctx.get_stats() + ioctx.close() + cluster.shutdown() + return stats + except (rados.Error, + rados.IOError, + rados.ObjectNotFound, + rados.NoData, + rados.NoSpace, + rados.PermissionError) as e: + action_fail(e.message) + + +def delete_pool_snapshot(): + pool_name = action_get("pool-name") + snapshot_name = action_get("snapshot-name") + remove_pool_snapshot(service='ceph', + pool_name=pool_name, + snapshot_name=snapshot_name) + + +# Note only one or the other can be set +def set_pool_max_bytes(): + pool_name = action_get("pool-name") + max_bytes = action_get("max") + set_pool_quota(service='ceph', + pool_name=pool_name, + max_bytes=max_bytes) + + +def snapshot_ceph_pool(): + pool_name = action_get("pool-name") + snapshot_name = action_get("snapshot-name") + snapshot_pool(service='ceph', + pool_name=pool_name, + snapshot_name=snapshot_name) diff --git a/actions/create-erasure-profile b/actions/create-erasure-profile new file mode 100755 index 0000000..2b00b58 --- /dev/null +++ b/actions/create-erasure-profile @@ -0,0 +1,89 @@ +#!/usr/bin/python +from subprocess import CalledProcessError +import sys + +sys.path.append('hooks') + +from charmhelpers.contrib.storage.linux.ceph import create_erasure_profile +from charmhelpers.core.hookenv import action_get, log, action_fail + + +def make_erasure_profile(): + name = action_get("name") + plugin = action_get("plugin") + failure_domain = action_get("failure-domain") + + # jerasure requires k+m + # isa requires k+m + # local requires k+m+l + # shec requires k+m+c + + if plugin == "jerasure": + k = action_get("data-chunks") + m = action_get("coding-chunks") + try: + create_erasure_profile(service='admin', + erasure_plugin_name=plugin, + profile_name=name, + data_chunks=k, + coding_chunks=m, + failure_domain=failure_domain) + except CalledProcessError as e: + log(e) + action_fail("Create erasure profile failed with " + "message: {}".format(e.message)) + elif plugin == "isa": + k = action_get("data-chunks") + m = action_get("coding-chunks") + try: + create_erasure_profile(service='admin', + erasure_plugin_name=plugin, + profile_name=name, + data_chunks=k, + coding_chunks=m, + failure_domain=failure_domain) + except CalledProcessError as e: + log(e) + action_fail("Create erasure profile failed with " + "message: {}".format(e.message)) + elif plugin == "local": + k = action_get("data-chunks") + m = action_get("coding-chunks") + l = action_get("locality-chunks") + try: + create_erasure_profile(service='admin', + erasure_plugin_name=plugin, + profile_name=name, + data_chunks=k, + coding_chunks=m, + locality=l, + failure_domain=failure_domain) + except CalledProcessError as e: + log(e) + action_fail("Create erasure profile failed with " + "message: {}".format(e.message)) + elif plugin == "shec": + k = action_get("data-chunks") + m = action_get("coding-chunks") + c = action_get("durability-estimator") + try: + create_erasure_profile(service='admin', + erasure_plugin_name=plugin, + profile_name=name, + data_chunks=k, + coding_chunks=m, + durability_estimator=c, + failure_domain=failure_domain) + except CalledProcessError as e: + log(e) + action_fail("Create erasure profile failed with " + "message: {}".format(e.message)) + else: + # Unknown erasure plugin + action_fail("Unknown erasure-plugin type of {}. " + "Only jerasure, isa, local or shec is " + "allowed".format(plugin)) + + +if __name__ == '__main__': + make_erasure_profile() diff --git a/actions/create-pool b/actions/create-pool new file mode 100755 index 0000000..4d1d214 --- /dev/null +++ b/actions/create-pool @@ -0,0 +1,38 @@ +#!/usr/bin/python +import sys + +sys.path.append('hooks') +from subprocess import CalledProcessError +from charmhelpers.core.hookenv import action_get, log, action_fail +from charmhelpers.contrib.storage.linux.ceph import ErasurePool, ReplicatedPool + + +def create_pool(): + pool_name = action_get("name") + pool_type = action_get("pool-type") + try: + if pool_type == "replicated": + replicas = action_get("replicas") + replicated_pool = ReplicatedPool(name=pool_name, + service='admin', + replicas=replicas) + replicated_pool.create() + + elif pool_type == "erasure": + crush_profile_name = action_get("erasure-profile-name") + erasure_pool = ErasurePool(name=pool_name, + erasure_code_profile=crush_profile_name, + service='admin') + erasure_pool.create() + else: + log("Unknown pool type of {}. Only erasure or replicated is " + "allowed".format(pool_type)) + action_fail("Unknown pool type of {}. Only erasure or replicated " + "is allowed".format(pool_type)) + except CalledProcessError as e: + action_fail("Pool creation failed because of a failed process. " + "Ret Code: {} Message: {}".format(e.returncode, e.message)) + + +if __name__ == '__main__': + create_pool() diff --git a/actions/delete-erasure-profile b/actions/delete-erasure-profile new file mode 100755 index 0000000..075c410 --- /dev/null +++ b/actions/delete-erasure-profile @@ -0,0 +1,24 @@ +#!/usr/bin/python +from subprocess import CalledProcessError + +__author__ = 'chris' +import sys + +sys.path.append('hooks') + +from charmhelpers.contrib.storage.linux.ceph import remove_erasure_profile +from charmhelpers.core.hookenv import action_get, log, action_fail + + +def delete_erasure_profile(): + name = action_get("name") + + try: + remove_erasure_profile(service='admin', profile_name=name) + except CalledProcessError as e: + action_fail("Remove erasure profile failed with error: {}".format( + e.message)) + + +if __name__ == '__main__': + delete_erasure_profile() diff --git a/actions/delete-pool b/actions/delete-pool new file mode 100755 index 0000000..3d65507 --- /dev/null +++ b/actions/delete-pool @@ -0,0 +1,28 @@ +#!/usr/bin/python +import sys + +sys.path.append('hooks') + +import rados +from ceph_ops import connect +from charmhelpers.core.hookenv import action_get, log, action_fail + + +def remove_pool(): + try: + pool_name = action_get("name") + cluster = connect() + log("Deleting pool: {}".format(pool_name)) + cluster.delete_pool(str(pool_name)) # Convert from unicode + cluster.shutdown() + except (rados.IOError, + rados.ObjectNotFound, + rados.NoData, + rados.NoSpace, + rados.PermissionError) as e: + log(e) + action_fail(e) + + +if __name__ == '__main__': + remove_pool() diff --git a/actions/get-erasure-profile b/actions/get-erasure-profile new file mode 100755 index 0000000..29ece59 --- /dev/null +++ b/actions/get-erasure-profile @@ -0,0 +1,18 @@ +#!/usr/bin/python +__author__ = 'chris' +import sys + +sys.path.append('hooks') + +from charmhelpers.contrib.storage.linux.ceph import get_erasure_profile +from charmhelpers.core.hookenv import action_get, action_set + + +def make_erasure_profile(): + name = action_get("name") + out = get_erasure_profile(service='admin', name=name) + action_set({'message': out}) + + +if __name__ == '__main__': + make_erasure_profile() diff --git a/actions/list-erasure-profiles b/actions/list-erasure-profiles new file mode 100755 index 0000000..cf6dfa0 --- /dev/null +++ b/actions/list-erasure-profiles @@ -0,0 +1,22 @@ +#!/usr/bin/python +__author__ = 'chris' +import sys +from subprocess import check_output, CalledProcessError + +sys.path.append('hooks') + +from charmhelpers.core.hookenv import action_get, log, action_set, action_fail + +if __name__ == '__main__': + name = action_get("name") + try: + out = check_output(['ceph', + '--id', 'admin', + 'osd', + 'erasure-code-profile', + 'ls']).decode('UTF-8') + action_set({'message': out}) + except CalledProcessError as e: + log(e) + action_fail("Listing erasure profiles failed with error: {}".format( + e.message)) diff --git a/actions/list-pools b/actions/list-pools new file mode 100755 index 0000000..102667c --- /dev/null +++ b/actions/list-pools @@ -0,0 +1,17 @@ +#!/usr/bin/python +__author__ = 'chris' +import sys +from subprocess import check_output, CalledProcessError + +sys.path.append('hooks') + +from charmhelpers.core.hookenv import log, action_set, action_fail + +if __name__ == '__main__': + try: + out = check_output(['ceph', '--id', 'admin', + 'osd', 'lspools']).decode('UTF-8') + action_set({'message': out}) + except CalledProcessError as e: + log(e) + action_fail("List pools failed with error: {}".format(e.message)) diff --git a/actions/pool-get b/actions/pool-get new file mode 100755 index 0000000..e4f924b --- /dev/null +++ b/actions/pool-get @@ -0,0 +1,19 @@ +#!/usr/bin/python +__author__ = 'chris' +import sys +from subprocess import check_output, CalledProcessError + +sys.path.append('hooks') + +from charmhelpers.core.hookenv import log, action_set, action_get, action_fail + +if __name__ == '__main__': + name = action_get('pool-name') + key = action_get('key') + try: + out = check_output(['ceph', '--id', 'admin', + 'osd', 'pool', 'get', name, key]).decode('UTF-8') + action_set({'message': out}) + except CalledProcessError as e: + log(e) + action_fail("Pool get failed with message: {}".format(e.message)) diff --git a/actions/pool-set b/actions/pool-set new file mode 100755 index 0000000..1f6e13b --- /dev/null +++ b/actions/pool-set @@ -0,0 +1,23 @@ +#!/usr/bin/python +from subprocess import CalledProcessError +import sys + +sys.path.append('hooks') + +from charmhelpers.core.hookenv import action_get, log, action_fail +from ceph_broker import handle_set_pool_value + +if __name__ == '__main__': + name = action_get("pool-name") + key = action_get("key") + value = action_get("value") + request = {'name': name, + 'key': key, + 'value': value} + + try: + handle_set_pool_value(service='admin', request=request) + except CalledProcessError as e: + log(e.message) + action_fail("Setting pool key: {} and value: {} failed with " + "message: {}".format(key, value, e.message)) diff --git a/actions/pool-statistics b/actions/pool-statistics new file mode 100755 index 0000000..536c889 --- /dev/null +++ b/actions/pool-statistics @@ -0,0 +1,15 @@ +#!/usr/bin/python +import sys + +sys.path.append('hooks') +from subprocess import check_output, CalledProcessError +from charmhelpers.core.hookenv import log, action_set, action_fail + +if __name__ == '__main__': + try: + out = check_output(['ceph', '--id', 'admin', + 'df']).decode('UTF-8') + action_set({'message': out}) + except CalledProcessError as e: + log(e) + action_fail("ceph df failed with message: {}".format(e.message)) diff --git a/actions/remove-pool-snapshot b/actions/remove-pool-snapshot new file mode 100755 index 0000000..387849e --- /dev/null +++ b/actions/remove-pool-snapshot @@ -0,0 +1,19 @@ +#!/usr/bin/python +import sys + +sys.path.append('hooks') +from subprocess import CalledProcessError +from charmhelpers.core.hookenv import action_get, log, action_fail +from charmhelpers.contrib.storage.linux.ceph import remove_pool_snapshot + +if __name__ == '__main__': + name = action_get("pool-name") + snapname = action_get("snapshot-name") + try: + remove_pool_snapshot(service='admin', + pool_name=name, + snapshot_name=snapname) + except CalledProcessError as e: + log(e) + action_fail("Remove pool snapshot failed with message: {}".format( + e.message)) diff --git a/actions/rename-pool b/actions/rename-pool new file mode 100755 index 0000000..6fe088e --- /dev/null +++ b/actions/rename-pool @@ -0,0 +1,16 @@ +#!/usr/bin/python +import sys + +sys.path.append('hooks') +from subprocess import CalledProcessError +from charmhelpers.core.hookenv import action_get, log, action_fail +from charmhelpers.contrib.storage.linux.ceph import rename_pool + +if __name__ == '__main__': + name = action_get("pool-name") + new_name = action_get("new-name") + try: + rename_pool(service='admin', old_name=name, new_name=new_name) + except CalledProcessError as e: + log(e) + action_fail("Renaming pool failed with message: {}".format(e.message)) diff --git a/actions/set-pool-max-bytes b/actions/set-pool-max-bytes new file mode 100755 index 0000000..8636088 --- /dev/null +++ b/actions/set-pool-max-bytes @@ -0,0 +1,16 @@ +#!/usr/bin/python +import sys + +sys.path.append('hooks') +from subprocess import CalledProcessError +from charmhelpers.core.hookenv import action_get, log, action_fail +from charmhelpers.contrib.storage.linux.ceph import set_pool_quota + +if __name__ == '__main__': + max_bytes = action_get("max") + name = action_get("pool-name") + try: + set_pool_quota(service='admin', pool_name=name, max_bytes=max_bytes) + except CalledProcessError as e: + log(e) + action_fail("Set pool quota failed with message: {}".format(e.message)) diff --git a/actions/snapshot-pool b/actions/snapshot-pool new file mode 100755 index 0000000..a02619b --- /dev/null +++ b/actions/snapshot-pool @@ -0,0 +1,18 @@ +#!/usr/bin/python +import sys + +sys.path.append('hooks') +from subprocess import CalledProcessError +from charmhelpers.core.hookenv import action_get, log, action_fail +from charmhelpers.contrib.storage.linux.ceph import snapshot_pool + +if __name__ == '__main__': + name = action_get("pool-name") + snapname = action_get("snapshot-name") + try: + snapshot_pool(service='admin', + pool_name=name, + snapshot_name=snapname) + except CalledProcessError as e: + log(e) + action_fail("Snapshot pool failed with message: {}".format(e.message)) diff --git a/config.yaml b/config.yaml index 30abb8a..c486a85 100644 --- a/config.yaml +++ b/config.yaml @@ -121,3 +121,7 @@ options: description: | A comma-separated list of nagios servicegroups. If left empty, the nagios_context will be used as the servicegroup + use-direct-io: + default: True + type: boolean + description: Configure use of direct IO for OSD journals. diff --git a/hooks/ceph_broker.py b/hooks/ceph_broker.py index bd23d43..d01d38e 100644 --- a/hooks/ceph_broker.py +++ b/hooks/ceph_broker.py @@ -1,24 +1,71 @@ #!/usr/bin/python # -# Copyright 2014 Canonical Ltd. +# Copyright 2015 Canonical Ltd. # import json +from charmhelpers.contrib.storage.linux.ceph import validator, \ + erasure_profile_exists, ErasurePool, set_pool_quota, \ + pool_set, snapshot_pool, remove_pool_snapshot, create_erasure_profile, \ + ReplicatedPool, rename_pool, Pool, get_osds, pool_exists, delete_pool + from charmhelpers.core.hookenv import ( log, DEBUG, INFO, ERROR, ) -from charmhelpers.contrib.storage.linux.ceph import ( - create_pool, - get_osds, - pool_exists, -) + +# This comes from http://docs.ceph.com/docs/master/rados/operations/pools/ +# This should do a decent job of preventing people from passing in bad values. +# It will give a useful error message +POOL_KEYS = { + # "Ceph Key Name": [Python type, [Valid Range]] + "size": [int], + "min_size": [int], + "crash_replay_interval": [int], + "pgp_num": [int], # = or < pg_num + "crush_ruleset": [int], + "hashpspool": [bool], + "nodelete": [bool], + "nopgchange": [bool], + "nosizechange": [bool], + "write_fadvise_dontneed": [bool], + "noscrub": [bool], + "nodeep-scrub": [bool], + "hit_set_type": [basestring, ["bloom", "explicit_hash", + "explicit_object"]], + "hit_set_count": [int, [1, 1]], + "hit_set_period": [int], + "hit_set_fpp": [float, [0.0, 1.0]], + "cache_target_dirty_ratio": [float], + "cache_target_dirty_high_ratio": [float], + "cache_target_full_ratio": [float], + "target_max_bytes": [int], + "target_max_objects": [int], + "cache_min_flush_age": [int], + "cache_min_evict_age": [int], + "fast_read": [bool], +} + +CEPH_BUCKET_TYPES = [ + 'osd', + 'host', + 'chassis', + 'rack', + 'row', + 'pdu', + 'pod', + 'room', + 'datacenter', + 'region', + 'root' +] def decode_req_encode_rsp(f): """Decorator to decode incoming requests and encode responses.""" + def decode_inner(req): return json.dumps(f(json.loads(req))) @@ -42,15 +89,14 @@ def process_requests(reqs): resp['request-id'] = request_id return resp - except Exception as exc: log(str(exc), level=ERROR) msg = ("Unexpected error occurred while processing requests: %s" % - (reqs)) + reqs) log(msg, level=ERROR) return {'exit-code': 1, 'stderr': msg} - msg = ("Missing or invalid api version (%s)" % (version)) + msg = ("Missing or invalid api version (%s)" % version) resp = {'exit-code': 1, 'stderr': msg} if request_id: resp['request-id'] = request_id @@ -58,6 +104,156 @@ def process_requests(reqs): return resp +def handle_create_erasure_profile(request, service): + # "local" | "shec" or it defaults to "jerasure" + erasure_type = request.get('erasure-type') + # "host" | "rack" or it defaults to "host" # Any valid Ceph bucket + failure_domain = request.get('failure-domain') + name = request.get('name') + k = request.get('k') + m = request.get('m') + l = request.get('l') + + if failure_domain not in CEPH_BUCKET_TYPES: + msg = "failure-domain must be one of {}".format(CEPH_BUCKET_TYPES) + log(msg, level=ERROR) + return {'exit-code': 1, 'stderr': msg} + + create_erasure_profile(service=service, erasure_plugin_name=erasure_type, + profile_name=name, failure_domain=failure_domain, + data_chunks=k, coding_chunks=m, locality=l) + + +def handle_erasure_pool(request, service): + pool_name = request.get('name') + erasure_profile = request.get('erasure-profile') + quota = request.get('max-bytes') + + if erasure_profile is None: + erasure_profile = "default-canonical" + + # Check for missing params + if pool_name is None: + msg = "Missing parameter. name is required for the pool" + log(msg, level=ERROR) + return {'exit-code': 1, 'stderr': msg} + + # TODO: Default to 3/2 erasure coding. I believe this requires min 5 osds + if not erasure_profile_exists(service=service, name=erasure_profile): + # TODO: Fail and tell them to create the profile or default + msg = "erasure-profile {} does not exist. Please create it with: " \ + "create-erasure-profile".format(erasure_profile) + log(msg, level=ERROR) + return {'exit-code': 1, 'stderr': msg} + pass + pool = ErasurePool(service=service, name=pool_name, + erasure_code_profile=erasure_profile) + # Ok make the erasure pool + if not pool_exists(service=service, name=pool_name): + log("Creating pool '%s' (erasure_profile=%s)" % (pool, + erasure_profile), + level=INFO) + pool.create() + + # Set a quota if requested + if quota is not None: + set_pool_quota(service=service, pool_name=pool_name, max_bytes=quota) + + +def handle_replicated_pool(request, service): + pool_name = request.get('name') + replicas = request.get('replicas') + quota = request.get('max-bytes') + + # Optional params + pg_num = request.get('pg_num') + if pg_num: + # Cap pg_num to max allowed just in case. + osds = get_osds(service) + if osds: + pg_num = min(pg_num, (len(osds) * 100 // replicas)) + + # Check for missing params + if pool_name is None or replicas is None: + msg = "Missing parameter. name and replicas are required" + log(msg, level=ERROR) + return {'exit-code': 1, 'stderr': msg} + + pool = ReplicatedPool(service=service, + name=pool_name, + replicas=replicas, + pg_num=pg_num) + if not pool_exists(service=service, name=pool_name): + log("Creating pool '%s' (replicas=%s)" % (pool, replicas), + level=INFO) + pool.create() + else: + log("Pool '%s' already exists - skipping create" % pool, + level=DEBUG) + + # Set a quota if requested + if quota is not None: + set_pool_quota(service=service, pool_name=pool_name, max_bytes=quota) + + +def handle_create_cache_tier(request, service): + # mode = "writeback" | "readonly" + storage_pool = request.get('cold-pool') + cache_pool = request.get('hot-pool') + cache_mode = request.get('mode') + + if cache_mode is None: + cache_mode = "writeback" + + # cache and storage pool must exist first + if not pool_exists(service=service, name=storage_pool) or not pool_exists( + service=service, name=cache_pool): + msg = "cold-pool: {} and hot-pool: {} must exist. Please create " \ + "them first".format(storage_pool, cache_pool) + log(msg, level=ERROR) + return {'exit-code': 1, 'stderr': msg} + p = Pool(service=service, name=storage_pool) + p.add_cache_tier(cache_pool=cache_pool, mode=cache_mode) + + +def handle_remove_cache_tier(request, service): + storage_pool = request.get('cold-pool') + cache_pool = request.get('hot-pool') + # cache and storage pool must exist first + if not pool_exists(service=service, name=storage_pool) or not pool_exists( + service=service, name=cache_pool): + msg = "cold-pool: {} or hot-pool: {} doesn't exist. Not " \ + "deleting cache tier".format(storage_pool, cache_pool) + log(msg, level=ERROR) + return {'exit-code': 1, 'stderr': msg} + + pool = Pool(name=storage_pool, service=service) + pool.remove_cache_tier(cache_pool=cache_pool) + + +def handle_set_pool_value(request, service): + # Set arbitrary pool values + params = {'pool': request.get('name'), + 'key': request.get('key'), + 'value': request.get('value')} + if params['key'] not in POOL_KEYS: + msg = "Invalid key '%s'" % params['key'] + log(msg, level=ERROR) + return {'exit-code': 1, 'stderr': msg} + + # Get the validation method + validator_params = POOL_KEYS[params['key']] + if len(validator_params) is 1: + # Validate that what the user passed is actually legal per Ceph's rules + validator(params['value'], validator_params[0]) + else: + # Validate that what the user passed is actually legal per Ceph's rules + validator(params['value'], validator_params[0], validator_params[1]) + # Set the value + pool_set(service=service, pool_name=params['pool'], key=params['key'], + value=params['value']) + + def process_requests_v1(reqs): """Process v1 requests. @@ -70,45 +266,45 @@ def process_requests_v1(reqs): log("Processing %s ceph broker requests" % (len(reqs)), level=INFO) for req in reqs: op = req.get('op') - log("Processing op='%s'" % (op), level=DEBUG) + log("Processing op='%s'" % op, level=DEBUG) # Use admin client since we do not have other client key locations # setup to use them for these operations. svc = 'admin' if op == "create-pool": - params = {'pool': req.get('name'), - 'replicas': req.get('replicas')} - if not all(params.iteritems()): - msg = ("Missing parameter(s): %s" % - (' '.join([k for k in params.iterkeys() - if not params[k]]))) - log(msg, level=ERROR) - return {'exit-code': 1, 'stderr': msg} + pool_type = req.get('pool-type') # "replicated" | "erasure" - # Mandatory params - pool = params['pool'] - replicas = params['replicas'] - - # Optional params - pg_num = req.get('pg_num') - if pg_num: - # Cap pg_num to max allowed just in case. - osds = get_osds(svc) - if osds: - pg_num = min(pg_num, (len(osds) * 100 // replicas)) - - # Ensure string - pg_num = str(pg_num) - - if not pool_exists(service=svc, name=pool): - log("Creating pool '%s' (replicas=%s)" % (pool, replicas), - level=INFO) - create_pool(service=svc, name=pool, replicas=replicas, - pg_num=pg_num) + # Default to replicated if pool_type isn't given + if pool_type == 'erasure': + handle_erasure_pool(request=req, service=svc) else: - log("Pool '%s' already exists - skipping create" % (pool), - level=DEBUG) + handle_replicated_pool(request=req, service=svc) + elif op == "create-cache-tier": + handle_create_cache_tier(request=req, service=svc) + elif op == "remove-cache-tier": + handle_remove_cache_tier(request=req, service=svc) + elif op == "create-erasure-profile": + handle_create_erasure_profile(request=req, service=svc) + elif op == "delete-pool": + pool = req.get('name') + delete_pool(service=svc, name=pool) + elif op == "rename-pool": + old_name = req.get('name') + new_name = req.get('new-name') + rename_pool(service=svc, old_name=old_name, new_name=new_name) + elif op == "snapshot-pool": + pool = req.get('name') + snapshot_name = req.get('snapshot-name') + snapshot_pool(service=svc, pool_name=pool, + snapshot_name=snapshot_name) + elif op == "remove-pool-snapshot": + pool = req.get('name') + snapshot_name = req.get('snapshot-name') + remove_pool_snapshot(service=svc, pool_name=pool, + snapshot_name=snapshot_name) + elif op == "set-pool-value": + handle_set_pool_value(request=req, service=svc) else: - msg = "Unknown operation '%s'" % (op) + msg = "Unknown operation '%s'" % op log(msg, level=ERROR) return {'exit-code': 1, 'stderr': msg} diff --git a/hooks/ceph_hooks.py b/hooks/ceph_hooks.py index 354c155..385afdd 100755 --- a/hooks/ceph_hooks.py +++ b/hooks/ceph_hooks.py @@ -54,7 +54,7 @@ from charmhelpers.payload.execd import execd_preinstall from charmhelpers.contrib.openstack.alternatives import install_alternative from charmhelpers.contrib.network.ip import ( get_ipv6_addr, - format_ipv6_addr + format_ipv6_addr, ) from charmhelpers.core.sysctl import create as create_sysctl from charmhelpers.core.templating import render @@ -294,6 +294,7 @@ def emit_cephconf(): 'ceph_public_network': public_network, 'ceph_cluster_network': cluster_network, 'loglevel': config('loglevel'), + 'dio': str(config('use-direct-io')).lower(), } if config('prefer-ipv6'): diff --git a/templates/ceph.conf b/templates/ceph.conf index f64db7c..631381b 100644 --- a/templates/ceph.conf +++ b/templates/ceph.conf @@ -36,4 +36,3 @@ keyring = /var/lib/ceph/mon/$cluster-$id/keyring [mds] keyring = /var/lib/ceph/mds/$cluster-$id/keyring - diff --git a/tests/018-basic-trusty-liberty b/tests/018-basic-trusty-liberty old mode 100644 new mode 100755 diff --git a/tests/019-basic-trusty-mitaka b/tests/019-basic-trusty-mitaka old mode 100644 new mode 100755 diff --git a/tests/020-basic-wily-liberty b/tests/020-basic-wily-liberty old mode 100644 new mode 100755 diff --git a/tests/021-basic-xenial-mitaka b/tests/021-basic-xenial-mitaka old mode 100644 new mode 100755 diff --git a/tests/basic_deployment.py b/tests/basic_deployment.py index 1b24e60..63ddca4 100644 --- a/tests/basic_deployment.py +++ b/tests/basic_deployment.py @@ -3,6 +3,7 @@ import amulet import re import time + from charmhelpers.contrib.openstack.amulet.deployment import ( OpenStackAmuletDeployment ) @@ -30,6 +31,8 @@ class CephBasicDeployment(OpenStackAmuletDeployment): u.log.info('Waiting on extended status checks...') exclude_services = ['mysql'] + + # Wait for deployment ready msgs, except exclusions self._auto_wait_for_status(exclude_services=exclude_services) self._initialize_tests() @@ -79,6 +82,9 @@ class CephBasicDeployment(OpenStackAmuletDeployment): 'admin-token': 'ubuntutesting'} mysql_config = {'dataset-size': '50%'} cinder_config = {'block-device': 'None', 'glance-api-version': '2'} + + # Include a non-existent device as osd-devices is a whitelist, + # and this will catch cases where proposals attempt to change that. ceph_config = { 'monitor-count': '3', 'auth-supported': 'none', @@ -198,7 +204,6 @@ class CephBasicDeployment(OpenStackAmuletDeployment): self.cinder_sentry: ['cinder-api', 'cinder-scheduler', 'cinder-volume'], - self.ceph_osd_sentry: ['ceph-osd-all'], } if self._get_openstack_release() < self.vivid_kilo: @@ -212,6 +217,13 @@ class CephBasicDeployment(OpenStackAmuletDeployment): services[self.ceph1_sentry] = ceph_services services[self.ceph2_sentry] = ceph_services + ceph_osd_services = [ + 'ceph-osd id={}'.format(u.get_ceph_osd_id_cmd(0)), + 'ceph-osd id={}'.format(u.get_ceph_osd_id_cmd(1)) + ] + + services[self.ceph_osd_sentry] = ceph_osd_services + ret = u.validate_services_by_name(services) if ret: amulet.raise_status(amulet.FAIL, msg=ret) diff --git a/tests/tests.yaml b/tests/tests.yaml index 4d17631..49e721b 100644 --- a/tests/tests.yaml +++ b/tests/tests.yaml @@ -19,3 +19,4 @@ packages: - python-novaclient - python-pika - python-swiftclient + - python-nose \ No newline at end of file diff --git a/unit_tests/test_ceph_broker.py b/unit_tests/test_ceph_broker.py index 8f08cdc..b720d94 100644 --- a/unit_tests/test_ceph_broker.py +++ b/unit_tests/test_ceph_broker.py @@ -1,12 +1,12 @@ import json -import mock import unittest +import mock + import ceph_broker class CephBrokerTestCase(unittest.TestCase): - def setUp(self): super(CephBrokerTestCase, self).setUp() @@ -20,15 +20,15 @@ class CephBrokerTestCase(unittest.TestCase): def test_process_requests_missing_api_version(self, mock_log): req = json.dumps({'ops': []}) rc = ceph_broker.process_requests(req) - self.assertEqual(json.loads(rc), {'exit-code': 1, - 'stderr': - ('Missing or invalid api version ' - '(None)')}) + self.assertEqual(json.loads(rc), { + 'exit-code': 1, + 'stderr': 'Missing or invalid api version (None)'}) @mock.patch('ceph_broker.log') def test_process_requests_invalid_api_version(self, mock_log): req = json.dumps({'api-version': 2, 'ops': []}) rc = ceph_broker.process_requests(req) + print "Return: %s" % rc self.assertEqual(json.loads(rc), {'exit-code': 1, 'stderr': 'Missing or invalid api version (2)'}) @@ -41,90 +41,88 @@ class CephBrokerTestCase(unittest.TestCase): {'exit-code': 1, 'stderr': "Unknown operation 'invalid_op'"}) - @mock.patch('ceph_broker.create_pool') - @mock.patch('ceph_broker.pool_exists') - @mock.patch('ceph_broker.log') - def test_process_requests_create_pool(self, mock_log, mock_pool_exists, - mock_create_pool): - mock_pool_exists.return_value = False - reqs = json.dumps({'api-version': 1, - 'ops': [{'op': 'create-pool', 'name': - 'foo', 'replicas': 3}]}) - rc = ceph_broker.process_requests(reqs) - mock_pool_exists.assert_called_with(service='admin', name='foo') - mock_create_pool.assert_called_with(service='admin', name='foo', - replicas=3, pg_num=None) - self.assertEqual(json.loads(rc), {'exit-code': 0}) - @mock.patch('ceph_broker.get_osds') - @mock.patch('ceph_broker.create_pool') + @mock.patch('ceph_broker.ReplicatedPool') @mock.patch('ceph_broker.pool_exists') @mock.patch('ceph_broker.log') def test_process_requests_create_pool_w_pg_num(self, mock_log, mock_pool_exists, - mock_create_pool, + mock_replicated_pool, mock_get_osds): mock_get_osds.return_value = [0, 1, 2] mock_pool_exists.return_value = False reqs = json.dumps({'api-version': 1, - 'ops': [{'op': 'create-pool', 'name': - 'foo', 'replicas': 3, - 'pg_num': 100}]}) + 'ops': [{ + 'op': 'create-pool', + 'name': 'foo', + 'replicas': 3, + 'pg_num': 100}]}) rc = ceph_broker.process_requests(reqs) mock_pool_exists.assert_called_with(service='admin', name='foo') - mock_create_pool.assert_called_with(service='admin', name='foo', - replicas=3, pg_num='100') + mock_replicated_pool.assert_called_with(service='admin', name='foo', + replicas=3, pg_num=100) self.assertEqual(json.loads(rc), {'exit-code': 0}) @mock.patch('ceph_broker.get_osds') - @mock.patch('ceph_broker.create_pool') + @mock.patch('ceph_broker.ReplicatedPool') @mock.patch('ceph_broker.pool_exists') @mock.patch('ceph_broker.log') def test_process_requests_create_pool_w_pg_num_capped(self, mock_log, mock_pool_exists, - mock_create_pool, + mock_replicated_pool, mock_get_osds): mock_get_osds.return_value = [0, 1, 2] mock_pool_exists.return_value = False reqs = json.dumps({'api-version': 1, - 'ops': [{'op': 'create-pool', 'name': - 'foo', 'replicas': 3, - 'pg_num': 300}]}) + 'ops': [{ + 'op': 'create-pool', + 'name': 'foo', + 'replicas': 3, + 'pg_num': 300}]}) rc = ceph_broker.process_requests(reqs) - mock_pool_exists.assert_called_with(service='admin', name='foo') - mock_create_pool.assert_called_with(service='admin', name='foo', - replicas=3, pg_num='100') + mock_pool_exists.assert_called_with(service='admin', + name='foo') + mock_replicated_pool.assert_called_with(service='admin', name='foo', + replicas=3, pg_num=100) + self.assertEqual(json.loads(rc), {'exit-code': 0}) self.assertEqual(json.loads(rc), {'exit-code': 0}) - @mock.patch('ceph_broker.create_pool') + @mock.patch('ceph_broker.ReplicatedPool') @mock.patch('ceph_broker.pool_exists') @mock.patch('ceph_broker.log') def test_process_requests_create_pool_exists(self, mock_log, mock_pool_exists, - mock_create_pool): + mock_replicated_pool): mock_pool_exists.return_value = True reqs = json.dumps({'api-version': 1, - 'ops': [{'op': 'create-pool', 'name': 'foo', + 'ops': [{'op': 'create-pool', + 'name': 'foo', 'replicas': 3}]}) rc = ceph_broker.process_requests(reqs) - mock_pool_exists.assert_called_with(service='admin', name='foo') - self.assertFalse(mock_create_pool.called) + mock_pool_exists.assert_called_with(service='admin', + name='foo') + self.assertFalse(mock_replicated_pool.create.called) self.assertEqual(json.loads(rc), {'exit-code': 0}) - @mock.patch('ceph_broker.create_pool') + @mock.patch('ceph_broker.ReplicatedPool') @mock.patch('ceph_broker.pool_exists') @mock.patch('ceph_broker.log') - def test_process_requests_create_pool_rid(self, mock_log, mock_pool_exists, - mock_create_pool): + def test_process_requests_create_pool_rid(self, mock_log, + mock_pool_exists, + mock_replicated_pool): mock_pool_exists.return_value = False reqs = json.dumps({'api-version': 1, 'request-id': '1ef5aede', - 'ops': [{'op': 'create-pool', 'name': - 'foo', 'replicas': 3}]}) + 'ops': [{ + 'op': 'create-pool', + 'name': 'foo', + 'replicas': 3}]}) rc = ceph_broker.process_requests(reqs) mock_pool_exists.assert_called_with(service='admin', name='foo') - mock_create_pool.assert_called_with(service='admin', name='foo', - replicas=3, pg_num=None) + mock_replicated_pool.assert_called_with(service='admin', + name='foo', + pg_num=None, + replicas=3) self.assertEqual(json.loads(rc)['exit-code'], 0) self.assertEqual(json.loads(rc)['request-id'], '1ef5aede') diff --git a/unit_tests/test_ceph_ops.py b/unit_tests/test_ceph_ops.py new file mode 100644 index 0000000..88e64c7 --- /dev/null +++ b/unit_tests/test_ceph_ops.py @@ -0,0 +1,217 @@ +__author__ = 'chris' + +import json +from hooks import ceph_broker + +import mock +import unittest + + +class TestCephOps(unittest.TestCase): + """ + @mock.patch('ceph_broker.log') + def test_connect(self, mock_broker): + self.fail() + """ + + @mock.patch('ceph_broker.log') + @mock.patch('hooks.ceph_broker.create_erasure_profile') + def test_create_erasure_profile(self, mock_create_erasure, mock_log): + req = json.dumps({'api-version': 1, + 'ops': [{ + 'op': 'create-erasure-profile', + 'name': 'foo', + 'erasure-type': 'jerasure', + 'failure-domain': 'rack', + 'k': 3, + 'm': 2, + }]}) + rc = ceph_broker.process_requests(req) + mock_create_erasure.assert_called_with(service='admin', + profile_name='foo', + coding_chunks=2, + data_chunks=3, + locality=None, + failure_domain='rack', + erasure_plugin_name='jerasure') + self.assertEqual(json.loads(rc), {'exit-code': 0}) + + @mock.patch('ceph_broker.log') + @mock.patch('hooks.ceph_broker.pool_exists') + @mock.patch('hooks.ceph_broker.ReplicatedPool.create') + def test_process_requests_create_replicated_pool(self, + mock_replicated_pool, + mock_pool_exists, + mock_log): + mock_pool_exists.return_value = False + reqs = json.dumps({'api-version': 1, + 'ops': [{ + 'op': 'create-pool', + 'pool-type': 'replicated', + 'name': 'foo', + 'replicas': 3 + }]}) + rc = ceph_broker.process_requests(reqs) + mock_pool_exists.assert_called_with(service='admin', name='foo') + mock_replicated_pool.assert_called_with() + self.assertEqual(json.loads(rc), {'exit-code': 0}) + + @mock.patch('ceph_broker.log') + @mock.patch('hooks.ceph_broker.delete_pool') + def test_process_requests_delete_pool(self, + mock_delete_pool, + mock_log): + reqs = json.dumps({'api-version': 1, + 'ops': [{ + 'op': 'delete-pool', + 'name': 'foo', + }]}) + rc = ceph_broker.process_requests(reqs) + mock_delete_pool.assert_called_with(service='admin', name='foo') + self.assertEqual(json.loads(rc), {'exit-code': 0}) + + @mock.patch('ceph_broker.log') + @mock.patch('hooks.ceph_broker.pool_exists') + @mock.patch('hooks.ceph_broker.ErasurePool.create') + @mock.patch('hooks.ceph_broker.erasure_profile_exists') + def test_process_requests_create_erasure_pool(self, mock_profile_exists, + mock_erasure_pool, + mock_pool_exists, + mock_log): + mock_pool_exists.return_value = False + reqs = json.dumps({'api-version': 1, + 'ops': [{ + 'op': 'create-pool', + 'pool-type': 'erasure', + 'name': 'foo', + 'erasure-profile': 'default' + }]}) + rc = ceph_broker.process_requests(reqs) + mock_profile_exists.assert_called_with(service='admin', name='default') + mock_pool_exists.assert_called_with(service='admin', name='foo') + mock_erasure_pool.assert_called_with() + self.assertEqual(json.loads(rc), {'exit-code': 0}) + + @mock.patch('ceph_broker.log') + @mock.patch('hooks.ceph_broker.pool_exists') + @mock.patch('hooks.ceph_broker.Pool.add_cache_tier') + def test_process_requests_create_cache_tier(self, mock_pool, + mock_pool_exists, mock_log): + mock_pool_exists.return_value = True + reqs = json.dumps({'api-version': 1, + 'ops': [{ + 'op': 'create-cache-tier', + 'cold-pool': 'foo', + 'hot-pool': 'foo-ssd', + 'mode': 'writeback', + 'erasure-profile': 'default' + }]}) + rc = ceph_broker.process_requests(reqs) + mock_pool_exists.assert_any_call(service='admin', name='foo') + mock_pool_exists.assert_any_call(service='admin', name='foo-ssd') + + mock_pool.assert_called_with(cache_pool='foo-ssd', mode='writeback') + self.assertEqual(json.loads(rc), {'exit-code': 0}) + + @mock.patch('ceph_broker.log') + @mock.patch('hooks.ceph_broker.pool_exists') + @mock.patch('hooks.ceph_broker.Pool.remove_cache_tier') + def test_process_requests_remove_cache_tier(self, mock_pool, + mock_pool_exists, mock_log): + mock_pool_exists.return_value = True + reqs = json.dumps({'api-version': 1, + 'ops': [{ + 'op': 'remove-cache-tier', + 'hot-pool': 'foo-ssd', + }]}) + rc = ceph_broker.process_requests(reqs) + mock_pool_exists.assert_any_call(service='admin', name='foo-ssd') + + mock_pool.assert_called_with(cache_pool='foo-ssd') + self.assertEqual(json.loads(rc), {'exit-code': 0}) + + @mock.patch('ceph_broker.log') + @mock.patch('hooks.ceph_broker.snapshot_pool') + def test_snapshot_pool(self, mock_snapshot_pool, mock_log): + reqs = json.dumps({'api-version': 1, + 'ops': [{ + 'op': 'snapshot-pool', + 'name': 'foo', + 'snapshot-name': 'foo-snap1', + }]}) + rc = ceph_broker.process_requests(reqs) + mock_snapshot_pool.return_value = 1 + mock_snapshot_pool.assert_called_with(service='admin', + pool_name='foo', + snapshot_name='foo-snap1') + self.assertEqual(json.loads(rc), {'exit-code': 0}) + + @mock.patch('ceph_broker.log') + @mock.patch('hooks.ceph_broker.rename_pool') + def test_rename_pool(self, mock_rename_pool, mock_log): + reqs = json.dumps({'api-version': 1, + 'ops': [{ + 'op': 'rename-pool', + 'name': 'foo', + 'new-name': 'foo2', + }]}) + rc = ceph_broker.process_requests(reqs) + mock_rename_pool.assert_called_with(service='admin', + old_name='foo', + new_name='foo2') + self.assertEqual(json.loads(rc), {'exit-code': 0}) + + @mock.patch('ceph_broker.log') + @mock.patch('hooks.ceph_broker.remove_pool_snapshot') + def test_remove_pool_snapshot(self, mock_snapshot_pool, mock_broker): + reqs = json.dumps({'api-version': 1, + 'ops': [{ + 'op': 'remove-pool-snapshot', + 'name': 'foo', + 'snapshot-name': 'foo-snap1', + }]}) + rc = ceph_broker.process_requests(reqs) + mock_snapshot_pool.assert_called_with(service='admin', + pool_name='foo', + snapshot_name='foo-snap1') + self.assertEqual(json.loads(rc), {'exit-code': 0}) + + @mock.patch('ceph_broker.log') + @mock.patch('hooks.ceph_broker.pool_set') + def test_set_pool_value(self, mock_set_pool, mock_broker): + reqs = json.dumps({'api-version': 1, + 'ops': [{ + 'op': 'set-pool-value', + 'name': 'foo', + 'key': 'size', + 'value': 3, + }]}) + rc = ceph_broker.process_requests(reqs) + mock_set_pool.assert_called_with(service='admin', + pool_name='foo', + key='size', + value=3) + self.assertEqual(json.loads(rc), {'exit-code': 0}) + + @mock.patch('ceph_broker.log') + def test_set_invalid_pool_value(self, mock_broker): + reqs = json.dumps({'api-version': 1, + 'ops': [{ + 'op': 'set-pool-value', + 'name': 'foo', + 'key': 'size', + 'value': 'abc', + }]}) + rc = ceph_broker.process_requests(reqs) + # self.assertRaises(AssertionError) + self.assertEqual(json.loads(rc)['exit-code'], 1) + + ''' + @mock.patch('ceph_broker.log') + def test_set_pool_max_bytes(self, mock_broker): + self.fail() + ''' + + +if __name__ == '__main__': + unittest.main() diff --git a/unit_tests/test_status.py b/unit_tests/test_status.py index c433018..8862590 100644 --- a/unit_tests/test_status.py +++ b/unit_tests/test_status.py @@ -31,7 +31,6 @@ ENOUGH_PEERS_COMPLETE = { class ServiceStatusTestCase(test_utils.CharmTestCase): - def setUp(self): super(ServiceStatusTestCase, self).setUp(hooks, TO_PATCH) self.config.side_effect = self.test_config.get