bffa7de85973388733a19a2f09a7af72d8651def
[snaps.git] / snaps / openstack / create_image.py
1 # Copyright (c) 2016 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 import logging
16 import time
17
18 from glanceclient.exc import HTTPNotFound
19
20 from snaps.openstack.utils import glance_utils, nova_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.__glance = glance_utils.glance_client(os_creds)
47
48     def create(self, cleanup=False):
49         """
50         Creates the image in OpenStack if it does not already exist
51         :param cleanup: Denotes whether or not this is being called for cleanup or not
52         :return: The OpenStack Image object
53         """
54         from snaps.openstack.utils import nova_utils
55         nova = nova_utils.nova_client(self.__os_creds)
56
57         self.__image = glance_utils.get_image(nova, 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 not cleanup:
62             self.__image = glance_utils.create_image(self.__glance, self.image_settings)
63             logger.info('Creating image')
64             if self.image_active(block=True):
65                 logger.info('Image is now active with name - ' + self.image_settings.name)
66                 return self.__image
67             else:
68                 raise Exception('Image did not activate in the alloted amount of time')
69         else:
70             logger.info('Did not create image due to cleanup mode')
71
72         return self.__image
73
74     def clean(self):
75         """
76         Cleanse environment of all artifacts
77         :return: void
78         """
79         if self.__image:
80             try:
81                 glance_utils.delete_image(self.__glance, self.__image)
82             except HTTPNotFound:
83                 pass
84             self.__image = None
85
86     def get_image(self):
87         """
88         Returns the OpenStack image object as it was populated when create() was called
89         :return: the object
90         """
91         return self.__image
92
93     def image_active(self, block=False, timeout=IMAGE_ACTIVE_TIMEOUT, poll_interval=POLL_INTERVAL):
94         """
95         Returns true when the image status returns the value of expected_status_code
96         :param block: When true, thread will block until active or timeout value in seconds has been exceeded (False)
97         :param timeout: The timeout value
98         :param poll_interval: The polling interval in seconds
99         :return: T/F
100         """
101         return self._image_status_check(STATUS_ACTIVE, block, timeout, poll_interval)
102
103     def _image_status_check(self, expected_status_code, block, timeout, poll_interval):
104         """
105         Returns true when the image status returns the value of expected_status_code
106         :param expected_status_code: instance status evaluated with this string value
107         :param block: When true, thread will block until active or timeout value in seconds has been exceeded (False)
108         :param timeout: The timeout value
109         :param poll_interval: The polling interval in seconds
110         :return: T/F
111         """
112         # sleep and wait for image status change
113         if block:
114             start = time.time()
115         else:
116             start = time.time() - timeout
117
118         while timeout > time.time() - start:
119             status = self._status(expected_status_code)
120             if status:
121                 logger.info('Image is active with name - ' + self.image_settings.name)
122                 return True
123
124             logger.debug('Retry querying image status in ' + str(poll_interval) + ' seconds')
125             time.sleep(poll_interval)
126             logger.debug('Image status query timeout in ' + str(timeout - (time.time() - start)))
127
128         logger.error('Timeout checking for image status for ' + expected_status_code)
129         return False
130
131     def _status(self, expected_status_code):
132         """
133         Returns True when active else False
134         :param expected_status_code: instance status evaluated with this string value
135         :return: T/F
136         """
137         # TODO - Place this API call into glance_utils.
138         nova = nova_utils.nova_client(self.__os_creds)
139         instance = glance_utils.get_image(nova, self.__glance, self.image_settings.name)
140         # instance = self.__glance.images.get(self.__image)
141         if not instance:
142             logger.warn('Cannot find instance with id - ' + self.__image.id)
143             return False
144
145         if instance.status == 'ERROR':
146             raise Exception('Instance had an error during deployment')
147         logger.debug('Instance status is - ' + instance.status)
148         return instance.status == expected_status_code
149
150
151 class ImageSettings:
152     def __init__(self, config=None, name=None, image_user=None, img_format=None, url=None, image_file=None,
153                  extra_properties=None, nic_config_pb_loc=None):
154         """
155
156         :param config: dict() object containing the configuration settings using the attribute names below as each
157                        member's the key and overrides any of the other parameters.
158         :param name: the image's name (required)
159         :param image_user: the image's default sudo user (required)
160         :param img_format: the image type (required)
161         :param url: the image download location (requires url or img_file)
162         :param image_file: the image file location (requires url or img_file)
163         :param extra_properties: dict() object containing extra parameters to pass when loading the image;
164                                  can be ids of kernel and initramfs images for a 3-part image
165         :param nic_config_pb_loc: the file location to the Ansible Playbook that can configure multiple NICs
166         """
167
168         if config:
169             self.name = config.get('name')
170             self.image_user = config.get('image_user')
171             self.format = config.get('format')
172             self.url = config.get('download_url')
173             self.image_file = config.get('image_file')
174             self.extra_properties = config.get('extra_properties')
175             self.nic_config_pb_loc = config.get('nic_config_pb_loc')
176         else:
177             self.name = name
178             self.image_user = image_user
179             self.format = img_format
180             self.url = url
181             self.image_file = image_file
182             self.extra_properties = extra_properties
183             self.nic_config_pb_loc = nic_config_pb_loc
184
185         if not self.name or not self.image_user or not self.format:
186             raise Exception("The attributes name, image_user, format, and url are required for ImageSettings")
187
188         if not self.url and not self.image_file:
189             raise Exception('URL or image file must be set')
190
191         if self.url and self.image_file:
192             raise Exception('Please set either URL or image file, not both')