diff options
author | Sudhakar Reddy <reddysud@amdocs.com> | 2018-08-13 21:13:56 +0530 |
---|---|---|
committer | Sudhakar Reddy <Sudhakar.Reddy@amdocs.com> | 2018-08-20 14:19:54 +0530 |
commit | 6927c37a31ef5a874e923a4fe02feb1392a7dfb4 (patch) | |
tree | 28c1914b811a1bd3cac489ffc26890abd4ea15ae | |
parent | c790c7e939a0bcd9b424fc986f40a29485686315 (diff) |
Adoption of base framework code for azure plugin
This is the initial code which is created by referring to vmware
plugin.The logging and containerization features are readily available.
Change-Id: I3371d5f0671c2252edb1da7ea55f1f89ea27b3aa
Issue-ID: MULTICLOUD-308
Signed-off-by: Sudhakar Reddy <Sudhakar.Reddy@amdocs.com>
91 files changed, 4582 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64c410c --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +.project +.classpath +.vscode +.settings/ +.checkstyle +target/ +logs/*.log +*.pyc +*.swp +.idea/ + +# Test related files +azure/.coverage +azure/.tox/ +azure/logs/*.log +azure/test-reports/ + +# build files + +azure/build +azure/dist +azure/1.25.0 +azure/*.egg-info +azure/logs/*.log
\ No newline at end of file diff --git a/.gitreview b/.gitreview new file mode 100644 index 0000000..a5cf8f2 --- /dev/null +++ b/.gitreview @@ -0,0 +1,4 @@ +[gerrit] +host=gerrit.onap.org +port=29418 +project=multicloud/azure.git diff --git a/License.txt b/License.txt new file mode 100644 index 0000000..0dd49ad --- /dev/null +++ b/License.txt @@ -0,0 +1,37 @@ +/* +* ============LICENSE_START========================================== +* =================================================================== +* Copyright (c) 2018 Amdocs +* All rights reserved. +* =================================================================== +* +* Unless otherwise specified, all software contained herein is licensed +* under the Apache License, Version 2.0 (the “Licenseâ€); +* you may not use this software 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. +* +* +* +* Unless otherwise specified, all documentation contained herein is licensed +* under the Creative Commons License, Attribution 4.0 Intl. (the “Licenseâ€); +* you may not use this documentation except in compliance with the License. +* You may obtain a copy of the License at +* +* https://creativecommons.org/licenses/by/4.0/ +* +* Unless required by applicable law or agreed to in writing, documentation +* 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============================================ +*/
\ No newline at end of file diff --git a/azure/README.md b/azure/README.md new file mode 100644 index 0000000..cdca6b2 --- /dev/null +++ b/azure/README.md @@ -0,0 +1,12 @@ +## Multicloud Azure Plugin +This plugin is a part of multicloud component which contains the capability +to talk to Azure cloud based on the Service Principal credentials fetched +from AAI. + +The initial version of this plugin will provide the API to register a Cloud +region. The plugin will provide both versions(v0 and v1) of API for +cloud registration. + +#### Provided APIs: + +**/api/multicloud-azure/v0/(?P<vimid>[0-9a-z-A-Z\-\_]+)/registry** diff --git a/azure/assembly.xml b/azure/assembly.xml new file mode 100644 index 0000000..61a4883 --- /dev/null +++ b/azure/assembly.xml @@ -0,0 +1,66 @@ +<!-- + Copyright (c) 2018 Amdocs + + 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. + --> +<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd"> + <id>azure</id> + <formats> + <format>zip</format> + </formats> + <fileSets> + <fileSet> + <directory>azure</directory> + <outputDirectory>/azure</outputDirectory> + <includes> + <include>**/*.py</include> + <include>**/*.json</include> + <include>**/*.xml</include> + <include>**/*.wsdl</include> + <include>**/*.xsd</include> + <include>**/*.bpel</include> + <include>**/*.yml</include> + <include>**/*.conf</include> + </includes> + </fileSet> + <fileSet> + <directory>logs</directory> + <outputDirectory>/logs</outputDirectory> + <includes> + <include>*.txt</include> + </includes> + </fileSet> + <fileSet> + <directory>docker</directory> + <outputDirectory>/docker</outputDirectory> + <includes> + <include>*.sh</include> + <include>Dockerfile</include> + </includes> + </fileSet> + <fileSet> + <directory>.</directory> + <outputDirectory>/</outputDirectory> + <includes> + <include>*.py</include> + <include>*.txt</include> + <include>*.sh</include> + <include>*.ini</include> + <include>*.md</include> + <include>*.yml</include> + </includes> + </fileSet> + </fileSets> + <baseDirectory>azure</baseDirectory> + <!--baseDirectory>multivimdriver-openstack/azure</baseDirectory--> +</assembly> diff --git a/azure/azure/__init__.py b/azure/azure/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/azure/azure/__init__.py diff --git a/azure/azure/api_v2/__init__.py b/azure/azure/api_v2/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/azure/azure/api_v2/__init__.py diff --git a/azure/azure/api_v2/api_definition/__init__.py b/azure/azure/api_v2/api_definition/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/azure/azure/api_v2/api_definition/__init__.py diff --git a/azure/azure/api_v2/api_definition/hosts.yaml b/azure/azure/api_v2/api_definition/hosts.yaml new file mode 100644 index 0000000..88eaa09 --- /dev/null +++ b/azure/azure/api_v2/api_definition/hosts.yaml @@ -0,0 +1,72 @@ +--- + info: + version: "1.0.0" + title: "Multi Cloud Host" + description: "Definition of Host API" + termsOfService: "http://swagger.io/terms/" + schemes: + - "http" + produces: + - "application/json" + paths: + /{vimid}/{tenantid}/hosts/{hostid}: + parameters: + - type: string + name: vimid + - type: string + format: uuid + name: tenantid + - type: string + name: hostid + in: path + required: true + get: + produces: + - "application/json" + responses: + "200": + schema: + $ref: "#/definitions/host" + get_all: + produces: + - "application/json" + responses: + "200": + schema: + type: "array" + items: + $ref: "#/definitions/host" + vim_path: "/compute/os-hypervisors" + definitions: + host: + plural_vim_resource: "hypervisors" + vim_resource: "hypervisor" + plural: "hosts" + properties: + name: + type: string + required: true + source: hypervisor.hypervisor_hostname + id: + type: string + required: true + source: hypervisor.id + status: + type: string + source: hypervisor.status + state: + type: string + source: hypervisor.state + cpu: + type: integer + minimal: 1 + source: hypervisor.vcpus + action: copy + disk_gb: + type: integer + minimal: 0 + source: hypervisor.local_gb + memory_mb: + type: integer + minimal: 0 + source: hypervisor.memory_mb diff --git a/azure/azure/api_v2/api_definition/images.yaml b/azure/azure/api_v2/api_definition/images.yaml new file mode 100644 index 0000000..723884c --- /dev/null +++ b/azure/azure/api_v2/api_definition/images.yaml @@ -0,0 +1,76 @@ +--- + info: + version: "1.0.0" + title: "Multi Cloud Image" + description: "Definition of Image API" + termsOfService: "http://swagger.io/terms/" + schemes: + - "http" + produces: + - "application/json" + paths: + /{vimid}/{tenantid}/images/{imageid}: + parameters: + - type: string + name: vimid + - type: string + format: uuid + name: tenantid + - type: string + name: imageid + in: path + required: true + get: + produces: + - "application/json" + responses: + "200": + schema: + $ref: "#/definitions/image" + get_all: + produces: + - "application/json" + responses: + "200": + schema: + type: "array" + items: + $ref: "#/definitions/image" + post: + produces: + - "application/json" + responses: + "200": + schema: + $ref: "#/definitions/image" + delete: + responses: "204" + vim_path: "/image/v2/images" + definitions: + image: + plural_vim_resource: "images" + vim_resource: "image" + plural: "images" + properties: + name: + type: string + required: true + source: image.name + id: + type: string + source: image.id + status: + type: string + source: image.status + imageType: + type: string + source: image.disk_format + containerFormat: + type: string + source: image.container_format + visibility: + type: string + source: image.visibility + size: + type: integer + source: image.size
\ No newline at end of file diff --git a/azure/azure/api_v2/api_definition/networks.yaml b/azure/azure/api_v2/api_definition/networks.yaml new file mode 100644 index 0000000..c00808f --- /dev/null +++ b/azure/azure/api_v2/api_definition/networks.yaml @@ -0,0 +1,91 @@ +--- + info: + version: "1.0.0" + title: "Multi Cloud Network" + description: "Definition of Host API" + termsOfService: "http://swagger.io/terms/" + schemes: + - "http" + produces: + - "application/json" + paths: + /{vimid}/{tenantid}/networks/{networkid}: + parameters: + - type: string + name: vimid + - type: string + format: uuid + name: tenantid + - type: string + name: networkid + in: path + required: true + get: + produces: + - "application/json" + responses: + "200": + schema: + $ref: "#/definitions/network" + get_all: + produces: + - "application/json" + responses: + "200": + schema: + type: "array" + items: + $ref: "#/definitions/network" + post: + produces: + - "application/json" + responses: + "200": + schema: + $ref: "#/definitions/network" + delete: + responses: "204" + vim_path: "/network/v2.0/networks" + definitions: + network: + plural_vim_resource: "networks" + vim_resource: "network" + plural: "networks" + properties: + name: + type: string + required: true + source: network.name + id: + type: string + source: network.id + status: + type: string + source: network.status + segmentationId: + type: string + source: network.provider:segmentation_id + default: None + physicalNetwork: + type: string + source: network.provider:physical_network + default: None + networkType: + type: string + source: network.provider:network_type + default: None + tenantId: + type: string + source: network.tenant_id + shared: + type: boolean + source: network.shared + required: true + routerExternal: + type: boolean + source: network.router:external + required: true + vlanTransparent: + type: boolean + source: network.vlan_transparent + default: false diff --git a/azure/azure/api_v2/api_definition/ports.yaml b/azure/azure/api_v2/api_definition/ports.yaml new file mode 100644 index 0000000..e159593 --- /dev/null +++ b/azure/azure/api_v2/api_definition/ports.yaml @@ -0,0 +1,83 @@ +--- + info: + version: "1.0.0" + title: "Multi Cloud Port" + description: "Definition of Port API" + termsOfService: "http://swagger.io/terms/" + schemes: + - "http" + produces: + - "application/json" + paths: + /{vimid}/{tenantid}/ports/{portid}: + parameters: + - type: string + name: vimid + - type: string + format: uuid + name: tenantid + - type: string + name: portid + in: path + required: true + get: + produces: + - "application/json" + responses: + "200": + schema: + $ref: "#/definitions/port" + get_all: + produces: + - "application/json" + responses: + "200": + schema: + type: "array" + items: + $ref: "#/definitions/port" + post: + produces: + - "application/json" + responses: + "200": + schema: + $ref: "#/definitions/port" + delete: + responses: "204" + vim_path: "/network/v2.0/ports" + definitions: + port: + plural_vim_resource: "ports" + vim_resource: "port" + plural: "port" + properties: + name: + type: string + required: true + source: port.name + id: + type: string + source: port.id + status: + type: string + source: port.status + networkId: + type: string + source: port.network_id + required: true + vnicType: + source: port.binding:vnic_type + securityGroups: + type: string + source: port.security_groups + tenantId: + type: string + source: port.tenant_id + macAddress: + type: string + source: port.mac_address + subnetId: + source: port.fixed_ips[0].subnet_id + ip: + source: port.fixed_ips[0].ip_address diff --git a/azure/azure/api_v2/api_definition/subnets.yaml b/azure/azure/api_v2/api_definition/subnets.yaml new file mode 100644 index 0000000..e48d570 --- /dev/null +++ b/azure/azure/api_v2/api_definition/subnets.yaml @@ -0,0 +1,88 @@ +--- + info: + version: "1.0.0" + title: "Multi Cloud Subnet" + description: "Definition of Subnet API" + termsOfService: "http://swagger.io/terms/" + schemes: + - "http" + produces: + - "application/json" + paths: + /{vimid}/{tenantid}/subnets/{subnetid}: + parameters: + - type: string + name: vimid + - type: string + format: uuid + name: tenantid + - type: string + name: subnetid + in: path + required: true + get: + produces: + - "application/json" + responses: + "200": + schema: + $ref: "#/definitions/subnet" + get_all: + produces: + - "application/json" + responses: + "200": + schema: + type: "array" + items: + $ref: "#/definitions/subnet" + post: + produces: + - "application/json" + responses: + "200": + schema: + $ref: "#/definitions/subnet" + delete: + responses: "204" + vim_path: "/network/v2.0/subnets" + definitions: + subnet: + plural_vim_resource: "subnets" + vim_resource: "subnet" + plural: "subnets" + properties: + name: + type: string + required: true + source: subnet.name + id: + type: string + source: subnet.id + status: + type: string + source: subnet.status + networkId: + type: string + source: subnet.network_id + required: true + allocationPools: + source: subnet.allocation_pools + gatewayIp: + type: string + source: subnet.gateway_ip + default: None + tenantId: + type: string + source: subnet.tenant_id + enableDhcp: + type: boolean + source: subnet.enable_dhcp + ipVersion: + source: subnet.ip_version + dnsNameServers: + source: subnet.dns_nameservers + cidr: + source: subnet.cidr + hostRoutes: + source: subnet.host_routes diff --git a/azure/azure/api_v2/api_definition/utils.py b/azure/azure/api_v2/api_definition/utils.py new file mode 100644 index 0000000..3f5a8a1 --- /dev/null +++ b/azure/azure/api_v2/api_definition/utils.py @@ -0,0 +1,31 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +import pkg_resources +import yaml + + +def get_definition_list(): + """ Get API Definition from YAML files. """ + + api_def = [] + definition_dir = __name__[:__name__.rfind(".")] + for f in pkg_resources.resource_listdir(definition_dir, '.'): + if f.endswith(".yaml"): + with pkg_resources.resource_stream(definition_dir, f) as fd: + # TODO(xiaohhui): Should add exception handler to inform user + # of potential error. + api_def.append(yaml.safe_load(fd)) + + return api_def diff --git a/azure/azure/api_v2/api_router/__init__.py b/azure/azure/api_v2/api_router/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/azure/azure/api_v2/api_router/__init__.py diff --git a/azure/azure/api_v2/api_router/controller_builder.py b/azure/azure/api_v2/api_router/controller_builder.py new file mode 100644 index 0000000..ec66268 --- /dev/null +++ b/azure/azure/api_v2/api_router/controller_builder.py @@ -0,0 +1,224 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +import json +from keystoneauth1.identity import v2 as keystone_v2 +from keystoneauth1.identity import v3 as keystone_v3 +from keystoneauth1 import session +import pecan +from pecan import rest +import re + +from azure.api_v2.api_definition import utils +from azure.pub import exceptions +from azure.pub.msapi import extsys + + +OBJ_IN_ARRAY = "(\w+)\[(\d+)\]\.(\w+)" + + +def _get_vim_auth_session(vim_id, tenant_id): + """ Get the auth session to the given backend VIM """ + + try: + vim = extsys.get_vim_by_id(vim_id) + except exceptions.VimDriverAzureException as e: + return pecan.abort(500, str(e)) + + params = { + "auth_url": vim["url"], + "username": vim["userName"], + "password": vim["password"], + } + params["tenant_id"] = tenant_id + + if '/v2' in params["auth_url"]: + auth = keystone_v2.Password(**params) + else: + params["user_domain_name"] = vim["domain"] + params["project_domain_name"] = vim["domain"] + + if 'tenant_id' in params: + params["project_id"] = params.pop("tenant_id") + if 'tenant_name' in params: + params["project_name"] = params.pop("tenant_name") + if '/v3' not in params["auth_url"]: + params["auth_url"] = params["auth_url"] + "/v3", + auth = keystone_v3.Password(**params) + + return session.Session(auth=auth) + + +def _convert_default_value(default): + return {"None": None, "true": True, "false": False}[default] + + +def _property_exists(resource, attr, required=False): + if attr not in resource: + if required: + raise Exception("Required field %s is missed in VIM " + "resource %s", (attr, resource)) + else: + return False + + return True + + +def _convert_vim_res_to_mc_res(vim_resource, res_properties): + mc_resource = {} + for key in res_properties: + vim_res, attr = res_properties[key]["source"].split('.', 1) + # action = res_properties[key].get("action", "copy") + if re.match(OBJ_IN_ARRAY, attr): + attr, index, sub_attr = re.match(OBJ_IN_ARRAY, attr).groups() + if _property_exists(vim_resource[vim_res], attr): + mc_resource[key] = ( + vim_resource[vim_res][attr][int(index)][sub_attr]) + else: + if _property_exists(vim_resource[vim_res], attr, + res_properties[key].get("required")): + mc_resource[key] = vim_resource[vim_res][attr] + else: + if "default" in res_properties[key]: + mc_resource[key] = _convert_default_value( + res_properties[key]["default"]) + + return mc_resource + + +def _convert_mc_res_to_vim_res(mc_resource, res_properties): + vim_resource = {} + for key in res_properties: + vim_res, attr = res_properties[key]["source"].split('.', 1) + # action = res_properties[key].get("action", "copy") + if re.match(OBJ_IN_ARRAY, attr): + attr, index, sub_attr = re.match(OBJ_IN_ARRAY, attr).groups() + if _property_exists(mc_resource, key): + vim_resource[attr] = vim_resource.get(attr, []) + if vim_resource[attr]: + vim_resource[attr][0].update({sub_attr: mc_resource[key]}) + else: + vim_resource[attr].append({sub_attr: mc_resource[key]}) + else: + if _property_exists(mc_resource, key, + res_properties[key].get("required")): + vim_resource[attr] = mc_resource[key] + + return vim_resource + + +def _build_api_controller(api_meta): + # Assume that only one path + path, path_meta = api_meta['paths'].items()[0] + # url path is behind third slash. The first is vimid, the second is + # tenantid. + path = path.split("/")[3] + controller_name = path.upper() + "Controller" + delimiter = path_meta["vim_path"].find("/", 1) + service_type = path_meta["vim_path"][1:delimiter] + resource_url = path_meta["vim_path"][delimiter:] + + # Assume there is only one resource. + name, resource_meta = api_meta['definitions'].items()[0] + resource_properties = resource_meta['properties'] + + controller_meta = {} + if "get" in path_meta: + # Add the get method to controller. + @pecan.expose("json") + def _get(self, vim_id, tenant_id, resource_id): + """ General GET """ + session = _get_vim_auth_session(vim_id, tenant_id) + service = {'service_type': service_type, + 'interface': 'public'} + full_url = resource_url + "/%s" % resource_id + resp = session.get(full_url, endpoint_filter=service) + mc_res = _convert_vim_res_to_mc_res(resp.json(), + resource_properties) + mc_res.update({"vimName": vim_id, + "vimId": vim_id, + "tenantId": tenant_id, + "returnCode": 0}) + return mc_res + + controller_meta["get"] = _get + + if "get_all" in path_meta: + # Add the get_all method to controller. + @pecan.expose("json") + def _get_all(self, vim_id, tenant_id): + """ General GET all """ + session = _get_vim_auth_session(vim_id, tenant_id) + service = {'service_type': service_type, + 'interface': 'public'} + resp = session.get(resource_url, endpoint_filter=service) + vim_res = resp.json()[resource_meta['plural_vim_resource']] + mc_res = [_convert_vim_res_to_mc_res( + {resource_meta['vim_resource']: v}, + resource_properties) + for v in vim_res] + return {"vimName": vim_id, + resource_meta['plural']: mc_res, + "tenantId": tenant_id, + "vimid": vim_id} + + controller_meta["get_all"] = _get_all + + if "post" in path_meta: + # Add the post method to controller. + @pecan.expose("json") + def _post(self, vim_id, tenant_id): + """ General POST """ + session = _get_vim_auth_session(vim_id, tenant_id) + service = {'service_type': service_type, + 'interface': 'public'} + vim_res = _convert_mc_res_to_vim_res(pecan.request.json_body, + resource_properties) + + req_body = json.JSONEncoder().encode( + {resource_meta['vim_resource']: vim_res}) + resp = session.post(resource_url, + data=req_body, + endpoint_filter=service) + mc_res = _convert_vim_res_to_mc_res(resp.json(), + resource_properties) + mc_res.update({"vimName": vim_id, + "vimId": vim_id, + "tenantId": tenant_id, + "returnCode": 0}) + return mc_res + + controller_meta["post"] = _post + + if "delete" in path_meta: + # Add the delete method to controller. + @pecan.expose("json") + def _delete(self, vim_id, tenant_id, resource_id): + """ General DELETE """ + session = _get_vim_auth_session(vim_id, tenant_id) + service = {'service_type': service_type, + 'interface': 'public'} + full_url = resource_url + "/%s" % resource_id + session.delete(full_url, endpoint_filter=service) + + controller_meta["delete"] = _delete + + return path, type(controller_name, (rest.RestController,), controller_meta) + + +def insert_dynamic_controller(root_controller): + api_defs = utils.get_definition_list() + for d in api_defs: + path, con_class = _build_api_controller(d) + setattr(root_controller, path, con_class()) diff --git a/azure/azure/api_v2/api_router/root.py b/azure/azure/api_v2/api_router/root.py new file mode 100644 index 0000000..0b6f995 --- /dev/null +++ b/azure/azure/api_v2/api_router/root.py @@ -0,0 +1,34 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +import pecan +from pecan import rest + +from azure.api_v2.api_router import v0_controller + + +class AzureController(rest.RestController): + v0 = v0_controller.V0_Controller() + + +class APIController(rest.RestController): + pass + + +# Pecan workaround for the dash in path. +pecan.route(APIController, "multicloud-azure", AzureController()) + + +class RootController(object): + api = APIController() diff --git a/azure/azure/api_v2/api_router/swagger_json.py b/azure/azure/api_v2/api_router/swagger_json.py new file mode 100644 index 0000000..8573a46 --- /dev/null +++ b/azure/azure/api_v2/api_router/swagger_json.py @@ -0,0 +1,25 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +import pecan +from pecan import rest + +from azure.swagger import utils + + +class SwaggerJson(rest.RestController): + + @pecan.expose("json") + def get(self): + return utils.get_swagger_json_data() diff --git a/azure/azure/api_v2/api_router/v0_controller.py b/azure/azure/api_v2/api_router/v0_controller.py new file mode 100644 index 0000000..104597a --- /dev/null +++ b/azure/azure/api_v2/api_router/v0_controller.py @@ -0,0 +1,49 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +import pecan +from pecan import rest + +from azure.api_v2.api_router import controller_builder +from azure.api_v2.api_router import swagger_json + + +class V0_Controller(rest.RestController): + + def get(self, vim_id, tenant_id): + """ Placeholder for sub controllers. """ + pecan.abort(405) + + def put(self, vim_id, tenant_id): + """ Placeholder for sub controllers. """ + pecan.abort(405) + + def post(self, vim_id, tenant_id): + """ Placeholder for sub controllers. """ + pecan.abort(405) + + def delete(self, vim_id, tenant_id): + """ Placeholder for sub controllers. """ + pecan.abort(405) + + def get_all(self, vim_id, tenant_id): + """ Placeholder for sub controllers. """ + pecan.abort(405) + + +pecan.route(V0_Controller, "swagger.json", swagger_json.SwaggerJson()) + + +# Insert API stem from yaml files. +controller_builder.insert_dynamic_controller(V0_Controller) diff --git a/azure/azure/api_v2/app.py b/azure/azure/api_v2/app.py new file mode 100644 index 0000000..2c13403 --- /dev/null +++ b/azure/azure/api_v2/app.py @@ -0,0 +1,35 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +import pecan + + +def setup_app(config=None): + app_conf = { + 'root': "azure.api_v2.api_router.root.RootController", + 'modules': ["azure.api_v2"], + 'debug': True, + # NOTE: By default, guess_content_type_from_ext is True, and Pecan will + # strip the file extension from url. For example, ../../swagger.json + # will look like ../../swagger to Pecan API router. This makes other + # url like ../../swagger.txt get the same API route. Set this to False + # to do strict url mapping. + 'guess_content_type_from_ext': False + } + app = pecan.make_app( + app_conf.pop('root'), + **app_conf + ) + + return app diff --git a/azure/azure/api_v2/service.py b/azure/azure/api_v2/service.py new file mode 100644 index 0000000..2b73c15 --- /dev/null +++ b/azure/azure/api_v2/service.py @@ -0,0 +1,54 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +from oslo_concurrency import processutils +from oslo_config import cfg +from oslo_service import service +from oslo_service import wsgi + +from azure.api_v2 import app +from azure.pub.config import config as mc_cfg + + +CONF = cfg.CONF + + +class WSGIService(service.ServiceBase): + """Provides ability to launch API from wsgi app.""" + + def __init__(self): + self.app = app.setup_app() + + self.workers = processutils.get_worker_count() + + self.server = wsgi.Server( + CONF, + "azure", + self.app, + host="0.0.0.0", + port=mc_cfg.API_SERVER_PORT, + use_ssl=False + ) + + def start(self): + self.server.start() + + def stop(self): + self.server.stop() + + def wait(self): + self.server.wait() + + def reset(self): + self.server.reset() diff --git a/azure/azure/event_listener/__init__.py b/azure/azure/event_listener/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/azure/azure/event_listener/__init__.py diff --git a/azure/azure/event_listener/i18n.py b/azure/azure/event_listener/i18n.py new file mode 100644 index 0000000..8fd9a6e --- /dev/null +++ b/azure/azure/event_listener/i18n.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (c) 2018 Amdocs +# +# 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. + +import oslo_i18n + +DOMAIN = "test_oslo" + +_translators = oslo_i18n.TranslatorFactory(domain=DOMAIN) + +# The primary translation function using the well-known name "_" +_ = _translators.primary + +# The contextual translation function using the name "_C" +_C = _translators.contextual_form + +# The plural translation function using the name "_P" +_P = _translators.plural_form + +# Translators for log levels. +# +# The abbreviated names are meant to reflect the usual use of a short +# name like '_'. The "L" is for "log" and the other letter comes from +# the level. +_LI = _translators.log_info +_LW = _translators.log_warning +_LE = _translators.log_error +_LC = _translators.log_critical diff --git a/azure/azure/event_listener/listener.conf b/azure/azure/event_listener/listener.conf new file mode 100644 index 0000000..4376fe7 --- /dev/null +++ b/azure/azure/event_listener/listener.conf @@ -0,0 +1,15 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +[Listener] +rabbit_ip=10.154.9.172 +rabbit_passwd=6C2B96AsbinmFf1a9c6a
\ No newline at end of file diff --git a/azure/azure/event_listener/server.py b/azure/azure/event_listener/server.py new file mode 100644 index 0000000..7f1f830 --- /dev/null +++ b/azure/azure/event_listener/server.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (c) 2018 Amdocs +# +# 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. + +from oslo_config import cfg +from oslo_log import log as logging +from i18n import _LI +import oslo_messaging +import ConfigParser +import json +import os +import requests +from azure.pub.config.config import MR_ADDR +from azure.pub.config.config import MR_PORT + + +LOG = logging.getLogger(__name__) + + +def prepare(): + + product_name = "oslo_server" + logging.register_options(cfg.CONF) + logging.setup(cfg.CONF, product_name) + + +''' +below items must be added into vio nova.conf then restart nova services: +notification_driver=messaging +notification_topics= notifications_test +notify_on_state_change=vm_and_task_state +notify_on_any_change=True +instance_usage_audit=True +instance_usage_audit_period=hour +''' + + +def getConfig(section, key): + + config = ConfigParser.ConfigParser() + path = os.path.split(os.path.realpath(__file__))[0] + '/listener.conf' + config.read(path) + return config.get(section, key) + + +class NotificationEndPoint(): + + filter_rule = oslo_messaging.NotificationFilter( + publisher_id='^compute.*') + + def info(self, ctxt, publisher_id, event_type, payload, metadata): + + VM_EVENTS = { + 'compute.instance.unpause.start', + 'compute.instance.pause.start', + 'compute.instance.power_off.start', + 'compute.instance.reboot.start', + 'compute.instance.create.start' + } + + status = payload.get('state_description') + if status != '' and event_type in VM_EVENTS: + url = 'http://%s:%s/events/test' % (MR_ADDR, MR_PORT) + headers = {'Content-type': 'application/json'} + requests.post(url, json.dumps(payload), headers=headers) + + LOG.info(event_type) + self.action(payload) + + def action(self, data): + LOG.info(_LI(json.dumps(data))) + + +class Server(object): + + def __init__(self): + self.topic = 'notifications_test' + self.server = None + prepare() + + +class NotificationServer(Server): + + def __init__(self): + super(NotificationServer, self).__init__() + # rabbit IP and password come from listener.conf + url = 'rabbit://test:%s@%s:5672/' % ( + getConfig('Listener', 'rabbit_passwd'), + getConfig('Listener', 'rabbit_ip') + ) + self.transport = oslo_messaging.get_notification_transport( + cfg.CONF, + url=url) + # The exchange must be the same as + # control_exchange in transport setting in client. + self.targets = [oslo_messaging.Target( + topic=self.topic, + exchange='nova')] + self.endpoints = [NotificationEndPoint()] + + def start(self): + LOG.info(_LI("Start Notification server...")) + self.server = oslo_messaging.get_notification_listener( + self.transport, + self.targets, + self.endpoints, + executor='threading') + self.server.start() + self.server.wait() + + +if __name__ == '__main__': + + notification_server = NotificationServer() + notification_server.start() diff --git a/azure/azure/middleware.py b/azure/azure/middleware.py new file mode 100644 index 0000000..1edc44b --- /dev/null +++ b/azure/azure/middleware.py @@ -0,0 +1,59 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + + +import uuid +from onaplogging.mdcContext import MDC +from azure.pub.config.config import SERVICE_NAME +from azure.pub.config.config import FORWARDED_FOR_FIELDS + + +class LogContextMiddleware(object): + + # the last IP behind multiple proxies, if no exist proxies + # get local host ip. + def _getLastIp(self, request): + + ip = "" + try: + for field in FORWARDED_FOR_FIELDS: + if field in request.META: + if ',' in request.META[field]: + parts = request.META[field].split(',') + ip = parts[-1].strip().split(":")[0] + else: + ip = request.META[field].split(":")[0] + + if ip == "": + ip = request.META.get("HTTP_HOST").split(":")[0] + + except Exception: + pass + + return ip + + def process_request(self, request): + + ReqeustID = request.META.get("HTTP_X_TRANSACTIONID", None) + if ReqeustID is None: + ReqeustID = str(uuid.uuid3(uuid.NAMESPACE_URL, SERVICE_NAME)) + MDC.put("requestID", ReqeustID) + InovocationID = str(uuid.uuid4()) + MDC.put("invocationID", InovocationID) + MDC.put("serviceName", SERVICE_NAME) + MDC.put("serviceIP", self._getLastIp(request)) + return None + + def process_response(self, request, response): + + MDC.clear() + return response diff --git a/azure/azure/pub/__init__.py b/azure/azure/pub/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/azure/azure/pub/__init__.py diff --git a/azure/azure/pub/config/__init__.py b/azure/azure/pub/config/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/azure/azure/pub/config/__init__.py diff --git a/azure/azure/pub/config/config.py b/azure/azure/pub/config/config.py new file mode 100644 index 0000000..db09fd6 --- /dev/null +++ b/azure/azure/pub/config/config.py @@ -0,0 +1,41 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +import os + +# [MSB] +MSB_SERVICE_IP = "msb.onap.org" +MSB_SERVICE_PORT = "10080" + +# [IMAGE LOCAL PATH] +ROOT_PATH = os.path.dirname(os.path.dirname( + os.path.dirname(os.path.abspath(__file__)))) + +# [A&AI] +AAI_ADDR = "aai.api.simpledemo.openecomp.org" +AAI_PORT = "8443" +AAI_SERVICE_URL = 'https://%s:%s/aai' % (AAI_ADDR, AAI_PORT) +AAI_SCHEMA_VERSION = "v13" +AAI_USERNAME = "AAI" +AAI_PASSWORD = "AAI" + +# [DMaaP] +MR_ADDR = "" +MR_PORT = "" + +# [MDC] +SERVICE_NAME = "multicloud-azure" +FORWARDED_FOR_FIELDS = ["HTTP_X_FORWARDED_FOR", "HTTP_X_FORWARDED_HOST", + "HTTP_X_FORWARDED_SERVER"] + +# [Local Config] +API_SERVER_PORT = 9004 diff --git a/azure/azure/pub/config/log.yml b/azure/azure/pub/config/log.yml new file mode 100644 index 0000000..7bbc427 --- /dev/null +++ b/azure/azure/pub/config/log.yml @@ -0,0 +1,26 @@ +version: 1 +disable_existing_loggers: False + +loggers: + azure: + handlers: [azure_handler] + level: "DEBUG" + propagate: False +handlers: + azure_handler: + level: "DEBUG" + class: "logging.handlers.RotatingFileHandler" + filename: "/var/log/onap/multicloud/azure/azure.log" + formatter: "mdcFormat" + maxBytes: 52428800 + backupCount: 10 +formatters: + standard: + format: "%(asctime)s|||||%(name)s||%(thread)||%(funcName)s||%(levelname)s||%(message)s" + mdcFormat: + format: "%(asctime)s|||||%(name)s||%(thread)s||%(funcName)s||%(levelname)s||%(message)s||||%(mdc)s \t" + mdcfmt: "{requestID} {invocationID} {serviceName} {serviceIP}" + datefmt: "%Y-%m-%d %H:%M:%S" + (): onaplogging.mdcformatter.MDCFormatter + + diff --git a/azure/azure/pub/database/__init__.py b/azure/azure/pub/database/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/azure/azure/pub/database/__init__.py diff --git a/azure/azure/pub/database/models.py b/azure/azure/pub/database/models.py new file mode 100644 index 0000000..757430b --- /dev/null +++ b/azure/azure/pub/database/models.py @@ -0,0 +1,23 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +from django.db import models + + +class VimInstModel(models.Model): + class Meta: + db_table = 'vim_inst_type_mapping' + + vimid = models.CharField( + db_column='VIMID', primary_key=True, max_length=200) + vimtype = models.CharField(db_column="VIMTYPE", max_length=200) + viminst_url = models.CharField(db_column="VIMINSTURL", max_length=200) diff --git a/azure/azure/pub/exceptions.py b/azure/azure/pub/exceptions.py new file mode 100644 index 0000000..3f38f2c --- /dev/null +++ b/azure/azure/pub/exceptions.py @@ -0,0 +1,66 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + + +class ClientException(Exception): + + message = "ClientException" + + def __init__(self, message=None): + self.message = message or self.message + super(ClientException, self).__init__(self.message) + + +class ServerException(Exception): + + message = "ServerException" + + def __init__(self, message=None, status_code="", content=""): + super(ServerException, self).__init__(message) + self.message = message or self.message + self.status_code = status_code + self.content = content + + +class RetriableConnectionFailure(Exception): + pass + + +class ConnectionError(ClientException): + message = "Cannot connect to API service." + + +class ConnectTimeout(ConnectionError, RetriableConnectionFailure): + message = "Timed out connecting to service." + + +class ConnectFailure(ConnectionError, RetriableConnectionFailure): + message = "Connection failure that may be retried." + + +class SSLError(ConnectionError): + message = "An SSL error occurred." + + +class UnknownConnectionError(ConnectionError): + + def __init__(self, msg, original): + super(UnknownConnectionError, self).__init__(msg) + self.original = original + + +class NotFoundError(ServerException): + message = "Cannot find value" + + +class VimDriverAzureException(ServerException): + message = "Cannot find vim driver" diff --git a/azure/azure/pub/msapi/__init__.py b/azure/azure/pub/msapi/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/azure/azure/pub/msapi/__init__.py diff --git a/azure/azure/pub/msapi/extsys.py b/azure/azure/pub/msapi/extsys.py new file mode 100644 index 0000000..f0b9dcc --- /dev/null +++ b/azure/azure/pub/msapi/extsys.py @@ -0,0 +1,46 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +import logging + +from azure.pub.utils.restcall import AAIClient + +logger = logging.getLogger(__name__) + + +def split_vim_to_owner_region(vim_id): + split_vim = vim_id.split('_') + cloud_owner = split_vim[0] + cloud_region = "".join(split_vim[1:]) + return cloud_owner, cloud_region + + +def get_vim_by_id(vim_id): + cloud_owner, cloud_region = split_vim_to_owner_region(vim_id) + client = AAIClient(cloud_owner, cloud_region) + ret = client.get_vim(get_all=True) + esrInfo = ret['esr-system-info-list']['esr-system-info'][0] + data = { + 'type': ret['cloud-type'], + 'version': ret['cloud-region-version'], + 'vimId': vim_id, + 'name': vim_id, + 'userName': esrInfo['user-name'], + 'password': esrInfo['password'], + 'tenant': esrInfo['default-tenant'], + 'url': esrInfo['service-url'], + 'domain': esrInfo['cloud-domain'], + 'cacert': esrInfo.get('ssl-cacert', ""), + 'insecure': esrInfo.get('ssl-insecure', False) + } + ret.update(data) + return ret diff --git a/azure/azure/pub/utils/__init__.py b/azure/azure/pub/utils/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/azure/azure/pub/utils/__init__.py diff --git a/azure/azure/pub/utils/enumutil.py b/azure/azure/pub/utils/enumutil.py new file mode 100644 index 0000000..eb7a22c --- /dev/null +++ b/azure/azure/pub/utils/enumutil.py @@ -0,0 +1,15 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + + +def enum(**enums): + return type('Enum', (), enums) diff --git a/azure/azure/pub/utils/fileutil.py b/azure/azure/pub/utils/fileutil.py new file mode 100644 index 0000000..1868300 --- /dev/null +++ b/azure/azure/pub/utils/fileutil.py @@ -0,0 +1,50 @@ +# Copyright (c) 2018 Amdocs +# +# 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. +import os +import shutil +import logging +import traceback +import urllib2 + +logger = logging.getLogger(__name__) + + +def make_dirs(path): + if not os.path.exists(path): + os.makedirs(path, 0777) + + +def delete_dirs(path): + try: + if os.path.exists(path): + shutil.rmtree(path) + except Exception as e: + logger.error(traceback.format_exc()) + logger.error("Failed to delete %s:%s", path, e.message) + + +def download_file_from_http(url, local_dir, file_name): + local_file_name = os.path.join(local_dir, file_name) + is_download_ok = False + try: + make_dirs(local_dir) + r = urllib2.Request(url) + req = urllib2.urlopen(r) + save_file = open(local_file_name, 'wb') + save_file.write(req.read()) + save_file.close() + req.close() + is_download_ok = True + except Exception: + logger.error(traceback.format_exc()) + logger.error("Failed to download %s to %s.", url, local_file_name) + return is_download_ok, local_file_name diff --git a/azure/azure/pub/utils/idutil.py b/azure/azure/pub/utils/idutil.py new file mode 100644 index 0000000..be6e8a0 --- /dev/null +++ b/azure/azure/pub/utils/idutil.py @@ -0,0 +1,18 @@ +# Copyright (c) 2018 Amdocs +# +# 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. +from redisco import containers as cont + + +def get_auto_id(id_type, id_group="auto_id_hash"): + auto_id_hash = cont.Hash(id_group) + auto_id_hash.hincrby(id_type, 1) + return auto_id_hash.hget(id_type) diff --git a/azure/azure/pub/utils/restcall.py b/azure/azure/pub/utils/restcall.py new file mode 100644 index 0000000..4b28098 --- /dev/null +++ b/azure/azure/pub/utils/restcall.py @@ -0,0 +1,782 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +import sys +import traceback +import logging +import urllib2 +import uuid +import httplib2 +import json + +from azure.pub.config.config import AAI_SCHEMA_VERSION +from azure.pub.config.config import AAI_SERVICE_URL +from azure.pub.config.config import AAI_USERNAME +from azure.pub.config.config import AAI_PASSWORD +from azure.pub.config.config import MSB_SERVICE_IP, MSB_SERVICE_PORT + +from azure.pub.exceptions import VimDriverAzureException + +rest_no_auth, rest_oneway_auth, rest_bothway_auth = 0, 1, 2 +HTTP_200_OK, HTTP_201_CREATED = '200', '201' +HTTP_204_NO_CONTENT, HTTP_202_ACCEPTED = '204', '202' +status_ok_list = [HTTP_200_OK, HTTP_201_CREATED, + HTTP_204_NO_CONTENT, HTTP_202_ACCEPTED] +HTTP_404_NOTFOUND, HTTP_403_FORBIDDEN = '404', '403' +HTTP_401_UNAUTHORIZED, HTTP_400_BADREQUEST = '401', '400' + +logger = logging.getLogger(__name__) + + +def call_req(base_url, user, passwd, auth_type, resource, method, content='', + headers=None): + callid = str(uuid.uuid1()) +# logger.debug("[%s]call_req('%s','%s','%s',%s,'%s','%s','%s')" % ( +# callid, base_url, user, passwd, auth_type, resource, method, content)) + ret = None + resp_status = '' + resp = "" + full_url = "" + + try: + full_url = combine_url(base_url, resource) + if headers is None: + headers = {} + headers['content-type'] = 'application/json' + + if user: + headers['Authorization'] = 'Basic ' + \ + ('%s:%s' % (user, passwd)).encode("base64") + ca_certs = None + for retry_times in range(3): + http = httplib2.Http( + ca_certs=ca_certs, + disable_ssl_certificate_validation=( + auth_type == rest_no_auth)) + http.follow_all_redirects = True + try: + logger.debug("request=%s" % full_url) + resp, resp_content = http.request( + full_url, method=method.upper(), body=content, + headers=headers) + resp_status = resp['status'] + resp_body = resp_content.decode('UTF-8') + + if resp_status in status_ok_list: + ret = [0, resp_body, resp_status, resp] + else: + ret = [1, resp_body, resp_status, resp] + break + except Exception as ex: + if 'httplib.ResponseNotReady' in str(sys.exc_info()): + logger.error(traceback.format_exc()) + ret = [1, "Unable to connect to %s" % full_url, + resp_status, resp] + continue + raise ex + except urllib2.URLError as err: + ret = [2, str(err), resp_status, resp] + except Exception as ex: + logger.error(traceback.format_exc()) + logger.error("[%s]ret=%s" % (callid, str(sys.exc_info()))) + res_info = str(sys.exc_info()) + if 'httplib.ResponseNotReady' in res_info: + res_info = ("The URL[%s] request failed or is not responding." % + full_url) + ret = [3, res_info, resp_status, resp] +# logger.debug("[%s]ret=%s" % (callid, str(ret))) + return ret + + +def req_by_msb(resource, method, content=''): + base_url = "http://%s:%s/" % (MSB_SERVICE_IP, MSB_SERVICE_PORT) + return call_req(base_url, "", "", rest_no_auth, resource, method, content) + + +def combine_url(base_url, resource): + full_url = None + if base_url.endswith('/') and resource.startswith('/'): + full_url = base_url[:-1] + resource + elif base_url.endswith('/') and not resource.startswith('/'): + full_url = base_url + resource + elif not base_url.endswith('/') and resource.startswith('/'): + full_url = base_url + resource + else: + full_url = base_url + '/' + resource + return full_url + + +def get_res_from_aai(resource, content=''): + headers = { + 'X-FromAppId': 'MultiCloud', + 'X-TransactionId': '9001', + 'content-type': 'application/json', + 'accept': 'application/json' + } + base_url = "%s/%s" % (AAI_SERVICE_URL, AAI_SCHEMA_VERSION) + return call_req(base_url, AAI_USERNAME, AAI_PASSWORD, rest_no_auth, + resource, "GET", content, headers) + + +class AAIClient(object): + def __init__(self, cloud_owner, cloud_region): + self.base_url = "%s/%s" % (AAI_SERVICE_URL, AAI_SCHEMA_VERSION) + self.username = AAI_USERNAME + self.password = AAI_PASSWORD + self.default_headers = { + 'X-FromAppId': 'multicloud-openstack-vmware', + 'X-TransactionId': '9004', + 'content-type': 'application/json', + 'accept': 'application/json' + } + self.cloud_owner = cloud_owner + self.cloud_region = cloud_region + self._vim_info = None + + def get_vim(self, get_all=False): + resource = ("/cloud-infrastructure/cloud-regions/cloud-region" + "/%s/%s" % (self.cloud_owner, self.cloud_region)) + if get_all: + resource = "%s?depth=all" % resource + resp = call_req(self.base_url, self.username, self.password, + rest_no_auth, resource, "GET", + headers=self.default_headers) + if resp[0] != 0: + raise VimDriverAzureException( + status_code=404, + content="Failed to query VIM with id (%s_%s) from extsys." % ( + self.cloud_owner, self.cloud_region)) + return json.loads(resp[1]) + + def delete_vim(self): + resp = self.get_vim(get_all=True) + logger.debug('Delete tenants') + self._del_tenants(resp) + logger.debug('Delete images') + self._del_images(resp) + logger.debug('Delete flavors') + self._del_flavors(resp) + logger.debug('Delete networks') + self._del_networks(resp) + logger.debug('Delete availability zones') + self._del_azs(resp) + logger.debug('Delete cloud region') + resource = ("/cloud-infrastructure/cloud-regions/cloud-region" + "/%s/%s?resource-version=%s" % + (self.cloud_owner, self.cloud_region, + resp['resource-version'])) + resp = call_req(self.base_url, self.username, self.password, + rest_no_auth, resource, "DELETE", + headers=self.default_headers) + if resp[0] != 0: + raise VimDriverAzureException( + status_code=400, + content="Failed to delete cloud %s_%s: %s." % ( + self.cloud_owner, self.cloud_region, resp[1])) + + def update_vim(self, content): + # update identity url + self.update_identity_url() + # update tenants + self.add_tenants(content) + # update flavors + self.add_images(content) + # update images + self.add_flavors(content) + # update networks + self.add_networks(content) + # update pservers + self.add_pservers(content) + + def update_identity_url(self): + vim = self.get_vim() + vim['identity-url'] = ("http://%s/api/multicloud/v0/%s_%s/identity/" + "v3" % (MSB_SERVICE_IP, self.cloud_owner, + self.cloud_region)) + resource = ("/cloud-infrastructure/cloud-regions/cloud-region" + "/%s/%s" % (self.cloud_owner, self.cloud_region)) + logger.debug("Updating identity url %s" % vim) + call_req(self.base_url, self.username, self.password, + rest_no_auth, resource, "PUT", + content=json.dumps(vim), + headers=self.default_headers) + + def add_tenants(self, content): + for tenant in content['tenants']: + resource = ("/cloud-infrastructure/cloud-regions/cloud-region/" + "%s/%s/tenants/tenant/%s" % ( + self.cloud_owner, self.cloud_region, tenant['id'])) + body = {'tenant-name': tenant['name']} + logger.debug("Adding tenants to cloud region") + call_req(self.base_url, self.username, self.password, + rest_no_auth, resource, "PUT", + content=json.dumps(body), + headers=self.default_headers) + + def add_flavors(self, content): + for flavor in content['flavors']: + resource = ("/cloud-infrastructure/cloud-regions/cloud-region/" + "%s/%s/flavors/flavor/%s" % ( + self.cloud_owner, self.cloud_region, flavor['id'])) + body = { + 'flavor-name': flavor['name'], + 'flavor-vcpus': flavor['vcpus'], + 'flavor-ram': flavor['ram'], + 'flavor-disk': flavor['disk'], + 'flavor-ephemeral': flavor['ephemeral'], + 'flavor-swap': flavor['swap'], + 'flavor-is-public': flavor['is_public'], + 'flavor-selflink': flavor['links'][0]['href'], + 'flavor-disabled': flavor['is_disabled'] + } + # Handle extra specs + if flavor['name'].startswith("onap."): + hpa_capabilities = self._get_hpa_capabilities( + flavor) + body['hpa-capabilities'] = { + 'hpa-capability': hpa_capabilities} + + logger.debug("Adding flavors to cloud region") + call_req(self.base_url, self.username, self.password, + rest_no_auth, resource, "PUT", + content=json.dumps(body), + headers=self.default_headers) + + def add_images(self, content): + for image in content['images']: + resource = ("/cloud-infrastructure/cloud-regions/cloud-region/" + "%s/%s/images/image/%s" % ( + self.cloud_owner, self.cloud_region, image['id'])) + split_image_name = image['name'].split("-") + os_distro = split_image_name[0] + os_version = split_image_name[1] if \ + len(split_image_name) > 1 else "" + body = { + 'image-name': image['name'], + # 'image-architecture': image[''], + 'image-os-distro': os_distro, + 'image-os-version': os_version, + # 'application': image[''], + # 'application-vendor': image[''], + # 'application-version': image[''], + # TODO replace this with image proxy endpoint + 'image-selflink': "", + } + logger.debug("Adding images to cloud region") + call_req(self.base_url, self.username, self.password, + rest_no_auth, resource, "PUT", + content=json.dumps(body), + headers=self.default_headers) + + def add_networks(self, content): + for network in content['networks']: + resource = ("/cloud-infrastructure/cloud-regions/cloud-region/" + "%s/%s/oam-networks/oam-network/%s" % ( + self.cloud_owner, self.cloud_region, + network['id'])) + body = { + 'network-uuid': network['id'], + 'network-name': network['name'], + 'cvlan-tag': network['segmentationId'] or 0, + } + logger.debug("Adding networks to cloud region") + call_req(self.base_url, self.username, self.password, + rest_no_auth, resource, "PUT", + content=json.dumps(body), + headers=self.default_headers) + + def add_pservers(self, content): + for hypervisor in content['hypervisors']: + resource = ("/cloud-infrastructure/pservers/pserver/%s" % ( + hypervisor['name'])) + body = { + # 'ptnii-equip-name' + 'number-of-cpus': hypervisor['vcpus'], + 'disk-in-gigabytes': hypervisor['local_disk_size'], + 'ram-in-megabytes': hypervisor['memory_size'], + # 'equip-type' + # 'equip-vendor' + # 'equip-model' + # 'fqdn' + # 'pserver-selflink' + 'ipv4-oam-address': hypervisor['host_ip'], + # 'serial-number' + # 'ipaddress-v4-loopback-0' + # 'ipaddress-v6-loopback-0' + # 'ipaddress-v4-aim' + # 'ipaddress-v6-aim' + # 'ipaddress-v6-oam' + # 'inv-status' + 'pserver-id': hypervisor['id'], + # 'internet-topology' + } + logger.debug("Adding pservers") + call_req(self.base_url, self.username, self.password, + rest_no_auth, resource, "PUT", + content=json.dumps(body), + headers=self.default_headers) + # update relationship + resource = ("/cloud-infrastructure/pservers/pserver/%s/" + "relationship-list/relationship" % + hypervisor['name']) + related_link = ("%s/cloud-infrastructure/cloud-regions/" + "cloud-region/%s/%s" % ( + self.base_url, self.cloud_owner, + self.cloud_region)) + body = { + 'related-to': 'cloud-region', + 'related-link': related_link, + 'relationship-data': [ + { + 'relationship-key': 'cloud-region.cloud-owner', + 'relationship-value': self.cloud_owner + }, + { + 'relationship-key': 'cloud-region.cloud-region-id', + 'relationship-value': self.cloud_region + } + ] + } + logger.debug("Connecting pservers and cloud region") + call_req(self.base_url, self.username, self.password, + rest_no_auth, resource, "PUT", + content=json.dumps(body), + headers=self.default_headers) + + def _del_tenants(self, rsp): + tenants = rsp.get("tenants", []) + if not tenants: + return + for tenant in tenants["tenant"]: + resource = ("/cloud-infrastructure/cloud-regions/cloud-region/" + "%s/%s/tenants/tenant/%s?resource-version=%s" % ( + self.cloud_owner, self.cloud_region, + tenant['tenant-id'], tenant['resource-version'])) + resp = call_req(self.base_url, self.username, self.password, + rest_no_auth, resource, "DELETE", + headers=self.default_headers) + if resp[0] != 0: + raise VimDriverAzureException( + status_code=400, + content="Failed to delete tenant %s: %s." % ( + tenant['tenant-id'], resp[1])) + + def _del_hpa(self, flavor): + hpas = flavor.get("hpa-capabilities", {}).get("hpa-capability", []) + for hpa in hpas: + resource = ( + "/cloud-infrastructure/cloud-regions/cloud-region/" + "%s/%s/flavors/flavor/%s/hpa-capabilities/hpa-capability/%s" + "?resource-version=%s" % ( + self.cloud_owner, self.cloud_region, + flavor['flavor-id'], hpa['hpa-capability-id'], + hpa['resource-version'])) + resp = call_req(self.base_url, self.username, self.password, + rest_no_auth, resource, "DELETE", + headers=self.default_headers) + if resp[0] != 0: + raise VimDriverAzureException( + status_code=400, + content="Failed to delete flavor %s on hpa %s: %s." % ( + flavor['flavor-id'], hpa['hpa-capability-id'], + resp[1])) + + def _del_flavors(self, rsp): + flavors = rsp.get("flavors", []) + if not flavors: + return + for flavor in flavors["flavor"]: + self._del_hpa(flavor) + resource = ("/cloud-infrastructure/cloud-regions/cloud-region/" + "%s/%s/flavors/flavor/%s?resource-version=%s" % ( + self.cloud_owner, self.cloud_region, + flavor['flavor-id'], flavor['resource-version'])) + resp = call_req(self.base_url, self.username, self.password, + rest_no_auth, resource, "DELETE", + headers=self.default_headers) + if resp[0] != 0: + raise VimDriverAzureException( + status_code=400, + content="Failed to delete flavor %s: %s." % ( + flavor['flavor-id'], resp[1])) + + def _del_images(self, rsp): + tenants = rsp.get("images", []) + if not tenants: + return + for tenant in tenants["image"]: + resource = ("/cloud-infrastructure/cloud-regions/cloud-region/" + "%s/%s/images/image/%s?resource-version=%s" % ( + self.cloud_owner, self.cloud_region, + tenant['image-id'], tenant['resource-version'])) + resp = call_req(self.base_url, self.username, self.password, + rest_no_auth, resource, "DELETE", + headers=self.default_headers) + if resp[0] != 0: + raise VimDriverAzureException( + status_code=400, + content="Failed to delete image %s: %s." % ( + tenant['image-id'], resp[1])) + + def _del_networks(self, rsp): + networks = rsp.get("oam-networks", []) + if not networks: + return + for network in networks["oam-network"]: + resource = ("/cloud-infrastructure/cloud-regions/cloud-region/" + "%s/%s/oam-networks/oam-network/%s?" + "resource-version=%s" % ( + self.cloud_owner, self.cloud_region, + network['network-uuid'], + network['resource-version'])) + resp = call_req(self.base_url, self.username, self.password, + rest_no_auth, resource, "DELETE", + headers=self.default_headers) + if resp[0] != 0: + raise VimDriverAzureException( + status_code=400, + content="Failed to delete network %s: %s." % ( + network['network-uuid'], resp[1])) + + def _del_azs(self, rsp): + azs = rsp.get("availability-zones", []) + if not azs: + return + for az in azs["availability-zone"]: + resource = ("/cloud-infrastructure/cloud-regions/cloud-region/" + "%s/%s/availability-zones/availability-zone/%s?" + "resource-version=%s" % ( + self.cloud_owner, self.cloud_region, + az['availability-zone-name'], + az['resource-version'])) + resp = call_req(self.base_url, self.username, self.password, + rest_no_auth, resource, "DELETE", + headers=self.default_headers) + if resp[0] != 0: + raise VimDriverAzureException( + status_code=400, + content="Failed to delete availability zone %s: %s." % ( + az['availability-zone-name'], resp[1])) + + def _get_hpa_capabilities(self, flavor): + hpa_caps = [] + + # Basic capabilties + caps_dict = self._get_hpa_basic_capabilities(flavor) + if len(caps_dict) > 0: + logger.debug("basic_capabilities_info: %s" % caps_dict) + hpa_caps.append(caps_dict) + + # cpupining capabilities + caps_dict = self._get_cpupinning_capabilities(flavor['extra_specs']) + if len(caps_dict) > 0: + logger.debug("cpupining_capabilities_info: %s" % caps_dict) + hpa_caps.append(caps_dict) + + # cputopology capabilities + caps_dict = self._get_cputopology_capabilities(flavor['extra_specs']) + if len(caps_dict) > 0: + logger.debug("cputopology_capabilities_info: %s" % caps_dict) + hpa_caps.append(caps_dict) + + # hugepages capabilities + caps_dict = self._get_hugepages_capabilities(flavor['extra_specs']) + if len(caps_dict) > 0: + logger.debug("hugepages_capabilities_info: %s" % caps_dict) + hpa_caps.append(caps_dict) + + # numa capabilities + caps_dict = self._get_numa_capabilities(flavor['extra_specs']) + if len(caps_dict) > 0: + logger.debug("numa_capabilities_info: %s" % caps_dict) + hpa_caps.append(caps_dict) + + # storage capabilities + caps_dict = self._get_storage_capabilities(flavor) + if len(caps_dict) > 0: + logger.debug("storage_capabilities_info: %s" % caps_dict) + hpa_caps.append(caps_dict) + + # CPU instruction set extension capabilities + caps_dict = self._get_instruction_set_capabilities( + flavor['extra_specs']) + if len(caps_dict) > 0: + logger.debug("instruction_set_capabilities_info: %s" % caps_dict) + hpa_caps.append(caps_dict) + + # PCI passthrough capabilities + caps_dict = self._get_pci_passthrough_capabilities( + flavor['extra_specs']) + if len(caps_dict) > 0: + logger.debug("pci_passthrough_capabilities_info: %s" % caps_dict) + hpa_caps.append(caps_dict) + + # ovsdpdk capabilities + caps_dict = self._get_ovsdpdk_capabilities() + if len(caps_dict) > 0: + logger.debug("ovsdpdk_capabilities_info: %s" % caps_dict) + hpa_caps.append(caps_dict) + + return hpa_caps + + def _get_hpa_basic_capabilities(self, flavor): + basic_capability = {} + feature_uuid = uuid.uuid4() + + basic_capability['hpa-capability-id'] = str(feature_uuid) + basic_capability['hpa-feature'] = 'basicCapabilities' + basic_capability['architecture'] = 'generic' + basic_capability['hpa-version'] = 'v1' + + basic_capability['hpa-feature-attributes'] = [] + basic_capability['hpa-feature-attributes'].append({ + 'hpa-attribute-key': 'numVirtualCpu', + 'hpa-attribute-value': json.dumps( + {'value': str(flavor['vcpus'])})}) + basic_capability['hpa-feature-attributes'].append({ + 'hpa-attribute-key': 'virtualMemSize', + 'hpa-attribute-value': json.dumps({'value': str( + flavor['ram']), 'unit': 'MB'})}) + + return basic_capability + + def _get_cpupinning_capabilities(self, extra_specs): + cpupining_capability = {} + feature_uuid = uuid.uuid4() + + if (extra_specs.get('hw:cpu_policy') or + extra_specs.get('hw:cpu_thread_policy')): + cpupining_capability['hpa-capability-id'] = str(feature_uuid) + cpupining_capability['hpa-feature'] = 'cpuPinning' + cpupining_capability['architecture'] = 'generic' + cpupining_capability['hpa-version'] = 'v1' + + cpupining_capability['hpa-feature-attributes'] = [] + if extra_specs.get('hw:cpu_thread_policy'): + cpupining_capability['hpa-feature-attributes'].append({ + 'hpa-attribute-key': 'logicalCpuThreadPinningPolicy', + 'hpa-attribute-value': json.dumps({'value': str( + extra_specs['hw:cpu_thread_policy'])})}) + if extra_specs.get('hw:cpu_policy'): + cpupining_capability['hpa-feature-attributes'].append({ + 'hpa-attribute-key': 'logicalCpuPinningPolicy', + 'hpa-attribute-value': json.dumps({'value': str( + extra_specs['hw:cpu_policy'])})}) + + return cpupining_capability + + def _get_cputopology_capabilities(self, extra_specs): + cputopology_capability = {} + feature_uuid = uuid.uuid4() + + if (extra_specs.get('hw:cpu_sockets') or + extra_specs.get('hw:cpu_cores') or + extra_specs.get('hw:cpu_threads')): + cputopology_capability['hpa-capability-id'] = str(feature_uuid) + cputopology_capability['hpa-feature'] = 'cpuTopology' + cputopology_capability['architecture'] = 'generic' + cputopology_capability['hpa-version'] = 'v1' + + cputopology_capability['hpa-feature-attributes'] = [] + if extra_specs.get('hw:cpu_sockets'): + cputopology_capability['hpa-feature-attributes'].append({ + 'hpa-attribute-key': 'numCpuSockets', + 'hpa-attribute-value': json.dumps({'value': str( + extra_specs['hw:cpu_sockets'])})}) + if extra_specs.get('hw:cpu_cores'): + cputopology_capability['hpa-feature-attributes'].append({ + 'hpa-attribute-key': 'numCpuCores', + 'hpa-attribute-value': json.dumps({'value': str( + extra_specs['hw:cpu_cores'])})}) + if extra_specs.get('hw:cpu_threads'): + cputopology_capability['hpa-feature-attributes'].append({ + 'hpa-attribute-key': 'numCpuThreads', + 'hpa-attribute-value': json.dumps({'value': str( + extra_specs['hw:cpu_threads'])})}) + + return cputopology_capability + + def _get_hugepages_capabilities(self, extra_specs): + hugepages_capability = {} + feature_uuid = uuid.uuid4() + + if extra_specs.get('hw:mem_page_size'): + hugepages_capability['hpa-capability-id'] = str(feature_uuid) + hugepages_capability['hpa-feature'] = 'hugePages' + hugepages_capability['architecture'] = 'generic' + hugepages_capability['hpa-version'] = 'v1' + + hugepages_capability['hpa-feature-attributes'] = [] + if extra_specs['hw:mem_page_size'] == 'large': + hugepages_capability['hpa-feature-attributes'].append({ + 'hpa-attribute-key': 'memoryPageSize', + 'hpa-attribute-value': json.dumps( + {'value': '2', 'unit': 'MB'})}) + elif extra_specs['hw:mem_page_size'] == 'small': + hugepages_capability['hpa-feature-attributes'].append({ + 'hpa-attribute-key': 'memoryPageSize', + 'hpa-attribute-value': json.dumps( + {'value': '4', 'unit': 'KB'})}) + elif extra_specs['hw:mem_page_size'] == 'any': + logger.info("Currently HPA feature memoryPageSize " + "did not support 'any' page!!") + else: + hugepages_capability['hpa-feature-attributes'].append({ + 'hpa-attribute-key': 'memoryPageSize', + 'hpa-attribute-value': json.dumps({'value': str( + extra_specs['hw:mem_page_size']), 'unit': 'KB'}) + }) + + return hugepages_capability + + def _get_numa_capabilities(self, extra_specs): + numa_capability = {} + feature_uuid = uuid.uuid4() + + if extra_specs.get('hw:numa_nodes'): + numa_capability['hpa-capability-id'] = str(feature_uuid) + numa_capability['hpa-feature'] = 'numa' + numa_capability['architecture'] = 'generic' + numa_capability['hpa-version'] = 'v1' + + numa_capability['hpa-feature-attributes'] = [] + numa_capability['hpa-feature-attributes'].append({ + 'hpa-attribute-key': 'numaNodes', + 'hpa-attribute-value': json.dumps({'value': str( + extra_specs['hw:numa_nodes'])}) + }) + + for num in range(0, int(extra_specs['hw:numa_nodes'])): + numa_cpu_node = "hw:numa_cpus.%s" % num + numa_mem_node = "hw:numa_mem.%s" % num + numacpu_key = "numaCpu-%s" % num + numamem_key = "numaMem-%s" % num + + if (extra_specs.get(numa_cpu_node) and + extra_specs.get(numa_mem_node)): + numa_capability['hpa-feature-attributes'].append({ + 'hpa-attribute-key': numacpu_key, + 'hpa-attribute-value': json.dumps({'value': str( + extra_specs[numa_cpu_node])}) + }) + numa_capability['hpa-feature-attributes'].append({ + 'hpa-attribute-key': numamem_key, + 'hpa-attribute-value': json.dumps({'value': str( + extra_specs[numa_mem_node]), 'unit': 'MB'}) + }) + + return numa_capability + + def _get_storage_capabilities(self, flavor): + storage_capability = {} + feature_uuid = uuid.uuid4() + + storage_capability['hpa-capability-id'] = str(feature_uuid) + storage_capability['hpa-feature'] = 'localStorage' + storage_capability['architecture'] = 'generic' + storage_capability['hpa-version'] = 'v1' + + storage_capability['hpa-feature-attributes'] = [] + storage_capability['hpa-feature-attributes'].append({ + 'hpa-attribute-key': 'diskSize', + 'hpa-attribute-value': json.dumps({'value': str( + flavor['disk']), 'unit': 'GB'}) + }) + storage_capability['hpa-feature-attributes'].append({ + 'hpa-attribute-key': 'swapMemSize', + 'hpa-attribute-value': json.dumps({'value': str( + flavor.get('swap', 0)), 'unit': 'MB'}) + }) + storage_capability['hpa-feature-attributes'].append({ + 'hpa-attribute-key': 'ephemeralDiskSize', + 'hpa-attribute-value': json.dumps({'value': str( + flavor.get('OS-FLV-EXT-DATA:ephemeral', 0)), 'unit': 'GB'}) + }) + return storage_capability + + def _get_instruction_set_capabilities(self, extra_specs): + instruction_capability = {} + feature_uuid = uuid.uuid4() + + if extra_specs.get('hw:capabilities:cpu_info:features'): + instruction_capability['hpa-capability-id'] = str(feature_uuid) + instruction_capability['hpa-feature'] = 'instructionSetExtensions' + instruction_capability['architecture'] = 'Intel64' + instruction_capability['hpa-version'] = 'v1' + + instruction_capability['hpa-feature-attributes'] = [] + instruction_capability['hpa-feature-attributes'].append({ + 'hpa-attribute-key': 'instructionSetExtensions', + 'hpa-attribute-value': json.dumps( + {'value': extra_specs[ + 'hw:capabilities:cpu_info:features']}) + }) + return instruction_capability + + def _get_pci_passthrough_capabilities(self, extra_specs): + instruction_capability = {} + feature_uuid = uuid.uuid4() + + if extra_specs.get('pci_passthrough:alias'): + value1 = extra_specs['pci_passthrough:alias'].split(':') + value2 = value1[0].split('-') + + instruction_capability['hpa-capability-id'] = str(feature_uuid) + instruction_capability['hpa-feature'] = 'pciePassthrough' + instruction_capability['architecture'] = str(value2[2]) + instruction_capability['hpa-version'] = 'v1' + + instruction_capability['hpa-feature-attributes'] = [] + instruction_capability['hpa-feature-attributes'].append({ + 'hpa-attribute-key': 'pciCount', + 'hpa-attribute-value': json.dumps({'value': value1[1]}) + }) + instruction_capability['hpa-feature-attributes'].append({ + 'hpa-attribute-key': 'pciVendorId', + 'hpa-attribute-value': json.dumps({'value': value2[3]}) + }) + instruction_capability['hpa-feature-attributes'].append({ + 'hpa-attribute-key': 'pciDeviceId', + 'hpa-attribute-value': json.dumps({'value': value2[4]}) + }) + + return instruction_capability + + def _get_ovsdpdk_capabilities(self): + ovsdpdk_capability = {} + feature_uuid = uuid.uuid4() + + if not self._vim_info: + self._vim_info = self.get_vim(get_all=True) + cloud_extra_info_str = self._vim_info.get('cloud-extra-info') + if not isinstance(cloud_extra_info_str, dict): + try: + cloud_extra_info_str = json.loads(cloud_extra_info_str) + except Exception as ex: + logger.error("Can not convert cloud extra info %s %s" % ( + str(ex), cloud_extra_info_str)) + return {} + if cloud_extra_info_str: + cloud_dpdk_info = cloud_extra_info_str.get("ovsDpdk") + if cloud_dpdk_info: + ovsdpdk_capability['hpa-capability-id'] = str(feature_uuid) + ovsdpdk_capability['hpa-feature'] = 'ovsDpdk' + ovsdpdk_capability['architecture'] = 'Intel64' + ovsdpdk_capability['hpa-version'] = 'v1' + + ovsdpdk_capability['hpa-feature-attributes'] = [] + ovsdpdk_capability['hpa-feature-attributes'].append({ + 'hpa-attribute-key': str(cloud_dpdk_info.get("libname")), + 'hpa-attribute-value': json.dumps( + {'value': cloud_dpdk_info.get("libversion")}) + }) + return ovsdpdk_capability diff --git a/azure/azure/pub/utils/syscomm.py b/azure/azure/pub/utils/syscomm.py new file mode 100644 index 0000000..f838956 --- /dev/null +++ b/azure/azure/pub/utils/syscomm.py @@ -0,0 +1,111 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +import inspect +import json +from collections import defaultdict +from rest_framework import status + + +keystoneV2Json = \ + { + "auth": { + "tenantName": "", + "passwordCredentials": { + "username": "", + "password": "" + } + } + } + + +SUCCESS_STATE = [status.HTTP_200_OK, status.HTTP_201_CREATED, + status.HTTP_202_ACCEPTED] + + +def fun_name(): + return inspect.stack()[1][3] + + +def jsonResponse(data, encoding='utf-8'): + + content_type = "application/json" + try: + res = json.loads(data, encoding=encoding) + except Exception: + res = data + content_type = "text/plain" + return (res, content_type) + + +class Catalogs(object): + + def __init__(self): + self.ct = defaultdict(dict) + + def storeEndpoint(self, vimid, endpoints): + if vimid in self.ct: + self.ct[vimid].update(endpoints) + else: + self.ct.setdefault(vimid, endpoints) + + def getEndpointBy(self, vimid, serverType, interface='public'): + + vim = self.ct.get(vimid) + return vim.get(serverType).get(interface, "") if vim else "" + + +def verifyKeystoneV2(param): + + return _walk_json(param, keystoneV2Json) + + +# comapare two json by key +def _walk_json(data, data2): + if isinstance(data, dict) and isinstance(data2, dict): + if set(data.keys()) != set(data2.keys()): + return False + else: + v1 = data.values() + v2 = data2.values() + v1.sort() + v2.sort() + if len(v1) != len(v2): + return False + for (i, j) in zip(v1, v2): + # continue compare key + if isinstance(i, dict) and isinstance(j, dict): + if not _walk_json(i, j): + return False + # ignore value + else: + continue + + return True + + return False + + +def keystoneVersion(url, version="v3"): + + tmp = url.split("/") + v = tmp[-1] + if v not in ["v2.0", "v3"]: + url += "/" + version + else: + tmp[-1] = version + url = "/".join(tmp) + + return url + + +catalog = Catalogs() diff --git a/azure/azure/pub/utils/timeutil.py b/azure/azure/pub/utils/timeutil.py new file mode 100644 index 0000000..d5ef329 --- /dev/null +++ b/azure/azure/pub/utils/timeutil.py @@ -0,0 +1,17 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +import datetime + + +def now_time(fmt="%Y-%m-%d %H:%M:%S"): + return datetime.datetime.now().strftime(fmt) diff --git a/azure/azure/pub/utils/values.py b/azure/azure/pub/utils/values.py new file mode 100644 index 0000000..61d7114 --- /dev/null +++ b/azure/azure/pub/utils/values.py @@ -0,0 +1,22 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + + +def ignore_case_get(args, key, def_val=""): + if not key: + return def_val + if key in args: + return args[key] + for old_key in args: + if old_key.upper() == key.upper(): + return args[old_key] + return def_val diff --git a/azure/azure/pub/vim/__init__.py b/azure/azure/pub/vim/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/azure/azure/pub/vim/__init__.py diff --git a/azure/azure/pub/vim/const.py b/azure/azure/pub/vim/const.py new file mode 100644 index 0000000..dc5a3a4 --- /dev/null +++ b/azure/azure/pub/vim/const.py @@ -0,0 +1,14 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + + +SAMPLE_KEY = "sample_value" diff --git a/azure/azure/samples/__init__.py b/azure/azure/samples/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/azure/azure/samples/__init__.py diff --git a/azure/azure/samples/tests.py b/azure/azure/samples/tests.py new file mode 100644 index 0000000..e801e48 --- /dev/null +++ b/azure/azure/samples/tests.py @@ -0,0 +1,31 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +import unittest +import json +from django.test import Client +from rest_framework import status + + +class SampleViewTest(unittest.TestCase): + def setUp(self): + self.client = Client() + + def tearDown(self): + pass + + def test_sample(self): + response = self.client.get("/samples/") + self.assertEqual(status.HTTP_200_OK, + response.status_code, response.content) + resp_data = json.loads(response.content) + self.assertEqual("active", resp_data["status"]) diff --git a/azure/azure/samples/urls.py b/azure/azure/samples/urls.py new file mode 100644 index 0000000..39da376 --- /dev/null +++ b/azure/azure/samples/urls.py @@ -0,0 +1,17 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +from django.conf.urls import url +from azure.samples import views + +urlpatterns = [ + url(r'^samples/$', views.SampleList.as_view()), ] diff --git a/azure/azure/samples/views.py b/azure/azure/samples/views.py new file mode 100644 index 0000000..6576450 --- /dev/null +++ b/azure/azure/samples/views.py @@ -0,0 +1,35 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +import os +import logging + +from rest_framework.views import APIView +from rest_framework.response import Response + +logger = logging.getLogger(__name__) +log_file = "/var/log/onap/multicloud/azure/azure.log" + + +class SampleList(APIView): + """ + List all samples. + """ + + def get(self, request, format=None): + logger.debug("get") + output = "" + if os.path.exists(log_file): + with open("/var/log/onap/multicloud/azure/azure.log", "r") as f: + lines = f.readlines() + output = lines[-1] + return Response({"status": "active", "logs": output}) diff --git a/azure/azure/scripts/__init__.py b/azure/azure/scripts/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/azure/azure/scripts/__init__.py diff --git a/azure/azure/scripts/api.py b/azure/azure/scripts/api.py new file mode 100644 index 0000000..792fd5d --- /dev/null +++ b/azure/azure/scripts/api.py @@ -0,0 +1,41 @@ +# Copyright (c) 2018 Amdocs +# +# 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. +import eventlet +eventlet.monkey_patch() + +import os # noqa +from oslo_config import cfg # noqa +from oslo_service import service # noqa +import sys # noqa +# FIXME: Since there is no explicitly setup process for the project. Hack the +# python here. +sys.path.append(os.path.abspath('.')) + +from azure.api_v2 import service as api_service # noqa + + +def main(): + try: + api_server = api_service.WSGIService() + launcher = service.launch(cfg.CONF, + api_server, + workers=api_server.workers) + launcher.wait() + except RuntimeError as excp: + sys.stderr.write("ERROR: %s\n" % excp) + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/azure/azure/settings-cover.py b/azure/azure/settings-cover.py new file mode 100644 index 0000000..b4fecdd --- /dev/null +++ b/azure/azure/settings-cover.py @@ -0,0 +1,22 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +from azure.settings import * # noqa +from azure.settings import INSTALLED_APPS + +INSTALLED_APPS.append('django_nose') + +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' + +NOSE_ARGS = [ + '--with-coverage', + '--cover-package=azure', +] diff --git a/azure/azure/settings.py b/azure/azure/settings.py new file mode 100644 index 0000000..ace6e8f --- /dev/null +++ b/azure/azure/settings.py @@ -0,0 +1,98 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +import os +import sys +from logging import config +from onaplogging import monkey +monkey.patch_all() + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '3o-wney!99y)^h3v)0$j16l9=fdjxcb+a8g+q3tfbahcnu2b0o' + +# SECURITY WARNING: don't run with debug turned on in production! +# DEBUG = True + +ALLOWED_HOSTS = ['*'] + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'azure.pub.database', +] + +MIDDLEWARE_CLASSES = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'azure.middleware.LogContextMiddleware', +] + +ROOT_URLCONF = 'azure.urls' + +WSGI_APPLICATION = 'azure.wsgi.application' + +REST_FRAMEWORK = { + 'DEFAULT_RENDERER_CLASSES': ( + 'rest_framework.renderers.JSONRenderer', + ), + + 'DEFAULT_PARSER_CLASSES': ( + 'rest_framework.parsers.JSONParser', + 'rest_framework.parsers.MultiPartParser', + # 'rest_framework.parsers.FormParser', + # 'rest_framework.parsers.FileUploadParser', + ) +} + + +TIME_ZONE = 'UTC' + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.6/howto/static-files/ + +STATIC_URL = '/static/' + + +LOGGING_CONFIG = None +# yaml configuration of logging +LOGGING_FILE = os.path.join(BASE_DIR, 'azure/pub/config/log.yml') +config.yamlConfig(filepath=LOGGING_FILE, watchDog=True) + + +if 'test' in sys.argv: + from azure.pub.config import config + REST_FRAMEWORK = {} + import platform + + if platform.system() == 'Linux': + TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner' + TEST_OUTPUT_VERBOSE = True + TEST_OUTPUT_DESCRIPTIONS = True + TEST_OUTPUT_DIR = 'test-reports' diff --git a/azure/azure/swagger/__init__.py b/azure/azure/swagger/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/azure/azure/swagger/__init__.py diff --git a/azure/azure/swagger/image_utils.py b/azure/azure/swagger/image_utils.py new file mode 100644 index 0000000..a17565e --- /dev/null +++ b/azure/azure/swagger/image_utils.py @@ -0,0 +1,65 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + + +def image_formatter(image): + + image = image.to_dict() + properties = {} + if image.get("vmware_adaptertype"): + properties['vmware_adaptertype'] = image.get("vmware_adaptertype") + if image.get("vmware_ostype"): + properties['vmware_ostype'] = image.get("vmware_ostype") + + return { + 'id': image.get("id"), + 'name': image.get("name"), + 'imageType': image.get("disk_format"), + 'status': image.get("status"), + 'size': image.get("size"), + 'containerFormat': image.get("container_format"), + 'visibility': image.get("visibility"), + 'properties': properties + } + + +def vim_formatter(vim_info, tenantid): + + rsp = {} + rsp['vimId'] = vim_info.get('vimId') + rsp['vimName'] = vim_info.get('name') + rsp['tenantId'] = tenantid + return rsp + + +def sdk_param_formatter(data): + + param = {} + param['username'] = data.get('userName') + param['password'] = data.get('password') + param['auth_url'] = data.get('url') + param['project_id'] = data.get('tenant') + param['user_domain_name'] = 'default' + param['project_domain_name'] = 'default' + return param + + +def req_body_formatter(body): + + param = {} + param['name'] = body.get('name') + param['disk_format'] = body.get('imageType') + param['container_format'] = body.get('containerFormat') + param['visibility'] = body.get('visibility') + properties = body.get('properties', {}) + param.update(properties) + return param diff --git a/azure/azure/swagger/nova_utils.py b/azure/azure/swagger/nova_utils.py new file mode 100644 index 0000000..86f10a2 --- /dev/null +++ b/azure/azure/swagger/nova_utils.py @@ -0,0 +1,119 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +import six + + +def server_formatter(server, interfaces=[]): + r = { + "id": server.id, + "name": server.name, + "tenantId": server.project_id, + "availabilityZone": server.availability_zone, + "flavorId": server.flavor_id or server.flavor['id'], + "volumeArray": [], + "metadata": [], + "securityGroups": [], + # TODO finish following attributes + "serverGroup": "", + "contextArray": [], + "userdata": server.user_data, + "nicArray": [], + "status": server.status + } + if interfaces: + r['nicArray'] = [{'portId': i.port_id} for i in interfaces] + elif server.networks: + r['nicArray'] = [{'portId': n['port']} for n in server.networks] + # TODO: wait sdk fix block_device_mapping + try: + if server.attached_volumes: + r["volumeArray"] = [{'volumeId': v['id']} + for v in server.attached_volumes] + elif server.block_device_mapping: + r["volumeArray"] = [{'volumeId': v['uuid']} + for v in server.block_device_mapping] + except ValueError: + r['volumeArray'] = [{'volumeId': ""}] + if server.image_id or server.image: + r['boot'] = { + 'type': 2, + 'imageId': server.image_id or server.image['id'] + } + else: + r['boot'] = { + 'type': 1, + 'volumeId': r['volumeArray'][0]['volumeId'] + } + if server.metadata: + r["metadata"] = [{'keyName': k, 'value': v} + for k, v in six.iteritems(server.metadata)] + if server.security_groups: + r["securityGroups"] = [i['name'] for i in server.security_groups] + return r + + +def flavor_formatter(flavor, extra_specs): + r = { + "id": flavor.id, + "name": flavor.name, + "vcpu": flavor.vcpus, + "memory": flavor.ram, + "disk": flavor.disk, + "ephemeral": flavor.ephemeral, + "swap": flavor.swap, + "isPublic": flavor.is_public} + if extra_specs: + r["extraSpecs"] = extra_specs_formatter(extra_specs) + return r + + +def extra_specs_formatter(extra_specs): + return [{"keyName": k, "value": v} + for k, v in six.iteritems(extra_specs.extra_specs)] + + +def server_limits_formatter(limits): + return { + # nova + 'maxPersonality': limits.absolute.personality, + 'maxPersonalitySize': limits.absolute.personality_size, + 'maxServerGroupMembers': limits.absolute.server_group_members, + 'maxServerGroups': limits.absolute.server_groups, + 'maxImageMeta': limits.absolute.image_meta, + 'maxTotalCores': limits.absolute.total_cores, + 'maxTotalInstances': limits.absolute.instances, + 'maxTotalKeypairs': limits.absolute.keypairs, + 'maxTotalRAMSize': limits.absolute.total_ram, + 'security_group_rules': limits.absolute.security_group_rules, + 'security_group': limits.absolute.security_groups, + + # cinder + # neutron + } + + +def service_formatter(service): + return { + 'service': service.binary, + 'name': service.host, + 'zone': service.zone, + } + + +def hypervisor_formatter(hypervisor): + return { + 'name': hypervisor.name, + 'cpu': hypervisor.vcpus, + 'disk_gb': hypervisor.local_disk_size, + 'memory_mb': hypervisor.memory_size, + } diff --git a/azure/azure/swagger/tests.py b/azure/azure/swagger/tests.py new file mode 100644 index 0000000..4631455 --- /dev/null +++ b/azure/azure/swagger/tests.py @@ -0,0 +1,31 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +import unittest +# import json +from django.test import Client +from rest_framework import status + + +class SampleViewTest(unittest.TestCase): + def setUp(self): + self.client = Client() + + def tearDown(self): + pass + + def test_sample(self): + response = self.client.get("/api/multicloud-azure/v0/swagger.json") + self.assertEqual(status.HTTP_200_OK, + response.status_code, response.content) +# resp_data = json.loads(response.content) +# self.assertEqual({"status": "active"}, resp_data) diff --git a/azure/azure/swagger/urls.py b/azure/azure/swagger/urls.py new file mode 100644 index 0000000..f80fa26 --- /dev/null +++ b/azure/azure/swagger/urls.py @@ -0,0 +1,37 @@ +# Copyright (c) 2018 Amdocs +# Copyright (c) 2018 Amdocs +# +# 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. + +from django.conf.urls import url +from rest_framework.urlpatterns import format_suffix_patterns + +from azure.swagger.views.swagger_json import SwaggerJsonView + + +# Registry +from azure.swagger.views.registry.views import Registry +from azure.swagger.views.registry.views import UnRegistry + + +urlpatterns = [ + # swagger + url(r'^api/multicloud-azure/v0/swagger.json$', SwaggerJsonView.as_view()), + + # Registry + url(r'^api/multicloud-azure/v0/(?P<vimid>[0-9a-z-A-Z\-\_]+)/registry$', + Registry.as_view()), + url(r'^api/multicloud-azure/v0/(?P<vimid>[0-9a-z-A-Z\-\_]+)$', + UnRegistry.as_view()), + +] + +urlpatterns = format_suffix_patterns(urlpatterns) diff --git a/azure/azure/swagger/utils.py b/azure/azure/swagger/utils.py new file mode 100644 index 0000000..cb7e4f0 --- /dev/null +++ b/azure/azure/swagger/utils.py @@ -0,0 +1,37 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +import json +import os + + +def get_swagger_json_data(): + json_file = os.path.join(os.path.dirname( + __file__), 'multivim.flavor.swagger.json') + f = open(json_file) + json_data = json.JSONDecoder().decode(f.read()) + f.close() + # json_file = os.path.join(os.path.dirname( + # __file__), 'multivim.image.swagger.json') + # f = open(json_file) + # json_data_temp = json.JSONDecoder().decode(f.read()) + # f.close() + # json_data["paths"].update(json_data_temp["paths"]) + # json_data["definitions"].update(json_data_temp["definitions"]) + + json_data["basePath"] = "/api/multicloud-azure/v0/" + json_data["info"]["title"] = "MultiVIM driver \ + of Microsoft Azure Service NBI" + + return json_data diff --git a/azure/azure/swagger/views.py b/azure/azure/swagger/views.py new file mode 100644 index 0000000..f0b7028 --- /dev/null +++ b/azure/azure/swagger/views.py @@ -0,0 +1,29 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +import logging +# import traceback + +# from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView + +# from azure.pub.exceptions import VimDriverAzureException +from azure.swagger import utils + +logger = logging.getLogger(__name__) + + +class SwaggerJsonView(APIView): + def get(self, request): + + return Response(utils.get_swagger_json_data()) diff --git a/azure/azure/swagger/views/__init__.py b/azure/azure/swagger/views/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/azure/azure/swagger/views/__init__.py diff --git a/azure/azure/swagger/views/multivim.swagger.json b/azure/azure/swagger/views/multivim.swagger.json new file mode 100644 index 0000000..de3419f --- /dev/null +++ b/azure/azure/swagger/views/multivim.swagger.json @@ -0,0 +1,51 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "MultiVIM Service rest API" + }, + "basePath": "/api/multicloud-azure/v0/", + "tags": [ + { + "name": "MultiVIM Azure services" + } + ], + "paths": { + "/{vimid}/registry": { + "post": { + "tags": [ + "vim registration" + ], + "summary": "vim registration API", + "description": "vim registration API", + "operationId": "vim_registration", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "vimid", + "in": "path", + "description": "vim instance id", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation" + }, + "404": { + "description": "the vim id is wrong" + }, + "500": { + "description": "error occured during the process" + } + } + } + } + } +} diff --git a/azure/azure/swagger/views/registry/__init__.py b/azure/azure/swagger/views/registry/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/azure/azure/swagger/views/registry/__init__.py diff --git a/azure/azure/swagger/views/registry/views.py b/azure/azure/swagger/views/registry/views.py new file mode 100644 index 0000000..b5db805 --- /dev/null +++ b/azure/azure/swagger/views/registry/views.py @@ -0,0 +1,167 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + + +import logging + +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView + +from azure.pub.exceptions import VimDriverAzureException +from azure.pub.msapi import extsys +from azure.pub.utils.restcall import AAIClient + + +logger = logging.getLogger(__name__) + + +class Registry(APIView): + # def _get_tenants(self, auth_info): + # tenant_instance = OperateTenant.OperateTenant() + # try: + # projects = tenant_instance.get_projects(auth_info) + # except Exception as e: + # logger.exception("get tenants error %(e)s", {"e": e}) + # raise e + # + # rsp = {"tenants": []} + # for project in projects: + # rsp['tenants'].append(project.to_dict()) + # return rsp + # + # def _get_images(self, auth_info): + # image_instance = OperateImage.OperateImage(auth_info) + # try: + # images = image_instance.get_vim_images() + # except Exception as e: + # logger.exception("get images error %(e)s", {"e": e}) + # raise e + # + # rsp = {"images": []} + # for image in images: + # rsp['images'].append(image.to_dict()) + # return rsp + # + # def _get_flavors(self, auth_info): + # flavors_op = OperateFlavors.OperateFlavors() + # try: + # flavors = flavors_op.list_flavors( + # auth_info, auth_info['tenant']) + # except Exception as e: + # logger.exception("get flavors error %(e)s", {"e": e}) + # raise e + # + # rsp = {"flavors": []} + # for flavor in flavors: + # flavor_info = flavor[0].to_dict() + # flavor_info['extra_specs'] = flavor[1].extra_specs + # rsp['flavors'].append(flavor_info) + # return rsp + # + # def _get_networks(self, auth_info): + # net_op = OperateNetwork.OperateNetwork() + # try: + # resp = net_op.list_networks( + # auth_info['vimId'], auth_info['tenant']) + # except Exception as e: + # logger.exception("get networks error %(e)s", {"e": e}) + # raise e + # + # rsp = {'networks': resp['networks']} + # return rsp + # + # def _get_hypervisors(self, auth_info): + # hypervisor_op = OperateHypervisor.OperateHypervisor() + # try: + # hypervisors = hypervisor_op.list_hypervisors(auth_info) + # except Exception as e: + # logger.exception("get hypervisors error %(e)s", {"e": e}) + # raise e + # + # rsp = {"hypervisors": []} + # for hypervisor in hypervisors: + # rsp['hypervisors'].append(hypervisor.to_dict()) + # return rsp + + def _find_tenant_id(self, name, tenants): + for tenant in tenants['tenants']: + if tenant['name'] == name: + return tenant['id'] + + def post(self, request, vimid): + try: + vim_info = extsys.get_vim_by_id(vimid) + except VimDriverAzureException as e: + return Response(data={'error': str(e)}, status=e.status_code) + data = {} + data['vimId'] = vim_info['vimId'] + data['username'] = vim_info['userName'] + data['userName'] = vim_info['userName'] + data['password'] = vim_info['password'] + data['url'] = vim_info['url'] + data['project_name'] = vim_info['tenant'] + + rsp = {} + # get tenants + try: + logger.debug('Getting tenants') + tenants = self._get_tenants(data) + rsp.update(tenants) + data['tenant'] = self._find_tenant_id( + data['project_name'], tenants) + data['project_id'] = data['tenant'] + # set default tenant + # get images + logger.debug('Getting images') + images = self._get_images(data) + rsp.update(images) + # get flavors + logger.debug('Getting flavors') + flavors = self._get_flavors(data) + rsp.update(flavors) + # get networks + logger.debug('Getting networks') + networks = self._get_networks(data) + rsp.update(networks) + # get hypervisors + logger.debug('Getting hypervisors') + hypervisors = self._get_hypervisors(data) + rsp.update(hypervisors) + # update A&AI + logger.debug('Put data into A&AI') + cloud_owner, cloud_region = extsys.split_vim_to_owner_region( + vimid) + aai_adapter = AAIClient(cloud_owner, cloud_region) + aai_adapter.update_vim(rsp) + except Exception as e: + if hasattr(e, "http_status"): + return Response(data={'error': str(e)}, status=e.http_status) + else: + return Response(data={'error': str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + return Response(data="", status=status.HTTP_200_OK) + + +class UnRegistry(APIView): + + def delete(self, request, vimid): + try: + cloud_owner, cloud_region = extsys.split_vim_to_owner_region( + vimid) + aai_adapter = AAIClient(cloud_owner, cloud_region) + aai_adapter.delete_vim() + except Exception as e: + return Response(data=e.message, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response(data="", status=status.HTTP_204_NO_CONTENT) diff --git a/azure/azure/swagger/views/swagger_json.py b/azure/azure/swagger/views/swagger_json.py new file mode 100644 index 0000000..91c00dd --- /dev/null +++ b/azure/azure/swagger/views/swagger_json.py @@ -0,0 +1,42 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + + +import json +import logging +import os + +from rest_framework.response import Response +from rest_framework.views import APIView + + +logger = logging.getLogger(__name__) + + +class SwaggerJsonView(APIView): + def get(self, request): + json_file = os.path.join(os.path.dirname( + __file__), 'multivim.swagger.json') + f = open(json_file) + json_data = json.JSONDecoder().decode(f.read()) + f.close() + # json_file = os.path.join(os.path.dirname( + # __file__), 'multivim.image.swagger.json') + # f = open(json_file) + # json_data_temp = json.JSONDecoder().decode(f.read()) + # f.close() + # json_data["paths"].update(json_data_temp["paths"]) + # json_data["definitions"].update(json_data_temp["definitions"]) + json_data["basePath"] = "/api/multicloud-azure/v0/" + json_data["info"]["title"] = "MultiVIM \ + driver of Microsoft Azure Service NBI" + return Response(json_data) diff --git a/azure/azure/swagger/volume_utils.py b/azure/azure/swagger/volume_utils.py new file mode 100644 index 0000000..ae11285 --- /dev/null +++ b/azure/azure/swagger/volume_utils.py @@ -0,0 +1,72 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + + +def volume_formatter(volume): + + attachments = [] + for attach in volume.attachments: + vim_attach = { + 'device': attach['device'], + 'volumeId': attach['volume_id'], + 'hostName': attach['host_name'], + 'Id': attach['attachment_id'], + 'serverId': attach['server_id'] + } + attachments.append(vim_attach) + + return { + 'id': volume.id, + 'name': volume.name, + 'createTime': volume.created_at, + 'status': volume.status, + 'type': volume.volume_type, + 'size': volume.size, + 'availabilityZone': volume.availability_zone, + 'attachments': attachments + } + + +def vim_formatter(vim_info, tenantid): + + rsp = {} + rsp['vimId'] = vim_info.get('vimId') + rsp['vimName'] = vim_info.get('name') + rsp['tenantId'] = tenantid + return rsp + + +def sdk_param_formatter(data): + + param = {} + param['username'] = data.get('userName') + param['password'] = data.get('password') + param['auth_url'] = data.get('url') + param['project_id'] = data.get('tenant') + param['user_domain_name'] = 'default' + param['project_domain_name'] = 'default' + return param + + +def req_body_formatter(body): + + param = {} + param['name'] = body.get('name') + param['size'] = body.get('volumeSize') + + if body.get('volumeType'): + param['volume_type'] = body.get('volumeType') + if body.get('availabilityZone'): + param['availability_zone'] = body.get('availabilityZone') + if body.get('imageId'): + param['image_id'] = body.get('imageId') + return param diff --git a/azure/azure/tests/__init__.py b/azure/azure/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/azure/azure/tests/__init__.py diff --git a/azure/azure/tests/test_aai_client.py b/azure/azure/tests/test_aai_client.py new file mode 100644 index 0000000..31ff37d --- /dev/null +++ b/azure/azure/tests/test_aai_client.py @@ -0,0 +1,378 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +import json +import mock +import unittest + +from azure.pub.exceptions import VimDriverAzureException +from azure.pub.utils import restcall + + +class TestAAIClient(unittest.TestCase): + + def setUp(self): + self.view = restcall.AAIClient("vmware", "4.0") + + @mock.patch.object(restcall, "call_req") + def test_get_vim(self, mock_call): + mock_call.return_value = [0, '{"cloudOwner": "vmware"}'] + ret = self.view.get_vim(get_all=True) + expect_ret = {"cloudOwner": "vmware"} + self.assertEqual(expect_ret, ret) + + @mock.patch.object(restcall.AAIClient, "get_vim") + @mock.patch.object(restcall, "call_req") + def test_update_identity_url(self, mock_call, mock_getvim): + mock_getvim.return_value = {} + self.view.update_identity_url() + mock_call.assert_called_once() + + @mock.patch.object(restcall, "call_req") + def test_add_tenants(self, mock_call): + tenants = {"tenants": [{"name": "admin", "id": "admin-id"}]} + self.view.add_tenants(tenants) + mock_call.assert_called_once() + + @mock.patch.object(restcall, "call_req") + def test_add_flavors(self, mock_call): + flavors = { + "flavors": [{ + "name": "m1.small", + "id": "1", + "vcpus": 1, + "ram": 512, + "disk": 10, + "ephemeral": 0, + "swap": 0, + "is_public": True, + "links": [{"href": "http://fake-url"}], + "is_disabled": False + }] + } + self.view.add_flavors(flavors) + mock_call.assert_called_once() + + @mock.patch.object(restcall, "call_req") + def test_add_flavors_with_hpa(self, mock_call): + flavors = { + "flavors": [{ + "name": "onap.small", + "id": "1", + "vcpus": 1, + "ram": 512, + "disk": 10, + "ephemeral": 0, + "swap": 0, + "is_public": True, + "links": [{"href": "http://fake-url"}], + "is_disabled": False, + "extra_specs": {}, + }] + } + self.view._get_ovsdpdk_capabilities = mock.MagicMock() + self.view._get_ovsdpdk_capabilities.return_value = {} + self.view.add_flavors(flavors) + mock_call.assert_called_once() + + @mock.patch.object(restcall, "call_req") + def test_add_images(self, mock_call): + images = { + "images": [{ + "name": "ubuntu-16.04", + "id": "image-id" + }] + } + self.view.add_images(images) + mock_call.assert_called_once() + + @mock.patch.object(restcall, "call_req") + def test_add_networks(self, mock_call): + networks = { + "networks": [{ + "name": "net-1", + "id": "net-id", + "segmentationId": 144 + }] + } + self.view.add_networks(networks) + mock_call.assert_called_once() + + @mock.patch.object(restcall, "call_req") + def test_add_pservers(self, mock_call): + pservers = { + "hypervisors": [{ + "name": "compute-1", + "vcpus": 100, + "local_disk_size": 1000, + "memory_size": 10240, + "host_ip": "10.0.0.7", + "id": "compute-1-id" + }] + } + self.view.add_pservers(pservers) + self.assertEqual(mock_call.call_count, 2) + + @mock.patch.object(restcall, "call_req") + def test_del_tenants(self, mock_call): + mock_call.return_value = [0] + rsp = { + "tenants": { + "tenant": [{ + "tenant-id": "tenant-id", + "resource-version": "version-1" + }] + } + } + self.view._del_tenants(rsp) + mock_call.assert_called_once() + + @mock.patch.object(restcall, "call_req") + def test_del_flavors(self, mock_call): + mock_call.return_value = [0] + rsp = { + "flavors": { + "flavor": [{ + "flavor-id": "fake-id", + "resource-version": "fake-version" + }] + } + } + self.view._del_flavors(rsp) + mock_call.assert_called_once() + + @mock.patch.object(restcall, "call_req") + def test_del_images(self, mock_call): + mock_call.return_value = [0] + rsp = { + "images": { + "image": [{ + "image-id": "fake-id", + "resource-version": "fake-version" + }] + } + } + self.view._del_images(rsp) + mock_call.assert_called_once() + + @mock.patch.object(restcall, "call_req") + def test_del_networks(self, mock_call): + mock_call.return_value = [0] + rsp = { + "oam-networks": { + "oam-network": [{ + "network-uuid": "fake-id", + "resource-version": "fake-version" + }] + } + } + self.view._del_networks(rsp) + mock_call.assert_called_once() + + @mock.patch.object(restcall, "call_req") + def test_del_azs(self, mock_call): + mock_call.return_value = [0] + rsp = { + "availability-zones": { + "availability-zone": [{ + "availability-zone-name": "fake-name", + "resource-version": "fake-version" + }] + } + } + self.view._del_azs(rsp) + mock_call.assert_called_once() + + @mock.patch.object(restcall, "call_req") + def test_del_hpa(self, mock_call): + mock_call.return_value = [0] + rsp = { + "flavor-id": "id1", + "hpa-capabilities": { + "hpa-capability": [{ + "resource-version": "v1", + "hpa-capability-id": "id2" + }] + } + } + self.view._del_hpa(rsp) + mock_call.assert_called_once() + + @mock.patch.object(restcall, "call_req") + def test_del_vim(self, mock_call): + resp = { + "resource-version": "1" + } + self.view.get_vim = mock.MagicMock() + self.view.get_vim.return_value = resp + mock_call.return_value = [0, "", "", ""] + self.view.delete_vim() + mock_call.assert_called_once() + + @mock.patch.object(restcall, "call_req") + def test_del_vim_fail(self, mock_call): + resp = { + "resource-version": "1" + } + self.view.get_vim = mock.MagicMock() + self.view.get_vim.return_value = resp + mock_call.return_value = [1, "", "", ""] + self.assertRaises(VimDriverAzureException, self.view.delete_vim) + + @mock.patch.object(restcall, "call_req") + def test_update_vim(self, mock_call): + resp = { + "resource-version": "1" + } + self.view.get_vim = mock.MagicMock() + self.view.get_vim.return_value = resp + content = { + "tenants": [], + "images": [], + "flavors": [], + "networks": [], + "hypervisors": [] + } + self.view.update_vim(content) + mock_call.assert_called_once() + + @mock.patch.object(restcall, "call_req") + def test_get_hpa(self, mock_call): + self.view._get_hpa_basic_capabilities = mock.MagicMock() + self.view._get_hpa_basic_capabilities.return_value = {"hpa": "basic"} + self.view._get_cpupinning_capabilities = mock.MagicMock() + self.view._get_cpupinning_capabilities.return_value = {"hpa": "basic"} + self.view._get_cputopology_capabilities = mock.MagicMock() + self.view._get_cputopology_capabilities.return_value = {"hpa": "basic"} + self.view._get_hugepages_capabilities = mock.MagicMock() + self.view._get_hugepages_capabilities.return_value = {"hpa": "basic"} + self.view._get_numa_capabilities = mock.MagicMock() + self.view._get_numa_capabilities.return_value = {"hpa": "basic"} + self.view._get_storage_capabilities = mock.MagicMock() + self.view._get_storage_capabilities.return_value = {"hpa": "basic"} + self.view._get_instruction_set_capabilities = mock.MagicMock() + self.view._get_instruction_set_capabilities.return_value = { + "hpa": "basic"} + self.view._get_pci_passthrough_capabilities = mock.MagicMock() + self.view._get_pci_passthrough_capabilities.return_value = { + "hpa": "basic"} + self.view._get_ovsdpdk_capabilities = mock.MagicMock() + self.view._get_ovsdpdk_capabilities.return_value = {"hpa": "basic"} + ret = self.view._get_hpa_capabilities({"extra_specs": {}}) + self.assertEqual([{"hpa": "basic"}]*9, ret) + + @mock.patch.object(restcall, "call_req") + def test_get_hpa_basic(self, mock_call): + flavor = { + "vcpus": 1, + "ram": 1024 + } + ret = self.view._get_hpa_basic_capabilities(flavor) + self.assertEqual(len(ret["hpa-feature-attributes"]), 2) + + @mock.patch.object(restcall, "call_req") + def test_get_hpa_cpupin(self, mock_call): + extra = { + "hw:cpu_policy": "cpu_policy", + "hw:cpu_thread_policy": "thread_policy" + } + ret = self.view._get_cpupinning_capabilities(extra) + self.assertEqual(len(ret["hpa-feature-attributes"]), 2) + + @mock.patch.object(restcall, "call_req") + def test_get_hpa_cputopo(self, mock_call): + extra = { + "hw:cpu_sockets": 2, + "hw:cpu_cores": 2, + "hw:cpu_threads": 4 + } + ret = self.view._get_cputopology_capabilities(extra) + self.assertEqual(len(ret["hpa-feature-attributes"]), 3) + + @mock.patch.object(restcall, "call_req") + def test_get_hpa_hugepage_large(self, mock_call): + extra = { + "hw:mem_page_size": "large" + } + ret = self.view._get_hugepages_capabilities(extra) + self.assertIn( + "2", ret["hpa-feature-attributes"][0]["hpa-attribute-value"]) + + @mock.patch.object(restcall, "call_req") + def test_get_hpa_hugepage_small(self, mock_call): + extra = { + "hw:mem_page_size": "small" + } + ret = self.view._get_hugepages_capabilities(extra) + self.assertIn( + "4", ret["hpa-feature-attributes"][0]["hpa-attribute-value"]) + + @mock.patch.object(restcall, "call_req") + def test_get_hpa_hugepage_int(self, mock_call): + extra = { + "hw:mem_page_size": 8, + } + ret = self.view._get_hugepages_capabilities(extra) + self.assertIn( + "8", ret["hpa-feature-attributes"][0]["hpa-attribute-value"]) + + @mock.patch.object(restcall, "call_req") + def test_get_hpa_hugepage_any(self, mock_call): + extra = { + "hw:mem_page_size": "any", + } + ret = self.view._get_hugepages_capabilities(extra) + self.assertEqual(0, len(ret["hpa-feature-attributes"])) + + @mock.patch.object(restcall, "call_req") + def test_get_hpa_numa(self, mock_call): + extra = { + "hw:numa_nodes": 1, + "hw:numa_cpus.0": 1, + "hw:numa_mem.0": 1024, + } + ret = self.view._get_numa_capabilities(extra) + self.assertEqual(3, len(ret["hpa-feature-attributes"])) + + @mock.patch.object(restcall, "call_req") + def test_get_hpa_storage(self, mock_call): + extra = { + "disk": 10, + } + ret = self.view._get_storage_capabilities(extra) + self.assertEqual(3, len(ret["hpa-feature-attributes"])) + + @mock.patch.object(restcall, "call_req") + def test_get_hpa_instru(self, mock_call): + extra = { + "hw:capabilities:cpu_info:features": "avx", + } + ret = self.view._get_instruction_set_capabilities(extra) + self.assertEqual(1, len(ret["hpa-feature-attributes"])) + + @mock.patch.object(restcall, "call_req") + def test_get_hpa_pci(self, mock_call): + extra = { + "pci_passthrough:alias": "gpu-nvidia-x86-0011-0022:1", + } + ret = self.view._get_pci_passthrough_capabilities(extra) + self.assertEqual(3, len(ret["hpa-feature-attributes"])) + + @mock.patch.object(restcall, "call_req") + def test_get_hpa_dpdk(self, mock_call): + self.view.get_vim = mock.MagicMock() + self.view.get_vim.return_value = { + "cloud-extra-info": json.dumps({'ovsDpdk': { + 'libname': 'generic', 'libversion': '17.04'}}) + } + ret = self.view._get_ovsdpdk_capabilities() + self.assertEqual(1, len(ret["hpa-feature-attributes"])) diff --git a/azure/azure/tests/test_restcall.py b/azure/azure/tests/test_restcall.py new file mode 100644 index 0000000..bf45ccf --- /dev/null +++ b/azure/azure/tests/test_restcall.py @@ -0,0 +1,101 @@ +# Copyright (c) 2018 Amdocs +# 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. + +import mock +import unittest +import urllib2 + +from azure.pub.utils import restcall + + +class TestRestCall(unittest.TestCase): + + def test_combine_url(self): + url = ["http://a.com/test/", "http://a.com/test/", + "http://a.com/test", "http://a.com/test"] + res = ["/resource", "resource", "/resource", "resource"] + expected = "http://a.com/test/resource" + for i in range(len(url)): + self.assertEqual(expected, restcall.combine_url(url[i], res[i])) + + @mock.patch.object(restcall, "call_req") + def test_get_res_from_aai(self, mock_call): + res = "cloud-regions" + content = "" + expect_url = "https://aai.api.simpledemo.openecomp.org:8443/aai/v13" + expect_user = "AAI" + expect_pass = "AAI" + expect_headers = { + 'X-FromAppId': 'MultiCloud', + 'X-TransactionId': '9001', + 'content-type': 'application/json', + 'accept': 'application/json' + } + restcall.get_res_from_aai(res, content=content) + mock_call.assert_called_once_with( + expect_url, expect_user, expect_pass, restcall.rest_no_auth, + res, "GET", content, expect_headers) + + @mock.patch.object(restcall, "call_req") + def test_req_by_msb(self, mock_call): + res = "multicloud" + method = "GET" + content = "no content" + restcall.req_by_msb(res, method, content=content) + expect_url = "http://msb.onap.org:10080/" + mock_call.assert_called_once_with( + expect_url, "", "", restcall.rest_no_auth, res, method, + content) + + @mock.patch("httplib2.Http.request") + def test_call_req_success(self, mock_req): + mock_resp = { + "status": "200" + } + resp_content = "hello" + mock_req.return_value = mock_resp, resp_content + expect_ret = [0, resp_content, "200", mock_resp] + ret = restcall.call_req("http://onap.org/", "user", "pass", + restcall.rest_no_auth, "vim", "GET") + self.assertEqual(expect_ret, ret) + + @mock.patch("httplib2.Http.request") + def test_call_req_not_200(self, mock_req): + mock_resp = { + "status": "404" + } + resp_content = "hello" + mock_req.return_value = mock_resp, resp_content + expect_ret = [1, resp_content, "404", mock_resp] + ret = restcall.call_req("http://onap.org/", "user", "pass", + restcall.rest_no_auth, "vim", "GET") + self.assertEqual(expect_ret, ret) + + @mock.patch("traceback.format_exc") + @mock.patch("sys.exc_info") + @mock.patch("httplib2.Http.request") + def test_call_req_response_not_ready(self, mock_req, mock_sys, + mock_traceback): + mock_sys.return_value = "httplib.ResponseNotReady" + mock_req.side_effect = [Exception("httplib.ResponseNotReady")] * 3 + expect_ret = [1, "Unable to connect to http://onap.org/vim", "", ""] + ret = restcall.call_req("http://onap.org/", "user", "pass", + restcall.rest_no_auth, "vim", "GET") + self.assertEqual(expect_ret, ret) + self.assertEqual(3, mock_req.call_count) + + @mock.patch("httplib2.Http.request") + def test_call_req_url_err(self, mock_req): + urlerr = urllib2.URLError("urlerror") + mock_req.side_effect = [urlerr] + expect_ret = [2, str(urlerr), "", ""] + ret = restcall.call_req("http://onap.org/", "user", "pass", + restcall.rest_no_auth, "vim", "GET") + self.assertEqual(expect_ret, ret) diff --git a/azure/azure/urls.py b/azure/azure/urls.py new file mode 100644 index 0000000..028f008 --- /dev/null +++ b/azure/azure/urls.py @@ -0,0 +1,18 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +from django.conf.urls import include, url + +urlpatterns = [ + url(r'^', include('azure.swagger.urls')), + url(r'^', include('azure.samples.urls')), +] diff --git a/azure/azure/wsgi.py b/azure/azure/wsgi.py new file mode 100644 index 0000000..355b604 --- /dev/null +++ b/azure/azure/wsgi.py @@ -0,0 +1,20 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "azure.settings") + +application = get_wsgi_application() diff --git a/azure/docker/Dockerfile b/azure/docker/Dockerfile new file mode 100644 index 0000000..1b232ae --- /dev/null +++ b/azure/docker/Dockerfile @@ -0,0 +1,29 @@ +FROM python:2 + +ENV MSB_ADDR "127.0.0.1" +ENV MSB_PORT "80" +ENV AAI_ADDR "aai.api.simpledemo.openecomp.org" +ENV AAI_PORT "8443" +ENV AAI_SCHEMA_VERSION "v13" +ENV AAI_USERNAME "AAI" +ENV AAI_PASSWORD "AAI" +ENV MR_ADDR "127.0.0.1" +ENV MR_PORT "3904" + +EXPOSE 9004 + +RUN apt-get update && \ + apt-get install -y unzip && \ + apt-get install -y curl && \ + apt-get install -y wget + + +RUN cd /opt/ && \ + wget -q -O multicloud-azure.zip 'https://nexus.onap.org/service/local/artifact/maven/redirect?r=snapshots&g=org.onap.multicloud.azure&a=multicloud-azure&v=LATEST&e=zip' && \ + unzip multicloud-azure.zip && \ + rm -rf multicloud-azure.zip && \ + pip install -r azure/requirements.txt + + +WORKDIR /opt +ENTRYPOINT azure/docker/docker-entrypoint.sh diff --git a/azure/docker/build_image.sh b/azure/docker/build_image.sh new file mode 100644 index 0000000..24ba356 --- /dev/null +++ b/azure/docker/build_image.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# Copyright (c) 2018 Amdocs +# +# 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. + +DIRNAME=`dirname $0` +DOCKER_BUILD_DIR=`cd $DIRNAME/; pwd` +echo "DOCKER_BUILD_DIR=${DOCKER_BUILD_DIR}" +cd ${DOCKER_BUILD_DIR} + +BUILD_ARGS="--no-cache" +ORG="onap" +VERSION="1.2.0-SNAPSHOT" +STAGING="1.2.0-STAGING" +PROJECT="multicloud" +IMAGE="azure" +DOCKER_REPOSITORY="nexus3.onap.org:10003" +IMAGE_NAME="${DOCKER_REPOSITORY}/${ORG}/${PROJECT}/${IMAGE}" + +if [ $HTTP_PROXY ]; then + BUILD_ARGS+=" --build-arg HTTP_PROXY=${HTTP_PROXY}" +fi +if [ $HTTPS_PROXY ]; then + BUILD_ARGS+=" --build-arg HTTPS_PROXY=${HTTPS_PROXY}" +fi + +function build_image { + echo "Start build docker image: ${IMAGE_NAME}" + docker build ${BUILD_ARGS} -t ${IMAGE_NAME}:${VERSION} -t ${IMAGE_NAME}:latest -t ${IMAGE_NAME}:${STAGING} . +} + +function push_image { + echo "Start push docker image: ${IMAGE_NAME}" + docker push ${IMAGE_NAME}:${VERSION} + docker push ${IMAGE_NAME}:latest + docker push ${IMAGE_NAME}:${STAGING} +} + +build_image +push_image diff --git a/azure/docker/docker-entrypoint.sh b/azure/docker/docker-entrypoint.sh new file mode 100644 index 0000000..5566aff --- /dev/null +++ b/azure/docker/docker-entrypoint.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# Copyright (c) 2018 Amdocs +# +# 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. + + +if [ -z "$SERVICE_IP" ]; then + export SERVICE_IP=`hostname -i` +fi +echo +echo Environment Variables: +echo "SERVICE_IP=$SERVICE_IP" + +if [ -z "$MSB_ADDR" ]; then + echo "Missing required variable MSB_ADDR: Microservices Service Bus address <ip>:<port>" + exit 1 +fi +echo "MSB_ADDR=$MSB_ADDR" +echo + + +echo + +# Configure service based on docker environment variables +azure/docker/instance-config.sh + + +# Perform one-time config +if [ ! -e init.log ]; then + + # microservice-specific one-time initialization + azure/docker/instance-init.sh + + date > init.log +fi + +# Start the microservice +azure/docker/instance-run.sh diff --git a/azure/docker/instance-config.sh b/azure/docker/instance-config.sh new file mode 100644 index 0000000..6500d03 --- /dev/null +++ b/azure/docker/instance-config.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# Copyright (c) 2018 Amdocs +# +# 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. + +# Configure MSB IP address +MSB_IP=`echo $MSB_ADDR | cut -d: -f 1` +MSB_PORT=`echo $MSB_PORT | cut -d: -f 2` +sed -i "s|MSB_SERVICE_IP.*|MSB_SERVICE_IP = '$MSB_IP'|" azure/azure/pub/config/config.py +sed -i "s|MSB_SERVICE_PORT.*|MSB_SERVICE_PORT = '$MSB_PORT'|" azure/azure/pub/config/config.py +sed -i "s|DB_NAME.*|DB_NAME = 'inventory'|" azure/azure/pub/config/config.py +sed -i "s|DB_USER.*|DB_USER = 'inventory'|" azure/azure/pub/config/config.py +sed -i "s|DB_PASSWD.*|DB_PASSWD = 'inventory'|" azure/azure/pub/config/config.py +sed -i "s|\"ip\": \".*\"|\"ip\": \"$SERVICE_IP\"|" azure/azure/pub/config/config.py + +# Configure MYSQL +if [ -z "$MYSQL_ADDR" ]; then + export MYSQL_IP=`hostname -i` + export MYSQL_PORT=3306 + export MYSQL_ADDR=$MYSQL_IP:$MYSQL_PORT +else + MYSQL_IP=`echo $MYSQL_ADDR | cut -d: -f 1` + MYSQL_PORT=`echo $MYSQL_ADDR | cut -d: -f 2` +fi +echo "MYSQL_ADDR=$MYSQL_ADDR" +sed -i "s|DB_IP.*|DB_IP = '$MYSQL_IP'|" azure/azure/pub/config/config.py +sed -i "s|DB_PORT.*|DB_PORT = $MYSQL_PORT|" azure/azure/pub/config/config.py + +cat azure/azure/pub/config/config.py + +sed -i "s/sip=.*/sip=$SERVICE_IP/g" azure/run.sh +sed -i "s/sip=.*/sip=$SERVICE_IP/g" azure/stop.sh + +# Create log directory +logDir="/var/log/onap/multicloud/azure" +if [ ! -x $logDir ]; then + mkdir -p $logDir +fi diff --git a/azure/docker/instance-init.sh b/azure/docker/instance-init.sh new file mode 100644 index 0000000..cd2222c --- /dev/null +++ b/azure/docker/instance-init.sh @@ -0,0 +1,22 @@ +#!/bin/bash -v +# Copyright (c) 2018 Amdocs +# +# 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. + +# Initialize DB schema +#./bin/initDB.sh root rootpass 3306 127.0.0.1 + +# Install python requirements +cd /opt/azure +./initialize.sh +cd /opt diff --git a/azure/docker/instance-run.sh b/azure/docker/instance-run.sh new file mode 100644 index 0000000..90d3e9d --- /dev/null +++ b/azure/docker/instance-run.sh @@ -0,0 +1,22 @@ +#!/bin/bash -v +# Copyright (c) 2018 Amdocs +# +# 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. + +cd ./azure +./run.sh + +while [ ! -f /var/log/onap/multicloud/azure/azure.log ]; do + sleep 1 +done +tail -F /var/log/onap/multicloud/azure/azure.log diff --git a/azure/images/empty.txt b/azure/images/empty.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/azure/images/empty.txt diff --git a/azure/initialize.sh b/azure/initialize.sh new file mode 100644 index 0000000..6128a8b --- /dev/null +++ b/azure/initialize.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Copyright (c) 2018 Amdocs +# +# 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. + +pip install -r requirements.txt diff --git a/azure/logs/empty.txt b/azure/logs/empty.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/azure/logs/empty.txt diff --git a/azure/manage.py b/azure/manage.py new file mode 100644 index 0000000..4a98417 --- /dev/null +++ b/azure/manage.py @@ -0,0 +1,20 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +import os +import sys + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "azure.settings") + +if __name__ == "__main__": + from django.core.management import execute_from_command_line + execute_from_command_line(sys.argv) diff --git a/azure/pom.xml b/azure/pom.xml new file mode 100644 index 0000000..42740aa --- /dev/null +++ b/azure/pom.xml @@ -0,0 +1,61 @@ +<?xml version="1.0"?> +<!-- + Copyright (c) 2018 Amdocs + + 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. + --> +<project + xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <parent> + <groupId>org.onap.oparent</groupId> + <artifactId>oparent</artifactId> + <version>1.1.0</version> + <relativePath>../oparent</relativePath> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>org.onap.multicloud.azure</groupId> + <artifactId>multicloud-azure</artifactId> + <version>1.2.0-SNAPSHOT</version> + <packaging>pom</packaging> + <name>multicloud-azure</name> + <description>multicloud azure</description> + <properties> + <encoding>UTF-8</encoding> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> + <nexusproxy>https://nexus.onap.org</nexusproxy> + </properties> + <build> + <plugins> + <plugin> + <artifactId>maven-assembly-plugin</artifactId> + <configuration> + <appendAssemblyId>false</appendAssemblyId> + <descriptors> + <descriptor>assembly.xml</descriptor> + </descriptors> + </configuration> + <executions> + <execution> + <id>make-assembly</id> + <phase>package</phase> + <goals> + <goal>single</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + + </build> +</project> diff --git a/azure/requirements.txt b/azure/requirements.txt new file mode 100644 index 0000000..aa98578 --- /dev/null +++ b/azure/requirements.txt @@ -0,0 +1,39 @@ +# rest framework +Django==1.9.6 +djangorestframework==3.3.3 + +# redis cache +redis==2.10.5 + +# for access redis cache +redisco==0.1.4 +django-redis-cache==0.13.1 + +# for call rest api +httplib2==0.9.2 + +# for call openstack api +# openstacksdk==0.9.15 +# os-client-config==1.29.0 +# python-cinderclient==3.5.0 + +# for unit test +django-nose>=1.4.0 +coverage==4.2 +mock==2.0.0 +unittest_xml_reporting==1.12.0 + +# for onap logging +onappylog>=1.0.6 + +# for event +oslo_messaging + +# for pecan framework +uwsgi +pecan>=1.2.1 +oslo.concurrency>=3.21.0 +oslo.config>=4.11.0 +oslo.service>=1.25.0 +eventlet>=0.20.0 +PyYAML>=3.1.0 diff --git a/azure/run.sh b/azure/run.sh new file mode 100644 index 0000000..6883739 --- /dev/null +++ b/azure/run.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# Copyright (c) 2018 Amdocs +# +# 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. + +sed -i "s/MSB_SERVICE_IP =.*/MSB_SERVICE_IP = \"${MSB_ADDR}\"/g" azure/pub/config/config.py +sed -i "s/MSB_SERVICE_PORT =.*/MSB_SERVICE_PORT = \"${MSB_PORT}\"/g" azure/pub/config/config.py +sed -i "s/AAI_ADDR =.*/AAI_ADDR = \"${AAI_ADDR}\"/g" azure/pub/config/config.py +sed -i "s/AAI_PORT =.*/AAI_PORT = \"${AAI_PORT}\"/g" azure/pub/config/config.py +sed -i "s/AAI_SCHEMA_VERSION =.*/AAI_SCHEMA_VERSION = \"${AAI_SCHEMA_VERSION}\"/g" azure/pub/config/config.py +sed -i "s/AAI_USERNAME =.*/AAI_USERNAME = \"${AAI_USERNAME}\"/g" azure/pub/config/config.py +sed -i "s/AAI_PASSWORD =.*/AAI_PASSWORD = \"${AAI_PASSWORD}\"/g" azure/pub/config/config.py +sed -i "s/MR_ADDR =.*/MR_ADDR = \"${MR_ADDR}\"/g" azure/pub/config/config.py +sed -i "s/MR_PORT =.*/MR_PORT = \"${MR_PORT}\"/g" azure/pub/config/config.py + + +logDir="/var/log/onap/multicloud/azure" + +if [ "$WEB_FRAMEWORK" == "pecan" ] +then + python multivimbroker/scripts/api.py +else + # nohup python manage.py runserver 0.0.0.0:9008 2>&1 & + nohup uwsgi --http :9008 --module azure.wsgi --master --processes 4 & + nohup python -m azure.event_listener.server 2>&1 & + + while [ ! -f $logDir/azure.log ]; do + sleep 1 + done +tail -F $logDir/azure.log +fi diff --git a/azure/setup.py b/azure/setup.py new file mode 100644 index 0000000..c81e04f --- /dev/null +++ b/azure/setup.py @@ -0,0 +1,23 @@ +# Copyright (c) 2018 Amdocs +# +# 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. + +import setuptools + +setuptools.setup( + name="azure", + version="1.0", + packages=setuptools.find_packages(), + include_package_data=True, + zip_safe=True +) diff --git a/azure/stop.sh b/azure/stop.sh new file mode 100644 index 0000000..04e822a --- /dev/null +++ b/azure/stop.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Copyright (c) 2018 Amdocs +# +# 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. + +# ps auxww | grep 'manage.py runserver 0.0.0.0:9004' | awk '{print $2}' | xargs kill -9 +ps auxww |grep 'uwsgi --http :9004 --module azure.wsgi --master' |awk '{print $2}' |xargs kill -9 diff --git a/azure/tox.ini b/azure/tox.ini new file mode 100644 index 0000000..d6559ab --- /dev/null +++ b/azure/tox.ini @@ -0,0 +1,28 @@ +[tox] +envlist = py27,pep8 +skipsdist = true + +[tox:jenkins] +downloadcache = ~/cache/pip + +[testenv] +deps = -r{toxinidir}/requirements.txt +commands = + /usr/bin/find . -type f -name "*.py[c|o]" -delete + python manage.py test azure + +[testenv:pep8] +deps=flake8 +commands=flake8 + +[testenv:py27] +commands = + {[testenv]commands} + +[testenv:cover] +setenv= + DJANGO_SETTINGS_MODULE = azure.settings-cover +commands = + coverage erase + {[testenv]commands} + coverage xml -i
\ No newline at end of file diff --git a/azure/version.properties b/azure/version.properties new file mode 100644 index 0000000..831b951 --- /dev/null +++ b/azure/version.properties @@ -0,0 +1,27 @@ +# +# Copyright (c) 2018 Amdocs +# +# 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. +# + +# Versioning variables +# Note that these variables cannot be structured (e.g. : version.release or version.snapshot etc... ) +# because they are used in Jenkins, whose plug-in doesn't support + +major=1 +minor=2 +patch=0 + +base_version=${major}.${minor}.${patch} + +# Release must be completed with git revision # in Jenkins +release_version=${base_version} +snapshot_version=${base_version}-SNAPSHOT @@ -0,0 +1,99 @@ +<?xml version="1.0"?> +<!-- + Copyright (c) 2018 Amdocs + + 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. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <parent> + <groupId>org.onap.oparent</groupId> + <artifactId>oparent</artifactId> + <version>1.1.0</version> + <relativePath>../oparent</relativePath> + </parent> + <modelVersion>4.0.0</modelVersion> + <groupId>org.onap.multicloud.azure</groupId> + <artifactId>multicloud-azure</artifactId> + <version>1.2.0-SNAPSHOT</version> + <packaging>pom</packaging> + <name>multicloud-azure</name> + <description>multicloud azure</description> + <properties> + <encoding>UTF-8</encoding> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> + <nexusproxy>https://nexus.onap.org</nexusproxy> + + <sonar.sourceEncoding>UTF-8</sonar.sourceEncoding> + <sonar.sources>.</sonar.sources> + <sonar.junit.reportsPath>xunit-results.xml</sonar.junit.reportsPath> + <sonar.python.coverage.reportPath>azure/coverage.xml</sonar.python.coverage.reportPath> + <sonar.language>py</sonar.language> + <sonar.pluginName>Python</sonar.pluginName> + <sonar.inclusions>**/*.py</sonar.inclusions> + <sonar.exclusions>**/tests/*,setup.py</sonar.exclusions> + </properties> + <build> + <pluginManagement> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>exec-maven-plugin</artifactId> + <version>1.2.1</version> + <configuration> + <executable>${session.executionRootDirectory}/sonar.sh</executable> + <environmentVariables> + <!-- make mvn properties as env for our script --> + <MVN_PROJECT_GROUPID>${project.groupId}</MVN_PROJECT_GROUPID> + <MVN_PROJECT_ARTIFACTID>${project.artifactId}</MVN_PROJECT_ARTIFACTID> + <MVN_PROJECT_VERSION>${project.version}</MVN_PROJECT_VERSION> + </environmentVariables> + </configuration> + </plugin> + </plugins> + </pluginManagement> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>exec-maven-plugin</artifactId> + <version>1.2.1</version> + <executions> + <execution> + <id>clean phase script</id> + <phase>clean</phase> + <goals> + <goal>exec</goal> + </goals> + <configuration> + <arguments> + <argument>__</argument> + <argument>clean</argument> + </arguments> + </configuration> + </execution> + <execution> + <id>test script</id> + <phase>test</phase> + <goals> + <goal>exec</goal> + </goals> + <configuration> + <arguments> + <argument>__</argument> + <argument>test</argument> + </arguments> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/sonar.sh b/sonar.sh new file mode 100755 index 0000000..25cc44c --- /dev/null +++ b/sonar.sh @@ -0,0 +1,83 @@ +#!/bin/bash +# Copyright 2018 Amdocs +# +# 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. + + +set -e + +echo "running script: [$0] for module [$1] at stage [$2]" + +export SETTINGS_FILE=${SETTINGS_FILE:-$HOME/.m2/settings.xml} +MVN_PROJECT_MODULEID="$1" +MVN_PHASE="$2" + + +FQDN="${MVN_PROJECT_GROUPID}.${MVN_PROJECT_ARTIFACTID}" +if [ "$MVN_PROJECT_MODULEID" == "__" ]; then + MVN_PROJECT_MODULEID="" +fi + +if [ -z "$WORKSPACE" ]; then + WORKSPACE=$(pwd) +fi + +# mvn phase in life cycle +MVN_PHASE="$2" + + +echo "MVN_PROJECT_MODULEID is [$MVN_PROJECT_MODULEID]" +echo "MVN_PHASE is [$MVN_PHASE]" +echo "MVN_PROJECT_GROUPID is [$MVN_PROJECT_GROUPID]" +echo "MVN_PROJECT_ARTIFACTID is [$MVN_PROJECT_ARTIFACTID]" +echo "MVN_PROJECT_VERSION is [$MVN_PROJECT_VERSION]" + +run_tox_test() +{ + set -x + cd azure + CURDIR=$(pwd) + TOXINIS=$(find . -name "tox.ini") + cd .. + for TOXINI in "${TOXINIS[@]}"; do + DIR=$(echo "$TOXINI" | rev | cut -f2- -d'/' | rev) + cd "${CURDIR}/${DIR}" + rm -rf ./venv-tox ./.tox + virtualenv ./venv-tox + source ./venv-tox/bin/activate + pip install --upgrade pip + pip install --upgrade tox argparse + pip freeze + cd azure + tox -e cover + deactivate + cd .. + rm -rf ./venv-tox ./.tox + done +} + + +case $MVN_PHASE in +clean) + echo "==> clean phase script" + rm -rf ./venv-* + ;; +test) + echo "==> test phase script" + run_tox_test + ;; +*) + echo "==> unprocessed phase" + ;; +esac + diff --git a/version.properties b/version.properties new file mode 100644 index 0000000..831b951 --- /dev/null +++ b/version.properties @@ -0,0 +1,27 @@ +# +# Copyright (c) 2018 Amdocs +# +# 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. +# + +# Versioning variables +# Note that these variables cannot be structured (e.g. : version.release or version.snapshot etc... ) +# because they are used in Jenkins, whose plug-in doesn't support + +major=1 +minor=2 +patch=0 + +base_version=${major}.${minor}.${patch} + +# Release must be completed with git revision # in Jenkins +release_version=${base_version} +snapshot_version=${base_version}-SNAPSHOT |