From 1532d5b6c663d3c7e64260b28d3c7236f12eaf1d Mon Sep 17 00:00:00 2001 From: DR695H Date: Wed, 15 Feb 2017 15:03:20 -0500 Subject: Initial OpenECOMP Temporary Heatbridge commit Change-Id: Ifb183a84f596d7adacd68ca0f9b1e3780311036a Signed-off-by: DR695H --- .gitignore | 2 + LICENSE.TXT | 25 +++++ README.TXT | 8 ++ heatbridge/AAIManager.py | 219 +++++++++++++++++++++++++++++++++++++++++ heatbridge/HeatBridge.py | 66 +++++++++++++ heatbridge/OpenstackContext.py | 16 +++ heatbridge/OpenstackManager.py | 144 +++++++++++++++++++++++++++ heatbridge/__init__.py | 4 + setup.cfg | 5 + setup.py | 10 ++ tox.ini | 12 +++ 11 files changed, 511 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.TXT create mode 100644 README.TXT create mode 100644 heatbridge/AAIManager.py create mode 100644 heatbridge/HeatBridge.py create mode 100644 heatbridge/OpenstackContext.py create mode 100644 heatbridge/OpenstackManager.py create mode 100644 heatbridge/__init__.py create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9bd9f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.tox/* +openecomp_heatbridge.egg-info/* diff --git a/LICENSE.TXT b/LICENSE.TXT new file mode 100644 index 0000000..4aa836c --- /dev/null +++ b/LICENSE.TXT @@ -0,0 +1,25 @@ +ECOMP and OpenECOMP are trademarks and service marks of AT&T Intellectual Property. + +LICENSE.TXT file can be appended as follows: +/* + * ============LICENSE_START========================================== + * =================================================================== + * Copyright © 2017 AT&T Intellectual Property. All rights reserved. + * =================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END============================================ + * + * ECOMP and OpenECOMP are trademarks + * and service marks of AT&T Intellectual Property. + * + */ \ No newline at end of file diff --git a/README.TXT b/README.TXT new file mode 100644 index 0000000..9c2f259 --- /dev/null +++ b/README.TXT @@ -0,0 +1,8 @@ +OpenECOMP HeatBridge +======================= + + + +Script to input heat stack information into aai + +to install locally, checkout this repo and then run 'pip install -e .' in the root \ No newline at end of file diff --git a/heatbridge/AAIManager.py b/heatbridge/AAIManager.py new file mode 100644 index 0000000..70f974b --- /dev/null +++ b/heatbridge/AAIManager.py @@ -0,0 +1,219 @@ +from collections import defaultdict + +class AAIManager: + """AAIManager manages the connection state and interaction between an aai instance and the heatbridge.""" + + def __init__(self, context): + self.openstack_context = context; + + def get_link(self, links, link_type): + for link in links: + if link['rel'] == link_type: + return link['href']; + + def create_transactions(self, *args): + transactions = defaultdict(list); + for arg in args: + transactions['transactions'].extend(arg); + return transactions; + + def create_put(self, *args): + put = defaultdict(list); + put['put'].extend(args); + return put; + + def create_vserver_put(self, server_info_dict, volumes_dict): + #setup + put = dict(); + body = dict(); + put['body'] = body; + + #required fields + put['uri'] = "/cloud-infrastructure/cloud-regions/cloud-region/" + self.openstack_context.owner \ + + "/" + self.openstack_context.region + "/tenants/tenant/" + self.openstack_context.tenant \ + + "/vservers/vserver/" + server_info_dict['id']; + body['vserver-id'] = server_info_dict['id']; + body['vserver-name'] = server_info_dict['name']; + body['vserver-name2'] = server_info_dict['name']; + body['prov-status'] = server_info_dict['status']; + body['vserver-selflink'] = self.get_link(server_info_dict['links'], "self"); + #body['in-maint']; + #body['is-closed-loop-disabled']; + #body['resource-version']; + + #optional fields + volumes = [] + body['volumes'] = volumes + for volume in volumes_dict: + volumes['volume'] = self.create_volume(volume); + + relations = []; + if self.__exists(server_info_dict['metadata'], 'vnf_id'): + data = self.__create_relationship_data("generic-vnf", "vnf-id", server_info_dict['metadata']['vnf_id']); + list = self.__create_relationship_data_list(data); + relations.append(self.__create_relationship("generic-vnf", list)); + if self.__exists(server_info_dict['flavor'], 'id'): + data = self.__create_relationship_data("flavor", "flavor-id", server_info_dict['flavor']['id']); + data2 = self.__create_relationship_data("cloud-region", "cloud-owner", self.openstack_context.owner); + data3 = self.__create_relationship_data("cloud-region", "cloud-region-id", self.openstack_context.region); + list = self.__create_relationship_data_list(data, data2, data3); + relations.append(self.__create_relationship("flavor", list)); + if self.__exists(server_info_dict['image'], 'id'): + data = self.__create_relationship_data("image", "image-id", server_info_dict['image']['id']); + data2 = self.__create_relationship_data("cloud-region", "cloud-owner", self.openstack_context.owner); + data3 = self.__create_relationship_data("cloud-region", "cloud-region-id", self.openstack_context.region); + list = self.__create_relationship_data_list(data, data2, data3); + relations.append(self.__create_relationship("image", list)); + body['relationship-list'] = self.__create_relationship_list(relations); + return put + + def __create_relationship_list(self, items): + rel_list = dict() + relationship = [] + rel_list['relationship'] = relationship; + relationship.extend(items); + return rel_list; + + def __create_relationship_data_list(self, *relationship_data): + relationship_data_list = []; + relationship_data_list.extend(relationship_data); + return relationship_data_list; + + def __create_relationship(self, related_to, relationship_data_list): + relationship = dict(); + relationship['related-to'] = related_to; + relationship['relationship-data'] = relationship_data_list + return relationship; + + def __create_relationship_data(self, parent, key, value): + relationship_data = dict(); + relationship_data['relationship-key'] = parent + "." + key; + relationship_data['relationship-value'] = value; + return relationship_data; + + def create_volume(self, volume_dict): + #setup + volume = dict() + volume['volume-id'] = volume_dict['volume']['id'] + #volume['volume-selflink'] + #volume['resource-version'] + #volume['relationship-list'] + return volume; + + def create_flavor_put(self, flavor_dict): + #setup + put = dict(); + body = dict() + put['body'] = body + + #required fields + put['uri'] = "/cloud-infrastructure/cloud-regions/cloud-region/" + self.openstack_context.owner \ + + "/" + self.openstack_context.region + "/flavors/flavor/" + flavor_dict['id'] + body['flavor-id'] = flavor_dict['id'] + body['flavor-name'] = flavor_dict['name'] + body['flavor-vcpus'] = flavor_dict['vcpus'] + body['flavor-ram'] = flavor_dict['ram'] + body['flavor-disk'] = flavor_dict['disk'] + if flavor_dict['OS-FLV-EXT-DATA:ephemeral'] != "": + body['flavor-ephemeral'] = flavor_dict['OS-FLV-EXT-DATA:ephemeral'] + if flavor_dict['swap'] != "": + body['flavor-swap'] = flavor_dict['swap'] + body['flavor-selflink'] = self.get_link(flavor_dict['links'], "self"); + #body['flavor-is-public''] + #body['flavor-disabled'] + #body['relationship-list'] + return put + + def create_image_put(self, image_dict, server_info): + #setup + put = dict(); + body = dict() + put['body'] = body + put['uri'] = "/cloud-infrastructure/cloud-regions/cloud-region/" + self.openstack_context.owner \ + + "/" + self.openstack_context.region + "/images/image/" + image_dict['id'] + body['image-id'] = image_dict['id'] + body['image-name'] = image_dict['name'] + body['image-selflink'] = self.get_link(server_info['image']['links'], "bookmark"); + if self.__exists(image_dict, 'org.openstack__1__architecture'): + body['image-architecture'] = image_dict['org.openstack__1__architecture'] + if self.__exists(image_dict, 'org.openstack__1__os_distro'): + body['image-name'] = image_dict['org.openstack__1__os_distro'] + if self.__exists(image_dict, 'org.openstack__1__os_version'): + body['image-os-version'] = image_dict['org.openstack__1__os_version'] + if self.__exists(image_dict, 'org.openstack__1__application_version'): + body['application-version'] = image_dict['org.openstack__1__application_version'] + if self.__exists(image_dict, 'org.openstack__1__application_vendor'): + body['application-vendor'] = image_dict['org.openstack__1__application_vendor'] + if self.__exists(image_dict, 'org.openstack__1__application'): + body['application'] = image_dict['org.openstack__1__application'] + if self.__exists(image_dict, 'os_distro'): + body['image-os-distro'] = image_dict['os_distro'] + #body['metadata'] + #body['relationship-list']; + return put + + def __exists(self, the_dict, key): + if key in the_dict and the_dict[key] != "": + return True; + else: + return False; + + def create_l_interface_put(self, port_dict, server_info_dict): + #setup + port = port_dict['port'] + put = dict(); + body = dict() + put['body'] = body + put['uri'] = "/cloud-infrastructure/cloud-regions/cloud-region/" + self.openstack_context.owner \ + + "/" + self.openstack_context.region + "/tenants/tenant/" + self.openstack_context.tenant \ + + "/vservers/vserver/" + port['device_id'] + "/l-interfaces/l-interface/" + port['name'] + body['interface-id'] = port['id'] + body['interface-name'] = port['name'] + body['macaddr'] = port['mac_address'] + #optional fields + v4list = [] + v6list = [] + body['l3-interface-ipv4-address-list'] = v4list + body['l3-interface-ipv6-address-list'] = v6list + for ips in port['fixed_ips']: + if '.' in ips['ip_address']: + v4list.append(self.create_l3_interface_ipv4_address(port, ips)); + if ':' in ips['ip_address']: + v6list.append(self.create_l3_interface_ipv6_address(port, ips)); + body['network-name'] = port['network_id'] + #body['selflink'] + #body['interface-role'] + #body['v6-wan-link-ip'] + #body['management-option'] + + #optional fields + return put + + def create_l3_interface_ipv4_address(self, port_dict, fixed_ip): + #setup + address = dict() + address['l3-interface-ipv4-address'] = fixed_ip['ip_address'] + address['l3-interface-ipv4-prefix-length'] = '32' + address['neutron-network-id'] = port_dict['network_id'] + address['neutron-subnet-id'] = fixed_ip['subnet_id'] + #address['vlan-id-inner'] + #address['vlan-id-outer'] + #address['is-floating'] + #address['relationship-list'] + return address; + + def create_l3_interface_ipv6_address(self, port_dict, fixed_ip): + #setup + address = dict() + address['l3-interface-ipv6-address'] = fixed_ip['ip_address'] + address['l3-interface-ipv6-prefix-length'] = '128' + address['neutron-network-id'] = port_dict['network_id'] + address['neutron-subnet-id'] = fixed_ip['subnet_id'] + #address['vlan-id-inner'] + #address['vlan-id-outer'] + #address['is-floating'] + #address['relationship-list'] + return address; + + def load_aai_data(self, request): + return True; \ No newline at end of file diff --git a/heatbridge/HeatBridge.py b/heatbridge/HeatBridge.py new file mode 100644 index 0000000..f9b9580 --- /dev/null +++ b/heatbridge/HeatBridge.py @@ -0,0 +1,66 @@ +import json +from OpenstackManager import OpenstackManager +from OpenstackContext import OpenstackContext +from AAIManager import AAIManager + +class HeatBridge: + def __init__(self): + pass; + + def init_bridge(self, openstack_identity_url, username, password, tenant, region, owner): + self.om = OpenstackManager(openstack_identity_url, OpenstackContext(username, password, tenant, region, owner)); + self.am = AAIManager(OpenstackContext(username, password, tenant, region, owner)); + + def filterbyvalue(self, seq, key, value): + for el in seq: + if el[key]==value: yield el + + def build_request(self, heat_stack_id): + resources = self.om.get_stack_resources(heat_stack_id) + servers = list(self.filterbyvalue(resources, "resource_type", "OS::Nova::Server")); + #networks = list(self.filterbyvalue(resources, "resource_type", "OS::Neutron::Net")); + #subnets = list(self.filterbyvalue(resources, "resource_type", "OS::Neutron::Subnet")); + ports = list(self.filterbyvalue(resources, "resource_type", "OS::Neutron::Port")); + #keys = list(self.filterbyvalue(resources, "resource_type", "OS::Nova::KeyPair")); + + put_blocks = [] + + #build the servers and attach them to vnf + server_put_blocks = [] + image_put_blocks = [] + flavor_put_blocks = [] + for item in servers: + server_info = self.om.get_server_info(item['physical_resource_id']); + server_volumes = self.om.get_server_volumes(item['physical_resource_id']); + volumes = []; + for vols in server_volumes: + volumes.append(self.om.get_volume_info(vols['id'])); + aai_vserver = self.am.create_vserver_put(server_info, volumes); + flavor_info = self.om.get_flavor_info(server_info['flavor']['id']); + aai_flavor = self.am.create_flavor_put(flavor_info); + image_info = self.om.get_image_info(server_info['image']['id']); + aai_image = self.am.create_image_put(image_info, server_info); + server_put_blocks.append(self.am.create_put(aai_vserver)); + image_put_blocks.append(self.am.create_put(aai_image)); + flavor_put_blocks.append(self.am.create_put(aai_flavor)); + put_blocks.extend(image_put_blocks); + put_blocks.extend(flavor_put_blocks); + put_blocks.extend(server_put_blocks); + + #build the ports and attach them to servers + linterface_put_blocks = [] + #all servers have same vnf id + random_server_info = self.om.get_server_info(servers[0]['physical_resource_id']); + for item in ports: + #todo: pass in the networks from above + port_info = self.om.get_port_info(item['physical_resource_id']) + aai_linterface = self.am.create_l_interface_put(port_info, random_server_info); + linterface_put_blocks.append(self.am.create_put(aai_linterface)); + put_blocks.extend(linterface_put_blocks); + + return json.dumps(self.am.create_transactions(put_blocks)); + + def bridge_data(self, heat_stack_id): + request = self.build_request(heat_stack_id); + print request; + return request; \ No newline at end of file diff --git a/heatbridge/OpenstackContext.py b/heatbridge/OpenstackContext.py new file mode 100644 index 0000000..5e852a2 --- /dev/null +++ b/heatbridge/OpenstackContext.py @@ -0,0 +1,16 @@ +class OpenstackContext: + """OpenstackContext is a simple class that holds the provided information that heatbridge uses.""" + + #this holds the info of the openstack clients + username = None; + password = None; + tenant = None; + region = None; + owner = None; + + def __init__(self, username, password, tenant, region, owner): + self.username = username; + self.password = password; + self.tenant = tenant; + self.region = region; + self.owner = owner; \ No newline at end of file diff --git a/heatbridge/OpenstackManager.py b/heatbridge/OpenstackManager.py new file mode 100644 index 0000000..ed4cd6f --- /dev/null +++ b/heatbridge/OpenstackManager.py @@ -0,0 +1,144 @@ +from heatclient.v1.client import Client as HeatClient; +from novaclient.v2.client import Client as NovaClient; +from cinderclient.v1.client import Client as CinderClient; +from glanceclient.v2.client import Client as GlanceClient; +from neutronclient.v2_0.client import Client as NeutronClient; +import os_client_config +import logging + +class OpenstackManager: + """OpenstackManager manages the connection state and interaction between an openstack cloud and the heatbridge.""" + + #this holds the session of the openstack clients + __heat_client = None; + __nova_client = None; + __cinder_client = None; + __glance_client = None; + __neutron_client = None; + __auth_resp = None; + + def __init__(self, identity_url, context): + """ OpenstackManager + + `identity_url` Base identity_url of the identity server + 'context' Instance of OpenstackContext + """ + self.openstack_context = context; + self.identity_url = identity_url; + self.authenticate(context.username, context.password, context.tenant, context.region) + logging.basicConfig(level=logging.DEBUG) + + def authenticate(self, username, password, tenant, region): + """ Authenticate to openstack env + + `username` username to authenticate to openstack + + `password` password to send + + `tenant` tenant to authenticate under + + `region` region to authenticate under + """ + self.__heat_client = os_client_config.make_client('orchestration', + auth_url=self.identity_url, + username=username, + password=password, + project_name=tenant, + region_name=region); + self.__nova_client = os_client_config.make_client('compute', + auth_url=self.identity_url, + username=username, + password=password, + project_name=tenant, + region_name=region); + self.__cinder_client = os_client_config.make_client('volume', + auth_url=self.identity_url, + username=username, + password=password, + project_name=tenant, + region_name=region); + self.__glance_client = os_client_config.make_client('image', + auth_url=self.identity_url, + username=username, + password=password, + project_name=tenant, + region_name=region); + self.__neutron_client = os_client_config.make_client('network', + auth_url=self.identity_url, + username=username, + password=password, + project_name=tenant, + region_name=region); + #this next line is needed because for v2 apis that are after a certain release stopped providing version info in keytone url but rackspace did not + self.__neutron_client.action_prefix = ""; + self.__auth_resp = True; + + def get_stack(self, stack_id): + self.__check_authenticated() + #: :type client: HeatClient + client = self.__heat_client + stack = client.stacks.get(stack_id) + return stack.to_dict(); + + def get_stack_resources(self, stack_id): + self.__check_authenticated() + #: :type client: HeatClient + client = self.__heat_client; + stack_resources = client.resources.list(stack_id); + stack_resources_dict = map(lambda x:x.to_dict(),stack_resources) + return stack_resources_dict; + + def get_server_volumes(self, server_id): + self.__check_authenticated() + #: :type client: NovaClient + client = self.__nova_client; + server_volumes = client.volumes.get_server_volumes(server_id); + server_volumes_dict = map(lambda x:x.to_dict(),server_volumes) + return server_volumes_dict; + + def get_server_interfaces(self, server_id): + self.__check_authenticated() + #: :type client: NovaClient + client = self.__nova_client; + server_interfaces = client.virtual_interfaces.list(server_id); + server_interfaces_dict = map(lambda x:x.to_dict(),server_interfaces) + return server_interfaces_dict; + + def get_volume_info(self, volume_id): + self.__check_authenticated() + #: :type client: CinderClient + client = self.__cinder_client; + volume_info = client.volumes.get(volume_id); + return volume_info.to_dict(); + + def get_server_info(self, server_id): + self.__check_authenticated() + #: :type client: NovaClient + client = self.__nova_client; + server_info = client.servers.get(server_id); + return server_info.to_dict(); + + def get_image_info(self, image_id): + self.__check_authenticated() + #: :type client: GlanceClient + client = self.__glance_client; + image_info = client.images.get(image_id); + return image_info; + + def get_flavor_info(self, flavor_id): + self.__check_authenticated() + #: :type client: NovaClient + client = self.__nova_client; + flavor_info = client.flavors.get(flavor_id); + return flavor_info.to_dict(); + + def get_port_info(self, port_id): + self.__check_authenticated() + #: :type client: NeutronClient + client = self.__neutron_client; + port_info = client.show_port(port_id); + return port_info; + + def __check_authenticated(self): + if self.__auth_resp == None: + raise AssertionError('__auth_resp should exist before calling operation') \ No newline at end of file diff --git a/heatbridge/__init__.py b/heatbridge/__init__.py new file mode 100644 index 0000000..8be6f3b --- /dev/null +++ b/heatbridge/__init__.py @@ -0,0 +1,4 @@ +class heatbridge: + + def __init__(self): + pass; diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..493416b --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[bdist_wheel] +# This flag says that the code is written to work on both Python 2 and Python +# 3. If at all possible, it is good practice to do this. If you cannot, you +# will need to generate wheels for each Python version that you support. +universal=0 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..a43e2d2 --- /dev/null +++ b/setup.py @@ -0,0 +1,10 @@ +from setuptools import setup + +setup( + name='openecomp-heatbridge', # This is the name of your PyPI-package. + version='0.1', # Update the version number for new releases + description='Script to input heat stack information into aai', # Info about script + install_requires=['python-novaclient','python-cinderclient','python-glanceclient', 'os_client_config', 'python-neutronclient', 'python-heatclient'], # what we need + packages=['heatbridge'], # The name of your scipts package + package_dir={'heatbridge': 'heatbridge'} # The location of your scipts package +) \ No newline at end of file diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..09b4c98 --- /dev/null +++ b/tox.ini @@ -0,0 +1,12 @@ +# Tox (https://tox.readthedocs.io/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + +[tox] +envlist = py27 + +[testenv] +commands = {envpython} setup.py test +deps = + -- cgit 1.2.3-korg