from __future__ import print_function import json import re import sys from xml.etree import ElementTree as xml_et import osaapi import requests from apsconnectcli.config import get_config, CFG_FILE_PATH RPC_CONNECT_PARAMS = ('host', 'user', 'password', 'ssl', 'port') APS_CONNECT_PARAMS = ('aps_host', 'aps_port', 'use_tls_aps') def json_decode(content): if sys.version_info.major < 3 or ( sys.version_info.major == 3 and sys.version_info.minor < 6 ): return json.loads(content.decode('utf-8')) return json.loads(content) def osaapi_raise_for_status(r): if r['status']: if 'error_message' in r: raise Exception("Error: {}".format(r['error_message'])) else: raise Exception("Error: Unknown {}".format(r)) def apsapi_raise_for_status(r): try: r.raise_for_status() except Exception as e: if 'error' in r.json(): err = "{} {}".format(r.json()['error'], r.json()['message']) else: err = str(e) print("Hub APS API response {} code.\n" "Error: {}".format(r.status_code, err)) sys.exit(1) class Hub(object): osaapi = None aps = None hub_id = None extension_id = None def __init__(self): config = get_config() self.osaapi = osaapi.OSA(**{k: config[k] for k in RPC_CONNECT_PARAMS}) self.aps = APS(self.get_admin_token()) self.hub_id = self._get_id() self.extension_id = self._get_extension_id() @staticmethod def configure(hub_host, user='admin', pwd='1q2w3e', use_tls=False, port=8440, aps_host=None, aps_port=6308, use_tls_aps=True): if not aps_host: aps_host = hub_host use_tls = use_tls in ('Yes', 'True', '1') hub = osaapi.OSA(host=hub_host, user=user, password=pwd, ssl=use_tls, port=port) try: hub_version = Hub._get_hub_version(hub) print("Connectivity with Hub RPC API [ok]") Hub._assert_supported_version(hub_version) print("Hub version {}".format(hub_version)) aps_url = '{}://{}:{}'.format('https' if use_tls_aps else 'http', aps_host, aps_port) aps = APS(Hub._get_user_token(hub, 1), aps_url) response = aps.get('aps/2/applications/') response.raise_for_status() print("Connectivity with Hub APS API [ok]") except Exception as e: print("Unable to communicate with hub {}, error: {}".format(hub_host, e)) sys.exit(1) else: with open(CFG_FILE_PATH, 'w+') as cfg: cfg.write(json.dumps({'host': hub_host, 'user': user, 'password': pwd, 'ssl': use_tls, 'port': port, 'aps_port': aps_port, 'aps_host': aps_host, 'use_tls_aps': use_tls_aps}, indent=4)) print("Config saved [{}]".format(CFG_FILE_PATH)) @staticmethod def _get_hub_version(api): r = api.statistics.getStatisticsReport(reports=[{'name': 'BuildHistory', 'value': ''}]) osaapi_raise_for_status(r) tree = xml_et.fromstring(r['result'][0]['value']) return tree.find('Build/Build').text @staticmethod def _assert_supported_version(version, experimental=False): supported = False match = re.match(r'^((?P<oamajor>oa-8)|(?P<cbmajor>cb-20))\.(?P<minor>\d+)-', version) if match: ''' Supported versions: OA-8.0 or upper on counter mode OA-8.3 or upper in experimental mode CB-20.4 in any mode ''' if match.groupdict()['oamajor']: oamajor = int(match.groupdict()['oamajor'].replace('oa-', '')) else: oamajor = 0 if match.groupdict()['cbmajor']: cbmajor = int(match.groupdict()['cbmajor'].replace('cb-', '')) else: cbmajor = 0 minor = int(match.groupdict()['minor']) if experimental: supported = (oamajor >= 8 and minor >= 3)\ or (cbmajor >= 20 and minor >= 4)\ or cbmajor > 20 if not supported: print( "Experimental functionality requires Hub version 8.3 " + "and above, got {}".format(version)) sys.exit(1) else: supported = (oamajor >= 8 and minor >= 0)\ or (cbmajor >= 20 and minor >= 4)\ or cbmajor > 20 if not supported: print("Hub 8.0 version or above needed, got {}".format(version)) sys.exit(1) @staticmethod def _get_user_token(hub, user_id): r = hub.APS.getUserToken(user_id=user_id) osaapi_raise_for_status(r) return {'APS-Token': r['result']['aps_token']} @staticmethod def _get_resclass_name(unit): resclass_name = { 'Kbit/sec': 'rc.saas.resource.kbps', 'kb': 'rc.saas.resource', 'mb-h': 'rc.saas.resource.mbh', 'mhz': 'rc.saas.resource.mhz', 'mhzh': 'rc.saas.resource.mhzh', 'unit': 'rc.saas.resource.unit', 'unit-h': 'rc.saas.resource.unith' }.get(unit) return resclass_name or 'rc.saas.resource.unit' def _get_extension_id(self): url = 'aps/2/resources?implementing(http://odin.com/servicesSelector/globals/2.0)' r = self.aps.get(url) try: data = json_decode(r.content) except ValueError: print("APSController provided non-json format") sys.exit(1) else: return data[0]['aps']['id'] if data else None def _get_id(self): url = 'aps/2/resources?implementing(http://parallels.com/aps/types/pa/poa/1.0)' r = self.aps.get(url) r.raise_for_status() try: data = json_decode(r.content) except ValueError: print("APSController provided non-json format") sys.exit(1) else: return data[0]['aps']['id'] if data else None def get_admin_token(self): return Hub._get_user_token(self.osaapi, 1) def aps_devel_mode(self, disable=False): r = self.osaapi.setSystemProperty(account_id=1, name='APS_DEVEL_MODE', bool_value=not bool(disable)) osaapi_raise_for_status(r) print("APS Development mode {}.".format('DISABLED' if disable else 'ENABLED')) def check_package_operation(self, package): app_id = self.get_application_id(package.connector_id) if app_id is None: print("INFO: package is not installed") return "install" app_instances = self.get_application_instances(int(app_id)) if len(app_instances) == 0: return "install" url = '/aps/2/resources/{}'.format(app_instances[0]['application_resource_id']) r = self.aps.get(url) try: data = json_decode(r.content) except ValueError: print("APSController provided non-json format") sys.exit(1) if 'aps' not in data: print("INFO: package is not installed") return "install" latest = 0 match = re.match(r'{}/app/(?P<major>\d+)\.0'.format( package.connector_id), data['aps']['type'] ) if match: major = int(match.groupdict()['major']) if int(latest) < major: latest = major if int(latest) == int(package.version): return "createRTs" elif int(latest) > int(package.version): print("ERROR: Is not possible to import a version older than existing one at hub") sys.exit(1) print("Upgrade operation from version {} to version {} required".format(latest, package.version)) return "upgrade" def import_package(self, package): args = {'package_url': package.source} if package.is_http else { 'package_body': package.body} r = self.osaapi.APS.importPackage(**args) osaapi_raise_for_status(r) return str(r['result']['application_id']) def create_instance(self, package, oauth_key, oauth_secret, backend_url, settings={}, network='proxy', hub_id=None): payload = { 'aps': { 'package': { 'type': package.connector_id, 'version': package.version, 'release': package.release, }, 'endpoint': backend_url, 'network': network, 'auth': { 'oauth': { 'key': oauth_key, 'secret': oauth_secret, }, }, }, } payload.update(settings) r = self.aps.post('aps/2/applications/', json=payload) try: r.raise_for_status() except Exception as e: if 'error' in r.json(): err = "{} {}".format(r.json()['error'], r.json()['message']) else: err = str(e) print("Installation of connector {} FAILED.\n" "Hub APS API response {} code.\n" "Error: {}".format(package.connector_id, r.status_code, err)) sys.exit(1) return r.json()['app']['aps']['id'] if not package.instance_only else None def _create_core_rts(self, package, app_id, instance_uuid): rt_ids = {} core_resource_types_payload = [ { 'resclass_name': 'rc.saas.service.link', 'name': '{} app instance'.format(package.connector_name), 'act_params': [ { 'var_name': 'app_id', 'var_value': app_id }, { 'var_name': 'resource_uid', 'var_value': instance_uuid }, ] }, { 'resclass_name': 'rc.saas.service', 'name': '{} tenant'.format(package.connector_name), 'act_params': [ { 'var_name': 'app_id', 'var_value': app_id }, { 'var_name': 'service_id', 'var_value': 'tenant' }, { 'var_name': 'autoprovide_service', 'var_value': '1' }, ] }, ] for t in core_resource_types_payload: r = self.osaapi.addResourceType(**t) osaapi_raise_for_status(r) rt_ids[r['result']['resource_type_id']] = 1 return rt_ids def _get_package(self, instance_uuid): r = self.aps.get('aps/2/resources/{}'.format(instance_uuid)) apsapi_raise_for_status(r) return r.json()['aps']['package']['id'] def _is_service_profile_supported(self, instance_uuid): package_uuid = self._get_package(instance_uuid) r = self.aps.get('aps/2/packages/{}'.format(package_uuid)) apsapi_raise_for_status(r) services = r.json()['services'] if 'itemProfile' in services.keys(): print("INFO: Item Profile is Supported") return True print("INFO: Item profile is not supported") return False def _get_item_info_from_local_id(self, product, local_id): payload = { 'product': product, 'local_id': local_id, } r = self.aps.post('aps/2/resources/{}/itemInfo'.format(self.extension_id), json=payload) try: apsapi_raise_for_status(r) data = json_decode(r.content) except ValueError: print("Error while decoding item information") sys.exit(1) if data.get('local_id'): return data print("ERROR: Connector contains non valid items, please contact CloudBlue Connect Support") sys.exit(1) def _create_service_profile(self, package, counter, title): item_info = self._get_item_info_from_local_id(package.product_id, counter) payload = { 'aps': {'type': '{}/{}/{}.{}'.format( package.connector_id, "itemProfile", package.version, package.release)}, 'profileName': title, 'mpn': item_info.get('mpn'), 'itemId': item_info.get('id'), } r = self.aps.post('aps/2/resources/', json=payload) try: r.raise_for_status() except Exception as e: if 'error' in r.json(): err = "{} {}".format(r.json()['error'], r.json()['message']) else: err = str(e) print("ERROR: Adding service profile {} FAILED.\n" "Hub APS API response {} code.\n" "Error: {}".format(counter, r.status_code, err)) sys.exit(1) return r.json()['aps']['id'] def _type_manager_available(self): r = self.aps.get('aps/2/services/resource-type-manager') try: apsapi_raise_for_status(r) except Exception: print("ERROR: Operations with type manager app needed, but not available on this\n" + "HUB.\nCreation of new resource types is not possible without it.\n" + "Please contact CloudBlue support to update your Hub or use Configure Option " + "available on the connector instance on Hub control panel") sys.exit(1) return r.json() def _exists_item_profile_resource(self, package): r = self.aps.get('aps/2/resources?implementing({}/{}/{}.{})'.format( package.connector_id, "itemProfile", package.version, package.release)) try: data = json_decode(r.content) if data[0]['aps']['id']: return True return False except Exception: return False def _get_existing_ref_rts(self, app_id): self._type_manager_available() payload = { 'resclass_name': 'rc.saas.countedlenk', } rts = self.osaapi.getResourceTypesByClass(**payload) existing_resources = [] for resource in rts['result']: if sys.version_info.major < 3 or ( sys.version_info.major == 3 and sys.version_info.minor < 6 ): aps_rt = json.loads( self.aps.get('aps/2/services/resource-type-manager/resourceTypes/{}'.format( resource['resource_type_id'])).content.decode('utf-8')) else: aps_rt = json.loads( self.aps.get('aps/2/services/resource-type-manager/resourceTypes/{}'.format( resource['resource_type_id'])).content) if 'app_id' in aps_rt['activationParameters'] and int(aps_rt['activationParameters'][ 'app_id']) == int(app_id): existing_resources.append(aps_rt['activationParameters']['resource_id']) return existing_resources def _find_existing(self, resource, app_id): if sys.version_info.major < 3 or ( sys.version_info.major == 3 and sys.version_info.minor < 6 ): aps_rt = json.loads( self.aps.get('aps/2/services/resource-type-manager/resourceTypes/{}'.format( resource['resource_type_id'])).content.decode('utf-8')) else: aps_rt = json.loads( self.aps.get('aps/2/services/resource-type-manager/resourceTypes/{}'.format( resource['resource_type_id'])).content) if 'app_id' in aps_rt['activationParameters'] and int( aps_rt['activationParameters'][ 'app_id']) == int(app_id): return aps_rt['activationParameters']['resource_id'] return False def _get_existing_counter_rts(self, app_id): self._type_manager_available() existing_resources = [] rt_classes = ['rc.saas.resource.kbps', 'rc.saas.resource', 'rc.saas.resource.mbh', 'rc.saas.resource.mhz', 'rc.saas.resource.mhzh', 'rc.saas.resource.unit', 'rc.saas.resource.unith', ] for rt_class in rt_classes: payload = { 'resclass_name': rt_class, } rts = self.osaapi.getResourceTypesByClass(**payload) for resource in rts['result']: existing = self._find_existing(resource=resource, app_id=app_id) if existing: existing_resources.append(existing) return existing_resources def _create_counted_ref_rts(self, package, app_id, update=False): rt_ids = {} if update: existis_item_profile = self._exists_item_profile_resource(package) if not existis_item_profile: print("ERROR: Create new resource types for this package version requested \n" + "using new experimental mode, but seams this package uses counter mode.\n" + "update operation aborted, either use normal mode, either ensure " + "some item resources") sys.exit(1) existing_resources = self._get_existing_ref_rts(app_id) for counter, schema in package.counters.items(): if update and counter in existing_resources: continue title = schema.get('title') resource_uid = self._create_service_profile(package, counter, title) payload = { 'resclass_name': 'rc.saas.countedlenk', 'name': title[:60], 'act_params': [ { 'var_name': 'app_id', 'var_value': str(app_id) }, { 'var_name': 'resource_uid', 'var_value': str(resource_uid) }, { 'var_name': 'service_id', 'var_value': 'tenant' }, { 'var_name': 'resource_id', 'var_value': str(counter) } ] } response = self.osaapi.addResourceType(**payload) osaapi_raise_for_status(response) rt_ids[response['result']['resource_type_id']] = 0 return rt_ids def _create_counter_rts(self, package, app_id, update=False): rt_ids = {} if update: existis_item_profile = self._exists_item_profile_resource(package) if existis_item_profile: print("ERROR: Create new resource types for this package version requested \n" + "but seams this package has been instantiated using experimental mode.\n" + "update operation aborted, either use experimental mode, either ensure " + "marketProfile resources does not exists") sys.exit(1) existing_resources = self._get_existing_counter_rts(app_id) for counter, schema in package.counters.items(): if update and counter in existing_resources: continue oa_unit_type = Hub._get_resclass_name(schema['unit']) payload = { 'resclass_name': oa_unit_type, 'name': '{}'.format(schema.get('title'))[:60], 'act_params': [ { 'var_name': 'app_id', 'var_value': str(app_id) }, { 'var_name': 'service_id', 'var_value': 'tenant' }, { 'var_name': 'resource_id', 'var_value': str(counter) }, ] } response = self.osaapi.addResourceType(**payload) osaapi_raise_for_status(response) if oa_unit_type == "rc.saas.resource.unith" or oa_unit_type == "rc.saas.resource.mhzh": rt_ids[response['result']['resource_type_id']] = -1 else: rt_ids[response['result']['resource_type_id']] = 0 return rt_ids def _create_parameter_rts(self, package, app_id): rt_ids = {} for parameter, schema in package.parameters.items(): payload = { 'resclass_name': Hub._get_resclass_name(schema['unit']), 'name': '{} {}'.format(package.connector_name, parameter), 'act_params': [ { 'var_name': 'app_id', 'var_value': app_id }, { 'var_name': 'service_id', 'var_value': 'tenant' }, { 'var_name': 'resource_id', 'var_value': parameter }, ] } response = self.osaapi.addResourceType(**payload) osaapi_raise_for_status(response) rt_ids[response['result']['resource_type_id']] = 0 return rt_ids def create_rts(self, package, app_id, instance_uuid, experimental, update_rts=False): rts = {} if not update_rts: rts.update(self._create_core_rts(package, app_id, instance_uuid)) if self._is_service_profile_supported(instance_uuid) and experimental: rts.update(self._create_counted_ref_rts(package, app_id, update_rts)) else: rts.update(self._create_counter_rts(package, app_id, update_rts)) rts.update(self._create_parameter_rts(package, app_id)) return rts def create_st(self, package, rts): payload = { 'name': package.connector_name, 'owner_id': 1, 'resources': [{'resource_type_id': rt_id} for rt_id in rts], } r = self.osaapi.addServiceTemplate(**payload) osaapi_raise_for_status(r) return r['result']['st_id'] def apply_st_limits(self, st_id, rts): payload = { 'st_id': st_id, 'limits': [{'resource_id': t, 'resource_limit64': str(l)} for t, l in rts.items()], } r = self.osaapi.setSTRTLimits(**payload) osaapi_raise_for_status(r) def check_experimental_support(self): hub_version = self._get_hub_version(self.osaapi) self._assert_supported_version(hub_version, experimental=True) def check_connect_hub_app_installed(self): url = 'aps/2/resources?implementing(http://odin.com/servicesSelector/globals/2.0)' r = self.aps.get(url) try: data = json_decode(r.content) except ValueError: print("APSController provided non-json format") sys.exit(1) else: if len(data) == 0: print("ERROR: Connect Hub extension is not installed") print("ERROR: Please follow the instructions available here: " + "https://connect.cloudblue.com/documentation/extensions/" + "cloudblue-commerce/reseller-control-panel/") sys.exit(1) match = re.match( r'http://odin.com/servicesSelector/globals/(?P<major>\d)\.(?P<minor>\d+)', data[0]['aps']['type']) if match: major = int(match.groupdict()['major']) minor = int(match.groupdict()['minor']) supported = (major >= 2 and minor > 1) or major > 2 if not supported: print("ERROR: Connect Hub extension is outdated") print("ERROR: Please upgrade it using instructions available here: " + "https://connect.cloudblue.com/documentation/extensions/" + "cloudblue-commerce/reseller-control-panel/") sys.exit(1) def get_connections(self, product_id): url = 'aps/2/resources/{}/connectionsInfo'.format(self.extension_id) payload = { "product": product_id } r = self.aps.post(url, json=payload) try: data = json_decode(r.content) if data.get('id'): return data print("ERROR: Product {} has no connection to this HUB.\n".format(product_id) + "Please access CloudBlue Connect provider panel to create a new one") exit(1) except ValueError: print("APSController provided non-json format") sys.exit(1) def get_application_id(self, package_id): payload = { 'aps_application_id': package_id, } r = self.osaapi.aps.getApplications(**payload) osaapi_raise_for_status(r) if len(r['result']) == 0: return None return r['result'][0]['application_id'] or None def get_application_instances(self, application_id): payload = { 'app_id': application_id, } r = self.osaapi.aps.getApplicationInstances(**payload) osaapi_raise_for_status(r) return r['result'] def upgrade_application_instance(self, instance): payload = { 'application_instance_id': int(instance), } r = self.osaapi.aps.upgradeApplicationInstance(**payload) osaapi_raise_for_status(r) return class APS(object): url = None token = None def __init__(self, token, url=None): if url: self.url = url else: config = get_config() self.url = APS._get_aps_url(**{k: config[k] for k in APS_CONNECT_PARAMS}) self.token = token @staticmethod def _get_aps_url(aps_host, aps_port, use_tls_aps): return '{}://{}:{}'.format('https' if use_tls_aps else 'http', aps_host, aps_port) def get(self, uri): return requests.get('{}/{}'.format(self.url, uri), headers=self.token, verify=False) def post(self, uri, json=None): return requests.post('{}/{}'.format(self.url, uri), headers=self.token, json=json, verify=False) def put(self, uri, json=None): return requests.put('{}/{}'.format(self.url, uri), headers=self.token, json=json, verify=False) def delete(self, uri): return requests.delete('{}/{}'.format(self.url, uri), headers=self.token, verify=False)