Add support for logical networks

This commit is contained in:
Alexandru Coman 2017-01-30 22:06:15 +02:00
parent 52f8d3e54e
commit 488af328cf
No known key found for this signature in database
GPG Key ID: A7B6A9021F704507
8 changed files with 485 additions and 9 deletions

View File

@ -44,6 +44,23 @@ class _BaseHNVModel(model.Model):
the context of the resource if it is a top-level resource, or in the the context of the resource if it is a top-level resource, or in the
context of the direct parent resource if it is a child resource.""" context of the direct parent resource if it is a child resource."""
parent_id = model.Field(name="parent_id",
key="parentResourceID",
is_property=False, is_required=False,
is_read_only=True)
"""The parent resource ID field contains the resource ID that is
associated with network objects that are ancestors of the necessary
resource.
"""
grandparent_id = model.Field(name="grandparent_id",
key="grandParentResourceID",
is_property=False, is_required=False,
is_read_only=True)
"""The grand parent resource ID field contains the resource ID that
is associated with network objects that are ancestors of the parent
of the necessary resource."""
instance_id = model.Field(name="instance_id", key="instanceId", instance_id = model.Field(name="instance_id", key="instanceId",
is_property=False) is_property=False)
"""The globally unique Id generated and used internally by the Network """The globally unique Id generated and used internally by the Network
@ -63,10 +80,6 @@ class _BaseHNVModel(model.Model):
"""Indicates the various states of the resource. Valid values are """Indicates the various states of the resource. Valid values are
Deleting, Failed, Succeeded, and Updating.""" Deleting, Failed, Succeeded, and Updating."""
def __init__(self, **fields):
self._parent_id = fields.pop("parent_id", None)
super(_BaseHNVModel, self).__init__(**fields)
@staticmethod @staticmethod
def _get_client(): def _get_client():
"""Create a new client for the HNV REST API.""" """Create a new client for the HNV REST API."""
@ -76,11 +89,6 @@ class _BaseHNVModel(model.Model):
allow_insecure=CONFIG.HNV.https_allow_insecure, allow_insecure=CONFIG.HNV.https_allow_insecure,
ca_bundle=CONFIG.HNV.https_ca_bundle) ca_bundle=CONFIG.HNV.https_ca_bundle)
@property
def parent_id(self):
"""The identifier for the specific ancestor resource."""
return self._parent_id
@classmethod @classmethod
def get(cls, resource_id=None, parent_id=None): def get(cls, resource_id=None, parent_id=None):
"""Retrieves the required resources. """Retrieves the required resources.
@ -187,3 +195,178 @@ class _BaseHNVModel(model.Model):
self._set_fields(fields) self._set_fields(fields)
# Lock the current model # Lock the current model
self._provision_done = True self._provision_done = True
class Resource(model.Model):
"""Model for the resource references."""
resource_ref = model.Field(name="resource_ref", key="resourceRef",
is_property=False, is_required=True)
"""A relative URI to an associated resource."""
class IPPools(_BaseHNVModel):
"""Model for IP Pools.
The ipPools resource represents the range of IP addresses from which IP
addresses will be allocated for nodes within a subnet. The subnet is a
logical or physical subnet inside a logical network.
The ipPools for a virtual subnet are implicit. The start and end IP
addresses of the pool of the virtual subnet is based on the IP prefix
of the virtual subnet.
"""
_endpoint = ("/networking/v1/logicalNetworks/{grandparent_id}"
"/logicalSubnets/{parent_id}/ipPools/{resource_id}")
parent_id = model.Field(name="parent_id",
key="parentResourceID",
is_property=False, is_required=True,
is_read_only=True)
"""The parent resource ID field contains the resource ID that is
associated with network objects that are ancestors of the necessary
resource.
"""
grandparent_id = model.Field(name="grandparent_id",
key="grandParentResourceID",
is_property=False, is_required=True,
is_read_only=True)
"""The grand parent resource ID field contains the resource ID that
is associated with network objects that are ancestors of the parent
of the necessary resource."""
start_ip_address = model.Field(name="start_ip_address",
key="startIpAddress",
is_required=True, is_read_only=False)
"""Start IP address of the pool.
Note: This is an inclusive value so it is a valid IP address from
this pool."""
end_ip_address = model.Field(name="end_ip_address", key="endIpAddress",
is_required=True, is_read_only=False)
"""End IP address of the pool.
Note: This is an inclusive value so it is a valid IP address from
this pool."""
usage = model.Field(name="usage", key="usage",
is_required=False, is_read_only=True)
"""Statistics of the usage of the IP pool."""
class LogicalSubnetworks(_BaseHNVModel):
"""Logical subnetworks model.
The logicalSubnets resource consists of a subnet/VLAN pair.
The vlan resource is required; however it MAY contain a value of zero
if the subnet is not associated with a vlan.
"""
_endpoint = ("/networking/v1/logicalNetworks/{parent_id}"
"/logicalSubnets/{resource_id}")
parent_id = model.Field(name="parent_id",
key="parentResourceID",
is_property=False, is_required=True,
is_read_only=True)
"""The parent resource ID field contains the resource ID that is
associated with network objects that are ancestors of the necessary
resource.
"""
address_prefix = model.Field(name="address_prefix", key="addressPrefix")
"""Identifies the subnet id in form of ipAddresss/prefixlength."""
vlan_id = model.Field(name="vlan_id", key="vlanId", is_required=True,
default=0)
"""Indicates the VLAN ID associated with the logical subnet."""
routes = model.Field(name="routes", key="routes", is_required=False)
"""Indicates the routes that are contained in the logical subnet."""
ip_pools = model.Field(name="ip_pools", key="ipPools",
is_required=False)
"""Indicates the IP Pools that are contained in the logical subnet."""
dns_servers = model.Field(name="dns_servers", key="dnsServers",
is_required=False)
"""Indicates one or more DNS servers that are used for resolving DNS
queries by devices or host connected to this logical subnet."""
network_interfaces = model.Field(name="network_interfaces",
key="networkInterfaces",
is_read_only=True)
"""Indicates an array of references to networkInterfaces resources
that are attached to the logical subnet."""
is_public = model.Field(name="is_public", key="isPublic")
"""Boolean flag specifying whether the logical subnet is a
public subnet."""
default_gateways = model.Field(name="default_gateways",
key="defaultGateways")
"""A collection of one or more gateways for the subnet."""
@classmethod
def from_raw_data(cls, raw_data):
"""Create a new model using raw API response."""
ip_pools = []
properties = raw_data["properties"]
for raw_ip_pool in properties.get("ipPools", []):
raw_ip_pool["parentResourceID"] = raw_data["resourceId"]
raw_ip_pool["grandParentResourceID"] = raw_data["parentResourceID"]
ip_pools.append(IPPools.from_raw_data(raw_ip_pool))
properties["ipPools"] = ip_pools
return super(LogicalSubnetworks, cls).from_raw_data(raw_data)
class LogicalNetworks(_BaseHNVModel):
"""Logical networks model.
The logicalNetworks resource represents a logical partition of physical
network that is dedicated for a specific purpose.
A logical network comprises of a collection of logical subnets.
"""
_endpoint = "/networking/v1/logicalNetworks/{resource_id}"
subnetworks = model.Field(name="subnetworks", key="subnets",
is_required=False, default=[])
"""Indicates the subnets that are contained in the logical network."""
network_virtualization_enabled = model.Field(
name="network_virtualization_enabled",
key="networkVirtualizationEnabled", default=False, is_required=False)
"""Indicates if the network is enabled to be the Provider Address network
for one or more virtual networks. Valid values are `True` or `False`.
The default is `False`."""
virtual_networks = model.Field(name="virtual_networks",
key="virtualNetworks",
is_read_only=True)
"""Indicates an array of virtualNetwork resources that are using
the network."""
@classmethod
def from_raw_data(cls, raw_data):
"""Create a new model using raw API response."""
properties = raw_data["properties"]
subnetworks = []
for raw_subnet in properties.get("subnets", []):
raw_subnet["parentResourceID"] = raw_data["resourceId"]
subnetworks.append(LogicalSubnetworks.from_raw_data(raw_subnet))
properties["subnets"] = subnetworks
virtual_networks = []
for raw_network in properties.get("virtualNetworks", []):
virtual_networks.append(Resource.from_raw_data(raw_network))
properties["virtualNetworks"] = virtual_networks
return super(LogicalNetworks, cls).from_raw_data(raw_data)

View File

View File

@ -0,0 +1,47 @@
# Copyright 2017 Cloudbase Solutions Srl
#
# 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.
"""This module contains fake HVN API response."""
import json
import pkg_resources
class FakeResponse(object):
"""HNV API fake responses."""
def __init__(self):
self._resources = "hnv_client.tests.fake.response"
self._cache = {}
def _load_resource(self, resource):
"""Load the json response for the required resource."""
if resource not in self._cache:
json_response = pkg_resources.resource_stream(
self._resources, resource)
self._cache[resource] = json.load(json_response)
return self._cache[resource]
def logical_networks(self):
"""Fake GET(all) response for logical networks."""
return self._load_resource("logical_networks.json")
def logical_subnets(self):
"""Fake GET(all) response for logical subnets."""
return self._load_resource("logical_subnets.json")
def ip_pools(self):
"""Fake GET(all) response for IP pools."""
return self._load_resource("ip_pools.json")

View File

@ -0,0 +1,51 @@
{
"value": [
{
"resourceId": "{uniqueString}",
"etag": "00000000-0000-0000-0000-000000000000",
"instanceId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"tags": {
"key": "value"
},
"resourceMetadata": {
"client": "<Insert likely client>",
"tenantId": "{subscriptionid}",
"groupId": "{groupname}",
"name": "{name}",
"originalHref": "https://..."
},
"properties": {
"provisioningState": "Updating|Deleting|Failed|Succeeded",
"ipConfigurations": [],
"networkInterfaces": [],
"vlanID": "1",
"routes": [],
"dnsServers": [
"10.0.0.1",
"10.0.0.2"
],
"defaultGateways": [
"192.168.1.1",
"192.168.1.2"
],
"isPublic": true,
"ipPools": []
}
},
{
"resourceId": "{uniqueString}",
"etag": "00000000-0000-0000-0000-000000000000",
"instanceId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"tags": {
"key": "value"
},
"resourceMetadata": {
"client": "<Insert likely client>",
"tenantId": "{subscriptionid}",
"groupId": "{groupname}",
"name": "{name}",
"originalHref": "https://..."
}
}
]
}

View File

@ -0,0 +1,75 @@
{
"value": [
{
"resourceRef": "/logicalnetworks/72570539-58a9-43d6-b858-d7ec3f202c6d",
"resourceId": "72570539-58a9-43d6-b858-d7ec3f202c6d",
"etag": "W/\"34b565dc-c69e-4165-97ea-6e8ef6c84420\"",
"instanceId": "b75b250f-f2d1-4a2f-bb2e-57380523b407",
"properties": {
"provisioningState": "Succeeded",
"subnets": [
{
"resourceRef": "/logicalnetworks/72570539-58a9-43d6-b858-d7ec3f202c6d/subnets/3d46ae72-b1d0-48fa-b4fe-ab183e737493",
"resourceId": "3d46ae72-b1d0-48fa-b4fe-ab183e737493",
"etag": "W/\"34b565dc-c69e-4165-97ea-6e8ef6c84420\"",
"instanceId": "78c262d9-de13-4f33-a564-5f168b38a573",
"properties": {
"provisioningState": "Succeeded",
"addressPrefix": "192.83.0.0/16",
"ipConfigurations": [],
"networkInterfaces": [
{
"resourceRef": "/servers/27-3145F0416/networkInterfaces/ab055aa1-27d6-4a2e-a4b7-7916008dd1a4"
}
],
"gatewayPools": [],
"networkConnections": [],
"vlanID": "109",
"ipPools": [
{
"resourceRef": "/logicalnetworks/72570539-58a9-43d6-b858-d7ec3f202c6d/subnets/3d46ae72-b1d0-48fa-b4fe-ab183e737493/ipPools/66ce16cb-7c9e-4666-b6b4-41208a497604",
"resourceId": "66ce16cb-7c9e-4666-b6b4-41208a497604",
"etag": "W/\"34b565dc-c69e-4165-97ea-6e8ef6c84420\"",
"instanceId": "0d68218b-50dc-4cc9-bb36-66324e93b407",
"properties": {
"provisioningState": "Succeeded",
"startIpAddress": "192.83.0.100",
"endIpAddress": "192.83.255.255"
}
},
{
"resourceRef": "/logicalnetworks/72570539-58a9-43d6-b858-d7ec3f202c6d/subnets/3d46ae72-b1d0-48fa-b4fe-ab183e737493/ipPools/small",
"resourceId": "small",
"etag": "W/\"34b565dc-c69e-4165-97ea-6e8ef6c84420\"",
"instanceId": "581b56e7-dfb2-4fc1-833c-1aaf970c91e6",
"properties": {
"provisioningState": "Succeeded",
"startIpAddress": "192.83.0.90",
"endIpAddress": "192.83.0.98"
}
}
],
"dnsServers": [],
"defaultGateways": [
"192.83.0.1"
],
"isPublic": false,
"usage": {
"numberOfIPAddresses": 65445,
"numberofIPAddressesAllocated": 2,
"numberOfIPAddressesInTransition": 0
}
}
}
],
"virtualNetworks": [
{
"resourceRef": "/virtualNetworks/fcfc99f9-50ce-4644-8a47-a23711c3b704"
}
],
"networkVirtualizationEnabled": "True"
}
}
],
"nextLink": ""
}

View File

@ -0,0 +1,68 @@
{
"value": [
{
"resourceId": "{uniqueString}",
"etag": "00000000-0000-0000-0000-000000000000",
"instanceId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"tags": {
"key": "value"
},
"resourceMetadata": {
"client": "<Insert likely client>",
"tenantId": "{subscriptionid}",
"groupId": "{groupname}",
"name": "{name}",
"originalHref": "https://..."
},
"properties": {
"provisioningState": "Updating",
"ipConfigurations": [],
"networkInterfaces": [],
"vlanID": "1",
"routes": [],
"dnsServers": [
"10.0.0.1",
"10.0.0.2"
],
"defaultGateways": [
"192.168.1.1",
"192.168.1.2"
],
"isPublic": true,
"ipPools": []
}
},
{
"resourceId": "{uniqueString}",
"etag": "00000000-0000-0000-0000-000000000000",
"instanceId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"tags": {
"key": "value"
},
"resourceMetadata": {
"client": "<Insert likely client>",
"tenantId": "{subscriptionid}",
"groupId": "{groupname}",
"name": "{name}",
"originalHref": "https://..."
},
"properties": {
"provisioningState": "Succeeded",
"ipConfigurations": [],
"networkInterfaces": [],
"vlanID": "1",
"routes": [],
"dnsServers": [
"10.0.0.1",
"10.0.0.2"
],
"defaultGateways": [
"192.168.1.1",
"192.168.1.2"
],
"isPublic": true,
"ipPools": []
}
}
]
}

View File

@ -15,6 +15,7 @@
# pylint: disable=protected-access # pylint: disable=protected-access
import unittest import unittest
try: try:
import unittest.mock as mock import unittest.mock as mock
except ImportError: except ImportError:
@ -24,6 +25,8 @@ from hnv_client import client
from hnv_client.common import constant from hnv_client.common import constant
from hnv_client.common import exception from hnv_client.common import exception
from hnv_client import config as hnv_config from hnv_client import config as hnv_config
from hnv_client.tests.fake import fake_response
from hnv_client.tests import utils as test_utils
CONFIG = hnv_config.CONFIG CONFIG = hnv_config.CONFIG
@ -163,3 +166,52 @@ class TestBaseHNVModel(unittest.TestCase):
def test_commit_invalid_response(self): def test_commit_invalid_response(self):
self._test_commit(loop_count=1, timeout=False, self._test_commit(loop_count=1, timeout=False,
failed=False, invalid_response=True) failed=False, invalid_response=True)
class TestClient(unittest.TestCase):
def setUp(self):
self._response = fake_response.FakeResponse()
def _test_get_resource(self, model, raw_data):
with test_utils.LogSnatcher("hnv_client.client") as logging:
model.from_raw_data(raw_data)
self.assertEqual(logging.output, [])
def test_logical_networks(self):
resources = self._response.logical_networks()
for raw_data in resources.get("value", []):
self._test_get_resource(model=client.LogicalNetworks,
raw_data=raw_data)
def test_logical_network_structure(self):
raw_data = self._response.logical_networks()["value"][0]
logical_network = client.LogicalNetworks.from_raw_data(raw_data)
for logical_subnetwork in logical_network.subnetworks:
self.assertIsInstance(logical_subnetwork,
client.LogicalSubnetworks)
for virtual_network in logical_network.virtual_networks:
self.assertIsInstance(virtual_network, client.Resource)
def test_logical_subnets(self):
resources = self._response.logical_subnets()
for raw_data in resources.get("value", []):
self._test_get_resource(model=client.LogicalSubnetworks,
raw_data=raw_data)
def test_logical_subnets_structure(self):
raw_data = self._response.logical_subnets()["value"][0]
logical_subnetwork = client.LogicalSubnetworks.from_raw_data(raw_data)
for ip_pool in logical_subnetwork.ip_pools:
self.assertIsInstance(ip_pool, client.IPPools)
def test_ip_pools(self):
resources = self._response.ip_pools()
for raw_data in resources.get("value", []):
raw_data["parentResourceID"] = "{uniqueString}"
raw_data["grandParentResourceID"] = "{uniqueString}"
self._test_get_resource(model=client.IPPools,
raw_data=raw_data)