| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | # |
| 4 | # Copyright 2016 RIFT.IO Inc |
| 5 | # |
| 6 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 7 | # you may not use this file except in compliance with the License. |
| 8 | # You may obtain 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, |
| 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | # See the License for the specific language governing permissions and |
| 16 | # limitations under the License. |
| 17 | # |
| 18 | |
| 19 | from gi import require_version |
| 20 | require_version('RwCal', '1.0') |
| 21 | |
| 22 | from gi.repository import RwcalYang |
| 23 | from gi.repository.RwTypes import RwStatus |
| 24 | import logging |
| 25 | import rw_peas |
| 26 | import rwlogger |
| 27 | import time |
| 28 | import argparse |
| 29 | import os |
| 30 | import sys |
| 31 | import uuid |
| 32 | from os.path import basename |
| 33 | |
| 34 | FLAVOR_NAME = 'm1.medium' |
| 35 | DEFAULT_IMAGE='/net/sharedfiles/home1/common/vm/rift-root-latest.qcow2' |
| 36 | |
| 37 | persistent_resources = { |
| 38 | 'vms' : ['mission_control','launchpad',], |
| 39 | 'networks' : ['public', 'private', 'multisite'], |
| 40 | 'flavors' : ['m1.tiny', 'm1.small', 'm1.medium', 'm1.large', 'm1.xlarge'], |
| 41 | 'images' : ['rwimage','rift-root-latest.qcow2','rift-root-latest-trafgen.qcow2', 'rift-root-latest-trafgen-f.qcow2'] |
| 42 | } |
| 43 | |
| 44 | # |
| 45 | # Important information about openstack installation. This needs to be manually verified |
| 46 | # |
| 47 | openstack_info = { |
| 48 | 'username' : 'pluto', |
| 49 | 'password' : 'mypasswd', |
| 50 | 'project_name' : 'demo', |
| 51 | 'mgmt_network' : 'private', |
| 52 | 'physical_network' : 'physnet1', |
| 53 | 'network_type' : 'VLAN', |
| 54 | 'segmentation_id' : 42, ### What else? |
| 55 | 'subnets' : ["11.0.0.0/24", "12.0.0.0/24", "13.0.0.0/24", "14.0.0.0/24"], |
| 56 | 'subnet_index' : 0, |
| 57 | } |
| 58 | |
| 59 | |
| 60 | logging.basicConfig(level=logging.INFO) |
| 61 | |
| 62 | USERDATA_FILENAME = os.path.join(os.environ['RIFT_INSTALL'], |
| 63 | 'etc/userdata-template') |
| 64 | |
| 65 | |
| 66 | RIFT_BASE_USERDATA = ''' |
| 67 | #cloud-config |
| 68 | runcmd: |
| 69 | - sleep 5 |
| 70 | - /usr/rift/scripts/cloud/enable_lab |
| 71 | - /usr/rift/etc/fix_this_vm |
| 72 | ''' |
| 73 | |
| 74 | try: |
| 75 | fd = open(USERDATA_FILENAME, 'r') |
| 76 | except Exception as e: |
| 77 | #logger.error("Received exception during opening of userdata (%s) file. Exception: %s" %(USERDATA_FILENAME, str(e))) |
| 78 | sys.exit(-1) |
| 79 | else: |
| 80 | LP_USERDATA_FILE = fd.read() |
| 81 | # Run the enable lab script when the openstack vm comes up |
| 82 | LP_USERDATA_FILE += "runcmd:\n" |
| 83 | LP_USERDATA_FILE += " - /usr/rift/scripts/cloud/enable_lab\n" |
| 84 | LP_USERDATA_FILE += " - /usr/rift/etc/fix_this_vm\n" |
| 85 | |
| 86 | |
| 87 | |
| 88 | def get_cal_plugin(): |
| 89 | """ |
| 90 | Loads rw.cal plugin via libpeas |
| 91 | """ |
| 92 | plugin = rw_peas.PeasPlugin('rwcal_openstack', 'RwCal-1.0') |
| 93 | engine, info, extension = plugin() |
| 94 | cal = plugin.get_interface("Cloud") |
| 95 | # Get the RwLogger context |
| 96 | rwloggerctx = rwlogger.RwLog.Ctx.new("Cal-Log") |
| 97 | try: |
| 98 | rc = cal.init(rwloggerctx) |
| 99 | assert rc == RwStatus.SUCCESS |
| 100 | except: |
| 101 | logger.error("ERROR:Cal plugin instantiation failed. Aborting tests") |
| 102 | else: |
| 103 | logger.info("Openstack Cal plugin successfully instantiated") |
| 104 | return cal |
| 105 | |
| 106 | def get_cal_account(auth_url): |
| 107 | """ |
| 108 | Returns cal account |
| 109 | """ |
| Jeremy Mordkoff | 4870d0e | 2017-09-30 20:28:33 -0400 | [diff] [blame] | 110 | account = RwcalYang.YangData_RwProject_Project_CloudAccounts_CloudAccountList() |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 111 | account.account_type = "openstack" |
| 112 | account.openstack.key = openstack_info['username'] |
| 113 | account.openstack.secret = openstack_info['password'] |
| 114 | account.openstack.auth_url = auth_url |
| 115 | account.openstack.tenant = openstack_info['project_name'] |
| 116 | account.openstack.mgmt_network = openstack_info['mgmt_network'] |
| 117 | return account |
| 118 | |
| 119 | |
| 120 | logger = logging.getLogger('rift.cal.openstackresources') |
| 121 | |
| 122 | class OpenstackResources(object): |
| 123 | """ |
| 124 | A stupid class to manage bunch of openstack resources |
| 125 | """ |
| 126 | def __init__(self, controller): |
| 127 | self._cal = get_cal_plugin() |
| 128 | self._acct = get_cal_account('http://'+controller+':5000/v3/') |
| 129 | self._id = 0 |
| 130 | self._image_id = None |
| 131 | self._flavor_id = None |
| 132 | |
| 133 | def _destroy_vms(self): |
| 134 | """ |
| 135 | Destroy VMs |
| 136 | """ |
| 137 | logger.info("Initiating VM cleanup") |
| 138 | rc, rsp = self._cal.get_vdu_list(self._acct) |
| 139 | vdu_list = [vm for vm in rsp.vdu_info_list if vm.name not in persistent_resources['vms']] |
| 140 | logger.info("Deleting VMs : %s" %([x.name for x in vdu_list])) |
| 141 | |
| 142 | for vdu in vdu_list: |
| 143 | self._cal.delete_vdu(self._acct, vdu.vdu_id) |
| 144 | |
| 145 | logger.info("VM cleanup complete") |
| 146 | |
| 147 | def _destroy_networks(self): |
| 148 | """ |
| 149 | Destroy Networks |
| 150 | """ |
| 151 | logger.info("Initiating Network cleanup") |
| 152 | rc, rsp = self._cal.get_virtual_link_list(self._acct) |
| 153 | vlink_list = [vlink for vlink in rsp.virtual_link_info_list if vlink.name not in persistent_resources['networks']] |
| 154 | |
| 155 | logger.info("Deleting Networks : %s" %([x.name for x in vlink_list])) |
| 156 | for vlink in vlink_list: |
| 157 | self._cal.delete_virtual_link(self._acct, vlink.virtual_link_id) |
| 158 | logger.info("Network cleanup complete") |
| 159 | |
| 160 | def _destroy_flavors(self): |
| 161 | """ |
| 162 | Destroy Flavors |
| 163 | """ |
| 164 | logger.info("Initiating flavor cleanup") |
| 165 | rc, rsp = self._cal.get_flavor_list(self._acct) |
| 166 | flavor_list = [flavor for flavor in rsp.flavorinfo_list if flavor.name not in persistent_resources['flavors']] |
| 167 | |
| 168 | logger.info("Deleting flavors : %s" %([x.name for x in flavor_list])) |
| 169 | |
| 170 | for flavor in flavor_list: |
| 171 | self._cal.delete_flavor(self._acct, flavor.id) |
| 172 | |
| 173 | logger.info("Flavor cleanup complete") |
| 174 | |
| 175 | def _destroy_images(self): |
| 176 | logger.info("Initiating image cleanup") |
| 177 | rc, rsp = self._cal.get_image_list(self._acct) |
| 178 | image_list = [image for image in rsp.imageinfo_list if image.name not in persistent_resources['images']] |
| 179 | |
| 180 | logger.info("Deleting images : %s" %([x.name for x in image_list])) |
| 181 | |
| 182 | for image in image_list: |
| 183 | self._cal.delete_image(self._acct, image.id) |
| 184 | |
| 185 | logger.info("Image cleanup complete") |
| 186 | |
| 187 | def destroy_resource(self): |
| 188 | """ |
| 189 | Destroy resources |
| 190 | """ |
| 191 | logger.info("Cleaning up openstack resources") |
| 192 | self._destroy_vms() |
| 193 | self._destroy_networks() |
| 194 | self._destroy_flavors() |
| 195 | self._destroy_images() |
| 196 | logger.info("Cleaning up openstack resources.......[Done]") |
| 197 | |
| 198 | def create_mission_control(self): |
| 199 | vm_id = self.create_vm('mission_control', |
| 200 | userdata = RIFT_BASE_USERDATA) |
| 201 | return vm_id |
| 202 | |
| 203 | |
| 204 | def create_launchpad_vm(self, salt_master=None): |
| 205 | node_id = str(uuid.uuid4()) |
| 206 | if salt_master is not None: |
| 207 | userdata = LP_USERDATA_FILE.format(master_ip = salt_master, |
| 208 | lxcname = node_id) |
| 209 | else: |
| 210 | userdata = RIFT_BASE_USERDATA |
| 211 | |
| 212 | vm_id = self.create_vm('launchpad', |
| 213 | userdata = userdata, |
| 214 | node_id = node_id) |
| 215 | # vm_id = self.create_vm('launchpad2', |
| 216 | # userdata = userdata, |
| 217 | # node_id = node_id) |
| 218 | return vm_id |
| 219 | |
| 220 | def create_vm(self, name, userdata, node_id = None): |
| 221 | """ |
| 222 | Creates a VM. The VM name is derived from username |
| 223 | |
| 224 | """ |
| Jeremy Mordkoff | 4870d0e | 2017-09-30 20:28:33 -0400 | [diff] [blame] | 225 | vm = RwcalYang.YangData_RwProject_Project_VduInitParams() |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 226 | vm.name = name |
| 227 | vm.flavor_id = self._flavor_id |
| 228 | vm.image_id = self._image_id |
| 229 | if node_id is not None: |
| 230 | vm.node_id = node_id |
| 231 | vm.vdu_init.userdata = userdata |
| 232 | vm.allocate_public_address = True |
| 233 | logger.info("Starting a VM with parameter: %s" %(vm)) |
| 234 | |
| 235 | rc, vm_id = self._cal.create_vdu(self._acct, vm) |
| 236 | assert rc == RwStatus.SUCCESS |
| 237 | logger.info('Created vm: %s with id: %s', name, vm_id) |
| 238 | return vm_id |
| 239 | |
| 240 | def create_network(self, name): |
| 241 | logger.info("Creating network with name: %s" %name) |
| Jeremy Mordkoff | 4870d0e | 2017-09-30 20:28:33 -0400 | [diff] [blame] | 242 | network = RwcalYang.YangData_RwProject_Project_VimResources_NetworkinfoList() |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 243 | network.network_name = name |
| 244 | network.subnet = openstack_info['subnets'][openstack_info['subnet_index']] |
| 245 | |
| 246 | if openstack_info['subnet_index'] == len(openstack_info['subnets']): |
| 247 | openstack_info['subnet_index'] = 0 |
| 248 | else: |
| 249 | openstack_info['subnet_index'] += 1 |
| 250 | |
| 251 | if openstack_info['physical_network']: |
| 252 | network.provider_network.physical_network = openstack_info['physical_network'] |
| 253 | if openstack_info['network_type']: |
| 254 | network.provider_network.overlay_type = openstack_info['network_type'] |
| 255 | if openstack_info['segmentation_id']: |
| 256 | network.provider_network.segmentation_id = openstack_info['segmentation_id'] |
| 257 | openstack_info['segmentation_id'] += 1 |
| 258 | |
| 259 | rc, net_id = self._cal.create_network(self._acct, network) |
| 260 | assert rc == RwStatus.SUCCESS |
| 261 | |
| 262 | logger.info("Successfully created network with id: %s" %net_id) |
| 263 | return net_id |
| 264 | |
| 265 | |
| 266 | |
| 267 | def create_image(self, location): |
| Jeremy Mordkoff | 4870d0e | 2017-09-30 20:28:33 -0400 | [diff] [blame] | 268 | img = RwcalYang.YangData_RwProject_Project_VimResources_ImageinfoList() |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 269 | img.name = basename(location) |
| 270 | img.location = location |
| 271 | img.disk_format = "qcow2" |
| 272 | img.container_format = "bare" |
| 273 | |
| 274 | logger.info("Uploading image : %s" %img.name) |
| 275 | rc, img_id = self._cal.create_image(self._acct, img) |
| 276 | assert rc == RwStatus.SUCCESS |
| 277 | |
| 278 | rs = None |
| 279 | rc = None |
| 280 | image = None |
| 281 | for i in range(100): |
| 282 | rc, rs = self._cal.get_image(self._acct, img_id) |
| 283 | assert rc == RwStatus.SUCCESS |
| 284 | logger.info("Image (image_id: %s) reached status : %s" %(img_id, rs.state)) |
| 285 | if rs.state == 'active': |
| 286 | image = rs |
| 287 | break |
| 288 | else: |
| 289 | time.sleep(2) # Sleep for a second |
| 290 | |
| 291 | if image is None: |
| 292 | logger.error("Failed to upload openstack image: %s", img) |
| 293 | sys.exit(1) |
| 294 | |
| 295 | self._image_id = img_id |
| 296 | logger.info("Uploading image.......[Done]") |
| 297 | |
| 298 | def create_flavor(self): |
| 299 | """ |
| 300 | Create Flavor suitable for rift_ping_pong VNF |
| 301 | """ |
| Jeremy Mordkoff | 4870d0e | 2017-09-30 20:28:33 -0400 | [diff] [blame] | 302 | flavor = RwcalYang.YangData_RwProject_Project_VimResources_FlavorinfoList() |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 303 | flavor.name = FLAVOR_NAME |
| 304 | flavor.vm_flavor.memory_mb = 16384 # 16GB |
| 305 | flavor.vm_flavor.vcpu_count = 4 |
| 306 | flavor.vm_flavor.storage_gb = 20 # 20 GB |
| 307 | |
| 308 | logger.info("Creating new flavor. Flavor Info: %s" %str(flavor.vm_flavor)) |
| 309 | |
| 310 | rc, flavor_id = self._cal.create_flavor(self._acct, flavor) |
| 311 | assert rc == RwStatus.SUCCESS |
| 312 | logger.info("Creating new flavor.......[Done]") |
| 313 | return flavor_id |
| 314 | |
| 315 | def find_image(self, name): |
| 316 | logger.info("Searching for uploaded image: %s" %name) |
| 317 | rc, rsp = self._cal.get_image_list(self._acct) |
| 318 | image_list = [image for image in rsp.imageinfo_list if image.name == name] |
| 319 | |
| 320 | if not image_list: |
| 321 | logger.error("Image %s not found" %name) |
| 322 | return None |
| 323 | |
| 324 | self._image_id = image_list[0].id |
| 325 | logger.info("Searching for uploaded image.......[Done]") |
| 326 | return self._image_id |
| 327 | |
| 328 | def find_flavor(self, name=FLAVOR_NAME): |
| 329 | logger.info("Searching for required flavor: %s" %name) |
| 330 | rc, rsp = self._cal.get_flavor_list(self._acct) |
| 331 | flavor_list = [flavor for flavor in rsp.flavorinfo_list if flavor.name == name] |
| 332 | |
| 333 | if not flavor_list: |
| 334 | logger.error("Flavor %s not found" %name) |
| 335 | self._flavor_id = self.create_flavor() |
| 336 | else: |
| 337 | self._flavor_id = flavor_list[0].id |
| 338 | |
| 339 | logger.info("Searching for required flavor.......[Done]") |
| 340 | return self._flavor_id |
| 341 | |
| 342 | |
| 343 | |
| 344 | |
| 345 | def main(): |
| 346 | """ |
| 347 | Main routine |
| 348 | """ |
| 349 | parser = argparse.ArgumentParser(description='Script to manage openstack resources') |
| 350 | |
| 351 | parser.add_argument('--controller', |
| 352 | action = 'store', |
| 353 | dest = 'controller', |
| 354 | type = str, |
| 355 | help='IP Address of openstack controller. This is mandatory parameter') |
| 356 | |
| 357 | parser.add_argument('--cleanup', |
| 358 | action = 'store', |
| 359 | dest = 'cleanup', |
| 360 | nargs = '+', |
| 361 | type = str, |
| 362 | help = 'Perform resource cleanup for openstack installation. \n Possible options are {all, flavors, vms, networks, images}') |
| 363 | |
| 364 | parser.add_argument('--persist-vms', |
| 365 | action = 'store', |
| 366 | dest = 'persist_vms', |
| 367 | help = 'VM instance name to persist') |
| 368 | |
| 369 | parser.add_argument('--salt-master', |
| 370 | action = 'store', |
| 371 | dest = 'salt_master', |
| 372 | type = str, |
| 373 | help='IP Address of salt controller. Required, if VMs are being created.') |
| 374 | |
| 375 | parser.add_argument('--upload-image', |
| 376 | action = 'store', |
| 377 | dest = 'upload_image', |
| 378 | help='Openstack image location to upload and use when creating vms.x') |
| 379 | |
| 380 | parser.add_argument('--use-image', |
| 381 | action = 'store', |
| 382 | dest = 'use_image', |
| 383 | help='Image name to be used for VM creation') |
| 384 | |
| 385 | parser.add_argument('--use-flavor', |
| 386 | action = 'store', |
| 387 | dest = 'use_flavor', |
| 388 | help='Flavor name to be used for VM creation') |
| 389 | |
| 390 | parser.add_argument('--mission-control', |
| 391 | action = 'store_true', |
| 392 | dest = 'mission_control', |
| 393 | help='Create Mission Control VM') |
| 394 | |
| 395 | |
| 396 | parser.add_argument('--launchpad', |
| 397 | action = 'store_true', |
| 398 | dest = 'launchpad', |
| 399 | help='Create LaunchPad VM') |
| 400 | |
| 401 | parser.add_argument('--use-project', |
| 402 | action = 'store', |
| 403 | dest = 'use_project', |
| 404 | help='Project name to be used for VM creation') |
| 405 | |
| 406 | parser.add_argument('--clean-mclp', |
| 407 | action='store_true', |
| 408 | dest='clean_mclp', |
| 409 | help='Remove Mission Control and Launchpad VMs') |
| 410 | |
| 411 | argument = parser.parse_args() |
| 412 | |
| 413 | if argument.persist_vms is not None: |
| 414 | global persistent_resources |
| 415 | vm_name_list = argument.persist_vms.split(',') |
| 416 | for single_vm in vm_name_list: |
| 417 | persistent_resources['vms'].append(single_vm) |
| 418 | logger.info("persist-vms: %s" % persistent_resources['vms']) |
| 419 | |
| 420 | if argument.clean_mclp: |
| 421 | persistent_resources['vms'] = [] |
| 422 | |
| 423 | if argument.controller is None: |
| 424 | logger.error('Need openstack controller IP address') |
| 425 | sys.exit(-1) |
| 426 | |
| 427 | |
| 428 | if argument.use_project is not None: |
| 429 | openstack_info['project_name'] = argument.use_project |
| 430 | |
| 431 | ### Start processing |
| 432 | logger.info("Instantiating cloud-abstraction-layer") |
| 433 | drv = OpenstackResources(argument.controller) |
| 434 | logger.info("Instantiating cloud-abstraction-layer.......[Done]") |
| 435 | |
| 436 | |
| 437 | if argument.cleanup is not None: |
| 438 | for r_type in argument.cleanup: |
| 439 | if r_type == 'all': |
| 440 | drv.destroy_resource() |
| 441 | break |
| 442 | if r_type == 'images': |
| 443 | drv._destroy_images() |
| 444 | if r_type == 'flavors': |
| 445 | drv._destroy_flavors() |
| 446 | if r_type == 'vms': |
| 447 | drv._destroy_vms() |
| 448 | if r_type == 'networks': |
| 449 | drv._destroy_networks() |
| 450 | |
| 451 | if argument.upload_image is not None: |
| 452 | image_name_list = argument.upload_image.split(',') |
| 453 | logger.info("Will upload %d image(s): %s" % (len(image_name_list), image_name_list)) |
| 454 | for image_name in image_name_list: |
| 455 | drv.create_image(image_name) |
| 456 | #print("Uploaded :", image_name) |
| 457 | |
| 458 | elif argument.use_image is not None: |
| 459 | img = drv.find_image(argument.use_image) |
| 460 | if img == None: |
| 461 | logger.error("Image: %s not found" %(argument.use_image)) |
| 462 | sys.exit(-4) |
| 463 | else: |
| 464 | if argument.mission_control or argument.launchpad: |
| 465 | img = drv.find_image(basename(DEFAULT_IMAGE)) |
| 466 | if img == None: |
| 467 | drv.create_image(DEFAULT_IMAGE) |
| 468 | |
| 469 | if argument.use_flavor is not None: |
| 470 | drv.find_flavor(argument.use_flavor) |
| 471 | else: |
| 472 | drv.find_flavor() |
| 473 | |
| 474 | if argument.mission_control == True: |
| 475 | drv.create_mission_control() |
| 476 | |
| 477 | if argument.launchpad == True: |
| 478 | drv.create_launchpad_vm(salt_master = argument.salt_master) |
| 479 | |
| 480 | |
| 481 | if __name__ == '__main__': |
| 482 | main() |
| 483 | |