Updated documentation to reflect new block storage support.
[snaps.git] / snaps / openstack / create_volume.py
1 # Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
2 #                    and others.  All rights reserved.
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain 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,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15
16 import logging
17 import time
18
19 from cinderclient.exceptions import NotFound
20
21 from snaps.openstack.openstack_creator import OpenStackVolumeObject
22 from snaps.openstack.utils import cinder_utils
23
24 __author__ = 'spisarski'
25
26 logger = logging.getLogger('create_volume')
27
28 VOLUME_ACTIVE_TIMEOUT = 300
29 VOLUME_DELETE_TIMEOUT = 60
30 POLL_INTERVAL = 3
31 STATUS_ACTIVE = 'available'
32 STATUS_IN_USE = 'in-use'
33 STATUS_FAILED = 'error'
34 STATUS_DELETED = 'deleted'
35
36
37 class OpenStackVolume(OpenStackVolumeObject):
38     """
39     Class responsible for managing an volume in OpenStack
40     """
41
42     def __init__(self, os_creds, volume_settings):
43         """
44         Constructor
45         :param os_creds: The OpenStack connection credentials
46         :param volume_settings: The volume settings
47         :return:
48         """
49         super(self.__class__, self).__init__(os_creds)
50
51         self.volume_settings = volume_settings
52         self.__volume = None
53
54     def initialize(self):
55         """
56         Loads the existing Volume
57         :return: The Volume domain object or None
58         """
59         super(self.__class__, self).initialize()
60
61         self.__volume = cinder_utils.get_volume(
62             self._cinder, volume_settings=self.volume_settings)
63         return self.__volume
64
65     def create(self, block=False):
66         """
67         Creates the volume in OpenStack if it does not already exist and
68         returns the domain Volume object
69         :return: The Volume domain object or None
70         """
71         self.initialize()
72
73         if not self.__volume:
74             self.__volume = cinder_utils.create_volume(
75                 self._cinder, self.volume_settings)
76
77             logger.info(
78                 'Created volume with name - %s', self.volume_settings.name)
79             if self.__volume:
80                 if block:
81                     if self.volume_active(block=True):
82                         logger.info('Volume is now active with name - %s',
83                                     self.volume_settings.name)
84                         return self.__volume
85                     else:
86                         raise VolumeCreationError(
87                             'Volume was not created or activated in the '
88                             'alloted amount of time')
89         else:
90             logger.info('Did not create volume due to cleanup mode')
91
92         return self.__volume
93
94     def clean(self):
95         """
96         Cleanse environment of all artifacts
97         :return: void
98         """
99         if self.__volume:
100             try:
101                 if self.volume_active():
102                     cinder_utils.delete_volume(self._cinder, self.__volume)
103                 else:
104                     logger.warn('Timeout waiting to delete volume %s',
105                                 self.__volume.name)
106             except NotFound:
107                 pass
108
109             try:
110                 if self.volume_deleted(block=True):
111                     logger.info(
112                         'Volume has been properly deleted with name - %s',
113                         self.volume_settings.name)
114                     self.__vm = None
115                 else:
116                     logger.error(
117                         'Volume not deleted within the timeout period of %s '
118                         'seconds', VOLUME_DELETE_TIMEOUT)
119             except Exception as e:
120                 logger.error(
121                     'Unexpected error while checking VM instance status - %s',
122                     e)
123
124         self.__volume = None
125
126     def get_volume(self):
127         """
128         Returns the domain Volume object as it was populated when create() was
129         called
130         :return: the object
131         """
132         return self.__volume
133
134     def volume_active(self, block=False, timeout=VOLUME_ACTIVE_TIMEOUT,
135                       poll_interval=POLL_INTERVAL):
136         """
137         Returns true when the volume status returns the value of
138         expected_status_code
139         :param block: When true, thread will block until active or timeout
140                       value in seconds has been exceeded (False)
141         :param timeout: The timeout value
142         :param poll_interval: The polling interval in seconds
143         :return: T/F
144         """
145         return self._volume_status_check(STATUS_ACTIVE, block, timeout,
146                                          poll_interval)
147
148     def volume_in_use(self):
149         """
150         Returns true when the volume status returns the value of
151         expected_status_code
152         :return: T/F
153         """
154         return self._volume_status_check(STATUS_IN_USE, False, 0, 0)
155
156     def volume_deleted(self, block=False, poll_interval=POLL_INTERVAL):
157         """
158         Returns true when the VM status returns the value of
159         expected_status_code or instance retrieval throws a NotFound exception.
160         :param block: When true, thread will block until active or timeout
161                       value in seconds has been exceeded (False)
162         :param poll_interval: The polling interval in seconds
163         :return: T/F
164         """
165         try:
166             return self._volume_status_check(
167                 STATUS_DELETED, block, VOLUME_DELETE_TIMEOUT, poll_interval)
168         except NotFound as e:
169             logger.debug(
170                 "Volume not found when querying status for %s with message "
171                 "%s", STATUS_DELETED, e)
172             return True
173
174     def _volume_status_check(self, expected_status_code, block, timeout,
175                              poll_interval):
176         """
177         Returns true when the volume status returns the value of
178         expected_status_code
179         :param expected_status_code: instance status evaluated with this string
180                                      value
181         :param block: When true, thread will block until active or timeout
182                       value in seconds has been exceeded (False)
183         :param timeout: The timeout value
184         :param poll_interval: The polling interval in seconds
185         :return: T/F
186         """
187         # sleep and wait for volume status change
188         if block:
189             start = time.time()
190         else:
191             start = time.time() - timeout + 1
192
193         while timeout > time.time() - start:
194             status = self._status(expected_status_code)
195             if status:
196                 logger.debug('Volume is active with name - %s',
197                              self.volume_settings.name)
198                 return True
199
200             logger.debug('Retry querying volume status in %s seconds',
201                          str(poll_interval))
202             time.sleep(poll_interval)
203             logger.debug('Volume status query timeout in %s',
204                          str(timeout - (time.time() - start)))
205
206         logger.error(
207             'Timeout checking for volume status for ' + expected_status_code)
208         return False
209
210     def _status(self, expected_status_code):
211         """
212         Returns True when active else False
213         :param expected_status_code: instance status evaluated with this string
214                                      value
215         :return: T/F
216         """
217         status = cinder_utils.get_volume_status(self._cinder, self.__volume)
218         if not status:
219             logger.warning(
220                 'Cannot volume status for volume with ID - %s',
221                 self.__volume.id)
222             return False
223
224         if status == 'ERROR':
225             raise VolumeCreationError(
226                 'Instance had an error during deployment')
227         logger.debug('Instance status is - ' + status)
228         return status == expected_status_code
229
230
231 class VolumeSettings:
232     def __init__(self, **kwargs):
233         """
234         Constructor
235         :param name: the volume's name (required)
236         :param description: the volume's name (optional)
237         :param size: the volume's size in GB (default 1)
238         :param image_name: when a glance image is used for the image source
239                            (optional)
240         :param type_name: the associated volume's type name (optional)
241         :param availability_zone: the name of the compute server on which to
242                                   deploy the volume (optional)
243         :param multi_attach: when true, volume can be attached to more than one
244                              server (default False)
245         """
246
247         self.name = kwargs.get('name')
248         self.description = kwargs.get('description')
249         self.size = int(kwargs.get('size', 1))
250         self.image_name = kwargs.get('image_name')
251         self.type_name = kwargs.get('type_name')
252         self.availability_zone = kwargs.get('availability_zone')
253
254         if kwargs.get('availability_zone'):
255             self.multi_attach = bool(kwargs.get('availability_zone'))
256         else:
257             self.multi_attach = False
258
259         if not self.name:
260             raise VolumeSettingsError("The attribute name is required")
261
262
263 class VolumeSettingsError(Exception):
264     """
265     Exception to be thrown when an volume settings are incorrect
266     """
267
268     def __init__(self, message):
269         Exception.__init__(self, message)
270
271
272 class VolumeCreationError(Exception):
273     """
274     Exception to be thrown when an volume cannot be created
275     """
276
277     def __init__(self, message):
278         Exception.__init__(self, message)