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