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