feature 8029 change RO to python3. Using vim plugins
Change-Id: I1e7bf61db9c39c66e0233c81bd8b4caa6650d389
Signed-off-by: tierno <alfonso.tiernosepulveda@telefonica.com>
diff --git a/RO-VIM-azure/Makefile b/RO-VIM-azure/Makefile
new file mode 100644
index 0000000..d5b779a
--- /dev/null
+++ b/RO-VIM-azure/Makefile
@@ -0,0 +1,25 @@
+##
+# 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.
+##
+
+all: clean package
+
+clean:
+ rm -rf dist deb_dist osm_rovim_azure-*.tar.gz osm_rovim_azure.egg-info .eggs
+
+package:
+ python3 setup.py --command-packages=stdeb.command sdist_dsc
+ cp debian/python3-osm-rovim-azure.postinst deb_dist/osm-rovim-azure*/debian/
+ cd deb_dist/osm-rovim-azure*/ && dpkg-buildpackage -rfakeroot -uc -us
+
diff --git a/RO-VIM-azure/debian/python3-osm-rovim-azure.postinst b/RO-VIM-azure/debian/python3-osm-rovim-azure.postinst
new file mode 100755
index 0000000..ebb69b1
--- /dev/null
+++ b/RO-VIM-azure/debian/python3-osm-rovim-azure.postinst
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+##
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact with: OSM_TECH@list.etsi.org
+##
+
+echo "POST INSTALL OSM-ROVIM-AZURE"
+
+#Pip packages required for azure connector
+python3 -m pip install azure
+
diff --git a/RO-VIM-azure/osm_rovim_azure/vimconn_azure.py b/RO-VIM-azure/osm_rovim_azure/vimconn_azure.py
new file mode 100755
index 0000000..0cc143f
--- /dev/null
+++ b/RO-VIM-azure/osm_rovim_azure/vimconn_azure.py
@@ -0,0 +1,495 @@
+# -*- coding: utf-8 -*-
+
+__author__='Sergio Gonzalez'
+__date__ ='$18-apr-2019 23:59:59$'
+
+from osm_ro import vimconn
+import logging
+
+from os import getenv
+from uuid import uuid4
+
+from azure.common.credentials import ServicePrincipalCredentials
+from azure.mgmt.resource import ResourceManagementClient
+from azure.mgmt.network import NetworkManagementClient
+from azure.mgmt.compute import ComputeManagementClient
+
+
+class vimconnector(vimconn.vimconnector):
+
+ def __init__(self, uuid, name, tenant_id, tenant_name, url, url_admin=None, user=None, passwd=None, log_level=None,
+ config={}, persistent_info={}):
+
+ vimconn.vimconnector.__init__(self, uuid, name, tenant_id, tenant_name, url, url_admin, user, passwd, log_level,
+ config, persistent_info)
+
+ # LOGGER
+ self.logger = logging.getLogger('openmano.vim.azure')
+ if log_level:
+ logging.basicConfig()
+ self.logger.setLevel(getattr(logging, log_level))
+
+ # CREDENTIALS
+ self.credentials = ServicePrincipalCredentials(
+ client_id=user,
+ secret=passwd,
+ tenant=(tenant_id or tenant_name)
+ )
+
+ # SUBSCRIPTION
+ if 'subscription_id' in config:
+ self.subscription_id = config.get('subscription_id')
+ self.logger.debug('Setting subscription '+str(self.subscription_id))
+ else:
+ raise vimconn.vimconnException('Subscription not specified')
+ # REGION
+ if 'region_name' in config:
+ self.region = config.get('region_name')
+ else:
+ raise vimconn.vimconnException('Azure region_name is not specified at config')
+ # RESOURCE_GROUP
+ if 'resource_group' in config:
+ self.resource_group = config.get('resource_group')
+ else:
+ raise vimconn.vimconnException('Azure resource_group is not specified at config')
+ # VNET_NAME
+ if 'vnet_name' in config:
+ self.vnet_name = config["vnet_name"]
+
+ # public ssh key
+ self.pub_key = config.get('pub_key')
+
+ def _reload_connection(self):
+ """
+ Sets connections to work with Azure service APIs
+ :return:
+ """
+ self.logger.debug('Reloading API Connection')
+ try:
+ self.conn = ResourceManagementClient(self.credentials, self.subscription_id)
+ self.conn_compute = ComputeManagementClient(self.credentials, self.subscription_id)
+ self.conn_vnet = NetworkManagementClient(self.credentials, self.subscription_id)
+ self._check_or_create_resource_group()
+ self._check_or_create_vnet()
+ except Exception as e:
+ self.format_vimconn_exception(e)
+
+ def _get_resource_name_from_resource_id(self, resource_id):
+ return str(resource_id.split('/')[-1])
+
+ def _get_location_from_resource_group(self, resource_group_name):
+ return self.conn.resource_groups.get(resource_group_name).location
+
+ def _get_resource_group_name_from_resource_id(self, resource_id):
+ return str(resource_id.split('/')[4])
+
+ def _check_subnets_for_vm(self, net_list):
+ # All subnets must belong to the same resource group and vnet
+ if len(set(self._get_resource_group_name_from_resource_id(net['id']) +
+ self._get_resource_name_from_resource_id(net['id']) for net in net_list)) != 1:
+ raise self.format_vimconn_exception('Azure VMs can only attach to subnets in same VNET')
+
+ def format_vimconn_exception(self, e):
+ """
+ Params: an Exception object
+ :param e:
+ :return: Raises the proper vimconnException
+ """
+ self.conn = None
+ self.conn_vnet = None
+ raise vimconn.vimconnConnectionException(type(e).__name__ + ': ' + str(e))
+
+ def _check_or_create_resource_group(self):
+ """
+ Creates a resource group in indicated region
+ :return: None
+ """
+ self.logger.debug('Creating RG {} in location {}'.format(self.resource_group, self.region))
+ self.conn.resource_groups.create_or_update(self.resource_group, {'location': self.region})
+
+ def _check_or_create_vnet(self):
+ try:
+ vnet_params = {
+ 'location': self.region,
+ 'address_space': {
+ 'address_prefixes': "10.0.0.0/8"
+ },
+ }
+ self.conn_vnet.virtual_networks.create_or_update(self.resource_group, self.vnet_name, vnet_params)
+ except Exception as e:
+ self.format_vimconn_exception(e)
+
+ def new_network(self, net_name, net_type, ip_profile=None, shared=False, vlan=None):
+ """
+ Adds a tenant network to VIM
+ :param net_name: name of the network
+ :param net_type:
+ :param ip_profile: is a dict containing the IP parameters of the network (Currently only IPv4 is implemented)
+ 'ip-version': can be one of ['IPv4','IPv6']
+ 'subnet-address': ip_prefix_schema, that is X.X.X.X/Y
+ 'gateway-address': (Optional) ip_schema, that is X.X.X.X
+ 'dns-address': (Optional) ip_schema,
+ 'dhcp': (Optional) dict containing
+ 'enabled': {'type': 'boolean'},
+ 'start-address': ip_schema, first IP to grant
+ 'count': number of IPs to grant.
+ :param shared:
+ :param vlan:
+ :return: a tuple with the network identifier and created_items, or raises an exception on error
+ created_items can be None or a dictionary where this method can include key-values that will be passed to
+ the method delete_network. Can be used to store created segments, created l2gw connections, etc.
+ Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same
+ as not present.
+ """
+
+ return self._new_subnet(net_name, ip_profile)
+
+ def _new_subnet(self, net_name, ip_profile):
+ """
+ Adds a tenant network to VIM. It creates a new VNET with a single subnet
+ :param net_name:
+ :param ip_profile:
+ :return:
+ """
+ self.logger.debug('Adding a subnet to VNET '+self.vnet_name)
+ self._reload_connection()
+
+ if ip_profile is None:
+ # TODO get a non used vnet ip range /24 and allocate automatically
+ raise vimconn.vimconnException('Azure cannot create VNET with no CIDR')
+
+ try:
+ vnet_params= {
+ 'location': self.region,
+ 'address_space': {
+ 'address_prefixes': [ip_profile['subnet_address']]
+ },
+ 'subnets': [
+ {
+ 'name': "{}-{}".format(net_name[:24], uuid4()),
+ 'address_prefix': ip_profile['subnet_address']
+ }
+ ]
+ }
+ self.conn_vnet.virtual_networks.create_or_update(self.resource_group, self.vnet_name, vnet_params)
+ # TODO return a tuple (subnet-ID, None)
+ except Exception as e:
+ self.format_vimconn_exception(e)
+
+ def _create_nic(self, subnet_id, nic_name, static_ip=None):
+ self._reload_connection()
+
+ resource_group_name=self._get_resource_group_name_from_resource_id(subnet_id)
+ location = self._get_location_from_resource_group(resource_group_name)
+
+ if static_ip:
+ async_nic_creation = self.conn_vnet.network_interfaces.create_or_update(
+ resource_group_name,
+ nic_name,
+ {
+ 'location': location,
+ 'ip_configurations': [{
+ 'name': nic_name + 'ipconfiguration',
+ 'privateIPAddress': static_ip,
+ 'privateIPAllocationMethod': 'Static',
+ 'subnet': {
+ 'id': subnet_id
+ }
+ }]
+ }
+ )
+ else:
+ async_nic_creation = self.conn_vnet.network_interfaces.create_or_update(
+ resource_group_name,
+ nic_name,
+ {
+ 'location': location,
+ 'ip_configurations': [{
+ 'name': nic_name + 'ipconfiguration',
+ 'subnet': {
+ 'id': subnet_id
+ }
+ }]
+ }
+ )
+
+ return async_nic_creation.result()
+
+ def get_image_list(self, filter_dict={}):
+ """
+ The urn contains for marketplace 'publisher:offer:sku:version'
+
+ :param filter_dict:
+ :return:
+ """
+ image_list = []
+
+ self._reload_connection()
+ if filter_dict.get("name"):
+ params = filter_dict["name"].split(":")
+ if len(params) >= 3:
+ publisher = params[0]
+ offer = params[1]
+ sku = params[2]
+ version = None
+ if len(params) == 4:
+ version = params[3]
+ images = self.conn_compute.virtual_machine_images.list(self.region, publisher, offer, sku)
+ for image in images:
+ if version:
+ image_version = str(image.id).split("/")[-1]
+ if image_version != version:
+ continue
+ image_list.append({
+ 'id': str(image.id),
+ 'name': self._get_resource_name_from_resource_id(image.id)
+ })
+ return image_list
+
+ images = self.conn_compute.virtual_machine_images.list()
+
+ for image in images:
+ # TODO implement filter_dict
+ if filter_dict:
+ if filter_dict.get("id") and str(image.id) != filter_dict["id"]:
+ continue
+ if filter_dict.get("name") and \
+ self._get_resource_name_from_resource_id(image.id) != filter_dict["name"]:
+ continue
+ # TODO add checksum
+ image_list.append({
+ 'id': str(image.id),
+ 'name': self._get_resource_name_from_resource_id(image.id),
+ })
+ return image_list
+
+ def get_network_list(self, filter_dict={}):
+ """Obtain tenant networks of VIM
+ Filter_dict can be:
+ name: network name
+ id: network uuid
+ shared: boolean
+ tenant_id: tenant
+ admin_state_up: boolean
+ status: 'ACTIVE'
+ Returns the network list of dictionaries
+ """
+ self.logger.debug('Getting all subnets from VIM')
+ try:
+ self._reload_connection()
+ vnet = self.conn_vnet.virtual_networks.get(self.config["resource_group"], self.vnet_name)
+ subnet_list = []
+
+ for subnet in vnet.subnets:
+ # TODO implement filter_dict
+ if filter_dict:
+ if filter_dict.get("id") and str(subnet.id) != filter_dict["id"]:
+ continue
+ if filter_dict.get("name") and \
+ self._get_resource_name_from_resource_id(subnet.id) != filter_dict["name"]:
+ continue
+
+ subnet_list.append({
+ 'id': str(subnet.id),
+ 'name': self._get_resource_name_from_resource_id(subnet.id),
+ 'status': str(vnet.provisioning_state), # TODO Does subnet contains status???
+ 'cidr_block': str(subnet.address_prefix)
+ }
+ )
+ return subnet_list
+ except Exception as e:
+ self.format_vimconn_exception(e)
+
+ def new_vminstance(self, vm_name, description, start, image_id, flavor_id, net_list, cloud_config=None,
+ disk_list=None, availability_zone_index=None, availability_zone_list=None):
+
+ return self._new_vminstance(vm_name, image_id, flavor_id, net_list)
+
+ def _new_vminstance(self, vm_name, image_id, flavor_id, net_list, cloud_config=None, disk_list=None,
+ availability_zone_index=None, availability_zone_list=None):
+ #Create NICs
+ self._check_subnets_for_vm(net_list)
+ vm_nics = []
+ for idx, net in enumerate(net_list):
+ subnet_id=net['subnet_id']
+ nic_name = vm_name + '-nic-'+str(idx)
+ vm_nic = self._create_nic(subnet_id, nic_name)
+ vm_nics.append({ 'id': str(vm_nic.id)})
+
+ try:
+ vm_parameters = {
+ 'location': self.region,
+ 'os_profile': {
+ 'computer_name': vm_name, # TODO if vm_name cannot be repeated add uuid4() suffix
+ 'admin_username': 'sergio', # TODO is it mandatory???
+ 'linuxConfiguration': {
+ 'disablePasswordAuthentication': 'true',
+ 'ssh': {
+ 'publicKeys': [
+ {
+ 'path': '/home/sergio/.ssh/authorized_keys',
+ 'keyData': self.pub_key
+ }
+ ]
+ }
+ }
+
+ },
+ 'hardware_profile': {
+ 'vm_size':flavor_id
+ },
+ 'storage_profile': {
+ 'image_reference': image_id
+ },
+ 'network_profile': {
+ 'network_interfaces': [
+ vm_nics[0]
+ ]
+ }
+ }
+ creation_result = self.conn_compute.virtual_machines.create_or_update(
+ self.resource_group,
+ vm_name,
+ vm_parameters
+ )
+
+ run_command_parameters = {
+ 'command_id': 'RunShellScript', # For linux, don't change it
+ 'script': [
+ 'date > /home/sergio/test.txt'
+ ]
+ }
+ poller = self.conn_compute.virtual_machines.run_command(
+ self.resource_group,
+ vm_name,
+ run_command_parameters
+ )
+ # TODO return a tuple (vm-ID, None)
+ except Exception as e:
+ self.format_vimconn_exception(e)
+
+ def get_flavor_id_from_data(self, flavor_dict):
+ self.logger.debug("Getting flavor id from data")
+ self._reload_connection()
+ vm_sizes_list = [vm_size.serialize() for vm_size in self.conn_compute.virtual_machine_sizes.list(self.region)]
+
+ cpus = flavor_dict['vcpus']
+ memMB = flavor_dict['ram']
+
+ filteredSizes = [size for size in vm_sizes_list if size['numberOfCores'] > cpus and size['memoryInMB'] > memMB]
+ listedFilteredSizes = sorted(filteredSizes, key=lambda k: k['numberOfCores'])
+
+ return listedFilteredSizes[0]['name']
+
+ def check_vim_connectivity(self):
+ try:
+ self._reload_connection()
+ return True
+ except Exception as e:
+ raise vimconn.vimconnException("Connectivity issue with Azure API: {}".format(e))
+
+ def get_network(self, net_id):
+ resGroup = self._get_resource_group_name_from_resource_id(net_id)
+ resName = self._get_resource_name_from_resource_id(net_id)
+
+ self._reload_connection()
+ vnet = self.conn_vnet.virtual_networks.get(resGroup, resName)
+
+ return vnet
+
+ def delete_network(self, net_id):
+ resGroup = self._get_resource_group_name_from_resource_id(net_id)
+ resName = self._get_resource_name_from_resource_id(net_id)
+
+ self._reload_connection()
+ self.conn_vnet.virtual_networks.delete(resGroup, resName)
+
+ def delete_vminstance(self, vm_id):
+ resGroup = self._get_resource_group_name_from_resource_id(net_id)
+ resName = self._get_resource_name_from_resource_id(net_id)
+
+ self._reload_connection()
+ self.conn_compute.virtual_machines.delete(resGroup, resName)
+
+ def get_vminstance(self, vm_id):
+ resGroup = self._get_resource_group_name_from_resource_id(net_id)
+ resName = self._get_resource_name_from_resource_id(net_id)
+
+ self._reload_connection()
+ vm=self.conn_compute.virtual_machines.get(resGroup, resName)
+
+ return vm
+
+ def get_flavor(self, flavor_id):
+ self._reload_connection()
+ for vm_size in self.conn_compute.virtual_machine_sizes.list(self.region):
+ if vm_size.name == flavor_id :
+ return vm_size
+
+
+# TODO refresh_nets_status ver estado activo
+# TODO refresh_vms_status ver estado activo
+# TODO get_vminstance_console for getting console
+
+if __name__ == "__main__":
+
+ # Making some basic test
+ vim_id='azure'
+ vim_name='azure'
+ needed_test_params = {
+ "client_id": "AZURE_CLIENT_ID",
+ "secret": "AZURE_SECRET",
+ "tenant": "AZURE_TENANT",
+ "resource_group": "AZURE_RESOURCE_GROUP",
+ "subscription_id": "AZURE_SUBSCRIPTION_ID",
+ "vnet_name": "AZURE_VNET_NAME",
+ }
+ test_params = {}
+
+ for param, env_var in needed_test_params.items():
+ value = getenv(env_var)
+ if not value:
+ raise Exception("Provide a valid value for env '{}'".format(env_var))
+ test_params[param] = value
+
+ config = {
+ 'region_name': getenv("AZURE_REGION_NAME", 'westeurope'),
+ 'resource_group': getenv("AZURE_RESOURCE_GROUP"),
+ 'subscription_id': getenv("AZURE_SUBSCRIPTION_ID"),
+ 'pub_key': getenv("AZURE_PUB_KEY", None),
+ 'vnet_name': getenv("AZURE_VNET_NAME", 'myNetwork'),
+ }
+
+ virtualMachine = {
+ 'name': 'sergio',
+ 'description': 'new VM',
+ 'status': 'running',
+ 'image': {
+ 'publisher': 'Canonical',
+ 'offer': 'UbuntuServer',
+ 'sku': '16.04.0-LTS',
+ 'version': 'latest'
+ },
+ 'hardware_profile': {
+ 'vm_size': 'Standard_DS1_v2'
+ },
+ 'networks': [
+ 'sergio'
+ ]
+ }
+
+ vnet_config = {
+ 'subnet_address': '10.1.2.0/24',
+ #'subnet_name': 'subnet-oam'
+ }
+ ###########################
+
+ azure = vimconnector(vim_id, vim_name, tenant_id=test_params["tenant"], tenant_name=None, url=None, url_admin=None,
+ user=test_params["client_id"], passwd=test_params["secret"], log_level=None, config=config)
+
+ # azure.get_flavor_id_from_data("here")
+ # subnets=azure.get_network_list()
+ # azure.new_vminstance(virtualMachine['name'], virtualMachine['description'], virtualMachine['status'],
+ # virtualMachine['image'], virtualMachine['hardware_profile']['vm_size'], subnets)
+
+ azure.get_flavor("Standard_A11")
diff --git a/RO-VIM-azure/requirements.txt b/RO-VIM-azure/requirements.txt
new file mode 100644
index 0000000..920d03a
--- /dev/null
+++ b/RO-VIM-azure/requirements.txt
@@ -0,0 +1,20 @@
+##
+# 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.
+##
+
+PyYAML
+requests
+netaddr
+azure
+
diff --git a/RO-VIM-azure/setup.py b/RO-VIM-azure/setup.py
new file mode 100644
index 0000000..557feda
--- /dev/null
+++ b/RO-VIM-azure/setup.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+##
+# 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 setuptools import setup
+
+_name = "osm_rovim_azure"
+
+README = """
+===========
+osm-rovim_azure
+===========
+
+osm-ro pluging for azure VIM
+"""
+
+setup(
+ name=_name,
+ description='OSM ro vim plugin for azure',
+ long_description=README,
+ version_command=('git describe --match v* --tags --long --dirty', 'pep440-git-full'),
+ # version=VERSION,
+ # python_requires='>3.5.0',
+ author='ETSI OSM',
+ author_email='alfonso.tiernosepulveda@telefonica.com',
+ maintainer='Alfonso Tierno',
+ maintainer_email='alfonso.tiernosepulveda@telefonica.com',
+ url='https://osm.etsi.org/gitweb/?p=osm/RO.git;a=summary',
+ license='Apache 2.0',
+
+ packages=[_name],
+ include_package_data=True,
+ dependency_links=["git+https://osm.etsi.org/gerrit/osm/RO.git#egg=osm-ro"],
+ install_requires=["requests", "netaddr", "PyYAML", "azure", "osm-ro"],
+ setup_requires=['setuptools-version-command'],
+ entry_points={
+ 'osm_rovim.plugins': ['rovim_azure = osm_rovim_azure.vimconn_azure'],
+ },
+)
diff --git a/RO-VIM-azure/stdeb.cfg b/RO-VIM-azure/stdeb.cfg
new file mode 100644
index 0000000..968c55e
--- /dev/null
+++ b/RO-VIM-azure/stdeb.cfg
@@ -0,0 +1,19 @@
+#
+# 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.
+#
+
+[DEFAULT]
+X-Python3-Version : >= 3.5
+Depends3: python3-requests, python3-netaddr, python3-yaml, python3-osm-ro, python3-pip
+
diff --git a/RO-VIM-azure/tox.ini b/RO-VIM-azure/tox.ini
new file mode 100644
index 0000000..9bc1472
--- /dev/null
+++ b/RO-VIM-azure/tox.ini
@@ -0,0 +1,41 @@
+##
+# 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.
+##
+
+[tox]
+envlist = py3
+toxworkdir={homedir}/.tox
+
+[testenv]
+basepython = python3
+install_command = python3 -m pip install -r requirements.txt -U {opts} {packages}
+# deps = -r{toxinidir}/test-requirements.txt
+commands=python3 -m unittest discover -v
+
+[testenv:flake8]
+basepython = python3
+deps = flake8
+commands = flake8 osm_rovim_azure --max-line-length 120 \
+ --exclude .svn,CVS,.gz,.git,__pycache__,.tox,local,temp --ignore W291,W293,E226,W504
+
+[testenv:unittest]
+basepython = python3
+commands = python3 -m unittest osm_rovim_azure.tests
+
+[testenv:build]
+basepython = python3
+deps = stdeb
+ setuptools-version-command
+commands = python3 setup.py --command-packages=stdeb.command bdist_deb
+