Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 1 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 |
| 2 | |
| 3 | # Copyright 2012 OpenStack Foundation |
| 4 | # All Rights Reserved. |
| 5 | # |
| 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may |
| 7 | # not use this file except in compliance with the License. You may obtain |
| 8 | # a copy of the License at |
| 9 | # |
| 10 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | # |
| 12 | # Unless required by applicable law or agreed to in writing, software |
| 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 15 | # License for the specific language governing permissions and limitations |
| 16 | # under the License. |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 17 | # |
| 18 | # This script aims to configure an initial Openstack environment with all the |
| 19 | # necessary configurations for tempest's run using nothing but Openstack's |
| 20 | # native API. |
| 21 | # That includes, creating users, tenants, registering images (cirros), |
| 22 | # configuring neutron and so on. |
| 23 | # |
| 24 | # ASSUMPTION: this script is run by an admin user as it is meant to configure |
| 25 | # the Openstack environment prior to actual use. |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 26 | |
| 27 | # Config |
| 28 | import ConfigParser |
| 29 | import os |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 30 | import tarfile |
| 31 | import urllib2 |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 32 | |
| 33 | # Default client libs |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 34 | import glanceclient as glance_client |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 35 | import keystoneclient.v2_0.client as keystone_client |
| 36 | |
| 37 | # Import Openstack exceptions |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 38 | import glanceclient.exc as glance_exception |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 39 | import keystoneclient.exceptions as keystone_exception |
| 40 | |
| 41 | |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 42 | TEMPEST_TEMP_DIR = os.getenv("TEMPEST_TEMP_DIR", "/tmp").rstrip('/') |
| 43 | TEMPEST_ROOT_DIR = os.getenv("TEMPEST_ROOT_DIR", os.getenv("HOME")).rstrip('/') |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 44 | |
| 45 | # Environment variables override defaults |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 46 | TEMPEST_CONFIG_DIR = os.getenv("TEMPEST_CONFIG_DIR", |
| 47 | "%s%s" % (TEMPEST_ROOT_DIR, "/etc")).rstrip('/') |
| 48 | TEMPEST_CONFIG_FILE = os.getenv("TEMPEST_CONFIG_FILE", |
| 49 | "%s%s" % (TEMPEST_CONFIG_DIR, "/tempest.conf")) |
| 50 | TEMPEST_CONFIG_SAMPLE = os.getenv("TEMPEST_CONFIG_SAMPLE", |
| 51 | "%s%s" % (TEMPEST_CONFIG_DIR, |
| 52 | "/tempest.conf.sample")) |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 53 | # Image references |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 54 | IMAGE_DOWNLOAD_CHUNK_SIZE = 8 * 1024 |
| 55 | IMAGE_UEC_SOURCE_URL = os.getenv("IMAGE_UEC_SOURCE_URL", |
| 56 | "http://download.cirros-cloud.net/0.3.1/" |
| 57 | "cirros-0.3.1-x86_64-uec.tar.gz") |
| 58 | TEMPEST_IMAGE_ID = os.getenv('IMAGE_ID') |
| 59 | TEMPEST_IMAGE_ID_ALT = os.getenv('IMAGE_ID_ALT') |
| 60 | IMAGE_STATUS_ACTIVE = 'active' |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 61 | |
| 62 | |
| 63 | class ClientManager(object): |
| 64 | """ |
| 65 | Manager that provides access to the official python clients for |
| 66 | calling various OpenStack APIs. |
| 67 | """ |
| 68 | def __init__(self): |
| 69 | self.identity_client = None |
| 70 | self.image_client = None |
| 71 | self.network_client = None |
| 72 | self.compute_client = None |
| 73 | self.volume_client = None |
| 74 | |
| 75 | def get_identity_client(self, **kwargs): |
| 76 | """ |
| 77 | Returns the openstack identity python client |
| 78 | :param username: a string representing the username |
| 79 | :param password: a string representing the user's password |
| 80 | :param tenant_name: a string representing the tenant name of the user |
| 81 | :param auth_url: a string representing the auth url of the identity |
| 82 | :param insecure: True if we wish to disable ssl certificate validation, |
| 83 | False otherwise |
| 84 | :returns an instance of openstack identity python client |
| 85 | """ |
| 86 | if not self.identity_client: |
| 87 | self.identity_client = keystone_client.Client(**kwargs) |
| 88 | |
| 89 | return self.identity_client |
| 90 | |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 91 | def get_image_client(self, version="1", *args, **kwargs): |
| 92 | """ |
| 93 | This method returns Openstack glance python client |
| 94 | :param version: a string representing the version of the glance client |
| 95 | to use. |
| 96 | :param string endpoint: A user-supplied endpoint URL for the glance |
| 97 | service. |
| 98 | :param string token: Token for authentication. |
| 99 | :param integer timeout: Allows customization of the timeout for client |
| 100 | http requests. (optional) |
| 101 | :return: a Client object representing the glance client |
| 102 | """ |
| 103 | if not self.image_client: |
| 104 | self.image_client = glance_client.Client(version, *args, **kwargs) |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 105 | |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 106 | return self.image_client |
| 107 | |
| 108 | |
| 109 | def get_tempest_config(path_to_config): |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 110 | """ |
| 111 | Gets the tempest configuration file as a ConfigParser object |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 112 | :param path_to_config: path to the config file |
| 113 | :return: a ConfigParser object representing the tempest configuration file |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 114 | """ |
| 115 | # get the sample config file from the sample |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 116 | config = ConfigParser.ConfigParser() |
| 117 | config.readfp(open(path_to_config)) |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 118 | |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 119 | return config |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 120 | |
| 121 | |
| 122 | def update_config_admin_credentials(config, config_section): |
| 123 | """ |
| 124 | Updates the tempest config with the admin credentials |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 125 | :param config: a ConfigParser object representing the tempest config file |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 126 | :param config_section: the section name where the admin credentials are |
| 127 | """ |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 128 | # Check if credentials are present, default uses the config credentials |
| 129 | OS_USERNAME = os.getenv('OS_USERNAME', |
| 130 | config.get(config_section, "admin_username")) |
| 131 | OS_PASSWORD = os.getenv('OS_PASSWORD', |
| 132 | config.get(config_section, "admin_password")) |
| 133 | OS_TENANT_NAME = os.getenv('OS_TENANT_NAME', |
| 134 | config.get(config_section, "admin_tenant_name")) |
| 135 | OS_AUTH_URL = os.getenv('OS_AUTH_URL', config.get(config_section, "uri")) |
| 136 | |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 137 | if not (OS_AUTH_URL and |
| 138 | OS_USERNAME and |
| 139 | OS_PASSWORD and |
| 140 | OS_TENANT_NAME): |
| 141 | raise Exception("Admin environment variables not found.") |
| 142 | |
| 143 | # TODO(tkammer): Add support for uri_v3 |
| 144 | config_identity_params = {'uri': OS_AUTH_URL, |
| 145 | 'admin_username': OS_USERNAME, |
| 146 | 'admin_password': OS_PASSWORD, |
| 147 | 'admin_tenant_name': OS_TENANT_NAME} |
| 148 | |
| 149 | update_config_section_with_params(config, |
| 150 | config_section, |
| 151 | config_identity_params) |
| 152 | |
| 153 | |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 154 | def update_config_section_with_params(config, config_section, params): |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 155 | """ |
| 156 | Updates a given config object with given params |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 157 | :param config: a ConfigParser object representing the tempest config file |
| 158 | :param config_section: the section we would like to update |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 159 | :param params: the parameters we wish to update for that section |
| 160 | """ |
| 161 | for option, value in params.items(): |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 162 | config.set(config_section, option, value) |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 163 | |
| 164 | |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 165 | def get_identity_client_kwargs(config, config_section): |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 166 | """ |
| 167 | Get the required arguments for the identity python client |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 168 | :param config: a ConfigParser object representing the tempest config file |
| 169 | :param config_section: the section name in the configuration where the |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 170 | arguments can be found |
| 171 | :return: a dictionary representing the needed arguments for the identity |
| 172 | client |
| 173 | """ |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 174 | username = config.get(config_section, 'admin_username') |
| 175 | password = config.get(config_section, 'admin_password') |
| 176 | tenant_name = config.get(config_section, 'admin_tenant_name') |
| 177 | auth_url = config.get(config_section, 'uri') |
| 178 | dscv = config.get(config_section, 'disable_ssl_certificate_validation') |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 179 | kwargs = {'username': username, |
| 180 | 'password': password, |
| 181 | 'tenant_name': tenant_name, |
| 182 | 'auth_url': auth_url, |
| 183 | 'insecure': dscv} |
| 184 | |
| 185 | return kwargs |
| 186 | |
| 187 | |
| 188 | def create_user_with_tenant(identity_client, username, password, tenant_name): |
| 189 | """ |
| 190 | Creates a user using a given identity client |
| 191 | :param identity_client: openstack identity python client |
| 192 | :param username: a string representing the username |
| 193 | :param password: a string representing the user's password |
| 194 | :param tenant_name: a string representing the tenant name of the user |
| 195 | """ |
| 196 | # Try to create the necessary tenant |
| 197 | tenant_id = None |
| 198 | try: |
| 199 | tenant_description = "Tenant for Tempest %s user" % username |
| 200 | tenant = identity_client.tenants.create(tenant_name, |
| 201 | tenant_description) |
| 202 | tenant_id = tenant.id |
| 203 | except keystone_exception.Conflict: |
| 204 | |
| 205 | # if already exist, use existing tenant |
| 206 | tenant_list = identity_client.tenants.list() |
| 207 | for tenant in tenant_list: |
| 208 | if tenant.name == tenant_name: |
| 209 | tenant_id = tenant.id |
| 210 | |
| 211 | # Try to create the user |
| 212 | try: |
| 213 | email = "%s@test.com" % username |
| 214 | identity_client.users.create(name=username, |
| 215 | password=password, |
| 216 | email=email, |
| 217 | tenant_id=tenant_id) |
| 218 | except keystone_exception.Conflict: |
| 219 | |
| 220 | # if already exist, use existing user |
| 221 | pass |
| 222 | |
| 223 | |
| 224 | def create_users_and_tenants(identity_client, |
| 225 | config, |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 226 | config_section): |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 227 | """ |
| 228 | Creates the two non admin users and tenants for tempest |
| 229 | :param identity_client: openstack identity python client |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 230 | :param config: a ConfigParser object representing the tempest config file |
| 231 | :param config_section: the section name of identity in the config |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 232 | """ |
| 233 | # Get the necessary params from the config file |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 234 | tenant_name = config.get(config_section, 'tenant_name') |
| 235 | username = config.get(config_section, 'username') |
| 236 | password = config.get(config_section, 'password') |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 237 | |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 238 | alt_tenant_name = config.get(config_section, 'alt_tenant_name') |
| 239 | alt_username = config.get(config_section, 'alt_username') |
| 240 | alt_password = config.get(config_section, 'alt_password') |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 241 | |
| 242 | # Create the necessary users for the test runs |
| 243 | create_user_with_tenant(identity_client, username, password, tenant_name) |
| 244 | create_user_with_tenant(identity_client, alt_username, alt_password, |
| 245 | alt_tenant_name) |
| 246 | |
| 247 | |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 248 | def get_image_client_kwargs(identity_client, config, config_section): |
| 249 | """ |
| 250 | Get the required arguments for the image python client |
| 251 | :param identity_client: openstack identity python client |
| 252 | :param config: a ConfigParser object representing the tempest config file |
| 253 | :param config_section: the section name of identity in the config |
| 254 | :return: a dictionary representing the needed arguments for the image |
| 255 | client |
| 256 | """ |
| 257 | |
| 258 | token = identity_client.auth_token |
| 259 | endpoint = identity_client.\ |
| 260 | service_catalog.url_for(service_type='image', endpoint_type='publicURL' |
| 261 | ) |
| 262 | dscv = config.get(config_section, 'disable_ssl_certificate_validation') |
| 263 | kwargs = {'endpoint': endpoint, |
| 264 | 'token': token, |
| 265 | 'insecure': dscv} |
| 266 | |
| 267 | return kwargs |
| 268 | |
| 269 | |
| 270 | def images_exist(image_client): |
| 271 | """ |
| 272 | Checks whether the images ID's located in the environment variable are |
| 273 | indeed registered |
| 274 | :param image_client: the openstack python client representing the image |
| 275 | client |
| 276 | """ |
| 277 | exist = True |
| 278 | if not TEMPEST_IMAGE_ID or not TEMPEST_IMAGE_ID_ALT: |
| 279 | exist = False |
| 280 | else: |
| 281 | try: |
| 282 | image_client.images.get(TEMPEST_IMAGE_ID) |
| 283 | image_client.images.get(TEMPEST_IMAGE_ID_ALT) |
| 284 | except glance_exception.HTTPNotFound: |
| 285 | exist = False |
| 286 | |
| 287 | return exist |
| 288 | |
| 289 | |
| 290 | def download_and_register_uec_images(image_client, download_url, |
| 291 | download_folder): |
| 292 | """ |
| 293 | Downloads and registered the UEC AKI/AMI/ARI images |
| 294 | :param image_client: |
| 295 | :param download_url: the url of the uec tar file |
| 296 | :param download_folder: the destination folder we wish to save the file to |
| 297 | """ |
| 298 | basename = os.path.basename(download_url) |
| 299 | path = os.path.join(download_folder, basename) |
| 300 | |
| 301 | request = urllib2.urlopen(download_url) |
| 302 | |
| 303 | # First, download the file |
| 304 | with open(path, "wb") as fp: |
| 305 | while True: |
| 306 | chunk = request.read(IMAGE_DOWNLOAD_CHUNK_SIZE) |
| 307 | if not chunk: |
| 308 | break |
| 309 | |
| 310 | fp.write(chunk) |
| 311 | |
| 312 | # Then extract and register images |
| 313 | tar = tarfile.open(path, "r") |
| 314 | for name in tar.getnames(): |
| 315 | file_obj = tar.extractfile(name) |
| 316 | format = "aki" |
| 317 | |
| 318 | if file_obj.name.endswith(".img"): |
| 319 | format = "ami" |
| 320 | |
| 321 | if file_obj.name.endswith("initrd"): |
| 322 | format = "ari" |
| 323 | |
| 324 | # Register images in image client |
| 325 | image_client.images.create(name=file_obj.name, disk_format=format, |
| 326 | container_format=format, data=file_obj, |
| 327 | is_public="true") |
| 328 | |
| 329 | tar.close() |
| 330 | |
| 331 | |
| 332 | def create_images(image_client, config, config_section, |
| 333 | download_url=IMAGE_UEC_SOURCE_URL, |
| 334 | download_folder=TEMPEST_TEMP_DIR): |
| 335 | """ |
| 336 | Creates images for tempest's use and registers the environment variables |
| 337 | IMAGE_ID and IMAGE_ID_ALT with registered images |
| 338 | :param image_client: Openstack python image client |
| 339 | :param config: a ConfigParser object representing the tempest config file |
| 340 | :param config_section: the section name where the IMAGE ids are set |
| 341 | :param download_url: the URL from which we should download the UEC tar |
| 342 | :param download_folder: the place where we want to save the download file |
| 343 | """ |
| 344 | if not images_exist(image_client): |
| 345 | # Falls down to the default uec images |
| 346 | download_and_register_uec_images(image_client, download_url, |
| 347 | download_folder) |
| 348 | image_ids = [] |
| 349 | for image in image_client.images.list(): |
| 350 | image_ids.append(image.id) |
| 351 | |
| 352 | os.environ["IMAGE_ID"] = image_ids[0] |
| 353 | os.environ["IMAGE_ID_ALT"] = image_ids[1] |
| 354 | |
| 355 | params = {'image_ref': os.getenv("IMAGE_ID"), |
| 356 | 'image_ref_alt': os.getenv("IMAGE_ID_ALT")} |
| 357 | |
| 358 | update_config_section_with_params(config, config_section, params) |
| 359 | |
| 360 | |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 361 | def main(): |
| 362 | """ |
| 363 | Main module to control the script |
| 364 | """ |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 365 | # Check if config file exists or fall to the default sample otherwise |
| 366 | path_to_config = TEMPEST_CONFIG_SAMPLE |
| 367 | |
| 368 | if os.path.isfile(TEMPEST_CONFIG_FILE): |
| 369 | path_to_config = TEMPEST_CONFIG_FILE |
| 370 | |
| 371 | config = get_tempest_config(path_to_config) |
| 372 | update_config_admin_credentials(config, 'identity') |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 373 | |
| 374 | client_manager = ClientManager() |
| 375 | |
| 376 | # Set the identity related info for tempest |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 377 | identity_client_kwargs = get_identity_client_kwargs(config, |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 378 | 'identity') |
| 379 | identity_client = client_manager.get_identity_client( |
| 380 | **identity_client_kwargs) |
| 381 | |
| 382 | # Create the necessary users and tenants for tempest run |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 383 | create_users_and_tenants(identity_client, config, 'identity') |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 384 | |
Tal Kammer | f30b4ef | 2013-11-12 14:11:23 +0200 | [diff] [blame] | 385 | # Set the image related info for tempest |
| 386 | image_client_kwargs = get_image_client_kwargs(identity_client, |
| 387 | config, |
| 388 | 'identity') |
| 389 | image_client = client_manager.get_image_client(**image_client_kwargs) |
| 390 | |
| 391 | # Create the necessary users and tenants for tempest run |
| 392 | create_images(image_client, config, 'compute') |
| 393 | |
| 394 | # TODO(tkammer): add network implementation |
Tal Kammer | c6b9788 | 2013-08-20 18:06:18 +0300 | [diff] [blame] | 395 | |
| 396 | if __name__ == "__main__": |
| 397 | main() |