Browse Source

Merge "Support create amphora instance from volume based."

changes/44/681144/3
Zuul 1 week ago
parent
commit
b7278ceab4

+ 1
- 0
devstack/plugin.sh View File

@@ -271,6 +271,7 @@ function octavia_configure {
271 271
     # Setting other required default options
272 272
     iniset $OCTAVIA_CONF controller_worker amphora_driver ${OCTAVIA_AMPHORA_DRIVER}
273 273
     iniset $OCTAVIA_CONF controller_worker compute_driver ${OCTAVIA_COMPUTE_DRIVER}
274
+    iniset $OCTAVIA_CONF controller_worker volume_driver  ${OCTAVIA_VOLUME_DRIVER}
274 275
     iniset $OCTAVIA_CONF controller_worker network_driver ${OCTAVIA_NETWORK_DRIVER}
275 276
     iniset $OCTAVIA_CONF controller_worker amp_image_tag ${OCTAVIA_AMP_IMAGE_TAG}
276 277
 

+ 1
- 0
devstack/settings View File

@@ -20,6 +20,7 @@ OCTAVIA_RUN_DIR=${OCTAVIA_RUN_DIR:-"/var/run/octavia"}
20 20
 OCTAVIA_AMPHORA_DRIVER=${OCTAVIA_AMPHORA_DRIVER:-"amphora_haproxy_rest_driver"}
21 21
 OCTAVIA_NETWORK_DRIVER=${OCTAVIA_NETWORK_DRIVER:-"allowed_address_pairs_driver"}
22 22
 OCTAVIA_COMPUTE_DRIVER=${OCTAVIA_COMPUTE_DRIVER:-"compute_nova_driver"}
23
+OCTAVIA_VOLUME_DRIVER=${OCTAVIA_VOLUME_DRIVER:-"volume_noop_driver"}
23 24
 
24 25
 OCTAVIA_USERNAME=${OCTAVIA_ADMIN_USER:-"admin"}
25 26
 OCTAVIA_PASSWORD=${OCTAVIA_PASSWORD:-${ADMIN_PASSWORD}}

+ 42
- 0
etc/octavia.conf View File

@@ -243,6 +243,10 @@
243 243
 #                            allowed_address_pairs_driver
244 244
 #
245 245
 # network_driver = network_noop_driver
246
+# Volume driver options are volume_noop_driver
247
+#                           volume_cinder_driver
248
+#
249
+# volume_driver = volume_noop_driver
246 250
 #
247 251
 # Distributor driver options are distributor_noop_driver
248 252
 #                                single_VIP_amphora
@@ -421,6 +425,44 @@
421 425
 # Nova supports: anti-affinity and soft-anti-affinity
422 426
 # anti_affinity_policy = anti-affinity
423 427
 
428
+[cinder]
429
+# The name of the cinder service in the keystone catalog
430
+# service_name =
431
+# Custom cinder endpoint if override is necessary
432
+# endpoint =
433
+
434
+# Region in Identity service catalog to use for communication with the
435
+# OpenStack services.
436
+# region_name =
437
+
438
+# Endpoint type in Identity service catalog to use for communication with
439
+# the OpenStack services.
440
+# endpoint_type = publicURL
441
+
442
+# Availability zone to use for creating Volume
443
+# availability_zone =
444
+
445
+# CA certificates file to verify cinder connections when TLS is enabled
446
+# insecure = False
447
+# ca_certificates_file =
448
+
449
+# Size of root volume in GB for Amphora Instance when use Cinder
450
+# In some storage backends such as ScaleIO, the size of volume is multiple of 8
451
+# volume_size = 16
452
+
453
+# Volume type to be used for Amphora Instance root disk
454
+# If not specified, default_volume_type from cinder.conf will be used
455
+# volume_type =
456
+
457
+# Interval time to wait until volume becomes available
458
+# volume_create_retry_interval = 5
459
+
460
+# Timeout to wait for volume creation success
461
+# volume_create_timeout = 300
462
+
463
+# Maximum number of retries to create volume
464
+# volume_create_max_retries = 5
465
+
424 466
 [glance]
425 467
 # The name of the glance service in the keystone catalog
426 468
 # service_name =

+ 1
- 0
lower-constraints.txt View File

@@ -172,3 +172,4 @@ WebTest==2.0.29
172 172
 Werkzeug==0.14.1
173 173
 wrapt==1.10.11
174 174
 WSME==0.8.0
175
+python-cinderclient==3.3.0

+ 42
- 0
octavia/common/clients.py View File

@@ -10,6 +10,7 @@
10 10
 #    License for the specific language governing permissions and limitations
11 11
 #    under the License.
12 12
 
13
+from cinderclient import client as cinder_client
13 14
 from glanceclient import client as glance_client
14 15
 from neutronclient.neutron import client as neutron_client
15 16
 from novaclient import api_versions
@@ -26,6 +27,7 @@ CONF = cfg.CONF
26 27
 GLANCE_VERSION = '2'
27 28
 NEUTRON_VERSION = '2.0'
28 29
 NOVA_VERSION = '2.15'
30
+CINDER_VERSION = '3'
29 31
 
30 32
 
31 33
 class NovaAuth(object):
@@ -143,3 +145,43 @@ class GlanceAuth(object):
143 145
                 with excutils.save_and_reraise_exception():
144 146
                     LOG.exception("Error creating Glance client.")
145 147
         return cls.glance_client
148
+
149
+
150
+class CinderAuth(object):
151
+    cinder_client = None
152
+
153
+    @classmethod
154
+    def get_cinder_client(cls, region, service_name=None, endpoint=None,
155
+                          endpoint_type='publicURL', insecure=False,
156
+                          cacert=None):
157
+        """Create cinder client object.
158
+
159
+        :param region: The region of the service
160
+        :param service_name: The name of the cinder service in the catalog
161
+        :param endpoint: The endpoint of the service
162
+        :param endpoint_type: The endpoint type of the service
163
+        :param insecure: Turn off certificate validation
164
+        :param cacert: CA Cert file path
165
+        :return: a Cinder Client object
166
+        :raise Exception: if the client cannot be created
167
+        """
168
+        ksession = keystone.KeystoneSession()
169
+        if not cls.cinder_client:
170
+            kwargs = {'region_name': region,
171
+                      'session': ksession.get_session(),
172
+                      'interface': endpoint_type}
173
+            if service_name:
174
+                kwargs['service_name'] = service_name
175
+            if endpoint:
176
+                kwargs['endpoint'] = endpoint
177
+                if endpoint.startwith("https"):
178
+                    kwargs['insecure'] = insecure
179
+                    kwargs['cacert'] = cacert
180
+            try:
181
+                cls.cinder_client = cinder_client.Client(
182
+                    CINDER_VERSION, **kwargs
183
+                )
184
+            except Exception:
185
+                with excutils.save_and_reraise_exception():
186
+                    LOG.exception("Error creating Cinder client.")
187
+        return cls.cinder_client

+ 37
- 0
octavia/common/config.py View File

@@ -410,6 +410,10 @@ controller_worker_opts = [
410 410
     cfg.StrOpt('network_driver',
411 411
                default='network_noop_driver',
412 412
                help=_('Name of the network driver to use')),
413
+    cfg.StrOpt('volume_driver',
414
+               default=constants.VOLUME_NOOP_DRIVER,
415
+               choices=constants.SUPPORTED_VOLUME_DRIVERS,
416
+               help=_('Name of the volume driver to use')),
413 417
     cfg.StrOpt('distributor_driver',
414 418
                default='distributor_noop_driver',
415 419
                help=_('Name of the distributor driver to use')),
@@ -560,6 +564,38 @@ nova_opts = [
560 564
     cfg.StrOpt('availability_zone', default=None,
561 565
                help=_('Availability zone to use for creating Amphorae')),
562 566
 ]
567
+
568
+cinder_opts = [
569
+    cfg.StrOpt('service_name',
570
+               help=_('The name of the cinder service in the keystone '
571
+                      'catalog')),
572
+    cfg.StrOpt('endpoint', help=_('A new endpoint to override the endpoint '
573
+                                  'in the keystone catalog.')),
574
+    cfg.StrOpt('region_name',
575
+               help=_('Region in Identity service catalog to use for '
576
+                      'communication with the OpenStack services.')),
577
+    cfg.StrOpt('endpoint_type', default='publicURL',
578
+               help=_('Endpoint interface in identity service to use')),
579
+    cfg.StrOpt('ca_certificates_file',
580
+               help=_('CA certificates file path')),
581
+    cfg.StrOpt('availability_zone', default=None,
582
+               help=_('Availability zone to use for creating Volume')),
583
+    cfg.BoolOpt('insecure',
584
+                default=False,
585
+                help=_('Disable certificate validation on SSL connections')),
586
+    cfg.IntOpt('volume_size', default=16,
587
+               help=_('Size of volume for Amphora instance')),
588
+    cfg.StrOpt('volume_type', default=None,
589
+               help=_('Type of volume for Amphorae volume root disk')),
590
+    cfg.IntOpt('volume_create_retry_interval', default=5,
591
+               help=_('Interval time to wait volume is created in available'
592
+                      'state')),
593
+    cfg.IntOpt('volume_create_timeout', default=300,
594
+               help=_('Timeout to wait for volume creation success')),
595
+    cfg.IntOpt('volume_create_max_retries', default=5,
596
+               help=_('Maximum number of retries to create volume'))
597
+]
598
+
563 599
 neutron_opts = [
564 600
     cfg.StrOpt('service_name',
565 601
                help=_('The name of the neutron service in the '
@@ -685,6 +721,7 @@ cfg.CONF.register_cli_opts(core_cli_opts)
685 721
 cfg.CONF.register_opts(certificate_opts, group='certificates')
686 722
 cfg.CONF.register_cli_opts(healthmanager_opts, group='health_manager')
687 723
 cfg.CONF.register_opts(nova_opts, group='nova')
724
+cfg.CONF.register_opts(cinder_opts, group='cinder')
688 725
 cfg.CONF.register_opts(glance_opts, group='glance')
689 726
 cfg.CONF.register_opts(neutron_opts, group='neutron')
690 727
 cfg.CONF.register_opts(quota_opts, group='quotas')

+ 10
- 0
octavia/common/constants.py View File

@@ -704,3 +704,13 @@ L4_PROTOCOL_MAP = {
704 704
     PROTOCOL_PROXY: PROTOCOL_TCP,
705 705
     PROTOCOL_UDP: PROTOCOL_UDP,
706 706
 }
707
+
708
+# Volume drivers
709
+VOLUME_NOOP_DRIVER = 'volume_noop_driver'
710
+SUPPORTED_VOLUME_DRIVERS = [VOLUME_NOOP_DRIVER,
711
+                            'volume_cinder_driver']
712
+
713
+# Cinder volume driver constants
714
+CINDER_STATUS_AVAILABLE = 'available'
715
+CINDER_STATUS_ERROR = 'error'
716
+CINDER_ACTION_CREATE_VOLUME = 'create volume'

+ 8
- 0
octavia/common/exceptions.py View File

@@ -383,3 +383,11 @@ class ObjectInUse(APIException):
383 383
 class ProviderFlavorMismatchError(APIException):
384 384
     msg = _("Flavor '%(flav)s' is not compatible with provider '%(prov)s'")
385 385
     code = 400
386
+
387
+
388
+class VolumeDeleteException(OctaviaException):
389
+    message = _('Failed to delete volume instance.')
390
+
391
+
392
+class VolumeGetException(OctaviaException):
393
+    message = _('Failed to retrieve volume instance.')

+ 51
- 2
octavia/compute/drivers/nova_driver.py View File

@@ -18,6 +18,7 @@ import string
18 18
 from novaclient import exceptions as nova_exceptions
19 19
 from oslo_config import cfg
20 20
 from oslo_log import log as logging
21
+from stevedore import driver as stevedore_driver
21 22
 
22 23
 from octavia.common import clients
23 24
 from octavia.common import constants
@@ -88,6 +89,11 @@ class VirtualMachineManager(compute_base.ComputeBase):
88 89
         self.manager = self._nova_client.servers
89 90
         self.server_groups = self._nova_client.server_groups
90 91
         self.flavor_manager = self._nova_client.flavors
92
+        self.volume_driver = stevedore_driver.DriverManager(
93
+            namespace='octavia.volume.drivers',
94
+            name=CONF.controller_worker.volume_driver,
95
+            invoke_on_load=True
96
+        ).driver
91 97
 
92 98
     def build(self, name="amphora_name", amphora_flavor=None,
93 99
               image_id=None, image_tag=None, image_owner=None,
@@ -122,6 +128,7 @@ class VirtualMachineManager(compute_base.ComputeBase):
122 128
 
123 129
         '''
124 130
 
131
+        volume_id = None
125 132
         try:
126 133
             network_ids = network_ids or []
127 134
             port_ids = port_ids or []
@@ -143,9 +150,25 @@ class VirtualMachineManager(compute_base.ComputeBase):
143 150
                     [r.choice(string.ascii_uppercase + string.digits)
144 151
                      for i in range(CONF.nova.random_amphora_name_length - 1)]
145 152
                 ))
146
-
153
+            block_device_mapping = {}
154
+            if CONF.controller_worker.volume_driver != \
155
+                    constants.VOLUME_NOOP_DRIVER:
156
+                # creating volume
157
+                LOG.debug('Creating volume for amphora from image %s',
158
+                          image_id)
159
+                volume_id = self.volume_driver.create_volume_from_image(
160
+                    image_id)
161
+                LOG.debug('Created boot volume %s for amphora', volume_id)
162
+                # If use volume based, does not require image ID anymore
163
+                image_id = None
164
+                # Boot from volume with parameters: target device name = vda,
165
+                # device id = volume_id, device type and size unspecified,
166
+                # delete-on-terminate = true (volume will be deleted by Nova
167
+                # on instance termination)
168
+                block_device_mapping = {'vda': '%s:::true' % volume_id}
147 169
             amphora = self.manager.create(
148 170
                 name=name, image=image_id, flavor=amphora_flavor,
171
+                block_device_mapping=block_device_mapping,
149 172
                 key_name=key_name, security_groups=sec_groups,
150 173
                 nics=nics,
151 174
                 files=config_drive_files,
@@ -157,6 +180,9 @@ class VirtualMachineManager(compute_base.ComputeBase):
157 180
 
158 181
             return amphora.id
159 182
         except Exception as e:
183
+            if CONF.controller_worker.volume_driver != \
184
+                    constants.VOLUME_NOOP_DRIVER:
185
+                self.volume_driver.delete_volume(volume_id)
160 186
             LOG.exception("Nova failed to build the instance due to: %s", e)
161 187
             raise exceptions.ComputeBuildException(fault=e)
162 188
 
@@ -216,6 +242,7 @@ class VirtualMachineManager(compute_base.ComputeBase):
216 242
 
217 243
         lb_network_ip = None
218 244
         availability_zone = None
245
+        image_id = None
219 246
         fault = None
220 247
 
221 248
         try:
@@ -242,12 +269,34 @@ class VirtualMachineManager(compute_base.ComputeBase):
242 269
                       'os-interfaces extension failed.')
243 270
 
244 271
         fault = getattr(nova_response, 'fault', None)
272
+        if CONF.controller_worker.volume_driver == \
273
+                constants.VOLUME_NOOP_DRIVER:
274
+            image_id = nova_response.image.get("id")
275
+        else:
276
+            try:
277
+                volumes = self._nova_client.volumes.get_server_volumes(
278
+                    nova_response.id)
279
+            except Exception:
280
+                LOG.debug('Extracting volumes through nova '
281
+                          'os-volumes extension failed.')
282
+                volumes = []
283
+            if not volumes:
284
+                LOG.warning('Boot volume not found for volume backed '
285
+                            'amphora instance %s ', nova_response.id)
286
+            else:
287
+                if len(volumes) > 1:
288
+                    LOG.warning('Found more than one (%s) volumes '
289
+                                'for amphora instance %s',
290
+                                len(volumes), nova_response.id)
291
+                volume_id = volumes[0].volumeId
292
+                image_id = self.volume_driver.get_image_from_volume(volume_id)
293
+
245 294
         response = models.Amphora(
246 295
             compute_id=nova_response.id,
247 296
             status=nova_response.status,
248 297
             lb_network_ip=lb_network_ip,
249 298
             cached_zone=availability_zone,
250
-            image_id=nova_response.image.get("id"),
299
+            image_id=image_id,
251 300
             compute_flavor=nova_response.flavor.get("id")
252 301
         )
253 302
         return response, fault

+ 39
- 0
octavia/tests/unit/common/test_clients.py View File

@@ -10,6 +10,7 @@
10 10
 #    License for the specific language governing permissions and limitations
11 11
 #    under the License.
12 12
 
13
+import cinderclient.v3
13 14
 import glanceclient.v2
14 15
 import mock
15 16
 import neutronclient.v2_0
@@ -135,3 +136,41 @@ class TestGlanceAuth(base.TestCase):
135 136
             region="test-region", service_name="glanceEndpoint1",
136 137
             endpoint="test-endpoint", endpoint_type='publicURL', insecure=True)
137 138
         self.assertIs(bc1, bc2)
139
+
140
+
141
+class TestCinderAuth(base.TestCase):
142
+
143
+    def setUp(self):
144
+        # Reset the session and client
145
+        clients.CinderAuth.cinder_client = None
146
+        keystone._SESSION = None
147
+
148
+        super(TestCinderAuth, self).setUp()
149
+
150
+    @mock.patch('keystoneauth1.session.Session', mock.Mock())
151
+    def test_get_cinder_client(self):
152
+        # There should be no existing client
153
+        self.assertIsNone(
154
+            clients.CinderAuth.cinder_client
155
+        )
156
+
157
+        # Mock out the keystone session and get the client
158
+        keystone._SESSION = mock.MagicMock()
159
+        bc1 = clients.CinderAuth.get_cinder_client(
160
+            region=None, endpoint_type='publicURL', insecure=True)
161
+
162
+        # Our returned client should also be the saved client
163
+        self.assertIsInstance(
164
+            clients.CinderAuth.cinder_client,
165
+            cinderclient.v3.client.Client
166
+        )
167
+        self.assertIs(
168
+            clients.CinderAuth.cinder_client,
169
+            bc1
170
+        )
171
+
172
+        # Getting the session again should return the same object
173
+        bc2 = clients.CinderAuth.get_cinder_client(
174
+            region="test-region", service_name="cinderEndpoint1",
175
+            endpoint="test-endpoint", endpoint_type='publicURL', insecure=True)
176
+        self.assertIs(bc1, bc2)

+ 58
- 4
octavia/tests/unit/compute/drivers/test_nova_driver.py View File

@@ -96,12 +96,13 @@ class TestNovaClient(base.TestCase):
96 96
         conf.config(group="controller_worker",
97 97
                     amp_boot_network_list=['1', '2'])
98 98
         self.conf = conf
99
+        self.fake_image_uuid = uuidutils.generate_uuid()
99 100
 
100 101
         self.amphora = models.Amphora(
101 102
             compute_id=uuidutils.generate_uuid(),
102 103
             status='ACTIVE',
103 104
             lb_network_ip='10.0.0.1',
104
-            image_id=uuidutils.generate_uuid(),
105
+            image_id=self.fake_image_uuid,
105 106
             compute_flavor=uuidutils.generate_uuid()
106 107
         )
107 108
 
@@ -148,6 +149,9 @@ class TestNovaClient(base.TestCase):
148 149
         self.server_group_mock.policy = self.server_group_policy
149 150
         self.server_group_mock.id = self.server_group_id
150 151
 
152
+        self.volume_mock = mock.MagicMock()
153
+        setattr(self.volume_mock, 'volumeId', '1')
154
+
151 155
         self.port_id = uuidutils.generate_uuid()
152 156
         self.compute_id = uuidutils.generate_uuid()
153 157
         self.network_id = uuidutils.generate_uuid()
@@ -177,7 +181,39 @@ class TestNovaClient(base.TestCase):
177 181
             userdata='Blah',
178 182
             config_drive=True,
179 183
             scheduler_hints=None,
180
-            availability_zone=None
184
+            availability_zone=None,
185
+            block_device_mapping={}
186
+        )
187
+
188
+    @mock.patch('stevedore.driver.DriverManager.driver')
189
+    def test_build_with_cinder_volume(self, mock_driver):
190
+        self.conf.config(group="controller_worker",
191
+                         volume_driver='volume_cinder_driver')
192
+        self.manager.volume_driver = mock_driver
193
+        mock_driver.create_volume_from_image.return_value = 1
194
+        amphora_id = self.manager.build(amphora_flavor=1, image_id=1,
195
+                                        key_name=1,
196
+                                        sec_groups=1,
197
+                                        network_ids=[1],
198
+                                        port_ids=[2],
199
+                                        user_data='Blah',
200
+                                        config_drive_files='Files Blah')
201
+
202
+        self.assertEqual(self.amphora.compute_id, amphora_id)
203
+        mock_driver.create_volume_from_image.assert_called_with(1)
204
+        self.manager.manager.create.assert_called_with(
205
+            name="amphora_name",
206
+            nics=[{'net-id': 1}, {'port-id': 2}],
207
+            image=None,
208
+            flavor=1,
209
+            key_name=1,
210
+            security_groups=1,
211
+            files='Files Blah',
212
+            userdata='Blah',
213
+            config_drive=True,
214
+            scheduler_hints=None,
215
+            availability_zone=None,
216
+            block_device_mapping={'vda': '1:::true'}
181 217
         )
182 218
 
183 219
     def test_build_with_availability_zone(self):
@@ -205,7 +241,8 @@ class TestNovaClient(base.TestCase):
205 241
             userdata='Blah',
206 242
             config_drive=True,
207 243
             scheduler_hints=None,
208
-            availability_zone=FAKE_AZ
244
+            availability_zone=FAKE_AZ,
245
+            block_device_mapping={}
209 246
         )
210 247
 
211 248
     def test_build_with_random_amphora_name_length(self):
@@ -241,7 +278,8 @@ class TestNovaClient(base.TestCase):
241 278
             userdata='Blah',
242 279
             config_drive=True,
243 280
             scheduler_hints=None,
244
-            availability_zone=None
281
+            availability_zone=None,
282
+            block_device_mapping={}
245 283
         )
246 284
 
247 285
     def test_bad_build(self):
@@ -312,6 +350,22 @@ class TestNovaClient(base.TestCase):
312 350
         self.assertIsNone(amphora.lb_network_ip)
313 351
         self.nova_response.interface_list.called_with()
314 352
 
353
+    @mock.patch('stevedore.driver.DriverManager.driver')
354
+    def test_translate_amphora_use_cinder(self, mock_driver):
355
+        self.conf.config(group="controller_worker",
356
+                         volume_driver='volume_cinder_driver')
357
+        volumes_manager = self.manager._nova_client.volumes
358
+        volumes_manager.get_server_volumes.return_value = [self.volume_mock]
359
+        self.manager.volume_driver = mock_driver
360
+        mock_driver.get_image_from_volume.return_value = self.fake_image_uuid
361
+        amphora, fault = self.manager._translate_amphora(self.nova_response)
362
+        self.assertEqual(self.amphora, amphora)
363
+        self.assertEqual(self.nova_response.fault, fault)
364
+        self.nova_response.interface_list.called_with()
365
+        volumes_manager.get_server_volumes.assert_called_with(
366
+            self.nova_response.id)
367
+        mock_driver.get_image_from_volume.assert_called_with('1')
368
+
315 369
     def test_create_server_group(self):
316 370
         self.manager.server_groups.create.return_value = self.server_group_mock
317 371
 

+ 0
- 0
octavia/tests/unit/volume/__init__.py View File


+ 0
- 0
octavia/tests/unit/volume/drivers/__init__.py View File


+ 99
- 0
octavia/tests/unit/volume/drivers/test_cinder_driver.py View File

@@ -0,0 +1,99 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    a copy of the License at
4
+#
5
+#         http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#    Unless required by applicable law or agreed to in writing, software
8
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+from cinderclient import exceptions as cinder_exceptions
14
+import mock
15
+from oslo_config import cfg
16
+from oslo_config import fixture as oslo_fixture
17
+from oslo_utils import uuidutils
18
+
19
+from octavia.common import exceptions
20
+import octavia.tests.unit.base as base
21
+import octavia.volume.drivers.cinder_driver as cinder_common
22
+
23
+
24
+CONF = cfg.CONF
25
+
26
+
27
+class TestCinderClient(base.TestCase):
28
+
29
+    def setUp(self):
30
+        fake_uuid1 = uuidutils.generate_uuid()
31
+        fake_uuid2 = uuidutils.generate_uuid()
32
+        fake_uuid3 = uuidutils.generate_uuid()
33
+
34
+        conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
35
+        self.conf = conf
36
+
37
+        self.manager = cinder_common.VolumeManager()
38
+        self.manager.manager = mock.MagicMock()
39
+
40
+        self.cinder_response = mock.Mock()
41
+        self.cinder_response.id = fake_uuid1
42
+
43
+        self.manager.manager.get.return_value.status = 'available'
44
+        self.manager.manager.create.return_value = self.cinder_response
45
+        self.image_id = fake_uuid2
46
+        self.volume_id = fake_uuid3
47
+
48
+        super(TestCinderClient, self).setUp()
49
+
50
+    def test_create_volume_from_image(self):
51
+        self.conf.config(group="controller_worker",
52
+                         volume_driver='volume_cinder_driver')
53
+        self.conf.config(group="cinder", volume_create_retry_interval=0)
54
+        self.manager.create_volume_from_image(self.image_id)
55
+        self.manager.manager.create.assert_called_with(
56
+            size=16,
57
+            volume_type=None,
58
+            availability_zone=None,
59
+            imageRef=self.image_id)
60
+
61
+    def test_create_volume_from_image_error(self):
62
+        self.conf.config(group="controller_worker",
63
+                         volume_driver='volume_cinder_driver')
64
+        self.conf.config(group="cinder", volume_create_retry_interval=0)
65
+        self.manager.manager.get.return_value.status = 'error'
66
+        self.assertRaises(cinder_exceptions.ResourceInErrorState,
67
+                          self.manager.create_volume_from_image,
68
+                          self.image_id)
69
+
70
+    def test_build_cinder_volume_timeout(self):
71
+        self.conf.config(group="controller_worker",
72
+                         volume_driver='volume_cinder_driver')
73
+        self.conf.config(group="cinder", volume_create_timeout=0)
74
+        self.conf.config(group="cinder", volume_create_retry_interval=0)
75
+        self.manager.manager.get.return_value.status = 'build'
76
+        self.manager.create_volume_from_image.retry.sleep = mock.Mock()
77
+        self.assertRaises(cinder_exceptions.TimeoutException,
78
+                          self.manager.create_volume_from_image,
79
+                          self.image_id)
80
+
81
+    def test_get_image_from_volume(self):
82
+        self.conf.config(group="controller_worker",
83
+                         volume_driver='volume_cinder_driver')
84
+        self.conf.config(group="cinder",
85
+                         volume_create_retry_interval=0)
86
+        self.manager.get_image_from_volume(self.volume_id)
87
+        self.manager.manager.get.assert_called_with(
88
+            self.volume_id)
89
+
90
+    def test_get_image_from_volume_error(self):
91
+        self.conf.config(group="controller_worker",
92
+                         volume_driver='volume_cinder_driver')
93
+        self.conf.config(group="cinder",
94
+                         volume_create_retry_interval=0)
95
+        self.manager.manager.get.side_effect = [
96
+            exceptions.VolumeGetException('test_exception')]
97
+        self.assertRaises(exceptions.VolumeGetException,
98
+                          self.manager.get_image_from_volume,
99
+                          self.volume_id)

+ 46
- 0
octavia/tests/unit/volume/drivers/test_volume_noop_driver.py View File

@@ -0,0 +1,46 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    a copy of the License at
4
+#
5
+#         http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#    Unless required by applicable law or agreed to in writing, software
8
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+from oslo_config import cfg
14
+from oslo_utils import uuidutils
15
+
16
+import octavia.tests.unit.base as base
17
+from octavia.volume.drivers.noop_driver import driver
18
+
19
+
20
+CONF = cfg.CONF
21
+
22
+
23
+class TestNoopVolumeDriver(base.TestCase):
24
+    FAKE_UUID_1 = uuidutils.generate_uuid()
25
+    FAKE_UUID_2 = uuidutils.generate_uuid()
26
+
27
+    def setUp(self):
28
+        super(TestNoopVolumeDriver, self).setUp()
29
+        self.driver = driver.NoopVolumeDriver()
30
+
31
+        self.image_id = self.FAKE_UUID_1
32
+        self.volume_id = self.FAKE_UUID_2
33
+
34
+    def test_create_volume_from_image(self):
35
+        self.driver.create_volume_from_image(self.image_id)
36
+        self.assertEqual((self.image_id, 'create_volume_from_image'),
37
+                         self.driver.driver.volumeconfig[(
38
+                             self.image_id
39
+                         )])
40
+
41
+    def test_get_image_from_volume(self):
42
+        self.driver.get_image_from_volume(self.volume_id)
43
+        self.assertEqual((self.volume_id, 'get_image_from_volume'),
44
+                         self.driver.driver.volumeconfig[(
45
+                             self.volume_id
46
+                         )])

+ 0
- 0
octavia/volume/__init__.py View File


+ 0
- 0
octavia/volume/drivers/__init__.py View File


+ 123
- 0
octavia/volume/drivers/cinder_driver.py View File

@@ -0,0 +1,123 @@
1
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+#    not use this file except in compliance with the License. You may obtain
3
+#    a copy of the License at
4
+#
5
+#         http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+#    Unless required by applicable law or agreed to in writing, software
8
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+#    License for the specific language governing permissions and limitations
11
+#    under the License.
12
+
13
+import time
14
+
15
+from cinderclient import exceptions as cinder_exceptions
16
+from oslo_config import cfg
17
+from oslo_log import log as logging
18
+from tenacity import retry
19
+from tenacity import stop_after_attempt
20
+
21
+from octavia.common import clients
22
+from octavia.common import constants
23
+from octavia.common import exceptions
24
+from octavia.volume import volume_base
25
+
26
+LOG = logging.getLogger(__name__)
27
+
28
+CONF = cfg.CONF
29
+
30
+
31
+class VolumeManager(volume_base.VolumeBase):
32
+    '''Volume implementation of virtual machines via cinder.'''
33
+
34
+    def __init__(self):
35
+        super(VolumeManager, self).__init__()
36
+        # Must initialize cinder api
37
+        self._cinder_client = clients.CinderAuth.get_cinder_client(
38
+            service_name=CONF.cinder.service_name,
39
+            endpoint=CONF.cinder.endpoint,
40
+            region=CONF.cinder.region_name,
41
+            endpoint_type=CONF.cinder.endpoint_type,
42
+            insecure=CONF.cinder.insecure,
43
+            cacert=CONF.cinder.ca_certificates_file
44
+        )
45
+        self.manager = self._cinder_client.volumes
46
+
47
+    @retry(reraise=True,
48
+           stop=stop_after_attempt(CONF.cinder.volume_create_max_retries))
49
+    def create_volume_from_image(self, image_id):
50
+        """Create cinder volume
51
+
52
+        :param image_id: ID of amphora image
53
+
54
+        :return volume id
55
+        """
56
+        volume = self.manager.create(
57
+            size=CONF.cinder.volume_size,
58
+            volume_type=CONF.cinder.volume_type,
59
+            availability_zone=CONF.cinder.availability_zone,
60
+            imageRef=image_id)
61
+        resource_status = self.manager.get(volume.id).status
62
+
63
+        status = constants.CINDER_STATUS_AVAILABLE
64
+        start = int(time.time())
65
+
66
+        while resource_status != status:
67
+            time.sleep(CONF.cinder.volume_create_retry_interval)
68
+            instance_volume = self.manager.get(volume.id)
69
+            resource_status = instance_volume.status
70
+            if resource_status == constants.CINDER_STATUS_ERROR:
71
+                LOG.error('Error creating %s', instance_volume.id)
72
+                instance_volume.delete()
73
+                raise cinder_exceptions.ResourceInErrorState(
74
+                    obj=volume, fault_msg='Cannot create volume')
75
+            if int(time.time()) - start >= CONF.cinder.volume_create_timeout:
76
+                LOG.error('Timed out waiting to create cinder volume %s',
77
+                          instance_volume.id)
78
+                instance_volume.delete()
79
+                raise cinder_exceptions.TimeoutException(
80
+                    obj=volume, action=constants.CINDER_ACTION_CREATE_VOLUME)
81
+        return volume.id
82
+
83
+    def delete_volume(self, volume_id):
84
+        """Get glance image from volume
85
+
86
+        :param volume_id: ID of amphora boot volume
87
+
88
+        :return image id
89
+        """
90
+        LOG.debug('Deleting cinder volume %s', volume_id)
91
+        try:
92
+            instance_volume = self.manager.get(volume_id)
93
+            try:
94
+                instance_volume.delete()
95
+                LOG.debug("Deleted volume %s", volume_id)
96
+            except Exception:
97
+                LOG.exception("Error deleting cinder volume %s",
98
+                              volume_id)
99
+                raise exceptions.VolumeDeleteException()
100
+        except cinder_exceptions.NotFound:
101
+            LOG.warning("Volume %s not found: assuming already deleted",
102
+                        volume_id)
103
+
104
+    def get_image_from_volume(self, volume_id):
105
+        """Get glance image from volume
106
+
107
+        :param volume_id: ID of amphora boot volume
108
+
109
+        :return image id
110
+        """
111
+        image_id = None
112
+        LOG.debug('Get glance image for volume %s', volume_id)
113
+        try:
114
+            instance_volume = self.manager.get(volume_id)
115
+        except cinder_exceptions.NotFound:
116
+            LOG.exception("Volume %s not found", volume_id)
117
+            raise exceptions.VolumeGetException()
118
+        if hasattr(instance_volume, 'volume_image_metadata'):
119
+            image_id = instance_volume.volume_image_metadata.get("image_id")
120
+        else:
121
+            LOG.error("Volume %s has no image metadata", volume_id)
122
+            image_id = None
123
+        return image_id

+ 0
- 0
octavia/volume/drivers/noop_driver/__init__.py View File


+ 60
- 0
octavia/volume/drivers/noop_driver/driver.py View File

@@ -0,0 +1,60 @@
1
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
2
+# not use this file except in compliance with the License. You may obtain
3
+# a copy of the License at
4
+#
5
+# http://www.apache.org/licenses/LICENSE-2.0
6
+#
7
+# Unless required by applicable law or agreed to in writing, software
8
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
+# License for the specific language governing permissions and limitations
11
+# under the License.
12
+
13
+from oslo_log import log as logging
14
+from oslo_utils import uuidutils
15
+
16
+from octavia.volume import volume_base as driver_base
17
+
18
+LOG = logging.getLogger(__name__)
19
+
20
+
21
+class NoopManager(object):
22
+    def __init__(self):
23
+        super(NoopManager, self).__init__()
24
+        self.volumeconfig = {}
25
+
26
+    def create_volume_from_image(self, image_id):
27
+        LOG.debug("Volume %s no-op, image id %s",
28
+                  self.__class__.__name__, image_id)
29
+        self.volumeconfig[image_id] = (image_id, 'create_volume_from_image')
30
+        volume_id = uuidutils.generate_uuid()
31
+        return volume_id
32
+
33
+    def delete_volume(self, volume_id):
34
+        LOG.debug("Volume %s no-op, volume id %s",
35
+                  self.__class__.__name__, volume_id)
36
+        self.volumeconfig[volume_id] = (volume_id, 'delete')
37
+
38
+    def get_image_from_volume(self, volume_id):
39
+        LOG.debug("Volume %s no-op, volume id %s",
40
+                  self.__class__.__name__, volume_id)
41
+        self.volumeconfig[volume_id] = (volume_id, 'get_image_from_volume')
42
+        image_id = uuidutils.generate_uuid()
43
+        return image_id
44
+
45
+
46
+class NoopVolumeDriver(driver_base.VolumeBase):
47
+    def __init__(self):
48
+        super(NoopVolumeDriver, self).__init__()
49
+        self.driver = NoopManager()
50
+
51
+    def create_volume_from_image(self, image_id):
52
+        volume_id = self.driver.create_volume_from_image(image_id)
53
+        return volume_id
54
+
55
+    def delete_volume(self, volume_id):
56
+        self.driver.delete_volume(volume_id)
57
+
58
+    def get_image_from_volume(self, volume_id):
59
+        image_id = self.driver.get_image_from_volume(volume_id)
60
+        return image_id

+ 46
- 0
octavia/volume/volume_base.py View File

@@ -0,0 +1,46 @@
1
+#    Copyright 2011-2019 OpenStack Foundation
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
+import abc
16
+
17
+import six
18
+
19
+
20
+@six.add_metaclass(abc.ABCMeta)
21
+class VolumeBase(object):
22
+
23
+    @abc.abstractmethod
24
+    def create_volume_from_image(self, image_id):
25
+        """Create volume for instance
26
+
27
+        :param image_id: ID of amphora image
28
+
29
+        :return volume id
30
+        """
31
+
32
+    @abc.abstractmethod
33
+    def delete_volume(self, volume_id):
34
+        """Delete volume
35
+
36
+        :param volume_id: ID of amphora volume
37
+        """
38
+
39
+    @abc.abstractmethod
40
+    def get_image_from_volume(self, volume_id):
41
+        """Get cinder volume
42
+
43
+        :param volume_id: ID of amphora volume
44
+
45
+        :return image id
46
+        """

+ 14
- 0
releasenotes/notes/volume-based-amphora-9a1899634f5244b0.yaml View File

@@ -0,0 +1,14 @@
1
+---
2
+features:
3
+  - |
4
+    Allow creation of volume based amphora.
5
+    Many deploy production use volume based instances because of more flexibility.
6
+    Octavia will create volume and attach this to the amphora.
7
+
8
+    Have new settings:
9
+    * `volume_driver`: Whether to use volume driver (cinder) to create volume backed amphorae.
10
+    * `volume_size`: Size of root volume for Amphora Instance when using Cinder
11
+    * `volume_type` : Type of volume for Amphorae volume root disk
12
+    * `volume_create_retry_interval`: Interval time to wait volume is created in available state
13
+    * `volume_create_timeout`: Timeout When volume is not create success
14
+    * `volume_create_max_retries`: Maximum number of retries to create volume

+ 1
- 0
requirements.txt View File

@@ -34,6 +34,7 @@ PyMySQL>=0.7.6 # MIT License
34 34
 python-barbicanclient>=4.5.2 # Apache-2.0
35 35
 python-glanceclient>=2.8.0 # Apache-2.0
36 36
 python-novaclient>=9.1.0 # Apache-2.0
37
+python-cinderclient>=3.3.0 # Apache-2.0
37 38
 pyOpenSSL>=17.1.0 # Apache-2.0
38 39
 WSME>=0.8.0 # MIT
39 40
 Jinja2>=2.10 # BSD License (3 clause)

+ 3
- 0
setup.cfg View File

@@ -79,6 +79,9 @@ octavia.network.drivers =
79 79
     network_noop_driver = octavia.network.drivers.noop_driver.driver:NoopNetworkDriver
80 80
     allowed_address_pairs_driver = octavia.network.drivers.neutron.allowed_address_pairs:AllowedAddressPairsDriver
81 81
     containers_driver = octavia.network.drivers.neutron.containers:ContainersDriver
82
+octavia.volume.drivers =
83
+    volume_noop_driver = octavia.volume.drivers.noop_driver.driver:NoopVolumeDriver
84
+    volume_cinder_driver = octavia.volume.drivers.cinder_driver:VolumeManager
82 85
 octavia.distributor.drivers =
83 86
     distributor_noop_driver = octavia.distributor.drivers.noop_driver.driver:NoopDistributorDriver
84 87
     single_VIP_amphora = octavia.distributor.drivers.single_VIP_amphora.driver:SingleVIPAmpDistributorDriver

Loading…
Cancel
Save