Browse Source

Initial check in of reworked version

Funs Kessen 3 years ago
commit
f8f9ba1def

+ 11
- 0
.gitignore View File

@@ -0,0 +1,11 @@
1
+.tox
2
+.build
3
+*.pyc
4
+*.bak
5
+repositories/centos/*
6
+repositories/ubuntu/*
7
+deployment_scripts/puppet/modules/inifile
8
+deployment_scripts/puppet/modules/stdlib
9
+build.sh
10
+*.rpm
11
+.project

+ 202
- 0
LICENSE View File

@@ -0,0 +1,202 @@
1
+Apache License
2
+                           Version 2.0, January 2004
3
+                        http://www.apache.org/licenses/
4
+
5
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+   1. Definitions.
8
+
9
+      "License" shall mean the terms and conditions for use, reproduction,
10
+      and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+      "Licensor" shall mean the copyright owner or entity authorized by
13
+      the copyright owner that is granting the License.
14
+
15
+      "Legal Entity" shall mean the union of the acting entity and all
16
+      other entities that control, are controlled by, or are under common
17
+      control with that entity. For the purposes of this definition,
18
+      "control" means (i) the power, direct or indirect, to cause the
19
+      direction or management of such entity, whether by contract or
20
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+      outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+      "You" (or "Your") shall mean an individual or Legal Entity
24
+      exercising permissions granted by this License.
25
+
26
+      "Source" form shall mean the preferred form for making modifications,
27
+      including but not limited to software source code, documentation
28
+      source, and configuration files.
29
+
30
+      "Object" form shall mean any form resulting from mechanical
31
+      transformation or translation of a Source form, including but
32
+      not limited to compiled object code, generated documentation,
33
+      and conversions to other media types.
34
+
35
+      "Work" shall mean the work of authorship, whether in Source or
36
+      Object form, made available under the License, as indicated by a
37
+      copyright notice that is included in or attached to the work
38
+      (an example is provided in the Appendix below).
39
+
40
+      "Derivative Works" shall mean any work, whether in Source or Object
41
+      form, that is based on (or derived from) the Work and for which the
42
+      editorial revisions, annotations, elaborations, or other modifications
43
+      represent, as a whole, an original work of authorship. For the purposes
44
+      of this License, Derivative Works shall not include works that remain
45
+      separable from, or merely link (or bind by name) to the interfaces of,
46
+      the Work and Derivative Works thereof.
47
+
48
+      "Contribution" shall mean any work of authorship, including
49
+      the original version of the Work and any modifications or additions
50
+      to that Work or Derivative Works thereof, that is intentionally
51
+      submitted to Licensor for inclusion in the Work by the copyright owner
52
+      or by an individual or Legal Entity authorized to submit on behalf of
53
+      the copyright owner. For the purposes of this definition, "submitted"
54
+      means any form of electronic, verbal, or written communication sent
55
+      to the Licensor or its representatives, including but not limited to
56
+      communication on electronic mailing lists, source code control systems,
57
+      and issue tracking systems that are managed by, or on behalf of, the
58
+      Licensor for the purpose of discussing and improving the Work, but
59
+      excluding communication that is conspicuously marked or otherwise
60
+      designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+      "Contributor" shall mean Licensor and any individual or Legal Entity
63
+      on behalf of whom a Contribution has been received by Licensor and
64
+      subsequently incorporated within the Work.
65
+
66
+   2. Grant of Copyright License. Subject to the terms and conditions of
67
+      this License, each Contributor hereby grants to You a perpetual,
68
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+      copyright license to reproduce, prepare Derivative Works of,
70
+      publicly display, publicly perform, sublicense, and distribute the
71
+      Work and such Derivative Works in Source or Object form.
72
+
73
+   3. Grant of Patent License. Subject to the terms and conditions of
74
+      this License, each Contributor hereby grants to You a perpetual,
75
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+      (except as stated in this section) patent license to make, have made,
77
+      use, offer to sell, sell, import, and otherwise transfer the Work,
78
+      where such license applies only to those patent claims licensable
79
+      by such Contributor that are necessarily infringed by their
80
+      Contribution(s) alone or by combination of their Contribution(s)
81
+      with the Work to which such Contribution(s) was submitted. If You
82
+      institute patent litigation against any entity (including a
83
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+      or a Contribution incorporated within the Work constitutes direct
85
+      or contributory patent infringement, then any patent licenses
86
+      granted to You under this License for that Work shall terminate
87
+      as of the date such litigation is filed.
88
+
89
+   4. Redistribution. You may reproduce and distribute copies of the
90
+      Work or Derivative Works thereof in any medium, with or without
91
+      modifications, and in Source or Object form, provided that You
92
+      meet the following conditions:
93
+
94
+      (a) You must give any other recipients of the Work or
95
+          Derivative Works a copy of this License; and
96
+
97
+      (b) You must cause any modified files to carry prominent notices
98
+          stating that You changed the files; and
99
+
100
+      (c) You must retain, in the Source form of any Derivative Works
101
+          that You distribute, all copyright, patent, trademark, and
102
+          attribution notices from the Source form of the Work,
103
+          excluding those notices that do not pertain to any part of
104
+          the Derivative Works; and
105
+
106
+      (d) If the Work includes a "NOTICE" text file as part of its
107
+          distribution, then any Derivative Works that You distribute must
108
+          include a readable copy of the attribution notices contained
109
+          within such NOTICE file, excluding those notices that do not
110
+          pertain to any part of the Derivative Works, in at least one
111
+          of the following places: within a NOTICE text file distributed
112
+          as part of the Derivative Works; within the Source form or
113
+          documentation, if provided along with the Derivative Works; or,
114
+          within a display generated by the Derivative Works, if and
115
+          wherever such third-party notices normally appear. The contents
116
+          of the NOTICE file are for informational purposes only and
117
+          do not modify the License. You may add Your own attribution
118
+          notices within Derivative Works that You distribute, alongside
119
+          or as an addendum to the NOTICE text from the Work, provided
120
+          that such additional attribution notices cannot be construed
121
+          as modifying the License.
122
+
123
+      You may add Your own copyright statement to Your modifications and
124
+      may provide additional or different license terms and conditions
125
+      for use, reproduction, or distribution of Your modifications, or
126
+      for any such Derivative Works as a whole, provided Your use,
127
+      reproduction, and distribution of the Work otherwise complies with
128
+      the conditions stated in this License.
129
+
130
+   5. Submission of Contributions. Unless You explicitly state otherwise,
131
+      any Contribution intentionally submitted for inclusion in the Work
132
+      by You to the Licensor shall be under the terms and conditions of
133
+      this License, without any additional terms or conditions.
134
+      Notwithstanding the above, nothing herein shall supersede or modify
135
+      the terms of any separate license agreement you may have executed
136
+      with Licensor regarding such Contributions.
137
+
138
+   6. Trademarks. This License does not grant permission to use the trade
139
+      names, trademarks, service marks, or product names of the Licensor,
140
+      except as required for reasonable and customary use in describing the
141
+      origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+   7. Disclaimer of Warranty. Unless required by applicable law or
144
+      agreed to in writing, Licensor provides the Work (and each
145
+      Contributor provides its Contributions) on an "AS IS" BASIS,
146
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+      implied, including, without limitation, any warranties or conditions
148
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+      PARTICULAR PURPOSE. You are solely responsible for determining the
150
+      appropriateness of using or redistributing the Work and assume any
151
+      risks associated with Your exercise of permissions under this License.
152
+
153
+   8. Limitation of Liability. In no event and under no legal theory,
154
+      whether in tort (including negligence), contract, or otherwise,
155
+      unless required by applicable law (such as deliberate and grossly
156
+      negligent acts) or agreed to in writing, shall any Contributor be
157
+      liable to You for damages, including any direct, indirect, special,
158
+      incidental, or consequential damages of any character arising as a
159
+      result of this License or out of the use or inability to use the
160
+      Work (including but not limited to damages for loss of goodwill,
161
+      work stoppage, computer failure or malfunction, or any and all
162
+      other commercial damages or losses), even if such Contributor
163
+      has been advised of the possibility of such damages.
164
+
165
+   9. Accepting Warranty or Additional Liability. While redistributing
166
+      the Work or Derivative Works thereof, You may choose to offer,
167
+      and charge a fee for, acceptance of support, warranty, indemnity,
168
+      or other liability obligations and/or rights consistent with this
169
+      License. However, in accepting such obligations, You may act only
170
+      on Your own behalf and on Your sole responsibility, not on behalf
171
+      of any other Contributor, and only if You agree to indemnify,
172
+      defend, and hold each Contributor harmless for any liability
173
+      incurred by, or claims asserted against, such Contributor by reason
174
+      of your accepting any such warranty or additional liability.
175
+
176
+   END OF TERMS AND CONDITIONS
177
+
178
+   APPENDIX: How to apply the Apache License to your work.
179
+
180
+      To apply the Apache License to your work, attach the following
181
+      boilerplate notice, with the fields enclosed by brackets "{}"
182
+      replaced with your own identifying information. (Don't include
183
+      the brackets!)  The text should be enclosed in the appropriate
184
+      comment syntax for the file format. We also recommend that a
185
+      file or class name and description of purpose be included on the
186
+      same "printed page" as the copyright notice for easier
187
+      identification within third-party archives.
188
+
189
+   Copyright {yyyy} {name of copyright owner}
190
+
191
+   Licensed under the Apache License, Version 2.0 (the "License");
192
+   you may not use this file except in compliance with the License.
193
+   You may obtain a copy of the License at
194
+
195
+       http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+   Unless required by applicable law or agreed to in writing, software
198
+   distributed under the License is distributed on an "AS IS" BASIS,
199
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+   See the License for the specific language governing permissions and
201
+   limitations under the License.
202
+

+ 44
- 0
README.md View File

@@ -0,0 +1,44 @@
1
+fuel-plugin-datera-cinder
2
+============
3
+
4
+Plugin description
5
+--------------
6
+
7
+Datera plugin for Fuel extends Mirantis OpenStack functionality by
8
+adding support for Datera EBS.
9
+
10
+The Datera cluster is an iSCSI block storage device used as a
11
+Cinder backend.
12
+
13
+Requirements
14
+------------
15
+
16
+| Requirement                                          | Version/Comment |
17
+|------------------------------------------------------|-----------------|
18
+| Mirantis OpenStack compatibility                     | >= 7.1          |
19
+| Access to Datera via ccinder-volume node             |                 |
20
+| iSCSI initiator on all compute/cinder-volume nodes   |                 |
21
+
22
+Limitations
23
+-----------
24
+
25
+Datera configuration
26
+---------------------
27
+
28
+Before deployment the following needs to be verified:
29
+1. Your Datera Cluster is reachable by all compute nodes, as well as the
30
+    Cinder Control/Manager node.
31
+2. Create an Openstack account on the Datera cluster that can create
32
+    volumes. 
33
+    (san_login/password).
34
+3. Use the Management VIP address for the Datera cluster.
35
+    (as the san_ip)`
36
+
37
+Datera Cinder plugin installation
38
+---------------------------
39
+
40
+All of the code required for using Datera in an OpenStack deployment is
41
+included in the upstream OpenStack distribution.
42
+
43
+Datera plugin configuration
44
+----------------------------

+ 3
- 0
deployment_scripts/puppet/deploy.sh View File

@@ -0,0 +1,3 @@
1
+#!/bin/bash
2
+
3
+echo datera > /tmp/datera

+ 1
- 0
deployment_scripts/puppet/manifests/cinder_datera_config.pp View File

@@ -0,0 +1 @@
1
+include cinder_datera_config::cinder

+ 1
- 0
deployment_scripts/puppet/manifests/cinder_datera_driver.pp View File

@@ -0,0 +1 @@
1
+include cinder_datera_driver::cinder

+ 56
- 0
deployment_scripts/puppet/modules/cinder_datera_config/manifests/backend/datera.pp View File

@@ -0,0 +1,56 @@
1
+# == Class: cinder_datera_config::backend::datera
2
+#
3
+# Configures Cinder volume Datera driver.
4
+# Parameters are particular to each volume driver.
5
+#
6
+# === Parameters
7
+#
8
+# [*volume_backend_name*]
9
+#   (optional) Allows for the volume_backend_name to be separate of $name.
10
+#   Defaults to: $name
11
+#
12
+# [*volume_driver*]
13
+#   (optional) Setup cinder-volume to use Datera volume driver.
14
+#   Defaults to 'cinder.volume.drivers.datera.DateraDriver'
15
+#
16
+# [*san_ip*]
17
+#   (required) IP address of Datera clusters MVIP.
18
+#
19
+# [*san_login*]
20
+#   (required) Username for Datera tenant admin account.
21
+#
22
+# [*san_password*]
23
+#   (required) Password for Datera tenant admin account.
24
+#
25
+# [*datera_num_replicas*]
26
+#   (optional) The number of replicas to keep.
27
+#   Defaults to 2
28
+#
29
+# [*extra_options*]
30
+#   (optional) Hash of extra options to pass to the cinder.conf
31
+#   Defaults to: {}
32
+#   Example :
33
+#     { 'datera_backend/param1' => { 'value' => value1 } }
34
+#
35
+define cinder_datera_config::backend::datera(
36
+  $san_ip,
37
+  $san_login,
38
+  $san_password,
39
+  $datera_num_replicas,
40
+  $volume_backend_name = $name,
41
+  $volume_driver       = 'cinder.volume.drivers.datera.DateraDriver',
42
+  $extra_options       = {},
43
+) {
44
+
45
+  cinder_config {
46
+    "${name}/volume_backend_name": value => $volume_backend_name;
47
+    "${name}/volume_driver":       value => $volume_driver;
48
+    "${name}/san_ip":              value => $san_ip;
49
+    "${name}/san_login":           value => $san_login;
50
+    "${name}/san_password":        value => $san_password, secret => true;
51
+    "${name}/datera_num_replicas": value => $datera_num_replicas;
52
+  }
53
+
54
+  create_resources('cinder_config', $extra_options)
55
+
56
+}

+ 62
- 0
deployment_scripts/puppet/modules/cinder_datera_config/manifests/cinder.pp View File

@@ -0,0 +1,62 @@
1
+#    Copyright 2016 Datera, Inc.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+#
15
+
16
+class cinder_datera_config::cinder (
17
+    $backend_name  = 'datera',
18
+    $backends      = ''
19
+) {
20
+    include cinder::params
21
+    include cinder::client
22
+
23
+    $plugin_settings = hiera('fuel-plugin-datera-cinder')
24
+
25
+    if $::cinder::params::volume_package {
26
+      package { $::cinder::params::volume_package:
27
+        ensure => present,
28
+      }
29
+      Package[$::cinder::params::volume_package] -> Cinder_config<||>
30
+    }
31
+
32
+    if $plugin_settings['multibackend'] {
33
+      $section = $backend_name
34
+      cinder_config {
35
+        "DEFAULT/enabled_backends": value => "${backend_name},${backends}";
36
+      }
37
+    } else {
38
+      $section = 'DEFAULT'
39
+    }
40
+
41
+    cinder_datera_config::backend::datera{ $section :
42
+      san_ip               => $plugin_settings['datera_mvip'],
43
+      san_login            => $plugin_settings['datera_admin_login'],
44
+      san_password         => $plugin_settings['datera_admin_password'],
45
+      datera_num_replicas  => $plugin_settings['datera_num_replicas'],
46
+      extra_options        => {}
47
+    }
48
+
49
+    Cinder_config<||>~> Service[cinder_volume]
50
+
51
+    service { 'cinder_volume':
52
+      ensure     => running,
53
+      name       => $::cinder::params::volume_service,
54
+      enable     => true,
55
+      hasstatus  => true,
56
+      hasrestart => true,
57
+    }
58
+    package { 'open-iscsi' :
59
+      ensure => 'installed',
60
+    }
61
+
62
+}

+ 48
- 0
deployment_scripts/puppet/modules/cinder_datera_config/manifests/volume/datera.pp View File

@@ -0,0 +1,48 @@
1
+# == Class: cinder_datera_config::volume::datera
2
+#
3
+# Configures Cinder volume Datera driver.
4
+# Parameters are particular to each volume driver.
5
+#
6
+# === Parameters
7
+#
8
+# [*volume_driver*]
9
+#   (optional) Setup cinder-volume to use Datera volume driver.
10
+#   Defaults to 'cinder.volume.drivers.datera.DateraDriver'
11
+#
12
+# [*san_ip*]
13
+#   (required) IP address of Datera clusters MVIP.
14
+#
15
+# [*san_login*]
16
+#   (required) Username for Datera admin account.
17
+#
18
+# [*san_password*]
19
+#   (required) Password for Datera admin account.
20
+#
21
+# [*datera_num_replicas*]
22
+#   (optional) Number of replicas to keep.
23
+#   Defaults to 2
24
+#
25
+# [*extra_options*]
26
+#   (optional) Hash of extra options to pass to the cinder.conf
27
+#   Defaults to: {}
28
+#   Example :
29
+#     { 'datera_backend/param1' => { 'value' => value1 } }
30
+#
31
+class cinder_datera_config::volume::datera(
32
+  $san_ip,
33
+  $san_login,
34
+  $san_password,
35
+  $volume_driver       = 'cinder.volume.drivers.datera.DateraDriver',
36
+  $datera_num_replicas= '2',
37
+  $extra_options       = {},
38
+) {
39
+
40
+  cinder::backend::datera { 'DEFAULT':
41
+    san_ip              => $san_ip,
42
+    san_login           => $san_login,
43
+    san_password        => $san_password,
44
+    volume_driver       => $volume_driver,
45
+    datera_num_replicas => $datera_num_replicas,
46
+    extra_options       => $extra_options,
47
+  }
48
+}

+ 44
- 0
deployment_scripts/puppet/modules/cinder_datera_config/spec/classes/cinder_volume_datera_spec.rb View File

@@ -0,0 +1,44 @@
1
+require 'spec_helper'
2
+
3
+describe 'cinder::volume::datera' do
4
+  let :req_params do
5
+    {
6
+      :san_ip       => '192.168.1.81',
7
+      :san_login    => 'openstack_tenant0',
8
+      :san_password => 'password',
9
+      :datera_num_replicas => '2',
10
+    }
11
+  end
12
+
13
+  let :params do
14
+    req_params
15
+  end
16
+
17
+  describe 'datera volume driver' do
18
+    it 'configure datera volume driver' do
19
+      is_expected.to contain_cinder_config('DEFAULT/volume_driver').with_value('cinder.volume.drivers.datera.DateraDriver')
20
+      is_expected.to contain_cinder_config('DEFAULT/san_ip').with_value('192.168.1.81')
21
+      is_expected.to contain_cinder_config('DEFAULT/san_login').with_value('openstack_tenant0')
22
+      is_expected.to contain_cinder_config('DEFAULT/san_password').with_value('password')
23
+      is_expected.to contain_cinder_config('DEFAULT/datera_num_replicas').with_value('2')
24
+    end
25
+
26
+    it 'marks san_password as secret' do
27
+      is_expected.to contain_cinder_config('DEFAULT/san_password').with_secret( true )
28
+    end
29
+
30
+  end
31
+
32
+  describe 'datera volume driver with additional configuration' do
33
+    before :each do
34
+      params.merge!({:extra_options => {'datera_backend/param1' => {'value' => 'value1'}}})
35
+    end
36
+
37
+    it 'configure datera volume with additional configuration' do
38
+      should contain_cinder__backend__datera('DEFAULT').with({
39
+        :extra_options => {'datera_backend/param1' => {'value' => 'value1'}}
40
+      })
41
+    end
42
+  end
43
+
44
+end

+ 46
- 0
deployment_scripts/puppet/modules/cinder_datera_config/spec/defines/cinder_backend_datera_spec.rb View File

@@ -0,0 +1,46 @@
1
+require 'spec_helper'
2
+
3
+describe 'cinder::backend::datera' do
4
+  let (:title) { 'datera' }
5
+
6
+  let :req_params do
7
+    {
8
+      :san_ip       => '192.168.1.81',
9
+      :san_login    => 'openstack_tenant0',
10
+      :san_password => 'password',
11
+      :datera_num_replicas => '2',
12
+    }
13
+  end
14
+
15
+  let :params do
16
+    req_params
17
+  end
18
+
19
+  describe 'datera volume driver' do
20
+    it 'configure datera volume driver' do
21
+      is_expected.to contain_cinder_config('datera/volume_driver').with_value(
22
+        'cinder.volume.drivers.datera.DateraDriver')
23
+      is_expected.to contain_cinder_config('datera/san_ip').with_value(
24
+        '192.168.1.81')
25
+      is_expected.to contain_cinder_config('datera/san_login').with_value(
26
+        'openstack_tenant0')
27
+      is_expected.to contain_cinder_config('datera/san_password').with_value(
28
+        'password')
29
+      is_expected.to contain_cinder_config('datera/datera_num_replicas').with_value(
30
+        '2')
31
+    end
32
+  end
33
+
34
+  describe 'datera backend with additional configuration' do
35
+    before :each do
36
+      params.merge!({:extra_options => {'datera/param1' => {'value' => 'value1'}}})
37
+    end
38
+
39
+    it 'configure datera backend with additional configuration' do
40
+      should contain_cinder_config('datera/param1').with({
41
+        :value => 'value1',
42
+      })
43
+    end
44
+  end
45
+
46
+end

+ 470
- 0
deployment_scripts/puppet/modules/cinder_datera_driver/files/7.0/datera.py View File

@@ -0,0 +1,470 @@
1
+# Copyright 2015 Datera
2
+# All Rights Reserved.
3
+#
4
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5
+#    not use this file except in compliance with the License. You may obtain
6
+#    a copy of the License at
7
+#
8
+#         http://www.apache.org/licenses/LICENSE-2.0
9
+#
10
+#    Unless required by applicable law or agreed to in writing, software
11
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+#    License for the specific language governing permissions and limitations
14
+#    under the License.
15
+
16
+import json
17
+from functools import wraps
18
+
19
+from oslo_config import cfg
20
+from oslo_log import log as logging
21
+from oslo_utils import excutils
22
+from oslo_utils import units
23
+from cinder import utils
24
+import requests
25
+
26
+from cinder import exception
27
+from cinder.i18n import _, _LI, _LE, _LW
28
+from cinder.openstack.common import versionutils
29
+from cinder.volume.drivers.san import san
30
+
31
+LOG = logging.getLogger(__name__)
32
+
33
+d_opts = [
34
+    cfg.StrOpt('datera_api_token',
35
+               default=None,
36
+               help='DEPRECATED: This will be removed in the Liberty release. '
37
+                    'Use san_login and san_password instead. This directly '
38
+                    'sets the Datera API token.'),
39
+    cfg.StrOpt('datera_api_port',
40
+               default='7717',
41
+               help='Datera API port.'),
42
+    cfg.StrOpt('datera_api_version',
43
+               default='2',
44
+               help='Datera API version.'),
45
+    cfg.StrOpt('datera_num_replicas',
46
+               default='3',
47
+               help='Number of replicas to create of an inode.')
48
+]
49
+
50
+
51
+CONF = cfg.CONF
52
+CONF.import_opt('driver_client_cert_key', 'cinder.volume.driver')
53
+CONF.import_opt('driver_client_cert', 'cinder.volume.driver')
54
+CONF.import_opt('driver_use_ssl', 'cinder.volume.driver')
55
+CONF.register_opts(d_opts)
56
+
57
+DEFAULT_STORAGE_NAME = 'storage-1'
58
+DEFAULT_VOLUME_NAME = 'volume-1'
59
+
60
+
61
+def _authenticated(func):
62
+    """Ensure the driver is authenticated to make a request.
63
+
64
+    In do_setup() we fetch an auth token and store it. If that expires when
65
+    we do API request, we'll fetch a new one.
66
+    """
67
+    @wraps(func)
68
+    def func_wrapper(self, *args, **kwargs):
69
+        try:
70
+            return func(self, *args, **kwargs)
71
+        except exception.NotAuthorized:
72
+            # Prevent recursion loop. After the self arg is the
73
+            # resource_type arg from _issue_api_request(). If attempt to
74
+            # login failed, we should just give up.
75
+            if args[0] == 'login':
76
+                raise
77
+
78
+            # Token might've expired, get a new one, try again.
79
+            self._login()
80
+            return func(self, *args, **kwargs)
81
+    return func_wrapper
82
+
83
+
84
+class DateraDriver(san.SanISCSIDriver):
85
+
86
+    """The OpenStack Datera Driver
87
+
88
+    Version history:
89
+        1.0 - Initial driver
90
+        1.1 - Look for lun-0 instead of lun-1.
91
+        2.0 - Update For Datera API v2
92
+    """
93
+    VERSION = '2.0'
94
+
95
+    def __init__(self, *args, **kwargs):
96
+        super(DateraDriver, self).__init__(*args, **kwargs)
97
+        self.configuration.append_config_values(d_opts)
98
+        self.num_replicas = self.configuration.datera_num_replicas
99
+        self.username = self.configuration.san_login
100
+        self.password = self.configuration.san_password
101
+        self.auth_token = None
102
+        self.cluster_stats = {}
103
+
104
+    def _get_lunid(self):
105
+        return 0
106
+
107
+    def do_setup(self, context):
108
+        # If any of the deprecated options are set, we'll warn the operator to
109
+        # use the new authentication method.
110
+        DEPRECATED_OPTS = [
111
+            self.configuration.driver_client_cert_key,
112
+            self.configuration.driver_client_cert,
113
+            self.configuration.datera_api_token
114
+        ]
115
+
116
+        if any(DEPRECATED_OPTS):
117
+            msg = _LW("Client cert verification and datera_api_token are "
118
+                      "deprecated in the Datera driver, and will be removed "
119
+                      "in the Liberty release. Please set the san_login and "
120
+                      "san_password in your cinder.conf instead.")
121
+            versionutils.report_deprecated_feature(LOG, msg)
122
+            return
123
+
124
+        # If we can't authenticate through the old and new method, just fail
125
+        # now.
126
+        if not all([self.username, self.password]):
127
+            msg = _("san_login and/or san_password is not set for Datera "
128
+                    "driver in the cinder.conf. Set this information and "
129
+                    "start the cinder-volume service again.")
130
+            LOG.error(msg)
131
+            raise exception.InvalidInput(msg)
132
+
133
+        self._login()
134
+
135
+    @utils.retry(exception.VolumeDriverException, retries=3)
136
+    def _wait_for_resource(self, id, resource_type):
137
+        result = self._issue_api_request(resource_type, 'get', id)
138
+
139
+        if result['storage_instances'][DEFAULT_STORAGE_NAME]['volumes'][
140
+                DEFAULT_VOLUME_NAME]['op_state'] == 'available':
141
+            return
142
+        else:
143
+            raise exception.VolumeDriverException(msg=_('Resource not ready.'))
144
+
145
+    def _create_resource(self, resource, resource_type, body):
146
+        result = self._issue_api_request(resource_type, 'post', body=body)
147
+
148
+        if result['storage_instances'][DEFAULT_STORAGE_NAME]['volumes'][
149
+                DEFAULT_VOLUME_NAME]['op_state'] == 'available':
150
+            return
151
+        self._wait_for_resource(resource['id'], resource_type)
152
+
153
+    def create_volume(self, volume):
154
+        """Create a logical volume."""
155
+        # Generate App Instance, Storage Instance and Volume
156
+        # Volume ID will be used as the App Instance Name
157
+        # Storage Instance and Volumes will have standard names
158
+
159
+        app_params = \
160
+            {
161
+                'create_mode': "openstack",
162
+                'uuid': str(volume['id']),
163
+                'name': str(volume['id']),
164
+                'access_control_mode': 'allow_all',
165
+                'storage_instances': {
166
+                    DEFAULT_STORAGE_NAME: {
167
+                        'name': DEFAULT_STORAGE_NAME,
168
+                        'volumes': {
169
+                            DEFAULT_VOLUME_NAME: {
170
+                                'name': DEFAULT_VOLUME_NAME,
171
+                                'size': volume['size'],
172
+                                'replica_count': int(self.num_replicas),
173
+                                'snapshot_policies': {
174
+                                }
175
+                            }
176
+                        }
177
+                    }
178
+                }
179
+            }
180
+        self._create_resource(volume, 'app_instances', body=app_params)
181
+
182
+    def extend_volume(self, volume, new_size):
183
+        # Offline App Instance, if necessary
184
+        reonline = False
185
+        app_inst = self._issue_api_request(
186
+            "app_instances/{}".format(volume['id']))
187
+        if app_inst['admin_state'] == 'online':
188
+            reonline = True
189
+            self.detach_volume(None, volume)
190
+        # Change Volume Size
191
+        app_inst = volume['id']
192
+        storage_inst = DEFAULT_STORAGE_NAME
193
+        data = {
194
+            'size': new_size
195
+        }
196
+        self._issue_api_request(
197
+            'app_instances/{}/storage_instances/{}/volumes/{}'.format(
198
+                app_inst, storage_inst, DEFAULT_VOLUME_NAME),
199
+            method='put', body=data)
200
+        # Online Volume, if it was online before
201
+        if reonline:
202
+            self.create_export(None, volume)
203
+
204
+    def create_cloned_volume(self, volume, src_vref):
205
+        clone_src_template = "/app_instances/{}/storage_instances/{" + \
206
+                             "}/volumes/{}"
207
+        src = clone_src_template.format(src_vref['id'], DEFAULT_STORAGE_NAME,
208
+                                        DEFAULT_VOLUME_NAME)
209
+        data = {
210
+            'create_mode': 'openstack',
211
+            'name': str(volume['id']),
212
+            'uuid': str(volume['id']),
213
+            'clone_src': src,
214
+            'access_control_mode': 'allow_all'
215
+        }
216
+        self._issue_api_request('app_instances', 'post', body=data)
217
+
218
+    def delete_volume(self, volume):
219
+        self.detach_volume(None, volume)
220
+        app_inst = volume['id']
221
+        try:
222
+            self._issue_api_request('app_instances/{}'.format(app_inst),
223
+                                    method='delete')
224
+        except exception.NotFound:
225
+            msg = _("Tried to delete volume %s, but it was not found in the "
226
+                    "Datera cluster. Continuing with delete.")
227
+            LOG.info(msg, volume['id'])
228
+
229
+    def ensure_export(self, context, volume):
230
+        """Gets the associated account, retrieves CHAP info and updates."""
231
+        return self.create_export(context, volume)
232
+
233
+    def create_export(self, context, volume):
234
+        url = "app_instances/{}".format(volume['id'])
235
+        data = {
236
+            'admin_state': 'online'
237
+        }
238
+        app_inst = self._issue_api_request(url, method='put', body=data)
239
+        storage_instance = app_inst['storage_instances'][
240
+            DEFAULT_STORAGE_NAME]
241
+
242
+        # Portal, IQN, LUNID
243
+        portal = storage_instance['access']['ips'][0] + ':3260'
244
+        iqn = storage_instance['access']['iqn']
245
+
246
+        provider_location = '%s %s %s' % (portal, iqn, self._get_lunid())
247
+        return {'provider_location': provider_location}
248
+
249
+    def detach_volume(self, context, volume, attachment=None):
250
+        url = "app_instances/{}".format(volume['id'])
251
+        data = {
252
+            'admin_state': 'offline',
253
+            'force': True
254
+        }
255
+        try:
256
+            self._issue_api_request(url, method='put', body=data)
257
+        except exception.NotFound:
258
+            msg = _("Tried to detach volume %s, but it was not found in the "
259
+                    "Datera cluster. Continuing with detach.")
260
+            LOG.info(msg, volume['id'])
261
+
262
+    def create_snapshot(self, snapshot):
263
+        url_template = 'app_instances/{}/storage_instances/{}/volumes/{' \
264
+                       '}/snapshots'
265
+        url = url_template.format(snapshot['volume_id'],
266
+                                  DEFAULT_STORAGE_NAME,
267
+                                  DEFAULT_VOLUME_NAME)
268
+
269
+        snap_params = {
270
+            'uuid': snapshot['id'],
271
+        }
272
+        self._issue_api_request(url, method='post', body=snap_params)
273
+
274
+    def delete_snapshot(self, snapshot):
275
+        snap_temp = 'app_instances/{}/storage_instances/{}/volumes/{' \
276
+                    '}/snapshots'
277
+        snapu = snap_temp.format(snapshot['volume_id'],
278
+                                 DEFAULT_STORAGE_NAME,
279
+                                 DEFAULT_VOLUME_NAME)
280
+
281
+        snapshots = self._issue_api_request(snapu, method='get')
282
+
283
+        try:
284
+            for ts, snap in snapshots.viewitems():
285
+                if snap['uuid'] == snapshot['id']:
286
+                    url_template = snapu + '/{}'
287
+                    url = url_template.format(ts)
288
+                    self._issue_api_request(url, method='delete')
289
+                    break
290
+            else:
291
+                raise exception.NotFound
292
+        except exception.NotFound:
293
+            msg = _LI("Tried to delete snapshot %s, but was not found in "
294
+                      "Datera cluster. Continuing with delete.")
295
+            LOG.info(msg, snapshot['id'])
296
+
297
+    def create_volume_from_snapshot(self, volume, snapshot):
298
+        snap_temp = 'app_instances/{}/storage_instances/{}/volumes/{' \
299
+                    '}/snapshots'
300
+        snapu = snap_temp.format(snapshot['volume_id'],
301
+                                 DEFAULT_STORAGE_NAME,
302
+                                 DEFAULT_VOLUME_NAME)
303
+
304
+        snapshots = self._issue_api_request(snapu, method='get')
305
+        for ts, snap in snapshots.viewitems():
306
+            if snap['uuid'] == snapshot['id']:
307
+                found_ts = ts
308
+                break
309
+        else:
310
+            raise exception.NotFound
311
+
312
+        src = '/app_instances/{}/storage_instances/{}/volumes/{' \
313
+            '}/snapshots/{}'.format(
314
+                snapshot['volume_id'],
315
+                DEFAULT_STORAGE_NAME,
316
+                DEFAULT_VOLUME_NAME,
317
+                found_ts)
318
+        app_params = \
319
+            {
320
+                'create_mode': 'openstack',
321
+                'uuid': str(volume['id']),
322
+                'name': str(volume['id']),
323
+                'clone_src': src,
324
+                'access_control_mode': 'allow_all'
325
+            }
326
+        self._issue_api_request('app_instances', method='post', body=app_params)
327
+
328
+    def get_volume_stats(self, refresh=False):
329
+        """Get volume stats.
330
+
331
+        If 'refresh' is True, run update first.
332
+        The name is a bit misleading as
333
+        the majority of the data here is cluster
334
+        data.
335
+        """
336
+        if refresh or not self.cluster_stats:
337
+            try:
338
+                self._update_cluster_stats()
339
+            except exception.DateraAPIException:
340
+                LOG.error('Failed to get updated stats from Datera cluster.')
341
+
342
+        return self.cluster_stats
343
+
344
+    def _update_cluster_stats(self):
345
+        LOG.debug("Updating cluster stats info.")
346
+
347
+        results = self._issue_api_request('system')
348
+
349
+        if 'uuid' not in results:
350
+            LOG.error(_LE('Failed to get updated stats from Datera Cluster.'))
351
+
352
+        backend_name = self.configuration.safe_get('volume_backend_name')
353
+        stats = {
354
+            'volume_backend_name': backend_name or 'Datera',
355
+            'vendor_name': 'Datera',
356
+            'driver_version': self.VERSION,
357
+            'storage_protocol': 'iSCSI',
358
+            'total_capacity_gb': int(results['total_capacity']) / units.Gi,
359
+            'free_capacity_gb': int(results['available_capacity']) / units.Gi,
360
+            'reserved_percentage': 0,
361
+        }
362
+
363
+        self.cluster_stats = stats
364
+
365
+    def _login(self):
366
+        """Use the san_login and san_password to set self.auth_token."""
367
+        body = {
368
+            'name': self.username,
369
+            'password': self.password
370
+        }
371
+
372
+        # Unset token now, otherwise potential expired token will be sent
373
+        # along to be used for authorization when trying to login.
374
+        self.auth_token = None
375
+
376
+        try:
377
+            LOG.debug('Getting Datera auth token.')
378
+            results = self._issue_api_request('login', 'put', body=body,
379
+                                              sensitive=True)
380
+            self.auth_token = results['key']
381
+            self.configuration.datera_api_token = results['key']
382
+        except exception.NotAuthorized:
383
+            with excutils.save_and_reraise_exception():
384
+                LOG.error(_LE('Logging into the Datera cluster failed. Please '
385
+                              'check your username and password set in the '
386
+                              'cinder.conf and start the cinder-volume'
387
+                              'service again.'))
388
+
389
+    @_authenticated
390
+    def _issue_api_request(self, resource_type, method='get', resource=None,
391
+                           body=None, action=None, sensitive=False):
392
+        """All API requests to Datera cluster go through this method.
393
+
394
+        :param resource_type: the type of the resource
395
+        :param method: the request verb
396
+        :param resource: the identifier of the resource
397
+        :param body: a dict with options for the action_type
398
+        :param action: the action to perform
399
+        :returns: a dict of the response from the Datera cluster
400
+        """
401
+        host = self.configuration.san_ip
402
+        port = self.configuration.datera_api_port
403
+        api_token = self.configuration.datera_api_token
404
+        api_version = self.configuration.datera_api_version
405
+
406
+        payload = json.dumps(body, ensure_ascii=False)
407
+        payload.encode('utf-8')
408
+
409
+        if not sensitive:
410
+            LOG.debug("Payload for Datera API call: %s", payload)
411
+
412
+        header = {'Content-Type': 'application/json; charset=utf-8'}
413
+
414
+        protocol = 'http'
415
+        if self.configuration.driver_use_ssl:
416
+            protocol = 'https'
417
+
418
+        # TODO(thingee): Auth method through Auth-Token is deprecated. Remove
419
+        # this and client cert verification stuff in the Liberty release.
420
+        if api_token:
421
+            header['Auth-Token'] = api_token
422
+
423
+        client_cert = self.configuration.driver_client_cert
424
+        client_cert_key = self.configuration.driver_client_cert_key
425
+        cert_data = None
426
+
427
+        if client_cert:
428
+            protocol = 'https'
429
+            cert_data = (client_cert, client_cert_key)
430
+
431
+        connection_string = '%s://%s:%s/v%s/%s' % (protocol, host, port,
432
+                                                   api_version, resource_type)
433
+
434
+        if resource is not None:
435
+            connection_string += '/%s' % resource
436
+        if action is not None:
437
+            connection_string += '/%s' % action
438
+
439
+        LOG.debug("Endpoint for Datera API call: %s", connection_string)
440
+        try:
441
+            response = getattr(requests, method)(connection_string,
442
+                                                 data=payload, headers=header,
443
+                                                 verify=False, cert=cert_data)
444
+        except requests.exceptions.RequestException as ex:
445
+            msg = _('Failed to make a request to Datera cluster endpoint due '
446
+                    'to the following reason: %s') % ex.message
447
+            LOG.error(msg)
448
+            raise exception.DateraAPIException(msg)
449
+
450
+        data = response.json()
451
+        if not sensitive:
452
+            LOG.debug("Results of Datera API call: %s", data)
453
+
454
+        if not response.ok:
455
+            LOG.debug(_(response.url))
456
+            LOG.debug(_(payload))
457
+            LOG.debug(_(vars(response)))
458
+            if response.status_code == 404:
459
+                raise exception.NotFound(data['message'])
460
+            elif response.status_code in [403, 401]:
461
+                raise exception.NotAuthorized()
462
+            else:
463
+                msg = _('Request to Datera cluster returned bad status:'
464
+                        ' %(status)s | %(reason)s') % {
465
+                            'status': response.status_code,
466
+                            'reason': response.reason}
467
+                LOG.error(msg)
468
+                raise exception.DateraAPIException(msg)
469
+
470
+        return data

+ 26
- 0
deployment_scripts/puppet/modules/cinder_datera_driver/manifests/cinder.pp View File

@@ -0,0 +1,26 @@
1
+notice('PLUGIN: cinder_datera_driver::cinder: cinder.pp')
2
+
3
+class cinder_datera_driver::cinder {
4
+    $version = hiera('fuel_version')
5
+
6
+    # install the driver, only required on cinder nodes
7
+    notice("PLUGIN: cinder_datera_driver::cinder: trying to install Fuel $version plugin.")
8
+    if($version == '7.0') {
9
+        file { "/usr/lib/python2.7/dist-packages/cinder/volume/drivers/datera.py":
10
+            mode => "0644",
11
+            owner => 'root',
12
+            group => 'root',
13
+            source => 'puppet:///modules/cinder_datera_driver/7.0/datera.py',
14
+        }
15
+    } elsif ($version == '8.0') {
16
+        file { "/usr/lib/python2.7/dist-packages/cinder/volume/drivers/datera.py":
17
+            mode => "0644",
18
+            owner => 'root',
19
+            group => 'root',
20
+            source => 'puppet:///modules/cinder_datera_driver/8.0/datera.py',
21
+        }
22
+    } else {
23
+        notice("PLUGIN: cinder_datera_driver::cinder: $version is not supported by us.")
24
+    }
25
+}
26
+class { 'cinder_datera_driver::cinder': }

+ 19
- 0
deployment_tasks.yaml View File

@@ -0,0 +1,19 @@
1
+- id: cinder-datera-driver
2
+  type: puppet
3
+  role: [cinder]
4
+  required_for: [post_deployment_end]
5
+  requires: [post_deployment_start]
6
+  parameters:
7
+    puppet_manifest: puppet/manifests/cinder_datera_driver.pp
8
+    puppet_modules:  "puppet/modules/:/etc/puppet/modules/"
9
+    timeout: 360
10
+
11
+- id: cinder-datera-config
12
+  type: puppet
13
+  role: [cinder]
14
+  required_for: [post_deployment_end]
15
+  requires: [post_deployment_start, cinder-datera-driver]
16
+  parameters:
17
+    puppet_manifest: puppet/manifests/cinder_datera_config.pp
18
+    puppet_modules:  "puppet/modules/:/etc/puppet/modules/"
19
+    timeout: 360

+ 34
- 0
environment_config.yaml View File

@@ -0,0 +1,34 @@
1
+attributes:
2
+  multibackend:
3
+    value: false
4
+    label: 'Multibackend enabled'
5
+    description: 'Datera driver will be used with Cinder Multibackend feature'
6
+    weight: 15
7
+    type: "checkbox"
8
+  datera_mvip:
9
+    value: ''
10
+    label: 'Cluster Management VIP (san_ip)'
11
+    description: 'The hostname (or IP address) for the Datera management API endpoint.'
12
+    weight: 20
13
+    type: "text"
14
+  datera_admin_login:
15
+    value: ''
16
+    label: 'Login for Admin account (san_login)'
17
+    description: 'account used by Cinder service.'
18
+    weight: 30
19
+    type: "text"
20
+    regex:
21
+      source: '\S'
22
+      error: "Username field cannot be empty"
23
+  datera_admin_password:
24
+    value: ''
25
+    label: 'Password for Admin account (san_password)'
26
+    description: 'account used by Cinder service.'
27
+    weight: 40
28
+    type: "password"
29
+  datera_num_replicas:
30
+    value: '2'
31
+    label: 'Data replication factor'
32
+    description: 'Repliacte data X times over the cluster'
33
+    weight: 50
34
+    type: "text"

+ 24
- 0
metadata.yaml View File

@@ -0,0 +1,24 @@
1
+name: fuel-plugin-datera-cinder
2
+title: Fuel Datera driver for Cinder
3
+version: '0.1.43'
4
+description: Installs and enables the Datera driver in Cinder
5
+fuel_version: ['7.0']
6
+licenses: ['Apache License Version 2.0']
7
+authors: [ 'Funs Kessen <funs@barred.org>' ]
8
+homepage: 'https://github.com/stackforge/fuel-plugin-datera-cinder'
9
+groups: ['storage::cinder']
10
+
11
+releases:
12
+  - os: ubuntu
13
+    version: 2015.1-7.0
14
+    mode: ['ha', 'multinode']
15
+    deployment_scripts_path: deployment_scripts/
16
+    repository_path: repositories/ubuntu
17
+  - os: centos
18
+    version: 2015.1.0-7.0
19
+    mode: ['ha', 'multinode']
20
+    deployment_scripts_path: deployment_scripts/
21
+    repository_path: repositories/centos
22
+
23
+# Version of plugin package
24
+package_version: '3.0.0'

+ 5
- 0
pre_build_hook View File

@@ -0,0 +1,5 @@
1
+#!/bin/bash
2
+
3
+# Add here any the actions which are required before plugin build
4
+# like packages building, packages downloading from mirrors and so on.
5
+# The script should return 0 if there were no errors.

+ 131
- 0
specs/datera-plugin-specs.rst View File

@@ -0,0 +1,131 @@
1
+
2
+ This work is licensed under the Apache License, Version 2.0.
3
+
4
+ http://www.apache.org/licenses/LICENSE-2.0
5
+
6
+==================================================
7
+Fuel plugin for Datera as a Cinder backend
8
+==================================================
9
+
10
+The Datera plugin for Fuel extends Mirantis OpenStack functionality by adding
11
+support for Datera EBS in Cinder using the iSCSI protocol.
12
+
13
+Problem description
14
+===================
15
+
16
+Currently, Fuel has no support for Datera EBS as block storage for
17
+OpenStack environments. The Datera plugin aims to provide support for it.
18
+
19
+Proposed change
20
+===============
21
+
22
+Implement a Fuel plugin that will configure the Datera driver for
23
+Cinder on all Controller nodes. 
24
+
25
+Alternatives
26
+------------
27
+
28
+None
29
+
30
+Data model impact
31
+-----------------
32
+
33
+None
34
+
35
+REST API impact
36
+---------------
37
+
38
+None
39
+
40
+Upgrade impact
41
+--------------
42
+
43
+None
44
+
45
+Security impact
46
+---------------
47
+
48
+None
49
+
50
+Notifications impact
51
+--------------------
52
+
53
+None
54
+
55
+Other end user impact
56
+---------------------
57
+
58
+None
59
+
60
+Performance Impact
61
+------------------
62
+
63
+The Datera EBS provides high performance block storage for OpenStack 
64
+environments, and therefore enabling the Datera driver in OpenStack
65
+will greatly improve the peformance of OpenStack.
66
+
67
+Other deployer impact
68
+---------------------
69
+
70
+The deployer should make sure the IP of the management VIP is correct, prior
71
+to deploying the Fuel plugin to the controllers. If this is not done the VIP
72
+needs to altered and the cinder service will have to be restarted.
73
+
74
+Developer impact
75
+----------------
76
+
77
+None
78
+
79
+Implementation
80
+==============
81
+
82
+The plugin generates the approriate cinder.conf stanzas to enable the Datera
83
+within OpenStack. There are NO other packages required, the Datera driver
84
+which is included in the OpenStack distribution is all that is necessary.
85
+
86
+Plugin has two tasks. Each task per role. They are run in the following order:
87
+
88
+* The first task installs and configures cinder-volume on Primary Controller.
89
+* The second task installs and configures cinder-volume on Controller nodes.
90
+
91
+Cinder-volume service is installed on all Controller nodes and is managed by
92
+Pacemaker. It runs in active/passive mode where only one instance is active.
93
+All instances of cinder-volume have the same “host” parameter in cinder.conf
94
+file. This is required to achieve ability to manage all volumes in the
95
+environment by any cinder-volume instance.
96
+
97
+Assignee(s)
98
+-----------
99
+
100
+| Funs <funs@barred.org>
101
+
102
+Work Items
103
+----------
104
+
105
+* Implement the Fuel plugin.
106
+* Implement the Puppet manifests.
107
+* Testing.
108
+* Write the documentation.
109
+
110
+Dependencies
111
+============
112
+
113
+* Fuel 7.0 and higher.
114
+
115
+Testing
116
+=======
117
+
118
+* Prepare a test plan.
119
+* Test the plugin by deploying environments with all Fuel deployment modes.
120
+
121
+Documentation Impact
122
+====================
123
+
124
+* Deployment Guide (how to install the storage backends, how to prepare an
125
+  environment for installation, how to install the plugin, how to deploy an
126
+  OpenStack environment with the plugin).
127
+* User Guide (which features the plugin provides, how to use them in the
128
+  deployed OpenStack environment).
129
+* Test Plan.
130
+* Test Report.
131
+

+ 8
- 0
volumes.yaml View File

@@ -0,0 +1,8 @@
1
+volumes_roles_mapping:
2
+  # Default role mapping
3
+  cinder_datera:
4
+    - {allocate_size: "min", id: "os"}
5
+
6
+# Set here new volumes for your role
7
+volumes: []
8
+

Loading…
Cancel
Save