diff --git a/.pylintrc b/.pylintrc index 633018d..ea357cb 100644 --- a/.pylintrc +++ b/.pylintrc @@ -121,7 +121,7 @@ single-line-if-stmt=no no-space-check=trailing-comma,dict-separator # Maximum number of lines in a module -max-module-lines=2000 +max-module-lines=5000 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). @@ -394,7 +394,7 @@ max-attributes=7 min-public-methods=1 # Maximum number of public methods for a class (see R0904). -max-public-methods=20 +max-public-methods=30 # Maximum number of boolean expressions in a if statement max-bool-expr=5 diff --git a/hnv/client.py b/hnv/client.py index 6784dce..443f8b3 100644 --- a/hnv/client.py +++ b/hnv/client.py @@ -131,6 +131,33 @@ class _BaseHNVModel(model.Model): allow_insecure=CONFIG.HNV.https_allow_insecure, ca_bundle=CONFIG.HNV.https_ca_bundle) + @classmethod + def _get_all(cls, parent_id=None, grandparent_id=None): + """Retrives all the required resources.""" + client = cls._get_client() + endpoint = cls._endpoint.format(resource_id="", + parent_id=parent_id or "", + grandparent_id=grandparent_id or "") + resources = [] + while True: + raw_data = client.get_resource(endpoint) + for item in raw_data.get("value", []): + resources.append(cls.from_raw_data(item)) + endpoint = raw_data.get("nextLink") + if not endpoint: + break + return resources + + @classmethod + def _get(cls, resource_id, parent_id, grandparent_id): + """"Retrieves the required resource.""" + client = cls._get_client() + endpoint = cls._endpoint.format(resource_id=resource_id or "", + parent_id=parent_id or "", + grandparent_id=grandparent_id or "") + raw_data = client.get_resource(endpoint) + return cls.from_raw_data(raw_data) + @classmethod def get(cls, resource_id=None, parent_id=None, grandparent_id=None): """Retrieves the required resources. @@ -143,15 +170,11 @@ class _BaseHNVModel(model.Model): network objects that are ancestors of the parent of the necessary resource. """ - client = cls._get_client() - endpoint = cls._endpoint.format(resource_id=resource_id or "", - parent_id=parent_id or "", - grandparent_id=grandparent_id or "") - raw_data = client.get_resource(endpoint) - if resource_id is None: - return [cls.from_raw_data(item) for item in raw_data["value"]] + + if not resource_id: + return cls._get_all(parent_id, grandparent_id) else: - return cls.from_raw_data(raw_data) + return cls._get(resource_id, parent_id, grandparent_id) @classmethod def remove(cls, resource_id, parent_id=None, grandparent_id=None, @@ -207,7 +230,7 @@ class _BaseHNVModel(model.Model): response = client.get_resource(endpoint) self._reset_model(response) - def commit(self, wait=True, timeout=None): + def commit(self, if_match=None, wait=True, timeout=None): """Apply all the changes on the current model. :param wait: Whether to wait until the operation is completed @@ -231,7 +254,8 @@ class _BaseHNVModel(model.Model): endpoint = self._endpoint.format(resource_id=self.resource_id or "", parent_id=self.parent_id or "") request_body = self.dump(include_read_only=False) - response = client.update_resource(endpoint, data=request_body) + response = client.update_resource(endpoint, data=request_body, + if_match=if_match) elapsed_time = 0 while wait: @@ -252,6 +276,10 @@ class _BaseHNVModel(model.Model): else: self._reset_model(response) + # NOTE(alexcoman): In order to keep backwards compatibility the + # `method: commit` will return a reference to itself. + # An example for that can be the following use case: + # label = client.Model().commit() return self @classmethod @@ -573,7 +601,7 @@ class IPConfiguration(_BaseHNVModel): backend_address_pools = model.Field( name="backend_address_pools", key="loadBalancerBackendAddressPools", - is_required=False, is_read_only=True) + is_required=False, is_read_only=False) """Reference to backendAddressPools child resource of loadBalancers resource.""" @@ -1177,7 +1205,7 @@ class VirtualSwitchManager(_BaseHNVModel): is_required=False) def __init__(self, **fields): - qos_settings = fields.pop("qos_settings", {}) + qos_settings = fields.get("qos_settings", {}) if not isinstance(qos_settings, VirtualSwtichQosSettings): fields["qos_settings"] = VirtualSwtichQosSettings.from_raw_data( raw_data=qos_settings) @@ -1469,7 +1497,7 @@ class IPSecConfiguration(model.Model): @classmethod def from_raw_data(cls, raw_data): """Create a new model using raw API response.""" - raw_main = raw_data.pop("mainMode", None) + raw_main = raw_data.get("mainMode", None) if raw_main is not None: main_mode = MainMode.from_raw_data(raw_main) raw_data["mainMode"] = main_mode @@ -1838,3 +1866,682 @@ class PublicIPAddresses(_BaseHNVModel): associated. Private ip can be defined on NIC, loadBalancers, or gateways. """ + + +class BackendAddressPools(_BaseHNVModel): + + """Model for backend address pools. + + This resource represents the list of IPs that can receive network traffic + that comes via the front-end IPs. The Load Balancing MUX handles incoming + traffic via the front-end IPs and distributes them to backend IPs based + on load balancing configuration. + """ + + _endpoint = ("/networking/v1/loadBalancers/{parent_id}" + "/backendAddressPools/{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. + """ + + backend_ip_configurations = model.Field( + name="backend_ip_configurations", key="backendIPConfigurations", + is_required=False, is_read_only=False) + """Indicates an array of references to ipConfiguration Resources. + + There is no restriction on having the same IP configurations in multiple + backendAddressPools. An IpConfiguration can become a part of a + backendAddressPool by setting a reference to a backendAddressPool resource + in the loadBalancerBackendAddressPools array field on the IpConfiguration + resource. + """ + + load_balancing_rules = model.Field(name="load_balancing_rules", + key="loadBalancingRules", + is_required=False, is_read_only=False) + """Indicates an array of references to the set of loadBalancingRules + resources that use this backend address pool. + """ + + outbound_nat_rules = model.Field(name="outbound_nat_rules", + key="outboundNatRules", + is_required=False, is_read_only=False) + """Indicates an array of references to the set of outboundNatRules + resources that use this backend address pool.""" + + @classmethod + def from_raw_data(cls, raw_data): + """Create a new model using raw API response.""" + properties = raw_data.get("properties", {}) + + backend_ip_configurations = [] + for raw_content in properties.get("backendIPConfigurations", []): + resource = Resource.from_raw_data(raw_content) + backend_ip_configurations.append(resource) + properties["backendIPConfigurations"] = backend_ip_configurations + + load_balancing_rules = [] + for raw_content in properties.get("loadBalancingRules", []): + resource = Resource.from_raw_data(raw_content) + load_balancing_rules.append(resource) + properties["loadBalancingRules"] = load_balancing_rules + + outbound_nat_rules = [] + for raw_content in properties.get("outboundNatRules", []): + resource = Resource.from_raw_data(raw_content) + outbound_nat_rules.append(resource) + properties["outboundNatRules"] = outbound_nat_rules + + return super(BackendAddressPools, cls).from_raw_data(raw_data) + + +class FrontendIPConfigurations(_BaseHNVModel): + + """Model for frontend ip configurations. + + This resource represents the frontend IP addresses of the load balancer. + Either a publicIPAddress or a privateIPAddress and subnet must + be configured. + """ + + _endpoint = ("/networking/v1/loadBalancers/{parent_id}" + "/frontendIpConfigurations/{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. + """ + + inbound_nat_rules = model.Field( + name="inbound_nat_rules", key="inboundNatRules", + is_required=False, is_read_only=True) + """Indicates a reference to the inboundNatRules resource used by + the frontEndIpConfiguration.""" + + load_balancing_rules = model.Field( + name="load_balancing_rules", key="loadBalancingRules", + is_required=False, is_read_only=False) + """Indicates a reference to the loadBalancingRules resource used + by the frontEndIpConfiguration.""" + + outbound_nat_rules = model.Field( + name="outbound_nat_rules", key="outboundNatRules", + is_required=False, is_read_only=True) + """Indicates a reference to the outboundNatRules resource used by + the frontEndIpConfiguration.""" + + public_ip_address = model.Field( + name="public_ip_address", key="publicIPAddress", + is_required=False, is_read_only=False) + """Indicates a reference to the publicIPAddresses resource used by + the frontEndIpConfiguration. If a publicIPAddress is specified, + then a privateIPaddress is not specified. When a + publicIPAddress is specified, the privateIpAllocationMethod is + set to Dynamic. + """ + + private_ip_address = model.Field(name="private_ip_address", + key="privateIPAddress", + is_required=False, is_read_only=False) + """This is only specified if a specific private IP address identifies an + IP address which is statically configured for use with this + frontendIpConfiguration. + + PrivateIPAllocation method MUST be allocated static for this case. + If a privateIPAddress is specified, a reference to a publicIPaddress + cannot be specified at the same time. privateIPAddresses can be either + from the infrastructure address space or from a tenant address space, + in either case they MUST be accompanied with a valid subnet specified in + subnet element reference. + """ + + private_ip_allocation_method = model.Field( + name="private_ip_allocation_method", key="privateIPAllocationMethod", + is_required=False, is_read_only=False) + """Static or Dynamic.""" + + subnet = model.Field(name="subnet", key="subnet", + is_required=False, is_read_only=False) + """Indicates a references to the subnet resource used by the + frontendIpConfiguration resource. MUST be specified if a + privateIPaddress is specified. + A subnet reference to a logical network subnet is needed if the + privateIpAddress is from the infrastructure address space. A + subnet reference to a virtual network subnet is needed if the + privateIpAddress is from a tenant address space. + The subnet MUST include the IP address specified in + privateIpAddress. + """ + + @classmethod + def from_raw_data(cls, raw_data): + """Create a new model using raw API response.""" + properties = raw_data.get("properties", {}) + + load_balancing_rules = [] + for raw_content in properties.get("loadBalancingRules", []): + resource = Resource.from_raw_data(raw_content) + load_balancing_rules.append(resource) + properties["loadBalancingRules"] = load_balancing_rules + + inbound_nat_rules = [] + for raw_content in properties.get("inboundNatRules", []): + resource = Resource.from_raw_data(raw_content) + inbound_nat_rules.append(resource) + properties["inboundNatRules"] = inbound_nat_rules + + outbound_nat_rules = [] + for raw_content in properties.get("outboundNatRules", []): + resource = Resource.from_raw_data(raw_content) + outbound_nat_rules.append(resource) + properties["outboundNatRules"] = outbound_nat_rules + + raw_content = properties.get("subnet", None) + if raw_content is not None: + resource = Resource.from_raw_data(raw_content) + properties["subnet"] = resource + + return super(FrontendIPConfigurations, cls).from_raw_data(raw_data) + + +class InboundNATRules(_BaseHNVModel): + + """Model for inbound nat rules. + + This resource is used to configure the load balancer to apply + Network Address Translation of inbound traffic. + """ + + _endpoint = ("/networking/v1/loadBalancers/{parent_id}" + "/inboundNatRules/{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. + """ + + backend_ip_configuration = model.Field( + name="backend_ip_configuration", key="backendIPConfiguration", + is_required=False, is_read_only=False) + """Indicates a references to backendAddressPool resource. Traffic + sent to frontendPort of each of the frontendIPConfigurations is + forwarded to the backend IP. + """ + + backend_port = model.Field(name="backend_port", key="backendPort", + is_required=False, is_read_only=False) + """Indicates a port used for internal connections on the endpoint. + The localPort attribute maps the external port on the endpoint + to an internal port on a role. This is useful in scenarios where a + role has to communicate to an internal component on a port + that different from the one that is exposed externally. + Possible values range between 1 and 65535, inclusive. + This parameter is required if the protocol is TCP or UDP. + """ + + frontend_ip_configurations = model.Field( + name="frontend_ip_configurations", key="frontendIPConfigurations", + is_required=True, is_read_only=False) + """Indicates an array of references to frontendIPConfigurations + resources.""" + + frontend_port = model.Field(name="frontend_port", key="frontendPort", + is_required=False, is_read_only=False) + """The port for the external endpoint. Any port number can be + specified, but the port numbers specified for each role in the + service MUST be unique. Possible values range between 1 and + 65535, inclusive. + This parameter must be specified if protocol is TCP or UDP. + """ + + protocol = model.Field(name="protocol", key="protocol", + is_required=True, is_read_only=False) + """Indicates the inbound transport protocol for the external + endpoint. Valid values include `UDP`, `TCP`, `GRE`, `ESP` or `ALL`. + `ALL` indicates a wildcard. + """ + + idle_timeout = model.Field(name="idle_timeout", + key="idleTimeoutInMinutes", + is_required=False, is_read_only=False) + """Specifies the timeout for the TCP idle connection. + + The value can be set between 4 and 30 minutes. The default is 4 + minutes. If public IP is used as a frontend IP of a Load Balancer + this value is ignored. + """ + + floating_ip = model.Field(name="floating_ip", key="enableFloatingIP", + is_required=False, is_read_only=False) + """ + This specifies that a floating IP will be used on the available servers + behind a load balancer. Floating IP (VIP) will be forwarded by the load + balancer to the backend server. The back-end server will be configured + with that VIP, a datacenter IP and weakhost forwarding. + + Floating IP configuration is required if you are using the SQL AlwaysOn + Availability Group feature. This setting can't be changed after you create + the endpoint. + """ + + @classmethod + def from_raw_data(cls, raw_data): + """Create a new model using raw API response.""" + properties = raw_data.get("properties", {}) + + raw_ip_configuration = properties.get("backendIPConfiguration", []) + if isinstance(raw_ip_configuration, dict): + raw_ip_configuration = [raw_ip_configuration] + + for raw_content in raw_ip_configuration: + backend_ip_configuration = Resource.from_raw_data(raw_content) + properties["backendIPConfiguration"] = backend_ip_configuration + + frontend_ip_configurations = [] + for raw_content in properties.get("frontendIPConfigurations", []): + resource = Resource.from_raw_data(raw_content) + frontend_ip_configurations.append(resource) + properties["frontendIPConfigurations"] = frontend_ip_configurations + + return super(InboundNATRules, cls).from_raw_data(raw_data) + + +class LoadBalancingRules(_BaseHNVModel): + + """Model for load balancing rules. + + This resource is used to configure load balancing policies. The policies + dictate the kind of traffic that is load-balanced, and port mapping + between frontend IPs and backend IPs. + """ + + _endpoint = ("/networking/v1/loadBalancers/{parent_id}" + "/loadBalancingRules/{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. + """ + + backend_address_pool = model.Field( + name="backend_address_pool", key="backendAddressPool", + is_required=False, is_read_only=False) + """Indicates an array of references to a BackendAddressPool resource. + + Inbound traffic is randomly load balanced across IPs in the backend pool. + """ + + backend_port = model.Field(name="backend_port", key="backendPort", + is_required=False, is_read_only=False) + """Indicates the port used for internal connections on the endpoint. + + The localPort attribute maps the external port on the endpoint to an + internal port on a role. This is useful in scenarios where a role has + to communicate to an internal component on a port that different from + the one that is exposed externally. If not specified, the value of + localPort is the same as the port attribute. Set the value of localPort + to "*" to automatically assign an unallocated port that is discoverable + using the runtime API. + Possible values range between 1 and 65535, inclusive. + This parameter is required if the protocol is TCP or UDP. + """ + + frontend_ip_configurations = model.Field( + name="frontend_ip_configurations", key="frontendIPConfigurations", + is_required=True, is_read_only=False) + """Indicates an array of references to FrontendIPAddress resources.""" + + frontend_port = model.Field(name="frontend_port", key="frontendPort", + is_required=False, is_read_only=False) + """Indicates the port for the external endpoint. + + Possible values range between 1 and 65535, inclusive. This value MUST + be unique for the loadbalancer resource. + This parameter is required if the protocol is TCP or UDP. + """ + + idle_timeout = model.Field( + name="idle_timeout", key="idleTimeoutInMinutes", + is_required=False, is_read_only=False) + """Indicates the timeout for the Tcp idle connection in the inbound + direction, i.e. a connection initiated by an internet client to a VIP. + The value can be set between 4 and 30 minutes. The default value is + 4 minutes. + """ + + protocol = model.Field(name="protocol", key="protocol", + is_required=True, is_read_only=False) + """Indicates the inbound transport protocol for the external endpoint. + Valid values include `UDP`, `TCP`, `GRE`, `ESP` or `ALL`. + """ + + probe = model.Field(name="probe", key="probe", + is_required=False, is_read_only=False) + """Indicates a reference to the probe resource used by this + LoadBalancingRule. + """ + + floating_ip = model.Field(name="floating_ip", key="enableFloatingIP", + is_required=False, is_read_only=False) + """ + This specifies that a floating IP will be used on the available servers + behind a load balancer. Floating IP (VIP) will be forwarded by the load + balancer to the backend server. The back-end server will be configured + with that VIP, a datacenter IP and weakhost forwarding. + + Floating IP configuration is required if you are using the SQL AlwaysOn + Availability Group feature. This setting can't be changed after you create + the endpoint. + """ + + load_distribution = model.Field( + name="load_distribution", key="loadDistribution", + is_required=False, is_read_only=False) + """This specifies the load balancing distribution type to be used by + the load balancer. The loadBalancer uses a distribution algorithm which + is a 5 tuple (source IP, source port, destination IP, destination port, + protocol type) hash to map traffic to available servers. It provides + stickiness only within a transport session, which is a feature that routes + the requests for a particular session to the same physical machine that + serviced the first request for that session. Packets in the same TCP or + UDP session will be directed to the same datacenter IP instance behind the + load balanced endpoint. When the client closes and re-opens the connection + or starts a new session from the same source IP, the source port changes + and causes the traffic to go to a different datacenter IP endpoint. + + The loadBalancer can be configured to use a 2 tuple (Source IP, + Destination IP) or 3 tuple (Source IP, Destination IP, Protocol) to map + traffic to the available servers. By using SourceIPProtocol, connections + initiated from the same client computer goes to the same datacenter IP + endpoint. + * Default - The load balancer is configured to use a 5 tuple hash + to map traffic to available servers + * SourceIP - The load balancer is configured to use a 2 tuple hash + to map traffic to available servers + * SourceIPProtocol - The load balancer is configured to use a 3 tuple + hash to map traffic to available servers + """ + + @classmethod + def from_raw_data(cls, raw_data): + """Create a new model using raw API response.""" + properties = raw_data.get("properties", {}) + + frontend_ip_configurations = [] + for raw_content in properties.get("frontendIPConfigurations", []): + resource = Resource.from_raw_data(raw_content) + frontend_ip_configurations.append(resource) + properties["frontendIPConfigurations"] = frontend_ip_configurations + + raw_content = properties.get("backendAddressPool", None) + if raw_content is not None: + resource = Resource.from_raw_data(raw_content) + properties["backendAddressPool"] = resource + + raw_content = properties.get("probe", None) + if raw_content is not None: + resource = Resource.from_raw_data(raw_content) + properties["probe"] = resource + + return super(LoadBalancingRules, cls).from_raw_data(raw_data) + + +class OutboundNATRules(_BaseHNVModel): + + """Model for outbound NAT rules. + + This resource is used to configure the load balancer to apply + Network Address Translation of outbound traffic. + """ + + _endpoint = ("/networking/v1/loadBalancers/{parent_id}" + "/outboundNatRules/{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. + """ + + frontend_ip_configurations = model.Field( + name="frontend_ip_configurations", key="frontendIPConfigurations", + is_required=True, is_read_only=False) + """Indicates an array of frontendIpConfigurations resources. + + Indicates an array of references to frontendIpAddress resources. + """ + + backend_address_pool = model.Field( + name="backend_address_pool", key="backendAddressPool", + is_required=True, is_read_only=False) + """Indicates a reference to the backendAddressPool resource. + + This is the pool of IP addresses where outbound traffic originates. + """ + + protocol = model.Field( + name="protocol", key="protocol", + is_required=True, is_read_only=False) + """Protocol for outbound traffic. For transparent outbound NAT + specify "all". + Valid values include `TCP`, `UDP`, `GRE`, `ESP` or `All`. + """ + + @classmethod + def from_raw_data(cls, raw_data): + """Create a new model using raw API response.""" + properties = raw_data.get("properties", {}) + + frontend_ip_configurations = [] + for raw_content in properties.get("frontendIPConfigurations", []): + resource = Resource.from_raw_data(raw_content) + frontend_ip_configurations.append(resource) + properties["frontendIPConfigurations"] = frontend_ip_configurations + + raw_content = properties.get("backendAddressPool", None) + if raw_content is not None: + resource = Resource.from_raw_data(raw_content) + properties["backendAddressPool"] = resource + + return super(OutboundNATRules, cls).from_raw_data(raw_data) + + +class Probes(_BaseHNVModel): + + """Model for probes.""" + + _endpoint = ("/networking/v1/loadBalancers/{parent_id}" + "/probes/{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. + """ + + interval = model.Field(name="interval", key="intervalInSeconds", + is_required=False, is_read_only=False) + """Indicates the interval, in seconds, for how frequently to probe the + endpoint for health status. Typically, the interval is slightly less than + half the allocated timeout period (in seconds) which allows two full + probes before taking the instance out of rotation. The default value is + 15, the minimum value is 5. + """ + + load_balancing_rules = model.Field( + name="load_balancing_rules", key="loadBalancingRules", + is_required=False, is_read_only=True) + """Indicates an array of references to loadBalancingRule resources that + use this probe. + """ + + number_of_probes = model.Field( + name="number_of_probes", key="numberOfProbes", + is_required=False, is_read_only=False) + """Indicates the timeout period, in seconds, applied to the probe where + no response will result in stopping further traffic from being delivered + to the endpoint. This value allows endpoints to be taken out of rotation + faster or slower than the typical times (which are the defaults). + The default value is 31, the minimum value is 11. + """ + + protocol = model.Field(name="protocol", key="protocol", + is_required=True, is_read_only=False) + """Indicates the protocol of the end point. + + Valid values are `HTTP` or `TCP`. If `TCP` is specified, a received ACK + is required for the probe to be successful. If `HTTP` is specified, + a 200 OK response from the specified URI is required for the probe to + be successful. + """ + + port = model.Field(name="port", key="port", + is_required=True, is_read_only=False) + """Indicates the port for communicating the probe. Possible values range + from 1 to 65535, inclusive. + """ + + request_path = model.Field(name="request_path", key="requestPath", + is_required=True, is_read_only=False) + """Indicates the URI used for requesting health status from the VM. + + The path is required if protocol is set to HTTP. Otherwise, it is not + allowed. There is no default value. + """ + + @classmethod + def from_raw_data(cls, raw_data): + """Create a new model using raw API response.""" + properties = raw_data.get("properties", {}) + + load_balancing_rules = [] + for raw_content in properties.get("loadBalancingRules", []): + resource = Resource.from_raw_data(raw_content) + load_balancing_rules.append(resource) + properties["loadBalancingRules"] = load_balancing_rules + + return super(Probes, cls).from_raw_data(raw_data) + + +class LoadBalancers(_BaseHNVModel): + + """Model for load balancers. + + The loadBalancers resource allows fine-grained configuration of the + distribution of incoming traffic across VM instances that are hosted + in a Windows Server and System Center cloud. This resource has two + main parts: a frontend and a backend configuration. + + The frontend configuration exposes the IP address of the load balancer. + For example, this address can be a reserved public or private IP address + previously provided to the client, or it can be an IP address that is + dynamically allocated from a subnet of a virtual network. + """ + + _endpoint = "/networking/v1/loadBalancers/{resource_id}" + + backend_address_pools = model.Field(name="backend_address_pools", + key="backendAddressPools", + is_required=False, is_read_only=False) + """Indicates the backend Address Pool of the load balancer.""" + + frontend_ip_configurations = model.Field( + name="frontend_ip_configurations", key="frontendIPConfigurations", + is_required=True, is_read_only=False) + """Indicates the frontend IP addresses of the load balancer.""" + + load_balancing_rules = model.Field(name="load_balancing_rules", + key="loadBalancingRules", + is_required=False, is_read_only=False) + """A list of load balancing configurations. + + Each configuration describes what traffic and how it gets load balanced + between backend IPs. + """ + + inbound_nat_rules = model.Field(name="inbound_nat_rules", + key="inboundNatRules", + is_required=False, is_read_only=False) + """Indicates an array of inbound NAT rules configured for the + load balancer. + """ + + outbound_nat_rules = model.Field(name="outbound_nat_rules", + key="outboundNatRules", + is_required=False, is_read_only=False) + """Indicates an array of outbound NAT rules configured for the + load balancer. + """ + + probes = model.Field(name="probes", key="probes", + is_required=False, is_read_only=False) + """Indicates an array of probes configured for the + load balancer. + """ + + @classmethod + def from_raw_data(cls, raw_data): + properties = raw_data.get("properties", {}) + + backend_address_pools = [] + for raw_content in properties.get("backendAddressPools", []): + raw_content["parentResourceID"] = raw_data["resourceId"] + address_pool = BackendAddressPools.from_raw_data(raw_content) + backend_address_pools.append(address_pool) + properties["backendAddressPools"] = backend_address_pools + + frontend_ip_configurations = [] + for raw_content in properties.get("frontendIPConfigurations", []): + raw_content["parentResourceID"] = raw_data["resourceId"] + ip_configurations = FrontendIPConfigurations.from_raw_data( + raw_content) + frontend_ip_configurations.append(ip_configurations) + properties["frontendIPConfigurations"] = frontend_ip_configurations + + inbound_nat_rules = [] + for raw_content in properties.get("inboundNatRules", []): + raw_content["parentResourceID"] = raw_data["resourceId"] + inbound_nat_rule = InboundNATRules.from_raw_data(raw_content) + inbound_nat_rules.append(inbound_nat_rule) + properties["inboundNatRules"] = inbound_nat_rules + + outbound_nat_rules = [] + for raw_content in properties.get("outboundNatRules", []): + raw_content["parentResourceID"] = raw_data["resourceId"] + inbound_nat_rule = OutboundNATRules.from_raw_data(raw_content) + outbound_nat_rules.append(inbound_nat_rule) + properties["outboundNatRules"] = outbound_nat_rules + + load_balancing_rules = [] + for raw_content in properties.get("loadBalancingRules", []): + raw_content["parentResourceID"] = raw_data["resourceId"] + balancing_rule = LoadBalancingRules.from_raw_data(raw_content) + load_balancing_rules.append(balancing_rule) + properties["loadBalancingRules"] = load_balancing_rules + + probes = [] + for raw_content in properties.get("probes", []): + raw_content["parentResourceID"] = raw_data["resourceId"] + probe = Probes.from_raw_data(raw_content) + probes.append(probe) + properties["probes"] = probes + + return super(LoadBalancers, cls).from_raw_data(raw_data) diff --git a/hnv/common/model.py b/hnv/common/model.py index 93adefe..e7e5432 100644 --- a/hnv/common/model.py +++ b/hnv/common/model.py @@ -317,7 +317,7 @@ class Model(object): self._changes[field.key] = fields[field_name] self._data.update(self._changes) - def commit(self, wait=False, timeout=None): + def commit(self, if_match=None, wait=False, timeout=None): """Apply all the changes on the current model.""" # pylint: disable=unused-argument self._data.update(self._changes) diff --git a/hnv/common/utils.py b/hnv/common/utils.py index de1783e..1b7860c 100644 --- a/hnv/common/utils.py +++ b/hnv/common/utils.py @@ -104,13 +104,19 @@ class _HNVClient(object): else: return not self._https_allow_insecure - def _http_request(self, resource, method=constant.GET, body=None): - url = requests.compat.urljoin(self._base_url, resource) + def _http_request(self, resource, method=constant.GET, body=None, + if_match=None): + if not resource.startswith("http"): + url = requests.compat.urljoin(self._base_url, resource) + else: + url = resource + headers = self._get_headers() if method in (constant.PUT, constant.PATCH): - etag = (body or {}).get("etag", None) - if etag is not None: - headers["If-Match"] = etag + if not isinstance(if_match, six.string_types): + if_match = (body or {}).get("etag", None) + if if_match is not None: + headers["If-Match"] = if_match attemts = 0 while True: @@ -162,9 +168,10 @@ class _HNVClient(object): except ValueError: raise exception.ServiceException("Invalid service response.") - def update_resource(self, path, data): + def update_resource(self, path, data, if_match=None): """Update the required resource.""" - response = self._http_request(path, method="PUT", body=data) + response = self._http_request(resource=path, method="PUT", body=data, + if_match=if_match) try: return response.json() except ValueError: diff --git a/hnv/tests/common/test_utils.py b/hnv/tests/common/test_utils.py index 574d878..cadd1cd 100644 --- a/hnv/tests/common/test_utils.py +++ b/hnv/tests/common/test_utils.py @@ -105,31 +105,31 @@ class TestHNVClient(unittest.TestCase): if isinstance(expected_response, requests.exceptions.SSLError): self.assertRaises(exception.CertificateVerifyFailed, client._http_request, - mock.sentinel.resource, method, body) + "/fake/resource", method, body) return elif isinstance(expected_response, requests.ConnectionError): self.assertRaises(requests.ConnectionError, client._http_request, - mock.sentinel.resource, method, body) + "/fake/resource", method, body) return elif status_code == 400: self.assertRaises(exception.ServiceException, client._http_request, - mock.sentinel.resource, method, body) + "/fake/resource", method, body) elif status_code == 404: self.assertRaises(exception.NotFound, client._http_request, - mock.sentinel.resource, method, body) + "/fake/resource", method, body) elif status_code != 200: self.assertRaises(requests.HTTPError, client._http_request, - mock.sentinel.resource, method, body) + "/fake/resource", method, body) else: - client_response = client._http_request(mock.sentinel.resource, + client_response = client._http_request("/fake/resource", method, body) mock_join.assert_called_once_with(mock.sentinel.url, - mock.sentinel.resource) + "/fake/resource") mock_headers.assert_called_once_with() if not method == constant.GET: etag = (body or {}).get("etag", None) @@ -236,9 +236,9 @@ class TestHNVClient(unittest.TestCase): mock.sentinel.data) self.assertIs(response, mock.sentinel.response) - mock_http_request.assert_called_once_with(mock.sentinel.path, - method="PUT", - body=mock.sentinel.data) + mock_http_request.assert_called_once_with( + resource=mock.sentinel.path, method="PUT", body=mock.sentinel.data, + if_match=None) self.assertRaises(exception.ServiceException, client.update_resource, mock.sentinel.path, mock.sentinel.data) diff --git a/hnv/tests/fake/fake_response.py b/hnv/tests/fake/fake_response.py index d78503b..44431e8 100644 --- a/hnv/tests/fake/fake_response.py +++ b/hnv/tests/fake/fake_response.py @@ -89,3 +89,31 @@ class FakeResponse(object): def public_ip_addresses(self): """Fake GET(all) response for public IP addresses.""" return self._load_resource("public_ip_addresses.json") + + def backend_address_pools(self): + """Fake GET(all) response for backend address pools.""" + return self._load_resource("backend_address_pools.json") + + def frontend_ip_configurations(self): + """Fake GET(all) response for frontend ip configurations.""" + return self._load_resource("frontend_ip_configurations.json") + + def inbound_nat_rules(self): + """Fake GET(all) response for inbound nat rules.""" + return self._load_resource("inbound_nat_rules.json") + + def load_balancing_rules(self): + """Fake GET(all) response for load balacing rules.""" + return self._load_resource("load_balancing_rules.json") + + def outbound_nat_rules(self): + """Fake GET(all) response for outbound nat rules.""" + return self._load_resource("outbound_nat_rules.json") + + def probes(self): + """Fake GET(all) response for probes.""" + return self._load_resource("probes.json") + + def load_balancers(self): + """Fake GET(all) response for load balancers.""" + return self._load_resource("load_balancers.json") diff --git a/hnv/tests/fake/response/backend_address_pools.json b/hnv/tests/fake/response/backend_address_pools.json new file mode 100644 index 0000000..05c6373 --- /dev/null +++ b/hnv/tests/fake/response/backend_address_pools.json @@ -0,0 +1,51 @@ +{ + "value": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971- 2682d597e098/backendAddressPools/b32b5ef0-5332-49a8-b383-f91090135f71", + "resourceId": "b32b5ef0-5332-49a8-b383-f91090135f71", + "etag": "W/\"fb318cf6-9102-4e34-a684-5e25aee8d3f4\"", + "instanceId": "f980604c-258c-4d60-8be4-559edd085384", + "properties": { + "provisioningState": "Succeeded", + "backendIPConfigurations": [ + { + "resourceRef": "/networkInterfaces/97c69782-f173-4793-a408-64074e601dd1/ipConfigurations/1b94ce74-b012-49a7-8e93-9315252c6ab2" + }, + { + "resourceRef": "/networkInterfaces/e5ea0c14-ce85-4eb7-909a-993f0477f5ac/ipConfigurations/45af7ff3-555f-43b0-ae74-7fcce88c5197" + } + ], + "outboundNatRules": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/outboundNatRules/49053c15-2d0f-45a2-8148-be8615282160" + } + ], + "loadBalancingRules": [] + } + }, + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/backendAddressPools/b32b5ef0-5332-49a8-b383-f91090135f71", + "resourceId": "b32b5ef0-5332-49a8-b383-f91090135f71", + "etag": "W/\"fb318cf6-9102-4e34-a684-5e25aee8d3f4\"", + "instanceId": "f980604c-258c-4d60-8be4-559edd085384", + "properties": { + "provisioningState": "Succeeded", + "backendIPConfigurations": [ + { + "resourceRef": "/networkInterfaces/97c69782-f173-4793-a408-64074e601dd1/ipConfigurations/1b94ce74-b012-49a7-8e93-9315252c6ab2" + }, + { + "resourceRef": "/networkInterfaces/e5ea0c14-ce85-4eb7-909a-993f0477f5ac/ipConfigurations/45af7ff3-555f-43b0-ae74-7fcce88c5197" + } + ], + "outboundNatRules": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/outboundNatRules/49053c15-2d0f-45a2-8148-be8615282160" + } + ], + "loadBalancingRules": [] + } + } + ], + "nextLink": "" +} diff --git a/hnv/tests/fake/response/frontend_ip_configurations.json b/hnv/tests/fake/response/frontend_ip_configurations.json new file mode 100644 index 0000000..f11041a --- /dev/null +++ b/hnv/tests/fake/response/frontend_ip_configurations.json @@ -0,0 +1,80 @@ +{ + "value": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/frontendIPConfigurations/5187779d-c61c-44d2-87be-fa69ac2d9d57", + "resourceId": "5187779d-c61c-44d2-87be-fa69ac2d9d57", + "etag": "W/\"fb318cf6-9102-4e34-a684-5e25aee8d3f4\"", + "instanceId": "3902a530-9639-4759-9bbf-9bab6675593a", + "properties": { + "provisioningState": "Succeeded", + "privateIPAddress": "22.0.0.22", + "privateIPAllocationMethod": "Static", + "subnet": { + "resourceRef": "/logicalnetworks/ccb732ec-a3b5-4755-99ff-fddb91d50884/subnets/262b479f-0952-49b9-ad20-3d6732729389" + }, + "loadBalancingRules": [], + "inboundNatRules": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/inboundNatRules/fc44af15-be82-46c5-b75a-3e89ccd792a9" + } + ], + "outboundNatRules": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/outboundNatRules/49053c15-2d0f-45a2-8148-be8615282160" + } + ] + } + }, + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/frontendIPConfigurations/94c568d8-d839-431a-aed4-a5c178356018", + "resourceId": "94c568d8-d839-431a-aed4-a5c178356018", + "etag": "W/\"fb318cf6-9102-4e34-a684-5e25aee8d3f4\"", + "instanceId": "d896da12-37f2-4e36-b229-7278a672a0ac", + "properties": { + "provisioningState": "Succeeded", + "privateIPAddress": "22.0.0.23", + "privateIPAllocationMethod": "Static", + "subnet": { + "resourceRef": "/logicalnetworks/ccb732ec-a3b5-4755-99ff-fddb91d50884/subnets/262b479f-0952-49b9-ad20-3d6732729389" + }, + "loadBalancingRules": [], + "inboundNatRules": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/inboundNatRules/0e5ed8cf-60fb-40f4-b02a-90932d4de000" + } + ], + "outboundNatRules": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/outboundNatRules/49053c15-2d0f-45a2-8148-be8615282160" + } + ] + } + }, + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/frontendIPConfigurations/94c568d8-d839-431a-aed4-a5c178356018", + "resourceId": "94c568d8-d839-431a-aed4-a5c178356018", + "etag": "W/\"fb318cf6-9102-4e34-a684-5e25aee8d3f4\"", + "instanceId": "d896da12-37f2-4e36-b229-7278a672a0ac", + "properties": { + "provisioningState": "Succeeded", + "privateIPAddress": "22.0.0.23", + "privateIPAllocationMethod": "Static", + "subnet": { + "resourceRef": "/logicalnetworks/ccb732ec-a3b5-4755-99ff-fddb91d50884/subnets/262b479f-0952-49b9-ad20-3d6732729389" + }, + "loadBalancingRules": [], + "inboundNatRules": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/inboundNatRules/0e5ed8cf-60fb-40f4-b02a-90932d4de000" + } + ], + "outboundNatRules": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/outboundNatRules/49053c15-2d0f-45a2-8148-be8615282160" + } + ] + } + } + ], + "nextLink": "" +} diff --git a/hnv/tests/fake/response/inbound_nat_rules.json b/hnv/tests/fake/response/inbound_nat_rules.json new file mode 100644 index 0000000..1e25fa2 --- /dev/null +++ b/hnv/tests/fake/response/inbound_nat_rules.json @@ -0,0 +1,85 @@ +{ + "value": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/inboundNatRules/fc44af15-be82-46c5-b75a-3e89ccd792a9", + "resourceId": "fc44af15-be82-46c5-b75a-3e89ccd792a9", + "etag": "W/\"fb318cf6-9102-4e34-a684-5e25aee8d3f4\"", + "instanceId": "a748c5db-e2fd-4335-8c89-280b78d2511c", + "properties": { + "provisioningState": "Succeeded", + "frontendIPConfigurations": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/frontendIPConfigurations/5187779d-c61c-44d2-87be-fa69ac2d9d57" + } + ], + "protocol": "Tcp", + "frontendPort": 2003, + "backendPort": 2003, + "backendIPConfiguration": { + "resourceRef": "/networkInterfaces/e5ea0c14-ce85-4eb7-909a-993f0477f5ac/ipConfigurations/45af7ff3-555f-43b0-ae74-7fcce88c5197" + } + } + }, + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/inboundNatRules/0e5ed8cf-60fb-40f4-b02a-90932d4de000", + "resourceId": "0e5ed8cf-60fb-40f4-b02a-90932d4de000", + "etag": "W/\"fb318cf6-9102-4e34-a684-5e25aee8d3f4\"", + "instanceId": "e8c59538-e641-4796-968d-50c4e11225e7", + "properties": { + "provisioningState": "Succeeded", + "frontendIPConfigurations": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/frontendIPConfigurations/94c568d8-d839-431a-aed4-a5c178356018" + } + ], + "protocol": "Tcp", + "frontendPort": 2003, + "backendPort": 2003, + "backendIPConfiguration": { + "resourceRef": "/networkInterfaces/97c69782-f173-4793-a408-64074e601dd1/ipConfigurations/1b94ce74-b012-49a7-8e93-9315252c6ab2" + } + } + }, + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/inboundNatRules/fc44af15-be82-46c5-b75a-3e89ccd792a9", + "resourceId": "fc44af15-be82-46c5-b75a-3e89ccd792a9", + "etag": "W/\"fb318cf6-9102-4e34-a684-5e25aee8d3f4\"", + "instanceId": "a748c5db-e2fd-4335-8c89-280b78d2511c", + "properties": { + "provisioningState": "Succeeded", + "frontendIPConfigurations": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/frontendIPConfigurations/5187779d-c61c-44d2-87be-fa69ac2d9d57" + } + ], + "protocol": "Tcp", + "frontendPort": 2003, + "backendPort": 2003, + "backendIPConfiguration": { + "resourceRef": "/networkInterfaces/e5ea0c14-ce85-4eb7-909a-993f0477f5ac/ipConfigurations/45af7ff3-555f-43b0-ae74-7fcce88c5197" + } + } + }, + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/inboundNatRules/fc44af15-be82-46c5-b75a-3e89ccd792a9", + "resourceId": "fc44af15-be82-46c5-b75a-3e89ccd792a9", + "etag": "W/\"fb318cf6-9102-4e34-a684-5e25aee8d3f4\"", + "instanceId": "a748c5db-e2fd-4335-8c89-280b78d2511c", + "properties": { + "provisioningState": "Succeeded", + "frontendIPConfigurations": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/frontendIPConfigurations/5187779d-c61c-44d2-87be-fa69ac2d9d57" + } + ], + "protocol": "Tcp", + "frontendPort": 2003, + "backendPort": 2003, + "backendIPConfiguration": { + "resourceRef": "/networkInterfaces/e5ea0c14-ce85-4eb7-909a-993f0477f5ac/ipConfigurations/45af7ff3-555f-43b0-ae74-7fcce88c5197" + } + } + } + ], + "nextLink": "" +} diff --git a/hnv/tests/fake/response/load_balancers.json b/hnv/tests/fake/response/load_balancers.json new file mode 100644 index 0000000..e401b76 --- /dev/null +++ b/hnv/tests/fake/response/load_balancers.json @@ -0,0 +1,347 @@ +{ + "value": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098", + "resourceId": "0cac5f8a-9d5c-455a-a971-2682d597e098", + "etag": "W/\"fb318cf6-9102-4e34-a684-5e25aee8d3f4\"", + "instanceId": "d91f4951-faf7-4a15-a84a-8a9f6dffaff8", + "properties": { + "provisioningState": "Succeeded", + "frontendIPConfigurations": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/frontendIPConfigurations/5187779d-c61c-44d2-87be-fa69ac2d9d57", + "resourceId": "5187779d-c61c-44d2-87be-fa69ac2d9d57", + "etag": "W/\"fb318cf6-9102-4e34-a684-5e25aee8d3f4\"", + "instanceId": "3902a530-9639-4759-9bbf-9bab6675593a", + "properties": { + "provisioningState": "Succeeded", + "privateIPAddress": "22.0.0.22", + "privateIPAllocationMethod": "Static", + "subnet": { + "resourceRef": "/logicalnetworks/ccb732ec-a3b5-4755-99ff-fddb91d50884/subnets/262b479f-0952-49b9-ad20-3d6732729389" + }, + "loadBalancingRules": [], + "inboundNatRules": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/inboundNatRules/fc44af15-be82-46c5-b75a-3e89ccd792a9" + } + ], + "outboundNatRules": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/outboundNatRules/49053c15-2d0f-45a2-8148-be8615282160" + } + ] + } + }, + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/frontendIPConfigurations/94c568d8-d839-431a-aed4-a5c178356018", + "resourceId": "94c568d8-d839-431a-aed4-a5c178356018", + "etag": "W/\"fb318cf6-9102-4e34-a684-5e25aee8d3f4\"", + "instanceId": "d896da12-37f2-4e36-b229-7278a672a0ac", + "properties": { + "provisioningState": "Succeeded", + "privateIPAddress": "22.0.0.23", + "privateIPAllocationMethod": "Static", + "subnet": { + "resourceRef": "/logicalnetworks/ccb732ec-a3b5-4755-99ff-fddb91d50884/subnets/262b479f-0952-49b9-ad20-3d6732729389" + }, + "loadBalancingRules": [], + "inboundNatRules": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/inboundNatRules/0e5ed8cf-60fb-40f4-b02a-90932d4de000" + } + ], + "outboundNatRules": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/outboundNatRules/49053c15-2d0f-45a2-8148-be8615282160" + } + ] + } + } + ], + "backendAddressPools": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/backendAddressPools/b32b5ef0-5332-49a8-b383-f91090135f71", + "resourceId": "b32b5ef0-5332-49a8-b383-f91090135f71", + "etag": "W/\"fb318cf6-9102-4e34-a684-5e25aee8d3f4\"", + "instanceId": "f980604c-258c-4d60-8be4-559edd085384", + "properties": { + "provisioningState": "Succeeded", + "backendIPConfigurations": [ + { + "resourceRef": "/networkInterfaces/97c69782-f173-4793-a408-64074e601dd1/ipConfigurations/1b94ce74-b012-49a7-8e93-9315252c6ab2" + }, + { + "resourceRef": "/networkInterfaces/e5ea0c14-ce85-4eb7-909a-993f0477f5ac/ipConfigurations/45af7ff3-555f-43b0-ae74-7fcce88c5197" + } + ], + "outboundNatRules": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/outboundNatRules/49053c15-2d0f-45a2-8148-be8615282160" + } + ], + "loadBalancingRules": [] + } + } + ], + "probes": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/probes/9f940e29-1d25-44fc-88d3-c81151a0344e", + "resourceId": "9f940e29-1d25-44fc-88d3-c81151a0344e", + "etag": "W/\"fb318cf6-9102-4e34-a684-5e25aee8d3f4\"", + "instanceId": "0da65588-247b-475b-bd1a-7ead0ba1a182", + "properties": { + "provisioningState": "Succeeded", + "protocol": "Tcp", + "port": 55555, + "intervalInSeconds": 30, + "numberOfProbes": 1, + "loadBalancingRules": [] + } + } + ], + "inboundNatRules": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/inboundNatRules/fc44af15-be82-46c5-b75a-3e89ccd792a9", + "resourceId": "fc44af15-be82-46c5-b75a-3e89ccd792a9", + "etag": "W/\"fb318cf6-9102-4e34-a684-5e25aee8d3f4\"", + "instanceId": "a748c5db-e2fd-4335-8c89-280b78d2511c", + "properties": { + "provisioningState": "Succeeded", + "frontendIPConfigurations": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/frontendIPConfigurations/5187779d-c61c-44d2-87be-fa69ac2d9d57" + } + ], + "protocol": "Tcp", + "frontendPort": 2003, + "backendPort": 2003, + "enableFloatingIP": false, + "idleTimeoutInMinutes": 4, + "backendIPConfiguration": { + "resourceRef": "/networkInterfaces/e5ea0c14-ce85-4eb7-909a-993f0477f5ac/ipConfigurations/45af7ff3-555f-43b0-ae74-7fcce88c5197" + } + } + }, + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/inboundNatRules/0e5ed8cf-60fb-40f4-b02a-90932d4de000", + "resourceId": "0e5ed8cf-60fb-40f4-b02a-90932d4de000", + "etag": "W/\"fb318cf6-9102-4e34-a684-5e25aee8d3f4\"", + "instanceId": "e8c59538-e641-4796-968d-50c4e11225e7", + "properties": { + "provisioningState": "Succeeded", + "frontendIPConfigurations": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/frontendIPConfigurations/94c568d8-d839-431a-aed4-a5c178356018" + } + ], + "protocol": "Tcp", + "frontendPort": 2003, + "backendPort": 2003, + "enableFloatingIP": false, + "idleTimeoutInMinutes": 4, + "backendIPConfiguration": { + "resourceRef": "/networkInterfaces/97c69782-f173-4793-a408-64074e601dd1/ipConfigurations/1b94ce74-b012-49a7-8e93-9315252c6ab2" + } + } + } + ], + "outboundNatRules": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/outboundNatRules/49053c15-2d0f-45a2-8148-be8615282160", + "resourceId": "49053c15-2d0f-45a2-8148-be8615282160", + "etag": "W/\"fb318cf6-9102-4e34-a684-5e25aee8d3f4\"", + "instanceId": "c4000c95-7f90-4bb4-b68d-b2bc9c1dfc3e", + "properties": { + "provisioningState": "Succeeded", + "frontendIPConfigurations": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/frontendIPConfigurations/5187779d-c61c-44d2-87be-fa69ac2d9d57" + }, + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/frontendIPConfigurations/94c568d8-d839-431a-aed4-a5c178356018" + } + ], + "protocol": "All", + "backendAddressPool": { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/backendAddressPools/b32b5ef0-5332-49a8-b383-f91090135f71" + } + } + } + ] + } + }, + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1", + "resourceId": "d2251a0d-32d2-457e-b3aa-e0fe1f42cce1", + "etag": "W/\"72fdfa3d-34f4-4c90-ae94-d97ed73c9cf7\"", + "instanceId": "b32d0db3-13db-431a-a265-32185aa5a905", + "properties": { + "provisioningState": "Succeeded", + "frontendIPConfigurations": [ + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/frontendIPConfigurations/9f37a479-7d60-489a-aab6-d7eb2200306f", + "resourceId": "9f37a479-7d60-489a-aab6-d7eb2200306f", + "etag": "W/\"72fdfa3d-34f4-4c90-ae94-d97ed73c9cf7\"", + "instanceId": "51b57d2a-80da-464a-988a-4a805bd1d875", + "properties": { + "provisioningState": "Succeeded", + "privateIPAddress": "21.0.0.23", + "privateIPAllocationMethod": "Static", + "subnet": { + "resourceRef": "/logicalnetworks/9c1b2b61-dec2-49e3-b573-c2ecff57893d/subnets/a4f7c90b-6056-4dff-97fb-f46211ecdc10" + }, + "loadBalancingRules": [], + "inboundNatRules": [ + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/inboundNatRules/d076eae7-926a-457a-a60c-0a713a02977d" + } + ], + "outboundNatRules": [ + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/outboundNatRules/f3f3291d-b26c-44d3-8d55-99b644b70388" + } + ] + } + }, + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/frontendIPConfigurations/ab5ccbe7-2ce9-4cdf-a0da-e4e5d81479d8", + "resourceId": "ab5ccbe7-2ce9-4cdf-a0da-e4e5d81479d8", + "etag": "W/\"72fdfa3d-34f4-4c90-ae94-d97ed73c9cf7\"", + "instanceId": "fe6adbed-8b73-4fc2-82cd-191143753c4a", + "properties": { + "provisioningState": "Succeeded", + "privateIPAddress": "21.0.0.24", + "privateIPAllocationMethod": "Static", + "subnet": { + "resourceRef": "/logicalnetworks/9c1b2b61-dec2-49e3-b573-c2ecff57893d/subnets/a4f7c90b-6056-4dff-97fb-f46211ecdc10" + }, + "loadBalancingRules": [], + "inboundNatRules": [ + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/inboundNatRules/425eea91-5a9e-4777-b2c3-0442dfc20344" + } + ], + "outboundNatRules": [ + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/outboundNatRules/f3f3291d-b26c-44d3-8d55-99b644b70388" + } + ] + } + } + ], + "backendAddressPools": [ + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/backendAddressPools/db1fa644-bd00-4c05-b11b-f5f07bfed86b", + "resourceId": "db1fa644-bd00-4c05-b11b-f5f07bfed86b", + "etag": "W/\"72fdfa3d-34f4-4c90-ae94-d97ed73c9cf7\"", + "instanceId": "b638b320-5569-444f-9adf-78a683072269", + "properties": { + "provisioningState": "Succeeded", + "backendIPConfigurations": [ + { + "resourceRef": "/networkInterfaces/add9dac6-ddcc-4108-8543-e167c0a8d9dc/ipConfigurations/2e8a0316-66a6-4a3e-bd86-89b0e43b080f" + }, + { + "resourceRef": "/networkInterfaces/b3dc7295-7144-4f6e-8235-35d88b917482/ipConfigurations/581ab448-8e6f-436c-9dec-43366a9817dd" + } + ], + "outboundNatRules": [ + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/outboundNatRules/f3f3291d-b26c-44d3-8d55-99b644b70388" + } + ], + "loadBalancingRules": [] + } + } + ], + "probes": [ + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/probes/ddb4dab8-b1eb-4476-90ca-948697240317", + "resourceId": "ddb4dab8-b1eb-4476-90ca-948697240317", + "etag": "W/\"72fdfa3d-34f4-4c90-ae94-d97ed73c9cf7\"", + "instanceId": "18336b2f-8b2e-4bf2-a196-99009ec8feb8", + "properties": { + "provisioningState": "Succeeded", + "protocol": "Tcp", + "port": 55555, + "intervalInSeconds": 30, + "numberOfProbes": 1, + "loadBalancingRules": [] + } + } + ], + "inboundNatRules": [ + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/inboundNatRules/d076eae7-926a-457a-a60c-0a713a02977d", + "resourceId": "d076eae7-926a-457a-a60c-0a713a02977d", + "etag": "W/\"72fdfa3d-34f4-4c90-ae94-d97ed73c9cf7\"", + "instanceId": "4be2c156-cbcb-466d-a8fe-865bc9f0045d", + "properties": { + "provisioningState": "Succeeded", + "frontendIPConfigurations": [ + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/frontendIPConfigurations/9f37a479-7d60-489a-aab6-d7eb2200306f" + } + ], + "protocol": "Tcp", + "frontendPort": 2003, + "backendPort": 2003, + "enableFloatingIP": false, + "idleTimeoutInMinutes": 4, + "backendIPConfiguration": { + "resourceRef": "/networkInterfaces/b3dc7295-7144-4f6e-8235-35d88b917482/ipConfigurations/581ab448-8e6f-436c-9dec-43366a9817dd" + } + } + }, + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/inboundNatRules/425eea91-5a9e-4777-b2c3-0442dfc20344", + "resourceId": "425eea91-5a9e-4777-b2c3-0442dfc20344", + "etag": "W/\"72fdfa3d-34f4-4c90-ae94-d97ed73c9cf7\"", + "instanceId": "ae841775-a3b2-454e-bd69-b78a298ca7bf", + "properties": { + "provisioningState": "Succeeded", + "frontendIPConfigurations": [ + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/frontendIPConfigurations/ab5ccbe7-2ce9-4cdf-a0da-e4e5d81479d8" + } + ], + "protocol": "Tcp", + "frontendPort": 2003, + "backendPort": 2003, + "enableFloatingIP": false, + "idleTimeoutInMinutes": 4, + "backendIPConfiguration": { + "resourceRef": "/networkInterfaces/add9dac6-ddcc-4108-8543-e167c0a8d9dc/ipConfigurations/2e8a0316-66a6-4a3e-bd86-89b0e43b080f" + } + } + } + ], + "outboundNatRules": [ + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/outboundNatRules/f3f3291d-b26c-44d3-8d55-99b644b70388", + "resourceId": "f3f3291d-b26c-44d3-8d55-99b644b70388", + "etag": "W/\"72fdfa3d-34f4-4c90-ae94-d97ed73c9cf7\"", + "instanceId": "f5065c75-ab45-4e5b-bb76-fb69667bf5d6", + "properties": { + "provisioningState": "Succeeded", + "frontendIPConfigurations": [ + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/frontendIPConfigurations/9f37a479-7d60-489a-aab6-d7eb2200306f" + }, + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/frontendIPConfigurations/ab5ccbe7-2ce9-4cdf-a0da-e4e5d81479d8" + } + ], + "protocol": "All", + "backendAddressPool": { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/backendAddressPools/db1fa644-bd00-4c05-b11b-f5f07bfed86b" + } + } + } + ] + } + } + ], + "nextLink": "" +} diff --git a/hnv/tests/fake/response/load_balancing_rules.json b/hnv/tests/fake/response/load_balancing_rules.json new file mode 100644 index 0000000..780ec18 --- /dev/null +++ b/hnv/tests/fake/response/load_balancing_rules.json @@ -0,0 +1,74 @@ +{ + "value": [ + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa- e0fe1f42cce1/loadBalancingRules/6339de0b-5730-4057-b2ee-37e90d3e4470", + "resourceId": "6339de0b-5730-4057-b2ee-37e90d3e4470", + "etag": "W/\"87c5f43a-3d37-4955-b6ba-bc3037fcfefd\"", + "instanceId": "58b176c8-f4d1-4a5f-bfe4-623dcfe3ba2a", + "properties": { + "provisioningState": "Succeeded", + "frontendIPConfigurations": [ + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa- e0fe1f42cce1/frontendIPConfigurations/6bad6ea2-eca8-4143-8925-55aa497d3882" + } + ], + "protocol": "Tcp", + "frontendPort": 2003, + "backendPort": 2003, + "enableFloatingIP": false, + "idleTimeoutInMinutes": 4, + "backendAddressPool": { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa- e0fe1f42cce1/backendAddressPools/9827f986-4606-4331-b63f-7cc39665e2c9" + }, + "loadDistribution": "Default" + } + }, + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/loadBalancingRules/6339de0b-5730-4057-b2ee-37e90d3e4470", + "resourceId": "6339de0b-5730-4057-b2ee-37e90d3e4470", + "etag": "W/\"87c5f43a-3d37-4955-b6ba-bc3037fcfefd\"", + "instanceId": "58b176c8-f4d1-4a5f-bfe4-623dcfe3ba2a", + "properties": { + "provisioningState": "Succeeded", + "frontendIPConfigurations": [ + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/frontendIPConfigurations/6bad6ea2-eca8-4143-8925-55aa497d3882" + } + ], + "protocol": "Tcp", + "frontendPort": 2003, + "backendPort": 2003, + "enableFloatingIP": false, + "idleTimeoutInMinutes": 4, + "backendAddressPool": { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/backendAddressPools/9827f986-4606-4331-b63f-7cc39665e2c9" + }, + "loadDistribution": "Default" + } + }, + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/loadBalancingRules/6339de0b-5730-4057-b2ee-37e90d3e4470", + "resourceId": "6339de0b-5730-4057-b2ee-37e90d3e4470", + "etag": "W/\"87c5f43a-3d37-4955-b6ba-bc3037fcfefd\"", + "instanceId": "58b176c8-f4d1-4a5f-bfe4-623dcfe3ba2a", + "properties": { + "provisioningState": "Succeeded", + "frontendIPConfigurations": [ + { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/frontendIPConfigurations/6bad6ea2-eca8-4143-8925-55aa497d3882" + } + ], + "protocol": "Tcp", + "frontendPort": 2003, + "backendPort": 2003, + "enableFloatingIP": false, + "idleTimeoutInMinutes": 4, + "backendAddressPool": { + "resourceRef": "/loadBalancers/d2251a0d-32d2-457e-b3aa-e0fe1f42cce1/backendAddressPools/9827f986-4606-4331-b63f-7cc39665e2c9" + }, + "loadDistribution": "Default" + } + } + ], + "nextLink": "" +} diff --git a/hnv/tests/fake/response/outbound_nat_rules.json b/hnv/tests/fake/response/outbound_nat_rules.json new file mode 100644 index 0000000..3d60c96 --- /dev/null +++ b/hnv/tests/fake/response/outbound_nat_rules.json @@ -0,0 +1,47 @@ +{ + "value": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/outboundNatRules/49053c15-2d0f-45a2-8148-be8615282160", + "resourceId": "49053c15-2d0f-45a2-8148-be8615282160", + "etag": "W/\"fb318cf6-9102-4e34-a684-5e25aee8d3f4\"", + "instanceId": "c4000c95-7f90-4bb4-b68d-b2bc9c1dfc3e", + "properties": { + "provisioningState": "Succeeded", + "frontendIPConfigurations": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/frontendIPConfigurations/5187779d-c61c-44d2-87be-fa69ac2d9d57" + }, + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/frontendIPConfigurations/94c568d8-d839-431a-aed4-a5c178356018" + } + ], + "protocol": "All", + "backendAddressPool": { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/backendAddressPools/b32b5ef0-5332-49a8-b383-f91090135f71" + } + } + }, + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/outboundNatRules/49053c15-2d0f-45a2-8148-be8615282160", + "resourceId": "49053c15-2d0f-45a2-8148-be8615282160", + "etag": "W/\"fb318cf6-9102-4e34-a684-5e25aee8d3f4\"", + "instanceId": "c4000c95-7f90-4bb4-b68d-b2bc9c1dfc3e", + "properties": { + "provisioningState": "Succeeded", + "frontendIPConfigurations": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/frontendIPConfigurations/5187779d-c61c-44d2-87be-fa69ac2d9d57" + }, + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/frontendIPConfigurations/94c568d8-d839-431a-aed4-a5c178356018" + } + ], + "protocol": "All", + "backendAddressPool": { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/backendAddressPools/b32b5ef0-5332-49a8-b383-f91090135f71" + } + } + } + ], + "nextLink": "" +} \ No newline at end of file diff --git a/hnv/tests/fake/response/probes.json b/hnv/tests/fake/response/probes.json new file mode 100644 index 0000000..d782754 --- /dev/null +++ b/hnv/tests/fake/response/probes.json @@ -0,0 +1,37 @@ +{ + "value": [ + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/probes/9f940e29- 1d25-44fc-88d3-c81151a0344e", + "resourceId": "9f940e29-1d25-44fc-88d3-c81151a0344e", + "etag": "W/\"fb318cf6-9102-4e34-a684-5e25aee8d3f4\"", + "instanceId": "0da65588-247b-475b-bd1a-7ead0ba1a182", + "properties": { + "provisioningState": "Succeeded", + "protocol": "Tcp", + "port": 55555, + "intervalInSeconds": 30, + "numberOfProbes": 1, + "loadBalancingRules": [] + } + }, + { + "resourceRef": "/loadBalancers/0cac5f8a-9d5c-455a-a971-2682d597e098/probes/9f940e29-1d25- 44fc-88d3-c81151a0344e", + "resourceId": "9f940e29-1d25-44fc-88d3-c81151a0344e", + "etag": "W/\"fb318cf6-9102-4e34-a684-5e25aee8d3f4\"", + "instanceId": "0da65588-247b-475b-bd1a-7ead0ba1a182", + "properties": { + "provisioningState": "Succeeded", + "protocol": "Tcp", + "port": 55555, + "intervalInSeconds": 30, + "numberOfProbes": 1, + "loadBalancingRules": [ + { + "resourceRef": "/loadBalancers/ee396509-27d3-44f9-849c-f6ed28d59f66/loadBalancingRules/2ea746ea-968f-41f2-8bfa-71d2391ef752" + } + ] + } + } + ], + "nextLink": "" +} diff --git a/hnv/tests/test_client.py b/hnv/tests/test_client.py index d5fbfa0..9dc819d 100644 --- a/hnv/tests/test_client.py +++ b/hnv/tests/test_client.py @@ -153,7 +153,8 @@ class TestBaseHNVModel(unittest.TestCase): mock_dump.assert_called_once_with(include_read_only=False) update_resource.assert_called_once_with( - "test/hnv-client", data=mock.sentinel.request_body) + "test/hnv-client", data=mock.sentinel.request_body, + if_match=None) if request_wait: self.assertEqual(get_resource.call_count, loop_count + 1) @@ -325,3 +326,45 @@ class TestClient(unittest.TestCase): for raw_data in resources.get("value", []): self._test_get_resource(model=client.PublicIPAddresses, raw_data=raw_data) + + def test_backend_address_pools(self): + resources = self._response.backend_address_pools() + for raw_data in resources.get("value", []): + self._test_get_resource(model=client.BackendAddressPools, + raw_data=raw_data) + + def test_frontend_ip_configurations(self): + resources = self._response.frontend_ip_configurations() + for raw_data in resources.get("value", []): + self._test_get_resource(model=client.FrontendIPConfigurations, + raw_data=raw_data) + + def test_inbound_nat_rules(self): + resources = self._response.inbound_nat_rules() + for raw_data in resources.get("value", []): + self._test_get_resource(model=client.InboundNATRules, + raw_data=raw_data) + + def test_load_balancing_rules(self): + resources = self._response.load_balancing_rules() + for raw_data in resources.get("value", []): + self._test_get_resource(model=client.LoadBalancingRules, + raw_data=raw_data) + + def test_outbound_nat_rules(self): + resources = self._response.outbound_nat_rules() + for raw_data in resources.get("value", []): + self._test_get_resource(model=client.OutboundNATRules, + raw_data=raw_data) + + def test_probes(self): + resources = self._response.probes() + for raw_data in resources.get("value", []): + self._test_get_resource(model=client.Probes, + raw_data=raw_data) + + def test_load_balancers(self): + resources = self._response.load_balancers() + for raw_data in resources.get("value", []): + self._test_get_resource(model=client.LoadBalancers, + raw_data=raw_data)