1a8aa124a03bdf2d3126c62f59f76cf2644fdd3d
[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 from snaps.config import image
23
24 __author__ = 'spisarski'
25
26 logger = logging.getLogger('create_image')
27
28 IMAGE_ACTIVE_TIMEOUT = 600
29 POLL_INTERVAL = 3
30 STATUS_ACTIVE = 'active'
31
32
33 class OpenStackImage(OpenStackCloudObject):
34     """
35     Class responsible for managing an image in OpenStack
36     """
37
38     def __init__(self, os_creds, image_settings):
39         """
40         Constructor
41         :param os_creds: The OpenStack connection credentials
42         :param image_settings: An snaps.config.image.ImageConfig object
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         super(self.__class__, self).initialize()
58
59         self.__glance = glance_utils.glance_client(
60             self._os_creds, self._os_session)
61         self.__image = glance_utils.get_image(
62             self.__glance, image_settings=self.image_settings)
63
64         if self.__image:
65             logger.info('Found image with name - ' + self.image_settings.name)
66             return self.__image
67         elif (self.image_settings.exists and not self.image_settings.url
68                 and not self.image_settings.image_file):
69             raise ImageCreationError(
70                 'Image with does not exist with name - ' +
71                 self.image_settings.name)
72
73         if self.image_settings.kernel_image_settings:
74             self.__kernel_image = glance_utils.get_image(
75                 self.__glance,
76                 image_settings=self.image_settings.kernel_image_settings)
77
78         if self.image_settings.ramdisk_image_settings:
79             self.__ramdisk_image = glance_utils.get_image(
80                 self.__glance,
81                 image_settings=self.image_settings.ramdisk_image_settings)
82
83         return self.__image
84
85     def create(self):
86         """
87         Creates the image in OpenStack if it does not already exist and returns
88         the domain Image object
89         :return: The Image domain object or None
90         """
91         self.initialize()
92
93         if not self.__image:
94             extra_properties = self.image_settings.extra_properties or dict()
95
96             if self.image_settings.kernel_image_settings:
97                 if not self.__kernel_image:
98                     logger.info(
99                         'Creating associated kernel image with name - %s',
100                         self.image_settings.kernel_image_settings.name)
101                     self.__kernel_image = glance_utils.create_image(
102                         self.__glance,
103                         self.image_settings.kernel_image_settings)
104                 extra_properties['kernel_id'] = self.__kernel_image.id
105
106             if self.image_settings.ramdisk_image_settings:
107                 if not self.__ramdisk_image:
108                     logger.info(
109                         'Creating associated ramdisk image with name - %s',
110                         self.image_settings.ramdisk_image_settings.name)
111                     self.__ramdisk_image = glance_utils.create_image(
112                         self.__glance,
113                         self.image_settings.ramdisk_image_settings)
114                 extra_properties['ramdisk_id'] = self.__ramdisk_image.id
115
116             self.image_settings.extra_properties = extra_properties
117             self.__image = glance_utils.create_image(self.__glance,
118                                                      self.image_settings)
119
120             logger.info(
121                 'Created image with name - %s', self.image_settings.name)
122
123             if self.__image and self.image_active(block=True):
124                 logger.info(
125                     'Image is now active with name - %s',
126                     self.image_settings.name)
127                 return self.__image
128             else:
129                 raise ImageCreationError(
130                     'Image was not created or activated in the alloted amount'
131                     'of time')
132
133         return self.__image
134
135     def clean(self):
136         """
137         Cleanse environment of all artifacts
138         :return: void
139         """
140         for img in [self.__image, self.__kernel_image, self.__ramdisk_image]:
141             if img:
142                 try:
143                     glance_utils.delete_image(self.__glance, img)
144                 except HTTPNotFound:
145                     pass
146
147         self.__image = None
148         self.__kernel_image = None
149         self.__ramdisk_image = None
150
151         if self.__glance:
152             self.__glance.http_client.session.session.close()
153
154         super(self.__class__, self).clean()
155
156     def get_image(self):
157         """
158         Returns the domain Image object as it was populated when create() was
159         called
160         :return: the object
161         """
162         return self.__image
163
164     def get_kernel_image(self):
165         """
166         Returns the OpenStack kernel image object as it was populated when
167         create() was called
168         :return: the object
169         """
170         return self.__kernel_image
171
172     def get_ramdisk_image(self):
173         """
174         Returns the OpenStack ramdisk image object as it was populated when
175         create() was called
176         :return: the object
177         """
178         return self.__ramdisk_image
179
180     def image_active(self, block=False, timeout=IMAGE_ACTIVE_TIMEOUT,
181                      poll_interval=POLL_INTERVAL):
182         """
183         Returns true when the image status returns the value of
184         expected_status_code
185         :param block: When true, thread will block until active or timeout
186                       value in seconds has been exceeded (False)
187         :param timeout: The timeout value
188         :param poll_interval: The polling interval in seconds
189         :return: T/F
190         """
191         return self._image_status_check(STATUS_ACTIVE, block, timeout,
192                                         poll_interval)
193
194     def _image_status_check(self, expected_status_code, block, timeout,
195                             poll_interval):
196         """
197         Returns true when the image status returns the value of
198         expected_status_code
199         :param expected_status_code: instance status evaluated with this string
200                                      value
201         :param block: When true, thread will block until active or timeout
202                       value in seconds has been exceeded (False)
203         :param timeout: The timeout value
204         :param poll_interval: The polling interval in seconds
205         :return: T/F
206         """
207         # sleep and wait for image status change
208         if block:
209             start = time.time()
210         else:
211             start = time.time() - timeout
212
213         while timeout > time.time() - start:
214             status = self._status(expected_status_code)
215             if status:
216                 logger.debug(
217                     'Image is active with name - ' + self.image_settings.name)
218                 return True
219
220             logger.debug('Retry querying image status in ' + str(
221                 poll_interval) + ' seconds')
222             time.sleep(poll_interval)
223             logger.debug('Image status query timeout in ' + str(
224                 timeout - (time.time() - start)))
225
226         logger.error(
227             'Timeout checking for image status for ' + expected_status_code)
228         return False
229
230     def _status(self, expected_status_code):
231         """
232         Returns True when active else False
233         :param expected_status_code: instance status evaluated with this string
234                                      value
235         :return: T/F
236         """
237         status = glance_utils.get_image_status(self.__glance, self.__image)
238         if not status:
239             logger.warning(
240                 'Cannot image status for image with ID - ' + self.__image.id)
241             return False
242
243         if status == 'ERROR':
244             raise ImageCreationError('Instance had an error during deployment')
245         logger.debug('Instance status is - ' + status)
246         return status == expected_status_code
247
248
249 class ImageSettings(image.ImageConfig):
250     """
251     Deprecated, use snaps.config.image.ImageSettings instead
252     """
253     def __init__(self, **kwargs):
254         from warnings import warn
255         warn('Use snaps.config.image.ImageConfig instead', DeprecationWarning)
256         super(ImageSettings, self).__init__(**kwargs)
257
258
259 class ImageCreationError(Exception):
260     """
261     Exception to be thrown when an image cannot be created
262     """
263
264     def __init__(self, message):
265         Exception.__init__(self, message)