Modified code to support both Python 2.7 and 3.x
[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 = glance_utils.glance_client(os_creds)
49
50     def create(self, cleanup=False):
51         """
52         Creates the image in OpenStack if it does not already exist and returns the domain Image object
53         :param cleanup: Denotes whether or not this is being called for cleanup or not
54         :return: The OpenStack Image object
55         """
56         self.__image = glance_utils.get_image(self.__glance, self.image_settings.name)
57         if self.__image:
58             logger.info('Found image with name - ' + self.image_settings.name)
59             return self.__image
60         elif not cleanup:
61             extra_properties = self.image_settings.extra_properties or dict()
62
63             if self.image_settings.kernel_image_settings:
64                 self.__kernel_image = glance_utils.get_image(
65                     self.__glance, self.image_settings.kernel_image_settings.name)
66
67                 if not self.__kernel_image and not cleanup:
68                     logger.info('Creating associated kernel image')
69                     self.__kernel_image = glance_utils.create_image(
70                         self.__glance, self.image_settings.kernel_image_settings)
71                 extra_properties['kernel_id'] = self.__kernel_image.id
72             if self.image_settings.ramdisk_image_settings:
73                 self.__ramdisk_image = glance_utils.get_image(
74                     self.__glance, self.image_settings.ramdisk_image_settings.name)
75
76                 if not self.__ramdisk_image and not cleanup:
77                     logger.info('Creating associated ramdisk image')
78                     self.__ramdisk_image = glance_utils.create_image(
79                         self.__glance, self.image_settings.ramdisk_image_settings)
80                 extra_properties['ramdisk_id'] = self.__ramdisk_image.id
81
82             self.image_settings.extra_properties = extra_properties
83             self.__image = glance_utils.create_image(self.__glance, self.image_settings)
84             logger.info('Creating image')
85             if self.__image and self.image_active(block=True):
86                 logger.info('Image is now active with name - ' + self.image_settings.name)
87                 return self.__image
88             else:
89                 raise Exception('Image was not created or activated in the alloted amount of time')
90         else:
91             logger.info('Did not create image due to cleanup mode')
92
93         return self.__image
94
95     def clean(self):
96         """
97         Cleanse environment of all artifacts
98         :return: void
99         """
100         for image in [self.__image, self.__kernel_image, self.__ramdisk_image]:
101             if image:
102                 try:
103                     glance_utils.delete_image(self.__glance, image)
104                 except HTTPNotFound:
105                     pass
106
107         self.__image = None
108         self.__kernel_image = None
109         self.__ramdisk_image = None
110
111     def get_image(self):
112         """
113         Returns the domain Image object as it was populated when create() was called
114         :return: the object
115         """
116         return self.__image
117
118     def get_kernel_image(self):
119         """
120         Returns the OpenStack kernel image object as it was populated when create() was called
121         :return: the object
122         """
123         return self.__kernel_image
124
125     def get_ramdisk_image(self):
126         """
127         Returns the OpenStack ramdisk image object as it was populated when create() was called
128         :return: the object
129         """
130         return self.__ramdisk_image
131
132     def image_active(self, block=False, timeout=IMAGE_ACTIVE_TIMEOUT, poll_interval=POLL_INTERVAL):
133         """
134         Returns true when the image status returns the value of expected_status_code
135         :param block: When true, thread will block until active or timeout value in seconds has been exceeded (False)
136         :param timeout: The timeout value
137         :param poll_interval: The polling interval in seconds
138         :return: T/F
139         """
140         return self._image_status_check(STATUS_ACTIVE, block, timeout, poll_interval)
141
142     def _image_status_check(self, expected_status_code, block, timeout, poll_interval):
143         """
144         Returns true when the image status returns the value of expected_status_code
145         :param expected_status_code: instance status evaluated with this string value
146         :param block: When true, thread will block until active or timeout value in seconds has been exceeded (False)
147         :param timeout: The timeout value
148         :param poll_interval: The polling interval in seconds
149         :return: T/F
150         """
151         # sleep and wait for image status change
152         if block:
153             start = time.time()
154         else:
155             start = time.time() - timeout
156
157         while timeout > time.time() - start:
158             status = self._status(expected_status_code)
159             if status:
160                 logger.info('Image is active with name - ' + self.image_settings.name)
161                 return True
162
163             logger.debug('Retry querying image status in ' + str(poll_interval) + ' seconds')
164             time.sleep(poll_interval)
165             logger.debug('Image status query timeout in ' + str(timeout - (time.time() - start)))
166
167         logger.error('Timeout checking for image status for ' + expected_status_code)
168         return False
169
170     def _status(self, expected_status_code):
171         """
172         Returns True when active else False
173         :param expected_status_code: instance status evaluated with this string value
174         :return: T/F
175         """
176         # TODO - Place this API call into glance_utils.
177         status = glance_utils.get_image_status(self.__glance, self.__image)
178         if not status:
179             logger.warning('Cannot image status for image with ID - ' + self.__image.id)
180             return False
181
182         if status == 'ERROR':
183             raise Exception('Instance had an error during deployment')
184         logger.debug('Instance status is - ' + status)
185         return status == expected_status_code
186
187
188 class ImageSettings:
189     def __init__(self, config=None, name=None, image_user=None, img_format=None, url=None, image_file=None,
190                  extra_properties=None, nic_config_pb_loc=None, kernel_image_settings=None,
191                  ramdisk_image_settings=None):
192         """
193
194         :param config: dict() object containing the configuration settings using the attribute names below as each
195                        member's the key and overrides any of the other parameters.
196         :param name: the image's name (required)
197         :param image_user: the image's default sudo user (required)
198         :param img_format: the image type (required)
199         :param url: the image download location (requires url or img_file)
200         :param image_file: the image file location (requires url or img_file)
201         :param extra_properties: dict() object containing extra parameters to pass when loading the image;
202                                  can be ids of kernel and initramfs images for a 3-part image
203         :param nic_config_pb_loc: the file location to the Ansible Playbook that can configure multiple NICs
204         :param kernel_image_settings: the settings for a kernel image
205         :param ramdisk_image_settings: the settings for a kernel image
206         """
207
208         if config:
209             self.name = config.get('name')
210             self.image_user = config.get('image_user')
211             self.format = config.get('format')
212             self.url = config.get('download_url')
213             self.image_file = config.get('image_file')
214             self.extra_properties = config.get('extra_properties')
215             self.nic_config_pb_loc = config.get('nic_config_pb_loc')
216             if config.get('kernel_image_settings'):
217                 self.kernel_image_settings = ImageSettings(config=config['kernel_image_settings'])
218             else:
219                 self.kernel_image_settings = None
220
221             if config.get('ramdisk_image_settings'):
222                 self.ramdisk_image_settings = ImageSettings(config=config['ramdisk_image_settings'])
223             else:
224                 self.ramdisk_image_settings = None
225
226         else:
227             self.name = name
228             self.image_user = image_user
229             self.format = img_format
230             self.url = url
231             self.image_file = image_file
232             self.extra_properties = extra_properties
233             self.nic_config_pb_loc = nic_config_pb_loc
234             self.kernel_image_settings = kernel_image_settings
235             self.ramdisk_image_settings = ramdisk_image_settings
236
237         if not self.name or not self.image_user or not self.format:
238             raise Exception("The attributes name, image_user, format, and url are required for ImageSettings")
239
240         if not self.url and not self.image_file:
241             raise Exception('URL or image file must be set')
242
243         if self.url and self.image_file:
244             raise Exception('Please set either URL or image file, not both')