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