Merge "Default OSCreds cacert attribute to False."
[snaps.git] / snaps / openstack / create_image.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 from glanceclient.exc import HTTPNotFound
17 import logging
18 import time
19
20 from snaps.openstack.utils import glance_utils
21
22 __author__ = 'spisarski'
23
24 logger = logging.getLogger('create_image')
25
26 IMAGE_ACTIVE_TIMEOUT = 600
27 POLL_INTERVAL = 3
28 STATUS_ACTIVE = 'active'
29
30
31 class OpenStackImage:
32     """
33     Class responsible for creating an image in OpenStack
34     """
35
36     def __init__(self, os_creds, image_settings):
37         """
38         Constructor
39         :param os_creds: The OpenStack connection credentials
40         :param image_settings: The image settings
41         :return:
42         """
43         self.__os_creds = os_creds
44         self.image_settings = image_settings
45         self.__image = None
46         self.__kernel_image = None
47         self.__ramdisk_image = None
48         self.__glance = None
49
50     def create(self, cleanup=False):
51         """
52         Creates the image in OpenStack if it does not already exist and returns
53         the domain Image object
54         :param cleanup: Denotes whether or not this is being called for cleanup
55                         or not
56         :return: The OpenStack Image object
57         """
58         self.__glance = glance_utils.glance_client(self.__os_creds)
59         self.__image = glance_utils.get_image(self.__glance,
60                                               self.image_settings.name)
61         if self.__image:
62             logger.info('Found image with name - ' + self.image_settings.name)
63             return self.__image
64         elif self.image_settings.exists and not self.image_settings.url \
65                 and not self.image_settings.image_file:
66             raise ImageCreationError(
67                 'Image with does not exist with name - ' +
68                 self.image_settings.name)
69         elif not cleanup:
70             extra_properties = self.image_settings.extra_properties or dict()
71
72             if self.image_settings.kernel_image_settings:
73                 self.__kernel_image = glance_utils.get_image(
74                     self.__glance,
75                     self.image_settings.kernel_image_settings.name)
76
77                 if not self.__kernel_image and not cleanup:
78                     logger.info(
79                         'Creating associated kernel image with name - %s',
80                         self.image_settings.kernel_image_settings.name)
81                     self.__kernel_image = glance_utils.create_image(
82                         self.__glance,
83                         self.image_settings.kernel_image_settings)
84                 extra_properties['kernel_id'] = self.__kernel_image.id
85             if self.image_settings.ramdisk_image_settings:
86                 self.__ramdisk_image = glance_utils.get_image(
87                     self.__glance,
88                     self.image_settings.ramdisk_image_settings.name)
89
90                 if not self.__ramdisk_image and not cleanup:
91                     logger.info(
92                         'Creating associated ramdisk image with name - %s',
93                         self.image_settings.ramdisk_image_settings.name)
94                     self.__ramdisk_image = glance_utils.create_image(
95                         self.__glance,
96                         self.image_settings.ramdisk_image_settings)
97                 extra_properties['ramdisk_id'] = self.__ramdisk_image.id
98
99             self.image_settings.extra_properties = extra_properties
100             self.__image = glance_utils.create_image(self.__glance,
101                                                      self.image_settings)
102
103             logger.info(
104                 'Created image with name - %s', self.image_settings.name)
105             if self.__image and self.image_active(block=True):
106                 logger.info(
107                     'Image is now active with name - %s',
108                     self.image_settings.name)
109                 return self.__image
110             else:
111                 raise ImageCreationError(
112                     'Image was not created or activated in the alloted amount'
113                     'of time')
114         else:
115             logger.info('Did not create image due to cleanup mode')
116
117         return self.__image
118
119     def clean(self):
120         """
121         Cleanse environment of all artifacts
122         :return: void
123         """
124         for image in [self.__image, self.__kernel_image, self.__ramdisk_image]:
125             if image:
126                 try:
127                     glance_utils.delete_image(self.__glance, image)
128                 except HTTPNotFound:
129                     pass
130
131         self.__image = None
132         self.__kernel_image = None
133         self.__ramdisk_image = None
134
135     def get_image(self):
136         """
137         Returns the domain Image object as it was populated when create() was
138         called
139         :return: the object
140         """
141         return self.__image
142
143     def get_kernel_image(self):
144         """
145         Returns the OpenStack kernel image object as it was populated when
146         create() was called
147         :return: the object
148         """
149         return self.__kernel_image
150
151     def get_ramdisk_image(self):
152         """
153         Returns the OpenStack ramdisk image object as it was populated when
154         create() was called
155         :return: the object
156         """
157         return self.__ramdisk_image
158
159     def image_active(self, block=False, timeout=IMAGE_ACTIVE_TIMEOUT,
160                      poll_interval=POLL_INTERVAL):
161         """
162         Returns true when the image status returns the value of
163         expected_status_code
164         :param block: When true, thread will block until active or timeout
165                       value in seconds has been exceeded (False)
166         :param timeout: The timeout value
167         :param poll_interval: The polling interval in seconds
168         :return: T/F
169         """
170         return self._image_status_check(STATUS_ACTIVE, block, timeout,
171                                         poll_interval)
172
173     def _image_status_check(self, expected_status_code, block, timeout,
174                             poll_interval):
175         """
176         Returns true when the image status returns the value of
177         expected_status_code
178         :param expected_status_code: instance status evaluated with this string
179                                      value
180         :param block: When true, thread will block until active or timeout
181                       value in seconds has been exceeded (False)
182         :param timeout: The timeout value
183         :param poll_interval: The polling interval in seconds
184         :return: T/F
185         """
186         # sleep and wait for image status change
187         if block:
188             start = time.time()
189         else:
190             start = time.time() - timeout
191
192         while timeout > time.time() - start:
193             status = self._status(expected_status_code)
194             if status:
195                 logger.debug(
196                     'Image is active with name - ' + self.image_settings.name)
197                 return True
198
199             logger.debug('Retry querying image status in ' + str(
200                 poll_interval) + ' seconds')
201             time.sleep(poll_interval)
202             logger.debug('Image status query timeout in ' + str(
203                 timeout - (time.time() - start)))
204
205         logger.error(
206             'Timeout checking for image status for ' + expected_status_code)
207         return False
208
209     def _status(self, expected_status_code):
210         """
211         Returns True when active else False
212         :param expected_status_code: instance status evaluated with this string
213                                      value
214         :return: T/F
215         """
216         status = glance_utils.get_image_status(self.__glance, self.__image)
217         if not status:
218             logger.warning(
219                 'Cannot image status for image with ID - ' + self.__image.id)
220             return False
221
222         if status == 'ERROR':
223             raise ImageCreationError('Instance had an error during deployment')
224         logger.debug('Instance status is - ' + status)
225         return status == expected_status_code
226
227
228 class ImageSettings:
229     def __init__(self, **kwargs):
230         """
231         Constructor
232         :param name: the image's name (required)
233         :param image_user: the image's default sudo user (required)
234         :param format or img_format: the image type (required)
235         :param url or download_url: the image download location (requires url
236                                     or img_file)
237         :param image_file: the image file location (requires url or img_file)
238         :param extra_properties: dict() object containing extra parameters to
239                                  pass when loading the image;
240                                  can be ids of kernel and initramfs images for
241                                  a 3-part image
242         :param nic_config_pb_loc: the file location to the Ansible Playbook
243                                   that can configure multiple NICs
244         :param kernel_image_settings: the settings for a kernel image
245         :param ramdisk_image_settings: the settings for a kernel image
246         :param exists: When True, an image with the given name must exist
247         :param public: When True, an image will be created with public
248                        visibility
249         """
250
251         self.name = kwargs.get('name')
252         self.image_user = kwargs.get('image_user')
253         self.format = kwargs.get('format')
254         if not self.format:
255             self.format = kwargs.get('img_format')
256
257         self.url = kwargs.get('url')
258         if not self.url:
259             self.url = kwargs.get('download_url')
260
261         self.image_file = kwargs.get('image_file')
262         self.extra_properties = kwargs.get('extra_properties')
263         self.nic_config_pb_loc = kwargs.get('nic_config_pb_loc')
264
265         kernel_image_settings = kwargs.get('kernel_image_settings')
266         if kernel_image_settings:
267             if isinstance(kernel_image_settings, dict):
268                 self.kernel_image_settings = ImageSettings(
269                     **kernel_image_settings)
270             else:
271                 self.kernel_image_settings = kernel_image_settings
272         else:
273             self.kernel_image_settings = None
274
275         ramdisk_image_settings = kwargs.get('ramdisk_image_settings')
276         if ramdisk_image_settings:
277             if isinstance(ramdisk_image_settings, dict):
278                 self.ramdisk_image_settings = ImageSettings(
279                     **ramdisk_image_settings)
280             else:
281                 self.ramdisk_image_settings = ramdisk_image_settings
282         else:
283             self.ramdisk_image_settings = None
284
285         if 'exists' in kwargs and kwargs['exists'] is True:
286             self.exists = True
287         else:
288             self.exists = False
289
290         if 'public' in kwargs and kwargs['public'] is True:
291             self.public = True
292         else:
293             self.public = False
294
295         if not self.name:
296             raise ImageSettingsError("The attribute name is required")
297
298         if not (self.url or self.image_file) and not self.exists:
299             raise ImageSettingsError(
300                 'URL or image file must be set or image must already exist')
301
302         if self.url and self.image_file:
303             raise ImageSettingsError(
304                 'Please set either URL or image file, not both')
305
306         if not self.image_user:
307             raise ImageSettingsError('Image user is required')
308
309         if not self.format and not self.exists:
310             raise ImageSettingsError(
311                 'Format is required when the image should not already exist')
312
313
314 class ImageSettingsError(Exception):
315     """
316     Exception to be thrown when an image settings are incorrect
317     """
318
319     def __init__(self, message):
320         Exception.__init__(self, message)
321
322
323 class ImageCreationError(Exception):
324     """
325     Exception to be thrown when an image cannot be created
326     """
327
328     def __init__(self, message):
329         Exception.__init__(self, message)