diff --git a/hnv_client/client.py b/hnv_client/client.py index 526e2b4..10c326f 100644 --- a/hnv_client/client.py +++ b/hnv_client/client.py @@ -44,6 +44,23 @@ class _BaseHNVModel(model.Model): 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.""" + 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", is_property=False) """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 Deleting, Failed, Succeeded, and Updating.""" - def __init__(self, **fields): - self._parent_id = fields.pop("parent_id", None) - super(_BaseHNVModel, self).__init__(**fields) - @staticmethod def _get_client(): """Create a new client for the HNV REST API.""" @@ -76,11 +89,6 @@ class _BaseHNVModel(model.Model): allow_insecure=CONFIG.HNV.https_allow_insecure, ca_bundle=CONFIG.HNV.https_ca_bundle) - @property - def parent_id(self): - """The identifier for the specific ancestor resource.""" - return self._parent_id - @classmethod def get(cls, resource_id=None, parent_id=None): """Retrieves the required resources. @@ -187,3 +195,178 @@ class _BaseHNVModel(model.Model): self._set_fields(fields) # Lock the current model 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) diff --git a/hnv_client/tests/fake/__init__.py b/hnv_client/tests/fake/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hnv_client/tests/fake/fake_response.py b/hnv_client/tests/fake/fake_response.py new file mode 100644 index 0000000..27be09e --- /dev/null +++ b/hnv_client/tests/fake/fake_response.py @@ -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") diff --git a/hnv_client/tests/fake/response/__init__.py b/hnv_client/tests/fake/response/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hnv_client/tests/fake/response/ip_pools.json b/hnv_client/tests/fake/response/ip_pools.json new file mode 100644 index 0000000..34b9ab9 --- /dev/null +++ b/hnv_client/tests/fake/response/ip_pools.json @@ -0,0 +1,51 @@ +{ + "value": [ + { + "resourceId": "{uniqueString}", + "etag": "00000000-0000-0000-0000-000000000000", + "instanceId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "tags": { + "key": "value" + }, + "resourceMetadata": { + "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": "", + "tenantId": "{subscriptionid}", + "groupId": "{groupname}", + "name": "{name}", + "originalHref": "https://..." + } + } + ] +} \ No newline at end of file diff --git a/hnv_client/tests/fake/response/logical_networks.json b/hnv_client/tests/fake/response/logical_networks.json new file mode 100644 index 0000000..155a324 --- /dev/null +++ b/hnv_client/tests/fake/response/logical_networks.json @@ -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": "" +} diff --git a/hnv_client/tests/fake/response/logical_subnets.json b/hnv_client/tests/fake/response/logical_subnets.json new file mode 100644 index 0000000..cb6ffbe --- /dev/null +++ b/hnv_client/tests/fake/response/logical_subnets.json @@ -0,0 +1,68 @@ +{ + "value": [ + { + "resourceId": "{uniqueString}", + "etag": "00000000-0000-0000-0000-000000000000", + "instanceId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "tags": { + "key": "value" + }, + "resourceMetadata": { + "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": "", + "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": [] + } + } + ] +} diff --git a/hnv_client/tests/test_client.py b/hnv_client/tests/test_client.py index bae8aef..4e39978 100644 --- a/hnv_client/tests/test_client.py +++ b/hnv_client/tests/test_client.py @@ -15,6 +15,7 @@ # pylint: disable=protected-access import unittest + try: import unittest.mock as mock except ImportError: @@ -24,6 +25,8 @@ from hnv_client import client from hnv_client.common import constant from hnv_client.common import exception 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 @@ -163,3 +166,52 @@ class TestBaseHNVModel(unittest.TestCase): def test_commit_invalid_response(self): self._test_commit(loop_count=1, timeout=False, 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)